@tanstack/openai-base 0.9.3 → 0.9.5

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 (66) hide show
  1. package/dist/esm/adapters/chat-completions-text.d.ts +116 -0
  2. package/dist/esm/adapters/chat-completions-text.js +967 -0
  3. package/dist/esm/adapters/chat-completions-text.js.map +1 -0
  4. package/dist/esm/adapters/chat-completions-tool-converter.d.ts +35 -0
  5. package/dist/esm/adapters/chat-completions-tool-converter.js +42 -0
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -0
  7. package/dist/esm/adapters/responses-text.d.ts +135 -0
  8. package/dist/esm/adapters/responses-text.js +1324 -0
  9. package/dist/esm/adapters/responses-text.js.map +1 -0
  10. package/dist/esm/adapters/responses-tool-converter.d.ts +42 -0
  11. package/dist/esm/adapters/responses-tool-converter.js +38 -0
  12. package/dist/esm/adapters/responses-tool-converter.js.map +1 -0
  13. package/dist/esm/index.d.ts +7 -0
  14. package/dist/esm/index.js +59 -0
  15. package/dist/esm/index.js.map +1 -0
  16. package/dist/esm/tools/apply-patch-tool.d.ts +16 -0
  17. package/dist/esm/tools/apply-patch-tool.js +17 -0
  18. package/dist/esm/tools/apply-patch-tool.js.map +1 -0
  19. package/dist/esm/tools/code-interpreter-tool.d.ts +17 -0
  20. package/dist/esm/tools/code-interpreter-tool.js +22 -0
  21. package/dist/esm/tools/code-interpreter-tool.js.map +1 -0
  22. package/dist/esm/tools/computer-use-tool.d.ts +16 -0
  23. package/dist/esm/tools/computer-use-tool.js +23 -0
  24. package/dist/esm/tools/computer-use-tool.js.map +1 -0
  25. package/dist/esm/tools/custom-tool.d.ts +13 -0
  26. package/dist/esm/tools/custom-tool.js +25 -0
  27. package/dist/esm/tools/custom-tool.js.map +1 -0
  28. package/dist/esm/tools/file-search-tool.d.ts +18 -0
  29. package/dist/esm/tools/file-search-tool.js +35 -0
  30. package/dist/esm/tools/file-search-tool.js.map +1 -0
  31. package/dist/esm/tools/function-tool.d.ts +23 -0
  32. package/dist/esm/tools/function-tool.js +33 -0
  33. package/dist/esm/tools/function-tool.js.map +1 -0
  34. package/dist/esm/tools/image-generation-tool.d.ts +23 -0
  35. package/dist/esm/tools/image-generation-tool.js +28 -0
  36. package/dist/esm/tools/image-generation-tool.js.map +1 -0
  37. package/dist/esm/tools/index.d.ts +27 -0
  38. package/dist/esm/tools/local-shell-tool.d.ts +17 -0
  39. package/dist/esm/tools/local-shell-tool.js +17 -0
  40. package/dist/esm/tools/local-shell-tool.js.map +1 -0
  41. package/dist/esm/tools/mcp-tool.d.ts +18 -0
  42. package/dist/esm/tools/mcp-tool.js +31 -0
  43. package/dist/esm/tools/mcp-tool.js.map +1 -0
  44. package/dist/esm/tools/shell-tool.d.ts +26 -0
  45. package/dist/esm/tools/shell-tool.js +25 -0
  46. package/dist/esm/tools/shell-tool.js.map +1 -0
  47. package/dist/esm/tools/tool-choice.d.ts +17 -0
  48. package/dist/esm/tools/tool-converter.d.ts +6 -0
  49. package/dist/esm/tools/tool-converter.js +61 -0
  50. package/dist/esm/tools/tool-converter.js.map +1 -0
  51. package/dist/esm/tools/web-search-preview-tool.d.ts +19 -0
  52. package/dist/esm/tools/web-search-preview-tool.js +19 -0
  53. package/dist/esm/tools/web-search-preview-tool.js.map +1 -0
  54. package/dist/esm/tools/web-search-tool.d.ts +20 -0
  55. package/dist/esm/tools/web-search-tool.js +19 -0
  56. package/dist/esm/tools/web-search-tool.js.map +1 -0
  57. package/dist/esm/usage.d.ts +34 -0
  58. package/dist/esm/usage.js +83 -0
  59. package/dist/esm/usage.js.map +1 -0
  60. package/dist/esm/utils/request-options.d.ts +14 -0
  61. package/dist/esm/utils/request-options.js +11 -0
  62. package/dist/esm/utils/request-options.js.map +1 -0
  63. package/dist/esm/utils/schema-converter.d.ts +39 -0
  64. package/dist/esm/utils/schema-converter.js +145 -0
  65. package/dist/esm/utils/schema-converter.js.map +1 -0
  66. package/package.json +4 -4
@@ -0,0 +1,967 @@
1
+ import { EventType, normalizeSystemPrompts } from "@tanstack/ai";
2
+ import { BaseTextAdapter } from "@tanstack/ai/adapters";
3
+ import { toRunErrorPayload, toRunErrorRawEvent } from "@tanstack/ai/adapter-internals";
4
+ import { generateId } from "@tanstack/ai-utils";
5
+ import { extractRequestOptions } from "../utils/request-options.js";
6
+ import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
7
+ import { buildChatCompletionsUsage } from "../usage.js";
8
+ import { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
9
+ class OpenAIBaseChatCompletionsTextAdapter extends BaseTextAdapter {
10
+ kind = "text";
11
+ name;
12
+ client;
13
+ constructor(model, name, client) {
14
+ super({}, model);
15
+ this.name = name;
16
+ this.client = client;
17
+ }
18
+ async *chatStream(options) {
19
+ const aguiState = {
20
+ runId: generateId(this.name),
21
+ threadId: options.threadId ?? generateId(this.name),
22
+ messageId: generateId(this.name),
23
+ hasEmittedRunStarted: false
24
+ };
25
+ try {
26
+ const requestParams = this.mapOptionsToRequest(options);
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
+ );
31
+ const stream = await this.client.chat.completions.create(
32
+ {
33
+ ...requestParams,
34
+ stream: true,
35
+ stream_options: { include_usage: true }
36
+ },
37
+ extractRequestOptions(options.request)
38
+ );
39
+ yield* this.processStreamChunks(stream, options, aguiState);
40
+ } catch (error) {
41
+ const errorPayload = toRunErrorPayload(
42
+ error,
43
+ `${this.name}.chatStream failed`
44
+ );
45
+ const rawEvent = toRunErrorRawEvent(error);
46
+ if (!aguiState.hasEmittedRunStarted) {
47
+ aguiState.hasEmittedRunStarted = true;
48
+ yield {
49
+ type: EventType.RUN_STARTED,
50
+ runId: aguiState.runId,
51
+ threadId: aguiState.threadId,
52
+ model: options.model,
53
+ timestamp: Date.now(),
54
+ parentRunId: options.parentRunId
55
+ };
56
+ }
57
+ yield {
58
+ type: EventType.RUN_ERROR,
59
+ model: options.model,
60
+ timestamp: Date.now(),
61
+ message: errorPayload.message,
62
+ code: errorPayload.code,
63
+ // Forward the provider's structured error body so consumers can recover
64
+ // the upstream detail the `{ message, code }` payload drops. Omitted
65
+ // when the error carried no provider body (see toRunErrorRawEvent).
66
+ ...rawEvent !== void 0 && { rawEvent },
67
+ error: {
68
+ message: errorPayload.message,
69
+ code: errorPayload.code
70
+ }
71
+ };
72
+ options.logger.errors(`${this.name}.chatStream fatal`, {
73
+ error: errorPayload,
74
+ source: `${this.name}.chatStream`
75
+ });
76
+ }
77
+ }
78
+ /**
79
+ * Generate structured output using the provider's JSON Schema response format.
80
+ * Uses stream: false to get the complete response in one call.
81
+ *
82
+ * OpenAI-compatible APIs have strict requirements for structured output:
83
+ * - All properties must be in the `required` array
84
+ * - Optional fields should have null added to their type union
85
+ * - additionalProperties must be false for all objects
86
+ *
87
+ * The outputSchema is already JSON Schema (converted in the ai layer).
88
+ * We apply provider-specific transformations for structured output compatibility.
89
+ */
90
+ async structuredOutput(options) {
91
+ const { chatOptions, outputSchema } = options;
92
+ const requestParams = this.mapOptionsToRequest(chatOptions);
93
+ const jsonSchema = this.makeStructuredOutputCompatible(
94
+ outputSchema,
95
+ outputSchema.required
96
+ );
97
+ try {
98
+ const {
99
+ stream_options: _,
100
+ stream: __,
101
+ ...cleanParams
102
+ } = requestParams;
103
+ chatOptions.logger.request(
104
+ `activity=structuredOutput provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
105
+ { provider: this.name, model: this.model }
106
+ );
107
+ const response = await this.client.chat.completions.create(
108
+ {
109
+ ...cleanParams,
110
+ stream: false,
111
+ response_format: {
112
+ type: "json_schema",
113
+ json_schema: {
114
+ name: "structured_output",
115
+ schema: jsonSchema,
116
+ strict: true
117
+ }
118
+ }
119
+ },
120
+ extractRequestOptions(chatOptions.request)
121
+ );
122
+ const rawText = response.choices[0]?.message.content;
123
+ if (typeof rawText !== "string" || rawText.length === 0) {
124
+ throw new Error(
125
+ `${this.name}.structuredOutput: response contained no content`
126
+ );
127
+ }
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(rawText);
131
+ } catch {
132
+ throw new Error(
133
+ `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
134
+ );
135
+ }
136
+ const transformed = this.transformStructuredOutput(parsed);
137
+ return {
138
+ data: transformed,
139
+ rawText
140
+ };
141
+ } catch (error) {
142
+ chatOptions.logger.errors(`${this.name}.structuredOutput fatal`, {
143
+ error: toRunErrorPayload(error, `${this.name}.structuredOutput failed`),
144
+ source: `${this.name}.structuredOutput`
145
+ });
146
+ throw error;
147
+ }
148
+ }
149
+ /**
150
+ * Stream structured output. Single Chat Completions request with
151
+ * `response_format: json_schema` + `stream: true`. Emits the standard
152
+ * AG-UI lifecycle (`RUN_STARTED` → `REASONING_*?` → `TEXT_MESSAGE_*`
153
+ * carrying raw JSON deltas → terminal `CUSTOM 'structured-output.complete'`
154
+ * → `RUN_FINISHED`). Subclasses use the same SDK-call / reasoning /
155
+ * structured-output-transform hooks as `chatStream` / `structuredOutput` —
156
+ * no per-subclass override should be needed.
157
+ */
158
+ async *structuredOutputStream(options) {
159
+ const { chatOptions, outputSchema } = options;
160
+ const requestParams = this.mapOptionsToRequest(chatOptions);
161
+ const jsonSchema = this.makeStructuredOutputCompatible(
162
+ outputSchema,
163
+ outputSchema.required
164
+ );
165
+ const timestamp = Date.now();
166
+ const aguiState = {
167
+ runId: generateId(this.name),
168
+ threadId: chatOptions.threadId ?? generateId(this.name),
169
+ messageId: generateId(this.name),
170
+ timestamp,
171
+ hasEmittedRunStarted: false
172
+ };
173
+ let accumulatedContent = "";
174
+ let accumulatedReasoning = "";
175
+ let hasEmittedTextMessageStart = false;
176
+ let reasoningMessageId;
177
+ let hasClosedReasoning = false;
178
+ let stepId;
179
+ let lastModel;
180
+ let lastUsage;
181
+ const closeReasoningLifecycle = (function* () {
182
+ if (reasoningMessageId && !hasClosedReasoning) {
183
+ hasClosedReasoning = true;
184
+ yield {
185
+ type: EventType.REASONING_MESSAGE_END,
186
+ messageId: reasoningMessageId,
187
+ model: lastModel || chatOptions.model,
188
+ timestamp
189
+ };
190
+ yield {
191
+ type: EventType.REASONING_END,
192
+ messageId: reasoningMessageId,
193
+ model: lastModel || chatOptions.model,
194
+ timestamp
195
+ };
196
+ if (stepId) {
197
+ yield {
198
+ type: EventType.STEP_FINISHED,
199
+ stepName: stepId,
200
+ stepId,
201
+ model: lastModel || chatOptions.model,
202
+ timestamp,
203
+ content: accumulatedReasoning
204
+ };
205
+ }
206
+ }
207
+ }).bind(this);
208
+ try {
209
+ const {
210
+ stream_options: _so,
211
+ stream: _s,
212
+ tools: _t,
213
+ ...cleanParams
214
+ } = requestParams;
215
+ chatOptions.logger.request(
216
+ `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
217
+ { provider: this.name, model: this.model }
218
+ );
219
+ const stream = await this.client.chat.completions.create(
220
+ {
221
+ ...cleanParams,
222
+ stream: true,
223
+ stream_options: { include_usage: true },
224
+ response_format: {
225
+ type: "json_schema",
226
+ json_schema: {
227
+ name: "structured_output",
228
+ schema: jsonSchema,
229
+ strict: true
230
+ }
231
+ }
232
+ },
233
+ extractRequestOptions(chatOptions.request)
234
+ );
235
+ for await (const chunk of stream) {
236
+ const choiceForLog = chunk.choices[0];
237
+ chatOptions.logger.provider(
238
+ `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasUsage=${!!chunk.usage}`,
239
+ { provider: this.name, model: chunk.model }
240
+ );
241
+ if (chunk.model) lastModel = chunk.model;
242
+ const usage = chunk.usage ?? chunk.x_groq?.usage;
243
+ if (usage) lastUsage = usage;
244
+ if (!aguiState.hasEmittedRunStarted) {
245
+ aguiState.hasEmittedRunStarted = true;
246
+ yield {
247
+ type: EventType.RUN_STARTED,
248
+ runId: aguiState.runId,
249
+ threadId: aguiState.threadId,
250
+ model: chunk.model || chatOptions.model,
251
+ timestamp,
252
+ parentRunId: chatOptions.parentRunId
253
+ };
254
+ }
255
+ const reasoning = this.extractReasoning(chunk);
256
+ if (reasoning && reasoning.text) {
257
+ if (!reasoningMessageId) {
258
+ reasoningMessageId = generateId(this.name);
259
+ stepId = generateId(this.name);
260
+ yield {
261
+ type: EventType.REASONING_START,
262
+ messageId: reasoningMessageId,
263
+ model: chunk.model || chatOptions.model,
264
+ timestamp
265
+ };
266
+ yield {
267
+ type: EventType.REASONING_MESSAGE_START,
268
+ messageId: reasoningMessageId,
269
+ role: "reasoning",
270
+ model: chunk.model || chatOptions.model,
271
+ timestamp
272
+ };
273
+ yield {
274
+ type: EventType.STEP_STARTED,
275
+ stepName: stepId,
276
+ stepId,
277
+ model: chunk.model || chatOptions.model,
278
+ timestamp,
279
+ stepType: "thinking"
280
+ };
281
+ }
282
+ accumulatedReasoning += reasoning.text;
283
+ yield {
284
+ type: EventType.REASONING_MESSAGE_CONTENT,
285
+ messageId: reasoningMessageId,
286
+ delta: reasoning.text,
287
+ model: chunk.model || chatOptions.model,
288
+ timestamp
289
+ };
290
+ }
291
+ const choice = chunk.choices[0];
292
+ if (!choice) continue;
293
+ const deltaContent = choice.delta.content;
294
+ if (deltaContent) {
295
+ yield* closeReasoningLifecycle();
296
+ if (!hasEmittedTextMessageStart) {
297
+ hasEmittedTextMessageStart = true;
298
+ yield {
299
+ type: EventType.TEXT_MESSAGE_START,
300
+ messageId: aguiState.messageId,
301
+ model: chunk.model || chatOptions.model,
302
+ timestamp,
303
+ role: "assistant"
304
+ };
305
+ }
306
+ accumulatedContent += deltaContent;
307
+ yield {
308
+ type: EventType.TEXT_MESSAGE_CONTENT,
309
+ messageId: aguiState.messageId,
310
+ model: chunk.model || chatOptions.model,
311
+ timestamp,
312
+ delta: deltaContent,
313
+ content: accumulatedContent
314
+ };
315
+ }
316
+ }
317
+ yield* closeReasoningLifecycle();
318
+ if (hasEmittedTextMessageStart) {
319
+ yield {
320
+ type: EventType.TEXT_MESSAGE_END,
321
+ messageId: aguiState.messageId,
322
+ model: lastModel || chatOptions.model,
323
+ timestamp
324
+ };
325
+ }
326
+ if (accumulatedContent.length === 0) {
327
+ yield {
328
+ type: EventType.RUN_ERROR,
329
+ runId: aguiState.runId,
330
+ model: lastModel || chatOptions.model,
331
+ timestamp,
332
+ message: `${this.name}.structuredOutputStream: response contained no content`,
333
+ code: "empty-response",
334
+ error: {
335
+ message: `${this.name}.structuredOutputStream: response contained no content`,
336
+ code: "empty-response"
337
+ }
338
+ };
339
+ return;
340
+ }
341
+ let parsed;
342
+ try {
343
+ parsed = JSON.parse(accumulatedContent);
344
+ } catch {
345
+ yield {
346
+ type: EventType.RUN_ERROR,
347
+ runId: aguiState.runId,
348
+ model: lastModel || chatOptions.model,
349
+ timestamp,
350
+ message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
351
+ code: "parse-error",
352
+ error: {
353
+ message: "Failed to parse structured output as JSON",
354
+ code: "parse-error"
355
+ }
356
+ };
357
+ return;
358
+ }
359
+ const transformed = this.transformStructuredOutput(parsed);
360
+ yield {
361
+ type: EventType.CUSTOM,
362
+ name: "structured-output.complete",
363
+ value: {
364
+ object: transformed,
365
+ raw: accumulatedContent,
366
+ ...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
367
+ },
368
+ model: lastModel || chatOptions.model,
369
+ timestamp
370
+ };
371
+ yield {
372
+ type: EventType.RUN_FINISHED,
373
+ runId: aguiState.runId,
374
+ threadId: aguiState.threadId,
375
+ model: lastModel || chatOptions.model,
376
+ timestamp,
377
+ finishReason: "stop",
378
+ ...lastUsage && {
379
+ usage: buildChatCompletionsUsage(lastUsage)
380
+ }
381
+ };
382
+ } catch (error) {
383
+ if (!aguiState.hasEmittedRunStarted) {
384
+ aguiState.hasEmittedRunStarted = true;
385
+ yield {
386
+ type: EventType.RUN_STARTED,
387
+ runId: aguiState.runId,
388
+ threadId: aguiState.threadId,
389
+ model: chatOptions.model,
390
+ timestamp,
391
+ parentRunId: chatOptions.parentRunId
392
+ };
393
+ }
394
+ const isAbort = this.isAbortError(error);
395
+ const errorPayload = toRunErrorPayload(
396
+ error,
397
+ `${this.name}.structuredOutputStream failed`
398
+ );
399
+ const resolvedCode = isAbort ? "aborted" : errorPayload.code;
400
+ const rawEvent = isAbort ? void 0 : toRunErrorRawEvent(error);
401
+ yield {
402
+ type: EventType.RUN_ERROR,
403
+ runId: aguiState.runId,
404
+ model: lastModel || chatOptions.model,
405
+ timestamp,
406
+ message: errorPayload.message,
407
+ ...resolvedCode !== void 0 && { code: resolvedCode },
408
+ ...rawEvent !== void 0 && { rawEvent },
409
+ error: {
410
+ message: errorPayload.message,
411
+ ...resolvedCode !== void 0 && { code: resolvedCode }
412
+ }
413
+ };
414
+ chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
415
+ error: errorPayload,
416
+ source: `${this.name}.structuredOutputStream`
417
+ });
418
+ }
419
+ }
420
+ /**
421
+ * Cross-SDK abort detection for `structuredOutputStream`. Default duck-types
422
+ * on `name === 'APIUserAbortError'` (OpenAI SDK), `code === 'ERR_CANCELED'`,
423
+ * and standard `AbortError`s. Subclasses with proprietary error types (e.g.
424
+ * `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.
425
+ */
426
+ isAbortError(error) {
427
+ if (!error || typeof error !== "object") return false;
428
+ const e = error;
429
+ return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
430
+ }
431
+ /**
432
+ * Applies provider-specific transformations for structured output compatibility.
433
+ * Override this in subclasses to handle provider-specific quirks.
434
+ */
435
+ makeStructuredOutputCompatible(schema, originalRequired) {
436
+ return makeStructuredOutputCompatible(schema, originalRequired);
437
+ }
438
+ /**
439
+ * Extract reasoning content from a stream chunk. Default returns
440
+ * `undefined` because the OpenAI Chat Completions chunk shape doesn't
441
+ * carry reasoning. The chunk param is typed `unknown` so an override can
442
+ * narrow to its own SDK chunk type without an `as` dance — the base only
443
+ * passes through `processStreamChunks`'s structurally-iterated chunk.
444
+ */
445
+ extractReasoning(_chunk) {
446
+ return void 0;
447
+ }
448
+ /**
449
+ * Final shaping pass applied to parsed structured-output JSON before it is
450
+ * returned to the caller. Default is a passthrough.
451
+ *
452
+ * Provider `null`s are no longer stripped here: strict-mode null-widening is
453
+ * now undone precisely by the engine (`undoNullWidening`, driven by the
454
+ * schema's null-widening map) the moment the result is captured, so a blind
455
+ * `transformNullsToUndefined` at the adapter would only destroy genuine
456
+ * `.nullable()` nulls. Subclasses may still override to remap or reshape the
457
+ * provider's structured output.
458
+ */
459
+ transformStructuredOutput(parsed) {
460
+ return parsed;
461
+ }
462
+ /**
463
+ * Processes streamed chunks from the Chat Completions API and yields AG-UI events.
464
+ * Override this in subclasses to handle provider-specific stream behavior.
465
+ */
466
+ async *processStreamChunks(stream, options, aguiState) {
467
+ let accumulatedContent = "";
468
+ let hasEmittedTextMessageStart = false;
469
+ let lastModel;
470
+ let lastUsage;
471
+ let pendingFinishReason;
472
+ const toolCallsInProgress = /* @__PURE__ */ new Map();
473
+ let reasoningMessageId;
474
+ let hasClosedReasoning = false;
475
+ let stepId;
476
+ let accumulatedReasoning = "";
477
+ let emittedAnyToolCallEnd = false;
478
+ try {
479
+ for await (const chunk of stream) {
480
+ const choiceForLog = chunk.choices[0];
481
+ options.logger.provider(
482
+ `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasToolCalls=${!!choiceForLog?.delta.tool_calls} hasUsage=${!!chunk.usage}`,
483
+ { provider: this.name, model: chunk.model }
484
+ );
485
+ if (chunk.usage) {
486
+ lastUsage = chunk.usage;
487
+ }
488
+ if (chunk.model) {
489
+ lastModel = chunk.model;
490
+ }
491
+ if (!aguiState.hasEmittedRunStarted) {
492
+ aguiState.hasEmittedRunStarted = true;
493
+ yield {
494
+ type: EventType.RUN_STARTED,
495
+ runId: aguiState.runId,
496
+ threadId: aguiState.threadId,
497
+ model: chunk.model || options.model,
498
+ timestamp: Date.now(),
499
+ parentRunId: options.parentRunId
500
+ };
501
+ }
502
+ const reasoning = this.extractReasoning(chunk);
503
+ if (reasoning && reasoning.text) {
504
+ if (!reasoningMessageId) {
505
+ reasoningMessageId = generateId(this.name);
506
+ stepId = generateId(this.name);
507
+ yield {
508
+ type: EventType.REASONING_START,
509
+ messageId: reasoningMessageId,
510
+ model: chunk.model || options.model,
511
+ timestamp: Date.now()
512
+ };
513
+ yield {
514
+ type: EventType.REASONING_MESSAGE_START,
515
+ messageId: reasoningMessageId,
516
+ role: "reasoning",
517
+ model: chunk.model || options.model,
518
+ timestamp: Date.now()
519
+ };
520
+ yield {
521
+ type: EventType.STEP_STARTED,
522
+ stepName: stepId,
523
+ stepId,
524
+ model: chunk.model || options.model,
525
+ timestamp: Date.now(),
526
+ stepType: "thinking"
527
+ };
528
+ }
529
+ accumulatedReasoning += reasoning.text;
530
+ yield {
531
+ type: EventType.REASONING_MESSAGE_CONTENT,
532
+ messageId: reasoningMessageId,
533
+ delta: reasoning.text,
534
+ model: chunk.model || options.model,
535
+ timestamp: Date.now()
536
+ };
537
+ }
538
+ const choice = chunk.choices[0];
539
+ if (!choice) continue;
540
+ const delta = choice.delta;
541
+ const deltaContent = delta.content;
542
+ const deltaToolCalls = delta.tool_calls;
543
+ if (deltaContent) {
544
+ if (reasoningMessageId && !hasClosedReasoning) {
545
+ hasClosedReasoning = true;
546
+ yield {
547
+ type: EventType.REASONING_MESSAGE_END,
548
+ messageId: reasoningMessageId,
549
+ model: chunk.model || options.model,
550
+ timestamp: Date.now()
551
+ };
552
+ yield {
553
+ type: EventType.REASONING_END,
554
+ messageId: reasoningMessageId,
555
+ model: chunk.model || options.model,
556
+ timestamp: Date.now()
557
+ };
558
+ if (stepId) {
559
+ yield {
560
+ type: EventType.STEP_FINISHED,
561
+ stepName: stepId,
562
+ stepId,
563
+ model: chunk.model || options.model,
564
+ timestamp: Date.now(),
565
+ content: accumulatedReasoning
566
+ };
567
+ }
568
+ }
569
+ if (!hasEmittedTextMessageStart) {
570
+ hasEmittedTextMessageStart = true;
571
+ yield {
572
+ type: EventType.TEXT_MESSAGE_START,
573
+ messageId: aguiState.messageId,
574
+ model: chunk.model || options.model,
575
+ timestamp: Date.now(),
576
+ role: "assistant"
577
+ };
578
+ }
579
+ accumulatedContent += deltaContent;
580
+ yield {
581
+ type: EventType.TEXT_MESSAGE_CONTENT,
582
+ messageId: aguiState.messageId,
583
+ model: chunk.model || options.model,
584
+ timestamp: Date.now(),
585
+ delta: deltaContent,
586
+ content: accumulatedContent
587
+ };
588
+ }
589
+ if (deltaToolCalls) {
590
+ for (const toolCallDelta of deltaToolCalls) {
591
+ const index = toolCallDelta.index;
592
+ let toolCall = toolCallsInProgress.get(index);
593
+ if (!toolCall) {
594
+ toolCall = {
595
+ id: toolCallDelta.id || "",
596
+ name: toolCallDelta.function?.name || "",
597
+ arguments: "",
598
+ started: false
599
+ };
600
+ toolCallsInProgress.set(index, toolCall);
601
+ }
602
+ if (toolCallDelta.id) {
603
+ toolCall.id = toolCallDelta.id;
604
+ }
605
+ if (toolCallDelta.function?.name) {
606
+ toolCall.name = toolCallDelta.function.name;
607
+ }
608
+ if (toolCallDelta.function?.arguments) {
609
+ toolCall.arguments += toolCallDelta.function.arguments;
610
+ }
611
+ if (toolCall.id && toolCall.name && !toolCall.started) {
612
+ toolCall.started = true;
613
+ yield {
614
+ type: EventType.TOOL_CALL_START,
615
+ toolCallId: toolCall.id,
616
+ toolCallName: toolCall.name,
617
+ toolName: toolCall.name,
618
+ parentMessageId: aguiState.messageId,
619
+ model: chunk.model || options.model,
620
+ timestamp: Date.now(),
621
+ index
622
+ };
623
+ }
624
+ if (toolCallDelta.function?.arguments && toolCall.started) {
625
+ yield {
626
+ type: EventType.TOOL_CALL_ARGS,
627
+ toolCallId: toolCall.id,
628
+ model: chunk.model || options.model,
629
+ timestamp: Date.now(),
630
+ delta: toolCallDelta.function.arguments
631
+ };
632
+ }
633
+ }
634
+ }
635
+ if (choice.finish_reason) {
636
+ if (choice.finish_reason === "tool_calls" || toolCallsInProgress.size > 0) {
637
+ for (const [, toolCall] of toolCallsInProgress) {
638
+ if (!toolCall.started) continue;
639
+ let parsedInput = {};
640
+ if (toolCall.arguments) {
641
+ try {
642
+ const parsed = JSON.parse(toolCall.arguments);
643
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
644
+ } catch (parseError) {
645
+ options.logger.errors(
646
+ `${this.name}.processStreamChunks tool-args JSON parse failed`,
647
+ {
648
+ error: toRunErrorPayload(
649
+ parseError,
650
+ `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`
651
+ ),
652
+ source: `${this.name}.processStreamChunks`,
653
+ toolCallId: toolCall.id,
654
+ toolName: toolCall.name,
655
+ rawArguments: toolCall.arguments
656
+ }
657
+ );
658
+ parsedInput = {};
659
+ }
660
+ }
661
+ yield {
662
+ type: EventType.TOOL_CALL_END,
663
+ toolCallId: toolCall.id,
664
+ toolCallName: toolCall.name,
665
+ toolName: toolCall.name,
666
+ model: chunk.model || options.model,
667
+ timestamp: Date.now(),
668
+ input: parsedInput
669
+ };
670
+ emittedAnyToolCallEnd = true;
671
+ }
672
+ toolCallsInProgress.clear();
673
+ }
674
+ if (hasEmittedTextMessageStart) {
675
+ yield {
676
+ type: EventType.TEXT_MESSAGE_END,
677
+ messageId: aguiState.messageId,
678
+ model: chunk.model || options.model,
679
+ timestamp: Date.now()
680
+ };
681
+ hasEmittedTextMessageStart = false;
682
+ }
683
+ pendingFinishReason = choice.finish_reason;
684
+ }
685
+ }
686
+ if (aguiState.hasEmittedRunStarted) {
687
+ let pendingToolCount = 0;
688
+ for (const [, toolCall] of toolCallsInProgress) {
689
+ if (!toolCall.started) continue;
690
+ let parsedInput = {};
691
+ if (toolCall.arguments) {
692
+ try {
693
+ const parsed = JSON.parse(toolCall.arguments);
694
+ parsedInput = parsed && typeof parsed === "object" ? parsed : {};
695
+ } catch (parseError) {
696
+ options.logger.errors(
697
+ `${this.name}.processStreamChunks tool-args JSON parse failed (drain)`,
698
+ {
699
+ error: toRunErrorPayload(
700
+ parseError,
701
+ `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`
702
+ ),
703
+ source: `${this.name}.processStreamChunks`,
704
+ toolCallId: toolCall.id,
705
+ toolName: toolCall.name,
706
+ rawArguments: toolCall.arguments
707
+ }
708
+ );
709
+ parsedInput = {};
710
+ }
711
+ }
712
+ yield {
713
+ type: EventType.TOOL_CALL_END,
714
+ toolCallId: toolCall.id,
715
+ toolCallName: toolCall.name,
716
+ toolName: toolCall.name,
717
+ model: lastModel || options.model,
718
+ timestamp: Date.now(),
719
+ input: parsedInput
720
+ };
721
+ pendingToolCount += 1;
722
+ emittedAnyToolCallEnd = true;
723
+ }
724
+ toolCallsInProgress.clear();
725
+ if (hasEmittedTextMessageStart) {
726
+ yield {
727
+ type: EventType.TEXT_MESSAGE_END,
728
+ messageId: aguiState.messageId,
729
+ model: lastModel || options.model,
730
+ timestamp: Date.now()
731
+ };
732
+ }
733
+ if (reasoningMessageId && !hasClosedReasoning) {
734
+ hasClosedReasoning = true;
735
+ yield {
736
+ type: EventType.REASONING_MESSAGE_END,
737
+ messageId: reasoningMessageId,
738
+ model: lastModel || options.model,
739
+ timestamp: Date.now()
740
+ };
741
+ yield {
742
+ type: EventType.REASONING_END,
743
+ messageId: reasoningMessageId,
744
+ model: lastModel || options.model,
745
+ timestamp: Date.now()
746
+ };
747
+ if (stepId) {
748
+ yield {
749
+ type: EventType.STEP_FINISHED,
750
+ stepName: stepId,
751
+ stepId,
752
+ model: lastModel || options.model,
753
+ timestamp: Date.now(),
754
+ content: accumulatedReasoning
755
+ };
756
+ }
757
+ }
758
+ const finishReason = emittedAnyToolCallEnd ? "tool_calls" : pendingFinishReason === "tool_calls" ? "stop" : pendingFinishReason === "function_call" ? "tool_calls" : pendingFinishReason ?? "stop";
759
+ yield {
760
+ type: EventType.RUN_FINISHED,
761
+ runId: aguiState.runId,
762
+ threadId: aguiState.threadId,
763
+ model: lastModel || options.model,
764
+ timestamp: Date.now(),
765
+ ...lastUsage && {
766
+ usage: buildChatCompletionsUsage(lastUsage)
767
+ },
768
+ finishReason
769
+ };
770
+ }
771
+ } catch (error) {
772
+ const errorPayload = toRunErrorPayload(
773
+ error,
774
+ `${this.name}.processStreamChunks failed`
775
+ );
776
+ const rawEvent = toRunErrorRawEvent(error);
777
+ options.logger.errors(`${this.name}.processStreamChunks fatal`, {
778
+ error: errorPayload,
779
+ source: `${this.name}.processStreamChunks`
780
+ });
781
+ yield {
782
+ type: EventType.RUN_ERROR,
783
+ model: options.model,
784
+ timestamp: Date.now(),
785
+ message: errorPayload.message,
786
+ ...errorPayload.code !== void 0 && { code: errorPayload.code },
787
+ ...rawEvent !== void 0 && { rawEvent },
788
+ error: {
789
+ message: errorPayload.message,
790
+ ...errorPayload.code !== void 0 && { code: errorPayload.code }
791
+ }
792
+ };
793
+ }
794
+ }
795
+ /**
796
+ * Maps common TextOptions to Chat Completions API request format.
797
+ * Override this in subclasses to add provider-specific options.
798
+ */
799
+ mapOptionsToRequest(options) {
800
+ const tools = options.tools ? convertToolsToChatCompletionsFormat(
801
+ options.tools,
802
+ this.makeStructuredOutputCompatible.bind(this)
803
+ ) : void 0;
804
+ const messages = [];
805
+ const systemPrompts = normalizeSystemPrompts(options.systemPrompts);
806
+ if (systemPrompts.length > 0) {
807
+ messages.push({
808
+ role: "system",
809
+ content: systemPrompts.map((p) => p.content).join("\n")
810
+ });
811
+ }
812
+ for (const message of options.messages) {
813
+ messages.push(this.convertMessage(message));
814
+ }
815
+ const modelOptions = options.modelOptions;
816
+ const combinedSchema = options.outputSchema;
817
+ const responseFormat = combinedSchema ? {
818
+ response_format: {
819
+ type: "json_schema",
820
+ json_schema: {
821
+ name: "structured_output",
822
+ schema: this.makeStructuredOutputCompatible(
823
+ combinedSchema,
824
+ Array.isArray(combinedSchema.required) ? combinedSchema.required : void 0
825
+ ),
826
+ strict: true
827
+ }
828
+ }
829
+ } : void 0;
830
+ return {
831
+ ...modelOptions,
832
+ model: options.model,
833
+ messages,
834
+ // Conditional spread: `tools: undefined` would clobber any
835
+ // modelOptions.tools the caller set above.
836
+ ...tools && tools.length > 0 && {
837
+ tools
838
+ },
839
+ ...responseFormat ?? {},
840
+ stream: true
841
+ };
842
+ }
843
+ /**
844
+ * Modern OpenAI-compatible Chat Completions APIs support `tools` and
845
+ * `response_format: json_schema` together in a single streaming request
846
+ * (per issue #605). Subclasses can override — Groq, for instance, must
847
+ * return `false` because its API rejects schema + tools + stream with a
848
+ * 400.
849
+ */
850
+ supportsCombinedToolsAndSchema() {
851
+ return true;
852
+ }
853
+ /**
854
+ * Converts a single ModelMessage to the Chat Completions API message format.
855
+ * Override this in subclasses to handle provider-specific message formats.
856
+ */
857
+ convertMessage(message) {
858
+ if (message.role === "tool") {
859
+ return {
860
+ role: "tool",
861
+ tool_call_id: message.toolCallId || "",
862
+ content: typeof message.content === "string" ? message.content : JSON.stringify(message.content)
863
+ };
864
+ }
865
+ if (message.role === "assistant") {
866
+ const toolCalls = message.toolCalls?.map((tc) => ({
867
+ id: tc.id,
868
+ type: "function",
869
+ function: {
870
+ name: tc.function.name,
871
+ arguments: typeof tc.function.arguments === "string" ? tc.function.arguments : JSON.stringify(tc.function.arguments)
872
+ }
873
+ }));
874
+ const hasToolCalls = !!toolCalls && toolCalls.length > 0;
875
+ const textContent = this.extractTextContent(message.content);
876
+ return {
877
+ role: "assistant",
878
+ content: hasToolCalls && !textContent ? null : textContent,
879
+ ...hasToolCalls ? { tool_calls: toolCalls } : {}
880
+ };
881
+ }
882
+ const contentParts = this.normalizeContent(message.content);
883
+ if (contentParts.length === 1 && contentParts[0]?.type === "text") {
884
+ const text = contentParts[0].content;
885
+ if (text.length === 0) {
886
+ throw new Error(
887
+ `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.`
888
+ );
889
+ }
890
+ return {
891
+ role: "user",
892
+ content: text
893
+ };
894
+ }
895
+ const parts = [];
896
+ for (const part of contentParts) {
897
+ const converted = this.convertContentPart(part);
898
+ if (!converted) {
899
+ throw new Error(
900
+ `Unsupported content part type for ${this.name}: ${part.type}. Override convertContentPart() in a subclass to handle this type, or remove it from the message.`
901
+ );
902
+ }
903
+ parts.push(converted);
904
+ }
905
+ if (parts.length === 0) {
906
+ throw new Error(
907
+ `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.`
908
+ );
909
+ }
910
+ return {
911
+ role: "user",
912
+ content: parts
913
+ };
914
+ }
915
+ /**
916
+ * Converts a single ContentPart to the Chat Completions API content part format.
917
+ * Override this in subclasses to handle additional content types or provider-specific metadata.
918
+ */
919
+ convertContentPart(part) {
920
+ if (part.type === "text") {
921
+ return { type: "text", text: part.content };
922
+ }
923
+ if (part.type === "image") {
924
+ const imageMetadata = part.metadata;
925
+ const imageValue = part.source.value;
926
+ const imageMime = part.source.mimeType || "application/octet-stream";
927
+ const imageUrl = part.source.type === "data" && !imageValue.startsWith("data:") ? `data:${imageMime};base64,${imageValue}` : imageValue;
928
+ return {
929
+ type: "image_url",
930
+ image_url: {
931
+ url: imageUrl,
932
+ detail: imageMetadata?.detail || "auto"
933
+ }
934
+ };
935
+ }
936
+ return null;
937
+ }
938
+ /**
939
+ * Normalizes message content to an array of ContentPart.
940
+ * Handles backward compatibility with string content.
941
+ */
942
+ normalizeContent(content) {
943
+ if (content === null) {
944
+ return [];
945
+ }
946
+ if (typeof content === "string") {
947
+ return [{ type: "text", content }];
948
+ }
949
+ return content;
950
+ }
951
+ /**
952
+ * Extracts text content from a content value that may be string, null, or ContentPart array.
953
+ */
954
+ extractTextContent(content) {
955
+ if (content === null) {
956
+ return "";
957
+ }
958
+ if (typeof content === "string") {
959
+ return content;
960
+ }
961
+ return content.filter((p) => p.type === "text").map((p) => p.content).join("");
962
+ }
963
+ }
964
+ export {
965
+ OpenAIBaseChatCompletionsTextAdapter
966
+ };
967
+ //# sourceMappingURL=chat-completions-text.js.map