@veryfront/ext-llm-openai 0.1.1186 → 0.1.1189

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.
Files changed (39) hide show
  1. package/README.md +49 -2
  2. package/esm/_dnt.polyfills.d.ts +12 -0
  3. package/esm/_dnt.polyfills.d.ts.map +1 -0
  4. package/esm/_dnt.polyfills.js +15 -0
  5. package/esm/index.d.ts +1 -0
  6. package/esm/index.d.ts.map +1 -1
  7. package/esm/index.js +7 -1
  8. package/esm/openai-chat-request-builder.d.ts +5 -43
  9. package/esm/openai-chat-request-builder.d.ts.map +1 -1
  10. package/esm/openai-chat-request-builder.js +18 -4
  11. package/esm/openai-chat-stream.d.ts +8 -1
  12. package/esm/openai-chat-stream.d.ts.map +1 -1
  13. package/esm/openai-chat-stream.js +286 -104
  14. package/esm/openai-provider-options.d.ts +6 -0
  15. package/esm/openai-provider-options.d.ts.map +1 -0
  16. package/esm/openai-provider-options.js +14 -0
  17. package/esm/openai-provider.d.ts +6 -5
  18. package/esm/openai-provider.d.ts.map +1 -1
  19. package/esm/openai-provider.js +579 -129
  20. package/esm/openai-reasoning-models.d.ts +3 -6
  21. package/esm/openai-reasoning-models.d.ts.map +1 -1
  22. package/esm/openai-responses-request-builder.d.ts.map +1 -1
  23. package/esm/openai-responses-request-builder.js +240 -19
  24. package/esm/openai-responses-stream.d.ts +12 -1
  25. package/esm/openai-responses-stream.d.ts.map +1 -1
  26. package/esm/openai-responses-stream.js +1053 -113
  27. package/esm/openai-sse-buffer.d.ts +8 -0
  28. package/esm/openai-sse-buffer.d.ts.map +1 -0
  29. package/esm/openai-sse-buffer.js +47 -0
  30. package/esm/openai-stream-metadata.d.ts +6 -0
  31. package/esm/openai-stream-metadata.d.ts.map +1 -0
  32. package/esm/openai-stream-metadata.js +10 -0
  33. package/esm/openai-tool-input.d.ts +10 -0
  34. package/esm/openai-tool-input.d.ts.map +1 -0
  35. package/esm/openai-tool-input.js +30 -0
  36. package/esm/openai-web-search.d.ts +35 -0
  37. package/esm/openai-web-search.d.ts.map +1 -0
  38. package/esm/openai-web-search.js +369 -0
  39. package/package.json +3 -3
@@ -9,12 +9,16 @@
9
9
  */
10
10
  import { buildProviderError, createOpenAIRequestInit, createWarningCollector, getOpenAIChatCompletionsUrl, getOpenAIEmbeddingUrl, getOpenAIResponsesUrl, isNumberArray, mergeUsage, parseRetryAfterMs, ProviderError, ProviderOverloadedError, ProviderQuotaError, ProviderRateLimitError, ProviderRequestError, readGatewayBillingMode, readRecord, requestJson, requestStream, stringifyJsonValue, TOOL_INPUT_PENDING_THRESHOLD_MS, } from "veryfront/provider/shared";
11
11
  import { buildOpenAIChatRequest, } from "./openai-chat-request-builder.js";
12
- import { streamOpenAICompatibleParts } from "./openai-chat-stream.js";
12
+ import { MAX_OPENAI_STREAM_TOOL_CALLS, streamOpenAICompatibleParts } from "./openai-chat-stream.js";
13
13
  import { buildOpenAIResponsesRequest } from "./openai-responses-request-builder.js";
14
14
  import { isOpenAIReasoningModel } from "./openai-reasoning-models.js";
15
+ import { isBoundedOpenAIStreamString, MAX_OPENAI_STREAM_IDENTIFIER_BYTES, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES, MAX_OPENAI_STREAM_TOOL_NAME_BYTES, } from "./openai-stream-metadata.js";
15
16
  import { extractOpenAIResponsesUsage, normalizeOpenAIResponsesFinishReason, streamOpenAIResponsesParts, } from "./openai-responses-stream.js";
17
+ import { isJsonObjectText, MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES } from "./openai-tool-input.js";
18
+ import { createOpenAIRawResponseMetadata, MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES, MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS, normalizeOpenAIWebSearchCall, readOpenAIRawResponseOutputItems, resolveOpenAIWebSearchDescriptor, validateOpenAIUrlCitation, } from "./openai-web-search.js";
16
19
  // Re-export error classes so extension tests can import from this module.
17
20
  export { buildProviderError, isNumberArray, mergeUsage, parseRetryAfterMs, ProviderError, ProviderOverloadedError, ProviderQuotaError, ProviderRateLimitError, ProviderRequestError, TOOL_INPUT_PENDING_THRESHOLD_MS, };
21
+ const OPENAI_RESPONSE_TEXT_ENCODER = new TextEncoder();
18
22
  function readNonEmptyString(value) {
19
23
  return typeof value === "string" && value.length > 0 ? value : undefined;
20
24
  }
@@ -27,31 +31,92 @@ function getRuntimeOpenAIProviderName(config) {
27
31
  function getLLMOpenAIProviderName(config) {
28
32
  return readNonEmptyString(config.providerName) ?? "openai";
29
33
  }
34
+ function getOpenAICompatibleProviderKind(config) {
35
+ const providerName = getRuntimeOpenAIProviderName(config).trim().toLowerCase();
36
+ return providerName === "mistral" || providerName === "moonshotai" ? providerName : "openai";
37
+ }
38
+ function invalidOpenAIResponse(context, issue) {
39
+ return new ProviderRequestError({
40
+ provider: context.providerKind,
41
+ status: 200,
42
+ message: `${context.providerLabel} request failed: invalid successful response (${issue})`,
43
+ retryable: false,
44
+ });
45
+ }
46
+ function sanitizeRuntimeUsage(usage) {
47
+ const normalized = mergeUsage(undefined, usage);
48
+ return normalized && Object.keys(normalized).length > 0 ? normalized : undefined;
49
+ }
50
+ function readUsageTokenCount(value) {
51
+ return typeof value === "number" &&
52
+ Number.isFinite(value) &&
53
+ Number.isSafeInteger(value) &&
54
+ value >= 0
55
+ ? value
56
+ : undefined;
57
+ }
30
58
  // ---------------------------------------------------------------------------
31
59
  // Embedding helpers
32
60
  // ---------------------------------------------------------------------------
33
- function extractOpenAIEmbeddings(payload) {
61
+ function extractOpenAIEmbeddings(payload, expectedCount, context) {
34
62
  const record = readRecord(payload);
35
63
  const data = record?.data;
36
64
  if (!Array.isArray(data)) {
37
- throw new Error("Invalid OpenAI embedding response: data array missing");
65
+ throw invalidOpenAIResponse(context, "embedding data array missing");
66
+ }
67
+ if (data.length !== expectedCount) {
68
+ throw invalidOpenAIResponse(context, `expected ${expectedCount} embedding vectors but received ${data.length}`);
38
69
  }
39
- const embeddings = [];
70
+ const positionalEmbeddings = [];
71
+ const indexedEmbeddings = Array(expectedCount);
72
+ let indexMode;
73
+ let dimensions;
40
74
  for (const item of data) {
41
75
  const itemRecord = readRecord(item);
42
76
  const embedding = itemRecord?.embedding;
43
- if (!isNumberArray(embedding)) {
44
- throw new Error("Invalid OpenAI embedding response: embedding vector missing");
77
+ if (!isNumberArray(embedding) || embedding.length === 0) {
78
+ throw invalidOpenAIResponse(context, "embedding vector missing or invalid");
79
+ }
80
+ dimensions ??= embedding.length;
81
+ if (embedding.length !== dimensions) {
82
+ throw invalidOpenAIResponse(context, "embedding vectors had inconsistent dimensions");
83
+ }
84
+ const rawIndex = itemRecord?.index;
85
+ const itemIndexMode = rawIndex === undefined ? "positional" : "indexed";
86
+ if (indexMode !== undefined && indexMode !== itemIndexMode) {
87
+ throw invalidOpenAIResponse(context, "embedding indices were inconsistently provided");
88
+ }
89
+ indexMode = itemIndexMode;
90
+ if (itemIndexMode === "positional") {
91
+ positionalEmbeddings.push(embedding);
92
+ continue;
93
+ }
94
+ if (typeof rawIndex !== "number" ||
95
+ !Number.isSafeInteger(rawIndex) ||
96
+ rawIndex < 0 ||
97
+ rawIndex >= expectedCount) {
98
+ throw invalidOpenAIResponse(context, "embedding index was invalid");
45
99
  }
46
- embeddings.push(embedding);
100
+ if (indexedEmbeddings[rawIndex] !== undefined) {
101
+ throw invalidOpenAIResponse(context, "embedding indices contained a duplicate");
102
+ }
103
+ indexedEmbeddings[rawIndex] = embedding;
104
+ }
105
+ if (indexMode !== "indexed") {
106
+ return positionalEmbeddings;
47
107
  }
48
- return embeddings;
108
+ return indexedEmbeddings.map((embedding) => {
109
+ if (embedding === undefined) {
110
+ throw invalidOpenAIResponse(context, "embedding indices were incomplete");
111
+ }
112
+ return embedding;
113
+ });
49
114
  }
50
115
  function extractOpenAIUsageTokens(payload) {
51
116
  const record = readRecord(payload);
52
117
  const usage = readRecord(record?.usage);
53
118
  const totalTokens = usage?.total_tokens;
54
- return typeof totalTokens === "number" ? totalTokens : undefined;
119
+ return readUsageTokenCount(totalTokens);
55
120
  }
56
121
  // ---------------------------------------------------------------------------
57
122
  // Chat helpers
@@ -131,38 +196,94 @@ function extractOpenAIUsage(payload) {
131
196
  : {}),
132
197
  };
133
198
  }
134
- function extractOpenAIContentText(content) {
199
+ function extractOpenAIContentText(content, context) {
135
200
  if (typeof content === "string") {
201
+ if (OPENAI_RESPONSE_TEXT_ENCODER.encode(content).byteLength >
202
+ MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES) {
203
+ throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
204
+ }
136
205
  return content;
137
206
  }
138
207
  if (!Array.isArray(content)) {
139
208
  return "";
140
209
  }
141
- let text = "";
210
+ if (content.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
211
+ throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS} parts`);
212
+ }
213
+ const textParts = [];
214
+ let retainedBytes = 0;
142
215
  for (const part of content) {
143
216
  const record = readRecord(part);
217
+ if (!record) {
218
+ throw invalidOpenAIResponse(context, "message content part was not an object");
219
+ }
144
220
  const type = record?.type;
145
- if (type === "text" && typeof record?.text === "string") {
146
- text += record.text;
221
+ let value;
222
+ if (type === "text") {
223
+ if (typeof record.text !== "string") {
224
+ throw invalidOpenAIResponse(context, "text content part was malformed");
225
+ }
226
+ value = record.text;
227
+ }
228
+ else if (type === "refusal") {
229
+ if (typeof record.refusal !== "string") {
230
+ throw invalidOpenAIResponse(context, "refusal content part was malformed");
231
+ }
232
+ value = record.refusal;
233
+ }
234
+ else {
235
+ throw invalidOpenAIResponse(context, "message content part type was unsupported");
147
236
  }
237
+ const valueBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(value).byteLength;
238
+ if (valueBytes >
239
+ MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES - retainedBytes) {
240
+ throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
241
+ }
242
+ retainedBytes += valueBytes;
243
+ textParts.push(value);
148
244
  }
149
- return text;
245
+ return textParts.join("");
150
246
  }
151
- function extractOpenAIToolCalls(message) {
247
+ function extractOpenAIToolCalls(message, context) {
152
248
  const toolCalls = message.tool_calls;
153
- if (!Array.isArray(toolCalls)) {
249
+ if (toolCalls === undefined || toolCalls === null) {
154
250
  return [];
155
251
  }
252
+ if (!Array.isArray(toolCalls)) {
253
+ throw invalidOpenAIResponse(context, "message tool_calls was not an array");
254
+ }
255
+ if (toolCalls.length > MAX_OPENAI_STREAM_TOOL_CALLS) {
256
+ throw invalidOpenAIResponse(context, `message exceeded ${MAX_OPENAI_STREAM_TOOL_CALLS} tool calls`);
257
+ }
156
258
  const normalized = [];
259
+ const seenToolCallIds = new Set();
260
+ let retainedArgumentBytes = 0;
157
261
  for (const entry of toolCalls) {
158
262
  const record = readRecord(entry);
159
- const id = typeof record?.id === "string" ? record.id : undefined;
263
+ const id = isBoundedOpenAIStreamString(record?.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
264
+ ? record.id
265
+ : undefined;
160
266
  const fn = readRecord(record?.function);
161
- const name = typeof fn?.name === "string" ? fn.name : undefined;
267
+ const name = isBoundedOpenAIStreamString(fn?.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
268
+ ? fn.name
269
+ : undefined;
162
270
  const argumentsText = typeof fn?.arguments === "string" ? fn.arguments : undefined;
163
- if (!id || !name || argumentsText === undefined) {
164
- continue;
271
+ if (record?.type !== "function" || !id || !name || argumentsText === undefined) {
272
+ throw invalidOpenAIResponse(context, "message contained a malformed tool call");
273
+ }
274
+ if (seenToolCallIds.has(id)) {
275
+ throw invalidOpenAIResponse(context, "message contained duplicate tool call ids");
276
+ }
277
+ const argumentBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(argumentsText).byteLength;
278
+ if (argumentBytes >
279
+ MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES - retainedArgumentBytes) {
280
+ throw invalidOpenAIResponse(context, `message tool call arguments exceeded ${MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES} UTF-8 bytes`);
165
281
  }
282
+ if (!isJsonObjectText(argumentsText)) {
283
+ throw invalidOpenAIResponse(context, "message tool call arguments were not valid JSON object text");
284
+ }
285
+ retainedArgumentBytes += argumentBytes;
286
+ seenToolCallIds.add(id);
166
287
  normalized.push({
167
288
  toolCallId: id,
168
289
  toolName: name,
@@ -171,99 +292,386 @@ function extractOpenAIToolCalls(message) {
171
292
  }
172
293
  return normalized;
173
294
  }
174
- function extractFirstChoice(payload) {
295
+ function extractFirstChoice(payload, context) {
175
296
  const record = readRecord(payload);
176
297
  const choices = record?.choices;
177
298
  if (!Array.isArray(choices) || choices.length === 0) {
178
- return undefined;
299
+ throw invalidOpenAIResponse(context, "choices array missing or empty");
179
300
  }
180
301
  const first = readRecord(choices[0]);
181
302
  if (!first) {
182
- return undefined;
303
+ throw invalidOpenAIResponse(context, "first choice was not an object");
183
304
  }
184
305
  return first;
185
306
  }
186
- function buildOpenAIGenerateResult(payload) {
187
- const choice = extractFirstChoice(payload);
188
- const message = readRecord(choice?.message);
189
- const text = extractOpenAIContentText(message?.content);
190
- const toolCalls = message ? extractOpenAIToolCalls(message) : [];
307
+ function buildOpenAIGenerateResult(payload, context) {
308
+ const choice = extractFirstChoice(payload, context);
309
+ const message = readRecord(choice.message);
310
+ if (!message) {
311
+ throw invalidOpenAIResponse(context, "choice message missing");
312
+ }
313
+ if (message.role !== "assistant") {
314
+ throw invalidOpenAIResponse(context, "choice message role was not assistant");
315
+ }
316
+ if (message.content !== undefined &&
317
+ message.content !== null &&
318
+ typeof message.content !== "string" &&
319
+ !Array.isArray(message.content)) {
320
+ throw invalidOpenAIResponse(context, "message content had an invalid type");
321
+ }
322
+ if (message.refusal !== undefined &&
323
+ message.refusal !== null &&
324
+ typeof message.refusal !== "string") {
325
+ throw invalidOpenAIResponse(context, "message refusal had an invalid type");
326
+ }
327
+ if (typeof choice.finish_reason !== "string" || choice.finish_reason.length === 0) {
328
+ throw invalidOpenAIResponse(context, "choice finish reason missing");
329
+ }
330
+ const regularText = extractOpenAIContentText(message.content, context);
331
+ const refusalText = typeof message.refusal === "string" ? message.refusal : "";
332
+ const regularTextBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(regularText).byteLength;
333
+ const refusalTextBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(refusalText).byteLength;
334
+ if (refusalTextBytes >
335
+ MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES - regularTextBytes) {
336
+ throw invalidOpenAIResponse(context, `message content exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
337
+ }
338
+ const text = regularText + refusalText;
339
+ const toolCalls = extractOpenAIToolCalls(message, context);
340
+ if ((choice.finish_reason === "tool_calls") !== (toolCalls.length > 0)) {
341
+ throw invalidOpenAIResponse(context, "choice finish reason and tool calls were inconsistent");
342
+ }
343
+ const content = [
344
+ ...(text.length > 0 ? [{ type: "text", text }] : []),
345
+ ...toolCalls.map((toolCall) => ({
346
+ type: "tool-call",
347
+ toolCallId: toolCall.toolCallId,
348
+ toolName: toolCall.toolName,
349
+ input: toolCall.input,
350
+ })),
351
+ ];
191
352
  return {
192
- content: [
193
- ...(text.length > 0 ? [{ type: "text", text }] : []),
194
- ...toolCalls.map((toolCall) => ({
195
- type: "tool-call",
196
- toolCallId: toolCall.toolCallId,
197
- toolName: toolCall.toolName,
198
- input: toolCall.input,
199
- })),
200
- ],
353
+ content,
201
354
  finishReason: normalizeOpenAIFinishReason(choice?.finish_reason),
202
- usage: extractOpenAIUsage(payload),
355
+ usage: sanitizeRuntimeUsage(extractOpenAIUsage(payload)),
203
356
  };
204
357
  }
205
- function buildOpenAIResponsesGenerateResult(payload) {
358
+ function assertTerminalOpenAIResponsesOutputItem(item, context) {
359
+ const validStatus = item.status === undefined ||
360
+ item.status === "completed" ||
361
+ item.status === "incomplete" ||
362
+ (item.type === "web_search_call" && item.status === "failed");
363
+ if (!validStatus) {
364
+ throw invalidOpenAIResponse(context, "output item status was unsupported or nonterminal");
365
+ }
366
+ }
367
+ function createValidatedOpenAIRawResponseMetadata(outputItems, context) {
368
+ const metadata = createOpenAIRawResponseMetadata(outputItems);
369
+ try {
370
+ readOpenAIRawResponseOutputItems(metadata);
371
+ }
372
+ catch {
373
+ throw invalidOpenAIResponse(context, "raw response output items were unsafe to replay");
374
+ }
375
+ return metadata;
376
+ }
377
+ function buildOpenAIResponsesGenerateResult(payload, context, webSearchToolName) {
206
378
  const record = readRecord(payload);
207
- const output = Array.isArray(record?.output) ? record.output : [];
379
+ if (!record) {
380
+ throw invalidOpenAIResponse(context, "response body was not an object");
381
+ }
382
+ if (typeof record.status !== "string" || record.status.length === 0) {
383
+ throw invalidOpenAIResponse(context, "response status missing");
384
+ }
385
+ if (record.status !== "completed" &&
386
+ record.status !== "incomplete" &&
387
+ record.status !== "failed") {
388
+ throw invalidOpenAIResponse(context, "response status was unsupported or nonterminal");
389
+ }
390
+ if (!Array.isArray(record.output)) {
391
+ throw invalidOpenAIResponse(context, "output array missing");
392
+ }
393
+ const output = record.output;
394
+ if (output.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
395
+ throw invalidOpenAIResponse(context, `output exceeded ${MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS} items`);
396
+ }
208
397
  const content = [];
398
+ const rawOutputItems = [];
399
+ const seenOutputItemIds = new Set();
400
+ const seenToolCallIds = new Set();
401
+ let normalizedContentBytes = 0;
402
+ function reserveNormalizedContent(value, issue) {
403
+ const valueBytes = OPENAI_RESPONSE_TEXT_ENCODER.encode(value).byteLength;
404
+ if (valueBytes > MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES -
405
+ normalizedContentBytes) {
406
+ throw invalidOpenAIResponse(context, `${issue} exceeded ${MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES} UTF-8 bytes`);
407
+ }
408
+ normalizedContentBytes += valueBytes;
409
+ }
209
410
  for (const item of output) {
210
411
  const itemRecord = readRecord(item);
211
- const itemType = typeof itemRecord?.type === "string" ? itemRecord.type : undefined;
212
- if (itemType === "message" && Array.isArray(itemRecord?.content)) {
412
+ if (!itemRecord) {
413
+ throw invalidOpenAIResponse(context, "output item was not an object");
414
+ }
415
+ rawOutputItems.push(itemRecord);
416
+ const itemType = isBoundedOpenAIStreamString(itemRecord.type, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES)
417
+ ? itemRecord.type
418
+ : undefined;
419
+ if (!itemType) {
420
+ throw invalidOpenAIResponse(context, "output item type missing");
421
+ }
422
+ assertTerminalOpenAIResponsesOutputItem(itemRecord, context);
423
+ if (!isBoundedOpenAIStreamString(itemRecord.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)) {
424
+ throw invalidOpenAIResponse(context, "output item id was malformed");
425
+ }
426
+ if (seenOutputItemIds.has(itemRecord.id)) {
427
+ throw invalidOpenAIResponse(context, "response contained duplicate output item ids");
428
+ }
429
+ seenOutputItemIds.add(itemRecord.id);
430
+ if (itemType === "message") {
431
+ if (itemRecord.role !== "assistant") {
432
+ throw invalidOpenAIResponse(context, "message output role was not assistant");
433
+ }
434
+ if (!Array.isArray(itemRecord.content)) {
435
+ throw invalidOpenAIResponse(context, "message output content was not an array");
436
+ }
437
+ if (itemRecord.content.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
438
+ throw invalidOpenAIResponse(context, "message output contained too many content parts");
439
+ }
213
440
  // A message item bundles one or more output_text parts.
214
- let text = "";
441
+ const textParts = [];
215
442
  for (const part of itemRecord.content) {
216
443
  const p = readRecord(part);
217
- if (typeof p?.type === "string" && p.type === "output_text" && typeof p.text === "string") {
218
- text += p.text;
444
+ if (!p) {
445
+ throw invalidOpenAIResponse(context, "message content part was not an object");
446
+ }
447
+ if (p.type === "output_text") {
448
+ if (typeof p.text !== "string") {
449
+ throw invalidOpenAIResponse(context, "output-text content part was malformed");
450
+ }
451
+ if (p.annotations !== undefined) {
452
+ if (!Array.isArray(p.annotations) ||
453
+ p.annotations.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
454
+ throw invalidOpenAIResponse(context, "output-text annotations were malformed");
455
+ }
456
+ for (const annotation of p.annotations) {
457
+ const citation = validateOpenAIUrlCitation(annotation, (issue) => invalidOpenAIResponse(context, issue));
458
+ if (citation.end_index > p.text.length) {
459
+ throw invalidOpenAIResponse(context, "URL citation annotation range exceeded output text");
460
+ }
461
+ }
462
+ }
463
+ reserveNormalizedContent(p.text, "normalized response content");
464
+ textParts.push(p.text);
465
+ }
466
+ else if (p.type === "refusal") {
467
+ if (typeof p.refusal !== "string" ||
468
+ p.annotations !== undefined) {
469
+ throw invalidOpenAIResponse(context, "refusal content part was malformed");
470
+ }
471
+ reserveNormalizedContent(p.refusal, "normalized response content");
472
+ textParts.push(p.refusal);
473
+ }
474
+ else {
475
+ throw invalidOpenAIResponse(context, "message content part type was unsupported");
219
476
  }
220
477
  }
478
+ const text = textParts.join("");
221
479
  if (text.length > 0) {
222
480
  content.push({ type: "text", text });
223
481
  }
224
482
  continue;
225
483
  }
226
484
  if (itemType === "function_call") {
485
+ const toolCallId = isBoundedOpenAIStreamString(itemRecord.call_id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
486
+ ? itemRecord.call_id
487
+ : undefined;
488
+ const toolName = isBoundedOpenAIStreamString(itemRecord.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
489
+ ? itemRecord.name
490
+ : undefined;
491
+ if (!toolCallId ||
492
+ !toolName ||
493
+ !isBoundedOpenAIStreamString(itemRecord.arguments, MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES)) {
494
+ throw invalidOpenAIResponse(context, "function call output item was malformed");
495
+ }
496
+ if (seenToolCallIds.has(toolCallId)) {
497
+ throw invalidOpenAIResponse(context, "response contained duplicate function call ids");
498
+ }
499
+ if (!isJsonObjectText(itemRecord.arguments)) {
500
+ throw invalidOpenAIResponse(context, "function call arguments were not valid JSON object text");
501
+ }
502
+ reserveNormalizedContent(itemRecord.arguments, "normalized response content");
227
503
  content.push({
228
504
  type: "tool-call",
229
- toolCallId: typeof itemRecord?.call_id === "string"
230
- ? itemRecord.call_id
231
- : (typeof itemRecord?.id === "string" ? itemRecord.id : ""),
232
- toolName: typeof itemRecord?.name === "string" ? itemRecord.name : "",
233
- input: typeof itemRecord?.arguments === "string"
234
- ? itemRecord.arguments
235
- : stringifyJsonValue(itemRecord?.arguments ?? {}),
505
+ toolCallId,
506
+ toolName,
507
+ input: itemRecord.arguments,
236
508
  });
509
+ seenToolCallIds.add(toolCallId);
237
510
  continue;
238
511
  }
239
- if (itemType === "reasoning") {
240
- const summary = Array.isArray(itemRecord?.summary) ? itemRecord.summary : [];
241
- const summaries = [];
242
- for (const s of summary) {
243
- const sr = readRecord(s);
244
- if (typeof sr?.text === "string" && sr.text.length > 0) {
245
- summaries.push({
246
- ...(typeof sr?.id === "string" ? { id: sr.id } : {}),
247
- text: sr.text,
248
- });
249
- }
512
+ if (itemType === "web_search_call") {
513
+ if (!webSearchToolName) {
514
+ throw invalidOpenAIResponse(context, "provider emitted web search without a configured web-search tool");
250
515
  }
516
+ const webSearch = normalizeOpenAIWebSearchCall(itemRecord, (issue) => invalidOpenAIResponse(context, issue));
517
+ if (seenToolCallIds.has(webSearch.id)) {
518
+ throw invalidOpenAIResponse(context, "response contained duplicate tool call ids");
519
+ }
520
+ reserveNormalizedContent(webSearch.input, "normalized response content");
521
+ reserveNormalizedContent(stringifyJsonValue(webSearch.result), "normalized response content");
251
522
  content.push({
252
- type: "reasoning",
253
- ...(summaries.length > 0 ? { summaries } : {}),
254
- ...(typeof itemRecord?.encrypted_content === "string"
255
- ? { signature: itemRecord.encrypted_content }
256
- : {}),
523
+ type: "tool-call",
524
+ toolCallId: webSearch.id,
525
+ toolName: webSearchToolName,
526
+ input: webSearch.input,
527
+ providerExecuted: true,
257
528
  });
529
+ content.push({
530
+ type: "tool-result",
531
+ toolCallId: webSearch.id,
532
+ toolName: webSearchToolName,
533
+ result: webSearch.result,
534
+ ...(webSearch.isError ? { isError: true } : {}),
535
+ providerExecuted: true,
536
+ });
537
+ seenToolCallIds.add(webSearch.id);
258
538
  continue;
259
539
  }
540
+ if (itemType === "reasoning") {
541
+ if ((itemRecord.summary !== undefined && !Array.isArray(itemRecord.summary)) ||
542
+ (itemRecord.content !== undefined && !Array.isArray(itemRecord.content))) {
543
+ throw invalidOpenAIResponse(context, "reasoning summary or content was not an array");
544
+ }
545
+ const summary = Array.isArray(itemRecord.summary) ? itemRecord.summary : [];
546
+ const reasoningContent = Array.isArray(itemRecord.content) ? itemRecord.content : [];
547
+ if (summary.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS ||
548
+ reasoningContent.length > MAX_OPENAI_RAW_RESPONSE_OUTPUT_ITEMS) {
549
+ throw invalidOpenAIResponse(context, "reasoning contained too many parts");
550
+ }
551
+ const reasoningText = [];
552
+ for (const [parts, expectedType] of [
553
+ [summary, "summary_text"],
554
+ [reasoningContent, "reasoning_text"],
555
+ ]) {
556
+ for (const rawPart of parts) {
557
+ const part = readRecord(rawPart);
558
+ if (!part ||
559
+ part.type !== expectedType ||
560
+ typeof part.text !== "string" ||
561
+ (part.id !== undefined &&
562
+ !isBoundedOpenAIStreamString(part.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES))) {
563
+ throw invalidOpenAIResponse(context, expectedType === "summary_text"
564
+ ? "reasoning summary item was malformed"
565
+ : "reasoning content item was malformed");
566
+ }
567
+ if (part.text.length > 0) {
568
+ reserveNormalizedContent(part.text, "normalized response content");
569
+ reasoningText.push(part.text);
570
+ }
571
+ }
572
+ }
573
+ if (itemRecord.encrypted_content !== undefined &&
574
+ (typeof itemRecord.encrypted_content !== "string" ||
575
+ OPENAI_RESPONSE_TEXT_ENCODER.encode(itemRecord.encrypted_content).byteLength >
576
+ MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES)) {
577
+ throw invalidOpenAIResponse(context, "reasoning encrypted content was malformed");
578
+ }
579
+ const signature = typeof itemRecord.encrypted_content === "string"
580
+ ? itemRecord.encrypted_content
581
+ : undefined;
582
+ if (signature !== undefined) {
583
+ reserveNormalizedContent(signature, "normalized response content");
584
+ }
585
+ if (reasoningText.length > 0 || signature !== undefined) {
586
+ content.push({
587
+ type: "reasoning",
588
+ ...(reasoningText.length > 0 ? { text: reasoningText.join("") } : {}),
589
+ ...(signature !== undefined ? { signature } : {}),
590
+ });
591
+ }
592
+ continue;
593
+ }
594
+ throw invalidOpenAIResponse(context, "output item type was unsupported");
260
595
  }
261
596
  return {
262
597
  content,
263
598
  finishReason: normalizeOpenAIResponsesFinishReason(record?.status),
264
- usage: extractOpenAIResponsesUsage(payload),
599
+ usage: sanitizeRuntimeUsage(extractOpenAIResponsesUsage(payload)),
600
+ ...(rawOutputItems.length > 0
601
+ ? {
602
+ providerMetadata: createValidatedOpenAIRawResponseMetadata(rawOutputItems, context),
603
+ }
604
+ : {}),
605
+ };
606
+ }
607
+ function createOpenAIProviderAbortScope(callerSignal) {
608
+ const controller = new AbortController();
609
+ const abortFromCaller = () => controller.abort(callerSignal?.reason);
610
+ if (callerSignal?.aborted) {
611
+ abortFromCaller();
612
+ return { controller, dispose() { } };
613
+ }
614
+ callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
615
+ return {
616
+ controller,
617
+ dispose() {
618
+ callerSignal?.removeEventListener("abort", abortFromCaller);
619
+ },
265
620
  };
266
621
  }
622
+ function createCancelableOpenAIProviderStream(iterable, providerAbortController, disposeAbortScope) {
623
+ const iterator = iterable[Symbol.asyncIterator]();
624
+ let consumerCanceled = false;
625
+ let disposed = false;
626
+ const dispose = () => {
627
+ if (disposed)
628
+ return;
629
+ disposed = true;
630
+ disposeAbortScope();
631
+ };
632
+ return new ReadableStream({
633
+ async pull(controller) {
634
+ try {
635
+ const next = await iterator.next();
636
+ if (next.done) {
637
+ dispose();
638
+ controller.close();
639
+ return;
640
+ }
641
+ controller.enqueue(next.value);
642
+ }
643
+ catch (error) {
644
+ if (!providerAbortController.signal.aborted) {
645
+ providerAbortController.abort(error);
646
+ }
647
+ dispose();
648
+ if (!consumerCanceled) {
649
+ controller.error(error);
650
+ }
651
+ }
652
+ },
653
+ async cancel(reason) {
654
+ consumerCanceled = true;
655
+ if (!providerAbortController.signal.aborted) {
656
+ providerAbortController.abort(reason);
657
+ }
658
+ try {
659
+ await iterator.return?.();
660
+ }
661
+ catch (error) {
662
+ if (!providerAbortController.signal.aborted) {
663
+ throw error;
664
+ }
665
+ }
666
+ finally {
667
+ dispose();
668
+ }
669
+ },
670
+ },
671
+ // Avoid speculative reads so cancellation reaches the provider body as
672
+ // soon as the consumer stops after its most recent part.
673
+ { highWaterMark: 0 });
674
+ }
267
675
  // ---------------------------------------------------------------------------
268
676
  // Public factory functions
269
677
  // ---------------------------------------------------------------------------
@@ -271,13 +679,14 @@ export function createOpenAIModelRuntime(config, modelId) {
271
679
  const fetchImpl = config.fetch ?? globalThis.fetch;
272
680
  const providerLabel = getOpenAIProviderLabel(config);
273
681
  const providerName = getRuntimeOpenAIProviderName(config);
682
+ const providerKind = getOpenAICompatibleProviderKind(config);
683
+ const responseContext = { providerKind, providerLabel };
274
684
  return {
275
685
  provider: providerLabel,
276
686
  modelId,
277
687
  specificationVersion: "v3",
278
688
  supportedUrls: {},
279
- doGenerate(optionsForRuntime) {
280
- const options = optionsForRuntime;
689
+ doGenerate(options) {
281
690
  const url = getOpenAIChatCompletionsUrl(config.baseURL);
282
691
  const warnings = createWarningCollector();
283
692
  const body = buildOpenAIChatRequest(modelId, providerName, options, false, warnings);
@@ -285,7 +694,7 @@ export function createOpenAIModelRuntime(config, modelId) {
285
694
  url,
286
695
  fetchImpl,
287
696
  providerLabel,
288
- providerKind: "openai",
697
+ providerKind,
289
698
  init: createOpenAIRequestInit({
290
699
  apiKey: config.apiKey,
291
700
  extraHeaders: options.headers,
@@ -295,34 +704,39 @@ export function createOpenAIModelRuntime(config, modelId) {
295
704
  }).then((payload) => {
296
705
  const drained = warnings.drain();
297
706
  return {
298
- ...buildOpenAIGenerateResult(payload),
707
+ ...buildOpenAIGenerateResult(payload, responseContext),
299
708
  ...(drained.length > 0 ? { warnings: drained } : {}),
300
709
  };
301
710
  });
302
711
  },
303
- doStream(optionsForRuntime) {
304
- const options = optionsForRuntime;
712
+ async doStream(options) {
305
713
  const url = getOpenAIChatCompletionsUrl(config.baseURL);
306
714
  const warnings = createWarningCollector();
307
715
  const body = buildOpenAIChatRequest(modelId, providerName, options, true, warnings);
308
- return requestStream({
309
- url,
310
- fetchImpl,
311
- providerLabel,
312
- providerKind: "openai",
313
- init: createOpenAIRequestInit({
314
- apiKey: config.apiKey,
315
- extraHeaders: options.headers,
316
- body: JSON.stringify(body),
317
- signal: options.abortSignal,
318
- }),
319
- }).then((responseStream) => {
716
+ const providerAbortScope = createOpenAIProviderAbortScope(options.abortSignal);
717
+ try {
718
+ const responseStream = await requestStream({
719
+ url,
720
+ fetchImpl,
721
+ providerLabel,
722
+ providerKind,
723
+ init: createOpenAIRequestInit({
724
+ apiKey: config.apiKey,
725
+ extraHeaders: options.headers,
726
+ body: JSON.stringify(body),
727
+ signal: providerAbortScope.controller.signal,
728
+ }),
729
+ });
320
730
  const drained = warnings.drain();
321
731
  return {
322
- stream: ReadableStream.from(streamOpenAICompatibleParts(responseStream)),
732
+ stream: createCancelableOpenAIProviderStream(streamOpenAICompatibleParts(responseStream, responseContext), providerAbortScope.controller, providerAbortScope.dispose),
323
733
  ...(drained.length > 0 ? { warnings: drained } : {}),
324
734
  };
325
- });
735
+ }
736
+ catch (error) {
737
+ providerAbortScope.dispose();
738
+ throw error;
739
+ }
326
740
  },
327
741
  };
328
742
  }
@@ -330,21 +744,23 @@ export function createOpenAIResponsesRuntime(config, modelId) {
330
744
  const fetchImpl = config.fetch ?? globalThis.fetch;
331
745
  const providerLabel = getOpenAIProviderLabel(config);
332
746
  const providerName = getRuntimeOpenAIProviderName(config);
747
+ const providerKind = getOpenAICompatibleProviderKind(config);
748
+ const responseContext = { providerKind, providerLabel };
333
749
  return {
334
750
  provider: providerLabel,
335
751
  modelId,
336
752
  specificationVersion: "v3",
337
753
  supportedUrls: {},
338
- doGenerate(optionsForRuntime) {
339
- const options = optionsForRuntime;
754
+ doGenerate(options) {
340
755
  const url = getOpenAIResponsesUrl(config.baseURL);
341
756
  const warnings = createWarningCollector();
342
757
  const body = buildOpenAIResponsesRequest(modelId, providerName, options, false, warnings);
758
+ const webSearchToolName = resolveOpenAIWebSearchDescriptor(options.tools)?.name;
343
759
  return requestJson({
344
760
  url,
345
761
  fetchImpl,
346
762
  providerLabel,
347
- providerKind: "openai",
763
+ providerKind,
348
764
  init: createOpenAIRequestInit({
349
765
  apiKey: config.apiKey,
350
766
  extraHeaders: options.headers,
@@ -354,40 +770,76 @@ export function createOpenAIResponsesRuntime(config, modelId) {
354
770
  }).then((payload) => {
355
771
  const drained = warnings.drain();
356
772
  return {
357
- ...buildOpenAIResponsesGenerateResult(payload),
773
+ ...buildOpenAIResponsesGenerateResult(payload, responseContext, webSearchToolName),
358
774
  ...(drained.length > 0 ? { warnings: drained } : {}),
359
775
  };
360
776
  });
361
777
  },
362
- doStream(optionsForRuntime) {
363
- const options = optionsForRuntime;
778
+ async doStream(options) {
364
779
  const url = getOpenAIResponsesUrl(config.baseURL);
365
780
  const warnings = createWarningCollector();
366
781
  const body = buildOpenAIResponsesRequest(modelId, providerName, options, true, warnings);
367
- return requestStream({
368
- url,
369
- fetchImpl,
370
- providerLabel,
371
- providerKind: "openai",
372
- init: createOpenAIRequestInit({
373
- apiKey: config.apiKey,
374
- extraHeaders: options.headers,
375
- body: JSON.stringify(body),
376
- signal: options.abortSignal,
377
- }),
378
- }).then((responseStream) => {
782
+ const webSearchToolName = resolveOpenAIWebSearchDescriptor(options.tools)?.name;
783
+ const providerAbortScope = createOpenAIProviderAbortScope(options.abortSignal);
784
+ try {
785
+ const responseStream = await requestStream({
786
+ url,
787
+ fetchImpl,
788
+ providerLabel,
789
+ providerKind,
790
+ init: createOpenAIRequestInit({
791
+ apiKey: config.apiKey,
792
+ extraHeaders: options.headers,
793
+ body: JSON.stringify(body),
794
+ signal: providerAbortScope.controller.signal,
795
+ }),
796
+ });
379
797
  const drained = warnings.drain();
380
798
  return {
381
- stream: ReadableStream.from(streamOpenAIResponsesParts(responseStream)),
799
+ stream: createCancelableOpenAIProviderStream(streamOpenAIResponsesParts(responseStream, {
800
+ ...responseContext,
801
+ webSearchToolName,
802
+ preserveRawOutputItems: true,
803
+ }), providerAbortScope.controller, providerAbortScope.dispose),
382
804
  ...(drained.length > 0 ? { warnings: drained } : {}),
383
805
  };
384
- });
806
+ }
807
+ catch (error) {
808
+ providerAbortScope.dispose();
809
+ throw error;
810
+ }
811
+ },
812
+ };
813
+ }
814
+ function requestUsesOpenAIHostedTool(optionsForRuntime) {
815
+ const options = readRecord(optionsForRuntime);
816
+ if (!options || options.tools === undefined)
817
+ return false;
818
+ if (!Array.isArray(options.tools)) {
819
+ throw new TypeError("OpenAI runtime tools must be an array");
820
+ }
821
+ return resolveOpenAIWebSearchDescriptor(options.tools) !== undefined;
822
+ }
823
+ function createOpenAIAdaptiveModelRuntime(chatRuntime, responsesRuntime) {
824
+ return {
825
+ ...chatRuntime,
826
+ doGenerate(optionsForRuntime) {
827
+ return requestUsesOpenAIHostedTool(optionsForRuntime)
828
+ ? responsesRuntime.doGenerate(optionsForRuntime)
829
+ : chatRuntime.doGenerate(optionsForRuntime);
830
+ },
831
+ doStream(optionsForRuntime) {
832
+ return requestUsesOpenAIHostedTool(optionsForRuntime)
833
+ ? responsesRuntime.doStream(optionsForRuntime)
834
+ : chatRuntime.doStream(optionsForRuntime);
385
835
  },
386
836
  };
387
837
  }
388
838
  export function createOpenAIEmbeddingRuntime(config, modelId) {
389
839
  const fetchImpl = config.fetch ?? globalThis.fetch;
390
840
  const providerLabel = getOpenAIProviderLabel(config);
841
+ const providerKind = getOpenAICompatibleProviderKind(config);
842
+ const responseContext = { providerKind, providerLabel };
391
843
  return {
392
844
  provider: providerLabel,
393
845
  modelId,
@@ -405,7 +857,7 @@ export function createOpenAIEmbeddingRuntime(config, modelId) {
405
857
  url,
406
858
  fetchImpl,
407
859
  providerLabel,
408
- providerKind: "openai",
860
+ providerKind,
409
861
  init: createOpenAIRequestInit({
410
862
  apiKey: config.apiKey,
411
863
  body: JSON.stringify({
@@ -414,14 +866,15 @@ export function createOpenAIEmbeddingRuntime(config, modelId) {
414
866
  }),
415
867
  signal: abortSignal,
416
868
  }),
417
- }).then((payload) => ({
418
- embeddings: extractOpenAIEmbeddings(payload),
419
- usage: {
420
- tokens: extractOpenAIUsageTokens(payload),
421
- },
422
- rawResponse: payload,
423
- warnings: [],
424
- }));
869
+ }).then((payload) => {
870
+ const tokens = extractOpenAIUsageTokens(payload);
871
+ return {
872
+ embeddings: extractOpenAIEmbeddings(payload, values.length, responseContext),
873
+ ...(tokens !== undefined ? { usage: { tokens } } : {}),
874
+ rawResponse: payload,
875
+ warnings: [],
876
+ };
877
+ });
425
878
  },
426
879
  };
427
880
  }
@@ -430,28 +883,25 @@ export class OpenAIProvider {
430
883
  createModel(modelId, config) {
431
884
  const providerLabel = getOpenAIProviderLabel(config);
432
885
  const providerName = getLLMOpenAIProviderName(config);
433
- if (isOpenAIReasoningModel(modelId, providerName)) {
434
- return createOpenAIResponsesRuntime({
435
- apiKey: config.credential,
436
- baseURL: config.baseURL,
437
- name: providerLabel,
438
- providerName,
439
- fetch: config.fetch,
440
- }, modelId);
441
- }
442
- return createOpenAIModelRuntime({
886
+ const runtimeConfig = {
443
887
  apiKey: config.credential,
444
888
  baseURL: config.baseURL,
445
889
  name: providerLabel,
446
890
  providerName,
447
891
  fetch: config.fetch,
448
- }, modelId);
892
+ };
893
+ const responsesRuntime = createOpenAIResponsesRuntime(runtimeConfig, modelId);
894
+ if (isOpenAIReasoningModel(modelId, providerName)) {
895
+ return responsesRuntime;
896
+ }
897
+ return createOpenAIAdaptiveModelRuntime(createOpenAIModelRuntime(runtimeConfig, modelId), responsesRuntime);
449
898
  }
450
899
  createEmbedding(modelId, config) {
451
900
  return createOpenAIEmbeddingRuntime({
452
901
  apiKey: config.credential,
453
902
  baseURL: config.baseURL,
454
903
  name: getOpenAIProviderLabel(config),
904
+ providerName: getLLMOpenAIProviderName(config),
455
905
  fetch: config.fetch,
456
906
  }, modelId);
457
907
  }