@tanstack/openai-base 0.2.1 → 0.3.1

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