aisubs 0.3.5 → 0.3.7
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 +14 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +20 -10
- package/dist/cli.js +0 -0
- package/dist/compatibility.js +220 -151
- package/dist/dashboard/assets/index-CePTUw7Z.css +2 -0
- package/dist/dashboard/assets/index-Vscu32Dz.js +84 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard.js +15 -26
- package/dist/http.d.ts +3 -0
- package/dist/http.js +12 -31
- package/dist/providers/chatgpt.js +52 -21
- package/dist/proxy-headers.d.ts +2 -0
- package/dist/proxy-headers.js +35 -0
- package/dist/realtime.js +6 -20
- package/dist/store.js +11 -4
- package/dist/utils.js +1 -1
- package/package.json +7 -3
- package/dist/dashboard/assets/index-BJDbjHnw.css +0 -2
- package/dist/dashboard/assets/index-BbN84qld.js +0 -84
package/dist/compatibility.js
CHANGED
|
@@ -29,13 +29,22 @@ function requiredModel(raw) {
|
|
|
29
29
|
throw new CompatibilityError("A non-empty model is required");
|
|
30
30
|
return model;
|
|
31
31
|
}
|
|
32
|
+
function cacheControl(value) {
|
|
33
|
+
if (!isRecord(value) || value.cache_control == null)
|
|
34
|
+
return {};
|
|
35
|
+
const control = record(value.cache_control, "cache_control must be an object");
|
|
36
|
+
if (control.type !== "ephemeral" ||
|
|
37
|
+
(control.ttl != null && control.ttl !== "5m" && control.ttl !== "1h"))
|
|
38
|
+
throw new CompatibilityError("Unsupported cache_control type or TTL", "unsupported_feature");
|
|
39
|
+
return { cache_control: { type: "ephemeral", ...(control.ttl ? { ttl: control.ttl } : {}) } };
|
|
40
|
+
}
|
|
32
41
|
function text(value) {
|
|
33
42
|
if (typeof value === "string")
|
|
34
43
|
return { type: "text", text: value };
|
|
35
44
|
if (!isRecord(value))
|
|
36
45
|
return null;
|
|
37
|
-
const content =
|
|
38
|
-
return content == null ? null : { type: "text", text: content };
|
|
46
|
+
const content = typeof value.text === "string" ? value.text : undefined;
|
|
47
|
+
return content == null ? null : { type: "text", text: content, ...cacheControl(value) };
|
|
39
48
|
}
|
|
40
49
|
function imageUrl(value) {
|
|
41
50
|
if (typeof value === "string")
|
|
@@ -61,14 +70,14 @@ function openAiParts(value) {
|
|
|
61
70
|
const url = imageUrl(part.image_url) ?? stringValue(part.image_url) ?? stringValue(part.file_id);
|
|
62
71
|
if (!url)
|
|
63
72
|
throw new CompatibilityError("Image content requires image_url or file_id");
|
|
64
|
-
return { type: "image", url, detail: stringValue(part.detail) };
|
|
73
|
+
return { type: "image", url, detail: stringValue(part.detail), ...cacheControl(part) };
|
|
65
74
|
}
|
|
66
75
|
if (type === "input_audio") {
|
|
67
76
|
const audio = record(part.input_audio, "input_audio requires audio data");
|
|
68
77
|
const data = stringValue(audio.data);
|
|
69
78
|
if (!data)
|
|
70
79
|
throw new CompatibilityError("input_audio requires audio data");
|
|
71
|
-
return { type: "audio", data, format: stringValue(audio.format) };
|
|
80
|
+
return { type: "audio", data, format: stringValue(audio.format), ...cacheControl(part) };
|
|
72
81
|
}
|
|
73
82
|
if (type === "file" || type === "input_file") {
|
|
74
83
|
return {
|
|
@@ -76,6 +85,7 @@ function openAiParts(value) {
|
|
|
76
85
|
fileId: stringValue(part.file_id),
|
|
77
86
|
data: stringValue(part.file_data),
|
|
78
87
|
filename: stringValue(part.filename),
|
|
88
|
+
...cacheControl(part),
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
91
|
if (type === "refusal")
|
|
@@ -116,6 +126,7 @@ function chatTools(value) {
|
|
|
116
126
|
description: stringValue(fn.description),
|
|
117
127
|
parameters: fn.parameters,
|
|
118
128
|
strict: typeof fn.strict === "boolean" ? fn.strict : undefined,
|
|
129
|
+
...cacheControl(tool),
|
|
119
130
|
};
|
|
120
131
|
});
|
|
121
132
|
}
|
|
@@ -134,6 +145,7 @@ function parseChat(body) {
|
|
|
134
145
|
content: openAiParts(message.content),
|
|
135
146
|
toolCallId: stringValue(message.tool_call_id),
|
|
136
147
|
toolCalls: chatToolCalls(message.tool_calls),
|
|
148
|
+
...cacheControl(message),
|
|
137
149
|
};
|
|
138
150
|
});
|
|
139
151
|
return {
|
|
@@ -151,6 +163,7 @@ function parseChat(body) {
|
|
|
151
163
|
metadata: raw.metadata,
|
|
152
164
|
user: stringValue(raw.user),
|
|
153
165
|
promptCacheKey: stringValue(raw.prompt_cache_key),
|
|
166
|
+
cacheControl: cacheControl(raw).cache_control,
|
|
154
167
|
};
|
|
155
168
|
}
|
|
156
169
|
function responseTools(value) {
|
|
@@ -174,6 +187,9 @@ function responseTools(value) {
|
|
|
174
187
|
}
|
|
175
188
|
function parseResponses(body) {
|
|
176
189
|
const raw = json(body);
|
|
190
|
+
if (raw.previous_response_id != null || raw.background === true) {
|
|
191
|
+
throw new CompatibilityError("Stored or background Responses require native Responses support", "unsupported_feature");
|
|
192
|
+
}
|
|
177
193
|
const messages = [];
|
|
178
194
|
if (typeof raw.instructions === "string") {
|
|
179
195
|
messages.push({ role: "developer", content: [{ type: "text", text: raw.instructions }] });
|
|
@@ -218,13 +234,16 @@ function parseResponses(body) {
|
|
|
218
234
|
else if (value.type === "item_reference") {
|
|
219
235
|
throw new CompatibilityError("item_reference requires native Responses support", "unsupported_feature");
|
|
220
236
|
}
|
|
221
|
-
else {
|
|
237
|
+
else if (value.type == null || value.type === "message") {
|
|
222
238
|
const role = stringValue(value.role) ?? "user";
|
|
223
239
|
if (!["system", "developer", "user", "assistant"].includes(role)) {
|
|
224
240
|
throw new CompatibilityError(`Unsupported Responses role: ${role}`);
|
|
225
241
|
}
|
|
226
242
|
messages.push({ role: role, content: openAiParts(value.content) });
|
|
227
243
|
}
|
|
244
|
+
else {
|
|
245
|
+
throw new CompatibilityError(`Unsupported Responses input item: ${String(value.type)}`, "unsupported_feature");
|
|
246
|
+
}
|
|
228
247
|
}
|
|
229
248
|
}
|
|
230
249
|
else if (input != null)
|
|
@@ -236,7 +255,9 @@ function parseResponses(body) {
|
|
|
236
255
|
stream: raw.stream === true,
|
|
237
256
|
messages,
|
|
238
257
|
tools: responseTools(raw.tools),
|
|
239
|
-
toolChoice: raw.tool_choice
|
|
258
|
+
toolChoice: isRecord(raw.tool_choice) && raw.tool_choice.type === "function"
|
|
259
|
+
? { type: "function", function: { name: raw.tool_choice.name } }
|
|
260
|
+
: raw.tool_choice,
|
|
240
261
|
maxTokens: numberValue(raw.max_output_tokens),
|
|
241
262
|
temperature: numberValue(raw.temperature),
|
|
242
263
|
topP: numberValue(raw.top_p),
|
|
@@ -257,15 +278,16 @@ function anthropicParts(value) {
|
|
|
257
278
|
for (const item of value) {
|
|
258
279
|
const part = record(item, "Anthropic content blocks must be objects");
|
|
259
280
|
if (part.type === "text")
|
|
260
|
-
content.push({ type: "text", text: stringValue(part.text) ?? "" });
|
|
281
|
+
content.push({ type: "text", text: stringValue(part.text) ?? "", ...cacheControl(part) });
|
|
261
282
|
else if (part.type === "image") {
|
|
262
283
|
const source = record(part.source, "Anthropic image requires source");
|
|
263
284
|
if (source.type === "url")
|
|
264
|
-
content.push({ type: "image", url: stringValue(source.url) ?? "" });
|
|
285
|
+
content.push({ type: "image", url: stringValue(source.url) ?? "", ...cacheControl(part) });
|
|
265
286
|
else
|
|
266
287
|
content.push({
|
|
267
288
|
type: "image",
|
|
268
289
|
url: `data:${stringValue(source.media_type) ?? "image/png"};base64,${stringValue(source.data) ?? ""}`,
|
|
290
|
+
...cacheControl(part),
|
|
269
291
|
});
|
|
270
292
|
}
|
|
271
293
|
else if (part.type === "tool_use") {
|
|
@@ -273,6 +295,7 @@ function anthropicParts(value) {
|
|
|
273
295
|
id: stringValue(part.id) ?? `call_${crypto.randomUUID()}`,
|
|
274
296
|
name: stringValue(part.name) ?? "function",
|
|
275
297
|
arguments: JSON.stringify(part.input ?? {}),
|
|
298
|
+
...cacheControl(part),
|
|
276
299
|
});
|
|
277
300
|
}
|
|
278
301
|
else if (part.type !== "thinking" && part.type !== "redacted_thinking") {
|
|
@@ -301,6 +324,7 @@ function parseAnthropic(body) {
|
|
|
301
324
|
role: "tool",
|
|
302
325
|
toolCallId: stringValue(part.tool_use_id),
|
|
303
326
|
content: anthropicParts(part.content).content,
|
|
327
|
+
...cacheControl(part),
|
|
304
328
|
});
|
|
305
329
|
}
|
|
306
330
|
else
|
|
@@ -319,7 +343,12 @@ function parseAnthropic(body) {
|
|
|
319
343
|
const name = stringValue(tool.name);
|
|
320
344
|
if (!name)
|
|
321
345
|
throw new CompatibilityError("Tool requires a name");
|
|
322
|
-
return {
|
|
346
|
+
return {
|
|
347
|
+
name,
|
|
348
|
+
description: stringValue(tool.description),
|
|
349
|
+
parameters: tool.input_schema,
|
|
350
|
+
...cacheControl(tool),
|
|
351
|
+
};
|
|
323
352
|
})
|
|
324
353
|
: undefined;
|
|
325
354
|
return {
|
|
@@ -327,17 +356,25 @@ function parseAnthropic(body) {
|
|
|
327
356
|
stream: raw.stream === true,
|
|
328
357
|
messages,
|
|
329
358
|
tools,
|
|
330
|
-
toolChoice: raw.tool_choice
|
|
359
|
+
toolChoice: isRecord(raw.tool_choice)
|
|
360
|
+
? raw.tool_choice.type === "tool"
|
|
361
|
+
? { type: "function", function: { name: raw.tool_choice.name } }
|
|
362
|
+
: raw.tool_choice.type === "any"
|
|
363
|
+
? "required"
|
|
364
|
+
: raw.tool_choice.type
|
|
365
|
+
: raw.tool_choice,
|
|
331
366
|
maxTokens: numberValue(raw.max_tokens),
|
|
332
367
|
temperature: numberValue(raw.temperature),
|
|
333
368
|
topP: numberValue(raw.top_p),
|
|
334
369
|
stop: raw.stop_sequences,
|
|
335
370
|
metadata: raw.metadata,
|
|
371
|
+
cacheControl: cacheControl(raw).cache_control,
|
|
336
372
|
};
|
|
337
373
|
}
|
|
338
374
|
function parseGoogle(body, model, stream = false) {
|
|
339
375
|
const raw = json(body);
|
|
340
376
|
const messages = [];
|
|
377
|
+
const pendingCalls = [];
|
|
341
378
|
if (isRecord(raw.systemInstruction)) {
|
|
342
379
|
const parts = Array.isArray(raw.systemInstruction.parts) ? raw.systemInstruction.parts : [];
|
|
343
380
|
messages.push({
|
|
@@ -370,16 +407,23 @@ function parseGoogle(body, model, stream = false) {
|
|
|
370
407
|
content.push({ type: "image", url: stringValue(part.fileData.fileUri) ?? "" });
|
|
371
408
|
}
|
|
372
409
|
else if (isRecord(part.functionCall)) {
|
|
373
|
-
|
|
374
|
-
id: `call_${crypto.randomUUID()}`,
|
|
410
|
+
const call = {
|
|
411
|
+
id: stringValue(part.functionCall.id) ?? `call_${crypto.randomUUID()}`,
|
|
375
412
|
name: stringValue(part.functionCall.name) ?? "function",
|
|
376
413
|
arguments: JSON.stringify(part.functionCall.args ?? {}),
|
|
377
|
-
}
|
|
414
|
+
};
|
|
415
|
+
toolCalls.push(call);
|
|
416
|
+
pendingCalls.push(call);
|
|
378
417
|
}
|
|
379
418
|
else if (isRecord(part.functionResponse)) {
|
|
419
|
+
const result = part.functionResponse;
|
|
420
|
+
const callIndex = pendingCalls.findIndex((call) => typeof result.id === "string" ? call.id === result.id : call.name === result.name);
|
|
421
|
+
const call = callIndex < 0 ? undefined : pendingCalls.splice(callIndex, 1)[0];
|
|
422
|
+
if (!call)
|
|
423
|
+
throw new CompatibilityError("Function response has no matching function call");
|
|
380
424
|
messages.push({
|
|
381
425
|
role: "tool",
|
|
382
|
-
toolCallId:
|
|
426
|
+
toolCallId: call.id,
|
|
383
427
|
content: [{ type: "text", text: JSON.stringify(part.functionResponse.response ?? {}) }],
|
|
384
428
|
});
|
|
385
429
|
}
|
|
@@ -420,15 +464,16 @@ function dataUri(url) {
|
|
|
420
464
|
return match?.[1] && match[2] ? { mediaType: match[1], data: match[2] } : null;
|
|
421
465
|
}
|
|
422
466
|
function chatContent(parts) {
|
|
423
|
-
if (parts.every((part) => part.type === "text"))
|
|
467
|
+
if (parts.every((part) => part.type === "text" && !part.cache_control))
|
|
424
468
|
return parts.map((part) => (part.type === "text" ? part.text : "")).join("");
|
|
425
469
|
return parts.map((part) => {
|
|
426
470
|
if (part.type === "text")
|
|
427
|
-
return { type: "text", text: part.text };
|
|
471
|
+
return { type: "text", text: part.text, ...cacheControl(part) };
|
|
428
472
|
if (part.type === "image")
|
|
429
473
|
return {
|
|
430
474
|
type: "image_url",
|
|
431
475
|
image_url: { url: part.url, ...(part.detail ? { detail: part.detail } : {}) },
|
|
476
|
+
...cacheControl(part),
|
|
432
477
|
};
|
|
433
478
|
if (part.type === "audio")
|
|
434
479
|
return {
|
|
@@ -447,6 +492,7 @@ function toChat(request) {
|
|
|
447
492
|
const messages = request.messages.map((message) => ({
|
|
448
493
|
role: message.role,
|
|
449
494
|
content: chatContent(message.content),
|
|
495
|
+
...cacheControl(message),
|
|
450
496
|
...(message.toolCallId ? { tool_call_id: message.toolCallId } : {}),
|
|
451
497
|
...(message.toolCalls
|
|
452
498
|
? {
|
|
@@ -463,7 +509,13 @@ function toChat(request) {
|
|
|
463
509
|
messages,
|
|
464
510
|
stream: false,
|
|
465
511
|
...(request.tools
|
|
466
|
-
? {
|
|
512
|
+
? {
|
|
513
|
+
tools: request.tools.map(({ cache_control, ...tool }) => ({
|
|
514
|
+
type: "function",
|
|
515
|
+
function: tool,
|
|
516
|
+
...(cache_control ? { cache_control } : {}),
|
|
517
|
+
})),
|
|
518
|
+
}
|
|
467
519
|
: {}),
|
|
468
520
|
...(request.toolChoice != null ? { tool_choice: request.toolChoice } : {}),
|
|
469
521
|
...(request.maxTokens != null ? { max_completion_tokens: request.maxTokens } : {}),
|
|
@@ -550,7 +602,12 @@ function toResponses(request) {
|
|
|
550
602
|
stream: false,
|
|
551
603
|
store: false,
|
|
552
604
|
...(request.tools
|
|
553
|
-
? {
|
|
605
|
+
? {
|
|
606
|
+
tools: request.tools.map(({ cache_control: _cacheControl, ...tool }) => ({
|
|
607
|
+
type: "function",
|
|
608
|
+
...tool,
|
|
609
|
+
})),
|
|
610
|
+
}
|
|
554
611
|
: {}),
|
|
555
612
|
...(toolChoice != null ? { tool_choice: toolChoice } : {}),
|
|
556
613
|
...(request.maxTokens != null ? { max_output_tokens: request.maxTokens } : {}),
|
|
@@ -566,12 +623,16 @@ function toResponses(request) {
|
|
|
566
623
|
function anthropicContent(message) {
|
|
567
624
|
const parts = message.content.map((part) => {
|
|
568
625
|
if (part.type === "text")
|
|
569
|
-
return { type: "text", text: part.text };
|
|
626
|
+
return { type: "text", text: part.text, ...cacheControl(part) };
|
|
570
627
|
if (part.type === "image") {
|
|
571
628
|
const data = dataUri(part.url);
|
|
572
629
|
return data
|
|
573
|
-
? {
|
|
574
|
-
|
|
630
|
+
? {
|
|
631
|
+
type: "image",
|
|
632
|
+
source: { type: "base64", media_type: data.mediaType, data: data.data },
|
|
633
|
+
...cacheControl(part),
|
|
634
|
+
}
|
|
635
|
+
: { type: "image", source: { type: "url", url: part.url }, ...cacheControl(part) };
|
|
575
636
|
}
|
|
576
637
|
if (part.type === "file")
|
|
577
638
|
return {
|
|
@@ -580,6 +641,7 @@ function anthropicContent(message) {
|
|
|
580
641
|
? { type: "file", file_id: part.fileId }
|
|
581
642
|
: { type: "base64", media_type: "application/octet-stream", data: part.data ?? "" },
|
|
582
643
|
...(part.filename ? { title: part.filename } : {}),
|
|
644
|
+
...cacheControl(part),
|
|
583
645
|
};
|
|
584
646
|
throw new CompatibilityError("Anthropic Messages does not support OpenAI input_audio", "unsupported_feature");
|
|
585
647
|
});
|
|
@@ -589,7 +651,10 @@ function anthropicContent(message) {
|
|
|
589
651
|
id: call.id,
|
|
590
652
|
name: call.name,
|
|
591
653
|
input: JSON.parse(call.arguments || "{}"),
|
|
654
|
+
...cacheControl(call),
|
|
592
655
|
});
|
|
656
|
+
if (message.role !== "tool" && message.cache_control && parts.length)
|
|
657
|
+
Object.assign(parts[parts.length - 1], cacheControl(message));
|
|
593
658
|
return parts;
|
|
594
659
|
}
|
|
595
660
|
function toAnthropic(request) {
|
|
@@ -606,6 +671,7 @@ function toAnthropic(request) {
|
|
|
606
671
|
type: "tool_result",
|
|
607
672
|
tool_use_id: message.toolCallId,
|
|
608
673
|
content: anthropicContent(message),
|
|
674
|
+
...cacheControl(message),
|
|
609
675
|
},
|
|
610
676
|
],
|
|
611
677
|
}
|
|
@@ -621,12 +687,14 @@ function toAnthropic(request) {
|
|
|
621
687
|
max_tokens: request.maxTokens ?? 4096,
|
|
622
688
|
stream: false,
|
|
623
689
|
...(system.length ? { system } : {}),
|
|
690
|
+
...(request.cacheControl ? { cache_control: request.cacheControl } : {}),
|
|
624
691
|
...(request.tools
|
|
625
692
|
? {
|
|
626
693
|
tools: request.tools.map((tool) => ({
|
|
627
694
|
name: tool.name,
|
|
628
695
|
description: tool.description,
|
|
629
696
|
input_schema: tool.parameters ?? { type: "object", properties: {} },
|
|
697
|
+
...cacheControl(tool),
|
|
630
698
|
})),
|
|
631
699
|
}
|
|
632
700
|
: {}),
|
|
@@ -657,6 +725,7 @@ function googlePart(part) {
|
|
|
657
725
|
: { inlineData: { mimeType: "application/octet-stream", data: part.data ?? "" } };
|
|
658
726
|
}
|
|
659
727
|
function toGoogle(request) {
|
|
728
|
+
const callNames = new Map(request.messages.flatMap((message) => (message.toolCalls ?? []).map((call) => [call.id, call.name])));
|
|
660
729
|
const system = request.messages
|
|
661
730
|
.filter((message) => message.role === "system" || message.role === "developer")
|
|
662
731
|
.flatMap((message) => message.content.map(googlePart));
|
|
@@ -666,15 +735,19 @@ function toGoogle(request) {
|
|
|
666
735
|
const parts = message.role === "tool" ? [] : message.content.map(googlePart);
|
|
667
736
|
for (const call of message.toolCalls ?? [])
|
|
668
737
|
parts.push({ functionCall: { name: call.name, args: JSON.parse(call.arguments || "{}") } });
|
|
669
|
-
if (message.role === "tool")
|
|
738
|
+
if (message.role === "tool") {
|
|
739
|
+
const name = message.toolCallId ? callNames.get(message.toolCallId) : undefined;
|
|
740
|
+
if (!name)
|
|
741
|
+
throw new CompatibilityError("Tool result has no matching function call");
|
|
670
742
|
parts.push({
|
|
671
743
|
functionResponse: {
|
|
672
|
-
name
|
|
744
|
+
name,
|
|
673
745
|
response: {
|
|
674
746
|
result: message.content.map((part) => (part.type === "text" ? part.text : part)),
|
|
675
747
|
},
|
|
676
748
|
},
|
|
677
749
|
});
|
|
750
|
+
}
|
|
678
751
|
return { role: message.role === "assistant" ? "model" : "user", parts };
|
|
679
752
|
});
|
|
680
753
|
const schema = isRecord(request.responseFormat) && isRecord(request.responseFormat.json_schema)
|
|
@@ -740,10 +813,17 @@ function parseChatResult(raw, model) {
|
|
|
740
813
|
finishReason: finish === "length" || finish === "tool_calls" || finish === "content_filter"
|
|
741
814
|
? finish
|
|
742
815
|
: "stop",
|
|
743
|
-
usage: usage(numberValue(details?.prompt_tokens), numberValue(details?.completion_tokens), numberValue(
|
|
816
|
+
usage: usage(numberValue(details?.prompt_tokens), numberValue(details?.completion_tokens), numberValue(details?.cached_tokens) ??
|
|
817
|
+
numberValue(details?.prompt_cache_hit_tokens) ??
|
|
818
|
+
numberValue(promptDetails?.cached_tokens), numberValue(completionDetails?.reasoning_tokens), numberValue(promptDetails?.cache_write_tokens) ??
|
|
819
|
+
numberValue(promptDetails?.cache_creation_input_tokens)),
|
|
744
820
|
};
|
|
745
821
|
}
|
|
746
822
|
function parseResponsesResult(raw, model) {
|
|
823
|
+
if (raw.status === "failed" || raw.error != null) {
|
|
824
|
+
const error = isRecord(raw.error) ? stringValue(raw.error.message) : undefined;
|
|
825
|
+
throw new CompatibilityError(error ?? "Provider response failed", "provider_error", 502);
|
|
826
|
+
}
|
|
747
827
|
const content = [];
|
|
748
828
|
const toolCalls = [];
|
|
749
829
|
let refusal;
|
|
@@ -783,13 +863,7 @@ function parseResponsesResult(raw, model) {
|
|
|
783
863
|
content,
|
|
784
864
|
toolCalls,
|
|
785
865
|
refusal,
|
|
786
|
-
finishReason: toolCalls.length
|
|
787
|
-
? "tool_calls"
|
|
788
|
-
: raw.status === "incomplete"
|
|
789
|
-
? "length"
|
|
790
|
-
: raw.status === "failed"
|
|
791
|
-
? "error"
|
|
792
|
-
: "stop",
|
|
866
|
+
finishReason: raw.status === "incomplete" ? "length" : toolCalls.length ? "tool_calls" : "stop",
|
|
793
867
|
usage: usage(numberValue(details?.input_tokens), numberValue(details?.output_tokens), numberValue(inputDetails?.cached_tokens), numberValue(outputDetails?.reasoning_tokens), numberValue(inputDetails?.cache_write_tokens)),
|
|
794
868
|
};
|
|
795
869
|
}
|
|
@@ -1085,11 +1159,7 @@ function responsesToChatStream(upstream, model) {
|
|
|
1085
1159
|
const toolIndexes = new Map();
|
|
1086
1160
|
const emit = (controller, delta, finishReason = null, rawUsage) => {
|
|
1087
1161
|
const usage = rawUsage
|
|
1088
|
-
? {
|
|
1089
|
-
prompt_tokens: rawUsage.input_tokens,
|
|
1090
|
-
completion_tokens: rawUsage.output_tokens,
|
|
1091
|
-
total_tokens: rawUsage.total_tokens,
|
|
1092
|
-
}
|
|
1162
|
+
? resultToChat(parseResponsesResult({ usage: rawUsage }, model)).usage
|
|
1093
1163
|
: undefined;
|
|
1094
1164
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
1095
1165
|
id: `chatcmpl_${stringValue(response.id) ?? crypto.randomUUID()}`,
|
|
@@ -1114,120 +1184,108 @@ function responsesToChatStream(upstream, model) {
|
|
|
1114
1184
|
start(controller);
|
|
1115
1185
|
finished = true;
|
|
1116
1186
|
const incomplete = response.status === "incomplete";
|
|
1117
|
-
emit(controller, {},
|
|
1187
|
+
emit(controller, {}, incomplete ? "length" : hasToolCalls ? "tool_calls" : "stop", isRecord(response.usage) ? response.usage : undefined);
|
|
1118
1188
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
1119
1189
|
};
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
let buffer = "";
|
|
1127
|
-
const handle = (raw) => {
|
|
1128
|
-
const data = raw
|
|
1129
|
-
.split(/\r?\n/)
|
|
1130
|
-
.filter((line) => line.startsWith("data:"))
|
|
1131
|
-
.map((line) => line.slice(5).trim())
|
|
1132
|
-
.join("\n");
|
|
1133
|
-
if (!data || data === "[DONE]")
|
|
1134
|
-
return;
|
|
1135
|
-
const event = JSON.parse(data);
|
|
1136
|
-
if (!isRecord(event))
|
|
1137
|
-
return;
|
|
1138
|
-
const completed = isRecord(event.response) ? event.response : undefined;
|
|
1139
|
-
if (event.type === "response.created" && completed)
|
|
1140
|
-
response = completed;
|
|
1141
|
-
if (event.type === "response.output_text.delta") {
|
|
1142
|
-
start(controller);
|
|
1143
|
-
emit(controller, { content: stringValue(event.delta) ?? "" });
|
|
1144
|
-
}
|
|
1145
|
-
if (event.type === "response.refusal.delta") {
|
|
1146
|
-
start(controller);
|
|
1147
|
-
emit(controller, { refusal: stringValue(event.delta) ?? "" });
|
|
1148
|
-
}
|
|
1149
|
-
const outputIndex = numberValue(event.output_index) ?? toolIndexes.size;
|
|
1150
|
-
if (event.type === "response.output_item.added" &&
|
|
1151
|
-
isRecord(event.item) &&
|
|
1152
|
-
event.item.type === "function_call") {
|
|
1153
|
-
start(controller);
|
|
1154
|
-
hasToolCalls = true;
|
|
1155
|
-
const index = toolIndexes.size;
|
|
1156
|
-
toolIndexes.set(outputIndex, index);
|
|
1157
|
-
emit(controller, {
|
|
1158
|
-
tool_calls: [
|
|
1159
|
-
{
|
|
1160
|
-
index,
|
|
1161
|
-
id: stringValue(event.item.call_id) ??
|
|
1162
|
-
stringValue(event.item.id) ??
|
|
1163
|
-
`call_${crypto.randomUUID()}`,
|
|
1164
|
-
type: "function",
|
|
1165
|
-
function: {
|
|
1166
|
-
name: stringValue(event.item.name) ?? "function",
|
|
1167
|
-
arguments: stringValue(event.item.arguments) ?? "",
|
|
1168
|
-
},
|
|
1169
|
-
},
|
|
1170
|
-
],
|
|
1171
|
-
});
|
|
1172
|
-
}
|
|
1173
|
-
if (event.type === "response.function_call_arguments.delta") {
|
|
1174
|
-
start(controller);
|
|
1175
|
-
hasToolCalls = true;
|
|
1176
|
-
emit(controller, {
|
|
1177
|
-
tool_calls: [
|
|
1178
|
-
{
|
|
1179
|
-
index: toolIndexes.get(outputIndex) ?? 0,
|
|
1180
|
-
function: { arguments: stringValue(event.delta) ?? "" },
|
|
1181
|
-
},
|
|
1182
|
-
],
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
1186
|
-
finish(controller, completed);
|
|
1187
|
-
}
|
|
1188
|
-
if (event.type === "response.failed" || event.type === "error") {
|
|
1189
|
-
const detail = isRecord(event.error)
|
|
1190
|
-
? stringValue(event.error.message)
|
|
1191
|
-
: "Provider stream failed";
|
|
1192
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
1193
|
-
error: {
|
|
1194
|
-
message: detail ?? "Provider stream failed",
|
|
1195
|
-
type: "provider_error",
|
|
1196
|
-
code: "provider_error",
|
|
1197
|
-
},
|
|
1198
|
-
})}\n\n`));
|
|
1199
|
-
finished = true;
|
|
1200
|
-
}
|
|
1201
|
-
};
|
|
1202
|
-
try {
|
|
1203
|
-
for (;;) {
|
|
1204
|
-
const { done, value } = await reader.read();
|
|
1205
|
-
buffer += decoder.decode(value, { stream: !done });
|
|
1206
|
-
for (let end = buffer.search(/\r?\n\r?\n/); end !== -1; end = buffer.search(/\r?\n\r?\n/)) {
|
|
1207
|
-
const raw = buffer.slice(0, end);
|
|
1208
|
-
buffer = buffer.slice(end).replace(/^\r?\n\r?\n/, "");
|
|
1209
|
-
handle(raw);
|
|
1210
|
-
}
|
|
1211
|
-
if (done)
|
|
1212
|
-
break;
|
|
1213
|
-
}
|
|
1214
|
-
if (buffer)
|
|
1215
|
-
handle(buffer);
|
|
1216
|
-
finish(controller);
|
|
1217
|
-
controller.close();
|
|
1218
|
-
}
|
|
1219
|
-
catch (error) {
|
|
1220
|
-
controller.error(error);
|
|
1221
|
-
}
|
|
1222
|
-
finally {
|
|
1223
|
-
reader.releaseLock();
|
|
1224
|
-
upstreamReader = undefined;
|
|
1225
|
-
}
|
|
1190
|
+
const decoder = new TextDecoder();
|
|
1191
|
+
let buffer = "";
|
|
1192
|
+
const body = upstream.body.pipeThrough(new TransformStream({
|
|
1193
|
+
transform(chunk, controller) {
|
|
1194
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
1195
|
+
consume(controller);
|
|
1226
1196
|
},
|
|
1227
|
-
|
|
1228
|
-
|
|
1197
|
+
flush(controller) {
|
|
1198
|
+
buffer += decoder.decode();
|
|
1199
|
+
consume(controller);
|
|
1200
|
+
if (buffer)
|
|
1201
|
+
handle(buffer, controller);
|
|
1202
|
+
if (!finished)
|
|
1203
|
+
throw new Error("Provider stream ended before a terminal response event");
|
|
1229
1204
|
},
|
|
1230
|
-
});
|
|
1205
|
+
}));
|
|
1206
|
+
const handle = (raw, controller) => {
|
|
1207
|
+
if (finished)
|
|
1208
|
+
return;
|
|
1209
|
+
const data = raw
|
|
1210
|
+
.split(/\r?\n/)
|
|
1211
|
+
.filter((line) => line.startsWith("data:"))
|
|
1212
|
+
.map((line) => line.slice(5).trim())
|
|
1213
|
+
.join("\n");
|
|
1214
|
+
if (!data || data === "[DONE]")
|
|
1215
|
+
return;
|
|
1216
|
+
const event = JSON.parse(data);
|
|
1217
|
+
if (!isRecord(event))
|
|
1218
|
+
return;
|
|
1219
|
+
const completed = isRecord(event.response) ? event.response : undefined;
|
|
1220
|
+
if (event.type === "response.created" && completed)
|
|
1221
|
+
response = completed;
|
|
1222
|
+
if (event.type === "response.output_text.delta") {
|
|
1223
|
+
start(controller);
|
|
1224
|
+
emit(controller, { content: stringValue(event.delta) ?? "" });
|
|
1225
|
+
}
|
|
1226
|
+
if (event.type === "response.refusal.delta") {
|
|
1227
|
+
start(controller);
|
|
1228
|
+
emit(controller, { refusal: stringValue(event.delta) ?? "" });
|
|
1229
|
+
}
|
|
1230
|
+
const outputIndex = numberValue(event.output_index) ?? toolIndexes.size;
|
|
1231
|
+
if (event.type === "response.output_item.added" &&
|
|
1232
|
+
isRecord(event.item) &&
|
|
1233
|
+
event.item.type === "function_call") {
|
|
1234
|
+
start(controller);
|
|
1235
|
+
hasToolCalls = true;
|
|
1236
|
+
const index = toolIndexes.size;
|
|
1237
|
+
toolIndexes.set(outputIndex, index);
|
|
1238
|
+
emit(controller, {
|
|
1239
|
+
tool_calls: [
|
|
1240
|
+
{
|
|
1241
|
+
index,
|
|
1242
|
+
id: stringValue(event.item.call_id) ??
|
|
1243
|
+
stringValue(event.item.id) ??
|
|
1244
|
+
`call_${crypto.randomUUID()}`,
|
|
1245
|
+
type: "function",
|
|
1246
|
+
function: {
|
|
1247
|
+
name: stringValue(event.item.name) ?? "function",
|
|
1248
|
+
arguments: stringValue(event.item.arguments) ?? "",
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
],
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
if (event.type === "response.function_call_arguments.delta") {
|
|
1255
|
+
start(controller);
|
|
1256
|
+
hasToolCalls = true;
|
|
1257
|
+
emit(controller, {
|
|
1258
|
+
tool_calls: [
|
|
1259
|
+
{
|
|
1260
|
+
index: toolIndexes.get(outputIndex) ?? 0,
|
|
1261
|
+
function: { arguments: stringValue(event.delta) ?? "" },
|
|
1262
|
+
},
|
|
1263
|
+
],
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
1267
|
+
finish(controller, completed);
|
|
1268
|
+
}
|
|
1269
|
+
if (event.type === "response.failed" || event.type === "error") {
|
|
1270
|
+
const failure = event.error ?? completed?.error;
|
|
1271
|
+
const detail = isRecord(failure) ? stringValue(failure.message) : stringValue(event.message);
|
|
1272
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
|
|
1273
|
+
error: {
|
|
1274
|
+
message: detail ?? "Provider stream failed",
|
|
1275
|
+
type: "provider_error",
|
|
1276
|
+
code: "provider_error",
|
|
1277
|
+
},
|
|
1278
|
+
})}\n\n`));
|
|
1279
|
+
finished = true;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
function consume(controller) {
|
|
1283
|
+
for (let end = buffer.search(/\r?\n\r?\n/); end !== -1; end = buffer.search(/\r?\n\r?\n/)) {
|
|
1284
|
+
const raw = buffer.slice(0, end);
|
|
1285
|
+
buffer = buffer.slice(end).replace(/^\r?\n\r?\n/, "");
|
|
1286
|
+
handle(raw, controller);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1231
1289
|
return new Response(body, {
|
|
1232
1290
|
headers: {
|
|
1233
1291
|
"content-type": "text/event-stream; charset=utf-8",
|
|
@@ -1367,7 +1425,7 @@ async function upstreamResult(response, protocol, model) {
|
|
|
1367
1425
|
const body = await response.text();
|
|
1368
1426
|
let raw;
|
|
1369
1427
|
if (protocol === "responses" && /(^|\n)data:/.test(body)) {
|
|
1370
|
-
let completed
|
|
1428
|
+
let completed;
|
|
1371
1429
|
let outputText = "";
|
|
1372
1430
|
const calls = new Map();
|
|
1373
1431
|
for (const frame of body.split(/\r?\n\r?\n/)) {
|
|
@@ -1400,11 +1458,22 @@ async function upstreamResult(response, protocol, model) {
|
|
|
1400
1458
|
if (call)
|
|
1401
1459
|
call.arguments += stringValue(event.delta) ?? "";
|
|
1402
1460
|
}
|
|
1403
|
-
if (event.type === "response.completed"
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1461
|
+
if ((event.type === "response.completed" || event.type === "response.incomplete") &&
|
|
1462
|
+
isRecord(event.response))
|
|
1463
|
+
completed = {
|
|
1464
|
+
...event.response,
|
|
1465
|
+
status: event.type === "response.incomplete" ? "incomplete" : event.response.status,
|
|
1466
|
+
};
|
|
1467
|
+
if (event.type === "response.failed" || event.type === "error") {
|
|
1468
|
+
const failure = event.error ?? (isRecord(event.response) ? event.response.error : undefined);
|
|
1469
|
+
const message = isRecord(failure)
|
|
1470
|
+
? stringValue(failure.message)
|
|
1471
|
+
: stringValue(event.message);
|
|
1472
|
+
throw new CompatibilityError(message ?? "Provider stream failed", "provider_error", 502);
|
|
1473
|
+
}
|
|
1407
1474
|
}
|
|
1475
|
+
if (!completed)
|
|
1476
|
+
throw new CompatibilityError("Provider stream ended before a terminal response event", "provider_error", 502);
|
|
1408
1477
|
const output = Array.isArray(completed.output) ? [...completed.output] : [];
|
|
1409
1478
|
if (outputText && !output.some((item) => isRecord(item) && item.type === "message")) {
|
|
1410
1479
|
output.push({
|