@tanstack/openai-base 0.2.0 → 0.3.0

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 (79) hide show
  1. package/README.md +121 -0
  2. package/dist/esm/adapters/chat-completions-text.d.ts +49 -21
  3. package/dist/esm/adapters/chat-completions-text.js +476 -68
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.d.ts +8 -4
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  7. package/dist/esm/adapters/responses-text.d.ts +46 -33
  8. package/dist/esm/adapters/responses-text.js +657 -142
  9. package/dist/esm/adapters/responses-text.js.map +1 -1
  10. package/dist/esm/index.d.ts +2 -9
  11. package/dist/esm/index.js +4 -16
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/tools/apply-patch-tool.d.ts +2 -2
  14. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  15. package/dist/esm/tools/code-interpreter-tool.d.ts +3 -2
  16. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  17. package/dist/esm/tools/computer-use-tool.d.ts +2 -2
  18. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  19. package/dist/esm/tools/custom-tool.d.ts +2 -2
  20. package/dist/esm/tools/custom-tool.js.map +1 -1
  21. package/dist/esm/tools/file-search-tool.d.ts +2 -2
  22. package/dist/esm/tools/file-search-tool.js.map +1 -1
  23. package/dist/esm/tools/function-tool.d.ts +2 -2
  24. package/dist/esm/tools/function-tool.js.map +1 -1
  25. package/dist/esm/tools/image-generation-tool.d.ts +3 -2
  26. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  27. package/dist/esm/tools/local-shell-tool.d.ts +3 -2
  28. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  29. package/dist/esm/tools/mcp-tool.d.ts +3 -2
  30. package/dist/esm/tools/mcp-tool.js.map +1 -1
  31. package/dist/esm/tools/shell-tool.d.ts +2 -2
  32. package/dist/esm/tools/shell-tool.js.map +1 -1
  33. package/dist/esm/tools/web-search-preview-tool.d.ts +2 -2
  34. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  35. package/dist/esm/tools/web-search-tool.d.ts +2 -2
  36. package/dist/esm/tools/web-search-tool.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/adapters/chat-completions-text.ts +601 -117
  39. package/src/adapters/chat-completions-tool-converter.ts +9 -5
  40. package/src/adapters/responses-text.ts +865 -210
  41. package/src/index.ts +2 -12
  42. package/src/tools/apply-patch-tool.ts +2 -2
  43. package/src/tools/code-interpreter-tool.ts +4 -2
  44. package/src/tools/computer-use-tool.ts +2 -2
  45. package/src/tools/custom-tool.ts +2 -2
  46. package/src/tools/file-search-tool.ts +3 -3
  47. package/src/tools/function-tool.ts +2 -2
  48. package/src/tools/image-generation-tool.ts +4 -2
  49. package/src/tools/local-shell-tool.ts +4 -2
  50. package/src/tools/mcp-tool.ts +4 -2
  51. package/src/tools/shell-tool.ts +2 -2
  52. package/src/tools/web-search-preview-tool.ts +2 -2
  53. package/src/tools/web-search-tool.ts +2 -2
  54. package/dist/esm/adapters/image.d.ts +0 -32
  55. package/dist/esm/adapters/image.js +0 -89
  56. package/dist/esm/adapters/image.js.map +0 -1
  57. package/dist/esm/adapters/summarize.d.ts +0 -28
  58. package/dist/esm/adapters/summarize.js +0 -112
  59. package/dist/esm/adapters/summarize.js.map +0 -1
  60. package/dist/esm/adapters/transcription.d.ts +0 -34
  61. package/dist/esm/adapters/transcription.js +0 -131
  62. package/dist/esm/adapters/transcription.js.map +0 -1
  63. package/dist/esm/adapters/tts.d.ts +0 -26
  64. package/dist/esm/adapters/tts.js +0 -78
  65. package/dist/esm/adapters/tts.js.map +0 -1
  66. package/dist/esm/adapters/video.d.ts +0 -72
  67. package/dist/esm/adapters/video.js +0 -238
  68. package/dist/esm/adapters/video.js.map +0 -1
  69. package/dist/esm/types/config.d.ts +0 -4
  70. package/dist/esm/utils/client.d.ts +0 -3
  71. package/dist/esm/utils/client.js +0 -8
  72. package/dist/esm/utils/client.js.map +0 -1
  73. package/src/adapters/image.ts +0 -158
  74. package/src/adapters/summarize.ts +0 -174
  75. package/src/adapters/transcription.ts +0 -194
  76. package/src/adapters/tts.ts +0 -124
  77. package/src/adapters/video.ts +0 -385
  78. package/src/types/config.ts +0 -5
  79. package/src/utils/client.ts +0 -8
@@ -1,29 +1,27 @@
1
+ import { EventType } from "@tanstack/ai";
1
2
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
2
3
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
3
4
  import { generateId, transformNullsToUndefined } from "@tanstack/ai-utils";
4
- import { createOpenAICompatibleClient } from "../utils/client.js";
5
5
  import { extractRequestOptions } from "../utils/request-options.js";
6
6
  import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
7
7
  import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
8
- const asChunk = (chunk) => chunk;
9
- class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
10
- constructor(config, model, name = "openai-compatible-responses") {
8
+ class OpenAIBaseResponsesTextAdapter extends BaseTextAdapter {
9
+ constructor(model, name, client) {
11
10
  super({}, model);
12
11
  this.kind = "text";
13
12
  this.name = name;
14
- this.client = createOpenAICompatibleClient(config);
13
+ this.client = client;
15
14
  }
16
15
  async *chatStream(options) {
17
16
  const toolCallMetadata = /* @__PURE__ */ new Map();
18
- const requestParams = this.mapOptionsToRequest(options);
19
- const timestamp = Date.now();
20
17
  const aguiState = {
21
18
  runId: generateId(this.name),
19
+ threadId: options.threadId ?? generateId(this.name),
22
20
  messageId: generateId(this.name),
23
- timestamp,
24
21
  hasEmittedRunStarted: false
25
22
  };
26
23
  try {
24
+ const requestParams = this.mapOptionsToRequest(options);
27
25
  options.logger.request(
28
26
  `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
29
27
  { provider: this.name, model: this.model }
@@ -48,20 +46,22 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
48
46
  );
49
47
  if (!aguiState.hasEmittedRunStarted) {
50
48
  aguiState.hasEmittedRunStarted = true;
51
- yield asChunk({
52
- type: "RUN_STARTED",
49
+ yield {
50
+ type: EventType.RUN_STARTED,
53
51
  runId: aguiState.runId,
52
+ threadId: aguiState.threadId,
54
53
  model: options.model,
55
- timestamp
56
- });
54
+ timestamp: Date.now()
55
+ };
57
56
  }
58
- yield asChunk({
59
- type: "RUN_ERROR",
60
- runId: aguiState.runId,
57
+ yield {
58
+ type: EventType.RUN_ERROR,
61
59
  model: options.model,
62
- timestamp,
60
+ timestamp: Date.now(),
61
+ message: errorPayload.message,
62
+ code: errorPayload.code,
63
63
  error: errorPayload
64
- });
64
+ };
65
65
  options.logger.errors(`${this.name}.chatStream fatal`, {
66
66
  error: errorPayload,
67
67
  source: `${this.name}.chatStream`
@@ -115,9 +115,12 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
115
115
  },
116
116
  extractRequestOptions(chatOptions.request)
117
117
  );
118
- const rawText = this.extractTextFromResponse(
119
- response
120
- );
118
+ const rawText = this.extractTextFromResponse(response);
119
+ if (rawText.length === 0) {
120
+ throw new Error(
121
+ `${this.name}.structuredOutput: response contained no content`
122
+ );
123
+ }
121
124
  let parsed;
122
125
  try {
123
126
  parsed = JSON.parse(rawText);
@@ -126,7 +129,7 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
126
129
  `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
127
130
  );
128
131
  }
129
- const transformed = transformNullsToUndefined(parsed);
132
+ const transformed = this.transformStructuredOutput(parsed);
130
133
  return {
131
134
  data: transformed,
132
135
  rawText
@@ -139,6 +142,322 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
139
142
  throw error;
140
143
  }
141
144
  }
145
+ /**
146
+ * Stream structured output via the Responses API: single request with
147
+ * `text.format: json_schema` + `stream: true`. Consumes Responses-API
148
+ * events (`response.output_text.delta`, `response.reasoning_text.delta`,
149
+ * `response.reasoning_summary_text.delta`, `response.refusal.delta`,
150
+ * `response.completed`, `response.failed`) and re-emits the standard AG-UI
151
+ * lifecycle ending with `CUSTOM 'structured-output.complete'`.
152
+ *
153
+ * Tools are stripped (structured output is mutually exclusive with tool
154
+ * calls in this path). Reasoning text is accumulated and surfaced both as
155
+ * REASONING_* lifecycle events during the stream and on the terminal
156
+ * CUSTOM event's `value.reasoning`.
157
+ */
158
+ async *structuredOutputStream(options) {
159
+ const { chatOptions, outputSchema } = options;
160
+ const requestParams = this.mapOptionsToRequest(chatOptions);
161
+ const jsonSchema = this.makeStructuredOutputCompatible(
162
+ outputSchema,
163
+ outputSchema.required
164
+ );
165
+ const timestamp = Date.now();
166
+ const aguiState = {
167
+ runId: generateId(this.name),
168
+ threadId: chatOptions.threadId ?? generateId(this.name),
169
+ messageId: generateId(this.name),
170
+ timestamp,
171
+ hasEmittedRunStarted: false
172
+ };
173
+ let accumulatedContent = "";
174
+ let accumulatedReasoning = "";
175
+ let hasEmittedTextMessageStart = false;
176
+ let reasoningMessageId;
177
+ let stepId;
178
+ let hasClosedReasoning = false;
179
+ let model = chatOptions.model;
180
+ let usage;
181
+ const closeReasoning = (function* () {
182
+ if (reasoningMessageId && !hasClosedReasoning) {
183
+ hasClosedReasoning = true;
184
+ yield {
185
+ type: EventType.REASONING_MESSAGE_END,
186
+ messageId: reasoningMessageId,
187
+ model,
188
+ timestamp
189
+ };
190
+ yield {
191
+ type: EventType.REASONING_END,
192
+ messageId: reasoningMessageId,
193
+ model,
194
+ timestamp
195
+ };
196
+ if (stepId) {
197
+ yield {
198
+ type: EventType.STEP_FINISHED,
199
+ stepName: stepId,
200
+ stepId,
201
+ model,
202
+ timestamp,
203
+ content: accumulatedReasoning
204
+ };
205
+ }
206
+ }
207
+ }).bind(this);
208
+ const openReasoning = (function* () {
209
+ if (reasoningMessageId) return;
210
+ reasoningMessageId = generateId(this.name);
211
+ stepId = generateId(this.name);
212
+ yield {
213
+ type: EventType.REASONING_START,
214
+ messageId: reasoningMessageId,
215
+ model,
216
+ timestamp
217
+ };
218
+ yield {
219
+ type: EventType.REASONING_MESSAGE_START,
220
+ messageId: reasoningMessageId,
221
+ role: "reasoning",
222
+ model,
223
+ timestamp
224
+ };
225
+ yield {
226
+ type: EventType.STEP_STARTED,
227
+ stepName: stepId,
228
+ stepId,
229
+ model,
230
+ timestamp,
231
+ stepType: "thinking"
232
+ };
233
+ }).bind(this);
234
+ try {
235
+ const { tools: _tools, ...cleanParams } = requestParams;
236
+ void _tools;
237
+ chatOptions.logger.request(
238
+ `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
239
+ { provider: this.name, model: this.model }
240
+ );
241
+ const stream = await this.client.responses.create(
242
+ {
243
+ ...cleanParams,
244
+ stream: true,
245
+ text: {
246
+ format: {
247
+ type: "json_schema",
248
+ name: "structured_output",
249
+ schema: jsonSchema,
250
+ strict: true
251
+ }
252
+ }
253
+ },
254
+ extractRequestOptions(chatOptions.request)
255
+ );
256
+ for await (const chunk of stream) {
257
+ chatOptions.logger.provider(
258
+ `provider=${this.name} type=${chunk.type}`,
259
+ { provider: this.name, type: chunk.type }
260
+ );
261
+ if (!aguiState.hasEmittedRunStarted) {
262
+ aguiState.hasEmittedRunStarted = true;
263
+ yield {
264
+ type: EventType.RUN_STARTED,
265
+ runId: aguiState.runId,
266
+ threadId: aguiState.threadId,
267
+ model,
268
+ timestamp
269
+ };
270
+ }
271
+ if (chunk.type === "response.created" || chunk.type === "response.in_progress") {
272
+ const responseModel = chunk.response?.model;
273
+ if (responseModel) model = responseModel;
274
+ continue;
275
+ }
276
+ if (chunk.type === "response.refusal.delta") {
277
+ const delta = typeof chunk.delta === "string" ? chunk.delta : "";
278
+ yield {
279
+ type: EventType.RUN_ERROR,
280
+ runId: aguiState.runId,
281
+ model,
282
+ timestamp,
283
+ message: `Model refused: ${delta}`,
284
+ code: "refusal",
285
+ error: { message: `Model refused: ${delta}`, code: "refusal" }
286
+ };
287
+ return;
288
+ }
289
+ if (chunk.type === "response.reasoning_text.delta" || chunk.type === "response.reasoning_summary_text.delta") {
290
+ const raw = chunk.delta;
291
+ const reasoningDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
292
+ if (!reasoningDelta) continue;
293
+ yield* openReasoning();
294
+ const messageId = reasoningMessageId;
295
+ accumulatedReasoning += reasoningDelta;
296
+ yield {
297
+ type: EventType.REASONING_MESSAGE_CONTENT,
298
+ messageId,
299
+ delta: reasoningDelta,
300
+ model,
301
+ timestamp
302
+ };
303
+ continue;
304
+ }
305
+ if (chunk.type === "response.output_text.delta") {
306
+ const raw = chunk.delta;
307
+ const textDelta = Array.isArray(raw) ? raw.join("") : typeof raw === "string" ? raw : "";
308
+ if (!textDelta) continue;
309
+ yield* closeReasoning();
310
+ if (!hasEmittedTextMessageStart) {
311
+ hasEmittedTextMessageStart = true;
312
+ yield {
313
+ type: EventType.TEXT_MESSAGE_START,
314
+ messageId: aguiState.messageId,
315
+ model,
316
+ timestamp,
317
+ role: "assistant"
318
+ };
319
+ }
320
+ accumulatedContent += textDelta;
321
+ yield {
322
+ type: EventType.TEXT_MESSAGE_CONTENT,
323
+ messageId: aguiState.messageId,
324
+ model,
325
+ timestamp,
326
+ delta: textDelta,
327
+ content: accumulatedContent
328
+ };
329
+ continue;
330
+ }
331
+ if (chunk.type === "response.completed") {
332
+ const response = chunk.response;
333
+ if (response.usage) usage = response.usage;
334
+ if (response.model) model = response.model;
335
+ continue;
336
+ }
337
+ if (chunk.type === "response.failed") {
338
+ const response = chunk.response;
339
+ const message = response?.error?.message || "Responses API stream failed";
340
+ yield {
341
+ type: EventType.RUN_ERROR,
342
+ runId: aguiState.runId,
343
+ model,
344
+ timestamp,
345
+ message,
346
+ code: response?.error?.code,
347
+ error: { message, code: response?.error?.code }
348
+ };
349
+ return;
350
+ }
351
+ }
352
+ yield* closeReasoning();
353
+ if (hasEmittedTextMessageStart) {
354
+ yield {
355
+ type: EventType.TEXT_MESSAGE_END,
356
+ messageId: aguiState.messageId,
357
+ model,
358
+ timestamp
359
+ };
360
+ }
361
+ if (accumulatedContent.length === 0) {
362
+ yield {
363
+ type: EventType.RUN_ERROR,
364
+ runId: aguiState.runId,
365
+ model,
366
+ timestamp,
367
+ message: `${this.name}.structuredOutputStream: response contained no content`,
368
+ code: "empty-response",
369
+ error: {
370
+ message: `${this.name}.structuredOutputStream: response contained no content`,
371
+ code: "empty-response"
372
+ }
373
+ };
374
+ return;
375
+ }
376
+ let parsed;
377
+ try {
378
+ parsed = JSON.parse(accumulatedContent);
379
+ } catch {
380
+ yield {
381
+ type: EventType.RUN_ERROR,
382
+ runId: aguiState.runId,
383
+ model,
384
+ timestamp,
385
+ message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
386
+ code: "parse-error",
387
+ error: {
388
+ message: "Failed to parse structured output as JSON",
389
+ code: "parse-error"
390
+ }
391
+ };
392
+ return;
393
+ }
394
+ const transformed = transformNullsToUndefined(parsed);
395
+ yield {
396
+ type: EventType.CUSTOM,
397
+ name: "structured-output.complete",
398
+ value: {
399
+ object: transformed,
400
+ raw: accumulatedContent,
401
+ ...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
402
+ },
403
+ model,
404
+ timestamp
405
+ };
406
+ yield {
407
+ type: EventType.RUN_FINISHED,
408
+ runId: aguiState.runId,
409
+ threadId: aguiState.threadId,
410
+ model,
411
+ timestamp,
412
+ finishReason: "stop",
413
+ ...usage && {
414
+ usage: {
415
+ promptTokens: usage.input_tokens,
416
+ completionTokens: usage.output_tokens,
417
+ totalTokens: usage.total_tokens
418
+ }
419
+ }
420
+ };
421
+ } catch (error) {
422
+ if (!aguiState.hasEmittedRunStarted) {
423
+ aguiState.hasEmittedRunStarted = true;
424
+ yield {
425
+ type: EventType.RUN_STARTED,
426
+ runId: aguiState.runId,
427
+ threadId: aguiState.threadId,
428
+ model,
429
+ timestamp
430
+ };
431
+ }
432
+ const isAbort = this.isAbortError(error);
433
+ const errorPayload = toRunErrorPayload(
434
+ error,
435
+ `${this.name}.structuredOutputStream failed`
436
+ );
437
+ yield {
438
+ type: EventType.RUN_ERROR,
439
+ runId: aguiState.runId,
440
+ model,
441
+ timestamp,
442
+ message: errorPayload.message,
443
+ code: isAbort ? "aborted" : errorPayload.code,
444
+ error: { ...errorPayload, ...isAbort && { code: "aborted" } }
445
+ };
446
+ chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
447
+ error: errorPayload,
448
+ source: `${this.name}.structuredOutputStream`
449
+ });
450
+ }
451
+ }
452
+ /**
453
+ * Cross-SDK abort detection for `structuredOutputStream`. Mirrors the
454
+ * Chat Completions base; subclasses with proprietary error types override.
455
+ */
456
+ isAbortError(error) {
457
+ if (!error || typeof error !== "object") return false;
458
+ const e = error;
459
+ return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
460
+ }
142
461
  /**
143
462
  * Applies provider-specific transformations for structured output compatibility.
144
463
  * Override this in subclasses to handle provider-specific quirks.
@@ -146,6 +465,18 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
146
465
  makeStructuredOutputCompatible(schema, originalRequired) {
147
466
  return makeStructuredOutputCompatible(schema, originalRequired);
148
467
  }
468
+ /**
469
+ * Final shaping pass applied to parsed structured-output JSON before it is
470
+ * returned to the caller. Default converts `null` values to `undefined` so
471
+ * the result aligns with the original Zod schema's optional-field
472
+ * semantics. Subclasses with different conventions (OpenRouter historically
473
+ * preserves nulls) can override — mirrors the chat-completions base's hook
474
+ * so a subclass that opts out of null-stripping doesn't have to fork the
475
+ * whole `structuredOutput` method.
476
+ */
477
+ transformStructuredOutput(parsed) {
478
+ return transformNullsToUndefined(parsed);
479
+ }
149
480
  /**
150
481
  * Extract text content from a non-streaming Responses API response.
151
482
  * Override this in subclasses for provider-specific response shapes.
@@ -153,13 +484,23 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
153
484
  extractTextFromResponse(response) {
154
485
  let textContent = "";
155
486
  let refusal;
487
+ let sawMessageItem = false;
488
+ const observedItemTypes = /* @__PURE__ */ new Set();
156
489
  for (const item of response.output) {
490
+ observedItemTypes.add(item.type);
157
491
  if (item.type === "message") {
492
+ sawMessageItem = true;
158
493
  for (const part of item.content) {
159
- if (part.type === "output_text") {
160
- textContent += part.text;
494
+ const partType = part.type;
495
+ if (partType === "output_text") {
496
+ textContent += part.text ?? "";
497
+ } else if (partType === "refusal") {
498
+ const refusalText = part.refusal;
499
+ refusal = refusalText || refusal || "Refused without explanation";
161
500
  } else {
162
- refusal = part.refusal || refusal || "Refused without explanation";
501
+ throw new Error(
502
+ `${this.name}.extractTextFromResponse: unsupported message content part type "${partType}"`
503
+ );
163
504
  }
164
505
  }
165
506
  }
@@ -169,6 +510,11 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
169
510
  err.code = "refusal";
170
511
  throw err;
171
512
  }
513
+ if (!textContent && response.output.length > 0 && !sawMessageItem) {
514
+ throw new Error(
515
+ `${this.name}.extractTextFromResponse: response.output contained items of type(s) [${[...observedItemTypes].sort().join(", ")}] but no message text — the model returned a non-text response`
516
+ );
517
+ }
172
518
  return textContent;
173
519
  }
174
520
  /**
@@ -189,7 +535,6 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
189
535
  async *processStreamChunks(stream, toolCallMetadata, options, aguiState) {
190
536
  let accumulatedContent = "";
191
537
  let accumulatedReasoning = "";
192
- const timestamp = aguiState.timestamp;
193
538
  let hasStreamedContentDeltas = false;
194
539
  let hasStreamedReasoningDeltas = false;
195
540
  let model = options.model;
@@ -205,51 +550,52 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
205
550
  });
206
551
  if (!aguiState.hasEmittedRunStarted) {
207
552
  aguiState.hasEmittedRunStarted = true;
208
- yield asChunk({
209
- type: "RUN_STARTED",
553
+ yield {
554
+ type: EventType.RUN_STARTED,
210
555
  runId: aguiState.runId,
556
+ threadId: aguiState.threadId,
211
557
  model: model || options.model,
212
- timestamp
213
- });
558
+ timestamp: Date.now()
559
+ };
214
560
  }
215
561
  const handleContentPart = (contentPart) => {
216
562
  if (contentPart.type === "output_text") {
217
563
  accumulatedContent += contentPart.text || "";
218
- return asChunk({
219
- type: "TEXT_MESSAGE_CONTENT",
564
+ return {
565
+ type: EventType.TEXT_MESSAGE_CONTENT,
220
566
  messageId: aguiState.messageId,
221
567
  model: model || options.model,
222
- timestamp,
568
+ timestamp: Date.now(),
223
569
  delta: contentPart.text || "",
224
570
  content: accumulatedContent
225
- });
571
+ };
226
572
  }
227
573
  if (contentPart.type === "reasoning_text") {
228
574
  accumulatedReasoning += contentPart.text || "";
229
575
  if (!stepId) {
230
576
  stepId = generateId(this.name);
231
577
  }
232
- return asChunk({
233
- type: "STEP_FINISHED",
578
+ return {
579
+ type: EventType.STEP_FINISHED,
580
+ stepName: stepId,
234
581
  stepId,
235
582
  model: model || options.model,
236
- timestamp,
583
+ timestamp: Date.now(),
237
584
  delta: contentPart.text || "",
238
585
  content: accumulatedReasoning
239
- });
586
+ };
240
587
  }
241
588
  const isRefusal = contentPart.type === "refusal";
242
589
  const message = isRefusal ? contentPart.refusal || "Refused without explanation" : `Unsupported response content_part type: ${contentPart.type}`;
243
- return asChunk({
244
- type: "RUN_ERROR",
245
- runId: aguiState.runId,
590
+ const code = isRefusal ? "refusal" : contentPart.type;
591
+ return {
592
+ type: EventType.RUN_ERROR,
246
593
  model: model || options.model,
247
- timestamp,
248
- error: {
249
- message,
250
- code: isRefusal ? "refusal" : contentPart.type
251
- }
252
- });
594
+ timestamp: Date.now(),
595
+ message,
596
+ code,
597
+ error: { message, code }
598
+ };
253
599
  };
254
600
  if (chunk.type === "response.created" || chunk.type === "response.incomplete" || chunk.type === "response.failed") {
255
601
  model = chunk.response.model;
@@ -264,26 +610,27 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
264
610
  }
265
611
  if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
266
612
  if (hasEmittedTextMessageStart) {
267
- yield asChunk({
268
- type: "TEXT_MESSAGE_END",
613
+ yield {
614
+ type: EventType.TEXT_MESSAGE_END,
269
615
  messageId: aguiState.messageId,
270
616
  model: chunk.response.model,
271
- timestamp
272
- });
617
+ timestamp: Date.now()
618
+ };
273
619
  hasEmittedTextMessageStart = false;
274
620
  }
275
621
  const errorMessage = chunk.response.error?.message || chunk.response.incomplete_details?.reason || (chunk.type === "response.failed" ? "Response failed" : "Response ended incomplete");
276
- const errorCode = chunk.response.error?.code || (chunk.response.incomplete_details ? "incomplete" : void 0);
277
- yield asChunk({
278
- type: "RUN_ERROR",
279
- runId: aguiState.runId,
622
+ const errorCode = chunk.response.error?.code ?? (chunk.response.incomplete_details ? "incomplete" : void 0) ?? void 0;
623
+ yield {
624
+ type: EventType.RUN_ERROR,
280
625
  model: chunk.response.model,
281
- timestamp,
626
+ timestamp: Date.now(),
627
+ message: errorMessage,
628
+ ...errorCode !== void 0 && { code: errorCode },
282
629
  error: {
283
630
  message: errorMessage,
284
631
  ...errorCode !== void 0 && { code: errorCode }
285
632
  }
286
- });
633
+ };
287
634
  runFinishedEmitted = true;
288
635
  return;
289
636
  }
@@ -292,24 +639,24 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
292
639
  if (textDelta) {
293
640
  if (!hasEmittedTextMessageStart) {
294
641
  hasEmittedTextMessageStart = true;
295
- yield asChunk({
296
- type: "TEXT_MESSAGE_START",
642
+ yield {
643
+ type: EventType.TEXT_MESSAGE_START,
297
644
  messageId: aguiState.messageId,
298
645
  model: model || options.model,
299
- timestamp,
646
+ timestamp: Date.now(),
300
647
  role: "assistant"
301
- });
648
+ };
302
649
  }
303
650
  accumulatedContent += textDelta;
304
651
  hasStreamedContentDeltas = true;
305
- yield asChunk({
306
- type: "TEXT_MESSAGE_CONTENT",
652
+ yield {
653
+ type: EventType.TEXT_MESSAGE_CONTENT,
307
654
  messageId: aguiState.messageId,
308
655
  model: model || options.model,
309
- timestamp,
656
+ timestamp: Date.now(),
310
657
  delta: textDelta,
311
658
  content: accumulatedContent
312
- });
659
+ };
313
660
  }
314
661
  }
315
662
  if (chunk.type === "response.reasoning_text.delta" && chunk.delta) {
@@ -318,24 +665,27 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
318
665
  if (!hasEmittedStepStarted) {
319
666
  hasEmittedStepStarted = true;
320
667
  stepId = generateId(this.name);
321
- yield asChunk({
322
- type: "STEP_STARTED",
668
+ yield {
669
+ type: EventType.STEP_STARTED,
670
+ stepName: stepId,
323
671
  stepId,
324
672
  model: model || options.model,
325
- timestamp,
673
+ timestamp: Date.now(),
326
674
  stepType: "thinking"
327
- });
675
+ };
328
676
  }
329
677
  accumulatedReasoning += reasoningDelta;
330
678
  hasStreamedReasoningDeltas = true;
331
- yield asChunk({
332
- type: "STEP_FINISHED",
333
- stepId: stepId || generateId(this.name),
679
+ const fallbackStepId = stepId || generateId(this.name);
680
+ yield {
681
+ type: EventType.STEP_FINISHED,
682
+ stepName: fallbackStepId,
683
+ stepId: fallbackStepId,
334
684
  model: model || options.model,
335
- timestamp,
685
+ timestamp: Date.now(),
336
686
  delta: reasoningDelta,
337
687
  content: accumulatedReasoning
338
- });
688
+ };
339
689
  }
340
690
  }
341
691
  if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) {
@@ -344,48 +694,52 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
344
694
  if (!hasEmittedStepStarted) {
345
695
  hasEmittedStepStarted = true;
346
696
  stepId = generateId(this.name);
347
- yield asChunk({
348
- type: "STEP_STARTED",
697
+ yield {
698
+ type: EventType.STEP_STARTED,
699
+ stepName: stepId,
349
700
  stepId,
350
701
  model: model || options.model,
351
- timestamp,
702
+ timestamp: Date.now(),
352
703
  stepType: "thinking"
353
- });
704
+ };
354
705
  }
355
706
  accumulatedReasoning += summaryDelta;
356
707
  hasStreamedReasoningDeltas = true;
357
- yield asChunk({
358
- type: "STEP_FINISHED",
359
- stepId: stepId || generateId(this.name),
708
+ const fallbackStepId = stepId || generateId(this.name);
709
+ yield {
710
+ type: EventType.STEP_FINISHED,
711
+ stepName: fallbackStepId,
712
+ stepId: fallbackStepId,
360
713
  model: model || options.model,
361
- timestamp,
714
+ timestamp: Date.now(),
362
715
  delta: summaryDelta,
363
716
  content: accumulatedReasoning
364
- });
717
+ };
365
718
  }
366
719
  }
367
720
  if (chunk.type === "response.content_part.added") {
368
721
  const contentPart = chunk.part;
369
722
  if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
370
723
  hasEmittedTextMessageStart = true;
371
- yield asChunk({
372
- type: "TEXT_MESSAGE_START",
724
+ yield {
725
+ type: EventType.TEXT_MESSAGE_START,
373
726
  messageId: aguiState.messageId,
374
727
  model: model || options.model,
375
- timestamp,
728
+ timestamp: Date.now(),
376
729
  role: "assistant"
377
- });
730
+ };
378
731
  }
379
732
  if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
380
733
  hasEmittedStepStarted = true;
381
734
  stepId = generateId(this.name);
382
- yield asChunk({
383
- type: "STEP_STARTED",
735
+ yield {
736
+ type: EventType.STEP_STARTED,
737
+ stepName: stepId,
384
738
  stepId,
385
739
  model: model || options.model,
386
- timestamp,
740
+ timestamp: Date.now(),
387
741
  stepType: "thinking"
388
- });
742
+ };
389
743
  }
390
744
  if (contentPart.type === "output_text") {
391
745
  hasStreamedContentDeltas = true;
@@ -407,6 +761,27 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
407
761
  if (contentPart.type === "reasoning_text" && hasStreamedReasoningDeltas) {
408
762
  continue;
409
763
  }
764
+ if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
765
+ hasEmittedTextMessageStart = true;
766
+ yield {
767
+ type: EventType.TEXT_MESSAGE_START,
768
+ messageId: aguiState.messageId,
769
+ model: model || options.model,
770
+ timestamp: Date.now(),
771
+ role: "assistant"
772
+ };
773
+ } else if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
774
+ hasEmittedStepStarted = true;
775
+ stepId = generateId(this.name);
776
+ yield {
777
+ type: EventType.STEP_STARTED,
778
+ stepName: stepId,
779
+ stepId,
780
+ model: model || options.model,
781
+ timestamp: Date.now(),
782
+ stepType: "thinking"
783
+ };
784
+ }
410
785
  const doneChunk = handleContentPart(contentPart);
411
786
  yield doneChunk;
412
787
  if (doneChunk.type === "RUN_ERROR") {
@@ -418,24 +793,27 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
418
793
  const item = chunk.item;
419
794
  if (item.type === "function_call" && item.id) {
420
795
  const existing = toolCallMetadata.get(item.id);
421
- if (!existing?.started) {
422
- if (!existing) {
423
- toolCallMetadata.set(item.id, {
424
- index: chunk.output_index,
425
- name: item.name || "",
426
- started: false
427
- });
428
- }
429
- yield asChunk({
430
- type: "TOOL_CALL_START",
796
+ if (!existing) {
797
+ toolCallMetadata.set(item.id, {
798
+ index: chunk.output_index,
799
+ name: item.name || "",
800
+ started: false
801
+ });
802
+ } else if (!existing.name && item.name) {
803
+ existing.name = item.name;
804
+ }
805
+ const metadata = toolCallMetadata.get(item.id);
806
+ if (!metadata.started && metadata.name) {
807
+ yield {
808
+ type: EventType.TOOL_CALL_START,
431
809
  toolCallId: item.id,
432
- toolCallName: item.name || "",
433
- toolName: item.name || "",
810
+ toolCallName: metadata.name,
811
+ toolName: metadata.name,
434
812
  model: model || options.model,
435
- timestamp,
813
+ timestamp: Date.now(),
436
814
  index: chunk.output_index
437
- });
438
- toolCallMetadata.get(item.id).started = true;
815
+ };
816
+ metadata.started = true;
439
817
  }
440
818
  }
441
819
  }
@@ -452,20 +830,23 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
452
830
  );
453
831
  continue;
454
832
  }
455
- yield asChunk({
456
- type: "TOOL_CALL_ARGS",
833
+ yield {
834
+ type: EventType.TOOL_CALL_ARGS,
457
835
  toolCallId: chunk.item_id,
458
836
  model: model || options.model,
459
- timestamp,
837
+ timestamp: Date.now(),
460
838
  delta: chunk.delta
461
- });
839
+ };
462
840
  }
463
841
  if (chunk.type === "response.function_call_arguments.done") {
464
842
  const { item_id } = chunk;
465
843
  const metadata = toolCallMetadata.get(item_id);
466
844
  if (!metadata?.started) {
845
+ if (metadata) {
846
+ metadata.pendingArguments = chunk.arguments;
847
+ }
467
848
  options.logger.errors(
468
- `${this.name}.processStreamChunks orphan function_call_arguments.done`,
849
+ `${this.name}.processStreamChunks deferring function_call_arguments.done — TOOL_CALL_START not yet emitted (waiting for name)`,
469
850
  {
470
851
  source: `${this.name}.processStreamChunks`,
471
852
  toolCallId: item_id,
@@ -474,7 +855,9 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
474
855
  );
475
856
  continue;
476
857
  }
858
+ if (metadata.ended) continue;
477
859
  const name = metadata.name || "";
860
+ metadata.ended = true;
478
861
  let parsedInput = {};
479
862
  if (chunk.arguments) {
480
863
  try {
@@ -497,75 +880,206 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
497
880
  parsedInput = {};
498
881
  }
499
882
  }
500
- yield asChunk({
501
- type: "TOOL_CALL_END",
883
+ yield {
884
+ type: EventType.TOOL_CALL_END,
502
885
  toolCallId: item_id,
503
886
  toolCallName: name,
504
887
  toolName: name,
505
888
  model: model || options.model,
506
- timestamp,
889
+ timestamp: Date.now(),
507
890
  input: parsedInput
508
- });
891
+ };
892
+ }
893
+ if (chunk.type === "response.output_item.done") {
894
+ const item = chunk.item;
895
+ if (item.type === "function_call" && item.id) {
896
+ const metadata = toolCallMetadata.get(item.id) ?? {
897
+ index: chunk.output_index,
898
+ name: item.name || "",
899
+ started: false
900
+ };
901
+ if (!toolCallMetadata.has(item.id)) {
902
+ toolCallMetadata.set(item.id, metadata);
903
+ } else if (!metadata.name && item.name) {
904
+ metadata.name = item.name;
905
+ }
906
+ if (!metadata.started && metadata.name) {
907
+ yield {
908
+ type: EventType.TOOL_CALL_START,
909
+ toolCallId: item.id,
910
+ toolCallName: metadata.name,
911
+ toolName: metadata.name,
912
+ model: model || options.model,
913
+ timestamp: Date.now(),
914
+ index: metadata.index
915
+ };
916
+ metadata.started = true;
917
+ }
918
+ const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
919
+ if (metadata.started && !metadata.ended && rawArgs !== void 0) {
920
+ const name = metadata.name || "";
921
+ let parsedInput = {};
922
+ if (rawArgs) {
923
+ try {
924
+ const parsed = JSON.parse(rawArgs);
925
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
926
+ } catch (parseError) {
927
+ options.logger.errors(
928
+ `${this.name}.processStreamChunks tool-args JSON parse failed (output_item.done backfill)`,
929
+ {
930
+ error: toRunErrorPayload(
931
+ parseError,
932
+ `tool ${name} (${item.id}) returned malformed JSON arguments`
933
+ ),
934
+ source: `${this.name}.processStreamChunks`,
935
+ toolCallId: item.id,
936
+ toolName: name,
937
+ rawArguments: rawArgs
938
+ }
939
+ );
940
+ parsedInput = {};
941
+ }
942
+ }
943
+ yield {
944
+ type: EventType.TOOL_CALL_END,
945
+ toolCallId: item.id,
946
+ toolCallName: name,
947
+ toolName: name,
948
+ model: model || options.model,
949
+ timestamp: Date.now(),
950
+ input: parsedInput
951
+ };
952
+ metadata.ended = true;
953
+ metadata.pendingArguments = void 0;
954
+ }
955
+ }
509
956
  }
510
957
  if (chunk.type === "response.completed") {
958
+ for (const item of chunk.response.output) {
959
+ if (item.type !== "function_call" || !item.id) continue;
960
+ const metadata = toolCallMetadata.get(item.id) ?? {
961
+ index: 0,
962
+ name: item.name || "",
963
+ started: false
964
+ };
965
+ if (!toolCallMetadata.has(item.id)) {
966
+ toolCallMetadata.set(item.id, metadata);
967
+ } else if (!metadata.name && item.name) {
968
+ metadata.name = item.name;
969
+ }
970
+ if (!metadata.started && metadata.name) {
971
+ yield {
972
+ type: EventType.TOOL_CALL_START,
973
+ toolCallId: item.id,
974
+ toolCallName: metadata.name,
975
+ toolName: metadata.name,
976
+ model: model || options.model,
977
+ timestamp: Date.now(),
978
+ index: metadata.index
979
+ };
980
+ metadata.started = true;
981
+ }
982
+ const rawArgs = typeof item.arguments === "string" && item.arguments.length > 0 ? item.arguments : metadata.pendingArguments;
983
+ if (metadata.started && !metadata.ended) {
984
+ const name = metadata.name || "";
985
+ let parsedInput = {};
986
+ if (rawArgs) {
987
+ try {
988
+ const parsed = JSON.parse(rawArgs);
989
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
990
+ } catch (parseError) {
991
+ options.logger.errors(
992
+ `${this.name}.processStreamChunks tool-args JSON parse failed (response.completed backfill)`,
993
+ {
994
+ error: toRunErrorPayload(
995
+ parseError,
996
+ `tool ${name} (${item.id}) returned malformed JSON arguments`
997
+ ),
998
+ source: `${this.name}.processStreamChunks`,
999
+ toolCallId: item.id,
1000
+ toolName: name,
1001
+ rawArguments: rawArgs
1002
+ }
1003
+ );
1004
+ parsedInput = {};
1005
+ }
1006
+ }
1007
+ yield {
1008
+ type: EventType.TOOL_CALL_END,
1009
+ toolCallId: item.id,
1010
+ toolCallName: name,
1011
+ toolName: name,
1012
+ model: model || options.model,
1013
+ timestamp: Date.now(),
1014
+ input: parsedInput
1015
+ };
1016
+ metadata.ended = true;
1017
+ metadata.pendingArguments = void 0;
1018
+ }
1019
+ }
511
1020
  if (hasEmittedTextMessageStart) {
512
- yield asChunk({
513
- type: "TEXT_MESSAGE_END",
1021
+ yield {
1022
+ type: EventType.TEXT_MESSAGE_END,
514
1023
  messageId: aguiState.messageId,
515
1024
  model: model || options.model,
516
- timestamp
517
- });
1025
+ timestamp: Date.now()
1026
+ };
518
1027
  hasEmittedTextMessageStart = false;
519
1028
  }
520
1029
  const hasFunctionCalls = chunk.response.output.some(
521
1030
  (item) => item.type === "function_call"
522
1031
  );
523
- const finishReason = hasFunctionCalls ? "tool_calls" : chunk.response.incomplete_details?.reason ?? "stop";
524
- yield asChunk({
525
- type: "RUN_FINISHED",
1032
+ const incompleteReason = chunk.response.incomplete_details?.reason;
1033
+ const finishReason = hasFunctionCalls ? "tool_calls" : incompleteReason === "max_output_tokens" ? "length" : incompleteReason === "content_filter" ? "content_filter" : "stop";
1034
+ yield {
1035
+ type: EventType.RUN_FINISHED,
526
1036
  runId: aguiState.runId,
1037
+ threadId: aguiState.threadId,
527
1038
  model: model || options.model,
528
- timestamp,
1039
+ timestamp: Date.now(),
529
1040
  usage: {
530
1041
  promptTokens: chunk.response.usage?.input_tokens || 0,
531
1042
  completionTokens: chunk.response.usage?.output_tokens || 0,
532
1043
  totalTokens: chunk.response.usage?.total_tokens || 0
533
1044
  },
534
1045
  finishReason
535
- });
1046
+ };
536
1047
  runFinishedEmitted = true;
537
1048
  }
538
1049
  if (chunk.type === "error") {
539
- yield asChunk({
540
- type: "RUN_ERROR",
541
- runId: aguiState.runId,
1050
+ yield {
1051
+ type: EventType.RUN_ERROR,
542
1052
  model: model || options.model,
543
- timestamp,
1053
+ timestamp: Date.now(),
1054
+ message: chunk.message,
1055
+ code: chunk.code ?? void 0,
544
1056
  error: {
545
1057
  message: chunk.message,
546
1058
  code: chunk.code ?? void 0
547
1059
  }
548
- });
1060
+ };
549
1061
  runFinishedEmitted = true;
1062
+ return;
550
1063
  }
551
1064
  }
552
1065
  if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
553
1066
  if (hasEmittedTextMessageStart) {
554
- yield asChunk({
555
- type: "TEXT_MESSAGE_END",
1067
+ yield {
1068
+ type: EventType.TEXT_MESSAGE_END,
556
1069
  messageId: aguiState.messageId,
557
1070
  model: model || options.model,
558
- timestamp
559
- });
1071
+ timestamp: Date.now()
1072
+ };
560
1073
  }
561
- yield asChunk({
562
- type: "RUN_FINISHED",
1074
+ yield {
1075
+ type: EventType.RUN_FINISHED,
563
1076
  runId: aguiState.runId,
1077
+ threadId: aguiState.threadId,
564
1078
  model: model || options.model,
565
- timestamp,
1079
+ timestamp: Date.now(),
566
1080
  usage: void 0,
567
1081
  finishReason: toolCallMetadata.size > 0 ? "tool_calls" : "stop"
568
- });
1082
+ };
569
1083
  }
570
1084
  } catch (error) {
571
1085
  const errorPayload = toRunErrorPayload(
@@ -576,13 +1090,14 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
576
1090
  error: errorPayload,
577
1091
  source: `${this.name}.processStreamChunks`
578
1092
  });
579
- yield asChunk({
580
- type: "RUN_ERROR",
581
- runId: aguiState.runId,
1093
+ yield {
1094
+ type: EventType.RUN_ERROR,
582
1095
  model: options.model,
583
- timestamp,
1096
+ timestamp: Date.now(),
1097
+ message: errorPayload.message,
1098
+ code: errorPayload.code,
584
1099
  error: errorPayload
585
- });
1100
+ };
586
1101
  }
587
1102
  }
588
1103
  /**
@@ -755,6 +1270,6 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
755
1270
  }
756
1271
  }
757
1272
  export {
758
- OpenAICompatibleResponsesTextAdapter
1273
+ OpenAIBaseResponsesTextAdapter
759
1274
  };
760
1275
  //# sourceMappingURL=responses-text.js.map