summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src/routes/zen/util/provider/provider.ts
blob: 86446bfd853a61a14c28f94ad6d6bef2811280c7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import { ZenData } from "@opencode-ai/console-core/model.js"
import {
  fromAnthropicChunk,
  fromAnthropicRequest,
  fromAnthropicResponse,
  toAnthropicChunk,
  toAnthropicRequest,
  toAnthropicResponse,
} from "./anthropic"
import {
  fromOpenaiChunk,
  fromOpenaiRequest,
  fromOpenaiResponse,
  toOpenaiChunk,
  toOpenaiRequest,
  toOpenaiResponse,
} from "./openai"
import {
  fromOaCompatibleChunk,
  fromOaCompatibleRequest,
  fromOaCompatibleResponse,
  toOaCompatibleChunk,
  toOaCompatibleRequest,
  toOaCompatibleResponse,
} from "./openai-compatible"

export type UsageInfo = {
  inputTokens: number
  outputTokens: number
  reasoningTokens?: number
  cacheReadTokens?: number
  cacheWrite5mTokens?: number
  cacheWrite1hTokens?: number
}

export type ProviderHelper = (input: {
  reqModel: string
  providerModel: string
  adjustCacheUsage?: boolean
  workspaceID?: string
}) => {
  format: ZenData.Format
  modifyUrl: (providerApi: string, isStream?: boolean) => string
  modifyHeaders: (headers: Headers, body: Record<string, any>, apiKey: string) => void
  modifyBody: (body: Record<string, any>) => Record<string, any>
  createBinaryStreamDecoder: () => ((chunk: Uint8Array) => Uint8Array | undefined) | undefined
  streamSeparator: string
  createUsageParser: () => {
    parse: (chunk: string) => void
    retrieve: () => any
  }
  normalizeUsage: (usage: any) => UsageInfo
}

export interface CommonMessage {
  role: "system" | "user" | "assistant" | "tool"
  content?: string | Array<CommonContentPart>
  tool_call_id?: string
  tool_calls?: CommonToolCall[]
}

export interface CommonContentPart {
  type: "text" | "image_url"
  text?: string
  image_url?: { url: string }
}

export interface CommonToolCall {
  id: string
  type: "function"
  function: {
    name: string
    arguments: string
  }
}

export interface CommonTool {
  type: "function"
  function: {
    name: string
    description?: string
    parameters?: Record<string, any>
  }
}

export interface CommonUsage {
  input_tokens?: number
  output_tokens?: number
  total_tokens?: number
  prompt_tokens?: number
  completion_tokens?: number
  cache_read_input_tokens?: number
  cache_creation?: {
    ephemeral_5m_input_tokens?: number
    ephemeral_1h_input_tokens?: number
  }
  input_tokens_details?: {
    cached_tokens?: number
  }
  output_tokens_details?: {
    reasoning_tokens?: number
  }
}

export interface CommonRequest {
  model: string
  max_tokens?: number
  temperature?: number
  top_p?: number
  stop?: string | string[]
  messages: CommonMessage[]
  stream?: boolean
  tools?: CommonTool[]
  tool_choice?: "auto" | "required" | { type: "function"; function: { name: string } }
}

export interface CommonResponse {
  id: string
  object: "chat.completion"
  created: number
  model: string
  choices: Array<{
    index: number
    message: {
      role: "assistant"
      content?: string
      tool_calls?: CommonToolCall[]
    }
    finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
  }>
  usage?: {
    prompt_tokens?: number
    completion_tokens?: number
    total_tokens?: number
    prompt_tokens_details?: { cached_tokens?: number }
  }
}

export interface CommonChunk {
  id: string
  object: "chat.completion.chunk"
  created: number
  model: string
  choices: Array<{
    index: number
    delta: {
      role?: "assistant"
      content?: string
      tool_calls?: Array<{
        index: number
        id?: string
        type?: "function"
        function?: {
          name?: string
          arguments?: string
        }
      }>
    }
    finish_reason: "stop" | "tool_calls" | "length" | "content_filter" | null
  }>
  usage?: {
    prompt_tokens?: number
    completion_tokens?: number
    total_tokens?: number
    prompt_tokens_details?: { cached_tokens?: number }
  }
}

export function buildCostChunk(format: ZenData.Format, cost: string): string {
  switch (format) {
    case "anthropic":
      return `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`
    case "openai":
      return `event: ping\ndata: ${JSON.stringify({ type: "ping", cost })}\n\n`
    case "oa-compat":
      return `data: ${JSON.stringify({ choices: [], cost })}\n\n`
    default:
      return `data: ${JSON.stringify({ type: "ping", cost })}\n\n`
  }
}

export function createBodyConverter(from: ZenData.Format, to: ZenData.Format) {
  return (body: any): any => {
    if (from === to) return body

    let raw: CommonRequest
    if (from === "anthropic") raw = fromAnthropicRequest(body)
    else if (from === "openai") raw = fromOpenaiRequest(body)
    else raw = fromOaCompatibleRequest(body)

    if (to === "anthropic") return toAnthropicRequest(raw)
    if (to === "openai") return toOpenaiRequest(raw)
    if (to === "oa-compat") return toOaCompatibleRequest(raw)
  }
}

export function createStreamPartConverter(from: ZenData.Format, to: ZenData.Format) {
  return (part: any): any => {
    if (from === to) return part

    let raw: CommonChunk | string
    if (from === "anthropic") raw = fromAnthropicChunk(part)
    else if (from === "openai") raw = fromOpenaiChunk(part)
    else raw = fromOaCompatibleChunk(part)

    // If result is a string (error case), pass it through
    if (typeof raw === "string") return raw

    if (to === "anthropic") return toAnthropicChunk(raw)
    if (to === "openai") return toOpenaiChunk(raw)
    if (to === "oa-compat") return toOaCompatibleChunk(raw)
  }
}

export function createResponseConverter(from: ZenData.Format, to: ZenData.Format) {
  return (response: any): any => {
    if (from === to) return response

    let raw: CommonResponse
    if (from === "anthropic") raw = fromAnthropicResponse(response)
    else if (from === "openai") raw = fromOpenaiResponse(response)
    else raw = fromOaCompatibleResponse(response)

    if (to === "anthropic") return toAnthropicResponse(raw)
    if (to === "openai") return toOpenaiResponse(raw)
    if (to === "oa-compat") return toOaCompatibleResponse(raw)
  }
}