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