@tanstack/openai-base 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +121 -0
  2. package/dist/esm/adapters/chat-completions-text.d.ts +49 -21
  3. package/dist/esm/adapters/chat-completions-text.js +480 -68
  4. package/dist/esm/adapters/chat-completions-text.js.map +1 -1
  5. package/dist/esm/adapters/chat-completions-tool-converter.d.ts +8 -4
  6. package/dist/esm/adapters/chat-completions-tool-converter.js.map +1 -1
  7. package/dist/esm/adapters/responses-text.d.ts +46 -33
  8. package/dist/esm/adapters/responses-text.js +661 -142
  9. package/dist/esm/adapters/responses-text.js.map +1 -1
  10. package/dist/esm/index.d.ts +2 -9
  11. package/dist/esm/index.js +4 -16
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/tools/apply-patch-tool.d.ts +2 -2
  14. package/dist/esm/tools/apply-patch-tool.js.map +1 -1
  15. package/dist/esm/tools/code-interpreter-tool.d.ts +3 -2
  16. package/dist/esm/tools/code-interpreter-tool.js.map +1 -1
  17. package/dist/esm/tools/computer-use-tool.d.ts +2 -2
  18. package/dist/esm/tools/computer-use-tool.js.map +1 -1
  19. package/dist/esm/tools/custom-tool.d.ts +2 -2
  20. package/dist/esm/tools/custom-tool.js.map +1 -1
  21. package/dist/esm/tools/file-search-tool.d.ts +2 -2
  22. package/dist/esm/tools/file-search-tool.js.map +1 -1
  23. package/dist/esm/tools/function-tool.d.ts +2 -2
  24. package/dist/esm/tools/function-tool.js.map +1 -1
  25. package/dist/esm/tools/image-generation-tool.d.ts +3 -2
  26. package/dist/esm/tools/image-generation-tool.js.map +1 -1
  27. package/dist/esm/tools/local-shell-tool.d.ts +3 -2
  28. package/dist/esm/tools/local-shell-tool.js.map +1 -1
  29. package/dist/esm/tools/mcp-tool.d.ts +3 -2
  30. package/dist/esm/tools/mcp-tool.js.map +1 -1
  31. package/dist/esm/tools/shell-tool.d.ts +2 -2
  32. package/dist/esm/tools/shell-tool.js.map +1 -1
  33. package/dist/esm/tools/web-search-preview-tool.d.ts +2 -2
  34. package/dist/esm/tools/web-search-preview-tool.js.map +1 -1
  35. package/dist/esm/tools/web-search-tool.d.ts +2 -2
  36. package/dist/esm/tools/web-search-tool.js.map +1 -1
  37. package/package.json +6 -6
  38. package/src/adapters/chat-completions-text.ts +605 -117
  39. package/src/adapters/chat-completions-tool-converter.ts +9 -5
  40. package/src/adapters/responses-text.ts +869 -210
  41. package/src/index.ts +2 -12
  42. package/src/tools/apply-patch-tool.ts +2 -2
  43. package/src/tools/code-interpreter-tool.ts +4 -2
  44. package/src/tools/computer-use-tool.ts +2 -2
  45. package/src/tools/custom-tool.ts +2 -2
  46. package/src/tools/file-search-tool.ts +3 -3
  47. package/src/tools/function-tool.ts +2 -2
  48. package/src/tools/image-generation-tool.ts +4 -2
  49. package/src/tools/local-shell-tool.ts +4 -2
  50. package/src/tools/mcp-tool.ts +4 -2
  51. package/src/tools/shell-tool.ts +2 -2
  52. package/src/tools/web-search-preview-tool.ts +2 -2
  53. package/src/tools/web-search-tool.ts +2 -2
  54. package/dist/esm/adapters/image.d.ts +0 -32
  55. package/dist/esm/adapters/image.js +0 -89
  56. package/dist/esm/adapters/image.js.map +0 -1
  57. package/dist/esm/adapters/summarize.d.ts +0 -28
  58. package/dist/esm/adapters/summarize.js +0 -112
  59. package/dist/esm/adapters/summarize.js.map +0 -1
  60. package/dist/esm/adapters/transcription.d.ts +0 -34
  61. package/dist/esm/adapters/transcription.js +0 -131
  62. package/dist/esm/adapters/transcription.js.map +0 -1
  63. package/dist/esm/adapters/tts.d.ts +0 -26
  64. package/dist/esm/adapters/tts.js +0 -78
  65. package/dist/esm/adapters/tts.js.map +0 -1
  66. package/dist/esm/adapters/video.d.ts +0 -72
  67. package/dist/esm/adapters/video.js +0 -238
  68. package/dist/esm/adapters/video.js.map +0 -1
  69. package/dist/esm/types/config.d.ts +0 -4
  70. package/dist/esm/utils/client.d.ts +0 -3
  71. package/dist/esm/utils/client.js +0 -8
  72. package/dist/esm/utils/client.js.map +0 -1
  73. package/src/adapters/image.ts +0 -158
  74. package/src/adapters/summarize.ts +0 -174
  75. package/src/adapters/transcription.ts +0 -194
  76. package/src/adapters/tts.ts +0 -124
  77. package/src/adapters/video.ts +0 -385
  78. package/src/types/config.ts +0 -5
  79. package/src/utils/client.ts +0 -8
@@ -1,28 +1,26 @@
1
+ import { EventType } from "@tanstack/ai";
1
2
  import { BaseTextAdapter } from "@tanstack/ai/adapters";
2
3
  import { toRunErrorPayload } from "@tanstack/ai/adapter-internals";
3
4
  import { generateId, transformNullsToUndefined } from "@tanstack/ai-utils";
4
- import { createOpenAICompatibleClient } from "../utils/client.js";
5
5
  import { extractRequestOptions } from "../utils/request-options.js";
6
6
  import { makeStructuredOutputCompatible } from "../utils/schema-converter.js";
7
7
  import { convertToolsToChatCompletionsFormat } from "./chat-completions-tool-converter.js";
8
- const asChunk = (chunk) => chunk;
9
- class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
10
- constructor(config, model, name = "openai-compatible") {
8
+ class OpenAIBaseChatCompletionsTextAdapter extends BaseTextAdapter {
9
+ constructor(model, name, client) {
11
10
  super({}, model);
12
11
  this.kind = "text";
13
12
  this.name = name;
14
- this.client = createOpenAICompatibleClient(config);
13
+ this.client = client;
15
14
  }
16
15
  async *chatStream(options) {
17
- const requestParams = this.mapOptionsToRequest(options);
18
- const timestamp = Date.now();
19
16
  const aguiState = {
20
17
  runId: generateId(this.name),
18
+ threadId: options.threadId ?? generateId(this.name),
21
19
  messageId: generateId(this.name),
22
- timestamp,
23
20
  hasEmittedRunStarted: false
24
21
  };
25
22
  try {
23
+ const requestParams = this.mapOptionsToRequest(options);
26
24
  options.logger.request(
27
25
  `activity=chat provider=${this.name} model=${this.model} messages=${options.messages.length} tools=${options.tools?.length ?? 0} stream=true`,
28
26
  { provider: this.name, model: this.model }
@@ -43,20 +41,23 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
43
41
  );
44
42
  if (!aguiState.hasEmittedRunStarted) {
45
43
  aguiState.hasEmittedRunStarted = true;
46
- yield asChunk({
47
- type: "RUN_STARTED",
44
+ yield {
45
+ type: EventType.RUN_STARTED,
48
46
  runId: aguiState.runId,
47
+ threadId: aguiState.threadId,
49
48
  model: options.model,
50
- timestamp
51
- });
49
+ timestamp: Date.now(),
50
+ parentRunId: options.parentRunId
51
+ };
52
52
  }
53
- yield asChunk({
54
- type: "RUN_ERROR",
55
- runId: aguiState.runId,
53
+ yield {
54
+ type: EventType.RUN_ERROR,
56
55
  model: options.model,
57
- timestamp,
56
+ timestamp: Date.now(),
57
+ message: errorPayload.message,
58
+ code: errorPayload.code,
58
59
  error: errorPayload
59
- });
60
+ };
60
61
  options.logger.errors(`${this.name}.chatStream fatal`, {
61
62
  error: errorPayload,
62
63
  source: `${this.name}.chatStream`
@@ -107,7 +108,12 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
107
108
  },
108
109
  extractRequestOptions(chatOptions.request)
109
110
  );
110
- const rawText = response.choices[0]?.message.content || "";
111
+ const rawText = response.choices[0]?.message.content;
112
+ if (typeof rawText !== "string" || rawText.length === 0) {
113
+ throw new Error(
114
+ `${this.name}.structuredOutput: response contained no content`
115
+ );
116
+ }
111
117
  let parsed;
112
118
  try {
113
119
  parsed = JSON.parse(rawText);
@@ -116,7 +122,7 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
116
122
  `Failed to parse structured output as JSON. Content: ${rawText.slice(0, 200)}${rawText.length > 200 ? "..." : ""}`
117
123
  );
118
124
  }
119
- const transformed = transformNullsToUndefined(parsed);
125
+ const transformed = this.transformStructuredOutput(parsed);
120
126
  return {
121
127
  data: transformed,
122
128
  rawText
@@ -129,6 +135,286 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
129
135
  throw error;
130
136
  }
131
137
  }
138
+ /**
139
+ * Stream structured output. Single Chat Completions request with
140
+ * `response_format: json_schema` + `stream: true`. Emits the standard
141
+ * AG-UI lifecycle (`RUN_STARTED` → `REASONING_*?` → `TEXT_MESSAGE_*`
142
+ * carrying raw JSON deltas → terminal `CUSTOM 'structured-output.complete'`
143
+ * → `RUN_FINISHED`). Subclasses use the same SDK-call / reasoning /
144
+ * structured-output-transform hooks as `chatStream` / `structuredOutput` —
145
+ * no per-subclass override should be needed.
146
+ */
147
+ async *structuredOutputStream(options) {
148
+ const { chatOptions, outputSchema } = options;
149
+ const requestParams = this.mapOptionsToRequest(chatOptions);
150
+ const jsonSchema = this.makeStructuredOutputCompatible(
151
+ outputSchema,
152
+ outputSchema.required
153
+ );
154
+ const timestamp = Date.now();
155
+ const aguiState = {
156
+ runId: generateId(this.name),
157
+ threadId: chatOptions.threadId ?? generateId(this.name),
158
+ messageId: generateId(this.name),
159
+ timestamp,
160
+ hasEmittedRunStarted: false
161
+ };
162
+ let accumulatedContent = "";
163
+ let accumulatedReasoning = "";
164
+ let hasEmittedTextMessageStart = false;
165
+ let reasoningMessageId;
166
+ let hasClosedReasoning = false;
167
+ let stepId;
168
+ let lastModel;
169
+ let lastUsage;
170
+ const closeReasoningLifecycle = (function* () {
171
+ if (reasoningMessageId && !hasClosedReasoning) {
172
+ hasClosedReasoning = true;
173
+ yield {
174
+ type: EventType.REASONING_MESSAGE_END,
175
+ messageId: reasoningMessageId,
176
+ model: lastModel || chatOptions.model,
177
+ timestamp
178
+ };
179
+ yield {
180
+ type: EventType.REASONING_END,
181
+ messageId: reasoningMessageId,
182
+ model: lastModel || chatOptions.model,
183
+ timestamp
184
+ };
185
+ if (stepId) {
186
+ yield {
187
+ type: EventType.STEP_FINISHED,
188
+ stepName: stepId,
189
+ stepId,
190
+ model: lastModel || chatOptions.model,
191
+ timestamp,
192
+ content: accumulatedReasoning
193
+ };
194
+ }
195
+ }
196
+ }).bind(this);
197
+ try {
198
+ const {
199
+ stream_options: _so,
200
+ stream: _s,
201
+ tools: _t,
202
+ ...cleanParams
203
+ } = requestParams;
204
+ chatOptions.logger.request(
205
+ `activity=structuredOutputStream provider=${this.name} model=${this.model} messages=${chatOptions.messages.length}`,
206
+ { provider: this.name, model: this.model }
207
+ );
208
+ const stream = await this.client.chat.completions.create(
209
+ {
210
+ ...cleanParams,
211
+ stream: true,
212
+ stream_options: { include_usage: true },
213
+ response_format: {
214
+ type: "json_schema",
215
+ json_schema: {
216
+ name: "structured_output",
217
+ schema: jsonSchema,
218
+ strict: true
219
+ }
220
+ }
221
+ },
222
+ extractRequestOptions(chatOptions.request)
223
+ );
224
+ for await (const chunk of stream) {
225
+ const choiceForLog = chunk.choices[0];
226
+ chatOptions.logger.provider(
227
+ `provider=${this.name} finish_reason=${choiceForLog?.finish_reason ?? "none"} hasContent=${!!choiceForLog?.delta.content} hasUsage=${!!chunk.usage}`,
228
+ { provider: this.name, model: chunk.model }
229
+ );
230
+ if (chunk.model) lastModel = chunk.model;
231
+ const usage = chunk.usage ?? chunk.x_groq?.usage;
232
+ if (usage) lastUsage = usage;
233
+ if (!aguiState.hasEmittedRunStarted) {
234
+ aguiState.hasEmittedRunStarted = true;
235
+ yield {
236
+ type: EventType.RUN_STARTED,
237
+ runId: aguiState.runId,
238
+ threadId: aguiState.threadId,
239
+ model: chunk.model || chatOptions.model,
240
+ timestamp,
241
+ parentRunId: chatOptions.parentRunId
242
+ };
243
+ }
244
+ const reasoning = this.extractReasoning(chunk);
245
+ if (reasoning && reasoning.text) {
246
+ if (!reasoningMessageId) {
247
+ reasoningMessageId = generateId(this.name);
248
+ stepId = generateId(this.name);
249
+ yield {
250
+ type: EventType.REASONING_START,
251
+ messageId: reasoningMessageId,
252
+ model: chunk.model || chatOptions.model,
253
+ timestamp
254
+ };
255
+ yield {
256
+ type: EventType.REASONING_MESSAGE_START,
257
+ messageId: reasoningMessageId,
258
+ role: "reasoning",
259
+ model: chunk.model || chatOptions.model,
260
+ timestamp
261
+ };
262
+ yield {
263
+ type: EventType.STEP_STARTED,
264
+ stepName: stepId,
265
+ stepId,
266
+ model: chunk.model || chatOptions.model,
267
+ timestamp,
268
+ stepType: "thinking"
269
+ };
270
+ }
271
+ accumulatedReasoning += reasoning.text;
272
+ yield {
273
+ type: EventType.REASONING_MESSAGE_CONTENT,
274
+ messageId: reasoningMessageId,
275
+ delta: reasoning.text,
276
+ model: chunk.model || chatOptions.model,
277
+ timestamp
278
+ };
279
+ }
280
+ const choice = chunk.choices[0];
281
+ if (!choice) continue;
282
+ const deltaContent = choice.delta.content;
283
+ if (deltaContent) {
284
+ yield* closeReasoningLifecycle();
285
+ if (!hasEmittedTextMessageStart) {
286
+ hasEmittedTextMessageStart = true;
287
+ yield {
288
+ type: EventType.TEXT_MESSAGE_START,
289
+ messageId: aguiState.messageId,
290
+ model: chunk.model || chatOptions.model,
291
+ timestamp,
292
+ role: "assistant"
293
+ };
294
+ }
295
+ accumulatedContent += deltaContent;
296
+ yield {
297
+ type: EventType.TEXT_MESSAGE_CONTENT,
298
+ messageId: aguiState.messageId,
299
+ model: chunk.model || chatOptions.model,
300
+ timestamp,
301
+ delta: deltaContent,
302
+ content: accumulatedContent
303
+ };
304
+ }
305
+ }
306
+ yield* closeReasoningLifecycle();
307
+ if (hasEmittedTextMessageStart) {
308
+ yield {
309
+ type: EventType.TEXT_MESSAGE_END,
310
+ messageId: aguiState.messageId,
311
+ model: lastModel || chatOptions.model,
312
+ timestamp
313
+ };
314
+ }
315
+ if (accumulatedContent.length === 0) {
316
+ yield {
317
+ type: EventType.RUN_ERROR,
318
+ runId: aguiState.runId,
319
+ model: lastModel || chatOptions.model,
320
+ timestamp,
321
+ message: `${this.name}.structuredOutputStream: response contained no content`,
322
+ code: "empty-response",
323
+ error: {
324
+ message: `${this.name}.structuredOutputStream: response contained no content`,
325
+ code: "empty-response"
326
+ }
327
+ };
328
+ return;
329
+ }
330
+ let parsed;
331
+ try {
332
+ parsed = JSON.parse(accumulatedContent);
333
+ } catch {
334
+ yield {
335
+ type: EventType.RUN_ERROR,
336
+ runId: aguiState.runId,
337
+ model: lastModel || chatOptions.model,
338
+ timestamp,
339
+ message: `Failed to parse structured output as JSON. Content: ${accumulatedContent.slice(0, 200)}${accumulatedContent.length > 200 ? "..." : ""}`,
340
+ code: "parse-error",
341
+ error: {
342
+ message: "Failed to parse structured output as JSON",
343
+ code: "parse-error"
344
+ }
345
+ };
346
+ return;
347
+ }
348
+ const transformed = this.transformStructuredOutput(parsed);
349
+ yield {
350
+ type: EventType.CUSTOM,
351
+ name: "structured-output.complete",
352
+ value: {
353
+ object: transformed,
354
+ raw: accumulatedContent,
355
+ ...accumulatedReasoning ? { reasoning: accumulatedReasoning } : {}
356
+ },
357
+ model: lastModel || chatOptions.model,
358
+ timestamp
359
+ };
360
+ yield {
361
+ type: EventType.RUN_FINISHED,
362
+ runId: aguiState.runId,
363
+ threadId: aguiState.threadId,
364
+ model: lastModel || chatOptions.model,
365
+ timestamp,
366
+ finishReason: "stop",
367
+ ...lastUsage && {
368
+ usage: {
369
+ promptTokens: lastUsage.prompt_tokens,
370
+ completionTokens: lastUsage.completion_tokens,
371
+ totalTokens: lastUsage.total_tokens
372
+ }
373
+ }
374
+ };
375
+ } catch (error) {
376
+ if (!aguiState.hasEmittedRunStarted) {
377
+ aguiState.hasEmittedRunStarted = true;
378
+ yield {
379
+ type: EventType.RUN_STARTED,
380
+ runId: aguiState.runId,
381
+ threadId: aguiState.threadId,
382
+ model: chatOptions.model,
383
+ timestamp,
384
+ parentRunId: chatOptions.parentRunId
385
+ };
386
+ }
387
+ const isAbort = this.isAbortError(error);
388
+ const errorPayload = toRunErrorPayload(
389
+ error,
390
+ `${this.name}.structuredOutputStream failed`
391
+ );
392
+ yield {
393
+ type: EventType.RUN_ERROR,
394
+ runId: aguiState.runId,
395
+ model: lastModel || chatOptions.model,
396
+ timestamp,
397
+ message: errorPayload.message,
398
+ code: isAbort ? "aborted" : errorPayload.code,
399
+ error: { ...errorPayload, ...isAbort && { code: "aborted" } }
400
+ };
401
+ chatOptions.logger.errors(`${this.name}.structuredOutputStream fatal`, {
402
+ error: errorPayload,
403
+ source: `${this.name}.structuredOutputStream`
404
+ });
405
+ }
406
+ }
407
+ /**
408
+ * Cross-SDK abort detection for `structuredOutputStream`. Default duck-types
409
+ * on `name === 'APIUserAbortError'` (OpenAI SDK), `code === 'ERR_CANCELED'`,
410
+ * and standard `AbortError`s. Subclasses with proprietary error types (e.g.
411
+ * `@openrouter/sdk`'s `RequestAbortedError`) override to extend the check.
412
+ */
413
+ isAbortError(error) {
414
+ if (!error || typeof error !== "object") return false;
415
+ const e = error;
416
+ return e.name === "APIUserAbortError" || e.name === "AbortError" || e.code === "ERR_CANCELED";
417
+ }
132
418
  /**
133
419
  * Applies provider-specific transformations for structured output compatibility.
134
420
  * Override this in subclasses to handle provider-specific quirks.
@@ -136,18 +422,41 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
136
422
  makeStructuredOutputCompatible(schema, originalRequired) {
137
423
  return makeStructuredOutputCompatible(schema, originalRequired);
138
424
  }
425
+ /**
426
+ * Extract reasoning content from a stream chunk. Default returns
427
+ * `undefined` because the OpenAI Chat Completions chunk shape doesn't
428
+ * carry reasoning. The chunk param is typed `unknown` so an override can
429
+ * narrow to its own SDK chunk type without an `as` dance — the base only
430
+ * passes through `processStreamChunks`'s structurally-iterated chunk.
431
+ */
432
+ extractReasoning(_chunk) {
433
+ return void 0;
434
+ }
435
+ /**
436
+ * Final shaping pass applied to parsed structured-output JSON before it is
437
+ * returned to the caller. Default converts `null` values to `undefined` so
438
+ * the result aligns with the original Zod schema's optional-field
439
+ * semantics. Subclasses with different conventions (OpenRouter historically
440
+ * preserves nulls) can override.
441
+ */
442
+ transformStructuredOutput(parsed) {
443
+ return transformNullsToUndefined(parsed);
444
+ }
139
445
  /**
140
446
  * Processes streamed chunks from the Chat Completions API and yields AG-UI events.
141
447
  * Override this in subclasses to handle provider-specific stream behavior.
142
448
  */
143
449
  async *processStreamChunks(stream, options, aguiState) {
144
450
  let accumulatedContent = "";
145
- const timestamp = aguiState.timestamp;
146
451
  let hasEmittedTextMessageStart = false;
147
452
  let lastModel;
148
453
  let lastUsage;
149
454
  let pendingFinishReason;
150
455
  const toolCallsInProgress = /* @__PURE__ */ new Map();
456
+ let reasoningMessageId;
457
+ let hasClosedReasoning = false;
458
+ let stepId;
459
+ let accumulatedReasoning = "";
151
460
  let emittedAnyToolCallEnd = false;
152
461
  try {
153
462
  for await (const chunk of stream) {
@@ -164,12 +473,50 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
164
473
  }
165
474
  if (!aguiState.hasEmittedRunStarted) {
166
475
  aguiState.hasEmittedRunStarted = true;
167
- yield asChunk({
168
- type: "RUN_STARTED",
476
+ yield {
477
+ type: EventType.RUN_STARTED,
169
478
  runId: aguiState.runId,
479
+ threadId: aguiState.threadId,
170
480
  model: chunk.model || options.model,
171
- timestamp
172
- });
481
+ timestamp: Date.now(),
482
+ parentRunId: options.parentRunId
483
+ };
484
+ }
485
+ const reasoning = this.extractReasoning(chunk);
486
+ if (reasoning && reasoning.text) {
487
+ if (!reasoningMessageId) {
488
+ reasoningMessageId = generateId(this.name);
489
+ stepId = generateId(this.name);
490
+ yield {
491
+ type: EventType.REASONING_START,
492
+ messageId: reasoningMessageId,
493
+ model: chunk.model || options.model,
494
+ timestamp: Date.now()
495
+ };
496
+ yield {
497
+ type: EventType.REASONING_MESSAGE_START,
498
+ messageId: reasoningMessageId,
499
+ role: "reasoning",
500
+ model: chunk.model || options.model,
501
+ timestamp: Date.now()
502
+ };
503
+ yield {
504
+ type: EventType.STEP_STARTED,
505
+ stepName: stepId,
506
+ stepId,
507
+ model: chunk.model || options.model,
508
+ timestamp: Date.now(),
509
+ stepType: "thinking"
510
+ };
511
+ }
512
+ accumulatedReasoning += reasoning.text;
513
+ yield {
514
+ type: EventType.REASONING_MESSAGE_CONTENT,
515
+ messageId: reasoningMessageId,
516
+ delta: reasoning.text,
517
+ model: chunk.model || options.model,
518
+ timestamp: Date.now()
519
+ };
173
520
  }
174
521
  const choice = chunk.choices[0];
175
522
  if (!choice) continue;
@@ -177,25 +524,50 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
177
524
  const deltaContent = delta.content;
178
525
  const deltaToolCalls = delta.tool_calls;
179
526
  if (deltaContent) {
527
+ if (reasoningMessageId && !hasClosedReasoning) {
528
+ hasClosedReasoning = true;
529
+ yield {
530
+ type: EventType.REASONING_MESSAGE_END,
531
+ messageId: reasoningMessageId,
532
+ model: chunk.model || options.model,
533
+ timestamp: Date.now()
534
+ };
535
+ yield {
536
+ type: EventType.REASONING_END,
537
+ messageId: reasoningMessageId,
538
+ model: chunk.model || options.model,
539
+ timestamp: Date.now()
540
+ };
541
+ if (stepId) {
542
+ yield {
543
+ type: EventType.STEP_FINISHED,
544
+ stepName: stepId,
545
+ stepId,
546
+ model: chunk.model || options.model,
547
+ timestamp: Date.now(),
548
+ content: accumulatedReasoning
549
+ };
550
+ }
551
+ }
180
552
  if (!hasEmittedTextMessageStart) {
181
553
  hasEmittedTextMessageStart = true;
182
- yield asChunk({
183
- type: "TEXT_MESSAGE_START",
554
+ yield {
555
+ type: EventType.TEXT_MESSAGE_START,
184
556
  messageId: aguiState.messageId,
185
557
  model: chunk.model || options.model,
186
- timestamp,
558
+ timestamp: Date.now(),
187
559
  role: "assistant"
188
- });
560
+ };
189
561
  }
190
562
  accumulatedContent += deltaContent;
191
- yield asChunk({
192
- type: "TEXT_MESSAGE_CONTENT",
563
+ yield {
564
+ type: EventType.TEXT_MESSAGE_CONTENT,
193
565
  messageId: aguiState.messageId,
194
566
  model: chunk.model || options.model,
195
- timestamp,
567
+ timestamp: Date.now(),
196
568
  delta: deltaContent,
197
569
  content: accumulatedContent
198
- });
570
+ };
199
571
  }
200
572
  if (deltaToolCalls) {
201
573
  for (const toolCallDelta of deltaToolCalls) {
@@ -220,24 +592,24 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
220
592
  }
221
593
  if (toolCall.id && toolCall.name && !toolCall.started) {
222
594
  toolCall.started = true;
223
- yield asChunk({
224
- type: "TOOL_CALL_START",
595
+ yield {
596
+ type: EventType.TOOL_CALL_START,
225
597
  toolCallId: toolCall.id,
226
598
  toolCallName: toolCall.name,
227
599
  toolName: toolCall.name,
228
600
  model: chunk.model || options.model,
229
- timestamp,
601
+ timestamp: Date.now(),
230
602
  index
231
- });
603
+ };
232
604
  }
233
605
  if (toolCallDelta.function?.arguments && toolCall.started) {
234
- yield asChunk({
235
- type: "TOOL_CALL_ARGS",
606
+ yield {
607
+ type: EventType.TOOL_CALL_ARGS,
236
608
  toolCallId: toolCall.id,
237
609
  model: chunk.model || options.model,
238
- timestamp,
610
+ timestamp: Date.now(),
239
611
  delta: toolCallDelta.function.arguments
240
- });
612
+ };
241
613
  }
242
614
  }
243
615
  }
@@ -267,26 +639,26 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
267
639
  parsedInput = {};
268
640
  }
269
641
  }
270
- yield asChunk({
271
- type: "TOOL_CALL_END",
642
+ yield {
643
+ type: EventType.TOOL_CALL_END,
272
644
  toolCallId: toolCall.id,
273
645
  toolCallName: toolCall.name,
274
646
  toolName: toolCall.name,
275
647
  model: chunk.model || options.model,
276
- timestamp,
648
+ timestamp: Date.now(),
277
649
  input: parsedInput
278
- });
650
+ };
279
651
  emittedAnyToolCallEnd = true;
280
652
  }
281
653
  toolCallsInProgress.clear();
282
654
  }
283
655
  if (hasEmittedTextMessageStart) {
284
- yield asChunk({
285
- type: "TEXT_MESSAGE_END",
656
+ yield {
657
+ type: EventType.TEXT_MESSAGE_END,
286
658
  messageId: aguiState.messageId,
287
659
  model: chunk.model || options.model,
288
- timestamp
289
- });
660
+ timestamp: Date.now()
661
+ };
290
662
  hasEmittedTextMessageStart = false;
291
663
  }
292
664
  pendingFinishReason = choice.finish_reason;
@@ -301,44 +673,83 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
301
673
  try {
302
674
  const parsed = JSON.parse(toolCall.arguments);
303
675
  parsedInput = parsed && typeof parsed === "object" ? parsed : {};
304
- } catch {
676
+ } catch (parseError) {
677
+ options.logger.errors(
678
+ `${this.name}.processStreamChunks tool-args JSON parse failed (drain)`,
679
+ {
680
+ error: toRunErrorPayload(
681
+ parseError,
682
+ `tool ${toolCall.name} (${toolCall.id}) returned malformed JSON arguments`
683
+ ),
684
+ source: `${this.name}.processStreamChunks`,
685
+ toolCallId: toolCall.id,
686
+ toolName: toolCall.name,
687
+ rawArguments: toolCall.arguments
688
+ }
689
+ );
305
690
  parsedInput = {};
306
691
  }
307
692
  }
308
- yield asChunk({
309
- type: "TOOL_CALL_END",
693
+ yield {
694
+ type: EventType.TOOL_CALL_END,
310
695
  toolCallId: toolCall.id,
311
696
  toolCallName: toolCall.name,
312
697
  toolName: toolCall.name,
313
698
  model: lastModel || options.model,
314
- timestamp,
699
+ timestamp: Date.now(),
315
700
  input: parsedInput
316
- });
701
+ };
317
702
  pendingToolCount += 1;
318
703
  emittedAnyToolCallEnd = true;
319
704
  }
320
705
  toolCallsInProgress.clear();
321
706
  if (hasEmittedTextMessageStart) {
322
- yield asChunk({
323
- type: "TEXT_MESSAGE_END",
707
+ yield {
708
+ type: EventType.TEXT_MESSAGE_END,
324
709
  messageId: aguiState.messageId,
325
710
  model: lastModel || options.model,
326
- timestamp
327
- });
711
+ timestamp: Date.now()
712
+ };
713
+ }
714
+ if (reasoningMessageId && !hasClosedReasoning) {
715
+ hasClosedReasoning = true;
716
+ yield {
717
+ type: EventType.REASONING_MESSAGE_END,
718
+ messageId: reasoningMessageId,
719
+ model: lastModel || options.model,
720
+ timestamp: Date.now()
721
+ };
722
+ yield {
723
+ type: EventType.REASONING_END,
724
+ messageId: reasoningMessageId,
725
+ model: lastModel || options.model,
726
+ timestamp: Date.now()
727
+ };
728
+ if (stepId) {
729
+ yield {
730
+ type: EventType.STEP_FINISHED,
731
+ stepName: stepId,
732
+ stepId,
733
+ model: lastModel || options.model,
734
+ timestamp: Date.now(),
735
+ content: accumulatedReasoning
736
+ };
737
+ }
328
738
  }
329
- const finishReason = emittedAnyToolCallEnd ? "tool_calls" : pendingFinishReason === "tool_calls" ? "stop" : pendingFinishReason ?? "stop";
330
- yield asChunk({
331
- type: "RUN_FINISHED",
739
+ const finishReason = emittedAnyToolCallEnd ? "tool_calls" : pendingFinishReason === "tool_calls" ? "stop" : pendingFinishReason === "function_call" ? "tool_calls" : pendingFinishReason ?? "stop";
740
+ yield {
741
+ type: EventType.RUN_FINISHED,
332
742
  runId: aguiState.runId,
743
+ threadId: aguiState.threadId,
333
744
  model: lastModel || options.model,
334
- timestamp,
745
+ timestamp: Date.now(),
335
746
  usage: lastUsage ? {
336
747
  promptTokens: lastUsage.prompt_tokens || 0,
337
748
  completionTokens: lastUsage.completion_tokens || 0,
338
749
  totalTokens: lastUsage.total_tokens || 0
339
750
  } : void 0,
340
751
  finishReason
341
- });
752
+ };
342
753
  }
343
754
  } catch (error) {
344
755
  const errorPayload = toRunErrorPayload(
@@ -349,13 +760,14 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
349
760
  error: errorPayload,
350
761
  source: `${this.name}.processStreamChunks`
351
762
  });
352
- yield asChunk({
353
- type: "RUN_ERROR",
354
- runId: aguiState.runId,
763
+ yield {
764
+ type: EventType.RUN_ERROR,
355
765
  model: options.model,
356
- timestamp,
766
+ timestamp: Date.now(),
767
+ message: errorPayload.message,
768
+ code: errorPayload.code,
357
769
  error: errorPayload
358
- });
770
+ };
359
771
  }
360
772
  }
361
773
  /**
@@ -509,6 +921,6 @@ class OpenAICompatibleChatCompletionsTextAdapter extends BaseTextAdapter {
509
921
  }
510
922
  }
511
923
  export {
512
- OpenAICompatibleChatCompletionsTextAdapter
924
+ OpenAIBaseChatCompletionsTextAdapter
513
925
  };
514
926
  //# sourceMappingURL=chat-completions-text.js.map