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.
@@ -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 = stringValue(value.text);
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 { name, description: stringValue(tool.description), parameters: tool.input_schema };
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
- toolCalls.push({
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: stringValue(part.functionResponse.name),
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
- ? { tools: request.tools.map((tool) => ({ type: "function", function: tool })) }
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
- ? { tools: request.tools.map((tool) => ({ type: "function", ...tool })) }
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
- ? { type: "image", source: { type: "base64", media_type: data.mediaType, data: data.data } }
574
- : { type: "image", source: { type: "url", url: part.url } };
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: message.toolCallId ?? "function",
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(promptDetails?.cached_tokens), numberValue(completionDetails?.reasoning_tokens), numberValue(promptDetails?.cache_write_tokens)),
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, {}, hasToolCalls ? "tool_calls" : incomplete ? "length" : "stop", isRecord(response.usage) ? response.usage : undefined);
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
- let upstreamReader;
1121
- const body = new ReadableStream({
1122
- async start(controller) {
1123
- const reader = upstream.body.getReader();
1124
- upstreamReader = reader;
1125
- const decoder = new TextDecoder();
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
- cancel(reason) {
1228
- return upstreamReader?.cancel(reason);
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" && isRecord(event.response))
1404
- completed = event.response;
1405
- if ((event.type === "response.failed" || event.type === "error") && isRecord(event.response))
1406
- completed = event.response;
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({