@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.
- package/README.md +121 -0
- package/dist/esm/adapters/chat-completions-text.d.ts +49 -21
- package/dist/esm/adapters/chat-completions-text.js +480 -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 +661 -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 +605 -117
- package/src/adapters/chat-completions-tool-converter.ts +9 -5
- package/src/adapters/responses-text.ts +869 -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,23 @@ 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
|
+
parentRunId: options.parentRunId
|
|
51
|
+
};
|
|
52
52
|
}
|
|
53
|
-
yield
|
|
54
|
-
type:
|
|
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 =
|
|
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
|
|
168
|
-
type:
|
|
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
|
|
183
|
-
type:
|
|
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
|
|
192
|
-
type:
|
|
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
|
|
224
|
-
type:
|
|
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
|
|
235
|
-
type:
|
|
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
|
|
271
|
-
type:
|
|
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
|
|
285
|
-
type:
|
|
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
|
|
309
|
-
type:
|
|
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
|
|
323
|
-
type:
|
|
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
|
|
331
|
-
type:
|
|
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
|
|
353
|
-
type:
|
|
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
|
-
|
|
924
|
+
OpenAIBaseChatCompletionsTextAdapter
|
|
513
925
|
};
|
|
514
926
|
//# sourceMappingURL=chat-completions-text.js.map
|