@tanstack/openai-base 0.1.0 → 0.2.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/dist/esm/adapters/chat-completions-text.d.ts +2 -2
  3. package/dist/esm/adapters/chat-completions-text.js +183 -80
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.js +3 -1
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  7. package/dist/esm/adapters/image.js +33 -13
  8. package/dist/esm/adapters/image.js.map +1 -1
  9. package/dist/esm/adapters/responses-text.d.ts +4 -5
  10. package/dist/esm/adapters/responses-text.js +260 -135
  11. package/dist/esm/adapters/responses-text.js.map +1 -1
  12. package/dist/esm/adapters/responses-tool-converter.js +3 -1
  13. package/dist/esm/adapters/responses-tool-converter.js.map +1 -1
  14. package/dist/esm/adapters/summarize.js +63 -25
  15. package/dist/esm/adapters/summarize.js.map +1 -1
  16. package/dist/esm/adapters/transcription.d.ts +0 -5
  17. package/dist/esm/adapters/transcription.js +53 -61
  18. package/dist/esm/adapters/transcription.js.map +1 -1
  19. package/dist/esm/adapters/tts.js +27 -14
  20. package/dist/esm/adapters/tts.js.map +1 -1
  21. package/dist/esm/adapters/video.d.ts +24 -0
  22. package/dist/esm/adapters/video.js +72 -26
  23. package/dist/esm/adapters/video.js.map +1 -1
  24. package/dist/esm/index.d.ts +0 -2
  25. package/dist/esm/index.js +4 -2
  26. package/dist/esm/tools/apply-patch-tool.d.ts +8 -3
  27. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  28. package/dist/esm/tools/code-interpreter-tool.d.ts +9 -4
  29. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  30. package/dist/esm/tools/computer-use-tool.d.ts +9 -4
  31. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  32. package/dist/esm/tools/custom-tool.d.ts +6 -4
  33. package/dist/esm/tools/custom-tool.js.map +1 -1
  34. package/dist/esm/tools/file-search-tool.d.ts +11 -4
  35. package/dist/esm/tools/file-search-tool.js +3 -2
  36. package/dist/esm/tools/file-search-tool.js.map +1 -1
  37. package/dist/esm/tools/function-tool.d.ts +4 -2
  38. package/dist/esm/tools/function-tool.js.map +1 -1
  39. package/dist/esm/tools/image-generation-tool.d.ts +16 -5
  40. package/dist/esm/tools/image-generation-tool.js +4 -3
  41. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  42. package/dist/esm/tools/index.d.ts +13 -13
  43. package/dist/esm/tools/local-shell-tool.d.ts +8 -3
  44. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  45. package/dist/esm/tools/mcp-tool.d.ts +10 -5
  46. package/dist/esm/tools/mcp-tool.js.map +1 -1
  47. package/dist/esm/tools/shell-tool.d.ts +8 -3
  48. package/dist/esm/tools/shell-tool.js.map +1 -1
  49. package/dist/esm/tools/web-search-preview-tool.d.ts +13 -5
  50. package/dist/esm/tools/web-search-preview-tool.js +2 -3
  51. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  52. package/dist/esm/tools/web-search-tool.d.ts +14 -5
  53. package/dist/esm/tools/web-search-tool.js +4 -1
  54. package/dist/esm/tools/web-search-tool.js.map +1 -1
  55. package/dist/esm/utils/request-options.d.ts +14 -0
  56. package/dist/esm/utils/request-options.js +8 -0
  57. package/dist/esm/utils/request-options.js.map +1 -0
  58. package/package.json +16 -19
  59. package/dist/esm/types/message-metadata.d.ts +0 -19
  60. package/dist/esm/types/provider-options.d.ts +0 -40
@@ -1,8 +1,11 @@
1
1
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
2
+ import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
2
3
  import { generateId, transformNullsToUndefined } from "@tanstack/ai-utils";
3
4
  import { createOpenAICompatibleClient } from "../utils/client.js";
5
+ import { extractRequestOptions } from "../utils/request-options.js";
4
6
  import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
5
7
  import { convertToolsToResponsesFormat } from "./responses-tool-converter.js";
8
+ const asChunk = (chunk) => chunk;
6
9
  class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
7
10
  constructor(config, model, name = "openai-compatible-responses") {
8
11
  super({}, model);
@@ -21,15 +24,16 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
21
24
  hasEmittedRunStarted: false
22
25
  };
23
26
  try {
27
+ options.logger.request(
28
+ `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
29
+ { provider: this.name, model: this.model }
30
+ );
24
31
  const response = await this.client.responses.create(
25
32
  {
26
33
  ...requestParams,
27
34
  stream: true
28
35
  },
29
- {
30
- headers: options.request?.headers,
31
- signal: options.request?.signal
32
- }
36
+ extractRequestOptions(options.request)
33
37
  );
34
38
  yield* this.processStreamChunks(
35
39
  response,
@@ -38,32 +42,30 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
38
42
  aguiState
39
43
  );
40
44
  } catch (error) {
41
- const err = error;
45
+ const errorPayload = toRunErrorPayload(
46
+ error,
47
+ `${this.name}.chatStream failed`
48
+ );
42
49
  if (!aguiState.hasEmittedRunStarted) {
43
50
  aguiState.hasEmittedRunStarted = true;
44
- yield {
51
+ yield asChunk({
45
52
  type: "RUN_STARTED",
46
53
  runId: aguiState.runId,
47
54
  model: options.model,
48
55
  timestamp
49
- };
56
+ });
50
57
  }
51
- yield {
58
+ yield asChunk({
52
59
  type: "RUN_ERROR",
53
60
  runId: aguiState.runId,
54
61
  model: options.model,
55
62
  timestamp,
56
- error: {
57
- message: err.message || "Unknown error",
58
- code: err.code
59
- }
60
- };
61
- console.error(
62
- `>>> [${this.name}] chatStream: Fatal error during response creation <<<`
63
- );
64
- console.error(">>> Error message:", err.message);
65
- console.error(">>> Error stack:", err.stack);
66
- console.error(">>> Full error:", err);
63
+ error: errorPayload
64
+ });
65
+ options.logger.errors(`${this.name}.chatStream fatal`, {
66
+ error: errorPayload,
67
+ source: `${this.name}.chatStream`
68
+ });
67
69
  }
68
70
  }
69
71
  /**
@@ -83,12 +85,23 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
83
85
  const requestParams = this.mapOptionsToRequest(chatOptions);
84
86
  const jsonSchema = this.makeStructuredOutputCompatible(
85
87
  outputSchema,
86
- outputSchema.required || []
88
+ outputSchema.required
87
89
  );
88
90
  try {
91
+ const {
92
+ stream: _stream,
93
+ stream_options: _streamOptions,
94
+ ...cleanParams
95
+ } = requestParams;
96
+ void _stream;
97
+ void _streamOptions;
98
+ chatOptions.logger.request(
99
+ `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
100
+ { provider: this.name, model: this.model }
101
+ );
89
102
  const response = await this.client.responses.create(
90
103
  {
91
- ...requestParams,
104
+ ...cleanParams,
92
105
  stream: false,
93
106
  // Configure structured output via text.format
94
107
  text: {
@@ -100,10 +113,7 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
100
113
  }
101
114
  }
102
115
  },
103
- {
104
- headers: chatOptions.request?.headers,
105
- signal: chatOptions.request?.signal
106
- }
116
+ extractRequestOptions(chatOptions.request)
107
117
  );
108
118
  const rawText = this.extractTextFromResponse(
109
119
  response
@@ -122,11 +132,10 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
122
132
  rawText
123
133
  };
124
134
  } catch (error) {
125
- const err = error;
126
- console.error(
127
- `>>> [${this.name}] structuredOutput: Error during response creation <<<`
128
- );
129
- console.error(">>> Error message:", err.message);
135
+ chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
136
+ error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
137
+ source: `${this.name}.structuredOutput`
138
+ });
130
139
  throw error;
131
140
  }
132
141
  }
@@ -143,15 +152,23 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
143
152
  */
144
153
  extractTextFromResponse(response) {
145
154
  let textContent = "";
155
+ let refusal;
146
156
  for (const item of response.output) {
147
157
  if (item.type === "message") {
148
158
  for (const part of item.content) {
149
159
  if (part.type === "output_text") {
150
160
  textContent += part.text;
161
+ } else {
162
+ refusal = part.refusal || refusal || "Refused without explanation";
151
163
  }
152
164
  }
153
165
  }
154
166
  }
167
+ if (!textContent && refusal !== void 0) {
168
+ const err = new Error(`Model refused to respond: ${refusal}`);
169
+ err.code = "refusal";
170
+ throw err;
171
+ }
155
172
  return textContent;
156
173
  }
157
174
  /**
@@ -173,110 +190,126 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
173
190
  let accumulatedContent = "";
174
191
  let accumulatedReasoning = "";
175
192
  const timestamp = aguiState.timestamp;
176
- let chunkCount = 0;
177
193
  let hasStreamedContentDeltas = false;
178
194
  let hasStreamedReasoningDeltas = false;
179
195
  let model = options.model;
180
196
  let stepId = null;
181
197
  let hasEmittedTextMessageStart = false;
182
198
  let hasEmittedStepStarted = false;
199
+ let runFinishedEmitted = false;
183
200
  try {
184
201
  for await (const chunk of stream) {
185
- chunkCount++;
202
+ options.logger.provider(`provider=${this.name} type=${chunk.type}`, {
203
+ provider: this.name,
204
+ type: chunk.type
205
+ });
186
206
  if (!aguiState.hasEmittedRunStarted) {
187
207
  aguiState.hasEmittedRunStarted = true;
188
- yield {
208
+ yield asChunk({
189
209
  type: "RUN_STARTED",
190
210
  runId: aguiState.runId,
191
211
  model: model || options.model,
192
212
  timestamp
193
- };
213
+ });
194
214
  }
195
215
  const handleContentPart = (contentPart) => {
196
216
  if (contentPart.type === "output_text") {
197
217
  accumulatedContent += contentPart.text || "";
198
- return {
218
+ return asChunk({
199
219
  type: "TEXT_MESSAGE_CONTENT",
200
220
  messageId: aguiState.messageId,
201
221
  model: model || options.model,
202
222
  timestamp,
203
223
  delta: contentPart.text || "",
204
224
  content: accumulatedContent
205
- };
225
+ });
206
226
  }
207
227
  if (contentPart.type === "reasoning_text") {
208
228
  accumulatedReasoning += contentPart.text || "";
209
- return {
229
+ if (!stepId) {
230
+ stepId = generateId(this.name);
231
+ }
232
+ return asChunk({
210
233
  type: "STEP_FINISHED",
211
- stepId: stepId || generateId(this.name),
234
+ stepId,
212
235
  model: model || options.model,
213
236
  timestamp,
214
237
  delta: contentPart.text || "",
215
238
  content: accumulatedReasoning
216
- };
239
+ });
217
240
  }
218
- return {
241
+ const isRefusal = contentPart.type === "refusal";
242
+ const message = isRefusal ? contentPart.refusal || "Refused without explanation" : `Unsupported response content_part type: ${contentPart.type}`;
243
+ return asChunk({
219
244
  type: "RUN_ERROR",
220
245
  runId: aguiState.runId,
221
246
  model: model || options.model,
222
247
  timestamp,
223
248
  error: {
224
- message: contentPart.refusal || "Unknown refusal"
249
+ message,
250
+ code: isRefusal ? "refusal" : contentPart.type
225
251
  }
226
- };
252
+ });
227
253
  };
228
254
  if (chunk.type === "response.created" || chunk.type === "response.incomplete" || chunk.type === "response.failed") {
229
255
  model = chunk.response.model;
256
+ }
257
+ if (chunk.type === "response.created") {
230
258
  hasStreamedContentDeltas = false;
231
259
  hasStreamedReasoningDeltas = false;
232
260
  hasEmittedTextMessageStart = false;
233
261
  hasEmittedStepStarted = false;
234
262
  accumulatedContent = "";
235
263
  accumulatedReasoning = "";
236
- if (chunk.response.error) {
237
- yield {
238
- type: "RUN_ERROR",
239
- runId: aguiState.runId,
240
- model: chunk.response.model,
241
- timestamp,
242
- error: chunk.response.error
243
- };
244
- }
245
- if (chunk.response.incomplete_details) {
246
- yield {
247
- type: "RUN_ERROR",
248
- runId: aguiState.runId,
264
+ }
265
+ if (chunk.type === "response.failed" || chunk.type === "response.incomplete") {
266
+ if (hasEmittedTextMessageStart) {
267
+ yield asChunk({
268
+ type: "TEXT_MESSAGE_END",
269
+ messageId: aguiState.messageId,
249
270
  model: chunk.response.model,
250
- timestamp,
251
- error: {
252
- message: chunk.response.incomplete_details.reason ?? ""
253
- }
254
- };
271
+ timestamp
272
+ });
273
+ hasEmittedTextMessageStart = false;
255
274
  }
275
+ 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,
280
+ model: chunk.response.model,
281
+ timestamp,
282
+ error: {
283
+ message: errorMessage,
284
+ ...errorCode !== void 0 && { code: errorCode }
285
+ }
286
+ });
287
+ runFinishedEmitted = true;
288
+ return;
256
289
  }
257
290
  if (chunk.type === "response.output_text.delta" && chunk.delta) {
258
291
  const textDelta = Array.isArray(chunk.delta) ? chunk.delta.join("") : typeof chunk.delta === "string" ? chunk.delta : "";
259
292
  if (textDelta) {
260
293
  if (!hasEmittedTextMessageStart) {
261
294
  hasEmittedTextMessageStart = true;
262
- yield {
295
+ yield asChunk({
263
296
  type: "TEXT_MESSAGE_START",
264
297
  messageId: aguiState.messageId,
265
298
  model: model || options.model,
266
299
  timestamp,
267
300
  role: "assistant"
268
- };
301
+ });
269
302
  }
270
303
  accumulatedContent += textDelta;
271
304
  hasStreamedContentDeltas = true;
272
- yield {
305
+ yield asChunk({
273
306
  type: "TEXT_MESSAGE_CONTENT",
274
307
  messageId: aguiState.messageId,
275
308
  model: model || options.model,
276
309
  timestamp,
277
310
  delta: textDelta,
278
311
  content: accumulatedContent
279
- };
312
+ });
280
313
  }
281
314
  }
282
315
  if (chunk.type === "response.reasoning_text.delta" && chunk.delta) {
@@ -285,24 +318,24 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
285
318
  if (!hasEmittedStepStarted) {
286
319
  hasEmittedStepStarted = true;
287
320
  stepId = generateId(this.name);
288
- yield {
321
+ yield asChunk({
289
322
  type: "STEP_STARTED",
290
323
  stepId,
291
324
  model: model || options.model,
292
325
  timestamp,
293
326
  stepType: "thinking"
294
- };
327
+ });
295
328
  }
296
329
  accumulatedReasoning += reasoningDelta;
297
330
  hasStreamedReasoningDeltas = true;
298
- yield {
331
+ yield asChunk({
299
332
  type: "STEP_FINISHED",
300
333
  stepId: stepId || generateId(this.name),
301
334
  model: model || options.model,
302
335
  timestamp,
303
336
  delta: reasoningDelta,
304
337
  content: accumulatedReasoning
305
- };
338
+ });
306
339
  }
307
340
  }
308
341
  if (chunk.type === "response.reasoning_summary_text.delta" && chunk.delta) {
@@ -311,50 +344,60 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
311
344
  if (!hasEmittedStepStarted) {
312
345
  hasEmittedStepStarted = true;
313
346
  stepId = generateId(this.name);
314
- yield {
347
+ yield asChunk({
315
348
  type: "STEP_STARTED",
316
349
  stepId,
317
350
  model: model || options.model,
318
351
  timestamp,
319
352
  stepType: "thinking"
320
- };
353
+ });
321
354
  }
322
355
  accumulatedReasoning += summaryDelta;
323
356
  hasStreamedReasoningDeltas = true;
324
- yield {
357
+ yield asChunk({
325
358
  type: "STEP_FINISHED",
326
359
  stepId: stepId || generateId(this.name),
327
360
  model: model || options.model,
328
361
  timestamp,
329
362
  delta: summaryDelta,
330
363
  content: accumulatedReasoning
331
- };
364
+ });
332
365
  }
333
366
  }
334
367
  if (chunk.type === "response.content_part.added") {
335
368
  const contentPart = chunk.part;
336
369
  if (contentPart.type === "output_text" && !hasEmittedTextMessageStart) {
337
370
  hasEmittedTextMessageStart = true;
338
- yield {
371
+ yield asChunk({
339
372
  type: "TEXT_MESSAGE_START",
340
373
  messageId: aguiState.messageId,
341
374
  model: model || options.model,
342
375
  timestamp,
343
376
  role: "assistant"
344
- };
377
+ });
345
378
  }
346
379
  if (contentPart.type === "reasoning_text" && !hasEmittedStepStarted) {
347
380
  hasEmittedStepStarted = true;
348
381
  stepId = generateId(this.name);
349
- yield {
382
+ yield asChunk({
350
383
  type: "STEP_STARTED",
351
384
  stepId,
352
385
  model: model || options.model,
353
386
  timestamp,
354
387
  stepType: "thinking"
355
- };
388
+ });
389
+ }
390
+ if (contentPart.type === "output_text") {
391
+ hasStreamedContentDeltas = true;
392
+ } else if (contentPart.type === "reasoning_text") {
393
+ hasStreamedReasoningDeltas = true;
394
+ }
395
+ const partChunk = handleContentPart(contentPart);
396
+ yield partChunk;
397
+ if (partChunk.type === "RUN_ERROR") {
398
+ runFinishedEmitted = true;
399
+ return;
356
400
  }
357
- yield handleContentPart(contentPart);
358
401
  }
359
402
  if (chunk.type === "response.content_part.done") {
360
403
  const contentPart = chunk.part;
@@ -364,76 +407,121 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
364
407
  if (contentPart.type === "reasoning_text" && hasStreamedReasoningDeltas) {
365
408
  continue;
366
409
  }
367
- yield handleContentPart(contentPart);
410
+ const doneChunk = handleContentPart(contentPart);
411
+ yield doneChunk;
412
+ if (doneChunk.type === "RUN_ERROR") {
413
+ runFinishedEmitted = true;
414
+ return;
415
+ }
368
416
  }
369
417
  if (chunk.type === "response.output_item.added") {
370
418
  const item = chunk.item;
371
- if (item.type === "function_call") {
372
- const itemId = item.id || item.call_id;
373
- const callId = item.call_id || item.id || "";
374
- if (!toolCallMetadata.has(itemId)) {
375
- toolCallMetadata.set(itemId, {
376
- index: chunk.output_index,
377
- name: item.name || "",
378
- callId,
379
- started: false
419
+ if (item.type === "function_call" && item.id) {
420
+ 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",
431
+ toolCallId: item.id,
432
+ toolCallName: item.name || "",
433
+ toolName: item.name || "",
434
+ model: model || options.model,
435
+ timestamp,
436
+ index: chunk.output_index
380
437
  });
438
+ toolCallMetadata.get(item.id).started = true;
381
439
  }
382
- yield {
383
- type: "TOOL_CALL_START",
384
- toolCallId: callId,
385
- toolName: item.name || "",
386
- model: model || options.model,
387
- timestamp,
388
- index: chunk.output_index
389
- };
390
- toolCallMetadata.get(itemId).started = true;
391
440
  }
392
441
  }
393
442
  if (chunk.type === "response.function_call_arguments.delta" && chunk.delta) {
394
443
  const metadata = toolCallMetadata.get(chunk.item_id);
395
- yield {
444
+ if (!metadata?.started) {
445
+ options.logger.errors(
446
+ `${this.name}.processStreamChunks orphan function_call_arguments.delta`,
447
+ {
448
+ source: `${this.name}.processStreamChunks`,
449
+ toolCallId: chunk.item_id,
450
+ rawDelta: chunk.delta
451
+ }
452
+ );
453
+ continue;
454
+ }
455
+ yield asChunk({
396
456
  type: "TOOL_CALL_ARGS",
397
- toolCallId: metadata?.callId || chunk.item_id,
457
+ toolCallId: chunk.item_id,
398
458
  model: model || options.model,
399
459
  timestamp,
400
- delta: chunk.delta,
401
- args: metadata ? void 0 : chunk.delta
402
- };
460
+ delta: chunk.delta
461
+ });
403
462
  }
404
463
  if (chunk.type === "response.function_call_arguments.done") {
405
464
  const { item_id } = chunk;
406
465
  const metadata = toolCallMetadata.get(item_id);
407
- const name = metadata?.name || "";
408
- const callId = metadata?.callId || item_id;
466
+ if (!metadata?.started) {
467
+ options.logger.errors(
468
+ `${this.name}.processStreamChunks orphan function_call_arguments.done`,
469
+ {
470
+ source: `${this.name}.processStreamChunks`,
471
+ toolCallId: item_id,
472
+ rawArguments: chunk.arguments
473
+ }
474
+ );
475
+ continue;
476
+ }
477
+ const name = metadata.name || "";
409
478
  let parsedInput = {};
410
- try {
411
- parsedInput = chunk.arguments ? JSON.parse(chunk.arguments) : {};
412
- } catch {
413
- parsedInput = {};
479
+ if (chunk.arguments) {
480
+ try {
481
+ const parsed = JSON.parse(chunk.arguments);
482
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
483
+ } catch (parseError) {
484
+ options.logger.errors(
485
+ `${this.name}.processStreamChunks tool-args JSON parse failed`,
486
+ {
487
+ error: toRunErrorPayload(
488
+ parseError,
489
+ `tool ${name} (${item_id}) returned malformed JSON arguments`
490
+ ),
491
+ source: `${this.name}.processStreamChunks`,
492
+ toolCallId: item_id,
493
+ toolName: name,
494
+ rawArguments: chunk.arguments
495
+ }
496
+ );
497
+ parsedInput = {};
498
+ }
414
499
  }
415
- yield {
500
+ yield asChunk({
416
501
  type: "TOOL_CALL_END",
417
- toolCallId: callId,
502
+ toolCallId: item_id,
503
+ toolCallName: name,
418
504
  toolName: name,
419
505
  model: model || options.model,
420
506
  timestamp,
421
507
  input: parsedInput
422
- };
508
+ });
423
509
  }
424
510
  if (chunk.type === "response.completed") {
425
511
  if (hasEmittedTextMessageStart) {
426
- yield {
512
+ yield asChunk({
427
513
  type: "TEXT_MESSAGE_END",
428
514
  messageId: aguiState.messageId,
429
515
  model: model || options.model,
430
516
  timestamp
431
- };
517
+ });
518
+ hasEmittedTextMessageStart = false;
432
519
  }
433
520
  const hasFunctionCalls = chunk.response.output.some(
434
521
  (item) => item.type === "function_call"
435
522
  );
436
- yield {
523
+ const finishReason = hasFunctionCalls ? "tool_calls" : chunk.response.incomplete_details?.reason ?? "stop";
524
+ yield asChunk({
437
525
  type: "RUN_FINISHED",
438
526
  runId: aguiState.runId,
439
527
  model: model || options.model,
@@ -443,11 +531,12 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
443
531
  completionTokens: chunk.response.usage?.output_tokens || 0,
444
532
  totalTokens: chunk.response.usage?.total_tokens || 0
445
533
  },
446
- finishReason: hasFunctionCalls ? "tool_calls" : "stop"
447
- };
534
+ finishReason
535
+ });
536
+ runFinishedEmitted = true;
448
537
  }
449
538
  if (chunk.type === "error") {
450
- yield {
539
+ yield asChunk({
451
540
  type: "RUN_ERROR",
452
541
  runId: aguiState.runId,
453
542
  model: model || options.model,
@@ -456,22 +545,44 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
456
545
  message: chunk.message,
457
546
  code: chunk.code ?? void 0
458
547
  }
459
- };
548
+ });
549
+ runFinishedEmitted = true;
550
+ }
551
+ }
552
+ if (!runFinishedEmitted && aguiState.hasEmittedRunStarted) {
553
+ if (hasEmittedTextMessageStart) {
554
+ yield asChunk({
555
+ type: "TEXT_MESSAGE_END",
556
+ messageId: aguiState.messageId,
557
+ model: model || options.model,
558
+ timestamp
559
+ });
460
560
  }
561
+ yield asChunk({
562
+ type: "RUN_FINISHED",
563
+ runId: aguiState.runId,
564
+ model: model || options.model,
565
+ timestamp,
566
+ usage: void 0,
567
+ finishReason: toolCallMetadata.size > 0 ? "tool_calls" : "stop"
568
+ });
461
569
  }
462
570
  } catch (error) {
463
- const err = error;
464
- console.error(`[${this.name}] Stream ended with error:`, err.message);
465
- yield {
571
+ const errorPayload = toRunErrorPayload(
572
+ error,
573
+ `${this.name}.processStreamChunks failed`
574
+ );
575
+ options.logger.errors(`${this.name}.processStreamChunks fatal`, {
576
+ error: errorPayload,
577
+ source: `${this.name}.processStreamChunks`
578
+ });
579
+ yield asChunk({
466
580
  type: "RUN_ERROR",
467
581
  runId: aguiState.runId,
468
582
  model: options.model,
469
583
  timestamp,
470
- error: {
471
- message: err.message || "Unknown error occurred",
472
- code: err.code
473
- }
474
- };
584
+ error: errorPayload
585
+ });
475
586
  }
476
587
  }
477
588
  /**
@@ -486,15 +597,23 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
486
597
  ) : void 0;
487
598
  const modelOptions = options.modelOptions;
488
599
  return {
489
- model: options.model,
490
- temperature: options.temperature,
491
- max_output_tokens: options.maxTokens,
492
- top_p: options.topP,
493
- metadata: options.metadata,
494
- instructions: options.systemPrompts?.join("\n"),
495
600
  ...modelOptions,
601
+ model: options.model,
602
+ ...options.temperature !== void 0 && {
603
+ temperature: options.temperature
604
+ },
605
+ ...options.maxTokens !== void 0 && {
606
+ max_output_tokens: options.maxTokens
607
+ },
608
+ ...options.topP !== void 0 && { top_p: options.topP },
609
+ ...options.metadata !== void 0 && { metadata: options.metadata },
610
+ ...options.systemPrompts && options.systemPrompts.length > 0 && {
611
+ instructions: options.systemPrompts.join("\n")
612
+ },
496
613
  input,
497
- tools
614
+ // Conditional spread: `tools: undefined` would clobber any
615
+ // modelOptions.tools the caller set above.
616
+ ...tools && tools.length > 0 && { tools }
498
617
  };
499
618
  }
500
619
  /**
@@ -548,7 +667,9 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
548
667
  inputContent.push(this.convertContentPartToInput(part));
549
668
  }
550
669
  if (inputContent.length === 0) {
551
- inputContent.push({ type: "input_text", text: "" });
670
+ throw new Error(
671
+ `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.`
672
+ );
552
673
  }
553
674
  result.push({
554
675
  type: "message",
@@ -580,7 +701,8 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
580
701
  };
581
702
  }
582
703
  const imageValue = part.source.value;
583
- const imageUrl = imageValue.startsWith("data:") ? imageValue : `data:${part.source.mimeType};base64,${imageValue}`;
704
+ const imageMime = part.source.mimeType || "application/octet-stream";
705
+ const imageUrl = imageValue.startsWith("data:") ? imageValue : `data:${imageMime};base64,${imageValue}`;
584
706
  return {
585
707
  type: "input_image",
586
708
  image_url: imageUrl,
@@ -594,9 +716,12 @@ class OpenAICompatibleResponsesTextAdapter extends BaseTextAdapter {
594
716
  file_url: part.source.value
595
717
  };
596
718
  }
719
+ const audioValue = part.source.value;
720
+ const audioMime = part.source.mimeType || "application/octet-stream";
721
+ const audioFileData = audioValue.startsWith("data:") ? audioValue : `data:${audioMime};base64,${audioValue}`;
597
722
  return {
598
723
  type: "input_file",
599
- file_data: part.source.value
724
+ file_data: audioFileData
600
725
  };
601
726
  }
602
727
  default: