aisubs 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +141 -46
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +16 -0
- package/dist/compatibility.d.ts +14 -0
- package/dist/compatibility.js +1521 -0
- package/dist/dashboard/assets/index-CEDww1hA.css +2 -0
- package/dist/dashboard/assets/{index-V-AoOW2F.js → index-DrnM3oWy.js} +34 -33
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.d.ts +4 -2
- package/dist/dashboard.js +152 -86
- package/dist/http.d.ts +9 -6
- package/dist/http.js +254 -283
- package/dist/providers/chatgpt.js +2 -0
- package/dist/providers/claude.js +2 -0
- package/dist/providers/copilot.js +0 -2
- package/dist/realtime.d.ts +6 -0
- package/dist/realtime.js +163 -0
- package/examples/direct.mjs +31 -8
- package/examples/server.mjs +11 -1
- package/package.json +9 -1
- package/public/aisubs-dashboard.png +0 -0
- package/dist/dashboard/assets/index-DYe3pr2a.css +0 -2
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
import { isRecord, numberValue, stringValue } from "./utils.js";
|
|
2
|
+
export class CompatibilityError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, code = "invalid_request_error", status = 400) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function record(value, message) {
|
|
12
|
+
if (!isRecord(value))
|
|
13
|
+
throw new CompatibilityError(message);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function json(body) {
|
|
17
|
+
try {
|
|
18
|
+
return record(JSON.parse(body.toString("utf8")), "Request body must be a JSON object");
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof CompatibilityError)
|
|
22
|
+
throw error;
|
|
23
|
+
throw new CompatibilityError("Request body contains invalid JSON");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function requiredModel(raw) {
|
|
27
|
+
const model = stringValue(raw.model);
|
|
28
|
+
if (!model)
|
|
29
|
+
throw new CompatibilityError("A non-empty model is required");
|
|
30
|
+
return model;
|
|
31
|
+
}
|
|
32
|
+
function text(value) {
|
|
33
|
+
if (typeof value === "string")
|
|
34
|
+
return { type: "text", text: value };
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
return null;
|
|
37
|
+
const content = stringValue(value.text);
|
|
38
|
+
return content == null ? null : { type: "text", text: content };
|
|
39
|
+
}
|
|
40
|
+
function imageUrl(value) {
|
|
41
|
+
if (typeof value === "string")
|
|
42
|
+
return value;
|
|
43
|
+
return isRecord(value) ? stringValue(value.url) : undefined;
|
|
44
|
+
}
|
|
45
|
+
function openAiParts(value) {
|
|
46
|
+
if (typeof value === "string")
|
|
47
|
+
return [{ type: "text", text: value }];
|
|
48
|
+
if (value == null)
|
|
49
|
+
return [];
|
|
50
|
+
if (!Array.isArray(value))
|
|
51
|
+
throw new CompatibilityError("Message content must be text or parts");
|
|
52
|
+
return value.map((item) => {
|
|
53
|
+
const part = record(item, "Message content parts must be objects");
|
|
54
|
+
const type = stringValue(part.type);
|
|
55
|
+
if (["text", "input_text", "output_text"].includes(type ?? "")) {
|
|
56
|
+
const parsed = text(part);
|
|
57
|
+
if (parsed)
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
if (type === "image_url" || type === "input_image") {
|
|
61
|
+
const url = imageUrl(part.image_url) ?? stringValue(part.image_url) ?? stringValue(part.file_id);
|
|
62
|
+
if (!url)
|
|
63
|
+
throw new CompatibilityError("Image content requires image_url or file_id");
|
|
64
|
+
return { type: "image", url, detail: stringValue(part.detail) };
|
|
65
|
+
}
|
|
66
|
+
if (type === "input_audio") {
|
|
67
|
+
const audio = record(part.input_audio, "input_audio requires audio data");
|
|
68
|
+
const data = stringValue(audio.data);
|
|
69
|
+
if (!data)
|
|
70
|
+
throw new CompatibilityError("input_audio requires audio data");
|
|
71
|
+
return { type: "audio", data, format: stringValue(audio.format) };
|
|
72
|
+
}
|
|
73
|
+
if (type === "file" || type === "input_file") {
|
|
74
|
+
return {
|
|
75
|
+
type: "file",
|
|
76
|
+
fileId: stringValue(part.file_id),
|
|
77
|
+
data: stringValue(part.file_data),
|
|
78
|
+
filename: stringValue(part.filename),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (type === "refusal")
|
|
82
|
+
return { type: "text", text: stringValue(part.refusal) ?? "" };
|
|
83
|
+
throw new CompatibilityError(`Unsupported content part: ${type ?? "unknown"}`, "unsupported_feature");
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function chatToolCalls(value) {
|
|
87
|
+
if (!Array.isArray(value))
|
|
88
|
+
return undefined;
|
|
89
|
+
return value.map((item, index) => {
|
|
90
|
+
const call = record(item, "Tool calls must be objects");
|
|
91
|
+
const fn = record(call.function, "Tool call requires a function");
|
|
92
|
+
const name = stringValue(fn.name);
|
|
93
|
+
if (!name)
|
|
94
|
+
throw new CompatibilityError("Tool call requires a function name");
|
|
95
|
+
return {
|
|
96
|
+
id: stringValue(call.id) ?? `call_${index}_${crypto.randomUUID()}`,
|
|
97
|
+
name,
|
|
98
|
+
arguments: typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {}),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function chatTools(value) {
|
|
103
|
+
if (!Array.isArray(value))
|
|
104
|
+
return undefined;
|
|
105
|
+
return value.map((item) => {
|
|
106
|
+
const tool = record(item, "Tools must be objects");
|
|
107
|
+
if (tool.type !== "function") {
|
|
108
|
+
throw new CompatibilityError(`Unsupported Chat Completions tool: ${String(tool.type)}`);
|
|
109
|
+
}
|
|
110
|
+
const fn = record(tool.function, "Function tool is missing function");
|
|
111
|
+
const name = stringValue(fn.name);
|
|
112
|
+
if (!name)
|
|
113
|
+
throw new CompatibilityError("Function tool requires a name");
|
|
114
|
+
return {
|
|
115
|
+
name,
|
|
116
|
+
description: stringValue(fn.description),
|
|
117
|
+
parameters: fn.parameters,
|
|
118
|
+
strict: typeof fn.strict === "boolean" ? fn.strict : undefined,
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function parseChat(body) {
|
|
123
|
+
const raw = json(body);
|
|
124
|
+
if (!Array.isArray(raw.messages))
|
|
125
|
+
throw new CompatibilityError("messages must be an array");
|
|
126
|
+
const messages = raw.messages.map((item) => {
|
|
127
|
+
const message = record(item, "Messages must be objects");
|
|
128
|
+
const role = stringValue(message.role);
|
|
129
|
+
if (!role || !["system", "developer", "user", "assistant", "tool"].includes(role)) {
|
|
130
|
+
throw new CompatibilityError(`Unsupported message role: ${role ?? "unknown"}`);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
role: role,
|
|
134
|
+
content: openAiParts(message.content),
|
|
135
|
+
toolCallId: stringValue(message.tool_call_id),
|
|
136
|
+
toolCalls: chatToolCalls(message.tool_calls),
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
model: requiredModel(raw),
|
|
141
|
+
stream: raw.stream === true,
|
|
142
|
+
messages,
|
|
143
|
+
tools: chatTools(raw.tools),
|
|
144
|
+
toolChoice: raw.tool_choice,
|
|
145
|
+
maxTokens: numberValue(raw.max_completion_tokens) ?? numberValue(raw.max_tokens),
|
|
146
|
+
temperature: numberValue(raw.temperature),
|
|
147
|
+
topP: numberValue(raw.top_p),
|
|
148
|
+
stop: raw.stop,
|
|
149
|
+
reasoningEffort: stringValue(raw.reasoning_effort),
|
|
150
|
+
responseFormat: raw.response_format,
|
|
151
|
+
metadata: raw.metadata,
|
|
152
|
+
user: stringValue(raw.user),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function responseTools(value) {
|
|
156
|
+
if (!Array.isArray(value))
|
|
157
|
+
return undefined;
|
|
158
|
+
return value.map((item) => {
|
|
159
|
+
const tool = record(item, "Tools must be objects");
|
|
160
|
+
if (tool.type !== "function") {
|
|
161
|
+
throw new CompatibilityError(`${String(tool.type)} requires native Responses support`, "unsupported_feature");
|
|
162
|
+
}
|
|
163
|
+
const name = stringValue(tool.name);
|
|
164
|
+
if (!name)
|
|
165
|
+
throw new CompatibilityError("Function tool requires a name");
|
|
166
|
+
return {
|
|
167
|
+
name,
|
|
168
|
+
description: stringValue(tool.description),
|
|
169
|
+
parameters: tool.parameters,
|
|
170
|
+
strict: tool.strict === true,
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function parseResponses(body) {
|
|
175
|
+
const raw = json(body);
|
|
176
|
+
const messages = [];
|
|
177
|
+
if (typeof raw.instructions === "string") {
|
|
178
|
+
messages.push({ role: "developer", content: [{ type: "text", text: raw.instructions }] });
|
|
179
|
+
}
|
|
180
|
+
const input = raw.input;
|
|
181
|
+
if (typeof input === "string")
|
|
182
|
+
messages.push({ role: "user", content: [{ type: "text", text: input }] });
|
|
183
|
+
else if (Array.isArray(input)) {
|
|
184
|
+
for (const item of input) {
|
|
185
|
+
const value = record(item, "Responses input items must be objects");
|
|
186
|
+
if (value.type === "function_call") {
|
|
187
|
+
messages.push({
|
|
188
|
+
role: "assistant",
|
|
189
|
+
content: [],
|
|
190
|
+
toolCalls: [
|
|
191
|
+
{
|
|
192
|
+
id: stringValue(value.call_id) ??
|
|
193
|
+
stringValue(value.id) ??
|
|
194
|
+
`call_${crypto.randomUUID()}`,
|
|
195
|
+
name: stringValue(value.name) ?? "function",
|
|
196
|
+
arguments: typeof value.arguments === "string"
|
|
197
|
+
? value.arguments
|
|
198
|
+
: JSON.stringify(value.arguments ?? {}),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
else if (value.type === "function_call_output") {
|
|
204
|
+
messages.push({
|
|
205
|
+
role: "tool",
|
|
206
|
+
toolCallId: stringValue(value.call_id),
|
|
207
|
+
content: [
|
|
208
|
+
{
|
|
209
|
+
type: "text",
|
|
210
|
+
text: typeof value.output === "string"
|
|
211
|
+
? value.output
|
|
212
|
+
: JSON.stringify(value.output ?? ""),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (value.type === "item_reference") {
|
|
218
|
+
throw new CompatibilityError("item_reference requires native Responses support", "unsupported_feature");
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
const role = stringValue(value.role) ?? "user";
|
|
222
|
+
if (!["system", "developer", "user", "assistant"].includes(role)) {
|
|
223
|
+
throw new CompatibilityError(`Unsupported Responses role: ${role}`);
|
|
224
|
+
}
|
|
225
|
+
messages.push({ role: role, content: openAiParts(value.content) });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (input != null)
|
|
230
|
+
throw new CompatibilityError("Responses input must be text or an array");
|
|
231
|
+
const responseFormat = isRecord(raw.text) ? raw.text.format : undefined;
|
|
232
|
+
const reasoning = isRecord(raw.reasoning) ? raw.reasoning : undefined;
|
|
233
|
+
return {
|
|
234
|
+
model: requiredModel(raw),
|
|
235
|
+
stream: raw.stream === true,
|
|
236
|
+
messages,
|
|
237
|
+
tools: responseTools(raw.tools),
|
|
238
|
+
toolChoice: raw.tool_choice,
|
|
239
|
+
maxTokens: numberValue(raw.max_output_tokens),
|
|
240
|
+
temperature: numberValue(raw.temperature),
|
|
241
|
+
topP: numberValue(raw.top_p),
|
|
242
|
+
reasoningEffort: stringValue(reasoning?.effort),
|
|
243
|
+
responseFormat,
|
|
244
|
+
metadata: raw.metadata,
|
|
245
|
+
user: stringValue(raw.user),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function anthropicParts(value) {
|
|
249
|
+
if (typeof value === "string")
|
|
250
|
+
return { content: [{ type: "text", text: value }] };
|
|
251
|
+
if (!Array.isArray(value))
|
|
252
|
+
return { content: [] };
|
|
253
|
+
const content = [];
|
|
254
|
+
const toolCalls = [];
|
|
255
|
+
for (const item of value) {
|
|
256
|
+
const part = record(item, "Anthropic content blocks must be objects");
|
|
257
|
+
if (part.type === "text")
|
|
258
|
+
content.push({ type: "text", text: stringValue(part.text) ?? "" });
|
|
259
|
+
else if (part.type === "image") {
|
|
260
|
+
const source = record(part.source, "Anthropic image requires source");
|
|
261
|
+
if (source.type === "url")
|
|
262
|
+
content.push({ type: "image", url: stringValue(source.url) ?? "" });
|
|
263
|
+
else
|
|
264
|
+
content.push({
|
|
265
|
+
type: "image",
|
|
266
|
+
url: `data:${stringValue(source.media_type) ?? "image/png"};base64,${stringValue(source.data) ?? ""}`,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else if (part.type === "tool_use") {
|
|
270
|
+
toolCalls.push({
|
|
271
|
+
id: stringValue(part.id) ?? `call_${crypto.randomUUID()}`,
|
|
272
|
+
name: stringValue(part.name) ?? "function",
|
|
273
|
+
arguments: JSON.stringify(part.input ?? {}),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
else if (part.type !== "thinking" && part.type !== "redacted_thinking") {
|
|
277
|
+
throw new CompatibilityError(`Unsupported Anthropic content block: ${String(part.type)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return { content, toolCalls: toolCalls.length ? toolCalls : undefined };
|
|
281
|
+
}
|
|
282
|
+
function parseAnthropic(body) {
|
|
283
|
+
const raw = json(body);
|
|
284
|
+
const messages = [];
|
|
285
|
+
if (raw.system != null) {
|
|
286
|
+
const system = anthropicParts(raw.system);
|
|
287
|
+
messages.push({ role: "system", content: system.content });
|
|
288
|
+
}
|
|
289
|
+
if (!Array.isArray(raw.messages))
|
|
290
|
+
throw new CompatibilityError("messages must be an array");
|
|
291
|
+
for (const item of raw.messages) {
|
|
292
|
+
const value = record(item, "Messages must be objects");
|
|
293
|
+
const role = value.role === "assistant" ? "assistant" : "user";
|
|
294
|
+
if (Array.isArray(value.content)) {
|
|
295
|
+
const normal = [];
|
|
296
|
+
for (const part of value.content) {
|
|
297
|
+
if (isRecord(part) && part.type === "tool_result") {
|
|
298
|
+
messages.push({
|
|
299
|
+
role: "tool",
|
|
300
|
+
toolCallId: stringValue(part.tool_use_id),
|
|
301
|
+
content: anthropicParts(part.content).content,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
else
|
|
305
|
+
normal.push(part);
|
|
306
|
+
}
|
|
307
|
+
const parsed = anthropicParts(normal);
|
|
308
|
+
if (parsed.content.length || parsed.toolCalls?.length)
|
|
309
|
+
messages.push({ role, ...parsed });
|
|
310
|
+
}
|
|
311
|
+
else
|
|
312
|
+
messages.push({ role, ...anthropicParts(value.content) });
|
|
313
|
+
}
|
|
314
|
+
const tools = Array.isArray(raw.tools)
|
|
315
|
+
? raw.tools.map((item) => {
|
|
316
|
+
const tool = record(item, "Tools must be objects");
|
|
317
|
+
const name = stringValue(tool.name);
|
|
318
|
+
if (!name)
|
|
319
|
+
throw new CompatibilityError("Tool requires a name");
|
|
320
|
+
return { name, description: stringValue(tool.description), parameters: tool.input_schema };
|
|
321
|
+
})
|
|
322
|
+
: undefined;
|
|
323
|
+
return {
|
|
324
|
+
model: requiredModel(raw),
|
|
325
|
+
stream: raw.stream === true,
|
|
326
|
+
messages,
|
|
327
|
+
tools,
|
|
328
|
+
toolChoice: raw.tool_choice,
|
|
329
|
+
maxTokens: numberValue(raw.max_tokens),
|
|
330
|
+
temperature: numberValue(raw.temperature),
|
|
331
|
+
topP: numberValue(raw.top_p),
|
|
332
|
+
stop: raw.stop_sequences,
|
|
333
|
+
metadata: raw.metadata,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function parseGoogle(body, model, stream = false) {
|
|
337
|
+
const raw = json(body);
|
|
338
|
+
const messages = [];
|
|
339
|
+
if (isRecord(raw.systemInstruction)) {
|
|
340
|
+
const parts = Array.isArray(raw.systemInstruction.parts) ? raw.systemInstruction.parts : [];
|
|
341
|
+
messages.push({
|
|
342
|
+
role: "system",
|
|
343
|
+
content: parts.flatMap((part) => {
|
|
344
|
+
const parsed = text(part);
|
|
345
|
+
return parsed ? [parsed] : [];
|
|
346
|
+
}),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(raw.contents))
|
|
350
|
+
throw new CompatibilityError("contents must be an array");
|
|
351
|
+
for (const item of raw.contents) {
|
|
352
|
+
const value = record(item, "Google contents must be objects");
|
|
353
|
+
const role = value.role === "model" ? "assistant" : "user";
|
|
354
|
+
const content = [];
|
|
355
|
+
const toolCalls = [];
|
|
356
|
+
for (const itemPart of Array.isArray(value.parts) ? value.parts : []) {
|
|
357
|
+
const part = record(itemPart, "Google parts must be objects");
|
|
358
|
+
const parsedText = text(part);
|
|
359
|
+
if (parsedText)
|
|
360
|
+
content.push(parsedText);
|
|
361
|
+
else if (isRecord(part.inlineData)) {
|
|
362
|
+
content.push({
|
|
363
|
+
type: "image",
|
|
364
|
+
url: `data:${stringValue(part.inlineData.mimeType) ?? "image/png"};base64,${stringValue(part.inlineData.data) ?? ""}`,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
else if (isRecord(part.fileData)) {
|
|
368
|
+
content.push({ type: "image", url: stringValue(part.fileData.fileUri) ?? "" });
|
|
369
|
+
}
|
|
370
|
+
else if (isRecord(part.functionCall)) {
|
|
371
|
+
toolCalls.push({
|
|
372
|
+
id: `call_${crypto.randomUUID()}`,
|
|
373
|
+
name: stringValue(part.functionCall.name) ?? "function",
|
|
374
|
+
arguments: JSON.stringify(part.functionCall.args ?? {}),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
else if (isRecord(part.functionResponse)) {
|
|
378
|
+
messages.push({
|
|
379
|
+
role: "tool",
|
|
380
|
+
toolCallId: stringValue(part.functionResponse.name),
|
|
381
|
+
content: [{ type: "text", text: JSON.stringify(part.functionResponse.response ?? {}) }],
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (content.length || toolCalls.length)
|
|
386
|
+
messages.push({ role, content, toolCalls: toolCalls.length ? toolCalls : undefined });
|
|
387
|
+
}
|
|
388
|
+
const generation = isRecord(raw.generationConfig) ? raw.generationConfig : {};
|
|
389
|
+
const declarations = Array.isArray(raw.tools)
|
|
390
|
+
? raw.tools.flatMap((item) => isRecord(item) && Array.isArray(item.functionDeclarations) ? item.functionDeclarations : [])
|
|
391
|
+
: [];
|
|
392
|
+
return {
|
|
393
|
+
model,
|
|
394
|
+
stream,
|
|
395
|
+
messages,
|
|
396
|
+
tools: declarations.map((item) => {
|
|
397
|
+
const tool = record(item, "Function declarations must be objects");
|
|
398
|
+
return {
|
|
399
|
+
name: stringValue(tool.name) ?? "function",
|
|
400
|
+
description: stringValue(tool.description),
|
|
401
|
+
parameters: tool.parameters,
|
|
402
|
+
};
|
|
403
|
+
}),
|
|
404
|
+
maxTokens: numberValue(generation.maxOutputTokens),
|
|
405
|
+
temperature: numberValue(generation.temperature),
|
|
406
|
+
topP: numberValue(generation.topP),
|
|
407
|
+
stop: generation.stopSequences,
|
|
408
|
+
responseFormat: generation.responseSchema
|
|
409
|
+
? {
|
|
410
|
+
type: "json_schema",
|
|
411
|
+
json_schema: { name: "response", schema: generation.responseSchema },
|
|
412
|
+
}
|
|
413
|
+
: undefined,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function dataUri(url) {
|
|
417
|
+
const match = url.match(/^data:([^;,]+);base64,(.+)$/s);
|
|
418
|
+
return match?.[1] && match[2] ? { mediaType: match[1], data: match[2] } : null;
|
|
419
|
+
}
|
|
420
|
+
function chatContent(parts) {
|
|
421
|
+
if (parts.every((part) => part.type === "text"))
|
|
422
|
+
return parts.map((part) => part.text).join("");
|
|
423
|
+
return parts.map((part) => {
|
|
424
|
+
if (part.type === "text")
|
|
425
|
+
return { type: "text", text: part.text };
|
|
426
|
+
if (part.type === "image")
|
|
427
|
+
return {
|
|
428
|
+
type: "image_url",
|
|
429
|
+
image_url: { url: part.url, ...(part.detail ? { detail: part.detail } : {}) },
|
|
430
|
+
};
|
|
431
|
+
if (part.type === "audio")
|
|
432
|
+
return {
|
|
433
|
+
type: "input_audio",
|
|
434
|
+
input_audio: { data: part.data, format: part.format ?? "wav" },
|
|
435
|
+
};
|
|
436
|
+
return {
|
|
437
|
+
type: "file",
|
|
438
|
+
...(part.fileId ? { file_id: part.fileId } : {}),
|
|
439
|
+
...(part.data ? { file_data: part.data } : {}),
|
|
440
|
+
...(part.filename ? { filename: part.filename } : {}),
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function toChat(request) {
|
|
445
|
+
const messages = request.messages.map((message) => ({
|
|
446
|
+
role: message.role,
|
|
447
|
+
content: chatContent(message.content),
|
|
448
|
+
...(message.toolCallId ? { tool_call_id: message.toolCallId } : {}),
|
|
449
|
+
...(message.toolCalls
|
|
450
|
+
? {
|
|
451
|
+
tool_calls: message.toolCalls.map((call) => ({
|
|
452
|
+
id: call.id,
|
|
453
|
+
type: "function",
|
|
454
|
+
function: { name: call.name, arguments: call.arguments },
|
|
455
|
+
})),
|
|
456
|
+
}
|
|
457
|
+
: {}),
|
|
458
|
+
}));
|
|
459
|
+
return {
|
|
460
|
+
model: request.model,
|
|
461
|
+
messages,
|
|
462
|
+
stream: false,
|
|
463
|
+
...(request.tools
|
|
464
|
+
? { tools: request.tools.map((tool) => ({ type: "function", function: tool })) }
|
|
465
|
+
: {}),
|
|
466
|
+
...(request.toolChoice != null ? { tool_choice: request.toolChoice } : {}),
|
|
467
|
+
...(request.maxTokens != null ? { max_completion_tokens: request.maxTokens } : {}),
|
|
468
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
469
|
+
...(request.topP != null ? { top_p: request.topP } : {}),
|
|
470
|
+
...(request.stop != null ? { stop: request.stop } : {}),
|
|
471
|
+
...(request.reasoningEffort ? { reasoning_effort: request.reasoningEffort } : {}),
|
|
472
|
+
...(request.responseFormat != null ? { response_format: request.responseFormat } : {}),
|
|
473
|
+
...(request.metadata != null ? { metadata: request.metadata } : {}),
|
|
474
|
+
...(request.user ? { user: request.user } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function responseContent(part, role) {
|
|
478
|
+
if (part.type === "text")
|
|
479
|
+
return { type: role === "assistant" ? "output_text" : "input_text", text: part.text };
|
|
480
|
+
if (part.type === "image")
|
|
481
|
+
return {
|
|
482
|
+
type: "input_image",
|
|
483
|
+
image_url: part.url,
|
|
484
|
+
...(part.detail ? { detail: part.detail } : {}),
|
|
485
|
+
};
|
|
486
|
+
if (part.type === "audio")
|
|
487
|
+
return { type: "input_audio", input_audio: { data: part.data, format: part.format ?? "wav" } };
|
|
488
|
+
return {
|
|
489
|
+
type: "input_file",
|
|
490
|
+
...(part.fileId ? { file_id: part.fileId } : {}),
|
|
491
|
+
...(part.data ? { file_data: part.data } : {}),
|
|
492
|
+
...(part.filename ? { filename: part.filename } : {}),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function toResponses(request) {
|
|
496
|
+
const input = [];
|
|
497
|
+
const instructions = request.messages
|
|
498
|
+
.filter((message) => message.role === "system" || message.role === "developer")
|
|
499
|
+
.map((message) => message.content
|
|
500
|
+
.map((part) => {
|
|
501
|
+
if (part.type !== "text") {
|
|
502
|
+
throw new CompatibilityError("Responses instructions only support text content");
|
|
503
|
+
}
|
|
504
|
+
return part.text;
|
|
505
|
+
})
|
|
506
|
+
.join(""))
|
|
507
|
+
.filter(Boolean)
|
|
508
|
+
.join("\n\n");
|
|
509
|
+
for (const message of request.messages) {
|
|
510
|
+
if (message.role === "system" || message.role === "developer")
|
|
511
|
+
continue;
|
|
512
|
+
if (message.role === "tool") {
|
|
513
|
+
input.push({
|
|
514
|
+
type: "function_call_output",
|
|
515
|
+
call_id: message.toolCallId,
|
|
516
|
+
output: message.content
|
|
517
|
+
.map((part) => (part.type === "text" ? part.text : JSON.stringify(part)))
|
|
518
|
+
.join(""),
|
|
519
|
+
});
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (message.content.length)
|
|
523
|
+
input.push({
|
|
524
|
+
type: "message",
|
|
525
|
+
role: message.role,
|
|
526
|
+
content: message.content.map((part) => responseContent(part, message.role)),
|
|
527
|
+
});
|
|
528
|
+
for (const call of message.toolCalls ?? [])
|
|
529
|
+
input.push({
|
|
530
|
+
type: "function_call",
|
|
531
|
+
call_id: call.id,
|
|
532
|
+
name: call.name,
|
|
533
|
+
arguments: call.arguments,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
let toolChoice = request.toolChoice;
|
|
537
|
+
if (isRecord(toolChoice) && toolChoice.type === "function" && isRecord(toolChoice.function)) {
|
|
538
|
+
toolChoice = { type: "function", name: toolChoice.function.name };
|
|
539
|
+
}
|
|
540
|
+
let format = request.responseFormat;
|
|
541
|
+
if (isRecord(format) && format.type === "json_schema" && isRecord(format.json_schema))
|
|
542
|
+
format = { type: "json_schema", ...format.json_schema };
|
|
543
|
+
return {
|
|
544
|
+
model: request.model,
|
|
545
|
+
input,
|
|
546
|
+
...(instructions ? { instructions } : {}),
|
|
547
|
+
stream: false,
|
|
548
|
+
store: false,
|
|
549
|
+
...(request.tools
|
|
550
|
+
? { tools: request.tools.map((tool) => ({ type: "function", ...tool })) }
|
|
551
|
+
: {}),
|
|
552
|
+
...(toolChoice != null ? { tool_choice: toolChoice } : {}),
|
|
553
|
+
...(request.maxTokens != null ? { max_output_tokens: request.maxTokens } : {}),
|
|
554
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
555
|
+
...(request.topP != null ? { top_p: request.topP } : {}),
|
|
556
|
+
...(request.reasoningEffort ? { reasoning: { effort: request.reasoningEffort } } : {}),
|
|
557
|
+
...(format != null ? { text: { format } } : {}),
|
|
558
|
+
...(request.metadata != null ? { metadata: request.metadata } : {}),
|
|
559
|
+
...(request.user ? { user: request.user } : {}),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function anthropicContent(message) {
|
|
563
|
+
const parts = message.content.map((part) => {
|
|
564
|
+
if (part.type === "text")
|
|
565
|
+
return { type: "text", text: part.text };
|
|
566
|
+
if (part.type === "image") {
|
|
567
|
+
const data = dataUri(part.url);
|
|
568
|
+
return data
|
|
569
|
+
? { type: "image", source: { type: "base64", media_type: data.mediaType, data: data.data } }
|
|
570
|
+
: { type: "image", source: { type: "url", url: part.url } };
|
|
571
|
+
}
|
|
572
|
+
if (part.type === "file")
|
|
573
|
+
return {
|
|
574
|
+
type: "document",
|
|
575
|
+
source: part.fileId
|
|
576
|
+
? { type: "file", file_id: part.fileId }
|
|
577
|
+
: { type: "base64", media_type: "application/octet-stream", data: part.data ?? "" },
|
|
578
|
+
...(part.filename ? { title: part.filename } : {}),
|
|
579
|
+
};
|
|
580
|
+
throw new CompatibilityError("Anthropic Messages does not support OpenAI input_audio", "unsupported_feature");
|
|
581
|
+
});
|
|
582
|
+
for (const call of message.toolCalls ?? [])
|
|
583
|
+
parts.push({
|
|
584
|
+
type: "tool_use",
|
|
585
|
+
id: call.id,
|
|
586
|
+
name: call.name,
|
|
587
|
+
input: JSON.parse(call.arguments || "{}"),
|
|
588
|
+
});
|
|
589
|
+
return parts;
|
|
590
|
+
}
|
|
591
|
+
function toAnthropic(request) {
|
|
592
|
+
const system = request.messages
|
|
593
|
+
.filter((message) => message.role === "system" || message.role === "developer")
|
|
594
|
+
.flatMap(anthropicContent);
|
|
595
|
+
const messages = request.messages
|
|
596
|
+
.filter((message) => message.role !== "system" && message.role !== "developer")
|
|
597
|
+
.map((message) => message.role === "tool"
|
|
598
|
+
? {
|
|
599
|
+
role: "user",
|
|
600
|
+
content: [
|
|
601
|
+
{
|
|
602
|
+
type: "tool_result",
|
|
603
|
+
tool_use_id: message.toolCallId,
|
|
604
|
+
content: anthropicContent(message),
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
}
|
|
608
|
+
: { role: message.role, content: anthropicContent(message) });
|
|
609
|
+
let toolChoice = request.toolChoice;
|
|
610
|
+
if (toolChoice === "auto" || toolChoice === "none" || toolChoice === "required")
|
|
611
|
+
toolChoice = toolChoice === "required" ? { type: "any" } : { type: toolChoice };
|
|
612
|
+
else if (isRecord(toolChoice) && isRecord(toolChoice.function))
|
|
613
|
+
toolChoice = { type: "tool", name: toolChoice.function.name };
|
|
614
|
+
return {
|
|
615
|
+
model: request.model,
|
|
616
|
+
messages,
|
|
617
|
+
max_tokens: request.maxTokens ?? 4096,
|
|
618
|
+
stream: false,
|
|
619
|
+
...(system.length ? { system } : {}),
|
|
620
|
+
...(request.tools
|
|
621
|
+
? {
|
|
622
|
+
tools: request.tools.map((tool) => ({
|
|
623
|
+
name: tool.name,
|
|
624
|
+
description: tool.description,
|
|
625
|
+
input_schema: tool.parameters ?? { type: "object", properties: {} },
|
|
626
|
+
})),
|
|
627
|
+
}
|
|
628
|
+
: {}),
|
|
629
|
+
...(toolChoice != null ? { tool_choice: toolChoice } : {}),
|
|
630
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
631
|
+
...(request.topP != null ? { top_p: request.topP } : {}),
|
|
632
|
+
...(request.stop != null
|
|
633
|
+
? { stop_sequences: Array.isArray(request.stop) ? request.stop : [request.stop] }
|
|
634
|
+
: {}),
|
|
635
|
+
...(request.reasoningEffort
|
|
636
|
+
? { thinking: { type: "adaptive" }, output_config: { effort: request.reasoningEffort } }
|
|
637
|
+
: {}),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function googlePart(part) {
|
|
641
|
+
if (part.type === "text")
|
|
642
|
+
return { text: part.text };
|
|
643
|
+
if (part.type === "image") {
|
|
644
|
+
const data = dataUri(part.url);
|
|
645
|
+
return data
|
|
646
|
+
? { inlineData: { mimeType: data.mediaType, data: data.data } }
|
|
647
|
+
: { fileData: { fileUri: part.url } };
|
|
648
|
+
}
|
|
649
|
+
if (part.type === "audio")
|
|
650
|
+
return { inlineData: { mimeType: `audio/${part.format ?? "wav"}`, data: part.data } };
|
|
651
|
+
return part.fileId
|
|
652
|
+
? { fileData: { fileUri: part.fileId } }
|
|
653
|
+
: { inlineData: { mimeType: "application/octet-stream", data: part.data ?? "" } };
|
|
654
|
+
}
|
|
655
|
+
function toGoogle(request) {
|
|
656
|
+
const system = request.messages
|
|
657
|
+
.filter((message) => message.role === "system" || message.role === "developer")
|
|
658
|
+
.flatMap((message) => message.content.map(googlePart));
|
|
659
|
+
const contents = request.messages
|
|
660
|
+
.filter((message) => message.role !== "system" && message.role !== "developer")
|
|
661
|
+
.map((message) => {
|
|
662
|
+
const parts = message.role === "tool" ? [] : message.content.map(googlePart);
|
|
663
|
+
for (const call of message.toolCalls ?? [])
|
|
664
|
+
parts.push({ functionCall: { name: call.name, args: JSON.parse(call.arguments || "{}") } });
|
|
665
|
+
if (message.role === "tool")
|
|
666
|
+
parts.push({
|
|
667
|
+
functionResponse: {
|
|
668
|
+
name: message.toolCallId ?? "function",
|
|
669
|
+
response: {
|
|
670
|
+
result: message.content.map((part) => (part.type === "text" ? part.text : part)),
|
|
671
|
+
},
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
return { role: message.role === "assistant" ? "model" : "user", parts };
|
|
675
|
+
});
|
|
676
|
+
const schema = isRecord(request.responseFormat) && isRecord(request.responseFormat.json_schema)
|
|
677
|
+
? request.responseFormat.json_schema.schema
|
|
678
|
+
: undefined;
|
|
679
|
+
return {
|
|
680
|
+
contents,
|
|
681
|
+
...(system.length ? { systemInstruction: { parts: system } } : {}),
|
|
682
|
+
...(request.tools
|
|
683
|
+
? {
|
|
684
|
+
tools: [
|
|
685
|
+
{
|
|
686
|
+
functionDeclarations: request.tools.map((tool) => ({
|
|
687
|
+
name: tool.name,
|
|
688
|
+
description: tool.description,
|
|
689
|
+
parameters: tool.parameters,
|
|
690
|
+
})),
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
}
|
|
694
|
+
: {}),
|
|
695
|
+
generationConfig: {
|
|
696
|
+
...(request.maxTokens != null ? { maxOutputTokens: request.maxTokens } : {}),
|
|
697
|
+
...(request.temperature != null ? { temperature: request.temperature } : {}),
|
|
698
|
+
...(request.topP != null ? { topP: request.topP } : {}),
|
|
699
|
+
...(request.stop != null
|
|
700
|
+
? { stopSequences: Array.isArray(request.stop) ? request.stop : [request.stop] }
|
|
701
|
+
: {}),
|
|
702
|
+
...(schema ? { responseMimeType: "application/json", responseSchema: schema } : {}),
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
function usage(input, output, cached, reasoning) {
|
|
707
|
+
if (input == null && output == null)
|
|
708
|
+
return undefined;
|
|
709
|
+
return {
|
|
710
|
+
input: input ?? 0,
|
|
711
|
+
output: output ?? 0,
|
|
712
|
+
total: (input ?? 0) + (output ?? 0),
|
|
713
|
+
...(cached != null ? { cached } : {}),
|
|
714
|
+
...(reasoning != null ? { reasoning } : {}),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function parseChatResult(raw, model) {
|
|
718
|
+
const choice = Array.isArray(raw.choices) && isRecord(raw.choices[0]) ? raw.choices[0] : {};
|
|
719
|
+
const message = isRecord(choice.message) ? choice.message : {};
|
|
720
|
+
const details = isRecord(raw.usage) ? raw.usage : undefined;
|
|
721
|
+
const promptDetails = isRecord(details?.prompt_tokens_details)
|
|
722
|
+
? details.prompt_tokens_details
|
|
723
|
+
: undefined;
|
|
724
|
+
const completionDetails = isRecord(details?.completion_tokens_details)
|
|
725
|
+
? details.completion_tokens_details
|
|
726
|
+
: undefined;
|
|
727
|
+
const finish = stringValue(choice.finish_reason);
|
|
728
|
+
return {
|
|
729
|
+
id: stringValue(raw.id) ?? `chatcmpl_${crypto.randomUUID()}`,
|
|
730
|
+
model: stringValue(raw.model) ?? model,
|
|
731
|
+
created: numberValue(raw.created) ?? Math.floor(Date.now() / 1000),
|
|
732
|
+
content: openAiParts(message.content),
|
|
733
|
+
toolCalls: chatToolCalls(message.tool_calls) ?? [],
|
|
734
|
+
refusal: stringValue(message.refusal),
|
|
735
|
+
finishReason: finish === "length" || finish === "tool_calls" || finish === "content_filter"
|
|
736
|
+
? finish
|
|
737
|
+
: "stop",
|
|
738
|
+
usage: usage(numberValue(details?.prompt_tokens), numberValue(details?.completion_tokens), numberValue(promptDetails?.cached_tokens), numberValue(completionDetails?.reasoning_tokens)),
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function parseResponsesResult(raw, model) {
|
|
742
|
+
const content = [];
|
|
743
|
+
const toolCalls = [];
|
|
744
|
+
let refusal;
|
|
745
|
+
for (const item of Array.isArray(raw.output) ? raw.output : []) {
|
|
746
|
+
if (!isRecord(item))
|
|
747
|
+
continue;
|
|
748
|
+
if (item.type === "function_call")
|
|
749
|
+
toolCalls.push({
|
|
750
|
+
id: stringValue(item.call_id) ?? stringValue(item.id) ?? `call_${crypto.randomUUID()}`,
|
|
751
|
+
name: stringValue(item.name) ?? "function",
|
|
752
|
+
arguments: typeof item.arguments === "string"
|
|
753
|
+
? item.arguments
|
|
754
|
+
: JSON.stringify(item.arguments ?? {}),
|
|
755
|
+
});
|
|
756
|
+
for (const part of Array.isArray(item.content) ? item.content : []) {
|
|
757
|
+
if (!isRecord(part))
|
|
758
|
+
continue;
|
|
759
|
+
if (part.type === "output_text")
|
|
760
|
+
content.push({ type: "text", text: stringValue(part.text) ?? "" });
|
|
761
|
+
if (part.type === "refusal")
|
|
762
|
+
refusal = stringValue(part.refusal);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (!content.length && typeof raw.output_text === "string")
|
|
766
|
+
content.push({ type: "text", text: raw.output_text });
|
|
767
|
+
const details = isRecord(raw.usage) ? raw.usage : undefined;
|
|
768
|
+
const inputDetails = isRecord(details?.input_tokens_details)
|
|
769
|
+
? details.input_tokens_details
|
|
770
|
+
: undefined;
|
|
771
|
+
const outputDetails = isRecord(details?.output_tokens_details)
|
|
772
|
+
? details.output_tokens_details
|
|
773
|
+
: undefined;
|
|
774
|
+
return {
|
|
775
|
+
id: stringValue(raw.id) ?? `resp_${crypto.randomUUID()}`,
|
|
776
|
+
model: stringValue(raw.model) ?? model,
|
|
777
|
+
created: numberValue(raw.created_at) ?? Math.floor(Date.now() / 1000),
|
|
778
|
+
content,
|
|
779
|
+
toolCalls,
|
|
780
|
+
refusal,
|
|
781
|
+
finishReason: toolCalls.length
|
|
782
|
+
? "tool_calls"
|
|
783
|
+
: raw.status === "incomplete"
|
|
784
|
+
? "length"
|
|
785
|
+
: raw.status === "failed"
|
|
786
|
+
? "error"
|
|
787
|
+
: "stop",
|
|
788
|
+
usage: usage(numberValue(details?.input_tokens), numberValue(details?.output_tokens), numberValue(inputDetails?.cached_tokens), numberValue(outputDetails?.reasoning_tokens)),
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function parseAnthropicResult(raw, model) {
|
|
792
|
+
const parsed = anthropicParts(raw.content);
|
|
793
|
+
const rawUsage = isRecord(raw.usage) ? raw.usage : undefined;
|
|
794
|
+
const stop = stringValue(raw.stop_reason);
|
|
795
|
+
return {
|
|
796
|
+
id: stringValue(raw.id) ?? `msg_${crypto.randomUUID()}`,
|
|
797
|
+
model: stringValue(raw.model) ?? model,
|
|
798
|
+
created: Math.floor(Date.now() / 1000),
|
|
799
|
+
content: parsed.content,
|
|
800
|
+
toolCalls: parsed.toolCalls ?? [],
|
|
801
|
+
finishReason: stop === "max_tokens"
|
|
802
|
+
? "length"
|
|
803
|
+
: stop === "tool_use"
|
|
804
|
+
? "tool_calls"
|
|
805
|
+
: stop === "refusal"
|
|
806
|
+
? "content_filter"
|
|
807
|
+
: "stop",
|
|
808
|
+
usage: usage(numberValue(rawUsage?.input_tokens), numberValue(rawUsage?.output_tokens), numberValue(rawUsage?.cache_read_input_tokens)),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function parseGoogleResult(raw, model) {
|
|
812
|
+
const candidate = Array.isArray(raw.candidates) && isRecord(raw.candidates[0]) ? raw.candidates[0] : {};
|
|
813
|
+
const resultContent = isRecord(candidate.content) ? candidate.content : {};
|
|
814
|
+
const content = [];
|
|
815
|
+
const toolCalls = [];
|
|
816
|
+
for (const item of Array.isArray(resultContent.parts) ? resultContent.parts : []) {
|
|
817
|
+
if (!isRecord(item))
|
|
818
|
+
continue;
|
|
819
|
+
if (typeof item.text === "string")
|
|
820
|
+
content.push({ type: "text", text: item.text });
|
|
821
|
+
if (isRecord(item.functionCall))
|
|
822
|
+
toolCalls.push({
|
|
823
|
+
id: `call_${crypto.randomUUID()}`,
|
|
824
|
+
name: stringValue(item.functionCall.name) ?? "function",
|
|
825
|
+
arguments: JSON.stringify(item.functionCall.args ?? {}),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
const rawUsage = isRecord(raw.usageMetadata) ? raw.usageMetadata : undefined;
|
|
829
|
+
const finish = stringValue(candidate.finishReason);
|
|
830
|
+
return {
|
|
831
|
+
id: stringValue(raw.responseId) ?? `gemini_${crypto.randomUUID()}`,
|
|
832
|
+
model: stringValue(raw.modelVersion) ?? model,
|
|
833
|
+
created: Math.floor(Date.now() / 1000),
|
|
834
|
+
content,
|
|
835
|
+
toolCalls,
|
|
836
|
+
finishReason: toolCalls.length
|
|
837
|
+
? "tool_calls"
|
|
838
|
+
: finish === "MAX_TOKENS"
|
|
839
|
+
? "length"
|
|
840
|
+
: finish === "SAFETY" || finish === "BLOCKLIST"
|
|
841
|
+
? "content_filter"
|
|
842
|
+
: "stop",
|
|
843
|
+
usage: usage(numberValue(rawUsage?.promptTokenCount), numberValue(rawUsage?.candidatesTokenCount), numberValue(rawUsage?.cachedContentTokenCount), numberValue(rawUsage?.thoughtsTokenCount)),
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function resultToChat(result) {
|
|
847
|
+
return {
|
|
848
|
+
id: result.id.startsWith("chatcmpl_") ? result.id : `chatcmpl_${result.id}`,
|
|
849
|
+
object: "chat.completion",
|
|
850
|
+
created: result.created,
|
|
851
|
+
model: result.model,
|
|
852
|
+
choices: [
|
|
853
|
+
{
|
|
854
|
+
index: 0,
|
|
855
|
+
message: {
|
|
856
|
+
role: "assistant",
|
|
857
|
+
content: result.content
|
|
858
|
+
.filter((part) => part.type === "text")
|
|
859
|
+
.map((part) => part.text)
|
|
860
|
+
.join("") || null,
|
|
861
|
+
...(result.refusal ? { refusal: result.refusal } : {}),
|
|
862
|
+
...(result.toolCalls.length
|
|
863
|
+
? {
|
|
864
|
+
tool_calls: result.toolCalls.map((call) => ({
|
|
865
|
+
id: call.id,
|
|
866
|
+
type: "function",
|
|
867
|
+
function: { name: call.name, arguments: call.arguments },
|
|
868
|
+
})),
|
|
869
|
+
}
|
|
870
|
+
: {}),
|
|
871
|
+
},
|
|
872
|
+
finish_reason: result.finishReason,
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
...(result.usage
|
|
876
|
+
? {
|
|
877
|
+
usage: {
|
|
878
|
+
prompt_tokens: result.usage.input,
|
|
879
|
+
completion_tokens: result.usage.output,
|
|
880
|
+
total_tokens: result.usage.total,
|
|
881
|
+
...(result.usage.cached != null
|
|
882
|
+
? { prompt_tokens_details: { cached_tokens: result.usage.cached } }
|
|
883
|
+
: {}),
|
|
884
|
+
...(result.usage.reasoning != null
|
|
885
|
+
? { completion_tokens_details: { reasoning_tokens: result.usage.reasoning } }
|
|
886
|
+
: {}),
|
|
887
|
+
},
|
|
888
|
+
}
|
|
889
|
+
: {}),
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function resultToResponses(result) {
|
|
893
|
+
const output = [];
|
|
894
|
+
const textParts = result.content
|
|
895
|
+
.filter((part) => part.type === "text")
|
|
896
|
+
.map((part) => ({ type: "output_text", text: part.text, annotations: [] }));
|
|
897
|
+
if (result.refusal)
|
|
898
|
+
textParts.push({ type: "refusal", refusal: result.refusal });
|
|
899
|
+
if (textParts.length)
|
|
900
|
+
output.push({
|
|
901
|
+
id: `msg_${crypto.randomUUID()}`,
|
|
902
|
+
type: "message",
|
|
903
|
+
status: "completed",
|
|
904
|
+
role: "assistant",
|
|
905
|
+
content: textParts,
|
|
906
|
+
});
|
|
907
|
+
for (const call of result.toolCalls)
|
|
908
|
+
output.push({
|
|
909
|
+
id: `fc_${crypto.randomUUID()}`,
|
|
910
|
+
type: "function_call",
|
|
911
|
+
status: "completed",
|
|
912
|
+
call_id: call.id,
|
|
913
|
+
name: call.name,
|
|
914
|
+
arguments: call.arguments,
|
|
915
|
+
});
|
|
916
|
+
return {
|
|
917
|
+
id: result.id.startsWith("resp_") ? result.id : `resp_${result.id}`,
|
|
918
|
+
object: "response",
|
|
919
|
+
created_at: result.created,
|
|
920
|
+
status: result.finishReason === "error"
|
|
921
|
+
? "failed"
|
|
922
|
+
: result.finishReason === "length"
|
|
923
|
+
? "incomplete"
|
|
924
|
+
: "completed",
|
|
925
|
+
model: result.model,
|
|
926
|
+
output,
|
|
927
|
+
output_text: result.content
|
|
928
|
+
.filter((part) => part.type === "text")
|
|
929
|
+
.map((part) => part.text)
|
|
930
|
+
.join(""),
|
|
931
|
+
...(result.usage
|
|
932
|
+
? {
|
|
933
|
+
usage: {
|
|
934
|
+
input_tokens: result.usage.input,
|
|
935
|
+
output_tokens: result.usage.output,
|
|
936
|
+
total_tokens: result.usage.total,
|
|
937
|
+
input_tokens_details: { cached_tokens: result.usage.cached ?? 0 },
|
|
938
|
+
output_tokens_details: { reasoning_tokens: result.usage.reasoning ?? 0 },
|
|
939
|
+
},
|
|
940
|
+
}
|
|
941
|
+
: {}),
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function resultToAnthropic(result) {
|
|
945
|
+
const content = result.content
|
|
946
|
+
.filter((part) => part.type === "text")
|
|
947
|
+
.map((part) => ({ type: "text", text: part.text }));
|
|
948
|
+
for (const call of result.toolCalls)
|
|
949
|
+
content.push({
|
|
950
|
+
type: "tool_use",
|
|
951
|
+
id: call.id,
|
|
952
|
+
name: call.name,
|
|
953
|
+
input: JSON.parse(call.arguments || "{}"),
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
id: result.id.startsWith("msg_") ? result.id : `msg_${result.id}`,
|
|
957
|
+
type: "message",
|
|
958
|
+
role: "assistant",
|
|
959
|
+
model: result.model,
|
|
960
|
+
content,
|
|
961
|
+
stop_reason: result.finishReason === "length"
|
|
962
|
+
? "max_tokens"
|
|
963
|
+
: result.toolCalls.length
|
|
964
|
+
? "tool_use"
|
|
965
|
+
: "end_turn",
|
|
966
|
+
stop_sequence: null,
|
|
967
|
+
...(result.usage
|
|
968
|
+
? {
|
|
969
|
+
usage: {
|
|
970
|
+
input_tokens: result.usage.input,
|
|
971
|
+
output_tokens: result.usage.output,
|
|
972
|
+
...(result.usage.cached != null
|
|
973
|
+
? { cache_read_input_tokens: result.usage.cached }
|
|
974
|
+
: {}),
|
|
975
|
+
},
|
|
976
|
+
}
|
|
977
|
+
: {}),
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function resultToGoogle(result) {
|
|
981
|
+
return {
|
|
982
|
+
responseId: result.id,
|
|
983
|
+
modelVersion: result.model,
|
|
984
|
+
candidates: [
|
|
985
|
+
{
|
|
986
|
+
content: {
|
|
987
|
+
role: "model",
|
|
988
|
+
parts: [
|
|
989
|
+
...result.content
|
|
990
|
+
.filter((part) => part.type === "text")
|
|
991
|
+
.map((part) => ({ text: part.text })),
|
|
992
|
+
...result.toolCalls.map((call) => ({
|
|
993
|
+
functionCall: { name: call.name, args: JSON.parse(call.arguments || "{}") },
|
|
994
|
+
})),
|
|
995
|
+
],
|
|
996
|
+
},
|
|
997
|
+
finishReason: result.finishReason === "length"
|
|
998
|
+
? "MAX_TOKENS"
|
|
999
|
+
: result.finishReason === "content_filter"
|
|
1000
|
+
? "SAFETY"
|
|
1001
|
+
: "STOP",
|
|
1002
|
+
index: 0,
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
...(result.usage
|
|
1006
|
+
? {
|
|
1007
|
+
usageMetadata: {
|
|
1008
|
+
promptTokenCount: result.usage.input,
|
|
1009
|
+
candidatesTokenCount: result.usage.output,
|
|
1010
|
+
totalTokenCount: result.usage.total,
|
|
1011
|
+
...(result.usage.cached != null
|
|
1012
|
+
? { cachedContentTokenCount: result.usage.cached }
|
|
1013
|
+
: {}),
|
|
1014
|
+
...(result.usage.reasoning != null
|
|
1015
|
+
? { thoughtsTokenCount: result.usage.reasoning }
|
|
1016
|
+
: {}),
|
|
1017
|
+
},
|
|
1018
|
+
}
|
|
1019
|
+
: {}),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function sse(frames) {
|
|
1023
|
+
return new Response(frames
|
|
1024
|
+
.map((frame) => (typeof frame === "string" ? frame : `data: ${JSON.stringify(frame)}\n\n`))
|
|
1025
|
+
.join(""), {
|
|
1026
|
+
headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store" },
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
function streamChat(result) {
|
|
1030
|
+
const base = resultToChat(result);
|
|
1031
|
+
const choice = base.choices[0];
|
|
1032
|
+
const message = choice.message;
|
|
1033
|
+
const chunk = (delta, finish = null, includeUsage = false) => ({
|
|
1034
|
+
id: base.id,
|
|
1035
|
+
object: "chat.completion.chunk",
|
|
1036
|
+
created: base.created,
|
|
1037
|
+
model: base.model,
|
|
1038
|
+
choices: [{ index: 0, delta, finish_reason: finish }],
|
|
1039
|
+
...(includeUsage && base.usage ? { usage: base.usage } : {}),
|
|
1040
|
+
});
|
|
1041
|
+
const frames = [chunk({ role: "assistant", content: "" })];
|
|
1042
|
+
if (typeof message.content === "string" && message.content)
|
|
1043
|
+
frames.push(chunk({ content: message.content }));
|
|
1044
|
+
for (const [index, call] of (Array.isArray(message.tool_calls)
|
|
1045
|
+
? message.tool_calls
|
|
1046
|
+
: []).entries())
|
|
1047
|
+
frames.push(chunk({ tool_calls: [{ index, ...call }] }));
|
|
1048
|
+
frames.push(chunk({}, choice.finish_reason, true), "data: [DONE]\n\n");
|
|
1049
|
+
return sse(frames);
|
|
1050
|
+
}
|
|
1051
|
+
function responsesToChatStream(upstream, model) {
|
|
1052
|
+
if (!upstream.body)
|
|
1053
|
+
return upstream;
|
|
1054
|
+
const encoder = new TextEncoder();
|
|
1055
|
+
let response = {
|
|
1056
|
+
id: `resp_${crypto.randomUUID()}`,
|
|
1057
|
+
model,
|
|
1058
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
1059
|
+
};
|
|
1060
|
+
let started = false;
|
|
1061
|
+
let finished = false;
|
|
1062
|
+
let hasToolCalls = false;
|
|
1063
|
+
const toolIndexes = new Map();
|
|
1064
|
+
const emit = (controller, delta, finishReason = null, rawUsage) => {
|
|
1065
|
+
const usage = rawUsage
|
|
1066
|
+
? {
|
|
1067
|
+
prompt_tokens: rawUsage.input_tokens,
|
|
1068
|
+
completion_tokens: rawUsage.output_tokens,
|
|
1069
|
+
total_tokens: rawUsage.total_tokens,
|
|
1070
|
+
}
|
|
1071
|
+
: undefined;
|
|
1072
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
1073
|
+
id: `chatcmpl_${stringValue(response.id) ?? crypto.randomUUID()}`,
|
|
1074
|
+
object: "chat.completion.chunk",
|
|
1075
|
+
created: numberValue(response.created_at) ?? Math.floor(Date.now() / 1000),
|
|
1076
|
+
model: stringValue(response.model) ?? model,
|
|
1077
|
+
choices: [{ index: 0, delta, finish_reason: finishReason }],
|
|
1078
|
+
...(usage ? { usage } : {}),
|
|
1079
|
+
})}\n\n`));
|
|
1080
|
+
};
|
|
1081
|
+
const start = (controller) => {
|
|
1082
|
+
if (started)
|
|
1083
|
+
return;
|
|
1084
|
+
started = true;
|
|
1085
|
+
emit(controller, { role: "assistant", content: "" });
|
|
1086
|
+
};
|
|
1087
|
+
const finish = (controller, completed) => {
|
|
1088
|
+
if (finished)
|
|
1089
|
+
return;
|
|
1090
|
+
if (completed)
|
|
1091
|
+
response = completed;
|
|
1092
|
+
start(controller);
|
|
1093
|
+
finished = true;
|
|
1094
|
+
const incomplete = response.status === "incomplete";
|
|
1095
|
+
emit(controller, {}, hasToolCalls ? "tool_calls" : incomplete ? "length" : "stop", isRecord(response.usage) ? response.usage : undefined);
|
|
1096
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1097
|
+
};
|
|
1098
|
+
let upstreamReader;
|
|
1099
|
+
const body = new ReadableStream({
|
|
1100
|
+
async start(controller) {
|
|
1101
|
+
const reader = upstream.body.getReader();
|
|
1102
|
+
upstreamReader = reader;
|
|
1103
|
+
const decoder = new TextDecoder();
|
|
1104
|
+
let buffer = "";
|
|
1105
|
+
const handle = (raw) => {
|
|
1106
|
+
const data = raw
|
|
1107
|
+
.split(/\r?\n/)
|
|
1108
|
+
.filter((line) => line.startsWith("data:"))
|
|
1109
|
+
.map((line) => line.slice(5).trim())
|
|
1110
|
+
.join("\n");
|
|
1111
|
+
if (!data || data === "[DONE]")
|
|
1112
|
+
return;
|
|
1113
|
+
const event = JSON.parse(data);
|
|
1114
|
+
if (!isRecord(event))
|
|
1115
|
+
return;
|
|
1116
|
+
const completed = isRecord(event.response) ? event.response : undefined;
|
|
1117
|
+
if (event.type === "response.created" && completed)
|
|
1118
|
+
response = completed;
|
|
1119
|
+
if (event.type === "response.output_text.delta") {
|
|
1120
|
+
start(controller);
|
|
1121
|
+
emit(controller, { content: stringValue(event.delta) ?? "" });
|
|
1122
|
+
}
|
|
1123
|
+
if (event.type === "response.refusal.delta") {
|
|
1124
|
+
start(controller);
|
|
1125
|
+
emit(controller, { refusal: stringValue(event.delta) ?? "" });
|
|
1126
|
+
}
|
|
1127
|
+
const outputIndex = numberValue(event.output_index) ?? toolIndexes.size;
|
|
1128
|
+
if (event.type === "response.output_item.added" &&
|
|
1129
|
+
isRecord(event.item) &&
|
|
1130
|
+
event.item.type === "function_call") {
|
|
1131
|
+
start(controller);
|
|
1132
|
+
hasToolCalls = true;
|
|
1133
|
+
const index = toolIndexes.size;
|
|
1134
|
+
toolIndexes.set(outputIndex, index);
|
|
1135
|
+
emit(controller, {
|
|
1136
|
+
tool_calls: [
|
|
1137
|
+
{
|
|
1138
|
+
index,
|
|
1139
|
+
id: stringValue(event.item.call_id) ??
|
|
1140
|
+
stringValue(event.item.id) ??
|
|
1141
|
+
`call_${crypto.randomUUID()}`,
|
|
1142
|
+
type: "function",
|
|
1143
|
+
function: {
|
|
1144
|
+
name: stringValue(event.item.name) ?? "function",
|
|
1145
|
+
arguments: stringValue(event.item.arguments) ?? "",
|
|
1146
|
+
},
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1152
|
+
start(controller);
|
|
1153
|
+
hasToolCalls = true;
|
|
1154
|
+
emit(controller, {
|
|
1155
|
+
tool_calls: [
|
|
1156
|
+
{
|
|
1157
|
+
index: toolIndexes.get(outputIndex) ?? 0,
|
|
1158
|
+
function: { arguments: stringValue(event.delta) ?? "" },
|
|
1159
|
+
},
|
|
1160
|
+
],
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
1164
|
+
finish(controller, completed);
|
|
1165
|
+
}
|
|
1166
|
+
if (event.type === "response.failed" || event.type === "error") {
|
|
1167
|
+
const detail = isRecord(event.error)
|
|
1168
|
+
? stringValue(event.error.message)
|
|
1169
|
+
: "Provider stream failed";
|
|
1170
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
1171
|
+
error: {
|
|
1172
|
+
message: detail ?? "Provider stream failed",
|
|
1173
|
+
type: "provider_error",
|
|
1174
|
+
code: "provider_error",
|
|
1175
|
+
},
|
|
1176
|
+
})}\n\n`));
|
|
1177
|
+
finished = true;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
try {
|
|
1181
|
+
for (;;) {
|
|
1182
|
+
const { done, value } = await reader.read();
|
|
1183
|
+
buffer += decoder.decode(value, { stream: !done });
|
|
1184
|
+
for (let end = buffer.search(/\r?\n\r?\n/); end !== -1; end = buffer.search(/\r?\n\r?\n/)) {
|
|
1185
|
+
const raw = buffer.slice(0, end);
|
|
1186
|
+
buffer = buffer.slice(end).replace(/^\r?\n\r?\n/, "");
|
|
1187
|
+
handle(raw);
|
|
1188
|
+
}
|
|
1189
|
+
if (done)
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
if (buffer)
|
|
1193
|
+
handle(buffer);
|
|
1194
|
+
finish(controller);
|
|
1195
|
+
controller.close();
|
|
1196
|
+
}
|
|
1197
|
+
catch (error) {
|
|
1198
|
+
controller.error(error);
|
|
1199
|
+
}
|
|
1200
|
+
finally {
|
|
1201
|
+
reader.releaseLock();
|
|
1202
|
+
upstreamReader = undefined;
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
cancel(reason) {
|
|
1206
|
+
return upstreamReader?.cancel(reason);
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
return new Response(body, {
|
|
1210
|
+
headers: {
|
|
1211
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
1212
|
+
"cache-control": "no-store",
|
|
1213
|
+
},
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
function streamResponses(result) {
|
|
1217
|
+
const response = resultToResponses(result);
|
|
1218
|
+
const frames = [
|
|
1219
|
+
{ type: "response.created", response: { ...response, status: "in_progress", output: [] } },
|
|
1220
|
+
];
|
|
1221
|
+
for (const [outputIndex, item] of response.output.entries()) {
|
|
1222
|
+
frames.push({ type: "response.output_item.added", output_index: outputIndex, item });
|
|
1223
|
+
if (item.type === "message") {
|
|
1224
|
+
for (const [contentIndex, part] of item.content.entries()) {
|
|
1225
|
+
frames.push({
|
|
1226
|
+
type: "response.content_part.added",
|
|
1227
|
+
item_id: item.id,
|
|
1228
|
+
output_index: outputIndex,
|
|
1229
|
+
content_index: contentIndex,
|
|
1230
|
+
part,
|
|
1231
|
+
});
|
|
1232
|
+
if (part.type === "output_text")
|
|
1233
|
+
frames.push({
|
|
1234
|
+
type: "response.output_text.delta",
|
|
1235
|
+
item_id: item.id,
|
|
1236
|
+
output_index: outputIndex,
|
|
1237
|
+
content_index: contentIndex,
|
|
1238
|
+
delta: part.text,
|
|
1239
|
+
});
|
|
1240
|
+
frames.push({
|
|
1241
|
+
type: "response.content_part.done",
|
|
1242
|
+
item_id: item.id,
|
|
1243
|
+
output_index: outputIndex,
|
|
1244
|
+
content_index: contentIndex,
|
|
1245
|
+
part,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
else if (item.type === "function_call") {
|
|
1250
|
+
frames.push({
|
|
1251
|
+
type: "response.function_call_arguments.delta",
|
|
1252
|
+
item_id: item.id,
|
|
1253
|
+
output_index: outputIndex,
|
|
1254
|
+
delta: item.arguments,
|
|
1255
|
+
});
|
|
1256
|
+
frames.push({
|
|
1257
|
+
type: "response.function_call_arguments.done",
|
|
1258
|
+
item_id: item.id,
|
|
1259
|
+
output_index: outputIndex,
|
|
1260
|
+
arguments: item.arguments,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
frames.push({ type: "response.output_item.done", output_index: outputIndex, item });
|
|
1264
|
+
}
|
|
1265
|
+
frames.push({ type: "response.completed", response }, "data: [DONE]\n\n");
|
|
1266
|
+
return sse(frames);
|
|
1267
|
+
}
|
|
1268
|
+
function streamAnthropic(result) {
|
|
1269
|
+
const message = resultToAnthropic(result);
|
|
1270
|
+
const frames = [];
|
|
1271
|
+
const event = (name, data) => frames.push(`event: ${name}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
1272
|
+
event("message_start", {
|
|
1273
|
+
type: "message_start",
|
|
1274
|
+
message: {
|
|
1275
|
+
...message,
|
|
1276
|
+
content: [],
|
|
1277
|
+
stop_reason: null,
|
|
1278
|
+
usage: { input_tokens: result.usage?.input ?? 0, output_tokens: 0 },
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
for (const [index, block] of message.content.entries()) {
|
|
1282
|
+
event("content_block_start", {
|
|
1283
|
+
type: "content_block_start",
|
|
1284
|
+
index,
|
|
1285
|
+
content_block: block.type === "text" ? { type: "text", text: "" } : { ...block, input: {} },
|
|
1286
|
+
});
|
|
1287
|
+
event("content_block_delta", {
|
|
1288
|
+
type: "content_block_delta",
|
|
1289
|
+
index,
|
|
1290
|
+
delta: block.type === "text"
|
|
1291
|
+
? { type: "text_delta", text: block.text }
|
|
1292
|
+
: { type: "input_json_delta", partial_json: JSON.stringify(block.input ?? {}) },
|
|
1293
|
+
});
|
|
1294
|
+
event("content_block_stop", { type: "content_block_stop", index });
|
|
1295
|
+
}
|
|
1296
|
+
event("message_delta", {
|
|
1297
|
+
type: "message_delta",
|
|
1298
|
+
delta: { stop_reason: message.stop_reason, stop_sequence: null },
|
|
1299
|
+
usage: { output_tokens: result.usage?.output ?? 0 },
|
|
1300
|
+
});
|
|
1301
|
+
event("message_stop", { type: "message_stop" });
|
|
1302
|
+
return new Response(frames.join(""), {
|
|
1303
|
+
headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store" },
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
const DEFAULT_PROTOCOL = {
|
|
1307
|
+
chatgpt: "responses",
|
|
1308
|
+
claude: "messages",
|
|
1309
|
+
copilot: "chat/completions",
|
|
1310
|
+
grok: "chat/completions",
|
|
1311
|
+
"opencode-go": "chat/completions",
|
|
1312
|
+
"opencode-zen": "chat/completions",
|
|
1313
|
+
};
|
|
1314
|
+
function endpointProtocol(endpoint) {
|
|
1315
|
+
const normalized = endpoint.replace(/^\//, "");
|
|
1316
|
+
if (normalized.includes("responses"))
|
|
1317
|
+
return "responses";
|
|
1318
|
+
if (normalized.includes("chat/completions"))
|
|
1319
|
+
return "chat/completions";
|
|
1320
|
+
if (normalized.includes("messages"))
|
|
1321
|
+
return "messages";
|
|
1322
|
+
if (normalized.startsWith("models/"))
|
|
1323
|
+
return "google";
|
|
1324
|
+
return null;
|
|
1325
|
+
}
|
|
1326
|
+
function nativeProtocol(provider, model, requested) {
|
|
1327
|
+
const protocols = (model?.endpoints ?? []).flatMap((endpoint) => endpointProtocol(endpoint) ?? []);
|
|
1328
|
+
if (protocols.includes(requested))
|
|
1329
|
+
return requested;
|
|
1330
|
+
return protocols[0] ?? DEFAULT_PROTOCOL[provider] ?? requested;
|
|
1331
|
+
}
|
|
1332
|
+
function targetBody(protocol, request) {
|
|
1333
|
+
if (protocol === "responses")
|
|
1334
|
+
return toResponses(request);
|
|
1335
|
+
if (protocol === "messages")
|
|
1336
|
+
return toAnthropic(request);
|
|
1337
|
+
if (protocol === "google")
|
|
1338
|
+
return toGoogle(request);
|
|
1339
|
+
return toChat(request);
|
|
1340
|
+
}
|
|
1341
|
+
function targetPath(protocol, model) {
|
|
1342
|
+
return protocol === "google" ? `models/${encodeURIComponent(model)}:generateContent` : protocol;
|
|
1343
|
+
}
|
|
1344
|
+
async function upstreamResult(response, protocol, model) {
|
|
1345
|
+
const body = await response.text();
|
|
1346
|
+
let raw;
|
|
1347
|
+
if (protocol === "responses" && /(^|\n)data:/.test(body)) {
|
|
1348
|
+
let completed = {};
|
|
1349
|
+
let outputText = "";
|
|
1350
|
+
const calls = new Map();
|
|
1351
|
+
for (const frame of body.split(/\r?\n\r?\n/)) {
|
|
1352
|
+
const data = frame
|
|
1353
|
+
.split(/\r?\n/)
|
|
1354
|
+
.filter((line) => line.startsWith("data:"))
|
|
1355
|
+
.map((line) => line.slice(5).trim())
|
|
1356
|
+
.join("\n");
|
|
1357
|
+
if (!data || data === "[DONE]")
|
|
1358
|
+
continue;
|
|
1359
|
+
const event = JSON.parse(data);
|
|
1360
|
+
if (!isRecord(event))
|
|
1361
|
+
continue;
|
|
1362
|
+
if (event.type === "response.output_text.delta")
|
|
1363
|
+
outputText += stringValue(event.delta) ?? "";
|
|
1364
|
+
const index = numberValue(event.output_index) ?? calls.size;
|
|
1365
|
+
if (event.type === "response.output_item.added" &&
|
|
1366
|
+
isRecord(event.item) &&
|
|
1367
|
+
event.item.type === "function_call") {
|
|
1368
|
+
calls.set(index, {
|
|
1369
|
+
id: stringValue(event.item.call_id) ??
|
|
1370
|
+
stringValue(event.item.id) ??
|
|
1371
|
+
`call_${crypto.randomUUID()}`,
|
|
1372
|
+
name: stringValue(event.item.name) ?? "function",
|
|
1373
|
+
arguments: stringValue(event.item.arguments) ?? "",
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1377
|
+
const call = calls.get(index);
|
|
1378
|
+
if (call)
|
|
1379
|
+
call.arguments += stringValue(event.delta) ?? "";
|
|
1380
|
+
}
|
|
1381
|
+
if (event.type === "response.completed" && isRecord(event.response))
|
|
1382
|
+
completed = event.response;
|
|
1383
|
+
if ((event.type === "response.failed" || event.type === "error") && isRecord(event.response))
|
|
1384
|
+
completed = event.response;
|
|
1385
|
+
}
|
|
1386
|
+
const output = Array.isArray(completed.output) ? [...completed.output] : [];
|
|
1387
|
+
if (outputText && !output.some((item) => isRecord(item) && item.type === "message")) {
|
|
1388
|
+
output.push({
|
|
1389
|
+
type: "message",
|
|
1390
|
+
role: "assistant",
|
|
1391
|
+
content: [{ type: "output_text", text: outputText }],
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
if (calls.size && !output.some((item) => isRecord(item) && item.type === "function_call")) {
|
|
1395
|
+
output.push(...[...calls.values()].map((call) => ({
|
|
1396
|
+
type: "function_call",
|
|
1397
|
+
call_id: call.id,
|
|
1398
|
+
name: call.name,
|
|
1399
|
+
arguments: call.arguments,
|
|
1400
|
+
})));
|
|
1401
|
+
}
|
|
1402
|
+
raw = { ...completed, output };
|
|
1403
|
+
}
|
|
1404
|
+
else
|
|
1405
|
+
raw = JSON.parse(body);
|
|
1406
|
+
const value = record(raw, "Provider returned an invalid compatibility response");
|
|
1407
|
+
if (protocol === "responses")
|
|
1408
|
+
return parseResponsesResult(value, model);
|
|
1409
|
+
if (protocol === "messages")
|
|
1410
|
+
return parseAnthropicResult(value, model);
|
|
1411
|
+
if (protocol === "google")
|
|
1412
|
+
return parseGoogleResult(value, model);
|
|
1413
|
+
return parseChatResult(value, model);
|
|
1414
|
+
}
|
|
1415
|
+
function renderResult(result, protocol, stream) {
|
|
1416
|
+
if (stream) {
|
|
1417
|
+
if (protocol === "responses")
|
|
1418
|
+
return streamResponses(result);
|
|
1419
|
+
if (protocol === "messages")
|
|
1420
|
+
return streamAnthropic(result);
|
|
1421
|
+
if (protocol === "google")
|
|
1422
|
+
return sse([resultToGoogle(result)]);
|
|
1423
|
+
return streamChat(result);
|
|
1424
|
+
}
|
|
1425
|
+
const body = protocol === "responses"
|
|
1426
|
+
? resultToResponses(result)
|
|
1427
|
+
: protocol === "messages"
|
|
1428
|
+
? resultToAnthropic(result)
|
|
1429
|
+
: protocol === "google"
|
|
1430
|
+
? resultToGoogle(result)
|
|
1431
|
+
: resultToChat(result);
|
|
1432
|
+
return Response.json(body);
|
|
1433
|
+
}
|
|
1434
|
+
function errorResponse(error, protocol, status = 400) {
|
|
1435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1436
|
+
const code = error instanceof CompatibilityError ? error.code : "provider_error";
|
|
1437
|
+
const body = protocol === "messages"
|
|
1438
|
+
? { type: "error", error: { type: code, message } }
|
|
1439
|
+
: protocol === "google"
|
|
1440
|
+
? { error: { code: status, status: code.toUpperCase(), message } }
|
|
1441
|
+
: { error: { message, type: code, code } };
|
|
1442
|
+
return Response.json(body, {
|
|
1443
|
+
status: error instanceof CompatibilityError ? error.status : status,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
export function requestProtocol(path) {
|
|
1447
|
+
const clean = path.replace(/^\//, "").split("?")[0] ?? "";
|
|
1448
|
+
if (clean === "responses")
|
|
1449
|
+
return { protocol: "responses" };
|
|
1450
|
+
if (clean === "chat/completions")
|
|
1451
|
+
return { protocol: "chat/completions" };
|
|
1452
|
+
if (clean === "messages")
|
|
1453
|
+
return { protocol: "messages" };
|
|
1454
|
+
const google = clean.match(/^models\/([^/:]+):(streamGenerateContent|generateContent)$/);
|
|
1455
|
+
return google?.[1]
|
|
1456
|
+
? {
|
|
1457
|
+
protocol: "google",
|
|
1458
|
+
model: decodeURIComponent(google[1]),
|
|
1459
|
+
stream: google[2] === "streamGenerateContent",
|
|
1460
|
+
}
|
|
1461
|
+
: null;
|
|
1462
|
+
}
|
|
1463
|
+
export async function proxyCompatible(auth, provider, account, path, body, headers, signal) {
|
|
1464
|
+
const route = requestProtocol(path);
|
|
1465
|
+
if (!route)
|
|
1466
|
+
return null;
|
|
1467
|
+
try {
|
|
1468
|
+
if (!body.length && (DEFAULT_PROTOCOL[provider] ?? route.protocol) === route.protocol)
|
|
1469
|
+
return null;
|
|
1470
|
+
const raw = json(body);
|
|
1471
|
+
const modelHint = stringValue(raw.model) ?? route.model;
|
|
1472
|
+
if (!modelHint && (DEFAULT_PROTOCOL[provider] ?? route.protocol) === route.protocol)
|
|
1473
|
+
return null;
|
|
1474
|
+
const catalog = modelHint
|
|
1475
|
+
? await auth.getModels(provider, account, signal).catch(() => null)
|
|
1476
|
+
: null;
|
|
1477
|
+
const model = catalog?.models.find((candidate) => candidate.id === modelHint);
|
|
1478
|
+
const target = nativeProtocol(provider, model, route.protocol);
|
|
1479
|
+
if (target === route.protocol)
|
|
1480
|
+
return null;
|
|
1481
|
+
const request = route.protocol === "chat/completions"
|
|
1482
|
+
? parseChat(body)
|
|
1483
|
+
: route.protocol === "responses"
|
|
1484
|
+
? parseResponses(body)
|
|
1485
|
+
: route.protocol === "messages"
|
|
1486
|
+
? parseAnthropic(body)
|
|
1487
|
+
: parseGoogle(body, route.model ?? "", route.stream);
|
|
1488
|
+
const upstreamHeaders = new Headers(headers);
|
|
1489
|
+
upstreamHeaders.set("content-type", "application/json");
|
|
1490
|
+
upstreamHeaders.set("accept", "application/json");
|
|
1491
|
+
const outgoing = targetBody(target, request);
|
|
1492
|
+
if (provider === "chatgpt" && target === "responses") {
|
|
1493
|
+
delete outgoing.temperature;
|
|
1494
|
+
delete outgoing.top_p;
|
|
1495
|
+
}
|
|
1496
|
+
if (target === "responses" &&
|
|
1497
|
+
(provider === "chatgpt" || (request.stream && route.protocol === "chat/completions"))) {
|
|
1498
|
+
outgoing.stream = true;
|
|
1499
|
+
}
|
|
1500
|
+
const upstream = await auth.proxy(provider, account, targetPath(target, request.model), {
|
|
1501
|
+
method: "POST",
|
|
1502
|
+
headers: upstreamHeaders,
|
|
1503
|
+
body: JSON.stringify(outgoing),
|
|
1504
|
+
signal,
|
|
1505
|
+
});
|
|
1506
|
+
if (!upstream.ok) {
|
|
1507
|
+
const detail = await upstream.text();
|
|
1508
|
+
return errorResponse(new Error(detail || `Provider request failed (${upstream.status})`), route.protocol, upstream.status);
|
|
1509
|
+
}
|
|
1510
|
+
if (request.stream &&
|
|
1511
|
+
route.protocol === "chat/completions" &&
|
|
1512
|
+
target === "responses" &&
|
|
1513
|
+
upstream.body) {
|
|
1514
|
+
return responsesToChatStream(upstream, request.model);
|
|
1515
|
+
}
|
|
1516
|
+
return renderResult(await upstreamResult(upstream, target, request.model), route.protocol, request.stream);
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
return errorResponse(error, route.protocol);
|
|
1520
|
+
}
|
|
1521
|
+
}
|