@tanstack/openai-base 0.9.9 → 0.9.10

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 (44) hide show
  1. package/dist/esm/adapters/chat-completions-text.js +844 -962
  2. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  3. package/dist/esm/adapters/chat-completions-tool-converter.js +54 -38
  4. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  5. package/dist/esm/adapters/responses-text.js +1171 -1319
  6. package/dist/esm/adapters/responses-text.js.map +1 -1
  7. package/dist/esm/adapters/responses-tool-converter.js +50 -34
  8. package/dist/esm/adapters/responses-tool-converter.js.map +1 -1
  9. package/dist/esm/index.js +4 -43
  10. package/dist/esm/tools/apply-patch-tool.js +20 -13
  11. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  12. package/dist/esm/tools/code-interpreter-tool.js +26 -18
  13. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  14. package/dist/esm/tools/computer-use-tool.js +26 -19
  15. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  16. package/dist/esm/tools/custom-tool.js +23 -21
  17. package/dist/esm/tools/custom-tool.js.map +1 -1
  18. package/dist/esm/tools/file-search-tool.js +30 -30
  19. package/dist/esm/tools/file-search-tool.js.map +1 -1
  20. package/dist/esm/tools/function-tool.js +44 -31
  21. package/dist/esm/tools/function-tool.js.map +1 -1
  22. package/dist/esm/tools/image-generation-tool.js +30 -23
  23. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  24. package/dist/esm/tools/local-shell-tool.js +20 -13
  25. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  26. package/dist/esm/tools/mcp-tool.js +31 -25
  27. package/dist/esm/tools/mcp-tool.js.map +1 -1
  28. package/dist/esm/tools/shell-tool.js +25 -21
  29. package/dist/esm/tools/shell-tool.js.map +1 -1
  30. package/dist/esm/tools/tool-converter.js +37 -46
  31. package/dist/esm/tools/tool-converter.js.map +1 -1
  32. package/dist/esm/tools/web-search-preview-tool.js +26 -15
  33. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  34. package/dist/esm/tools/web-search-tool.js +27 -15
  35. package/dist/esm/tools/web-search-tool.js.map +1 -1
  36. package/dist/esm/usage.js +88 -76
  37. package/dist/esm/usage.js.map +1 -1
  38. package/dist/esm/utils/request-options.js +20 -9
  39. package/dist/esm/utils/request-options.js.map +1 -1
  40. package/dist/esm/utils/schema-converter.js +193 -146
  41. package/dist/esm/utils/schema-converter.js.map +1 -1
  42. package/package.json +7 -7
  43. package/src/adapters/responses-text.ts +5 -2
  44. package/dist/esm/index.js.map +0 -1
@@ -1,1324 +1,1176 @@
1
+ import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
2
+ import { buildResponsesUsage } from "../usage.js";
3
+ import { extractRequestOptions } from "../utils/request-options.js";
4
+ import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
1
5
  import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
2
6
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
3
7
  import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
4
8
  import { generateId } from "@tanstack/ai-utils";
5
- import { extractRequestOptions } from "../utils/request-options.js";
6
- import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
7
- import { buildResponsesUsage } from "../usage.js";
8
- import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
9
- class OpenAIBaseResponsesTextAdapter extends BaseTextAdapter {
10
- kind = "text";
11
- name;
12
- client;
13
- constructor(model, name, client) {
14
- super({}, model);
15
- this.name = name;
16
- this.client = client;
17
- }
18
- async *chatStream(options) {
19
- const toolCallMetadata = /* @__PURE__ */ new Map();
20
- const aguiState = {
21
- runId: generateId(this.name),
22
- threadId: options.threadId ?? generateId(this.name),
23
- messageId: generateId(this.name),
24
- hasEmittedRunStarted: false
25
- };
26
- try {
27
- const requestParams = this.mapOptionsToRequest(options);
28
- options.logger.request(
29
- `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
30
- { provider: this.name, model: this.model }
31
- );
32
- const response = await this.client.responses.create(
33
- {
34
- ...requestParams,
35
- stream: true
36
- },
37
- extractRequestOptions(options.request)
38
- );
39
- yield* this.processStreamChunks(
40
- response,
41
- toolCallMetadata,
42
- options,
43
- aguiState
44
- );
45
- } catch (error) {
46
- const errorPayload = toRunErrorPayload(
47
- error,
48
- `${this.name}.chatStream failed`
49
- );
50
- const rawEvent = toRunErrorRawEvent(error);
51
- if (!aguiState.hasEmittedRunStarted) {
52
- aguiState.hasEmittedRunStarted = true;
53
- yield {
54
- type: EventType.RUN_STARTED,
55
- runId: aguiState.runId,
56
- threadId: aguiState.threadId,
57
- model: options.model,
58
- timestamp: Date.now(),
59
- parentRunId: options.parentRunId
60
- };
61
- }
62
- yield {
63
- type: EventType.RUN_ERROR,
64
- model: options.model,
65
- timestamp: Date.now(),
66
- message: errorPayload.message,
67
- code: errorPayload.code,
68
- // Forward the provider's structured error body when present (see
69
- // toRunErrorRawEvent); omitted otherwise.
70
- ...rawEvent !== void 0 && { rawEvent },
71
- error: {
72
- message: errorPayload.message,
73
- code: errorPayload.code
74
- }
75
- };
76
- options.logger.errors(`${this.name}.chatStream fatal`, {
77
- error: errorPayload,
78
- source: `${this.name}.chatStream`
79
- });
80
- }
81
- }
82
- /**
83
- * Generate structured output using the provider's native JSON Schema response format.
84
- * Uses stream: false to get the complete response in one call.
85
- *
86
- * OpenAI-compatible Responses APIs have strict requirements for structured output:
87
- * - All properties must be in the `required` array
88
- * - Optional fields should have null added to their type union
89
- * - additionalProperties must be false for all objects
90
- *
91
- * The outputSchema is already JSON Schema (converted in the ai layer).
92
- * We apply provider-specific transformations for structured output compatibility.
93
- */
94
- async structuredOutput(options) {
95
- const { chatOptions, outputSchema } = options;
96
- const requestParams = this.mapOptionsToRequest(chatOptions);
97
- const jsonSchema = this.makeStructuredOutputCompatible(
98
- outputSchema,
99
- outputSchema.required
100
- );
101
- try {
102
- const {
103
- stream: _stream,
104
- stream_options: _streamOptions,
105
- ...cleanParams
106
- } = requestParams;
107
- void _stream;
108
- void _streamOptions;
109
- chatOptions.logger.request(
110
- `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
111
- { provider: this.name, model: this.model }
112
- );
113
- const response = await this.client.responses.create(
114
- {
115
- ...cleanParams,
116
- stream: false,
117
- // Configure structured output via text.format
118
- text: {
119
- format: {
120
- type: "json_schema",
121
- name: "structured_output",
122
- schema: jsonSchema,
123
- strict: true
124
- }
125
- }
126
- },
127
- extractRequestOptions(chatOptions.request)
128
- );
129
- const rawText = this.extractTextFromResponse(response);
130
- if (rawText.length === 0) {
131
- throw new Error(
132
- `${this.name}.structuredOutput: response contained no content`
133
- );
134
- }
135
- let parsed;
136
- try {
137
- parsed = JSON.parse(rawText);
138
- } catch {
139
- throw new Error(
140
- `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
141
- );
142
- }
143
- const transformed = this.transformStructuredOutput(parsed);
144
- return {
145
- data: transformed,
146
- rawText
147
- };
148
- } catch (error) {
149
- chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
150
- error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
151
- source: `${this.name}.structuredOutput`
152
- });
153
- throw error;
154
- }
155
- }
156
- /**
157
- * Stream structured output via the Responses API: single request with
158
- * `text.format: json_schema` + `stream: true`. Consumes Responses-API
159
- * events (`response.output_text.delta`, `response.reasoning_text.delta`,
160
- * `response.reasoning_summary_text.delta`, `response.refusal.delta`,
161
- * `response.completed`, `response.failed`) and re-emits the standard AG-UI
162
- * lifecycle ending with `CUSTOM 'structured-output.complete'`.
163
- *
164
- * Tools are stripped (structured output is mutually exclusive with tool
165
- * calls in this path). Reasoning text is accumulated and surfaced both as
166
- * REASONING_* lifecycle events during the stream and on the terminal
167
- * CUSTOM event's `value.reasoning`.
168
- */
169
- async *structuredOutputStream(options) {
170
- const { chatOptions, outputSchema } = options;
171
- const requestParams = this.mapOptionsToRequest(chatOptions);
172
- const jsonSchema = this.makeStructuredOutputCompatible(
173
- outputSchema,
174
- outputSchema.required
175
- );
176
- const timestamp = Date.now();
177
- const aguiState = {
178
- runId: generateId(this.name),
179
- threadId: chatOptions.threadId ?? generateId(this.name),
180
- messageId: generateId(this.name),
181
- timestamp,
182
- hasEmittedRunStarted: false
183
- };
184
- let accumulatedContent = "";
185
- let accumulatedReasoning = "";
186
- let hasEmittedTextMessageStart = false;
187
- let reasoningMessageId;
188
- let stepId;
189
- let hasClosedReasoning = false;
190
- let model = chatOptions.model;
191
- let usage;
192
- const closeReasoning = (function* () {
193
- if (reasoningMessageId && !hasClosedReasoning) {
194
- hasClosedReasoning = true;
195
- yield {
196
- type: EventType.REASONING_MESSAGE_END,
197
- messageId: reasoningMessageId,
198
- model,
199
- timestamp
200
- };
201
- yield {
202
- type: EventType.REASONING_END,
203
- messageId: reasoningMessageId,
204
- model,
205
- timestamp
206
- };
207
- if (stepId) {
208
- yield {
209
- type: EventType.STEP_FINISHED,
210
- stepName: stepId,
211
- stepId,
212
- model,
213
- timestamp,
214
- content: accumulatedReasoning
215
- };
216
- }
217
- }
218
- }).bind(this);
219
- const openReasoning = (function* () {
220
- if (reasoningMessageId) return;
221
- reasoningMessageId = generateId(this.name);
222
- stepId = generateId(this.name);
223
- yield {
224
- type: EventType.REASONING_START,
225
- messageId: reasoningMessageId,
226
- model,
227
- timestamp
228
- };
229
- yield {
230
- type: EventType.REASONING_MESSAGE_START,
231
- messageId: reasoningMessageId,
232
- role: "reasoning",
233
- model,
234
- timestamp
235
- };
236
- yield {
237
- type: EventType.STEP_STARTED,
238
- stepName: stepId,
239
- stepId,
240
- model,
241
- timestamp,
242
- stepType: "thinking"
243
- };
244
- }).bind(this);
245
- try {
246
- const { tools: _tools, ...cleanParams } = requestParams;
247
- void _tools;
248
- chatOptions.logger.request(
249
- `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
250
- { provider: this.name, model: this.model }
251
- );
252
- const stream = await this.client.responses.create(
253
- {
254
- ...cleanParams,
255
- stream: true,
256
- text: {
257
- format: {
258
- type: "json_schema",
259
- name: "structured_output",
260
- schema: jsonSchema,
261
- strict: true
262
- }
263
- }
264
- },
265
- extractRequestOptions(chatOptions.request)
266
- );
267
- for await (const chunk of stream) {
268
- chatOptions.logger.provider(
269
- `provider=${this.name} type=${chunk.type}`,
270
- { provider: this.name, type: chunk.type }
271
- );
272
- if (!aguiState.hasEmittedRunStarted) {
273
- aguiState.hasEmittedRunStarted = true;
274
- yield {
275
- type: EventType.RUN_STARTED,
276
- runId: aguiState.runId,
277
- threadId: aguiState.threadId,
278
- model,
279
- timestamp,
280
- parentRunId: chatOptions.parentRunId
281
- };
282
- }
283
- if (chunk.type === "response.created" || chunk.type === "response.in_progress") {
284
- const responseModel = chunk.response?.model;
285
- if (responseModel) model = responseModel;
286
- continue;
287
- }
288
- if (chunk.type === "response.refusal.delta") {
289
- const delta = typeof chunk.delta === "string" ? chunk.delta : "";
290
- yield {
291
- type: EventType.RUN_ERROR,
292
- runId: aguiState.runId,
293
- model,
294
- timestamp,
295
- message: `Model refused: ${delta}`,
296
- code: "refusal",
297
- error: { message: `Model refused: ${delta}`, code: "refusal" }
298
- };
299
- return;
300
- }
301
- if (chunk.type === "response.reasoning_text.delta" || chunk.type === "response.reasoning_summary_text.delta") {
302
- const raw = chunk.delta;
303
- const reasoningDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
304
- if (!reasoningDelta) continue;
305
- yield* openReasoning();
306
- if (!reasoningMessageId) continue;
307
- accumulatedReasoning += reasoningDelta;
308
- yield {
309
- type: EventType.REASONING_MESSAGE_CONTENT,
310
- messageId: reasoningMessageId,
311
- delta: reasoningDelta,
312
- model,
313
- timestamp
314
- };
315
- continue;
316
- }
317
- if (chunk.type === "response.output_text.delta") {
318
- const raw = chunk.delta;
319
- const textDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
320
- if (!textDelta) continue;
321
- yield* closeReasoning();
322
- if (!hasEmittedTextMessageStart) {
323
- hasEmittedTextMessageStart = true;
324
- yield {
325
- type: EventType.TEXT_MESSAGE_START,
326
- messageId: aguiState.messageId,
327
- model,
328
- timestamp,
329
- role: "assistant"
330
- };
331
- }
332
- accumulatedContent += textDelta;
333
- yield {
334
- type: EventType.TEXT_MESSAGE_CONTENT,
335
- messageId: aguiState.messageId,
336
- model,
337
- timestamp,
338
- delta: textDelta,
339
- content: accumulatedContent
340
- };
341
- continue;
342
- }
343
- if (chunk.type === "response.completed") {
344
- const response = chunk.response;
345
- if (response.usage) usage = response.usage;
346
- if (response.model) model = response.model;
347
- continue;
348
- }
349
- if (chunk.type === "response.failed") {
350
- const response = chunk.response;
351
- const message = response?.error?.message || "Responses API stream failed";
352
- const code = response?.error?.code;
353
- yield {
354
- type: EventType.RUN_ERROR,
355
- runId: aguiState.runId,
356
- model,
357
- timestamp,
358
- message,
359
- ...code !== void 0 && { code },
360
- error: { message, ...code !== void 0 && { code } }
361
- };
362
- return;
363
- }
364
- }
365
- yield* closeReasoning();
366
- if (hasEmittedTextMessageStart) {
367
- yield {
368
- type: EventType.TEXT_MESSAGE_END,
369
- messageId: aguiState.messageId,
370
- model,
371
- timestamp
372
- };
373
- }
374
- if (accumulatedContent.length === 0) {
375
- yield {
376
- type: EventType.RUN_ERROR,
377
- runId: aguiState.runId,
378
- model,
379
- timestamp,
380
- message: `${this.name}.structuredOutputStream: response contained no content`,
381
- code: "empty-response",
382
- error: {
383
- message: `${this.name}.structuredOutputStream: response contained no content`,
384
- code: "empty-response"
385
- }
386
- };
387
- return;
388
- }
389
- let parsed;
390
- try {
391
- parsed = JSON.parse(accumulatedContent);
392
- } catch {
393
- yield {
394
- type: EventType.RUN_ERROR,
395
- runId: aguiState.runId,
396
- model,
397
- timestamp,
398
- message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
399
- code: "parse-error",
400
- error: {
401
- message: "Failed to parse structured output as JSON",
402
- code: "parse-error"
403
- }
404
- };
405
- return;
406
- }
407
- const transformed = this.transformStructuredOutput(parsed);
408
- yield {
409
- type: EventType.CUSTOM,
410
- name: "structured-output.complete",
411
- value: {
412
- object: transformed,
413
- raw: accumulatedContent,
414
- ...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
415
- },
416
- model,
417
- timestamp
418
- };
419
- yield {
420
- type: EventType.RUN_FINISHED,
421
- runId: aguiState.runId,
422
- threadId: aguiState.threadId,
423
- model,
424
- timestamp,
425
- finishReason: "stop",
426
- ...usage && {
427
- usage: buildResponsesUsage(usage)
428
- }
429
- };
430
- } catch (error) {
431
- if (!aguiState.hasEmittedRunStarted) {
432
- aguiState.hasEmittedRunStarted = true;
433
- yield {
434
- type: EventType.RUN_STARTED,
435
- runId: aguiState.runId,
436
- threadId: aguiState.threadId,
437
- model,
438
- timestamp,
439
- parentRunId: chatOptions.parentRunId
440
- };
441
- }
442
- const isAbort = this.isAbortError(error);
443
- const errorPayload = toRunErrorPayload(
444
- error,
445
- `${this.name}.structuredOutputStream failed`
446
- );
447
- const resolvedCode = isAbort ? "aborted" : errorPayload.code;
448
- const rawEvent = isAbort ? void 0 : toRunErrorRawEvent(error);
449
- yield {
450
- type: EventType.RUN_ERROR,
451
- runId: aguiState.runId,
452
- model,
453
- timestamp,
454
- message: errorPayload.message,
455
- ...resolvedCode !== void 0 && { code: resolvedCode },
456
- ...rawEvent !== void 0 && { rawEvent },
457
- error: {
458
- message: errorPayload.message,
459
- ...resolvedCode !== void 0 && { code: resolvedCode }
460
- }
461
- };
462
- chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
463
- error: errorPayload,
464
- source: `${this.name}.structuredOutputStream`
465
- });
466
- }
467
- }
468
- /**
469
- * Cross-SDK abort detection for `structuredOutputStream`. Mirrors the
470
- * Chat Completions base; subclasses with proprietary error types override.
471
- */
472
- isAbortError(error) {
473
- if (!error || typeof error !== "object") return false;
474
- const e = error;
475
- return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
476
- }
477
- /**
478
- * Applies provider-specific transformations for structured output compatibility.
479
- * Override this in subclasses to handle provider-specific quirks.
480
- */
481
- makeStructuredOutputCompatible(schema, originalRequired) {
482
- return makeStructuredOutputCompatible(schema, originalRequired);
483
- }
484
- /**
485
- * Final shaping pass applied to parsed structured-output JSON before it is
486
- * returned to the caller. Default is a passthrough.
487
- *
488
- * Provider `null`s are no longer stripped here: strict-mode null-widening is
489
- * now undone precisely by the engine (`undoNullWidening`, driven by the
490
- * schema's null-widening map) the moment the result is captured, so a blind
491
- * `transformNullsToUndefined` at the adapter would only destroy genuine
492
- * `.nullable()` nulls. Subclasses may still override to remap or reshape the
493
- * provider's structured output.
494
- */
495
- transformStructuredOutput(parsed) {
496
- return parsed;
497
- }
498
- /**
499
- * Extract text content from a non-streaming Responses API response.
500
- * Override this in subclasses for provider-specific response shapes.
501
- */
502
- extractTextFromResponse(response) {
503
- let textContent = "";
504
- let refusal;
505
- let sawMessageItem = false;
506
- const observedItemTypes = /* @__PURE__ */ new Set();
507
- for (const item of response.output) {
508
- observedItemTypes.add(item.type);
509
- if (item.type === "message") {
510
- sawMessageItem = true;
511
- for (const part of item.content) {
512
- const partType = part.type;
513
- if (partType === "output_text") {
514
- textContent += part.text ?? "";
515
- } else if (partType === "refusal") {
516
- const refusalText = part.refusal;
517
- refusal = refusalText || refusal || "Refused without explanation";
518
- } else {
519
- throw new Error(
520
- `${this.name}.extractTextFromResponse: unsupported message content part type "${partType}"`
521
- );
522
- }
523
- }
524
- }
525
- }
526
- if (!textContent && refusal !== void 0) {
527
- const err = new Error(`Model refused to respond: ${refusal}`);
528
- err.code = "refusal";
529
- throw err;
530
- }
531
- if (!textContent && response.output.length > 0 && !sawMessageItem) {
532
- throw new Error(
533
- `${this.name}.extractTextFromResponse: response.output contained items of type(s) [${[...observedItemTypes].sort().join(", ")}] but no message text — the model returned a non-text response`
534
- );
535
- }
536
- return textContent;
537
- }
538
- /**
539
- * Processes streamed chunks from the Responses API and yields AG-UI events.
540
- * Override this in subclasses to handle provider-specific stream behavior.
541
- *
542
- * Handles the following event types:
543
- * - response.created / response.incomplete / response.failed
544
- * - response.output_text.delta
545
- * - response.reasoning_text.delta
546
- * - response.reasoning_summary_text.delta
547
- * - response.content_part.added / response.content_part.done
548
- * - response.output_item.added
549
- * - response.function_call_arguments.delta / response.function_call_arguments.done
550
- * - response.completed
551
- * - error
552
- */
553
- async *processStreamChunks(stream, toolCallMetadata, options, aguiState) {
554
- let accumulatedContent = "";
555
- let accumulatedReasoning = "";
556
- let hasStreamedContentDeltas = false;
557
- let hasStreamedReasoningDeltas = false;
558
- let model = options.model;
559
- let stepId = null;
560
- let hasEmittedTextMessageStart = false;
561
- let hasEmittedStepStarted = false;
562
- let runFinishedEmitted = false;
563
- try {
564
- for await (const chunk of stream) {
565
- options.logger.provider(`provider=${this.name} type=${chunk.type}`, {
566
- provider: this.name,
567
- type: chunk.type
568
- });
569
- if (!aguiState.hasEmittedRunStarted) {
570
- aguiState.hasEmittedRunStarted = true;
571
- yield {
572
- type: EventType.RUN_STARTED,
573
- runId: aguiState.runId,
574
- threadId: aguiState.threadId,
575
- model: model || options.model,
576
- timestamp: Date.now(),
577
- parentRunId: options.parentRunId
578
- };
579
- }
580
- const handleContentPart = (contentPart) => {
581
- if (contentPart.type === "output_text") {
582
- accumulatedContent += contentPart.text || "";
583
- return {
584
- type: EventType.TEXT_MESSAGE_CONTENT,
585
- messageId: aguiState.messageId,
586
- model: model || options.model,
587
- timestamp: Date.now(),
588
- delta: contentPart.text || "",
589
- content: accumulatedContent
590
- };
591
- }
592
- if (contentPart.type === "reasoning_text") {
593
- accumulatedReasoning += contentPart.text || "";
594
- if (!stepId) {
595
- stepId = generateId(this.name);
596
- }
597
- return {
598
- type: EventType.STEP_FINISHED,
599
- stepName: stepId,
600
- stepId,
601
- model: model || options.model,
602
- timestamp: Date.now(),
603
- delta: contentPart.text || "",
604
- content: accumulatedReasoning
605
- };
606
- }
607
- const isRefusal = contentPart.type === "refusal";
608
- const message = isRefusal ? contentPart.refusal || "Refused without explanation" : `Unsupported response content_part type: ${contentPart.type}`;
609
- const code = isRefusal ? "refusal" : contentPart.type;
610
- return {
611
- type: EventType.RUN_ERROR,
612
- model: model || options.model,
613
- timestamp: Date.now(),
614
- message,
615
- code,
616
- error: { message, code }
617
- };
618
- };
619
- if (chunk.type === "response.created" || chunk.type === "response.incomplete" || chunk.type === "response.failed") {
620
- model = chunk.response.model;
621
- }
622
- if (chunk.type === "response.created") {
623
- hasStreamedContentDeltas = false;
624
- hasStreamedReasoningDeltas = false;
625
- hasEmittedTextMessageStart = false;
626
- hasEmittedStepStarted = false;
627
- accumulatedContent = "";
628
- accumulatedReasoning = "";
629
- }
630
- if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
631
- if (hasEmittedTextMessageStart) {
632
- yield {
633
- type: EventType.TEXT_MESSAGE_END,
634
- messageId: aguiState.messageId,
635
- model: chunk.response.model,
636
- timestamp: Date.now()
637
- };
638
- hasEmittedTextMessageStart = false;
639
- }
640
- const errorMessage = chunk.response.error?.message || chunk.response.incomplete_details?.reason || (chunk.type === "response.failed" ? "Response failed" : "Response ended incomplete");
641
- const errorCode = chunk.response.error?.code ?? (chunk.response.incomplete_details ? "incomplete" : void 0) ?? void 0;
642
- yield {
643
- type: EventType.RUN_ERROR,
644
- model: chunk.response.model,
645
- timestamp: Date.now(),
646
- message: errorMessage,
647
- ...errorCode !== void 0 && { code: errorCode },
648
- error: {
649
- message: errorMessage,
650
- ...errorCode !== void 0 && { code: errorCode }
651
- }
652
- };
653
- runFinishedEmitted = true;
654
- return;
655
- }
656
- if (chunk.type === "response.output_text.delta" && chunk.delta) {
657
- const textDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
658
- if (textDelta) {
659
- if (!hasEmittedTextMessageStart) {
660
- hasEmittedTextMessageStart = true;
661
- yield {
662
- type: EventType.TEXT_MESSAGE_START,
663
- messageId: aguiState.messageId,
664
- model: model || options.model,
665
- timestamp: Date.now(),
666
- role: "assistant"
667
- };
668
- }
669
- accumulatedContent += textDelta;
670
- hasStreamedContentDeltas = true;
671
- yield {
672
- type: EventType.TEXT_MESSAGE_CONTENT,
673
- messageId: aguiState.messageId,
674
- model: model || options.model,
675
- timestamp: Date.now(),
676
- delta: textDelta,
677
- content: accumulatedContent
678
- };
679
- }
680
- }
681
- if (chunk.type === "response.reasoning_text.delta" && chunk.delta) {
682
- const reasoningDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
683
- if (reasoningDelta) {
684
- if (!hasEmittedStepStarted) {
685
- hasEmittedStepStarted = true;
686
- stepId = generateId(this.name);
687
- yield {
688
- type: EventType.STEP_STARTED,
689
- stepName: stepId,
690
- stepId,
691
- model: model || options.model,
692
- timestamp: Date.now(),
693
- stepType: "thinking"
694
- };
695
- }
696
- accumulatedReasoning += reasoningDelta;
697
- hasStreamedReasoningDeltas = true;
698
- const fallbackStepId = stepId || generateId(this.name);
699
- yield {
700
- type: EventType.STEP_FINISHED,
701
- stepName: fallbackStepId,
702
- stepId: fallbackStepId,
703
- model: model || options.model,
704
- timestamp: Date.now(),
705
- delta: reasoningDelta,
706
- content: accumulatedReasoning
707
- };
708
- }
709
- }
710
- if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) {
711
- const summaryDelta = typeof chunk.delta === "string" ? chunk.delta : "";
712
- if (summaryDelta) {
713
- if (!hasEmittedStepStarted) {
714
- hasEmittedStepStarted = true;
715
- stepId = generateId(this.name);
716
- yield {
717
- type: EventType.STEP_STARTED,
718
- stepName: stepId,
719
- stepId,
720
- model: model || options.model,
721
- timestamp: Date.now(),
722
- stepType: "thinking"
723
- };
724
- }
725
- accumulatedReasoning += summaryDelta;
726
- hasStreamedReasoningDeltas = true;
727
- const fallbackStepId = stepId || generateId(this.name);
728
- yield {
729
- type: EventType.STEP_FINISHED,
730
- stepName: fallbackStepId,
731
- stepId: fallbackStepId,
732
- model: model || options.model,
733
- timestamp: Date.now(),
734
- delta: summaryDelta,
735
- content: accumulatedReasoning
736
- };
737
- }
738
- }
739
- if (chunk.type === "response.content_part.added") {
740
- const contentPart = chunk.part;
741
- if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
742
- hasEmittedTextMessageStart = true;
743
- yield {
744
- type: EventType.TEXT_MESSAGE_START,
745
- messageId: aguiState.messageId,
746
- model: model || options.model,
747
- timestamp: Date.now(),
748
- role: "assistant"
749
- };
750
- }
751
- if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
752
- hasEmittedStepStarted = true;
753
- stepId = generateId(this.name);
754
- yield {
755
- type: EventType.STEP_STARTED,
756
- stepName: stepId,
757
- stepId,
758
- model: model || options.model,
759
- timestamp: Date.now(),
760
- stepType: "thinking"
761
- };
762
- }
763
- if (contentPart.type === "output_text") {
764
- hasStreamedContentDeltas = true;
765
- } else if (contentPart.type === "reasoning_text") {
766
- hasStreamedReasoningDeltas = true;
767
- }
768
- const partChunk = handleContentPart(contentPart);
769
- yield partChunk;
770
- if (partChunk.type === "RUN_ERROR") {
771
- runFinishedEmitted = true;
772
- return;
773
- }
774
- }
775
- if (chunk.type === "response.content_part.done") {
776
- const contentPart = chunk.part;
777
- if (contentPart.type === "output_text" && hasStreamedContentDeltas) {
778
- continue;
779
- }
780
- if (contentPart.type === "reasoning_text" && hasStreamedReasoningDeltas) {
781
- continue;
782
- }
783
- if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
784
- hasEmittedTextMessageStart = true;
785
- yield {
786
- type: EventType.TEXT_MESSAGE_START,
787
- messageId: aguiState.messageId,
788
- model: model || options.model,
789
- timestamp: Date.now(),
790
- role: "assistant"
791
- };
792
- } else if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
793
- hasEmittedStepStarted = true;
794
- stepId = generateId(this.name);
795
- yield {
796
- type: EventType.STEP_STARTED,
797
- stepName: stepId,
798
- stepId,
799
- model: model || options.model,
800
- timestamp: Date.now(),
801
- stepType: "thinking"
802
- };
803
- }
804
- const doneChunk = handleContentPart(contentPart);
805
- yield doneChunk;
806
- if (doneChunk.type === "RUN_ERROR") {
807
- runFinishedEmitted = true;
808
- return;
809
- }
810
- }
811
- if (chunk.type === "response.output_item.added") {
812
- const item = chunk.item;
813
- if (item.type === "function_call" && item.id) {
814
- let metadata = toolCallMetadata.get(item.id);
815
- if (!metadata) {
816
- metadata = {
817
- index: chunk.output_index,
818
- name: item.name || "",
819
- started: false
820
- };
821
- toolCallMetadata.set(item.id, metadata);
822
- } else if (!metadata.name && item.name) {
823
- metadata.name = item.name;
824
- }
825
- if (!metadata.started && metadata.name) {
826
- yield {
827
- type: EventType.TOOL_CALL_START,
828
- toolCallId: item.id,
829
- toolCallName: metadata.name,
830
- toolName: metadata.name,
831
- parentMessageId: aguiState.messageId,
832
- model: model || options.model,
833
- timestamp: Date.now(),
834
- index: chunk.output_index
835
- };
836
- metadata.started = true;
837
- }
838
- }
839
- }
840
- if (chunk.type === "response.function_call_arguments.delta" && chunk.delta) {
841
- const metadata = toolCallMetadata.get(chunk.item_id);
842
- if (!metadata?.started) {
843
- options.logger.errors(
844
- `${this.name}.processStreamChunks orphan function_call_arguments.delta`,
845
- {
846
- source: `${this.name}.processStreamChunks`,
847
- toolCallId: chunk.item_id,
848
- rawDelta: chunk.delta
849
- }
850
- );
851
- continue;
852
- }
853
- yield {
854
- type: EventType.TOOL_CALL_ARGS,
855
- toolCallId: chunk.item_id,
856
- model: model || options.model,
857
- timestamp: Date.now(),
858
- delta: chunk.delta
859
- };
860
- }
861
- if (chunk.type === "response.function_call_arguments.done") {
862
- const { item_id } = chunk;
863
- const metadata = toolCallMetadata.get(item_id);
864
- if (!metadata?.started) {
865
- if (metadata) {
866
- metadata.pendingArguments = chunk.arguments;
867
- }
868
- options.logger.errors(
869
- `${this.name}.processStreamChunks deferring function_call_arguments.done — TOOL_CALL_START not yet emitted (waiting for name)`,
870
- {
871
- source: `${this.name}.processStreamChunks`,
872
- toolCallId: item_id,
873
- rawArguments: chunk.arguments
874
- }
875
- );
876
- continue;
877
- }
878
- if (metadata.ended) continue;
879
- const name = metadata.name || "";
880
- metadata.ended = true;
881
- let parsedInput = {};
882
- if (chunk.arguments) {
883
- try {
884
- const parsed = JSON.parse(chunk.arguments);
885
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
886
- } catch (parseError) {
887
- options.logger.errors(
888
- `${this.name}.processStreamChunks tool-args JSON parse failed`,
889
- {
890
- error: toRunErrorPayload(
891
- parseError,
892
- `tool ${name} (${item_id}) returned malformed JSON arguments`
893
- ),
894
- source: `${this.name}.processStreamChunks`,
895
- toolCallId: item_id,
896
- toolName: name,
897
- rawArguments: chunk.arguments
898
- }
899
- );
900
- parsedInput = {};
901
- }
902
- }
903
- yield {
904
- type: EventType.TOOL_CALL_END,
905
- toolCallId: item_id,
906
- toolCallName: name,
907
- toolName: name,
908
- model: model || options.model,
909
- timestamp: Date.now(),
910
- input: parsedInput
911
- };
912
- }
913
- if (chunk.type === "response.output_item.done") {
914
- const item = chunk.item;
915
- if (item.type === "function_call" && item.id) {
916
- const metadata = toolCallMetadata.get(item.id) ?? {
917
- index: chunk.output_index,
918
- name: item.name || "",
919
- started: false
920
- };
921
- if (!toolCallMetadata.has(item.id)) {
922
- toolCallMetadata.set(item.id, metadata);
923
- } else if (!metadata.name && item.name) {
924
- metadata.name = item.name;
925
- }
926
- if (!metadata.started && metadata.name) {
927
- yield {
928
- type: EventType.TOOL_CALL_START,
929
- toolCallId: item.id,
930
- toolCallName: metadata.name,
931
- toolName: metadata.name,
932
- parentMessageId: aguiState.messageId,
933
- model: model || options.model,
934
- timestamp: Date.now(),
935
- index: metadata.index
936
- };
937
- metadata.started = true;
938
- }
939
- const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
940
- if (metadata.started && !metadata.ended && rawArgs !== void 0) {
941
- const name = metadata.name || "";
942
- let parsedInput = {};
943
- if (rawArgs) {
944
- try {
945
- const parsed = JSON.parse(rawArgs);
946
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
947
- } catch (parseError) {
948
- options.logger.errors(
949
- `${this.name}.processStreamChunks tool-args JSON parse failed (output_item.done backfill)`,
950
- {
951
- error: toRunErrorPayload(
952
- parseError,
953
- `tool ${name} (${item.id}) returned malformed JSON arguments`
954
- ),
955
- source: `${this.name}.processStreamChunks`,
956
- toolCallId: item.id,
957
- toolName: name,
958
- rawArguments: rawArgs
959
- }
960
- );
961
- parsedInput = {};
962
- }
963
- }
964
- yield {
965
- type: EventType.TOOL_CALL_END,
966
- toolCallId: item.id,
967
- toolCallName: name,
968
- toolName: name,
969
- model: model || options.model,
970
- timestamp: Date.now(),
971
- input: parsedInput
972
- };
973
- metadata.ended = true;
974
- metadata.pendingArguments = void 0;
975
- }
976
- }
977
- }
978
- if (chunk.type === "response.completed") {
979
- for (const item of chunk.response.output) {
980
- if (item.type !== "function_call" || !item.id) continue;
981
- const metadata = toolCallMetadata.get(item.id) ?? {
982
- index: 0,
983
- name: item.name || "",
984
- started: false
985
- };
986
- if (!toolCallMetadata.has(item.id)) {
987
- toolCallMetadata.set(item.id, metadata);
988
- } else if (!metadata.name && item.name) {
989
- metadata.name = item.name;
990
- }
991
- if (!metadata.started && metadata.name) {
992
- yield {
993
- type: EventType.TOOL_CALL_START,
994
- toolCallId: item.id,
995
- toolCallName: metadata.name,
996
- toolName: metadata.name,
997
- parentMessageId: aguiState.messageId,
998
- model: model || options.model,
999
- timestamp: Date.now(),
1000
- index: metadata.index
1001
- };
1002
- metadata.started = true;
1003
- }
1004
- const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
1005
- if (metadata.started && !metadata.ended) {
1006
- const name = metadata.name || "";
1007
- let parsedInput = {};
1008
- if (rawArgs) {
1009
- try {
1010
- const parsed = JSON.parse(rawArgs);
1011
- parsedInput = parsed && typeof parsed === "object" ? parsed : {};
1012
- } catch (parseError) {
1013
- options.logger.errors(
1014
- `${this.name}.processStreamChunks tool-args JSON parse failed (response.completed backfill)`,
1015
- {
1016
- error: toRunErrorPayload(
1017
- parseError,
1018
- `tool ${name} (${item.id}) returned malformed JSON arguments`
1019
- ),
1020
- source: `${this.name}.processStreamChunks`,
1021
- toolCallId: item.id,
1022
- toolName: name,
1023
- rawArguments: rawArgs
1024
- }
1025
- );
1026
- parsedInput = {};
1027
- }
1028
- }
1029
- yield {
1030
- type: EventType.TOOL_CALL_END,
1031
- toolCallId: item.id,
1032
- toolCallName: name,
1033
- toolName: name,
1034
- model: model || options.model,
1035
- timestamp: Date.now(),
1036
- input: parsedInput
1037
- };
1038
- metadata.ended = true;
1039
- metadata.pendingArguments = void 0;
1040
- }
1041
- }
1042
- if (hasEmittedTextMessageStart) {
1043
- yield {
1044
- type: EventType.TEXT_MESSAGE_END,
1045
- messageId: aguiState.messageId,
1046
- model: model || options.model,
1047
- timestamp: Date.now()
1048
- };
1049
- hasEmittedTextMessageStart = false;
1050
- }
1051
- const hasFunctionCalls = chunk.response.output.some(
1052
- (item) => item.type === "function_call"
1053
- );
1054
- const incompleteReason = chunk.response.incomplete_details?.reason;
1055
- const finishReason = hasFunctionCalls ? "tool_calls" : incompleteReason === "max_output_tokens" ? "length" : incompleteReason === "content_filter" ? "content_filter" : "stop";
1056
- yield {
1057
- type: EventType.RUN_FINISHED,
1058
- runId: aguiState.runId,
1059
- threadId: aguiState.threadId,
1060
- model: model || options.model,
1061
- timestamp: Date.now(),
1062
- // Omit usage entirely when the provider reported none rather than
1063
- // emitting fabricated zeros (also satisfies exactOptionalPropertyTypes).
1064
- ...chunk.response.usage && {
1065
- usage: buildResponsesUsage(chunk.response.usage)
1066
- },
1067
- finishReason
1068
- };
1069
- runFinishedEmitted = true;
1070
- }
1071
- if (chunk.type === "error") {
1072
- const code = chunk.code ?? void 0;
1073
- yield {
1074
- type: EventType.RUN_ERROR,
1075
- model: model || options.model,
1076
- timestamp: Date.now(),
1077
- message: chunk.message,
1078
- ...code !== void 0 && { code },
1079
- error: {
1080
- message: chunk.message,
1081
- ...code !== void 0 && { code }
1082
- }
1083
- };
1084
- runFinishedEmitted = true;
1085
- return;
1086
- }
1087
- }
1088
- if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
1089
- if (hasEmittedTextMessageStart) {
1090
- yield {
1091
- type: EventType.TEXT_MESSAGE_END,
1092
- messageId: aguiState.messageId,
1093
- model: model || options.model,
1094
- timestamp: Date.now()
1095
- };
1096
- }
1097
- yield {
1098
- type: EventType.RUN_FINISHED,
1099
- runId: aguiState.runId,
1100
- threadId: aguiState.threadId,
1101
- model: model || options.model,
1102
- timestamp: Date.now(),
1103
- finishReason: toolCallMetadata.size > 0 ? "tool_calls" : "stop"
1104
- };
1105
- }
1106
- } catch (error) {
1107
- const errorPayload = toRunErrorPayload(
1108
- error,
1109
- `${this.name}.processStreamChunks failed`
1110
- );
1111
- const rawEvent = toRunErrorRawEvent(error);
1112
- options.logger.errors(`${this.name}.processStreamChunks fatal`, {
1113
- error: errorPayload,
1114
- source: `${this.name}.processStreamChunks`
1115
- });
1116
- yield {
1117
- type: EventType.RUN_ERROR,
1118
- model: options.model,
1119
- timestamp: Date.now(),
1120
- message: errorPayload.message,
1121
- ...errorPayload.code !== void 0 && { code: errorPayload.code },
1122
- ...rawEvent !== void 0 && { rawEvent },
1123
- error: {
1124
- message: errorPayload.message,
1125
- ...errorPayload.code !== void 0 && { code: errorPayload.code }
1126
- }
1127
- };
1128
- }
1129
- }
1130
- /**
1131
- * Maps common TextOptions to Responses API request format.
1132
- * Override this in subclasses to add provider-specific options.
1133
- */
1134
- mapOptionsToRequest(options) {
1135
- const input = this.convertMessagesToInput(options.messages);
1136
- const tools = options.tools ? convertToolsToResponsesFormat(
1137
- options.tools,
1138
- this.makeStructuredOutputCompatible.bind(this)
1139
- ) : void 0;
1140
- const modelOptions = options.modelOptions;
1141
- const combinedSchema = options.outputSchema;
1142
- const textFormat = combinedSchema ? {
1143
- text: {
1144
- format: {
1145
- type: "json_schema",
1146
- name: "structured_output",
1147
- schema: this.makeStructuredOutputCompatible(
1148
- combinedSchema,
1149
- Array.isArray(combinedSchema.required) ? combinedSchema.required : void 0
1150
- ),
1151
- strict: true
1152
- }
1153
- }
1154
- } : void 0;
1155
- return {
1156
- ...modelOptions,
1157
- model: options.model,
1158
- ...options.metadata !== void 0 && { metadata: options.metadata },
1159
- ...(() => {
1160
- const prompts = normalizeSystemPrompts(options.systemPrompts);
1161
- if (prompts.length === 0) return {};
1162
- return { instructions: prompts.map((p) => p.content).join("\n") };
1163
- })(),
1164
- input,
1165
- // Conditional spread: `tools: undefined` would clobber any
1166
- // modelOptions.tools the caller set above.
1167
- ...tools && tools.length > 0 && { tools },
1168
- ...textFormat ?? {}
1169
- };
1170
- }
1171
- /**
1172
- * The OpenAI Responses API supports `tools` and `text.format: json_schema`
1173
- * together in a single streaming request (per issue #605). Subclasses
1174
- * that route to providers without this capability should override.
1175
- */
1176
- supportsCombinedToolsAndSchema() {
1177
- return true;
1178
- }
1179
- /**
1180
- * Converts ModelMessage[] to Responses API ResponseInput format.
1181
- * Override this in subclasses for provider-specific message format quirks.
1182
- *
1183
- * Key differences from Chat Completions:
1184
- * - Tool results use `function_call_output` type (not `tool` role)
1185
- * - Assistant tool calls are `function_call` objects (not nested in `tool_calls`)
1186
- * - User content uses `input_text`, `input_image`, `input_file` types
1187
- * - System prompts go in `instructions`, not as messages
1188
- */
1189
- convertMessagesToInput(messages) {
1190
- const result = [];
1191
- for (const message of messages) {
1192
- if (message.role === "tool") {
1193
- const toolContent = message.content;
1194
- const output = Array.isArray(toolContent) ? toolContent.map((part) => this.convertContentPartToInput(part)) : typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent);
1195
- result.push({
1196
- type: "function_call_output",
1197
- call_id: message.toolCallId || "",
1198
- output
1199
- });
1200
- continue;
1201
- }
1202
- if (message.role === "assistant") {
1203
- if (message.toolCalls && message.toolCalls.length > 0) {
1204
- for (const toolCall of message.toolCalls) {
1205
- const argumentsString = typeof toolCall.function.arguments === "string" ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments);
1206
- result.push({
1207
- type: "function_call",
1208
- call_id: toolCall.id,
1209
- name: toolCall.function.name,
1210
- arguments: argumentsString
1211
- });
1212
- }
1213
- }
1214
- if (message.content) {
1215
- const contentStr = this.extractTextContent(message.content);
1216
- if (contentStr) {
1217
- result.push({
1218
- type: "message",
1219
- role: "assistant",
1220
- content: contentStr
1221
- });
1222
- }
1223
- }
1224
- continue;
1225
- }
1226
- const contentParts = this.normalizeContent(message.content);
1227
- const inputContent = [];
1228
- for (const part of contentParts) {
1229
- inputContent.push(this.convertContentPartToInput(part));
1230
- }
1231
- if (inputContent.length === 0) {
1232
- throw new Error(
1233
- `User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio part or omit the message.`
1234
- );
1235
- }
1236
- result.push({
1237
- type: "message",
1238
- role: "user",
1239
- content: inputContent
1240
- });
1241
- }
1242
- return result;
1243
- }
1244
- /**
1245
- * Converts a ContentPart to Responses API input content item.
1246
- * Handles text, image, and audio content parts.
1247
- * Override this in subclasses for additional content types or provider-specific metadata.
1248
- */
1249
- convertContentPartToInput(part) {
1250
- switch (part.type) {
1251
- case "text":
1252
- return {
1253
- type: "input_text",
1254
- text: part.content
1255
- };
1256
- case "image": {
1257
- const imageMetadata = part.metadata;
1258
- if (part.source.type === "url") {
1259
- return {
1260
- type: "input_image",
1261
- image_url: part.source.value,
1262
- detail: imageMetadata?.detail || "auto"
1263
- };
1264
- }
1265
- const imageValue = part.source.value;
1266
- const imageMime = part.source.mimeType || "application/octet-stream";
1267
- const imageUrl = imageValue.startsWith("data:") ? imageValue : `data:${imageMime};base64,${imageValue}`;
1268
- return {
1269
- type: "input_image",
1270
- image_url: imageUrl,
1271
- detail: imageMetadata?.detail || "auto"
1272
- };
1273
- }
1274
- case "audio": {
1275
- if (part.source.type === "url") {
1276
- return {
1277
- type: "input_file",
1278
- file_url: part.source.value
1279
- };
1280
- }
1281
- const audioValue = part.source.value;
1282
- const audioMime = part.source.mimeType || "application/octet-stream";
1283
- const audioFileData = audioValue.startsWith("data:") ? audioValue : `data:${audioMime};base64,${audioValue}`;
1284
- return {
1285
- type: "input_file",
1286
- file_data: audioFileData
1287
- };
1288
- }
1289
- case "video":
1290
- case "document":
1291
- default:
1292
- throw new Error(`Unsupported content part type: ${part.type}`);
1293
- }
1294
- }
1295
- /**
1296
- * Normalizes message content to an array of ContentPart.
1297
- * Handles backward compatibility with string content.
1298
- */
1299
- normalizeContent(content) {
1300
- if (content === null) {
1301
- return [];
1302
- }
1303
- if (typeof content === "string") {
1304
- return [{ type: "text", content }];
1305
- }
1306
- return content;
1307
- }
1308
- /**
1309
- * Extracts text content from a content value that may be string, null, or ContentPart array.
1310
- */
1311
- extractTextContent(content) {
1312
- if (content === null) {
1313
- return "";
1314
- }
1315
- if (typeof content === "string") {
1316
- return content;
1317
- }
1318
- return content.filter((p) => p.type === "text").map((p) => p.content).join("");
1319
- }
1320
- }
1321
- export {
1322
- OpenAIBaseResponsesTextAdapter
9
+ //#region src/adapters/responses-text.ts
10
+ /**
11
+ * Shared implementation of the OpenAI Responses API. Holds the stream-event
12
+ * accumulator + AG-UI lifecycle and calls the OpenAI SDK directly. Subclasses
13
+ * (today: ai-openai) construct an OpenAI client with their provider-specific
14
+ * `baseURL` / headers and pass it in.
15
+ */
16
+ var OpenAIBaseResponsesTextAdapter = class extends BaseTextAdapter {
17
+ kind = "text";
18
+ name;
19
+ client;
20
+ constructor(model, name, client) {
21
+ super({}, model);
22
+ this.name = name;
23
+ this.client = client;
24
+ }
25
+ async *chatStream(options) {
26
+ const toolCallMetadata = /* @__PURE__ */ new Map();
27
+ const aguiState = {
28
+ runId: options.runId ?? generateId(this.name),
29
+ threadId: options.threadId ?? generateId(this.name),
30
+ messageId: generateId(this.name),
31
+ hasEmittedRunStarted: false
32
+ };
33
+ try {
34
+ const requestParams = this.mapOptionsToRequest(options);
35
+ options.logger.request(`activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`, {
36
+ provider: this.name,
37
+ model: this.model
38
+ });
39
+ const response = await this.client.responses.create({
40
+ ...requestParams,
41
+ stream: true
42
+ }, extractRequestOptions(options.request));
43
+ yield* this.processStreamChunks(response, toolCallMetadata, options, aguiState);
44
+ } catch (error) {
45
+ const errorPayload = toRunErrorPayload(error, `${this.name}.chatStream failed`);
46
+ const rawEvent = toRunErrorRawEvent(error);
47
+ if (!aguiState.hasEmittedRunStarted) {
48
+ aguiState.hasEmittedRunStarted = true;
49
+ yield {
50
+ type: EventType.RUN_STARTED,
51
+ runId: aguiState.runId,
52
+ threadId: aguiState.threadId,
53
+ model: options.model,
54
+ timestamp: Date.now(),
55
+ parentRunId: options.parentRunId
56
+ };
57
+ }
58
+ yield {
59
+ type: EventType.RUN_ERROR,
60
+ model: options.model,
61
+ timestamp: Date.now(),
62
+ message: errorPayload.message,
63
+ code: errorPayload.code,
64
+ ...rawEvent !== void 0 && { rawEvent },
65
+ error: {
66
+ message: errorPayload.message,
67
+ code: errorPayload.code
68
+ }
69
+ };
70
+ options.logger.errors(`${this.name}.chatStream fatal`, {
71
+ error: errorPayload,
72
+ source: `${this.name}.chatStream`
73
+ });
74
+ }
75
+ }
76
+ /**
77
+ * Generate structured output using the provider's native JSON Schema response format.
78
+ * Uses stream: false to get the complete response in one call.
79
+ *
80
+ * OpenAI-compatible Responses APIs have strict requirements for structured output:
81
+ * - All properties must be in the `required` array
82
+ * - Optional fields should have null added to their type union
83
+ * - additionalProperties must be false for all objects
84
+ *
85
+ * The outputSchema is already JSON Schema (converted in the ai layer).
86
+ * We apply provider-specific transformations for structured output compatibility.
87
+ */
88
+ async structuredOutput(options) {
89
+ const { chatOptions, outputSchema } = options;
90
+ const requestParams = this.mapOptionsToRequest(chatOptions);
91
+ const jsonSchema = this.makeStructuredOutputCompatible(outputSchema, outputSchema.required);
92
+ try {
93
+ const { stream: _stream, stream_options: _streamOptions, ...cleanParams } = requestParams;
94
+ chatOptions.logger.request(`activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, {
95
+ provider: this.name,
96
+ model: this.model
97
+ });
98
+ const response = await this.client.responses.create({
99
+ ...cleanParams,
100
+ stream: false,
101
+ text: { format: {
102
+ type: "json_schema",
103
+ name: "structured_output",
104
+ schema: jsonSchema,
105
+ strict: true
106
+ } }
107
+ }, extractRequestOptions(chatOptions.request));
108
+ const rawText = this.extractTextFromResponse(response);
109
+ if (rawText.length === 0) throw new Error(`${this.name}.structuredOutput: response contained no content`);
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(rawText);
113
+ } catch {
114
+ throw new Error(`Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`);
115
+ }
116
+ return {
117
+ data: this.transformStructuredOutput(parsed),
118
+ rawText
119
+ };
120
+ } catch (error) {
121
+ chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
122
+ error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
123
+ source: `${this.name}.structuredOutput`
124
+ });
125
+ throw error;
126
+ }
127
+ }
128
+ /**
129
+ * Stream structured output via the Responses API: single request with
130
+ * `text.format: json_schema` + `stream: true`. Consumes Responses-API
131
+ * events (`response.output_text.delta`, `response.reasoning_text.delta`,
132
+ * `response.reasoning_summary_text.delta`, `response.refusal.delta`,
133
+ * `response.completed`, `response.failed`) and re-emits the standard AG-UI
134
+ * lifecycle ending with `CUSTOM 'structured-output.complete'`.
135
+ *
136
+ * Tools are stripped (structured output is mutually exclusive with tool
137
+ * calls in this path). Reasoning text is accumulated and surfaced both as
138
+ * REASONING_* lifecycle events during the stream and on the terminal
139
+ * CUSTOM event's `value.reasoning`.
140
+ */
141
+ async *structuredOutputStream(options) {
142
+ const { chatOptions, outputSchema } = options;
143
+ const requestParams = this.mapOptionsToRequest(chatOptions);
144
+ const jsonSchema = this.makeStructuredOutputCompatible(outputSchema, outputSchema.required);
145
+ const timestamp = Date.now();
146
+ const aguiState = {
147
+ runId: generateId(this.name),
148
+ threadId: chatOptions.threadId ?? generateId(this.name),
149
+ messageId: generateId(this.name),
150
+ timestamp,
151
+ hasEmittedRunStarted: false
152
+ };
153
+ let accumulatedContent = "";
154
+ let accumulatedReasoning = "";
155
+ let hasEmittedTextMessageStart = false;
156
+ let reasoningMessageId;
157
+ let stepId;
158
+ let hasClosedReasoning = false;
159
+ let model = chatOptions.model;
160
+ let usage;
161
+ const closeReasoning = function* () {
162
+ if (reasoningMessageId && !hasClosedReasoning) {
163
+ hasClosedReasoning = true;
164
+ yield {
165
+ type: EventType.REASONING_MESSAGE_END,
166
+ messageId: reasoningMessageId,
167
+ model,
168
+ timestamp
169
+ };
170
+ yield {
171
+ type: EventType.REASONING_END,
172
+ messageId: reasoningMessageId,
173
+ model,
174
+ timestamp
175
+ };
176
+ if (stepId) yield {
177
+ type: EventType.STEP_FINISHED,
178
+ stepName: stepId,
179
+ stepId,
180
+ model,
181
+ timestamp,
182
+ content: accumulatedReasoning
183
+ };
184
+ }
185
+ }.bind(this);
186
+ const openReasoning = function* () {
187
+ if (reasoningMessageId) return;
188
+ reasoningMessageId = generateId(this.name);
189
+ stepId = generateId(this.name);
190
+ yield {
191
+ type: EventType.REASONING_START,
192
+ messageId: reasoningMessageId,
193
+ model,
194
+ timestamp
195
+ };
196
+ yield {
197
+ type: EventType.REASONING_MESSAGE_START,
198
+ messageId: reasoningMessageId,
199
+ role: "reasoning",
200
+ model,
201
+ timestamp
202
+ };
203
+ yield {
204
+ type: EventType.STEP_STARTED,
205
+ stepName: stepId,
206
+ stepId,
207
+ model,
208
+ timestamp,
209
+ stepType: "thinking"
210
+ };
211
+ }.bind(this);
212
+ try {
213
+ const { tools: _tools, ...cleanParams } = requestParams;
214
+ chatOptions.logger.request(`activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`, {
215
+ provider: this.name,
216
+ model: this.model
217
+ });
218
+ const stream = await this.client.responses.create({
219
+ ...cleanParams,
220
+ stream: true,
221
+ text: { format: {
222
+ type: "json_schema",
223
+ name: "structured_output",
224
+ schema: jsonSchema,
225
+ strict: true
226
+ } }
227
+ }, extractRequestOptions(chatOptions.request));
228
+ for await (const chunk of stream) {
229
+ chatOptions.logger.provider(`provider=${this.name} type=${chunk.type}`, {
230
+ provider: this.name,
231
+ type: chunk.type
232
+ });
233
+ if (!aguiState.hasEmittedRunStarted) {
234
+ aguiState.hasEmittedRunStarted = true;
235
+ yield {
236
+ type: EventType.RUN_STARTED,
237
+ runId: aguiState.runId,
238
+ threadId: aguiState.threadId,
239
+ model,
240
+ timestamp,
241
+ parentRunId: chatOptions.parentRunId
242
+ };
243
+ }
244
+ if (chunk.type === "response.created" || chunk.type === "response.in_progress") {
245
+ const responseModel = chunk.response?.model;
246
+ if (responseModel) model = responseModel;
247
+ continue;
248
+ }
249
+ if (chunk.type === "response.refusal.delta") {
250
+ const delta = typeof chunk.delta === "string" ? chunk.delta : "";
251
+ yield {
252
+ type: EventType.RUN_ERROR,
253
+ runId: aguiState.runId,
254
+ model,
255
+ timestamp,
256
+ message: `Model refused: ${delta}`,
257
+ code: "refusal",
258
+ error: {
259
+ message: `Model refused: ${delta}`,
260
+ code: "refusal"
261
+ }
262
+ };
263
+ return;
264
+ }
265
+ if (chunk.type === "response.reasoning_text.delta" || chunk.type === "response.reasoning_summary_text.delta") {
266
+ const raw = chunk.delta;
267
+ const reasoningDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
268
+ if (!reasoningDelta) continue;
269
+ yield* openReasoning();
270
+ if (!reasoningMessageId) continue;
271
+ accumulatedReasoning += reasoningDelta;
272
+ yield {
273
+ type: EventType.REASONING_MESSAGE_CONTENT,
274
+ messageId: reasoningMessageId,
275
+ delta: reasoningDelta,
276
+ model,
277
+ timestamp
278
+ };
279
+ continue;
280
+ }
281
+ if (chunk.type === "response.output_text.delta") {
282
+ const raw = chunk.delta;
283
+ const textDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
284
+ if (!textDelta) continue;
285
+ yield* closeReasoning();
286
+ if (!hasEmittedTextMessageStart) {
287
+ hasEmittedTextMessageStart = true;
288
+ yield {
289
+ type: EventType.TEXT_MESSAGE_START,
290
+ messageId: aguiState.messageId,
291
+ model,
292
+ timestamp,
293
+ role: "assistant"
294
+ };
295
+ }
296
+ accumulatedContent += textDelta;
297
+ yield {
298
+ type: EventType.TEXT_MESSAGE_CONTENT,
299
+ messageId: aguiState.messageId,
300
+ model,
301
+ timestamp,
302
+ delta: textDelta,
303
+ content: accumulatedContent
304
+ };
305
+ continue;
306
+ }
307
+ if (chunk.type === "response.completed") {
308
+ const response = chunk.response;
309
+ if (response.usage) usage = response.usage;
310
+ if (response.model) model = response.model;
311
+ continue;
312
+ }
313
+ if (chunk.type === "response.failed") {
314
+ const response = chunk.response;
315
+ const message = response?.error?.message || "Responses API stream failed";
316
+ const code = response?.error?.code;
317
+ yield {
318
+ type: EventType.RUN_ERROR,
319
+ runId: aguiState.runId,
320
+ model,
321
+ timestamp,
322
+ message,
323
+ ...code !== void 0 && { code },
324
+ error: {
325
+ message,
326
+ ...code !== void 0 && { code }
327
+ }
328
+ };
329
+ return;
330
+ }
331
+ }
332
+ yield* closeReasoning();
333
+ if (hasEmittedTextMessageStart) yield {
334
+ type: EventType.TEXT_MESSAGE_END,
335
+ messageId: aguiState.messageId,
336
+ model,
337
+ timestamp
338
+ };
339
+ if (accumulatedContent.length === 0) {
340
+ yield {
341
+ type: EventType.RUN_ERROR,
342
+ runId: aguiState.runId,
343
+ model,
344
+ timestamp,
345
+ message: `${this.name}.structuredOutputStream: response contained no content`,
346
+ code: "empty-response",
347
+ error: {
348
+ message: `${this.name}.structuredOutputStream: response contained no content`,
349
+ code: "empty-response"
350
+ }
351
+ };
352
+ return;
353
+ }
354
+ let parsed;
355
+ try {
356
+ parsed = JSON.parse(accumulatedContent);
357
+ } catch {
358
+ yield {
359
+ type: EventType.RUN_ERROR,
360
+ runId: aguiState.runId,
361
+ model,
362
+ timestamp,
363
+ message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
364
+ code: "parse-error",
365
+ error: {
366
+ message: "Failed to parse structured output as JSON",
367
+ code: "parse-error"
368
+ }
369
+ };
370
+ return;
371
+ }
372
+ const transformed = this.transformStructuredOutput(parsed);
373
+ yield {
374
+ type: EventType.CUSTOM,
375
+ name: "structured-output.complete",
376
+ value: {
377
+ object: transformed,
378
+ raw: accumulatedContent,
379
+ ...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
380
+ },
381
+ model,
382
+ timestamp
383
+ };
384
+ yield {
385
+ type: EventType.RUN_FINISHED,
386
+ runId: aguiState.runId,
387
+ threadId: aguiState.threadId,
388
+ model,
389
+ timestamp,
390
+ finishReason: "stop",
391
+ ...usage && { usage: buildResponsesUsage(usage) }
392
+ };
393
+ } catch (error) {
394
+ if (!aguiState.hasEmittedRunStarted) {
395
+ aguiState.hasEmittedRunStarted = true;
396
+ yield {
397
+ type: EventType.RUN_STARTED,
398
+ runId: aguiState.runId,
399
+ threadId: aguiState.threadId,
400
+ model,
401
+ timestamp,
402
+ parentRunId: chatOptions.parentRunId
403
+ };
404
+ }
405
+ const isAbort = this.isAbortError(error);
406
+ const errorPayload = toRunErrorPayload(error, `${this.name}.structuredOutputStream failed`);
407
+ const resolvedCode = isAbort ? "aborted" : errorPayload.code;
408
+ const rawEvent = isAbort ? void 0 : toRunErrorRawEvent(error);
409
+ yield {
410
+ type: EventType.RUN_ERROR,
411
+ runId: aguiState.runId,
412
+ model,
413
+ timestamp,
414
+ message: errorPayload.message,
415
+ ...resolvedCode !== void 0 && { code: resolvedCode },
416
+ ...rawEvent !== void 0 && { rawEvent },
417
+ error: {
418
+ message: errorPayload.message,
419
+ ...resolvedCode !== void 0 && { code: resolvedCode }
420
+ }
421
+ };
422
+ chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
423
+ error: errorPayload,
424
+ source: `${this.name}.structuredOutputStream`
425
+ });
426
+ }
427
+ }
428
+ /**
429
+ * Cross-SDK abort detection for `structuredOutputStream`. Mirrors the
430
+ * Chat Completions base; subclasses with proprietary error types override.
431
+ */
432
+ isAbortError(error) {
433
+ if (!error || typeof error !== "object") return false;
434
+ const e = error;
435
+ return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
436
+ }
437
+ /**
438
+ * Applies provider-specific transformations for structured output compatibility.
439
+ * Override this in subclasses to handle provider-specific quirks.
440
+ */
441
+ makeStructuredOutputCompatible(schema, originalRequired) {
442
+ return makeStructuredOutputCompatible(schema, originalRequired);
443
+ }
444
+ /**
445
+ * Final shaping pass applied to parsed structured-output JSON before it is
446
+ * returned to the caller. Default is a passthrough.
447
+ *
448
+ * Provider `null`s are no longer stripped here: strict-mode null-widening is
449
+ * now undone precisely by the engine (`undoNullWidening`, driven by the
450
+ * schema's null-widening map) the moment the result is captured, so a blind
451
+ * `transformNullsToUndefined` at the adapter would only destroy genuine
452
+ * `.nullable()` nulls. Subclasses may still override to remap or reshape the
453
+ * provider's structured output.
454
+ */
455
+ transformStructuredOutput(parsed) {
456
+ return parsed;
457
+ }
458
+ /**
459
+ * Extract text content from a non-streaming Responses API response.
460
+ * Override this in subclasses for provider-specific response shapes.
461
+ */
462
+ extractTextFromResponse(response) {
463
+ let textContent = "";
464
+ let refusal;
465
+ let sawMessageItem = false;
466
+ const observedItemTypes = /* @__PURE__ */ new Set();
467
+ for (const item of response.output) {
468
+ observedItemTypes.add(item.type);
469
+ if (item.type === "message") {
470
+ sawMessageItem = true;
471
+ for (const part of item.content) {
472
+ const partType = part.type;
473
+ if (partType === "output_text") textContent += part.text ?? "";
474
+ else if (partType === "refusal") refusal = part.refusal || refusal || "Refused without explanation";
475
+ else throw new Error(`${this.name}.extractTextFromResponse: unsupported message content part type "${partType}"`);
476
+ }
477
+ }
478
+ }
479
+ if (!textContent && refusal !== void 0) {
480
+ const err = /* @__PURE__ */ new Error(`Model refused to respond: ${refusal}`);
481
+ err.code = "refusal";
482
+ throw err;
483
+ }
484
+ if (!textContent && response.output.length > 0 && !sawMessageItem) throw new Error(`${this.name}.extractTextFromResponse: response.output contained items of type(s) [${[...observedItemTypes].sort().join(", ")}] but no message text — the model returned a non-text response`);
485
+ return textContent;
486
+ }
487
+ /**
488
+ * Processes streamed chunks from the Responses API and yields AG-UI events.
489
+ * Override this in subclasses to handle provider-specific stream behavior.
490
+ *
491
+ * Handles the following event types:
492
+ * - response.created / response.incomplete / response.failed
493
+ * - response.output_text.delta
494
+ * - response.reasoning_text.delta
495
+ * - response.reasoning_summary_text.delta
496
+ * - response.content_part.added / response.content_part.done
497
+ * - response.output_item.added
498
+ * - response.function_call_arguments.delta / response.function_call_arguments.done
499
+ * - response.completed
500
+ * - error
501
+ */
502
+ async *processStreamChunks(stream, toolCallMetadata, options, aguiState) {
503
+ let accumulatedContent = "";
504
+ let accumulatedReasoning = "";
505
+ let hasStreamedContentDeltas = false;
506
+ let hasStreamedReasoningDeltas = false;
507
+ let model = options.model;
508
+ let stepId = null;
509
+ let hasEmittedTextMessageStart = false;
510
+ let hasEmittedStepStarted = false;
511
+ let runFinishedEmitted = false;
512
+ try {
513
+ for await (const chunk of stream) {
514
+ options.logger.provider(`provider=${this.name} type=${chunk.type}`, {
515
+ provider: this.name,
516
+ type: chunk.type
517
+ });
518
+ if (!aguiState.hasEmittedRunStarted) {
519
+ aguiState.hasEmittedRunStarted = true;
520
+ yield {
521
+ type: EventType.RUN_STARTED,
522
+ runId: aguiState.runId,
523
+ threadId: aguiState.threadId,
524
+ model: model || options.model,
525
+ timestamp: Date.now(),
526
+ parentRunId: options.parentRunId
527
+ };
528
+ }
529
+ const handleContentPart = (contentPart) => {
530
+ if (contentPart.type === "output_text") {
531
+ accumulatedContent += contentPart.text || "";
532
+ return {
533
+ type: EventType.TEXT_MESSAGE_CONTENT,
534
+ messageId: aguiState.messageId,
535
+ model: model || options.model,
536
+ timestamp: Date.now(),
537
+ delta: contentPart.text || "",
538
+ content: accumulatedContent
539
+ };
540
+ }
541
+ if (contentPart.type === "reasoning_text") {
542
+ accumulatedReasoning += contentPart.text || "";
543
+ if (!stepId) stepId = generateId(this.name);
544
+ return {
545
+ type: EventType.STEP_FINISHED,
546
+ stepName: stepId,
547
+ stepId,
548
+ model: model || options.model,
549
+ timestamp: Date.now(),
550
+ delta: contentPart.text || "",
551
+ content: accumulatedReasoning
552
+ };
553
+ }
554
+ const isRefusal = contentPart.type === "refusal";
555
+ const message = isRefusal ? contentPart.refusal || "Refused without explanation" : `Unsupported response content_part type: ${contentPart.type}`;
556
+ const code = isRefusal ? "refusal" : contentPart.type;
557
+ return {
558
+ type: EventType.RUN_ERROR,
559
+ model: model || options.model,
560
+ timestamp: Date.now(),
561
+ message,
562
+ code,
563
+ error: {
564
+ message,
565
+ code
566
+ }
567
+ };
568
+ };
569
+ if (chunk.type === "response.created" || chunk.type === "response.incomplete" || chunk.type === "response.failed") model = chunk.response.model;
570
+ if (chunk.type === "response.created") {
571
+ hasStreamedContentDeltas = false;
572
+ hasStreamedReasoningDeltas = false;
573
+ hasEmittedTextMessageStart = false;
574
+ hasEmittedStepStarted = false;
575
+ accumulatedContent = "";
576
+ accumulatedReasoning = "";
577
+ }
578
+ if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
579
+ if (hasEmittedTextMessageStart) {
580
+ yield {
581
+ type: EventType.TEXT_MESSAGE_END,
582
+ messageId: aguiState.messageId,
583
+ model: chunk.response.model,
584
+ timestamp: Date.now()
585
+ };
586
+ hasEmittedTextMessageStart = false;
587
+ }
588
+ const errorMessage = chunk.response.error?.message || chunk.response.incomplete_details?.reason || (chunk.type === "response.failed" ? "Response failed" : "Response ended incomplete");
589
+ const errorCode = chunk.response.error?.code ?? (chunk.response.incomplete_details ? "incomplete" : void 0) ?? void 0;
590
+ yield {
591
+ type: EventType.RUN_ERROR,
592
+ model: chunk.response.model,
593
+ timestamp: Date.now(),
594
+ message: errorMessage,
595
+ ...errorCode !== void 0 && { code: errorCode },
596
+ error: {
597
+ message: errorMessage,
598
+ ...errorCode !== void 0 && { code: errorCode }
599
+ }
600
+ };
601
+ runFinishedEmitted = true;
602
+ return;
603
+ }
604
+ if (chunk.type === "response.output_text.delta" && chunk.delta) {
605
+ const textDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
606
+ if (textDelta) {
607
+ if (!hasEmittedTextMessageStart) {
608
+ hasEmittedTextMessageStart = true;
609
+ yield {
610
+ type: EventType.TEXT_MESSAGE_START,
611
+ messageId: aguiState.messageId,
612
+ model: model || options.model,
613
+ timestamp: Date.now(),
614
+ role: "assistant"
615
+ };
616
+ }
617
+ accumulatedContent += textDelta;
618
+ hasStreamedContentDeltas = true;
619
+ yield {
620
+ type: EventType.TEXT_MESSAGE_CONTENT,
621
+ messageId: aguiState.messageId,
622
+ model: model || options.model,
623
+ timestamp: Date.now(),
624
+ delta: textDelta,
625
+ content: accumulatedContent
626
+ };
627
+ }
628
+ }
629
+ if (chunk.type === "response.reasoning_text.delta" && chunk.delta) {
630
+ const reasoningDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
631
+ if (reasoningDelta) {
632
+ if (!hasEmittedStepStarted) {
633
+ hasEmittedStepStarted = true;
634
+ stepId = generateId(this.name);
635
+ yield {
636
+ type: EventType.STEP_STARTED,
637
+ stepName: stepId,
638
+ stepId,
639
+ model: model || options.model,
640
+ timestamp: Date.now(),
641
+ stepType: "thinking"
642
+ };
643
+ }
644
+ accumulatedReasoning += reasoningDelta;
645
+ hasStreamedReasoningDeltas = true;
646
+ const fallbackStepId = stepId || generateId(this.name);
647
+ yield {
648
+ type: EventType.STEP_FINISHED,
649
+ stepName: fallbackStepId,
650
+ stepId: fallbackStepId,
651
+ model: model || options.model,
652
+ timestamp: Date.now(),
653
+ delta: reasoningDelta,
654
+ content: accumulatedReasoning
655
+ };
656
+ }
657
+ }
658
+ if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) {
659
+ const summaryDelta = typeof chunk.delta === "string" ? chunk.delta : "";
660
+ if (summaryDelta) {
661
+ if (!hasEmittedStepStarted) {
662
+ hasEmittedStepStarted = true;
663
+ stepId = generateId(this.name);
664
+ yield {
665
+ type: EventType.STEP_STARTED,
666
+ stepName: stepId,
667
+ stepId,
668
+ model: model || options.model,
669
+ timestamp: Date.now(),
670
+ stepType: "thinking"
671
+ };
672
+ }
673
+ accumulatedReasoning += summaryDelta;
674
+ hasStreamedReasoningDeltas = true;
675
+ const fallbackStepId = stepId || generateId(this.name);
676
+ yield {
677
+ type: EventType.STEP_FINISHED,
678
+ stepName: fallbackStepId,
679
+ stepId: fallbackStepId,
680
+ model: model || options.model,
681
+ timestamp: Date.now(),
682
+ delta: summaryDelta,
683
+ content: accumulatedReasoning
684
+ };
685
+ }
686
+ }
687
+ if (chunk.type === "response.content_part.added") {
688
+ const contentPart = chunk.part;
689
+ if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
690
+ hasEmittedTextMessageStart = true;
691
+ yield {
692
+ type: EventType.TEXT_MESSAGE_START,
693
+ messageId: aguiState.messageId,
694
+ model: model || options.model,
695
+ timestamp: Date.now(),
696
+ role: "assistant"
697
+ };
698
+ }
699
+ if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
700
+ hasEmittedStepStarted = true;
701
+ stepId = generateId(this.name);
702
+ yield {
703
+ type: EventType.STEP_STARTED,
704
+ stepName: stepId,
705
+ stepId,
706
+ model: model || options.model,
707
+ timestamp: Date.now(),
708
+ stepType: "thinking"
709
+ };
710
+ }
711
+ if (contentPart.type === "output_text") hasStreamedContentDeltas = true;
712
+ else if (contentPart.type === "reasoning_text") hasStreamedReasoningDeltas = true;
713
+ const partChunk = handleContentPart(contentPart);
714
+ yield partChunk;
715
+ if (partChunk.type === "RUN_ERROR") {
716
+ runFinishedEmitted = true;
717
+ return;
718
+ }
719
+ }
720
+ if (chunk.type === "response.content_part.done") {
721
+ const contentPart = chunk.part;
722
+ if (contentPart.type === "output_text" && hasStreamedContentDeltas) continue;
723
+ if (contentPart.type === "reasoning_text" && hasStreamedReasoningDeltas) continue;
724
+ if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
725
+ hasEmittedTextMessageStart = true;
726
+ yield {
727
+ type: EventType.TEXT_MESSAGE_START,
728
+ messageId: aguiState.messageId,
729
+ model: model || options.model,
730
+ timestamp: Date.now(),
731
+ role: "assistant"
732
+ };
733
+ } else if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
734
+ hasEmittedStepStarted = true;
735
+ stepId = generateId(this.name);
736
+ yield {
737
+ type: EventType.STEP_STARTED,
738
+ stepName: stepId,
739
+ stepId,
740
+ model: model || options.model,
741
+ timestamp: Date.now(),
742
+ stepType: "thinking"
743
+ };
744
+ }
745
+ const doneChunk = handleContentPart(contentPart);
746
+ yield doneChunk;
747
+ if (doneChunk.type === "RUN_ERROR") {
748
+ runFinishedEmitted = true;
749
+ return;
750
+ }
751
+ }
752
+ if (chunk.type === "response.output_item.added") {
753
+ const item = chunk.item;
754
+ if (item.type === "function_call" && item.id) {
755
+ let metadata = toolCallMetadata.get(item.id);
756
+ if (!metadata) {
757
+ metadata = {
758
+ index: chunk.output_index,
759
+ name: item.name || "",
760
+ started: false
761
+ };
762
+ toolCallMetadata.set(item.id, metadata);
763
+ } else if (!metadata.name && item.name) metadata.name = item.name;
764
+ if (!metadata.started && metadata.name) {
765
+ yield {
766
+ type: EventType.TOOL_CALL_START,
767
+ toolCallId: item.id,
768
+ toolCallName: metadata.name,
769
+ toolName: metadata.name,
770
+ parentMessageId: aguiState.messageId,
771
+ model: model || options.model,
772
+ timestamp: Date.now(),
773
+ index: chunk.output_index
774
+ };
775
+ metadata.started = true;
776
+ }
777
+ }
778
+ }
779
+ if (chunk.type === "response.function_call_arguments.delta" && chunk.delta) {
780
+ if (!toolCallMetadata.get(chunk.item_id)?.started) {
781
+ options.logger.errors(`${this.name}.processStreamChunks orphan function_call_arguments.delta`, {
782
+ source: `${this.name}.processStreamChunks`,
783
+ toolCallId: chunk.item_id,
784
+ rawDelta: chunk.delta
785
+ });
786
+ continue;
787
+ }
788
+ yield {
789
+ type: EventType.TOOL_CALL_ARGS,
790
+ toolCallId: chunk.item_id,
791
+ model: model || options.model,
792
+ timestamp: Date.now(),
793
+ delta: chunk.delta
794
+ };
795
+ }
796
+ if (chunk.type === "response.function_call_arguments.done") {
797
+ const { item_id } = chunk;
798
+ const metadata = toolCallMetadata.get(item_id);
799
+ if (!metadata?.started) {
800
+ if (metadata) metadata.pendingArguments = chunk.arguments;
801
+ options.logger.errors(`${this.name}.processStreamChunks deferring function_call_arguments.done — TOOL_CALL_START not yet emitted (waiting for name)`, {
802
+ source: `${this.name}.processStreamChunks`,
803
+ toolCallId: item_id,
804
+ rawArguments: chunk.arguments
805
+ });
806
+ continue;
807
+ }
808
+ if (metadata.ended) continue;
809
+ const name = metadata.name || "";
810
+ metadata.ended = true;
811
+ let parsedInput = {};
812
+ if (chunk.arguments) try {
813
+ const parsed = JSON.parse(chunk.arguments);
814
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
815
+ } catch (parseError) {
816
+ options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed`, {
817
+ error: toRunErrorPayload(parseError, `tool ${name} (${item_id}) returned malformed JSON arguments`),
818
+ source: `${this.name}.processStreamChunks`,
819
+ toolCallId: item_id,
820
+ toolName: name,
821
+ rawArguments: chunk.arguments
822
+ });
823
+ parsedInput = {};
824
+ }
825
+ yield {
826
+ type: EventType.TOOL_CALL_END,
827
+ toolCallId: item_id,
828
+ toolCallName: name,
829
+ toolName: name,
830
+ model: model || options.model,
831
+ timestamp: Date.now(),
832
+ input: parsedInput
833
+ };
834
+ }
835
+ if (chunk.type === "response.output_item.done") {
836
+ const item = chunk.item;
837
+ if (item.type === "function_call" && item.id) {
838
+ const metadata = toolCallMetadata.get(item.id) ?? {
839
+ index: chunk.output_index,
840
+ name: item.name || "",
841
+ started: false
842
+ };
843
+ if (!toolCallMetadata.has(item.id)) toolCallMetadata.set(item.id, metadata);
844
+ else if (!metadata.name && item.name) metadata.name = item.name;
845
+ if (!metadata.started && metadata.name) {
846
+ yield {
847
+ type: EventType.TOOL_CALL_START,
848
+ toolCallId: item.id,
849
+ toolCallName: metadata.name,
850
+ toolName: metadata.name,
851
+ parentMessageId: aguiState.messageId,
852
+ model: model || options.model,
853
+ timestamp: Date.now(),
854
+ index: metadata.index
855
+ };
856
+ metadata.started = true;
857
+ }
858
+ const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
859
+ if (metadata.started && !metadata.ended && rawArgs !== void 0) {
860
+ const name = metadata.name || "";
861
+ let parsedInput = {};
862
+ if (rawArgs) try {
863
+ const parsed = JSON.parse(rawArgs);
864
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
865
+ } catch (parseError) {
866
+ options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (output_item.done backfill)`, {
867
+ error: toRunErrorPayload(parseError, `tool ${name} (${item.id}) returned malformed JSON arguments`),
868
+ source: `${this.name}.processStreamChunks`,
869
+ toolCallId: item.id,
870
+ toolName: name,
871
+ rawArguments: rawArgs
872
+ });
873
+ parsedInput = {};
874
+ }
875
+ yield {
876
+ type: EventType.TOOL_CALL_END,
877
+ toolCallId: item.id,
878
+ toolCallName: name,
879
+ toolName: name,
880
+ model: model || options.model,
881
+ timestamp: Date.now(),
882
+ input: parsedInput
883
+ };
884
+ metadata.ended = true;
885
+ metadata.pendingArguments = void 0;
886
+ }
887
+ }
888
+ }
889
+ if (chunk.type === "response.completed") {
890
+ for (const item of chunk.response.output) {
891
+ if (item.type !== "function_call" || !item.id) continue;
892
+ const metadata = toolCallMetadata.get(item.id) ?? {
893
+ index: 0,
894
+ name: item.name || "",
895
+ started: false
896
+ };
897
+ if (!toolCallMetadata.has(item.id)) toolCallMetadata.set(item.id, metadata);
898
+ else if (!metadata.name && item.name) metadata.name = item.name;
899
+ if (!metadata.started && metadata.name) {
900
+ yield {
901
+ type: EventType.TOOL_CALL_START,
902
+ toolCallId: item.id,
903
+ toolCallName: metadata.name,
904
+ toolName: metadata.name,
905
+ parentMessageId: aguiState.messageId,
906
+ model: model || options.model,
907
+ timestamp: Date.now(),
908
+ index: metadata.index
909
+ };
910
+ metadata.started = true;
911
+ }
912
+ const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
913
+ if (metadata.started && !metadata.ended) {
914
+ const name = metadata.name || "";
915
+ let parsedInput = {};
916
+ if (rawArgs) try {
917
+ const parsed = JSON.parse(rawArgs);
918
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
919
+ } catch (parseError) {
920
+ options.logger.errors(`${this.name}.processStreamChunks tool-args JSON parse failed (response.completed backfill)`, {
921
+ error: toRunErrorPayload(parseError, `tool ${name} (${item.id}) returned malformed JSON arguments`),
922
+ source: `${this.name}.processStreamChunks`,
923
+ toolCallId: item.id,
924
+ toolName: name,
925
+ rawArguments: rawArgs
926
+ });
927
+ parsedInput = {};
928
+ }
929
+ yield {
930
+ type: EventType.TOOL_CALL_END,
931
+ toolCallId: item.id,
932
+ toolCallName: name,
933
+ toolName: name,
934
+ model: model || options.model,
935
+ timestamp: Date.now(),
936
+ input: parsedInput
937
+ };
938
+ metadata.ended = true;
939
+ metadata.pendingArguments = void 0;
940
+ }
941
+ }
942
+ if (hasEmittedTextMessageStart) {
943
+ yield {
944
+ type: EventType.TEXT_MESSAGE_END,
945
+ messageId: aguiState.messageId,
946
+ model: model || options.model,
947
+ timestamp: Date.now()
948
+ };
949
+ hasEmittedTextMessageStart = false;
950
+ }
951
+ const hasFunctionCalls = chunk.response.output.some((item) => item.type === "function_call");
952
+ const incompleteReason = chunk.response.incomplete_details?.reason;
953
+ const finishReason = hasFunctionCalls ? "tool_calls" : incompleteReason === "max_output_tokens" ? "length" : incompleteReason === "content_filter" ? "content_filter" : "stop";
954
+ yield {
955
+ type: EventType.RUN_FINISHED,
956
+ runId: aguiState.runId,
957
+ threadId: aguiState.threadId,
958
+ model: model || options.model,
959
+ timestamp: Date.now(),
960
+ ...chunk.response.usage && { usage: buildResponsesUsage(chunk.response.usage) },
961
+ finishReason
962
+ };
963
+ runFinishedEmitted = true;
964
+ }
965
+ if (chunk.type === "error") {
966
+ const code = chunk.code ?? void 0;
967
+ yield {
968
+ type: EventType.RUN_ERROR,
969
+ model: model || options.model,
970
+ timestamp: Date.now(),
971
+ message: chunk.message,
972
+ ...code !== void 0 && { code },
973
+ error: {
974
+ message: chunk.message,
975
+ ...code !== void 0 && { code }
976
+ }
977
+ };
978
+ runFinishedEmitted = true;
979
+ return;
980
+ }
981
+ }
982
+ if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
983
+ if (hasEmittedTextMessageStart) yield {
984
+ type: EventType.TEXT_MESSAGE_END,
985
+ messageId: aguiState.messageId,
986
+ model: model || options.model,
987
+ timestamp: Date.now()
988
+ };
989
+ yield {
990
+ type: EventType.RUN_FINISHED,
991
+ runId: aguiState.runId,
992
+ threadId: aguiState.threadId,
993
+ model: model || options.model,
994
+ timestamp: Date.now(),
995
+ finishReason: toolCallMetadata.size > 0 ? "tool_calls" : "stop"
996
+ };
997
+ }
998
+ } catch (error) {
999
+ const errorPayload = toRunErrorPayload(error, `${this.name}.processStreamChunks failed`);
1000
+ const rawEvent = toRunErrorRawEvent(error);
1001
+ options.logger.errors(`${this.name}.processStreamChunks fatal`, {
1002
+ error: errorPayload,
1003
+ source: `${this.name}.processStreamChunks`
1004
+ });
1005
+ yield {
1006
+ type: EventType.RUN_ERROR,
1007
+ model: options.model,
1008
+ timestamp: Date.now(),
1009
+ message: errorPayload.message,
1010
+ ...errorPayload.code !== void 0 && { code: errorPayload.code },
1011
+ ...rawEvent !== void 0 && { rawEvent },
1012
+ error: {
1013
+ message: errorPayload.message,
1014
+ ...errorPayload.code !== void 0 && { code: errorPayload.code }
1015
+ }
1016
+ };
1017
+ }
1018
+ }
1019
+ /**
1020
+ * Maps common TextOptions to Responses API request format.
1021
+ * Override this in subclasses to add provider-specific options.
1022
+ */
1023
+ mapOptionsToRequest(options) {
1024
+ const input = this.convertMessagesToInput(options.messages);
1025
+ const tools = options.tools ? convertToolsToResponsesFormat(options.tools, this.makeStructuredOutputCompatible.bind(this)) : void 0;
1026
+ const modelOptions = options.modelOptions;
1027
+ const combinedSchema = options.outputSchema;
1028
+ const textFormat = combinedSchema ? { text: { format: {
1029
+ type: "json_schema",
1030
+ name: "structured_output",
1031
+ schema: this.makeStructuredOutputCompatible(combinedSchema, Array.isArray(combinedSchema.required) ? combinedSchema.required : void 0),
1032
+ strict: true
1033
+ } } } : void 0;
1034
+ return {
1035
+ ...modelOptions,
1036
+ model: options.model,
1037
+ ...options.metadata !== void 0 && { metadata: options.metadata },
1038
+ ...(() => {
1039
+ const prompts = normalizeSystemPrompts(options.systemPrompts);
1040
+ if (prompts.length === 0) return {};
1041
+ return { instructions: prompts.map((p) => p.content).join("\n") };
1042
+ })(),
1043
+ input,
1044
+ ...tools && tools.length > 0 && { tools },
1045
+ ...textFormat ?? {}
1046
+ };
1047
+ }
1048
+ /**
1049
+ * The OpenAI Responses API supports `tools` and `text.format: json_schema`
1050
+ * together in a single streaming request (per issue #605). Subclasses
1051
+ * that route to providers without this capability should override.
1052
+ */
1053
+ supportsCombinedToolsAndSchema() {
1054
+ return true;
1055
+ }
1056
+ /**
1057
+ * Converts ModelMessage[] to Responses API ResponseInput format.
1058
+ * Override this in subclasses for provider-specific message format quirks.
1059
+ *
1060
+ * Key differences from Chat Completions:
1061
+ * - Tool results use `function_call_output` type (not `tool` role)
1062
+ * - Assistant tool calls are `function_call` objects (not nested in `tool_calls`)
1063
+ * - User content uses `input_text`, `input_image`, `input_file` types
1064
+ * - System prompts go in `instructions`, not as messages
1065
+ */
1066
+ convertMessagesToInput(messages) {
1067
+ const result = [];
1068
+ for (const message of messages) {
1069
+ if (message.role === "tool") {
1070
+ const toolContent = message.content;
1071
+ const output = Array.isArray(toolContent) ? toolContent.map((part) => this.convertContentPartToInput(part)) : typeof toolContent === "string" ? toolContent : JSON.stringify(toolContent);
1072
+ result.push({
1073
+ type: "function_call_output",
1074
+ call_id: message.toolCallId || "",
1075
+ output
1076
+ });
1077
+ continue;
1078
+ }
1079
+ if (message.role === "assistant") {
1080
+ if (message.toolCalls && message.toolCalls.length > 0) for (const toolCall of message.toolCalls) {
1081
+ const argumentsString = typeof toolCall.function.arguments === "string" ? toolCall.function.arguments : JSON.stringify(toolCall.function.arguments);
1082
+ result.push({
1083
+ type: "function_call",
1084
+ call_id: toolCall.id,
1085
+ name: toolCall.function.name,
1086
+ arguments: argumentsString
1087
+ });
1088
+ }
1089
+ if (message.content) {
1090
+ const contentStr = this.extractTextContent(message.content);
1091
+ if (contentStr) result.push({
1092
+ type: "message",
1093
+ role: "assistant",
1094
+ content: contentStr
1095
+ });
1096
+ }
1097
+ continue;
1098
+ }
1099
+ const contentParts = this.normalizeContent(message.content);
1100
+ const inputContent = [];
1101
+ for (const part of contentParts) inputContent.push(this.convertContentPartToInput(part));
1102
+ if (inputContent.length === 0) throw new Error(`User message for ${this.name} has no content parts. Empty user messages would produce a paid request with no input; provide at least one text/image/audio part or omit the message.`);
1103
+ result.push({
1104
+ type: "message",
1105
+ role: "user",
1106
+ content: inputContent
1107
+ });
1108
+ }
1109
+ return result;
1110
+ }
1111
+ /**
1112
+ * Converts a ContentPart to Responses API input content item.
1113
+ * Handles text, image, and audio content parts.
1114
+ * Override this in subclasses for additional content types or provider-specific metadata.
1115
+ */
1116
+ convertContentPartToInput(part) {
1117
+ switch (part.type) {
1118
+ case "text": return {
1119
+ type: "input_text",
1120
+ text: part.content
1121
+ };
1122
+ case "image": {
1123
+ const imageMetadata = part.metadata;
1124
+ if (part.source.type === "url") return {
1125
+ type: "input_image",
1126
+ image_url: part.source.value,
1127
+ detail: imageMetadata?.detail || "auto"
1128
+ };
1129
+ const imageValue = part.source.value;
1130
+ const imageMime = part.source.mimeType || "application/octet-stream";
1131
+ return {
1132
+ type: "input_image",
1133
+ image_url: imageValue.startsWith("data:") ? imageValue : `data:${imageMime};base64,${imageValue}`,
1134
+ detail: imageMetadata?.detail || "auto"
1135
+ };
1136
+ }
1137
+ case "audio": {
1138
+ if (part.source.type === "url") return {
1139
+ type: "input_file",
1140
+ file_url: part.source.value
1141
+ };
1142
+ const audioValue = part.source.value;
1143
+ const audioMime = part.source.mimeType || "application/octet-stream";
1144
+ return {
1145
+ type: "input_file",
1146
+ file_data: audioValue.startsWith("data:") ? audioValue : `data:${audioMime};base64,${audioValue}`
1147
+ };
1148
+ }
1149
+ default: throw new Error(`Unsupported content part type: ${part.type}`);
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Normalizes message content to an array of ContentPart.
1154
+ * Handles backward compatibility with string content.
1155
+ */
1156
+ normalizeContent(content) {
1157
+ if (content === null) return [];
1158
+ if (typeof content === "string") return [{
1159
+ type: "text",
1160
+ content
1161
+ }];
1162
+ return content;
1163
+ }
1164
+ /**
1165
+ * Extracts text content from a content value that may be string, null, or ContentPart array.
1166
+ */
1167
+ extractTextContent(content) {
1168
+ if (content === null) return "";
1169
+ if (typeof content === "string") return content;
1170
+ return content.filter((p) => p.type === "text").map((p) => p.content).join("");
1171
+ }
1323
1172
  };
1324
- //# sourceMappingURL=responses-text.js.map
1173
+ //#endregion
1174
+ export { OpenAIBaseResponsesTextAdapter };
1175
+
1176
+ //# sourceMappingURL=responses-text.js.map