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.
@@ -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
+ }