@tanstack/openai-base 0.1.0 → 0.2.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 (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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tanner Linsley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -15,7 +15,7 @@ import { OpenAICompatibleClientConfig } from '../types/config.js';
15
15
  * All methods that build requests or process responses are `protected` so subclasses
16
16
  * can override them.
17
17
  */
18
- export declare class OpenAICompatibleChatCompletionsTextAdapter<TModel extends string, TProviderOptions extends Record<string, any> = Record<string, any>, TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>, TMessageMetadata extends DefaultMessageMetadataByModality = DefaultMessageMetadataByModality> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, TMessageMetadata> {
18
+ export declare class OpenAICompatibleChatCompletionsTextAdapter<TModel extends string, TProviderOptions extends Record<string, any> = Record<string, any>, TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>, TMessageMetadata extends DefaultMessageMetadataByModality = DefaultMessageMetadataByModality, TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>> extends BaseTextAdapter<TModel, TProviderOptions, TInputModalities, TMessageMetadata, TToolCapabilities> {
19
19
  readonly kind: "text";
20
20
  readonly name: string;
21
21
  protected client: OpenAI_SDK;
@@ -38,7 +38,7 @@ export declare class OpenAICompatibleChatCompletionsTextAdapter<TModel extends s
38
38
  * Applies provider-specific transformations for structured output compatibility.
39
39
  * Override this in subclasses to handle provider-specific quirks.
40
40
  */
41
- protected makeStructuredOutputCompatible(schema: Record<string, any>, originalRequired: Array<string>): Record<string, any>;
41
+ protected makeStructuredOutputCompatible(schema: Record<string, any>, originalRequired?: Array<string>): Record<string, any>;
42
42
  /**
43
43
  * Processes streamed chunks from the Chat Completions API and yields AG-UI events.
44
44
  * Override this in subclasses to handle provider-specific stream behavior.
@@ -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 { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
8
+ const asChunk = (chunk) => chunk;
6
9
  class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
7
10
  constructor(config, model, name = "openai-compatible") {
8
11
  super({}, model);
@@ -20,45 +23,44 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
20
23
  hasEmittedRunStarted: false
21
24
  };
22
25
  try {
26
+ options.logger.request(
27
+ `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
28
+ { provider: this.name, model: this.model }
29
+ );
23
30
  const stream = await this.client.chat.completions.create(
24
31
  {
25
32
  ...requestParams,
26
33
  stream: true,
27
34
  stream_options: { include_usage: 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(stream, options, aguiState);
35
39
  } catch (error) {
36
- const err = error;
40
+ const errorPayload = toRunErrorPayload(
41
+ error,
42
+ `${this.name}.chatStream failed`
43
+ );
37
44
  if (!aguiState.hasEmittedRunStarted) {
38
45
  aguiState.hasEmittedRunStarted = true;
39
- yield {
46
+ yield asChunk({
40
47
  type: "RUN_STARTED",
41
48
  runId: aguiState.runId,
42
49
  model: options.model,
43
50
  timestamp
44
- };
51
+ });
45
52
  }
46
- yield {
53
+ yield asChunk({
47
54
  type: "RUN_ERROR",
48
55
  runId: aguiState.runId,
49
56
  model: options.model,
50
57
  timestamp,
51
- error: {
52
- message: err.message || "Unknown error",
53
- code: err.code
54
- }
55
- };
56
- console.error(
57
- `>>> [${this.name}] chatStream: Fatal error during response creation <<<`
58
- );
59
- console.error(">>> Error message:", err.message);
60
- console.error(">>> Error stack:", err.stack);
61
- console.error(">>> Full error:", err);
58
+ error: errorPayload
59
+ });
60
+ options.logger.errors(`${this.name}.chatStream fatal`, {
61
+ error: errorPayload,
62
+ source: `${this.name}.chatStream`
63
+ });
62
64
  }
63
65
  }
64
66
  /**
@@ -78,7 +80,7 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
78
80
  const requestParams = this.mapOptionsToRequest(chatOptions);
79
81
  const jsonSchema = this.makeStructuredOutputCompatible(
80
82
  outputSchema,
81
- outputSchema.required || []
83
+ outputSchema.required
82
84
  );
83
85
  try {
84
86
  const {
@@ -86,6 +88,10 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
86
88
  stream: __,
87
89
  ...cleanParams
88
90
  } = requestParams;
91
+ chatOptions.logger.request(
92
+ `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
93
+ { provider: this.name, model: this.model }
94
+ );
89
95
  const response = await this.client.chat.completions.create(
90
96
  {
91
97
  ...cleanParams,
@@ -99,10 +105,7 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
99
105
  }
100
106
  }
101
107
  },
102
- {
103
- headers: chatOptions.request?.headers,
104
- signal: chatOptions.request?.signal
105
- }
108
+ extractRequestOptions(chatOptions.request)
106
109
  );
107
110
  const rawText = response.choices[0]?.message.content || "";
108
111
  let parsed;
@@ -119,11 +122,10 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
119
122
  rawText
120
123
  };
121
124
  } catch (error) {
122
- const err = error;
123
- console.error(
124
- `>>> [${this.name}] structuredOutput: Error during response creation <<<`
125
- );
126
- console.error(">>> Error message:", err.message);
125
+ chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
126
+ error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
127
+ source: `${this.name}.structuredOutput`
128
+ });
127
129
  throw error;
128
130
  }
129
131
  }
@@ -142,43 +144,58 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
142
144
  let accumulatedContent = "";
143
145
  const timestamp = aguiState.timestamp;
144
146
  let hasEmittedTextMessageStart = false;
147
+ let lastModel;
148
+ let lastUsage;
149
+ let pendingFinishReason;
145
150
  const toolCallsInProgress = /* @__PURE__ */ new Map();
151
+ let emittedAnyToolCallEnd = false;
146
152
  try {
147
153
  for await (const chunk of stream) {
148
- const choice = chunk.choices[0];
149
- if (!choice) continue;
154
+ const choiceForLog = chunk.choices[0];
155
+ options.logger.provider(
156
+ `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`,
157
+ { provider: this.name, model: chunk.model }
158
+ );
159
+ if (chunk.usage) {
160
+ lastUsage = chunk.usage;
161
+ }
162
+ if (chunk.model) {
163
+ lastModel = chunk.model;
164
+ }
150
165
  if (!aguiState.hasEmittedRunStarted) {
151
166
  aguiState.hasEmittedRunStarted = true;
152
- yield {
167
+ yield asChunk({
153
168
  type: "RUN_STARTED",
154
169
  runId: aguiState.runId,
155
170
  model: chunk.model || options.model,
156
171
  timestamp
157
- };
172
+ });
158
173
  }
174
+ const choice = chunk.choices[0];
175
+ if (!choice) continue;
159
176
  const delta = choice.delta;
160
177
  const deltaContent = delta.content;
161
178
  const deltaToolCalls = delta.tool_calls;
162
179
  if (deltaContent) {
163
180
  if (!hasEmittedTextMessageStart) {
164
181
  hasEmittedTextMessageStart = true;
165
- yield {
182
+ yield asChunk({
166
183
  type: "TEXT_MESSAGE_START",
167
184
  messageId: aguiState.messageId,
168
185
  model: chunk.model || options.model,
169
186
  timestamp,
170
187
  role: "assistant"
171
- };
188
+ });
172
189
  }
173
190
  accumulatedContent += deltaContent;
174
- yield {
191
+ yield asChunk({
175
192
  type: "TEXT_MESSAGE_CONTENT",
176
193
  messageId: aguiState.messageId,
177
194
  model: chunk.model || options.model,
178
195
  timestamp,
179
196
  delta: deltaContent,
180
197
  content: accumulatedContent
181
- };
198
+ });
182
199
  }
183
200
  if (deltaToolCalls) {
184
201
  for (const toolCallDelta of deltaToolCalls) {
@@ -203,81 +220,142 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
203
220
  }
204
221
  if (toolCall.id && toolCall.name && !toolCall.started) {
205
222
  toolCall.started = true;
206
- yield {
223
+ yield asChunk({
207
224
  type: "TOOL_CALL_START",
208
225
  toolCallId: toolCall.id,
226
+ toolCallName: toolCall.name,
209
227
  toolName: toolCall.name,
210
228
  model: chunk.model || options.model,
211
229
  timestamp,
212
230
  index
213
- };
231
+ });
214
232
  }
215
233
  if (toolCallDelta.function?.arguments && toolCall.started) {
216
- yield {
234
+ yield asChunk({
217
235
  type: "TOOL_CALL_ARGS",
218
236
  toolCallId: toolCall.id,
219
237
  model: chunk.model || options.model,
220
238
  timestamp,
221
239
  delta: toolCallDelta.function.arguments
222
- };
240
+ });
223
241
  }
224
242
  }
225
243
  }
226
244
  if (choice.finish_reason) {
227
245
  if (choice.finish_reason === "tool_calls" || toolCallsInProgress.size > 0) {
228
246
  for (const [, toolCall] of toolCallsInProgress) {
247
+ if (!toolCall.started) continue;
229
248
  let parsedInput = {};
230
- try {
231
- parsedInput = toolCall.arguments ? JSON.parse(toolCall.arguments) : {};
232
- } catch {
233
- parsedInput = {};
249
+ if (toolCall.arguments) {
250
+ try {
251
+ const parsed = JSON.parse(toolCall.arguments);
252
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
253
+ } catch (parseError) {
254
+ options.logger.errors(
255
+ `${this.name}.processStreamChunks tool-args JSON parse failed`,
256
+ {
257
+ error: toRunErrorPayload(
258
+ parseError,
259
+ `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`
260
+ ),
261
+ source: `${this.name}.processStreamChunks`,
262
+ toolCallId: toolCall.id,
263
+ toolName: toolCall.name,
264
+ rawArguments: toolCall.arguments
265
+ }
266
+ );
267
+ parsedInput = {};
268
+ }
234
269
  }
235
- yield {
270
+ yield asChunk({
236
271
  type: "TOOL_CALL_END",
237
272
  toolCallId: toolCall.id,
273
+ toolCallName: toolCall.name,
238
274
  toolName: toolCall.name,
239
275
  model: chunk.model || options.model,
240
276
  timestamp,
241
277
  input: parsedInput
242
- };
278
+ });
279
+ emittedAnyToolCallEnd = true;
243
280
  }
281
+ toolCallsInProgress.clear();
244
282
  }
245
- const computedFinishReason = choice.finish_reason === "tool_calls" || toolCallsInProgress.size > 0 ? "tool_calls" : "stop";
246
283
  if (hasEmittedTextMessageStart) {
247
- yield {
284
+ yield asChunk({
248
285
  type: "TEXT_MESSAGE_END",
249
286
  messageId: aguiState.messageId,
250
287
  model: chunk.model || options.model,
251
288
  timestamp
252
- };
289
+ });
290
+ hasEmittedTextMessageStart = false;
253
291
  }
254
- yield {
255
- type: "RUN_FINISHED",
256
- runId: aguiState.runId,
257
- model: chunk.model || options.model,
292
+ pendingFinishReason = choice.finish_reason;
293
+ }
294
+ }
295
+ if (aguiState.hasEmittedRunStarted) {
296
+ let pendingToolCount = 0;
297
+ for (const [, toolCall] of toolCallsInProgress) {
298
+ if (!toolCall.started) continue;
299
+ let parsedInput = {};
300
+ if (toolCall.arguments) {
301
+ try {
302
+ const parsed = JSON.parse(toolCall.arguments);
303
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
304
+ } catch {
305
+ parsedInput = {};
306
+ }
307
+ }
308
+ yield asChunk({
309
+ type: "TOOL_CALL_END",
310
+ toolCallId: toolCall.id,
311
+ toolCallName: toolCall.name,
312
+ toolName: toolCall.name,
313
+ model: lastModel || options.model,
258
314
  timestamp,
259
- usage: chunk.usage ? {
260
- promptTokens: chunk.usage.prompt_tokens || 0,
261
- completionTokens: chunk.usage.completion_tokens || 0,
262
- totalTokens: chunk.usage.total_tokens || 0
263
- } : void 0,
264
- finishReason: computedFinishReason
265
- };
315
+ input: parsedInput
316
+ });
317
+ pendingToolCount += 1;
318
+ emittedAnyToolCallEnd = true;
319
+ }
320
+ toolCallsInProgress.clear();
321
+ if (hasEmittedTextMessageStart) {
322
+ yield asChunk({
323
+ type: "TEXT_MESSAGE_END",
324
+ messageId: aguiState.messageId,
325
+ model: lastModel || options.model,
326
+ timestamp
327
+ });
266
328
  }
329
+ const finishReason = emittedAnyToolCallEnd ? "tool_calls" : pendingFinishReason === "tool_calls" ? "stop" : pendingFinishReason ?? "stop";
330
+ yield asChunk({
331
+ type: "RUN_FINISHED",
332
+ runId: aguiState.runId,
333
+ model: lastModel || options.model,
334
+ timestamp,
335
+ usage: lastUsage ? {
336
+ promptTokens: lastUsage.prompt_tokens || 0,
337
+ completionTokens: lastUsage.completion_tokens || 0,
338
+ totalTokens: lastUsage.total_tokens || 0
339
+ } : void 0,
340
+ finishReason
341
+ });
267
342
  }
268
343
  } catch (error) {
269
- const err = error;
270
- console.error(`[${this.name}] Stream ended with error:`, err.message);
271
- yield {
344
+ const errorPayload = toRunErrorPayload(
345
+ error,
346
+ `${this.name}.processStreamChunks failed`
347
+ );
348
+ options.logger.errors(`${this.name}.processStreamChunks fatal`, {
349
+ error: errorPayload,
350
+ source: `${this.name}.processStreamChunks`
351
+ });
352
+ yield asChunk({
272
353
  type: "RUN_ERROR",
273
354
  runId: aguiState.runId,
274
355
  model: options.model,
275
356
  timestamp,
276
- error: {
277
- message: err.message || "Unknown error occurred",
278
- code: err.code
279
- }
280
- };
357
+ error: errorPayload
358
+ });
281
359
  }
282
360
  }
283
361
  /**
@@ -304,10 +382,18 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
304
382
  ...modelOptions,
305
383
  model: options.model,
306
384
  messages,
307
- temperature: options.temperature,
308
- max_tokens: options.maxTokens,
309
- top_p: options.topP,
310
- tools,
385
+ ...options.temperature !== void 0 && {
386
+ temperature: options.temperature
387
+ },
388
+ ...options.maxTokens !== void 0 && {
389
+ max_tokens: options.maxTokens
390
+ },
391
+ ...options.topP !== void 0 && { top_p: options.topP },
392
+ // Conditional spread: `tools: undefined` would clobber any
393
+ // modelOptions.tools the caller set above.
394
+ ...tools && tools.length > 0 && {
395
+ tools
396
+ },
311
397
  stream: true
312
398
  };
313
399
  }
@@ -332,29 +418,45 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
332
418
  arguments: typeof tc.function.arguments === "string" ? tc.function.arguments : JSON.stringify(tc.function.arguments)
333
419
  }
334
420
  }));
421
+ const hasToolCalls = !!toolCalls && toolCalls.length > 0;
422
+ const textContent = this.extractTextContent(message.content);
335
423
  return {
336
424
  role: "assistant",
337
- content: this.extractTextContent(message.content),
338
- ...toolCalls && toolCalls.length > 0 ? { tool_calls: toolCalls } : {}
425
+ content: hasToolCalls && !textContent ? null : textContent,
426
+ ...hasToolCalls ? { tool_calls: toolCalls } : {}
339
427
  };
340
428
  }
341
429
  const contentParts = this.normalizeContent(message.content);
342
430
  if (contentParts.length === 1 && contentParts[0]?.type === "text") {
431
+ const text = contentParts[0].content;
432
+ if (text.length === 0) {
433
+ throw new Error(
434
+ `User message for ${this.name} has empty text content. Empty user messages would produce a paid request with no input; provide non-empty content or omit the message.`
435
+ );
436
+ }
343
437
  return {
344
438
  role: "user",
345
- content: contentParts[0].content
439
+ content: text
346
440
  };
347
441
  }
348
442
  const parts = [];
349
443
  for (const part of contentParts) {
350
444
  const converted = this.convertContentPart(part);
351
- if (converted) {
352
- parts.push(converted);
445
+ if (!converted) {
446
+ throw new Error(
447
+ `Unsupported content part type for ${this.name}: ${part.type}. Override convertContentPart() in a subclass to handle this type, or remove it from the message.`
448
+ );
353
449
  }
450
+ parts.push(converted);
451
+ }
452
+ if (parts.length === 0) {
453
+ throw new Error(
454
+ `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.`
455
+ );
354
456
  }
355
457
  return {
356
458
  role: "user",
357
- content: parts.length > 0 ? parts : ""
459
+ content: parts
358
460
  };
359
461
  }
360
462
  /**
@@ -368,7 +470,8 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
368
470
  if (part.type === "image") {
369
471
  const imageMetadata = part.metadata;
370
472
  const imageValue = part.source.value;
371
- const imageUrl = part.source.type === "data" && !imageValue.startsWith("data:") ? `data:${part.source.mimeType};base64,${imageValue}` : imageValue;
473
+ const imageMime = part.source.mimeType || "application/octet-stream";
474
+ const imageUrl = part.source.type === "data" && !imageValue.startsWith("data:") ? `data:${imageMime};base64,${imageValue}` : imageValue;
372
475
  return {
373
476
  type: "image_url",
374
477
  image_url: {
@@ -1 +1 @@
1
- {"version":3,"file":"chat-completions-text.js","sources":["../../../src/adapters/chat-completions-text.ts"],"sourcesContent":["import { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { generateId, transformNullsToUndefined } from '@tanstack/ai-utils'\nimport { createOpenAICompatibleClient } from '../utils/client'\nimport { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type OpenAI_SDK from 'openai'\nimport type {\n ContentPart,\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { OpenAICompatibleClientConfig } from '../types/config'\n\n/**\n * OpenAI-compatible Chat Completions Text Adapter\n *\n * A generalized base class for providers that use the OpenAI Chat Completions API\n * (`/v1/chat/completions`). Providers like Grok, Groq, OpenRouter, and others can\n * extend this class and only need to:\n * - Set `baseURL` in the config\n * - Lock the generic type parameters to provider-specific types\n * - Override specific methods for quirks\n *\n * All methods that build requests or process responses are `protected` so subclasses\n * can override them.\n */\nexport class OpenAICompatibleChatCompletionsTextAdapter<\n TModel extends string,\n TProviderOptions extends Record<string, any> = Record<string, any>,\n TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TMessageMetadata extends DefaultMessageMetadataByModality =\n DefaultMessageMetadataByModality,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n TMessageMetadata\n> {\n readonly kind = 'text' as const\n readonly name: string\n\n protected client: OpenAI_SDK\n\n constructor(\n config: OpenAICompatibleClientConfig,\n model: TModel,\n name: string = 'openai-compatible',\n ) {\n super({}, model)\n this.name = name\n this.client = createOpenAICompatibleClient(config)\n }\n\n async *chatStream(\n options: TextOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const requestParams = this.mapOptionsToRequest(options)\n const timestamp = Date.now()\n\n // AG-UI lifecycle tracking (mutable state object for ESLint compatibility)\n const aguiState = {\n runId: generateId(this.name),\n messageId: generateId(this.name),\n timestamp,\n hasEmittedRunStarted: false,\n }\n\n try {\n const stream = await this.client.chat.completions.create(\n {\n ...requestParams,\n stream: true,\n stream_options: { include_usage: true },\n },\n {\n headers: (options.request as RequestInit | undefined)?.headers as\n | Record<string, string>\n | undefined,\n signal: (options.request as RequestInit | undefined)?.signal,\n },\n )\n\n yield* this.processStreamChunks(stream, options, aguiState)\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n\n // Emit RUN_STARTED if not yet emitted\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: 'RUN_STARTED',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n }\n }\n\n // Emit AG-UI RUN_ERROR\n yield {\n type: 'RUN_ERROR',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n error: {\n message: err.message || 'Unknown error',\n code: err.code,\n },\n }\n\n console.error(\n `>>> [${this.name}] chatStream: Fatal error during response creation <<<`,\n )\n console.error('>>> Error message:', err.message)\n console.error('>>> Error stack:', err.stack)\n console.error('>>> Full error:', err)\n }\n }\n\n /**\n * Generate structured output using the provider's JSON Schema response format.\n * Uses stream: false to get the complete response in one call.\n *\n * OpenAI-compatible APIs have strict requirements for structured output:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for all objects\n *\n * The outputSchema is already JSON Schema (converted in the ai layer).\n * We apply provider-specific transformations for structured output compatibility.\n */\n async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required || [],\n )\n\n try {\n // Strip stream_options which is only valid for streaming calls\n const {\n stream_options: _,\n stream: __,\n ...cleanParams\n } = requestParams as any\n const response = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: false,\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n {\n headers: (chatOptions.request as RequestInit | undefined)?.headers as\n | Record<string, string>\n | undefined,\n signal: (chatOptions.request as RequestInit | undefined)?.signal,\n },\n )\n\n // Extract text content from the response\n const rawText = response.choices[0]?.message.content || ''\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n // Transform null values to undefined to match original Zod schema expectations\n // Provider returns null for optional fields we made nullable in the schema\n const transformed = transformNullsToUndefined(parsed)\n\n return {\n data: transformed,\n rawText,\n }\n } catch (error: unknown) {\n const err = error as Error\n console.error(\n `>>> [${this.name}] structuredOutput: Error during response creation <<<`,\n )\n console.error('>>> Error message:', err.message)\n throw error\n }\n }\n\n /**\n * Applies provider-specific transformations for structured output compatibility.\n * Override this in subclasses to handle provider-specific quirks.\n */\n protected makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired: Array<string>,\n ): Record<string, any> {\n return makeStructuredOutputCompatible(schema, originalRequired)\n }\n\n /**\n * Processes streamed chunks from the Chat Completions API and yields AG-UI events.\n * Override this in subclasses to handle provider-specific stream behavior.\n */\n protected async *processStreamChunks(\n stream: AsyncIterable<OpenAI_SDK.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n messageId: string\n timestamp: number\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<StreamChunk> {\n let accumulatedContent = ''\n const timestamp = aguiState.timestamp\n let hasEmittedTextMessageStart = false\n\n // Track tool calls being streamed (arguments come in chunks)\n const toolCallsInProgress = new Map<\n number,\n {\n id: string\n name: string\n arguments: string\n started: boolean // Track if TOOL_CALL_START has been emitted\n }\n >()\n\n try {\n for await (const chunk of stream) {\n const choice = chunk.choices[0]\n\n if (!choice) continue\n\n // Emit RUN_STARTED on first chunk\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield {\n type: 'RUN_STARTED',\n runId: aguiState.runId,\n model: chunk.model || options.model,\n timestamp,\n }\n }\n\n const delta = choice.delta\n const deltaContent = delta.content\n const deltaToolCalls = delta.tool_calls\n\n // Handle content delta\n if (deltaContent) {\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield {\n type: 'TEXT_MESSAGE_START',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n role: 'assistant',\n }\n }\n\n accumulatedContent += deltaContent\n\n // Emit AG-UI TEXT_MESSAGE_CONTENT\n yield {\n type: 'TEXT_MESSAGE_CONTENT',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n delta: deltaContent,\n content: accumulatedContent,\n }\n }\n\n // Handle tool calls - they come in as deltas\n if (deltaToolCalls) {\n for (const toolCallDelta of deltaToolCalls) {\n const index = toolCallDelta.index\n\n // Initialize or update the tool call in progress\n if (!toolCallsInProgress.has(index)) {\n toolCallsInProgress.set(index, {\n id: toolCallDelta.id || '',\n name: toolCallDelta.function?.name || '',\n arguments: '',\n started: false,\n })\n }\n\n const toolCall = toolCallsInProgress.get(index)!\n\n // Update with any new data from the delta\n if (toolCallDelta.id) {\n toolCall.id = toolCallDelta.id\n }\n if (toolCallDelta.function?.name) {\n toolCall.name = toolCallDelta.function.name\n }\n if (toolCallDelta.function?.arguments) {\n toolCall.arguments += toolCallDelta.function.arguments\n }\n\n // Emit TOOL_CALL_START when we have id and name\n if (toolCall.id && toolCall.name && !toolCall.started) {\n toolCall.started = true\n yield {\n type: 'TOOL_CALL_START',\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp,\n index,\n }\n }\n\n // Emit TOOL_CALL_ARGS for argument deltas\n if (toolCallDelta.function?.arguments && toolCall.started) {\n yield {\n type: 'TOOL_CALL_ARGS',\n toolCallId: toolCall.id,\n model: chunk.model || options.model,\n timestamp,\n delta: toolCallDelta.function.arguments,\n }\n }\n }\n }\n\n // Handle finish reason\n if (choice.finish_reason) {\n // Emit all completed tool calls\n if (\n choice.finish_reason === 'tool_calls' ||\n toolCallsInProgress.size > 0\n ) {\n for (const [, toolCall] of toolCallsInProgress) {\n // Parse arguments for TOOL_CALL_END\n let parsedInput: unknown = {}\n try {\n parsedInput = toolCall.arguments\n ? JSON.parse(toolCall.arguments)\n : {}\n } catch {\n parsedInput = {}\n }\n\n // Emit AG-UI TOOL_CALL_END\n yield {\n type: 'TOOL_CALL_END',\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp,\n input: parsedInput,\n }\n }\n }\n\n const computedFinishReason =\n choice.finish_reason === 'tool_calls' ||\n toolCallsInProgress.size > 0\n ? 'tool_calls'\n : 'stop'\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield {\n type: 'TEXT_MESSAGE_END',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n }\n }\n\n // Emit AG-UI RUN_FINISHED\n yield {\n type: 'RUN_FINISHED',\n runId: aguiState.runId,\n model: chunk.model || options.model,\n timestamp,\n usage: chunk.usage\n ? {\n promptTokens: chunk.usage.prompt_tokens || 0,\n completionTokens: chunk.usage.completion_tokens || 0,\n totalTokens: chunk.usage.total_tokens || 0,\n }\n : undefined,\n finishReason: computedFinishReason,\n }\n }\n }\n } catch (error: unknown) {\n const err = error as Error & { code?: string }\n console.error(`[${this.name}] Stream ended with error:`, err.message)\n\n // Emit AG-UI RUN_ERROR\n yield {\n type: 'RUN_ERROR',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n error: {\n message: err.message || 'Unknown error occurred',\n code: err.code,\n },\n }\n }\n }\n\n /**\n * Maps common TextOptions to Chat Completions API request format.\n * Override this in subclasses to add provider-specific options.\n */\n protected mapOptionsToRequest(\n options: TextOptions,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionCreateParamsStreaming {\n const tools = options.tools\n ? convertToolsToChatCompletionsFormat(\n options.tools,\n this.makeStructuredOutputCompatible.bind(this),\n )\n : undefined\n\n // Build messages array with system prompts\n const messages: Array<OpenAI_SDK.Chat.Completions.ChatCompletionMessageParam> =\n []\n\n // Add system prompts first\n if (options.systemPrompts && options.systemPrompts.length > 0) {\n messages.push({\n role: 'system',\n content: options.systemPrompts.join('\\n'),\n })\n }\n\n // Convert messages\n for (const message of options.messages) {\n messages.push(this.convertMessage(message))\n }\n\n const modelOptions = options.modelOptions\n\n return {\n ...modelOptions,\n model: options.model,\n messages,\n temperature: options.temperature,\n max_tokens: options.maxTokens,\n top_p: options.topP,\n tools: tools as Array<OpenAI_SDK.Chat.Completions.ChatCompletionTool>,\n stream: true,\n }\n }\n\n /**\n * Converts a single ModelMessage to the Chat Completions API message format.\n * Override this in subclasses to handle provider-specific message formats.\n */\n protected convertMessage(\n message: ModelMessage,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionMessageParam {\n // Handle tool messages\n if (message.role === 'tool') {\n return {\n role: 'tool',\n tool_call_id: message.toolCallId || '',\n content:\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content),\n }\n }\n\n // Handle assistant messages\n if (message.role === 'assistant') {\n const toolCalls = message.toolCalls?.map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.function.name,\n arguments:\n typeof tc.function.arguments === 'string'\n ? tc.function.arguments\n : JSON.stringify(tc.function.arguments),\n },\n }))\n\n return {\n role: 'assistant',\n content: this.extractTextContent(message.content),\n ...(toolCalls && toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),\n }\n }\n\n // Handle user messages - support multimodal content\n const contentParts = this.normalizeContent(message.content)\n\n // If only text, use simple string format\n if (contentParts.length === 1 && contentParts[0]?.type === 'text') {\n return {\n role: 'user',\n content: contentParts[0].content,\n }\n }\n\n // Otherwise, use array format for multimodal\n const parts: Array<OpenAI_SDK.Chat.Completions.ChatCompletionContentPart> =\n []\n for (const part of contentParts) {\n const converted = this.convertContentPart(part)\n if (converted) {\n parts.push(converted)\n }\n }\n\n return {\n role: 'user',\n content: parts.length > 0 ? parts : '',\n }\n }\n\n /**\n * Converts a single ContentPart to the Chat Completions API content part format.\n * Override this in subclasses to handle additional content types or provider-specific metadata.\n */\n protected convertContentPart(\n part: ContentPart,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionContentPart | null {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n if (part.type === 'image') {\n const imageMetadata = part.metadata as\n | { detail?: 'auto' | 'low' | 'high' }\n | undefined\n\n // For base64 data, construct a data URI using the mimeType from source\n const imageValue = part.source.value\n const imageUrl =\n part.source.type === 'data' && !imageValue.startsWith('data:')\n ? `data:${part.source.mimeType};base64,${imageValue}`\n : imageValue\n\n return {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: imageMetadata?.detail || 'auto',\n },\n }\n }\n\n // Unsupported content type — subclasses can override to handle more types\n return null\n }\n\n /**\n * Normalizes message content to an array of ContentPart.\n * Handles backward compatibility with string content.\n */\n protected normalizeContent(\n content: string | null | Array<ContentPart>,\n ): Array<ContentPart> {\n if (content === null) {\n return []\n }\n if (typeof content === 'string') {\n return [{ type: 'text', content: content }]\n }\n return content\n }\n\n /**\n * Extracts text content from a content value that may be string, null, or ContentPart array.\n */\n protected extractTextContent(\n content: string | null | Array<ContentPart>,\n ): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // It's an array of ContentPart\n return content\n .filter((p) => p.type === 'text')\n .map((p) => p.content)\n .join('')\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"chat-completions-text.js","sources":["../../../src/adapters/chat-completions-text.ts"],"sourcesContent":["import { BaseTextAdapter } from '@tanstack/ai/adapters'\nimport { toRunErrorPayload } from '@tanstack/ai/adapter-internals'\nimport { generateId, transformNullsToUndefined } from '@tanstack/ai-utils'\nimport { createOpenAICompatibleClient } from '../utils/client'\nimport { extractRequestOptions } from '../utils/request-options'\nimport { makeStructuredOutputCompatible } from '../utils/schema-converter'\nimport { convertToolsToChatCompletionsFormat } from './chat-completions-tool-converter'\nimport type {\n StructuredOutputOptions,\n StructuredOutputResult,\n} from '@tanstack/ai/adapters'\nimport type OpenAI_SDK from 'openai'\nimport type {\n ContentPart,\n DefaultMessageMetadataByModality,\n Modality,\n ModelMessage,\n StreamChunk,\n TextOptions,\n} from '@tanstack/ai'\nimport type { OpenAICompatibleClientConfig } from '../types/config'\n\n/** Cast an event object to StreamChunk. Adapters construct events with string\n * literal types which are structurally compatible with the EventType enum. */\nconst asChunk = (chunk: Record<string, unknown>) =>\n chunk as unknown as StreamChunk\n\n/**\n * OpenAI-compatible Chat Completions Text Adapter\n *\n * A generalized base class for providers that use the OpenAI Chat Completions API\n * (`/v1/chat/completions`). Providers like Grok, Groq, OpenRouter, and others can\n * extend this class and only need to:\n * - Set `baseURL` in the config\n * - Lock the generic type parameters to provider-specific types\n * - Override specific methods for quirks\n *\n * All methods that build requests or process responses are `protected` so subclasses\n * can override them.\n */\nexport class OpenAICompatibleChatCompletionsTextAdapter<\n TModel extends string,\n TProviderOptions extends Record<string, any> = Record<string, any>,\n TInputModalities extends ReadonlyArray<Modality> = ReadonlyArray<Modality>,\n TMessageMetadata extends DefaultMessageMetadataByModality =\n DefaultMessageMetadataByModality,\n TToolCapabilities extends ReadonlyArray<string> = ReadonlyArray<string>,\n> extends BaseTextAdapter<\n TModel,\n TProviderOptions,\n TInputModalities,\n TMessageMetadata,\n TToolCapabilities\n> {\n readonly kind = 'text' as const\n readonly name: string\n\n protected client: OpenAI_SDK\n\n constructor(\n config: OpenAICompatibleClientConfig,\n model: TModel,\n name: string = 'openai-compatible',\n ) {\n super({}, model)\n this.name = name\n this.client = createOpenAICompatibleClient(config)\n }\n\n async *chatStream(\n options: TextOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const requestParams = this.mapOptionsToRequest(options)\n const timestamp = Date.now()\n\n // AG-UI lifecycle tracking (mutable state object for ESLint compatibility)\n const aguiState = {\n runId: generateId(this.name),\n messageId: generateId(this.name),\n timestamp,\n hasEmittedRunStarted: false,\n }\n\n try {\n options.logger.request(\n `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,\n { provider: this.name, model: this.model },\n )\n const stream = await this.client.chat.completions.create(\n {\n ...requestParams,\n stream: true,\n stream_options: { include_usage: true },\n },\n extractRequestOptions(options.request),\n )\n\n yield* this.processStreamChunks(stream, options, aguiState)\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.chatStream failed`,\n )\n\n // Emit RUN_STARTED if not yet emitted\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield asChunk({\n type: 'RUN_STARTED',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n })\n }\n\n // Emit AG-UI RUN_ERROR\n yield asChunk({\n type: 'RUN_ERROR',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n error: errorPayload,\n })\n\n options.logger.errors(`${this.name}.chatStream fatal`, {\n error: errorPayload,\n source: `${this.name}.chatStream`,\n })\n }\n }\n\n /**\n * Generate structured output using the provider's JSON Schema response format.\n * Uses stream: false to get the complete response in one call.\n *\n * OpenAI-compatible APIs have strict requirements for structured output:\n * - All properties must be in the `required` array\n * - Optional fields should have null added to their type union\n * - additionalProperties must be false for all objects\n *\n * The outputSchema is already JSON Schema (converted in the ai layer).\n * We apply provider-specific transformations for structured output compatibility.\n */\n async structuredOutput(\n options: StructuredOutputOptions<TProviderOptions>,\n ): Promise<StructuredOutputResult<unknown>> {\n const { chatOptions, outputSchema } = options\n const requestParams = this.mapOptionsToRequest(chatOptions)\n\n const jsonSchema = this.makeStructuredOutputCompatible(\n outputSchema,\n outputSchema.required,\n )\n\n try {\n // Strip stream_options which is only valid for streaming calls\n const {\n stream_options: _,\n stream: __,\n ...cleanParams\n } = requestParams as any\n chatOptions.logger.request(\n `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,\n { provider: this.name, model: this.model },\n )\n const response = await this.client.chat.completions.create(\n {\n ...cleanParams,\n stream: false,\n response_format: {\n type: 'json_schema',\n json_schema: {\n name: 'structured_output',\n schema: jsonSchema,\n strict: true,\n },\n },\n },\n extractRequestOptions(chatOptions.request),\n )\n\n // Extract text content from the response\n const rawText = response.choices[0]?.message.content || ''\n\n // Parse the JSON response\n let parsed: unknown\n try {\n parsed = JSON.parse(rawText)\n } catch {\n throw new Error(\n `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? '...' : ''}`,\n )\n }\n\n // Transform null values to undefined to match original Zod schema expectations\n // Provider returns null for optional fields we made nullable in the schema\n const transformed = transformNullsToUndefined(parsed)\n\n return {\n data: transformed,\n rawText,\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {\n error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),\n source: `${this.name}.structuredOutput`,\n })\n throw error\n }\n }\n\n /**\n * Applies provider-specific transformations for structured output compatibility.\n * Override this in subclasses to handle provider-specific quirks.\n */\n protected makeStructuredOutputCompatible(\n schema: Record<string, any>,\n originalRequired?: Array<string>,\n ): Record<string, any> {\n return makeStructuredOutputCompatible(schema, originalRequired)\n }\n\n /**\n * Processes streamed chunks from the Chat Completions API and yields AG-UI events.\n * Override this in subclasses to handle provider-specific stream behavior.\n */\n protected async *processStreamChunks(\n stream: AsyncIterable<OpenAI_SDK.Chat.Completions.ChatCompletionChunk>,\n options: TextOptions,\n aguiState: {\n runId: string\n messageId: string\n timestamp: number\n hasEmittedRunStarted: boolean\n },\n ): AsyncIterable<StreamChunk> {\n let accumulatedContent = ''\n const timestamp = aguiState.timestamp\n let hasEmittedTextMessageStart = false\n let lastModel: string | undefined\n // Track usage from any chunk that carries it. With\n // `stream_options: { include_usage: true }` OpenAI emits a terminal chunk\n // whose `choices` is `[]` and only the `usage` field is populated; the\n // earlier `finish_reason` chunk does NOT include token counts. We must\n // therefore defer RUN_FINISHED until the iterator is exhausted so we can\n // pick up usage from the trailing chunk regardless of arrival order.\n let lastUsage:\n | OpenAI_SDK.Chat.Completions.ChatCompletionChunk['usage']\n | undefined\n let pendingFinishReason:\n | OpenAI_SDK.Chat.Completions.ChatCompletionChunk.Choice['finish_reason']\n | undefined\n\n // Track tool calls being streamed (arguments come in chunks)\n const toolCallsInProgress = new Map<\n number,\n {\n id: string\n name: string\n arguments: string\n started: boolean // Track if TOOL_CALL_START has been emitted\n }\n >()\n // Track whether ANY tool call lifecycle was actually completed across the\n // entire stream. Lets us downgrade a `tool_calls` finish_reason to `stop`\n // when the upstream signalled tool calls but never produced a complete\n // start/end pair — emitting RUN_FINISHED { finishReason: 'tool_calls' }\n // with no matching TOOL_CALL_END would leave consumers waiting for tool\n // results that never arrive.\n let emittedAnyToolCallEnd = false\n\n try {\n for await (const chunk of stream) {\n const choiceForLog = chunk.choices[0]\n options.logger.provider(\n `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? 'none'} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`,\n { provider: this.name, model: chunk.model },\n )\n\n // Capture usage from any chunk (including the terminal usage-only\n // chunk emitted when `stream_options.include_usage` is on).\n if (chunk.usage) {\n lastUsage = chunk.usage\n }\n if (chunk.model) {\n lastModel = chunk.model\n }\n\n // Emit RUN_STARTED on the first chunk of any kind so callers see a\n // run lifecycle even on streams that arrive entirely as usage-only\n // (no choices). Without this, a usage-first stream would skip\n // RUN_STARTED via `if (!choice) continue` below and the post-loop\n // synthetic block would also skip RUN_FINISHED (it gates on\n // `hasEmittedRunStarted`).\n if (!aguiState.hasEmittedRunStarted) {\n aguiState.hasEmittedRunStarted = true\n yield asChunk({\n type: 'RUN_STARTED',\n runId: aguiState.runId,\n model: chunk.model || options.model,\n timestamp,\n })\n }\n\n const choice = chunk.choices[0]\n\n if (!choice) continue\n\n const delta = choice.delta\n const deltaContent = delta.content\n const deltaToolCalls = delta.tool_calls\n\n // Handle content delta\n if (deltaContent) {\n // Emit TEXT_MESSAGE_START on first text content\n if (!hasEmittedTextMessageStart) {\n hasEmittedTextMessageStart = true\n yield asChunk({\n type: 'TEXT_MESSAGE_START',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n role: 'assistant',\n })\n }\n\n accumulatedContent += deltaContent\n\n // Emit AG-UI TEXT_MESSAGE_CONTENT\n yield asChunk({\n type: 'TEXT_MESSAGE_CONTENT',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n delta: deltaContent,\n content: accumulatedContent,\n })\n }\n\n // Handle tool calls - they come in as deltas\n if (deltaToolCalls) {\n for (const toolCallDelta of deltaToolCalls) {\n const index = toolCallDelta.index\n\n // Initialize or update the tool call in progress\n if (!toolCallsInProgress.has(index)) {\n toolCallsInProgress.set(index, {\n id: toolCallDelta.id || '',\n name: toolCallDelta.function?.name || '',\n arguments: '',\n started: false,\n })\n }\n\n const toolCall = toolCallsInProgress.get(index)!\n\n // Update with any new data from the delta\n if (toolCallDelta.id) {\n toolCall.id = toolCallDelta.id\n }\n if (toolCallDelta.function?.name) {\n toolCall.name = toolCallDelta.function.name\n }\n if (toolCallDelta.function?.arguments) {\n toolCall.arguments += toolCallDelta.function.arguments\n }\n\n // Emit TOOL_CALL_START when we have id and name\n if (toolCall.id && toolCall.name && !toolCall.started) {\n toolCall.started = true\n yield asChunk({\n type: 'TOOL_CALL_START',\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp,\n index,\n })\n }\n\n // Emit TOOL_CALL_ARGS for argument deltas\n if (toolCallDelta.function?.arguments && toolCall.started) {\n yield asChunk({\n type: 'TOOL_CALL_ARGS',\n toolCallId: toolCall.id,\n model: chunk.model || options.model,\n timestamp,\n delta: toolCallDelta.function.arguments,\n })\n }\n }\n }\n\n // Handle finish reason. We DO emit TOOL_CALL_END and TEXT_MESSAGE_END\n // here because the corresponding _START events have already fired,\n // and tool execution downstream wants to begin as soon as possible.\n // RUN_FINISHED is deferred until the iterator is fully exhausted so\n // we can capture the trailing usage chunk that arrives AFTER this\n // chunk when stream_options.include_usage is on.\n if (choice.finish_reason) {\n if (\n choice.finish_reason === 'tool_calls' ||\n toolCallsInProgress.size > 0\n ) {\n for (const [, toolCall] of toolCallsInProgress) {\n // Skip tool calls that never emitted TOOL_CALL_START — emitting\n // a stray TOOL_CALL_END here would violate AG-UI lifecycle\n // (END without matching START) for partial deltas where the\n // upstream never sent both id and name.\n if (!toolCall.started) continue\n\n // Parse arguments for TOOL_CALL_END. Surface parse failures via\n // the logger so a model emitting malformed JSON for tool args\n // is debuggable instead of silently invoking the tool with {}.\n // Non-object JSON (e.g. a bare string or number) is also coerced\n // to {} so downstream tool execution doesn't receive a primitive\n // input, mirroring the Responses adapter's guard.\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput =\n parsed && typeof parsed === 'object' ? parsed : {}\n } catch (parseError) {\n options.logger.errors(\n `${this.name}.processStreamChunks tool-args JSON parse failed`,\n {\n error: toRunErrorPayload(\n parseError,\n `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`,\n ),\n source: `${this.name}.processStreamChunks`,\n toolCallId: toolCall.id,\n toolName: toolCall.name,\n rawArguments: toolCall.arguments,\n },\n )\n parsedInput = {}\n }\n }\n\n // Emit AG-UI TOOL_CALL_END\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: chunk.model || options.model,\n timestamp,\n input: parsedInput,\n })\n emittedAnyToolCallEnd = true\n }\n // Clear tool-call state after emission so a subsequent\n // `finish_reason: 'stop'` chunk (or the post-loop synthetic\n // block) doesn't see lingering entries and misreport the finish.\n toolCallsInProgress.clear()\n }\n\n // Emit TEXT_MESSAGE_END if we had text content\n if (hasEmittedTextMessageStart) {\n yield asChunk({\n type: 'TEXT_MESSAGE_END',\n messageId: aguiState.messageId,\n model: chunk.model || options.model,\n timestamp,\n })\n hasEmittedTextMessageStart = false\n }\n\n // Remember the upstream finish_reason; RUN_FINISHED is emitted at\n // end-of-stream so we pick up the trailing usage-only chunk too.\n pendingFinishReason = choice.finish_reason\n }\n }\n\n // Emit a single terminal RUN_FINISHED after the iterator is exhausted.\n // This both delivers accurate token counts (the trailing usage chunk\n // may arrive AFTER the finish_reason chunk) and gives consumers a\n // guaranteed terminal event even when the upstream cuts off mid-stream\n // (no finish_reason chunk ever arrives).\n if (aguiState.hasEmittedRunStarted) {\n // Close any started tool calls that never got finish_reason. A\n // truncated stream that emitted TOOL_CALL_START but never reached\n // finish_reason would otherwise leave consumers with an unbalanced\n // start. Skip non-started entries (no matching START to close).\n let pendingToolCount = 0\n for (const [, toolCall] of toolCallsInProgress) {\n if (!toolCall.started) continue\n let parsedInput: unknown = {}\n if (toolCall.arguments) {\n try {\n const parsed: unknown = JSON.parse(toolCall.arguments)\n parsedInput = parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n parsedInput = {}\n }\n }\n yield asChunk({\n type: 'TOOL_CALL_END',\n toolCallId: toolCall.id,\n toolCallName: toolCall.name,\n toolName: toolCall.name,\n model: lastModel || options.model,\n timestamp,\n input: parsedInput,\n })\n pendingToolCount += 1\n emittedAnyToolCallEnd = true\n }\n toolCallsInProgress.clear()\n\n // Make sure the text message lifecycle is closed even on early\n // termination paths where finish_reason never arrives.\n if (hasEmittedTextMessageStart) {\n yield asChunk({\n type: 'TEXT_MESSAGE_END',\n messageId: aguiState.messageId,\n model: lastModel || options.model,\n timestamp,\n })\n }\n\n // Map upstream finish_reason to AG-UI's narrower vocabulary while\n // preserving the upstream value when it falls outside the AG-UI set.\n // Collapsing length / content_filter to 'stop' would hide why the\n // run terminated — surface it instead. Use `tool_calls` only when\n // a TOOL_CALL_END was actually emitted: an upstream that signalled\n // `tool_calls` but never produced a started/ended pair must NOT\n // surface `tool_calls` here, since downstream consumers wait for\n // tool results that would never arrive.\n const finishReason: string = emittedAnyToolCallEnd\n ? 'tool_calls'\n : pendingFinishReason === 'tool_calls'\n ? 'stop'\n : (pendingFinishReason ?? 'stop')\n\n yield asChunk({\n type: 'RUN_FINISHED',\n runId: aguiState.runId,\n model: lastModel || options.model,\n timestamp,\n usage: lastUsage\n ? {\n promptTokens: lastUsage.prompt_tokens || 0,\n completionTokens: lastUsage.completion_tokens || 0,\n totalTokens: lastUsage.total_tokens || 0,\n }\n : undefined,\n finishReason,\n })\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n const errorPayload = toRunErrorPayload(\n error,\n `${this.name}.processStreamChunks failed`,\n )\n options.logger.errors(`${this.name}.processStreamChunks fatal`, {\n error: errorPayload,\n source: `${this.name}.processStreamChunks`,\n })\n\n // Emit AG-UI RUN_ERROR\n yield asChunk({\n type: 'RUN_ERROR',\n runId: aguiState.runId,\n model: options.model,\n timestamp,\n error: errorPayload,\n })\n }\n }\n\n /**\n * Maps common TextOptions to Chat Completions API request format.\n * Override this in subclasses to add provider-specific options.\n */\n protected mapOptionsToRequest(\n options: TextOptions,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionCreateParamsStreaming {\n const tools = options.tools\n ? convertToolsToChatCompletionsFormat(\n options.tools,\n this.makeStructuredOutputCompatible.bind(this),\n )\n : undefined\n\n // Build messages array with system prompts\n const messages: Array<OpenAI_SDK.Chat.Completions.ChatCompletionMessageParam> =\n []\n\n // Add system prompts first\n if (options.systemPrompts && options.systemPrompts.length > 0) {\n messages.push({\n role: 'system',\n content: options.systemPrompts.join('\\n'),\n })\n }\n\n // Convert messages\n for (const message of options.messages) {\n messages.push(this.convertMessage(message))\n }\n\n const modelOptions = options.modelOptions\n\n // Build the request so explicit top-level options win over modelOptions\n // when set, but `undefined` top-level options do NOT clobber values the\n // caller put in modelOptions. Keeping the merge nullish-aware fixes the\n // silent regression where a `modelOptions: { temperature: 0.7 }` setting\n // was overwritten with `temperature: undefined`.\n return {\n ...modelOptions,\n model: options.model,\n messages,\n ...(options.temperature !== undefined && {\n temperature: options.temperature,\n }),\n ...(options.maxTokens !== undefined && {\n max_tokens: options.maxTokens,\n }),\n ...(options.topP !== undefined && { top_p: options.topP }),\n // Conditional spread: `tools: undefined` would clobber any\n // modelOptions.tools the caller set above.\n ...(tools &&\n tools.length > 0 && {\n tools,\n }),\n stream: true,\n }\n }\n\n /**\n * Converts a single ModelMessage to the Chat Completions API message format.\n * Override this in subclasses to handle provider-specific message formats.\n */\n protected convertMessage(\n message: ModelMessage,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionMessageParam {\n // Handle tool messages\n if (message.role === 'tool') {\n return {\n role: 'tool',\n tool_call_id: message.toolCallId || '',\n content:\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content),\n }\n }\n\n // Handle assistant messages\n if (message.role === 'assistant') {\n const toolCalls = message.toolCalls?.map((tc) => ({\n id: tc.id,\n type: 'function' as const,\n function: {\n name: tc.function.name,\n arguments:\n typeof tc.function.arguments === 'string'\n ? tc.function.arguments\n : JSON.stringify(tc.function.arguments),\n },\n }))\n const hasToolCalls = !!toolCalls && toolCalls.length > 0\n const textContent = this.extractTextContent(message.content)\n\n // Per the OpenAI Chat Completions contract, an assistant message that\n // only carries tool_calls should have `content: null` (or omit content)\n // rather than `content: ''`. Empty-string content interacts oddly with\n // tokenization on some backends; null is the documented shape.\n return {\n role: 'assistant',\n content: hasToolCalls && !textContent ? null : textContent,\n ...(hasToolCalls ? { tool_calls: toolCalls } : {}),\n }\n }\n\n // Handle user messages - support multimodal content\n const contentParts = this.normalizeContent(message.content)\n\n // If only text, use simple string format\n if (contentParts.length === 1 && contentParts[0]?.type === 'text') {\n const text = contentParts[0].content\n if (text.length === 0) {\n // Single empty text part is the same fail-loud condition as below —\n // an empty paid request mask a real intent (caller passed `null`/'',\n // or an upstream step normalised everything to an empty string).\n throw new Error(\n `User message for ${this.name} has empty text content. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide non-empty content or omit the message.`,\n )\n }\n return {\n role: 'user',\n content: text,\n }\n }\n\n // Otherwise, use array format for multimodal. Fail fast on unsupported\n // content parts rather than silently dropping them — a message of all\n // unsupported parts would otherwise turn into an empty user prompt and\n // mask a real capability mismatch.\n const parts: Array<OpenAI_SDK.Chat.Completions.ChatCompletionContentPart> =\n []\n for (const part of contentParts) {\n const converted = this.convertContentPart(part)\n if (!converted) {\n throw new Error(\n `Unsupported content part type for ${this.name}: ${part.type}. ` +\n `Override convertContentPart() in a subclass to handle this type, ` +\n `or remove it from the message.`,\n )\n }\n parts.push(converted)\n }\n\n if (parts.length === 0) {\n // The original message had no content parts at all (e.g. content was\n // explicitly null or []). Sending an empty user message to OpenAI\n // produces a paid request with no signal — fail loud instead.\n throw new Error(\n `User message for ${this.name} has no content parts. ` +\n `Empty user messages would produce a paid request with no input; ` +\n `provide at least one text/image/audio part or omit the message.`,\n )\n }\n\n return {\n role: 'user',\n content: parts,\n }\n }\n\n /**\n * Converts a single ContentPart to the Chat Completions API content part format.\n * Override this in subclasses to handle additional content types or provider-specific metadata.\n */\n protected convertContentPart(\n part: ContentPart,\n ): OpenAI_SDK.Chat.Completions.ChatCompletionContentPart | null {\n if (part.type === 'text') {\n return { type: 'text', text: part.content }\n }\n\n if (part.type === 'image') {\n const imageMetadata = part.metadata as\n | { detail?: 'auto' | 'low' | 'high' }\n | undefined\n\n // For base64 data, construct a data URI using the mimeType from source.\n // Default to a generic octet-stream MIME if the source didn't provide\n // one — interpolating `undefined` into the URI (\"data:undefined;base64,\n // ...\") would produce an invalid URI the API rejects.\n const imageValue = part.source.value\n const imageMime = part.source.mimeType || 'application/octet-stream'\n const imageUrl =\n part.source.type === 'data' && !imageValue.startsWith('data:')\n ? `data:${imageMime};base64,${imageValue}`\n : imageValue\n\n return {\n type: 'image_url',\n image_url: {\n url: imageUrl,\n detail: imageMetadata?.detail || 'auto',\n },\n }\n }\n\n // Unsupported content type — subclasses can override to handle more types\n return null\n }\n\n /**\n * Normalizes message content to an array of ContentPart.\n * Handles backward compatibility with string content.\n */\n protected normalizeContent(\n content: string | null | Array<ContentPart>,\n ): Array<ContentPart> {\n if (content === null) {\n return []\n }\n if (typeof content === 'string') {\n return [{ type: 'text', content: content }]\n }\n return content\n }\n\n /**\n * Extracts text content from a content value that may be string, null, or ContentPart array.\n */\n protected extractTextContent(\n content: string | null | Array<ContentPart>,\n ): string {\n if (content === null) {\n return ''\n }\n if (typeof content === 'string') {\n return content\n }\n // It's an array of ContentPart\n return content\n .filter((p) => p.type === 'text')\n .map((p) => p.content)\n .join('')\n }\n}\n"],"names":[],"mappings":"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"}