@tanstack/openai-base 0.9.9 → 0.9.11

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