@veryfront/ext-llm-openai 0.1.1185 → 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
@@ -1,4 +1,37 @@
1
- import { parseSseChunk, readGatewayBillingMode, readRecord } from "veryfront/provider/shared";
1
+ import { mergeUsage, ProviderRequestError, readGatewayBillingMode, readRecord, stringifyJsonValue, } from "veryfront/provider/shared";
2
+ import { appendOpenAIStreamToolArgument, isJsonObjectText, joinOpenAIStreamToolArguments, MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES, MAX_OPENAI_STREAM_TOOL_ARGUMENT_FRAGMENTS, } from "./openai-tool-input.js";
3
+ import { isBoundedOpenAIStreamString, MAX_OPENAI_STREAM_IDENTIFIER_BYTES, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES, MAX_OPENAI_STREAM_TOOL_NAME_BYTES, } from "./openai-stream-metadata.js";
4
+ import { appendOpenAISseChunk, finishOpenAISseDecoding, parseOpenAISseBuffer, } from "./openai-sse-buffer.js";
5
+ import { createOpenAIRawResponseMetadata, MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES, normalizeOpenAIWebSearchCall, readOpenAIRawResponseOutputItems, validateOpenAIUrlCitation, } from "./openai-web-search.js";
6
+ export const MAX_OPENAI_STREAM_MESSAGE_SNAPSHOT_BYTES = 8 * 1024 * 1024;
7
+ export const MAX_OPENAI_RESPONSES_RAW_METADATA_BYTES = MAX_OPENAI_RAW_RESPONSE_METADATA_BYTES;
8
+ export const MAX_OPENAI_RESPONSES_STREAM_OUTPUT_ITEMS = 4_096;
9
+ export const MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS = 4_096;
10
+ const MAX_OPENAI_STREAM_MESSAGE_DELTA_BATCH_BYTES = 64 * 1024;
11
+ const MAX_OPENAI_STREAM_MESSAGE_DELTA_BATCH_FRAGMENTS = 256;
12
+ function invalidOpenAIResponsesStream(context, issue) {
13
+ const providerKind = context.providerKind ?? "openai";
14
+ return new ProviderRequestError({
15
+ provider: providerKind,
16
+ status: 200,
17
+ message: `${context.providerLabel ?? providerKind} request failed: invalid successful stream (${issue})`,
18
+ retryable: false,
19
+ });
20
+ }
21
+ function mergeOpenAIResponsesUsage(current, payload) {
22
+ const merged = mergeUsage(current, extractOpenAIResponsesUsage(payload));
23
+ return merged && Object.keys(merged).length > 0 ? merged : undefined;
24
+ }
25
+ function assertFunctionCallInput(state, context) {
26
+ const argumentsText = joinOpenAIStreamToolArguments(state.argumentChunks);
27
+ if (!state.toolCallId || !state.name || !argumentsText) {
28
+ throw invalidOpenAIResponsesStream(context, "function call was incomplete");
29
+ }
30
+ if (!isJsonObjectText(argumentsText)) {
31
+ throw invalidOpenAIResponsesStream(context, "function call arguments were not valid JSON object text");
32
+ }
33
+ return argumentsText;
34
+ }
2
35
  /**
3
36
  * The Responses API uses `input_tokens` / `output_tokens` field names
4
37
  * instead of Chat Completions' `prompt_tokens` / `completion_tokens`.
@@ -85,6 +118,9 @@ export function normalizeOpenAIResponsesFinishReason(raw) {
85
118
  case "in_progress":
86
119
  return null;
87
120
  default:
121
+ // The streaming path pre-validates status against its event type, so
122
+ // unknown values only reach here from non-streaming responses, where
123
+ // they pass through for forward compatibility.
88
124
  return raw;
89
125
  }
90
126
  }
@@ -92,144 +128,1048 @@ export function normalizeOpenAIResponsesFinishReason(raw) {
92
128
  * Parse the Responses API streaming event grammar into the same UI part
93
129
  * shapes the existing OpenAI / Anthropic / Google streams emit.
94
130
  */
95
- export async function* streamOpenAIResponsesParts(stream) {
96
- const decoder = new TextDecoder();
131
+ export async function* streamOpenAIResponsesParts(stream, context = {}) {
132
+ const decoder = new TextDecoder("utf-8", { fatal: true });
97
133
  let buffer = "";
98
134
  const reasoningBlocks = new Map();
99
135
  const functionCalls = new Map();
100
- const startedToolCalls = new Set();
136
+ const messageItems = new Map();
137
+ const webSearchItems = new Map();
138
+ const outputItems = new Map();
139
+ const seenOutputItemIds = new Set();
140
+ const outputItemIdsByIndex = new Map();
141
+ const seenFunctionCallIds = new Set();
142
+ const rawOutputItemsByOrder = [];
143
+ const retainRawOutputItems = context.preserveRawOutputItems === true ||
144
+ context.webSearchToolName !== undefined;
145
+ const toolArgumentBudget = {
146
+ bytes: 0,
147
+ fragments: 0,
148
+ };
101
149
  let finishReason = null;
102
150
  let usage;
103
151
  let reasoningCounter = 0;
104
- for await (const chunk of stream) {
105
- buffer += decoder.decode(chunk, { stream: true });
106
- const parsed = parseSseChunk(buffer);
107
- buffer = parsed.remainder;
108
- for (const event of parsed.events) {
109
- if (event === "[DONE]")
110
- continue;
111
- const record = readRecord(event);
112
- const type = typeof record?.type === "string" ? record.type : undefined;
113
- if (!type)
114
- continue;
115
- // response.output_item.added: a new output item begins.
116
- if (type === "response.output_item.added") {
117
- const item = readRecord(record?.item);
118
- const itemType = typeof item?.type === "string" ? item.type : undefined;
119
- const itemId = typeof item?.id === "string" ? item.id : undefined;
120
- if (itemType === "function_call" && itemId) {
121
- const callId = typeof item?.call_id === "string" ? item.call_id : itemId;
122
- const name = typeof item?.name === "string" ? item.name : "";
123
- functionCalls.set(itemId, {
124
- id: itemId,
125
- toolCallId: callId,
126
- name,
127
- arguments: "",
128
- });
129
- }
130
- if (itemType === "reasoning" && itemId) {
131
- reasoningBlocks.set(itemId, {
132
- id: `reasoning-${reasoningCounter++}`,
133
- emittedStart: false,
134
- });
135
- }
136
- continue;
137
- }
138
- // response.output_text.delta: text chunk for a message item.
139
- if (type === "response.output_text.delta" && typeof record?.delta === "string") {
140
- if (record.delta.length > 0) {
141
- yield { type: "text-delta", delta: record.delta };
142
- }
143
- continue;
144
- }
145
- // response.reasoning_summary_text.delta: reasoning summary text chunk.
146
- if (type === "response.reasoning_summary_text.delta" && typeof record?.delta === "string") {
147
- const itemId = typeof record?.item_id === "string" ? record.item_id : undefined;
148
- const state = itemId ? reasoningBlocks.get(itemId) : undefined;
149
- if (state && record.delta.length > 0) {
150
- if (!state.emittedStart) {
151
- yield { type: "reasoning-start", id: state.id };
152
- state.emittedStart = true;
152
+ let sawTerminalEvent = false;
153
+ let sawDone = false;
154
+ let contentPartCount = 0;
155
+ let retainedMessageSnapshotBytes = 0;
156
+ let retainedRawOutputBytes = 0;
157
+ let outputItemOrder = 0;
158
+ const textEncoder = new TextEncoder();
159
+ function readEventItem(record, issue) {
160
+ const item = readRecord(record.item);
161
+ if (!item) {
162
+ throw invalidOpenAIResponsesStream(context, `${issue} item was not an object`);
163
+ }
164
+ const itemType = isBoundedOpenAIStreamString(item.type, MAX_OPENAI_STREAM_ITEM_TYPE_BYTES)
165
+ ? item.type
166
+ : undefined;
167
+ const itemId = isBoundedOpenAIStreamString(item.id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
168
+ ? item.id
169
+ : undefined;
170
+ if (!itemType || !itemId) {
171
+ throw invalidOpenAIResponsesStream(context, `${issue} item type or id was missing`);
172
+ }
173
+ return { item, itemType, itemId };
174
+ }
175
+ function readContentIndex(record) {
176
+ if (record.content_index === undefined) {
177
+ // Some compatible endpoints omit the index for their single message
178
+ // content part. Treat that shape as index zero while still validating
179
+ // every explicit index.
180
+ return 0;
181
+ }
182
+ if (!Number.isSafeInteger(record.content_index) || record.content_index < 0) {
183
+ throw invalidOpenAIResponsesStream(context, "message content index was malformed");
184
+ }
185
+ return record.content_index;
186
+ }
187
+ function readSummaryIndex(record) {
188
+ if (record.summary_index === undefined) {
189
+ return 0;
190
+ }
191
+ if (!Number.isSafeInteger(record.summary_index) || record.summary_index < 0) {
192
+ throw invalidOpenAIResponsesStream(context, "reasoning summary index was malformed");
193
+ }
194
+ return record.summary_index;
195
+ }
196
+ function readOptionalOutputIndex(record, issue) {
197
+ if (record.output_index === undefined)
198
+ return undefined;
199
+ if (!Number.isSafeInteger(record.output_index) || record.output_index < 0) {
200
+ throw invalidOpenAIResponsesStream(context, `${issue} output index was malformed`);
201
+ }
202
+ return record.output_index;
203
+ }
204
+ function assertOutputIndex(itemId, record, issue) {
205
+ const state = outputItems.get(itemId);
206
+ if (!state) {
207
+ throw invalidOpenAIResponsesStream(context, `${issue} referenced an unknown output item`);
208
+ }
209
+ const outputIndex = readOptionalOutputIndex(record, issue);
210
+ if (outputIndex === undefined)
211
+ return;
212
+ const existingItemId = outputItemIdsByIndex.get(outputIndex);
213
+ if (existingItemId !== undefined && existingItemId !== itemId) {
214
+ throw invalidOpenAIResponsesStream(context, `${issue} output index was owned by another item`);
215
+ }
216
+ if (state.outputIndex !== undefined && state.outputIndex !== outputIndex) {
217
+ throw invalidOpenAIResponsesStream(context, `${issue} output index changed`);
218
+ }
219
+ outputItemIdsByIndex.set(outputIndex, itemId);
220
+ state.outputIndex = outputIndex;
221
+ }
222
+ function readMessageItemId(record, issue) {
223
+ if (!isBoundedOpenAIStreamString(record.item_id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)) {
224
+ throw invalidOpenAIResponsesStream(context, `${issue} item id was missing`);
225
+ }
226
+ return record.item_id;
227
+ }
228
+ function readMessagePartKind(value, issue) {
229
+ if (value === "output_text" || value === "refusal") {
230
+ return value;
231
+ }
232
+ throw invalidOpenAIResponsesStream(context, `${issue} type was unsupported`);
233
+ }
234
+ function getMessageState(itemId, issue) {
235
+ const state = messageItems.get(itemId);
236
+ if (!state) {
237
+ throw invalidOpenAIResponsesStream(context, `${issue} referenced an unknown message item`);
238
+ }
239
+ return state;
240
+ }
241
+ function getOrCreateMessagePart(message, contentIndex, kind, issue) {
242
+ const existing = message.parts.get(contentIndex);
243
+ if (existing) {
244
+ if (existing.kind !== kind) {
245
+ throw invalidOpenAIResponsesStream(context, `${issue} changed content-part type`);
246
+ }
247
+ return existing;
248
+ }
249
+ if (contentPartCount >= MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS) {
250
+ throw invalidOpenAIResponsesStream(context, `stream exceeded ${MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS} content parts`);
251
+ }
252
+ contentPartCount++;
253
+ const created = {
254
+ kind,
255
+ annotations: new Map(),
256
+ ...createRetainedTextState(),
257
+ };
258
+ message.parts.set(contentIndex, created);
259
+ return created;
260
+ }
261
+ function createRetainedTextState() {
262
+ return {
263
+ deltaChunks: [],
264
+ pendingDeltaChunks: [],
265
+ pendingDeltaBytes: 0,
266
+ retainedValueBytes: 0,
267
+ sawDelta: false,
268
+ emittedValue: false,
269
+ valueDone: false,
270
+ contentPartAdded: false,
271
+ contentPartDone: false,
272
+ };
273
+ }
274
+ function getOrCreateReasoningPart(parts, index) {
275
+ const existing = parts.get(index);
276
+ if (existing)
277
+ return existing;
278
+ if (contentPartCount >= MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS) {
279
+ throw invalidOpenAIResponsesStream(context, `stream exceeded ${MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS} content parts`);
280
+ }
281
+ contentPartCount++;
282
+ const created = createRetainedTextState();
283
+ parts.set(index, created);
284
+ return created;
285
+ }
286
+ function reconcileRetainedTextValue(state, value, issue) {
287
+ if (state.snapshot !== undefined) {
288
+ if (state.snapshot !== value) {
289
+ throw invalidOpenAIResponsesStream(context, `${issue} changed its final value`);
290
+ }
291
+ return undefined;
292
+ }
293
+ if (state.sawDelta) {
294
+ if (state.pendingDeltaChunks.length > 0) {
295
+ state.deltaChunks.push(state.pendingDeltaChunks.join(""));
296
+ state.pendingDeltaChunks.length = 0;
297
+ state.pendingDeltaBytes = 0;
298
+ }
299
+ const streamedValue = state.deltaChunks.join("");
300
+ if (streamedValue !== value) {
301
+ throw invalidOpenAIResponsesStream(context, `${issue} disagreed with streamed deltas`);
302
+ }
303
+ }
304
+ else if (value.length > 0) {
305
+ const valueBytes = textEncoder.encode(value).byteLength;
306
+ if (valueBytes > MAX_OPENAI_STREAM_MESSAGE_SNAPSHOT_BYTES -
307
+ retainedMessageSnapshotBytes) {
308
+ throw invalidOpenAIResponsesStream(context, `message snapshot exceeded ${MAX_OPENAI_STREAM_MESSAGE_SNAPSHOT_BYTES} UTF-8 bytes`);
309
+ }
310
+ state.retainedValueBytes = valueBytes;
311
+ retainedMessageSnapshotBytes += valueBytes;
312
+ }
313
+ state.snapshot = value;
314
+ state.deltaChunks.length = 0;
315
+ state.sawDelta = false;
316
+ if (!state.emittedValue && value.length > 0) {
317
+ state.emittedValue = true;
318
+ return value;
319
+ }
320
+ return undefined;
321
+ }
322
+ function appendRetainedTextDelta(state, delta) {
323
+ if (delta.length === 0)
324
+ return;
325
+ const deltaBytes = textEncoder.encode(delta).byteLength;
326
+ if (deltaBytes > MAX_OPENAI_STREAM_MESSAGE_SNAPSHOT_BYTES -
327
+ retainedMessageSnapshotBytes) {
328
+ throw invalidOpenAIResponsesStream(context, `message snapshot exceeded ${MAX_OPENAI_STREAM_MESSAGE_SNAPSHOT_BYTES} UTF-8 bytes`);
329
+ }
330
+ state.retainedValueBytes += deltaBytes;
331
+ retainedMessageSnapshotBytes += deltaBytes;
332
+ state.pendingDeltaChunks.push(delta);
333
+ state.pendingDeltaBytes += deltaBytes;
334
+ if (state.pendingDeltaBytes >= MAX_OPENAI_STREAM_MESSAGE_DELTA_BATCH_BYTES ||
335
+ state.pendingDeltaChunks.length >= MAX_OPENAI_STREAM_MESSAGE_DELTA_BATCH_FRAGMENTS) {
336
+ state.deltaChunks.push(state.pendingDeltaChunks.join(""));
337
+ state.pendingDeltaChunks.length = 0;
338
+ state.pendingDeltaBytes = 0;
339
+ }
340
+ }
341
+ function releaseMessageState(state) {
342
+ for (const part of state.parts.values()) {
343
+ releaseRetainedTextState(part);
344
+ }
345
+ }
346
+ function releaseReasoningState(state) {
347
+ for (const part of state.summaryParts.values()) {
348
+ releaseRetainedTextState(part);
349
+ }
350
+ for (const part of state.contentParts.values()) {
351
+ releaseRetainedTextState(part);
352
+ }
353
+ }
354
+ function releaseRetainedTextState(state) {
355
+ retainedMessageSnapshotBytes -= state.retainedValueBytes;
356
+ state.retainedValueBytes = 0;
357
+ state.deltaChunks.length = 0;
358
+ state.pendingDeltaChunks.length = 0;
359
+ state.pendingDeltaBytes = 0;
360
+ state.snapshot = undefined;
361
+ }
362
+ function* emitReasoningText(state, text) {
363
+ if (text === undefined || text.length === 0)
364
+ return;
365
+ if (!state.emittedStart) {
366
+ yield { type: "reasoning-start", id: state.id };
367
+ state.emittedStart = true;
368
+ }
369
+ yield { type: "reasoning-delta", id: state.id, delta: text };
370
+ }
371
+ function* reconcileFinalReasoningParts(reasoning, rawParts, states, expectedType, issue) {
372
+ if (rawParts === undefined) {
373
+ if (states.size > 0) {
374
+ throw invalidOpenAIResponsesStream(context, `completed reasoning omitted its ${issue} parts`);
375
+ }
376
+ return;
377
+ }
378
+ if (!Array.isArray(rawParts)) {
379
+ throw invalidOpenAIResponsesStream(context, `completed reasoning ${issue} was not an array`);
380
+ }
381
+ const finalIndices = new Set();
382
+ for (const [index, rawPart] of rawParts.entries()) {
383
+ const part = readRecord(rawPart);
384
+ if (!part ||
385
+ part.type !== expectedType ||
386
+ typeof part.text !== "string") {
387
+ throw invalidOpenAIResponsesStream(context, `completed reasoning ${issue} part was malformed`);
388
+ }
389
+ const state = getOrCreateReasoningPart(states, index);
390
+ if (state.contentPartAdded && (!state.valueDone || !state.contentPartDone)) {
391
+ throw invalidOpenAIResponsesStream(context, `completed reasoning had an unfinished ${issue} part`);
392
+ }
393
+ const missingDelta = reconcileRetainedTextValue(state, part.text, `completed reasoning ${issue} part`);
394
+ yield* emitReasoningText(reasoning, missingDelta);
395
+ finalIndices.add(index);
396
+ }
397
+ for (const index of states.keys()) {
398
+ if (!finalIndices.has(index)) {
399
+ throw invalidOpenAIResponsesStream(context, `completed reasoning omitted a streamed ${issue} part`);
400
+ }
401
+ }
402
+ }
403
+ function appendFunctionCallArgument(state, fragment) {
404
+ const limit = appendOpenAIStreamToolArgument(toolArgumentBudget, state.argumentChunks, fragment);
405
+ if (limit === "bytes") {
406
+ throw invalidOpenAIResponsesStream(context, `function call arguments exceeded ${MAX_OPENAI_STREAM_TOOL_ARGUMENT_BYTES} UTF-8 bytes`);
407
+ }
408
+ if (limit === "fragments") {
409
+ throw invalidOpenAIResponsesStream(context, `function call arguments exceeded ${MAX_OPENAI_STREAM_TOOL_ARGUMENT_FRAGMENTS} fragments`);
410
+ }
411
+ }
412
+ function validateMessageContentPart(part, issue) {
413
+ const kind = readMessagePartKind(part.type, issue);
414
+ const value = kind === "output_text" ? part.text : part.refusal;
415
+ if (typeof value !== "string") {
416
+ throw invalidOpenAIResponsesStream(context, `${issue} value was malformed`);
417
+ }
418
+ return kind;
419
+ }
420
+ function serializeUrlCitation(value, issue) {
421
+ const annotation = validateOpenAIUrlCitation(value, (detail) => invalidOpenAIResponsesStream(context, `${issue}: ${detail}`));
422
+ return stringifyJsonValue({
423
+ type: annotation.type,
424
+ start_index: annotation.start_index,
425
+ end_index: annotation.end_index,
426
+ url: annotation.url,
427
+ title: annotation.title,
428
+ });
429
+ }
430
+ function reconcileMessageAnnotations(state, part, text, issue) {
431
+ if (state.kind !== "output_text") {
432
+ if (part.annotations !== undefined) {
433
+ throw invalidOpenAIResponsesStream(context, `${issue} attached annotations to a refusal`);
434
+ }
435
+ return;
436
+ }
437
+ const rawAnnotations = part.annotations ?? [];
438
+ if (!Array.isArray(rawAnnotations) ||
439
+ rawAnnotations.length > MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS) {
440
+ throw invalidOpenAIResponsesStream(context, `${issue} annotations were malformed`);
441
+ }
442
+ const annotations = rawAnnotations.map((annotation, index) => {
443
+ const serialized = serializeUrlCitation(annotation, `${issue} annotation ${index}`);
444
+ const parsed = readRecord(annotation);
445
+ if (!parsed ||
446
+ parsed.end_index > text.length) {
447
+ throw invalidOpenAIResponsesStream(context, `${issue} annotation range exceeded its text`);
448
+ }
449
+ return serialized;
450
+ });
451
+ if (state.annotationSnapshot !== undefined &&
452
+ (state.annotationSnapshot.length !== annotations.length ||
453
+ state.annotationSnapshot.some((annotation, index) => annotation !== annotations[index]))) {
454
+ throw invalidOpenAIResponsesStream(context, `${issue} annotations changed`);
455
+ }
456
+ if (state.annotations.size > 0) {
457
+ if (state.annotations.size !== annotations.length) {
458
+ throw invalidOpenAIResponsesStream(context, `${issue} annotations disagreed with streamed annotations`);
459
+ }
460
+ for (const [index, annotation] of state.annotations) {
461
+ if (annotations[index] !== annotation) {
462
+ throw invalidOpenAIResponsesStream(context, `${issue} annotations disagreed with streamed annotations`);
463
+ }
464
+ }
465
+ }
466
+ state.annotationSnapshot = annotations;
467
+ }
468
+ function retainCompletedOutputItem(state, item) {
469
+ if (!retainRawOutputItems)
470
+ return;
471
+ const itemBytes = textEncoder.encode(stringifyJsonValue(item)).byteLength;
472
+ if (itemBytes > MAX_OPENAI_RESPONSES_RAW_METADATA_BYTES -
473
+ retainedRawOutputBytes) {
474
+ throw invalidOpenAIResponsesStream(context, `raw response metadata exceeded ${MAX_OPENAI_RESPONSES_RAW_METADATA_BYTES} UTF-8 bytes`);
475
+ }
476
+ retainedRawOutputBytes += itemBytes;
477
+ rawOutputItemsByOrder[state.order] = {
478
+ item,
479
+ order: state.order,
480
+ outputIndex: state.outputIndex,
481
+ };
482
+ }
483
+ function* processEvent(event) {
484
+ if (event === "[DONE]") {
485
+ if (!sawTerminalEvent) {
486
+ throw invalidOpenAIResponsesStream(context, "done marker arrived before a terminal response event");
487
+ }
488
+ if (sawDone) {
489
+ throw invalidOpenAIResponsesStream(context, "stream contained multiple done markers");
490
+ }
491
+ sawDone = true;
492
+ return;
493
+ }
494
+ if (sawDone) {
495
+ throw invalidOpenAIResponsesStream(context, "stream contained data after its done marker");
496
+ }
497
+ if (sawTerminalEvent) {
498
+ throw invalidOpenAIResponsesStream(context, "stream contained data after its terminal event");
499
+ }
500
+ const record = readRecord(event);
501
+ if (!record) {
502
+ throw invalidOpenAIResponsesStream(context, "event was not an object");
503
+ }
504
+ const type = typeof record.type === "string" && record.type.length > 0
505
+ ? record.type
506
+ : undefined;
507
+ if (!type) {
508
+ throw invalidOpenAIResponsesStream(context, "event type was missing");
509
+ }
510
+ if (type === "error") {
511
+ throw invalidOpenAIResponsesStream(context, "provider emitted an error event");
512
+ }
513
+ if (type === "response.output_item.added") {
514
+ const { item, itemType, itemId } = readEventItem(record, "added output");
515
+ if (seenOutputItemIds.has(itemId)) {
516
+ throw invalidOpenAIResponsesStream(context, "output item was added twice");
517
+ }
518
+ if (seenOutputItemIds.size >= MAX_OPENAI_RESPONSES_STREAM_OUTPUT_ITEMS) {
519
+ throw invalidOpenAIResponsesStream(context, `stream exceeded ${MAX_OPENAI_RESPONSES_STREAM_OUTPUT_ITEMS} output items`);
520
+ }
521
+ const outputIndex = readOptionalOutputIndex(record, "added output item");
522
+ if (outputIndex !== undefined) {
523
+ const existingItemId = outputItemIdsByIndex.get(outputIndex);
524
+ if (existingItemId !== undefined && existingItemId !== itemId) {
525
+ throw invalidOpenAIResponsesStream(context, "output index was reused by another item");
526
+ }
527
+ outputItemIdsByIndex.set(outputIndex, itemId);
528
+ }
529
+ seenOutputItemIds.add(itemId);
530
+ outputItems.set(itemId, {
531
+ type: itemType,
532
+ outputIndex,
533
+ order: outputItemOrder++,
534
+ });
535
+ if (itemType === "function_call") {
536
+ const callId = isBoundedOpenAIStreamString(item.call_id, MAX_OPENAI_STREAM_IDENTIFIER_BYTES)
537
+ ? item.call_id
538
+ : undefined;
539
+ const name = isBoundedOpenAIStreamString(item.name, MAX_OPENAI_STREAM_TOOL_NAME_BYTES)
540
+ ? item.name
541
+ : undefined;
542
+ if (!callId || !name) {
543
+ throw invalidOpenAIResponsesStream(context, "added function call id or name was missing");
544
+ }
545
+ if (seenFunctionCallIds.has(callId)) {
546
+ throw invalidOpenAIResponsesStream(context, "function call id was reused");
547
+ }
548
+ if ((item.arguments !== undefined && item.arguments !== "") ||
549
+ (item.status !== undefined && item.status !== "in_progress")) {
550
+ throw invalidOpenAIResponsesStream(context, "added function call was not in its initial state");
551
+ }
552
+ functionCalls.set(itemId, {
553
+ id: itemId,
554
+ toolCallId: callId,
555
+ name,
556
+ argumentChunks: [],
557
+ sawNonEmptyArgumentDelta: false,
558
+ argumentsDone: false,
559
+ emittedStart: false,
560
+ });
561
+ seenFunctionCallIds.add(callId);
562
+ }
563
+ else if (itemType === "reasoning") {
564
+ if ((item.summary !== undefined &&
565
+ (!Array.isArray(item.summary) || item.summary.length > 0)) ||
566
+ (item.content !== undefined &&
567
+ (!Array.isArray(item.content) || item.content.length > 0)) ||
568
+ (item.status !== undefined && item.status !== "in_progress")) {
569
+ throw invalidOpenAIResponsesStream(context, "added reasoning item was not in its initial state");
570
+ }
571
+ reasoningBlocks.set(itemId, {
572
+ id: `reasoning-${reasoningCounter++}`,
573
+ emittedStart: false,
574
+ summaryParts: new Map(),
575
+ contentParts: new Map(),
576
+ });
577
+ }
578
+ else if (itemType === "message") {
579
+ if (item.role !== undefined && item.role !== "assistant") {
580
+ throw invalidOpenAIResponsesStream(context, "added message role was not assistant");
581
+ }
582
+ if (item.content !== undefined && !Array.isArray(item.content)) {
583
+ throw invalidOpenAIResponsesStream(context, "added message content was not an array");
584
+ }
585
+ if ((Array.isArray(item.content) && item.content.length > 0) ||
586
+ (item.status !== undefined && item.status !== "in_progress")) {
587
+ throw invalidOpenAIResponsesStream(context, "added message was not in its initial state");
588
+ }
589
+ messageItems.set(itemId, { parts: new Map() });
590
+ }
591
+ else if (itemType === "web_search_call") {
592
+ if (!context.webSearchToolName) {
593
+ throw invalidOpenAIResponsesStream(context, "provider emitted web search without a configured web-search tool");
594
+ }
595
+ if (item.action !== undefined ||
596
+ (item.status !== undefined && item.status !== "in_progress")) {
597
+ throw invalidOpenAIResponsesStream(context, "added web-search call was not in its initial state");
598
+ }
599
+ if (seenFunctionCallIds.has(itemId)) {
600
+ throw invalidOpenAIResponsesStream(context, "tool call id was reused");
601
+ }
602
+ webSearchItems.set(itemId, { phaseRank: 0 });
603
+ seenFunctionCallIds.add(itemId);
604
+ yield {
605
+ type: "tool-input-start",
606
+ id: itemId,
607
+ toolName: context.webSearchToolName,
608
+ providerExecuted: true,
609
+ };
610
+ }
611
+ return;
612
+ }
613
+ if (type === "response.web_search_call.in_progress" ||
614
+ type === "response.web_search_call.searching" ||
615
+ type === "response.web_search_call.completed") {
616
+ const itemId = readMessageItemId(record, "web-search lifecycle event");
617
+ const state = webSearchItems.get(itemId);
618
+ if (!state) {
619
+ throw invalidOpenAIResponsesStream(context, "web-search lifecycle event referenced an unknown item");
620
+ }
621
+ assertOutputIndex(itemId, record, "web-search lifecycle event");
622
+ const phaseRank = type === "response.web_search_call.in_progress"
623
+ ? 1
624
+ : type === "response.web_search_call.searching"
625
+ ? 2
626
+ : 3;
627
+ if (phaseRank <= state.phaseRank) {
628
+ throw invalidOpenAIResponsesStream(context, "web-search lifecycle moved backward or repeated a phase");
629
+ }
630
+ state.phaseRank = phaseRank;
631
+ return;
632
+ }
633
+ if (type === "response.content_part.added" ||
634
+ type === "response.content_part.done") {
635
+ const itemId = readMessageItemId(record, "message content-part event");
636
+ const outputItem = outputItems.get(itemId);
637
+ if (!outputItem) {
638
+ throw invalidOpenAIResponsesStream(context, "content-part event referenced an unknown output item");
639
+ }
640
+ assertOutputIndex(itemId, record, "message content-part event");
641
+ const contentIndex = readContentIndex(record);
642
+ const part = readRecord(record.part);
643
+ if (!part) {
644
+ throw invalidOpenAIResponsesStream(context, "message content part was not an object");
645
+ }
646
+ if (outputItem.type === "reasoning") {
647
+ if (part.type !== "reasoning_text" || typeof part.text !== "string") {
648
+ throw invalidOpenAIResponsesStream(context, "reasoning content part was malformed");
649
+ }
650
+ const reasoning = reasoningBlocks.get(itemId);
651
+ if (!reasoning) {
652
+ throw invalidOpenAIResponsesStream(context, "reasoning content part referenced an unknown reasoning item");
653
+ }
654
+ const existing = reasoning.contentParts.get(contentIndex);
655
+ if (type === "response.content_part.added") {
656
+ if (existing !== undefined) {
657
+ throw invalidOpenAIResponsesStream(context, "reasoning content part was added twice");
153
658
  }
154
- yield { type: "reasoning-delta", id: state.id, delta: record.delta };
155
- }
156
- continue;
157
- }
158
- // response.function_call_arguments.delta: tool call argument chunk.
159
- if (type === "response.function_call_arguments.delta" && typeof record?.delta === "string") {
160
- const itemId = typeof record?.item_id === "string" ? record.item_id : undefined;
161
- const state = itemId ? functionCalls.get(itemId) : undefined;
162
- if (state && record.delta.length > 0) {
163
- if (!startedToolCalls.has(state.id)) {
164
- yield {
165
- type: "tool-input-start",
166
- id: state.toolCallId,
167
- toolName: state.name,
168
- };
169
- startedToolCalls.add(state.id);
659
+ if (part.text !== "") {
660
+ throw invalidOpenAIResponsesStream(context, "added reasoning content part was not empty");
661
+ }
662
+ const state = getOrCreateReasoningPart(reasoning.contentParts, contentIndex);
663
+ state.contentPartAdded = true;
664
+ }
665
+ else {
666
+ if (!existing?.contentPartAdded) {
667
+ throw invalidOpenAIResponsesStream(context, "reasoning content part completed before it was added");
170
668
  }
171
- state.arguments += record.delta;
669
+ if (existing.contentPartDone) {
670
+ throw invalidOpenAIResponsesStream(context, "reasoning content part completed twice");
671
+ }
672
+ const missingDelta = reconcileRetainedTextValue(existing, part.text, "completed reasoning content part");
673
+ yield* emitReasoningText(reasoning, missingDelta);
674
+ existing.contentPartDone = true;
675
+ }
676
+ return;
677
+ }
678
+ if (outputItem.type !== "message") {
679
+ throw invalidOpenAIResponsesStream(context, "content-part event referenced an unsupported output item");
680
+ }
681
+ const message = getMessageState(itemId, "message content-part event");
682
+ const kind = validateMessageContentPart(part, "message content part");
683
+ if (type === "response.content_part.added" &&
684
+ kind === "output_text" &&
685
+ part.annotations !== undefined &&
686
+ (!Array.isArray(part.annotations) || part.annotations.length > 0)) {
687
+ throw invalidOpenAIResponsesStream(context, "added message content part annotations were not empty");
688
+ }
689
+ const existing = message.parts.get(contentIndex);
690
+ if (type === "response.content_part.added" && existing) {
691
+ throw invalidOpenAIResponsesStream(context, "message content part was added twice");
692
+ }
693
+ if (type === "response.content_part.done" && !existing?.contentPartAdded) {
694
+ throw invalidOpenAIResponsesStream(context, "message content part completed before it was added");
695
+ }
696
+ const state = getOrCreateMessagePart(message, contentIndex, kind, "message content part");
697
+ if (type === "response.content_part.added") {
698
+ const initialValue = kind === "output_text" ? part.text : part.refusal;
699
+ if (initialValue !== "") {
700
+ throw invalidOpenAIResponsesStream(context, "added message content part was not empty");
701
+ }
702
+ state.contentPartAdded = true;
703
+ }
704
+ if (type === "response.content_part.done") {
705
+ if (state.contentPartDone) {
706
+ throw invalidOpenAIResponsesStream(context, "message content part completed twice");
707
+ }
708
+ const value = kind === "output_text" ? part.text : part.refusal;
709
+ const missingDelta = reconcileRetainedTextValue(state, value, "completed message content part");
710
+ if (missingDelta !== undefined) {
711
+ yield { type: "text-delta", delta: missingDelta };
712
+ }
713
+ reconcileMessageAnnotations(state, part, value, "completed message content part");
714
+ state.contentPartDone = true;
715
+ }
716
+ return;
717
+ }
718
+ if (type === "response.output_text.delta" || type === "response.refusal.delta") {
719
+ if (typeof record.delta !== "string") {
720
+ throw invalidOpenAIResponsesStream(context, type === "response.refusal.delta"
721
+ ? "refusal delta was malformed"
722
+ : "output-text delta was malformed");
723
+ }
724
+ const itemId = readMessageItemId(record, "message delta");
725
+ const message = getMessageState(itemId, "message delta");
726
+ assertOutputIndex(itemId, record, "message delta");
727
+ const contentIndex = readContentIndex(record);
728
+ const kind = type === "response.refusal.delta" ? "refusal" : "output_text";
729
+ const part = getOrCreateMessagePart(message, contentIndex, kind, "message delta");
730
+ if (part.valueDone || part.contentPartDone) {
731
+ throw invalidOpenAIResponsesStream(context, "message delta followed a completed part");
732
+ }
733
+ if (record.delta.length > 0) {
734
+ part.sawDelta = true;
735
+ }
736
+ appendRetainedTextDelta(part, record.delta);
737
+ if (record.delta.length > 0) {
738
+ part.emittedValue = true;
739
+ yield { type: "text-delta", delta: record.delta };
740
+ }
741
+ return;
742
+ }
743
+ if (type === "response.output_text.annotation.added") {
744
+ const itemId = readMessageItemId(record, "output-text annotation event");
745
+ const message = getMessageState(itemId, "output-text annotation event");
746
+ assertOutputIndex(itemId, record, "output-text annotation event");
747
+ const contentIndex = readContentIndex(record);
748
+ if (!Number.isSafeInteger(record.annotation_index) ||
749
+ record.annotation_index < 0 ||
750
+ record.annotation_index >=
751
+ MAX_OPENAI_RESPONSES_STREAM_CONTENT_PARTS) {
752
+ throw invalidOpenAIResponsesStream(context, "output-text annotation index was malformed");
753
+ }
754
+ const part = getOrCreateMessagePart(message, contentIndex, "output_text", "output-text annotation event");
755
+ if (part.contentPartDone || part.annotationSnapshot !== undefined) {
756
+ throw invalidOpenAIResponsesStream(context, "output-text annotation followed a completed part");
757
+ }
758
+ const annotationIndex = record.annotation_index;
759
+ if (part.annotations.has(annotationIndex)) {
760
+ throw invalidOpenAIResponsesStream(context, "output-text annotation index was reused");
761
+ }
762
+ part.annotations.set(annotationIndex, serializeUrlCitation(record.annotation, `output-text annotation ${annotationIndex}`));
763
+ return;
764
+ }
765
+ if (type === "response.output_text.done" || type === "response.refusal.done") {
766
+ const itemId = readMessageItemId(record, "completed message value");
767
+ const message = getMessageState(itemId, "completed message value");
768
+ assertOutputIndex(itemId, record, "completed message value");
769
+ const contentIndex = readContentIndex(record);
770
+ const kind = type === "response.refusal.done" ? "refusal" : "output_text";
771
+ const value = kind === "refusal" ? record.refusal : record.text;
772
+ if (typeof value !== "string") {
773
+ throw invalidOpenAIResponsesStream(context, "completed message value was malformed");
774
+ }
775
+ const part = getOrCreateMessagePart(message, contentIndex, kind, "completed message value");
776
+ if (part.valueDone) {
777
+ throw invalidOpenAIResponsesStream(context, "message value completed twice");
778
+ }
779
+ const missingDelta = reconcileRetainedTextValue(part, value, "completed message value");
780
+ if (missingDelta !== undefined) {
781
+ yield { type: "text-delta", delta: missingDelta };
782
+ }
783
+ part.valueDone = true;
784
+ return;
785
+ }
786
+ if (type === "response.reasoning_summary_part.added" ||
787
+ type === "response.reasoning_summary_part.done") {
788
+ const itemId = readMessageItemId(record, "reasoning summary-part event");
789
+ const reasoning = reasoningBlocks.get(itemId);
790
+ const part = readRecord(record.part);
791
+ if (!reasoning ||
792
+ !part ||
793
+ part.type !== "summary_text" ||
794
+ typeof part.text !== "string") {
795
+ throw invalidOpenAIResponsesStream(context, "reasoning summary part was malformed");
796
+ }
797
+ assertOutputIndex(itemId, record, "reasoning summary-part event");
798
+ const summaryIndex = readSummaryIndex(record);
799
+ const existing = reasoning.summaryParts.get(summaryIndex);
800
+ if (type === "response.reasoning_summary_part.added") {
801
+ if (existing) {
802
+ throw invalidOpenAIResponsesStream(context, "reasoning summary part was added twice");
803
+ }
804
+ if (part.text !== "") {
805
+ throw invalidOpenAIResponsesStream(context, "added reasoning summary part was not empty");
806
+ }
807
+ getOrCreateReasoningPart(reasoning.summaryParts, summaryIndex).contentPartAdded = true;
808
+ }
809
+ else {
810
+ if (!existing?.contentPartAdded) {
811
+ throw invalidOpenAIResponsesStream(context, "reasoning summary part completed before it was added");
812
+ }
813
+ if (existing.contentPartDone) {
814
+ throw invalidOpenAIResponsesStream(context, "reasoning summary part completed twice");
815
+ }
816
+ const missingDelta = reconcileRetainedTextValue(existing, part.text, "completed reasoning summary part");
817
+ yield* emitReasoningText(reasoning, missingDelta);
818
+ existing.contentPartDone = true;
819
+ }
820
+ return;
821
+ }
822
+ if (type === "response.reasoning_summary_text.delta" ||
823
+ type === "response.reasoning_summary_text.done" ||
824
+ type === "response.reasoning_text.delta" ||
825
+ type === "response.reasoning_text.done") {
826
+ const itemId = readMessageItemId(record, "reasoning text event");
827
+ const reasoning = reasoningBlocks.get(itemId);
828
+ if (!reasoning) {
829
+ throw invalidOpenAIResponsesStream(context, "reasoning text event referenced an unknown item");
830
+ }
831
+ assertOutputIndex(itemId, record, "reasoning text event");
832
+ const isSummary = type.includes("reasoning_summary_");
833
+ const isDone = type.endsWith(".done");
834
+ const index = isSummary ? readSummaryIndex(record) : readContentIndex(record);
835
+ const parts = isSummary ? reasoning.summaryParts : reasoning.contentParts;
836
+ const state = getOrCreateReasoningPart(parts, index);
837
+ const value = isDone ? record.text : record.delta;
838
+ if (typeof value !== "string") {
839
+ throw invalidOpenAIResponsesStream(context, "reasoning text event was malformed");
840
+ }
841
+ if (isDone) {
842
+ if (state.valueDone) {
843
+ throw invalidOpenAIResponsesStream(context, "reasoning text completed twice");
844
+ }
845
+ const missingDelta = reconcileRetainedTextValue(state, value, "completed reasoning text");
846
+ yield* emitReasoningText(reasoning, missingDelta);
847
+ state.valueDone = true;
848
+ }
849
+ else {
850
+ if (state.valueDone || state.contentPartDone) {
851
+ throw invalidOpenAIResponsesStream(context, "reasoning delta followed a completed part");
852
+ }
853
+ if (value.length > 0) {
854
+ state.sawDelta = true;
855
+ }
856
+ appendRetainedTextDelta(state, value);
857
+ if (value.length > 0) {
858
+ state.emittedValue = true;
859
+ }
860
+ yield* emitReasoningText(reasoning, value);
861
+ }
862
+ return;
863
+ }
864
+ if (type === "response.function_call_arguments.delta") {
865
+ if (typeof record.item_id !== "string" ||
866
+ record.item_id.length === 0 ||
867
+ typeof record.delta !== "string") {
868
+ throw invalidOpenAIResponsesStream(context, "function-call delta was malformed");
869
+ }
870
+ const state = functionCalls.get(record.item_id);
871
+ if (!state) {
872
+ throw invalidOpenAIResponsesStream(context, "function-call delta referenced an unknown item");
873
+ }
874
+ assertOutputIndex(record.item_id, record, "function-call delta");
875
+ if (state.argumentsDone) {
876
+ throw invalidOpenAIResponsesStream(context, "function-call delta followed completed arguments");
877
+ }
878
+ if (record.delta.length > 0 && !state.emittedStart) {
879
+ yield {
880
+ type: "tool-input-start",
881
+ id: state.toolCallId,
882
+ toolName: state.name,
883
+ };
884
+ state.emittedStart = true;
885
+ }
886
+ appendFunctionCallArgument(state, record.delta);
887
+ if (record.delta.length > 0) {
888
+ state.sawNonEmptyArgumentDelta = true;
889
+ yield {
890
+ type: "tool-input-delta",
891
+ id: state.toolCallId,
892
+ delta: record.delta,
893
+ };
894
+ }
895
+ return;
896
+ }
897
+ if (type === "response.function_call_arguments.done") {
898
+ if (typeof record.item_id !== "string" ||
899
+ record.item_id.length === 0 ||
900
+ typeof record.arguments !== "string") {
901
+ throw invalidOpenAIResponsesStream(context, "completed function-call arguments malformed");
902
+ }
903
+ const state = functionCalls.get(record.item_id);
904
+ if (!state) {
905
+ throw invalidOpenAIResponsesStream(context, "completed function-call arguments referenced an unknown item");
906
+ }
907
+ assertOutputIndex(record.item_id, record, "completed function-call arguments");
908
+ if (record.name !== undefined && (typeof record.name !== "string" || record.name !== state.name)) {
909
+ throw invalidOpenAIResponsesStream(context, "completed function-call arguments name changed");
910
+ }
911
+ if (state.argumentsDone) {
912
+ throw invalidOpenAIResponsesStream(context, "function-call arguments completed twice");
913
+ }
914
+ const streamedArguments = joinOpenAIStreamToolArguments(state.argumentChunks);
915
+ if (state.sawNonEmptyArgumentDelta && streamedArguments !== record.arguments) {
916
+ throw invalidOpenAIResponsesStream(context, "completed function-call arguments disagreed with streamed deltas");
917
+ }
918
+ if (!state.sawNonEmptyArgumentDelta) {
919
+ appendFunctionCallArgument(state, record.arguments);
920
+ }
921
+ state.argumentsDone = true;
922
+ return;
923
+ }
924
+ if (type === "response.output_item.done") {
925
+ const { item, itemType, itemId } = readEventItem(record, "completed output");
926
+ const outputItem = outputItems.get(itemId);
927
+ if (!outputItem) {
928
+ throw invalidOpenAIResponsesStream(context, "completed output referenced an unknown item");
929
+ }
930
+ if (outputItem.type !== itemType) {
931
+ throw invalidOpenAIResponsesStream(context, "completed output item type changed");
932
+ }
933
+ assertOutputIndex(itemId, record, "completed output item");
934
+ const validStatus = item.status === undefined ||
935
+ item.status === "completed" ||
936
+ item.status === "incomplete" ||
937
+ (itemType === "web_search_call" && item.status === "failed");
938
+ if (!validStatus) {
939
+ throw invalidOpenAIResponsesStream(context, "completed output item status was malformed");
940
+ }
941
+ if (itemType === "reasoning") {
942
+ const state = reasoningBlocks.get(itemId);
943
+ if (!state) {
944
+ throw invalidOpenAIResponsesStream(context, "completed reasoning referenced an unknown item");
945
+ }
946
+ yield* reconcileFinalReasoningParts(state, item.summary, state.summaryParts, "summary_text", "summary");
947
+ yield* reconcileFinalReasoningParts(state, item.content, state.contentParts, "reasoning_text", "content");
948
+ if (state.emittedStart) {
949
+ yield { type: "reasoning-end", id: state.id };
950
+ }
951
+ releaseReasoningState(state);
952
+ reasoningBlocks.delete(itemId);
953
+ }
954
+ else if (itemType === "function_call") {
955
+ const state = functionCalls.get(itemId);
956
+ if (!state) {
957
+ throw invalidOpenAIResponsesStream(context, "completed function call referenced an unknown item");
958
+ }
959
+ if (typeof item.call_id !== "string" ||
960
+ item.call_id.length === 0 ||
961
+ item.call_id !== state.toolCallId) {
962
+ throw invalidOpenAIResponsesStream(context, "completed function call id changed");
963
+ }
964
+ if (typeof item.name !== "string" ||
965
+ item.name.length === 0 ||
966
+ item.name !== state.name) {
967
+ throw invalidOpenAIResponsesStream(context, "completed function call name changed");
968
+ }
969
+ if (typeof item.arguments !== "string" ||
970
+ (item.status !== undefined &&
971
+ item.status !== "completed" &&
972
+ item.status !== "incomplete")) {
973
+ throw invalidOpenAIResponsesStream(context, "completed function call arguments or status were malformed");
974
+ }
975
+ const streamedArguments = joinOpenAIStreamToolArguments(state.argumentChunks);
976
+ if ((state.argumentsDone || state.sawNonEmptyArgumentDelta) &&
977
+ streamedArguments !== item.arguments) {
978
+ throw invalidOpenAIResponsesStream(context, "completed function call arguments disagreed with streamed deltas");
979
+ }
980
+ if (!state.argumentsDone && !state.sawNonEmptyArgumentDelta) {
981
+ appendFunctionCallArgument(state, item.arguments);
982
+ }
983
+ state.argumentsDone = true;
984
+ const input = assertFunctionCallInput(state, context);
985
+ if (!state.emittedStart) {
986
+ yield {
987
+ type: "tool-input-start",
988
+ id: state.toolCallId,
989
+ toolName: state.name,
990
+ };
172
991
  yield {
173
992
  type: "tool-input-delta",
174
993
  id: state.toolCallId,
175
- delta: record.delta,
994
+ delta: input,
176
995
  };
177
996
  }
178
- continue;
179
- }
180
- // response.output_item.done: an item has finished emitting deltas.
181
- if (type === "response.output_item.done") {
182
- const item = readRecord(record?.item);
183
- const itemType = typeof item?.type === "string" ? item.type : undefined;
184
- const itemId = typeof item?.id === "string" ? item.id : undefined;
185
- if (itemType === "reasoning" && itemId) {
186
- const state = reasoningBlocks.get(itemId);
187
- if (state?.emittedStart) {
188
- yield { type: "reasoning-end", id: state.id };
997
+ yield {
998
+ type: "tool-call",
999
+ toolCallId: state.toolCallId,
1000
+ toolName: state.name,
1001
+ input,
1002
+ };
1003
+ functionCalls.delete(itemId);
1004
+ }
1005
+ else if (itemType === "web_search_call") {
1006
+ const state = webSearchItems.get(itemId);
1007
+ if (!state || !context.webSearchToolName) {
1008
+ throw invalidOpenAIResponsesStream(context, "completed web-search call referenced an unknown item");
1009
+ }
1010
+ const webSearch = normalizeOpenAIWebSearchCall(item, (issue) => invalidOpenAIResponsesStream(context, issue));
1011
+ yield {
1012
+ type: "tool-input-delta",
1013
+ id: webSearch.id,
1014
+ delta: webSearch.input,
1015
+ };
1016
+ yield {
1017
+ type: "tool-call",
1018
+ toolCallId: webSearch.id,
1019
+ toolName: context.webSearchToolName,
1020
+ input: webSearch.input,
1021
+ providerExecuted: true,
1022
+ };
1023
+ yield {
1024
+ type: "tool-result",
1025
+ toolCallId: webSearch.id,
1026
+ toolName: context.webSearchToolName,
1027
+ result: webSearch.result,
1028
+ ...(webSearch.isError ? { isError: true } : {}),
1029
+ providerExecuted: true,
1030
+ };
1031
+ webSearchItems.delete(itemId);
1032
+ }
1033
+ else if (itemType === "message") {
1034
+ const state = messageItems.get(itemId);
1035
+ if (!state) {
1036
+ throw invalidOpenAIResponsesStream(context, "completed message referenced an unknown item");
1037
+ }
1038
+ if (item.role !== "assistant" ||
1039
+ !Array.isArray(item.content) ||
1040
+ (item.status !== undefined &&
1041
+ item.status !== "completed" &&
1042
+ item.status !== "incomplete")) {
1043
+ throw invalidOpenAIResponsesStream(context, "completed message role, content, or status were malformed");
1044
+ }
1045
+ const finalContentIndices = new Set();
1046
+ for (const [contentIndex, rawPart] of item.content.entries()) {
1047
+ const part = readRecord(rawPart);
1048
+ if (!part) {
1049
+ throw invalidOpenAIResponsesStream(context, "completed message content part was not an object");
189
1050
  }
190
- reasoningBlocks.delete(itemId);
191
- }
192
- if (itemType === "function_call" && itemId) {
193
- const state = functionCalls.get(itemId);
194
- if (state) {
195
- yield {
196
- type: "tool-call",
197
- toolCallId: state.toolCallId,
198
- toolName: state.name,
199
- input: state.arguments,
200
- };
1051
+ const kind = validateMessageContentPart(part, "completed message content part");
1052
+ const partState = getOrCreateMessagePart(state, contentIndex, kind, "completed message content part");
1053
+ if (partState.contentPartAdded &&
1054
+ (!partState.valueDone || !partState.contentPartDone)) {
1055
+ throw invalidOpenAIResponsesStream(context, "completed message had an unfinished content part");
1056
+ }
1057
+ const finalValue = kind === "output_text" ? part.text : part.refusal;
1058
+ const missingDelta = reconcileRetainedTextValue(partState, finalValue, "completed message content part");
1059
+ if (missingDelta !== undefined) {
1060
+ yield { type: "text-delta", delta: missingDelta };
1061
+ }
1062
+ reconcileMessageAnnotations(partState, part, finalValue, "completed message content part");
1063
+ finalContentIndices.add(contentIndex);
1064
+ }
1065
+ for (const contentIndex of state.parts.keys()) {
1066
+ if (!finalContentIndices.has(contentIndex)) {
1067
+ throw invalidOpenAIResponsesStream(context, "completed message omitted a streamed content part");
201
1068
  }
202
- functionCalls.delete(itemId);
203
1069
  }
204
- continue;
1070
+ releaseMessageState(state);
1071
+ messageItems.delete(itemId);
205
1072
  }
206
- // response.completed: terminal event with the final response object.
207
- if (type === "response.completed") {
208
- usage = extractOpenAIResponsesUsage(record) ?? usage;
209
- const responseRecord = readRecord(record?.response);
210
- finishReason = normalizeOpenAIResponsesFinishReason(responseRecord?.status);
211
- continue;
1073
+ else {
1074
+ throw invalidOpenAIResponsesStream(context, "completed output item type was unsupported");
212
1075
  }
213
- if (type === "response.failed" || type === "response.incomplete") {
214
- const responseRecord = readRecord(record?.response);
215
- finishReason = normalizeOpenAIResponsesFinishReason(responseRecord?.status) ??
216
- (type === "response.failed"
217
- ? { unified: "error", raw: "failed" }
218
- : { unified: "length", raw: "incomplete" });
219
- usage = extractOpenAIResponsesUsage(record) ?? usage;
220
- continue;
1076
+ retainCompletedOutputItem(outputItem, item);
1077
+ outputItems.delete(itemId);
1078
+ return;
1079
+ }
1080
+ if (type === "response.completed" ||
1081
+ type === "response.failed" ||
1082
+ type === "response.incomplete") {
1083
+ if (sawTerminalEvent) {
1084
+ throw invalidOpenAIResponsesStream(context, "stream contained multiple terminal events");
1085
+ }
1086
+ if (outputItems.size > 0) {
1087
+ throw invalidOpenAIResponsesStream(context, "terminal response arrived with unfinished output items");
1088
+ }
1089
+ const responseRecord = readRecord(record.response);
1090
+ if (!responseRecord || typeof responseRecord.status !== "string") {
1091
+ throw invalidOpenAIResponsesStream(context, "terminal response status was missing");
1092
+ }
1093
+ const expectedStatus = type.slice("response.".length);
1094
+ if (responseRecord.status !== expectedStatus) {
1095
+ throw invalidOpenAIResponsesStream(context, "terminal response status did not match its event type");
221
1096
  }
1097
+ finishReason = normalizeOpenAIResponsesFinishReason(responseRecord.status);
1098
+ usage = mergeOpenAIResponsesUsage(usage, record);
1099
+ sawTerminalEvent = true;
1100
+ return;
222
1101
  }
1102
+ if (type === "response.created" ||
1103
+ type === "response.in_progress" ||
1104
+ type === "response.queued") {
1105
+ const response = readRecord(record.response);
1106
+ const expectedStatus = type === "response.created"
1107
+ ? "in_progress"
1108
+ : type.slice("response.".length);
1109
+ if (!response || response.status !== expectedStatus) {
1110
+ throw invalidOpenAIResponsesStream(context, "response lifecycle status did not match its event type");
1111
+ }
1112
+ return;
1113
+ }
1114
+ throw invalidOpenAIResponsesStream(context, `event type ${type} was unsupported`);
223
1115
  }
224
- // Close any reasoning streams still open at end-of-stream (defensive).
225
- for (const state of reasoningBlocks.values()) {
226
- if (state.emittedStart) {
227
- yield { type: "reasoning-end", id: state.id };
1116
+ for await (const chunk of stream) {
1117
+ buffer = appendOpenAISseChunk(decoder, buffer, chunk, (issue) => invalidOpenAIResponsesStream(context, issue));
1118
+ const parsed = parseOpenAISseBuffer(buffer, (issue) => invalidOpenAIResponsesStream(context, issue));
1119
+ buffer = parsed.remainder;
1120
+ for (const event of parsed.events) {
1121
+ yield* processEvent(event);
1122
+ }
1123
+ }
1124
+ buffer = finishOpenAISseDecoding(decoder, buffer, (issue) => invalidOpenAIResponsesStream(context, issue));
1125
+ if (buffer.trim().length > 0) {
1126
+ const parsed = parseOpenAISseBuffer(buffer, (issue) => invalidOpenAIResponsesStream(context, issue), true);
1127
+ for (const event of parsed.events) {
1128
+ yield* processEvent(event);
1129
+ }
1130
+ }
1131
+ if (!sawTerminalEvent) {
1132
+ throw invalidOpenAIResponsesStream(context, "stream ended before a terminal response event");
1133
+ }
1134
+ if (outputItems.size > 0 ||
1135
+ reasoningBlocks.size > 0 ||
1136
+ functionCalls.size > 0 ||
1137
+ messageItems.size > 0 ||
1138
+ webSearchItems.size > 0) {
1139
+ throw invalidOpenAIResponsesStream(context, "stream ended with unfinished output items");
1140
+ }
1141
+ let providerMetadata;
1142
+ if (retainRawOutputItems && rawOutputItemsByOrder.length > 0) {
1143
+ if (rawOutputItemsByOrder.length !== seenOutputItemIds.size ||
1144
+ rawOutputItemsByOrder.some((item) => item === undefined)) {
1145
+ throw invalidOpenAIResponsesStream(context, "raw response metadata omitted a completed output item");
1146
+ }
1147
+ const rawOutputEntries = rawOutputItemsByOrder;
1148
+ const indexedEntryCount = rawOutputEntries.filter((entry) => entry.outputIndex !== undefined).length;
1149
+ if (indexedEntryCount !== 0 &&
1150
+ indexedEntryCount !== rawOutputEntries.length) {
1151
+ throw invalidOpenAIResponsesStream(context, "raw response output order was only partially indexed");
1152
+ }
1153
+ const orderedEntries = indexedEntryCount === rawOutputEntries.length
1154
+ ? rawOutputEntries.toSorted((left, right) => left.outputIndex - right.outputIndex)
1155
+ : rawOutputEntries.toSorted((left, right) => left.order - right.order);
1156
+ if (indexedEntryCount > 0 &&
1157
+ orderedEntries.some((entry, index) => entry.outputIndex !== index)) {
1158
+ throw invalidOpenAIResponsesStream(context, "raw response output indexes were not contiguous");
1159
+ }
1160
+ const candidateMetadata = createOpenAIRawResponseMetadata(orderedEntries.map((entry) => entry.item));
1161
+ try {
1162
+ readOpenAIRawResponseOutputItems(candidateMetadata);
1163
+ }
1164
+ catch {
1165
+ throw invalidOpenAIResponsesStream(context, "raw response output items were unsafe to replay");
228
1166
  }
1167
+ providerMetadata = candidateMetadata;
229
1168
  }
230
1169
  yield {
231
1170
  type: "finish",
232
1171
  finishReason,
233
1172
  ...(usage ? { usage } : {}),
1173
+ ...(providerMetadata ? { providerMetadata } : {}),
234
1174
  };
235
1175
  }