@standardagents/openrouter 0.14.1 → 0.15.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/dist/index.d.ts +25 -1
- package/dist/index.js +1440 -1116
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,98 +1,231 @@
|
|
|
1
1
|
// src/OpenRouterProvider.ts
|
|
2
|
-
import { ProviderError } from "@standardagents/spec";
|
|
2
|
+
import { ProviderError, defineTool } from "@standardagents/spec";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
5
|
+
// src/server-tools.ts
|
|
6
|
+
var OPENROUTER_SERVER_TOOL_NAMES = [
|
|
7
|
+
"web_search",
|
|
8
|
+
"web_fetch",
|
|
9
|
+
"datetime",
|
|
10
|
+
"image_generation"
|
|
11
|
+
];
|
|
12
|
+
function isOpenRouterServerToolName(name) {
|
|
13
|
+
return OPENROUTER_SERVER_TOOL_NAMES.includes(name);
|
|
14
|
+
}
|
|
15
|
+
function isOpenRouterServerTool(value) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
17
|
+
const type = value.type;
|
|
18
|
+
return typeof type === "string" && type.startsWith("openrouter:") && isOpenRouterServerToolName(type.slice("openrouter:".length));
|
|
19
|
+
}
|
|
20
|
+
function isOpenRouterProviderTool(tool) {
|
|
21
|
+
return tool.executionMode === "provider" && tool.executionProvider === "openrouter" && isOpenRouterServerToolName(tool.function.name);
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
}
|
|
26
|
+
function readNumber(value) {
|
|
27
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
28
|
+
}
|
|
29
|
+
function getServerToolParameters(providerOptions, toolName) {
|
|
30
|
+
const rawConfig = providerOptions?.serverTools ?? providerOptions?.server_tools;
|
|
31
|
+
if (!isRecord(rawConfig)) return void 0;
|
|
32
|
+
const rawParameters = rawConfig[toolName];
|
|
33
|
+
if (!isRecord(rawParameters) || Object.keys(rawParameters).length === 0) {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
return rawParameters;
|
|
37
|
+
}
|
|
38
|
+
function toOpenRouterServerTool(name, parameters) {
|
|
39
|
+
return {
|
|
40
|
+
type: `openrouter:${name}`,
|
|
41
|
+
...parameters ? { parameters } : {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function splitOpenRouterProviderOptions(providerOptions) {
|
|
45
|
+
if (!providerOptions) return void 0;
|
|
46
|
+
const {
|
|
47
|
+
serverTools: _serverTools,
|
|
48
|
+
server_tools: _serverToolsSnake,
|
|
49
|
+
_metadata,
|
|
50
|
+
...rest
|
|
51
|
+
} = providerOptions;
|
|
52
|
+
return Object.keys(rest).length > 0 ? rest : void 0;
|
|
53
|
+
}
|
|
54
|
+
function serverToolNameFromUsageKey(key) {
|
|
55
|
+
if (!key.endsWith("_requests")) return void 0;
|
|
56
|
+
const name = key.slice(0, -"_requests".length);
|
|
57
|
+
return isOpenRouterServerToolName(name) ? name : void 0;
|
|
58
|
+
}
|
|
59
|
+
function extractServerToolUse(usage) {
|
|
60
|
+
if (!isRecord(usage)) return void 0;
|
|
61
|
+
const raw = usage.server_tool_use ?? usage.serverToolUse;
|
|
62
|
+
return isRecord(raw) ? raw : void 0;
|
|
63
|
+
}
|
|
64
|
+
function providerToolResultsFromServerToolUse(usage) {
|
|
65
|
+
const serverToolUse = extractServerToolUse(usage);
|
|
66
|
+
if (!serverToolUse) return void 0;
|
|
67
|
+
const results = [];
|
|
68
|
+
for (const [key, value] of Object.entries(serverToolUse)) {
|
|
69
|
+
const name = serverToolNameFromUsageKey(key);
|
|
70
|
+
const count = typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
71
|
+
if (!name || count === 0) continue;
|
|
72
|
+
for (let index = 0; index < count; index += 1) {
|
|
73
|
+
results.push({
|
|
74
|
+
name,
|
|
75
|
+
type: name,
|
|
76
|
+
provider: "openrouter",
|
|
77
|
+
id: `openrouter:${name}:${index + 1}`,
|
|
78
|
+
status: "completed",
|
|
79
|
+
metadata: {
|
|
80
|
+
usageKey: key,
|
|
81
|
+
index: index + 1,
|
|
82
|
+
total: count
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return results.length > 0 ? results : void 0;
|
|
88
|
+
}
|
|
89
|
+
function extractWebSearchCitations(value) {
|
|
90
|
+
if (!Array.isArray(value)) return [];
|
|
91
|
+
const citations = [];
|
|
92
|
+
for (const annotation of value) {
|
|
93
|
+
if (!isRecord(annotation)) continue;
|
|
94
|
+
const rawCitation = annotation.url_citation ?? annotation.urlCitation;
|
|
95
|
+
if (!isRecord(rawCitation)) continue;
|
|
96
|
+
const url = rawCitation.url;
|
|
97
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
98
|
+
citations.push({
|
|
99
|
+
type: typeof annotation.type === "string" ? annotation.type : void 0,
|
|
100
|
+
url,
|
|
101
|
+
title: typeof rawCitation.title === "string" ? rawCitation.title : void 0,
|
|
102
|
+
content: typeof rawCitation.content === "string" ? rawCitation.content : void 0,
|
|
103
|
+
startIndex: readNumber(rawCitation.start_index ?? rawCitation.startIndex),
|
|
104
|
+
endIndex: readNumber(rawCitation.end_index ?? rawCitation.endIndex)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return citations;
|
|
108
|
+
}
|
|
109
|
+
function providerToolResultFromWebSearchCitations(citations, responseId) {
|
|
110
|
+
if (citations.length === 0) return void 0;
|
|
111
|
+
return {
|
|
112
|
+
name: "web_search",
|
|
113
|
+
type: "web_search",
|
|
114
|
+
provider: "openrouter",
|
|
115
|
+
id: responseId ? `openrouter:web_search:${responseId}` : "openrouter:web_search:annotations",
|
|
116
|
+
status: "completed",
|
|
117
|
+
result: {
|
|
118
|
+
actions: [
|
|
119
|
+
{
|
|
120
|
+
type: "search",
|
|
121
|
+
sources: citations.map((citation) => ({
|
|
122
|
+
type: citation.type,
|
|
123
|
+
url: citation.url,
|
|
124
|
+
title: citation.title
|
|
125
|
+
}))
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
metadata: {
|
|
130
|
+
source: "annotations",
|
|
131
|
+
citationCount: citations.length
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function providerToolResultsFromServerToolUseOrCitations(usage, citations, responseId) {
|
|
136
|
+
const usageResults = providerToolResultsFromServerToolUse(usage);
|
|
137
|
+
const annotationResult = providerToolResultFromWebSearchCitations(citations, responseId);
|
|
138
|
+
if (usageResults && usageResults.length > 0) {
|
|
139
|
+
if (annotationResult && !usageResults.some((tool) => tool.name === "web_search")) {
|
|
140
|
+
return [...usageResults, annotationResult];
|
|
141
|
+
}
|
|
142
|
+
return usageResults;
|
|
143
|
+
}
|
|
144
|
+
return annotationResult ? [annotationResult] : void 0;
|
|
145
|
+
}
|
|
146
|
+
function providerToolDoneChunksFromServerToolUseOrCitations(usage, citations, responseId) {
|
|
147
|
+
return (providerToolResultsFromServerToolUseOrCitations(usage, citations, responseId) ?? []).map((tool) => ({
|
|
148
|
+
type: "provider-tool-done",
|
|
149
|
+
tool
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/chat-completions-transformers.ts
|
|
154
|
+
function transformChatContentPart(part) {
|
|
6
155
|
if (part.type === "text") {
|
|
7
|
-
return { type: "
|
|
156
|
+
return { type: "text", text: part.text };
|
|
8
157
|
}
|
|
9
158
|
if (part.type === "image") {
|
|
10
159
|
const data = part.data || "";
|
|
11
160
|
const imageUrl = data.startsWith("data:") ? data : `data:${part.mediaType || "image/png"};base64,${data}`;
|
|
12
161
|
return {
|
|
13
|
-
type: "
|
|
14
|
-
image_url:
|
|
15
|
-
|
|
162
|
+
type: "image_url",
|
|
163
|
+
image_url: {
|
|
164
|
+
url: imageUrl,
|
|
165
|
+
detail: part.detail || "auto"
|
|
166
|
+
}
|
|
16
167
|
};
|
|
17
168
|
}
|
|
18
169
|
if (part.type === "image_url") {
|
|
19
|
-
const url = part.image_url?.url || "";
|
|
20
|
-
const detail = part.image_url?.detail || "auto";
|
|
21
170
|
return {
|
|
22
|
-
type: "
|
|
23
|
-
image_url:
|
|
24
|
-
|
|
171
|
+
type: "image_url",
|
|
172
|
+
image_url: {
|
|
173
|
+
url: part.image_url?.url || "",
|
|
174
|
+
detail: part.image_url?.detail || "auto"
|
|
175
|
+
}
|
|
25
176
|
};
|
|
26
177
|
}
|
|
27
178
|
return {
|
|
28
|
-
type: "
|
|
179
|
+
type: "text",
|
|
29
180
|
text: `[File: ${part.filename || "file"}]`
|
|
30
181
|
};
|
|
31
182
|
}
|
|
32
|
-
function
|
|
183
|
+
function transformChatMessageContent(content) {
|
|
33
184
|
if (typeof content === "string") {
|
|
34
185
|
return content;
|
|
35
186
|
}
|
|
36
|
-
return content.map(
|
|
187
|
+
return content.map(transformChatContentPart);
|
|
37
188
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
189
|
+
function transformChatSystemMessage(msg) {
|
|
190
|
+
return {
|
|
191
|
+
role: "system",
|
|
192
|
+
content: msg.content
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function transformChatUserMessage(msg) {
|
|
40
196
|
return {
|
|
41
197
|
role: "user",
|
|
42
|
-
content:
|
|
198
|
+
content: transformChatMessageContent(msg.content)
|
|
43
199
|
};
|
|
44
200
|
}
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let encryptedContent;
|
|
51
|
-
for (const detail of reasoningDetails) {
|
|
52
|
-
if (detail.type === "reasoning.text") {
|
|
53
|
-
summaryTexts.push({ type: "summary_text", text: detail.text });
|
|
54
|
-
} else if (detail.type === "reasoning.encrypted") {
|
|
55
|
-
encryptedContent = detail.text;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (summaryTexts.length > 0) {
|
|
59
|
-
const reasoningItem = {
|
|
60
|
-
type: "reasoning",
|
|
61
|
-
id: "",
|
|
62
|
-
summary: summaryTexts,
|
|
63
|
-
...encryptedContent ? { encrypted_content: encryptedContent } : {}
|
|
64
|
-
};
|
|
65
|
-
items.push(reasoningItem);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (msg.content) {
|
|
69
|
-
const outputMessage = {
|
|
70
|
-
type: "message",
|
|
71
|
-
role: "assistant",
|
|
72
|
-
id: "",
|
|
73
|
-
status: "completed",
|
|
74
|
-
content: [{ type: "output_text", text: msg.content, annotations: [] }]
|
|
75
|
-
};
|
|
76
|
-
items.push(outputMessage);
|
|
77
|
-
}
|
|
201
|
+
function transformChatAssistantMessage(msg) {
|
|
202
|
+
const message = {
|
|
203
|
+
role: "assistant",
|
|
204
|
+
content: msg.content || null
|
|
205
|
+
};
|
|
78
206
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Unique output item ID (prefixed to distinguish from call_id)
|
|
84
|
-
call_id: tc.id,
|
|
207
|
+
message.tool_calls = msg.toolCalls.map((tc) => ({
|
|
208
|
+
id: tc.id,
|
|
209
|
+
type: "function",
|
|
210
|
+
function: {
|
|
85
211
|
name: tc.name,
|
|
86
|
-
arguments: JSON.stringify(tc.arguments)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
90
|
-
items.push(functionCall);
|
|
91
|
-
}
|
|
212
|
+
arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments)
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
92
215
|
}
|
|
93
|
-
|
|
216
|
+
if (msg.reasoningDetails && msg.reasoningDetails.length > 0) {
|
|
217
|
+
message.reasoning_details = msg.reasoningDetails.map((detail) => {
|
|
218
|
+
if (detail.type === "text") {
|
|
219
|
+
return { type: "reasoning.text", text: detail.text, format: detail.format };
|
|
220
|
+
} else if (detail.type === "summary") {
|
|
221
|
+
return { type: "reasoning.summary", summary: detail.text, format: detail.format };
|
|
222
|
+
}
|
|
223
|
+
return { type: "reasoning.encrypted", data: detail.data, id: detail.id, format: detail.format };
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return message;
|
|
94
227
|
}
|
|
95
|
-
function
|
|
228
|
+
function transformChatToolMessage(msg) {
|
|
96
229
|
let output;
|
|
97
230
|
if (typeof msg.content === "string") {
|
|
98
231
|
output = msg.content;
|
|
@@ -108,13 +241,13 @@ function transformToolMessage(msg) {
|
|
|
108
241
|
output = JSON.stringify(msg.content);
|
|
109
242
|
}
|
|
110
243
|
const imageAttachments = msg.attachments?.filter((a) => a.type === "image" && a.data) || [];
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
244
|
+
const toolMessage = {
|
|
245
|
+
role: "tool",
|
|
246
|
+
tool_call_id: msg.toolCallId,
|
|
247
|
+
content: output || (imageAttachments.length > 0 ? "Success" : "")
|
|
115
248
|
};
|
|
116
249
|
if (imageAttachments.length === 0) {
|
|
117
|
-
return {
|
|
250
|
+
return { toolMessage };
|
|
118
251
|
}
|
|
119
252
|
const imageContent = [];
|
|
120
253
|
const toolName = msg.toolName || "the tool";
|
|
@@ -122,87 +255,111 @@ function transformToolMessage(msg) {
|
|
|
122
255
|
const descriptor = isSingle ? "the file" : `${imageAttachments.length} files`;
|
|
123
256
|
const verb = isSingle ? "is" : "are";
|
|
124
257
|
imageContent.push({
|
|
125
|
-
type: "
|
|
258
|
+
type: "text",
|
|
126
259
|
text: `Here ${verb} ${descriptor} from ${toolName}:`
|
|
127
260
|
});
|
|
128
261
|
for (const attachment of imageAttachments) {
|
|
129
262
|
const attachmentData = attachment.data || "";
|
|
130
263
|
const imageData = attachmentData.startsWith("data:") ? attachmentData : `data:${attachment.mediaType || "image/png"};base64,${attachmentData}`;
|
|
131
264
|
imageContent.push({
|
|
132
|
-
type: "
|
|
133
|
-
image_url:
|
|
134
|
-
|
|
265
|
+
type: "image_url",
|
|
266
|
+
image_url: {
|
|
267
|
+
url: imageData,
|
|
268
|
+
detail: "auto"
|
|
269
|
+
}
|
|
135
270
|
});
|
|
136
271
|
}
|
|
137
272
|
const syntheticUserMessage = {
|
|
138
273
|
role: "user",
|
|
139
274
|
content: imageContent
|
|
140
275
|
};
|
|
141
|
-
return {
|
|
276
|
+
return { toolMessage, syntheticUserMessage };
|
|
142
277
|
}
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
const input = [];
|
|
278
|
+
function transformChatMessages(messages) {
|
|
279
|
+
const result = [];
|
|
146
280
|
for (const msg of messages) {
|
|
147
281
|
switch (msg.role) {
|
|
148
282
|
case "system":
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
${msg.content}` : msg.content;
|
|
152
|
-
input.push({
|
|
153
|
-
type: "message",
|
|
154
|
-
role: "system",
|
|
155
|
-
content: msg.content
|
|
156
|
-
});
|
|
283
|
+
result.push(transformChatSystemMessage(msg));
|
|
157
284
|
break;
|
|
158
285
|
case "user":
|
|
159
|
-
|
|
286
|
+
result.push(transformChatUserMessage(msg));
|
|
160
287
|
break;
|
|
161
288
|
case "assistant":
|
|
162
|
-
|
|
289
|
+
result.push(transformChatAssistantMessage(msg));
|
|
163
290
|
break;
|
|
164
291
|
case "tool": {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
|
|
292
|
+
const { toolMessage, syntheticUserMessage } = transformChatToolMessage(msg);
|
|
293
|
+
result.push(toolMessage);
|
|
294
|
+
if (syntheticUserMessage) {
|
|
295
|
+
result.push(syntheticUserMessage);
|
|
169
296
|
}
|
|
170
297
|
break;
|
|
171
298
|
}
|
|
172
299
|
}
|
|
173
300
|
}
|
|
174
|
-
return
|
|
301
|
+
return result;
|
|
175
302
|
}
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
303
|
+
function isRecord2(value) {
|
|
304
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
305
|
+
}
|
|
306
|
+
function normalizeStrictJsonSchema(schema) {
|
|
307
|
+
if (Array.isArray(schema)) {
|
|
308
|
+
return schema.map((item) => normalizeStrictJsonSchema(item));
|
|
309
|
+
}
|
|
310
|
+
if (!isRecord2(schema)) {
|
|
311
|
+
return schema;
|
|
312
|
+
}
|
|
313
|
+
const normalized = {};
|
|
314
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
315
|
+
normalized[key] = normalizeStrictJsonSchema(value);
|
|
316
|
+
}
|
|
317
|
+
const properties = isRecord2(normalized.properties) ? normalized.properties : void 0;
|
|
318
|
+
const type = normalized.type;
|
|
319
|
+
const isObjectSchema = type === "object" || Array.isArray(type) && type.includes("object") || !!properties;
|
|
320
|
+
if (isObjectSchema) {
|
|
321
|
+
if (normalized.type === void 0) {
|
|
322
|
+
normalized.type = "object";
|
|
323
|
+
}
|
|
324
|
+
normalized.properties = properties || {};
|
|
325
|
+
normalized.required = Object.keys(normalized.properties);
|
|
326
|
+
normalized.additionalProperties = false;
|
|
327
|
+
}
|
|
328
|
+
return normalized;
|
|
329
|
+
}
|
|
330
|
+
function normalizeStrictToolParameters(parameters) {
|
|
331
|
+
const base = isRecord2(parameters) ? { ...parameters } : {};
|
|
332
|
+
if (base.type === void 0) {
|
|
333
|
+
base.type = "object";
|
|
191
334
|
}
|
|
335
|
+
if (base.properties === void 0) {
|
|
336
|
+
base.properties = {};
|
|
337
|
+
}
|
|
338
|
+
return normalizeStrictJsonSchema(base);
|
|
339
|
+
}
|
|
340
|
+
function transformChatTool(tool, providerOptions) {
|
|
341
|
+
if (isOpenRouterProviderTool(tool)) {
|
|
342
|
+
const parameters2 = getServerToolParameters(providerOptions, tool.function.name);
|
|
343
|
+
return toOpenRouterServerTool(tool.function.name, parameters2);
|
|
344
|
+
}
|
|
345
|
+
const inputParams = tool.function.parameters;
|
|
346
|
+
const parameters = normalizeStrictToolParameters(inputParams);
|
|
192
347
|
return {
|
|
193
348
|
type: "function",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
349
|
+
function: {
|
|
350
|
+
name: tool.function.name,
|
|
351
|
+
description: tool.function.description || void 0,
|
|
352
|
+
parameters,
|
|
353
|
+
strict: true
|
|
354
|
+
}
|
|
198
355
|
};
|
|
199
356
|
}
|
|
200
|
-
function
|
|
201
|
-
return tools.map(
|
|
357
|
+
function transformChatTools(tools, providerOptions) {
|
|
358
|
+
return tools.map((tool) => transformChatTool(tool, providerOptions));
|
|
202
359
|
}
|
|
203
|
-
function
|
|
360
|
+
function transformChatToolChoice(choice) {
|
|
204
361
|
if (choice === "auto") {
|
|
205
|
-
return
|
|
362
|
+
return void 0;
|
|
206
363
|
}
|
|
207
364
|
if (choice === "none") {
|
|
208
365
|
return "none";
|
|
@@ -211,109 +368,44 @@ function transformToolChoice(choice) {
|
|
|
211
368
|
return "required";
|
|
212
369
|
}
|
|
213
370
|
if (typeof choice === "object" && "name" in choice) {
|
|
214
|
-
return { type: "function", name: choice.name };
|
|
371
|
+
return { type: "function", function: { name: choice.name } };
|
|
215
372
|
}
|
|
216
|
-
return
|
|
373
|
+
return void 0;
|
|
217
374
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
375
|
+
function mapChatFinishReason(finishReason) {
|
|
376
|
+
switch (finishReason) {
|
|
377
|
+
case "stop":
|
|
378
|
+
return "stop";
|
|
379
|
+
case "tool_calls":
|
|
380
|
+
return "tool_calls";
|
|
381
|
+
case "length":
|
|
224
382
|
return "length";
|
|
225
|
-
|
|
226
|
-
if (response.incompleteDetails?.reason === "content_filter") {
|
|
383
|
+
case "content_filter":
|
|
227
384
|
return "content_filter";
|
|
228
|
-
|
|
385
|
+
case "error":
|
|
386
|
+
return "error";
|
|
387
|
+
default:
|
|
388
|
+
return "stop";
|
|
229
389
|
}
|
|
230
|
-
const hasToolCalls = response.output.some(
|
|
231
|
-
(item) => item.type === "function_call"
|
|
232
|
-
);
|
|
233
|
-
if (hasToolCalls) {
|
|
234
|
-
return "tool_calls";
|
|
235
|
-
}
|
|
236
|
-
return "stop";
|
|
237
|
-
}
|
|
238
|
-
function extractTextContent(output) {
|
|
239
|
-
const textParts = [];
|
|
240
|
-
for (const item of output) {
|
|
241
|
-
if (item.type === "message" && item.role === "assistant") {
|
|
242
|
-
for (const content of item.content) {
|
|
243
|
-
if (content.type === "output_text") {
|
|
244
|
-
textParts.push(content.text);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return textParts.length > 0 ? textParts.join("") : null;
|
|
250
390
|
}
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (item.type === "function_call") {
|
|
255
|
-
let parsedArgs = {};
|
|
256
|
-
try {
|
|
257
|
-
parsedArgs = item.arguments ? JSON.parse(item.arguments) : {};
|
|
258
|
-
} catch {
|
|
259
|
-
}
|
|
260
|
-
toolCalls.push({
|
|
261
|
-
id: item.callId,
|
|
262
|
-
name: item.name,
|
|
263
|
-
arguments: parsedArgs
|
|
264
|
-
});
|
|
265
|
-
}
|
|
391
|
+
function extractChatToolCalls(toolCalls) {
|
|
392
|
+
if (!toolCalls || toolCalls.length === 0) {
|
|
393
|
+
return void 0;
|
|
266
394
|
}
|
|
267
|
-
return toolCalls.
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const itemAny = item;
|
|
273
|
-
if (itemAny.type === "image_generation_call" && itemAny.result) {
|
|
274
|
-
const imageData = itemAny.result;
|
|
275
|
-
const isDataUrl = imageData.startsWith("data:");
|
|
276
|
-
const isHttpUrl = imageData.startsWith("http://") || imageData.startsWith("https://");
|
|
277
|
-
let mediaType = "image/png";
|
|
278
|
-
if (isDataUrl) {
|
|
279
|
-
const match = imageData.match(/^data:([^;,]+)/);
|
|
280
|
-
if (match) {
|
|
281
|
-
mediaType = match[1];
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
images.push({
|
|
285
|
-
id: itemAny.id || void 0,
|
|
286
|
-
data: imageData,
|
|
287
|
-
mediaType,
|
|
288
|
-
// Include revised prompt if available
|
|
289
|
-
revisedPrompt: itemAny.revised_prompt || itemAny.revisedPrompt || void 0
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
if (itemAny.type === "message" && itemAny.content && Array.isArray(itemAny.content)) {
|
|
293
|
-
for (const content of itemAny.content) {
|
|
294
|
-
if (content.type === "output_image" || content.type === "image") {
|
|
295
|
-
const imageData = content.image || content.data || content.url || content.image_url;
|
|
296
|
-
if (imageData) {
|
|
297
|
-
let mediaType = content.media_type || content.mediaType || "image/png";
|
|
298
|
-
if (typeof imageData === "string" && imageData.startsWith("data:")) {
|
|
299
|
-
const match = imageData.match(/^data:([^;,]+)/);
|
|
300
|
-
if (match) {
|
|
301
|
-
mediaType = match[1];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
images.push({
|
|
305
|
-
id: content.id || void 0,
|
|
306
|
-
data: imageData,
|
|
307
|
-
mediaType
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
395
|
+
return toolCalls.map((tc) => {
|
|
396
|
+
let parsedArgs = {};
|
|
397
|
+
try {
|
|
398
|
+
parsedArgs = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
|
|
399
|
+
} catch {
|
|
312
400
|
}
|
|
313
|
-
|
|
314
|
-
|
|
401
|
+
return {
|
|
402
|
+
id: tc.id,
|
|
403
|
+
name: tc.function.name,
|
|
404
|
+
arguments: parsedArgs
|
|
405
|
+
};
|
|
406
|
+
});
|
|
315
407
|
}
|
|
316
|
-
function
|
|
408
|
+
function transformChatUsage(usage, actualProvider) {
|
|
317
409
|
if (!usage) {
|
|
318
410
|
return {
|
|
319
411
|
promptTokens: 0,
|
|
@@ -321,32 +413,69 @@ function transformUsage(usage, actualProvider) {
|
|
|
321
413
|
totalTokens: 0
|
|
322
414
|
};
|
|
323
415
|
}
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const totalTokens = usage.total_tokens ||
|
|
327
|
-
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || usage.outputTokensDetails?.reasoningTokens;
|
|
328
|
-
const cachedTokens = usage.input_tokens_details?.cached_tokens || usage.inputTokensDetails?.cachedTokens;
|
|
416
|
+
const promptTokens = usage.native_tokens_prompt || usage.prompt_tokens || 0;
|
|
417
|
+
const completionTokens = usage.native_tokens_completion || usage.completion_tokens || 0;
|
|
418
|
+
const totalTokens = usage.total_tokens || promptTokens + completionTokens;
|
|
329
419
|
return {
|
|
330
|
-
promptTokens
|
|
331
|
-
completionTokens
|
|
332
|
-
totalTokens
|
|
333
|
-
reasoningTokens,
|
|
334
|
-
cachedTokens,
|
|
420
|
+
promptTokens,
|
|
421
|
+
completionTokens,
|
|
422
|
+
totalTokens,
|
|
423
|
+
reasoningTokens: usage.completion_tokens_details?.reasoning_tokens,
|
|
424
|
+
cachedTokens: usage.prompt_tokens_details?.cached_tokens,
|
|
335
425
|
cost: usage.cost,
|
|
336
426
|
provider: actualProvider
|
|
337
427
|
};
|
|
338
428
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
429
|
+
function extractImageFromUrl(url, index) {
|
|
430
|
+
if (url.startsWith("data:")) {
|
|
431
|
+
const match = url.match(/^data:([^;]+);base64,(.+)$/);
|
|
432
|
+
if (match) {
|
|
433
|
+
return {
|
|
434
|
+
id: `image_${index}`,
|
|
435
|
+
data: match[2],
|
|
436
|
+
// base64 data without prefix
|
|
437
|
+
mediaType: match[1]
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
id: `image_${index}`,
|
|
442
|
+
data: url,
|
|
443
|
+
mediaType: "image/png"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
id: `image_${index}`,
|
|
448
|
+
data: url,
|
|
449
|
+
mediaType: "image/png"
|
|
450
|
+
// Default; actual type unknown without fetching
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function extractChatImages(images) {
|
|
454
|
+
if (!images || images.length === 0) {
|
|
455
|
+
return void 0;
|
|
456
|
+
}
|
|
457
|
+
return images.map((img, index) => extractImageFromUrl(img.image_url.url, index));
|
|
458
|
+
}
|
|
459
|
+
function transformChatResponse(response) {
|
|
460
|
+
const choice = response.choices?.[0];
|
|
461
|
+
const message = choice?.message;
|
|
462
|
+
const content = message?.content || null;
|
|
463
|
+
const toolCalls = extractChatToolCalls(message?.tool_calls);
|
|
464
|
+
const images = extractChatImages(message?.images);
|
|
465
|
+
const webSearchCitations = extractWebSearchCitations(message?.annotations);
|
|
466
|
+
const providerTools = providerToolResultsFromServerToolUseOrCitations(
|
|
467
|
+
response.usage,
|
|
468
|
+
webSearchCitations,
|
|
469
|
+
response.id
|
|
470
|
+
);
|
|
343
471
|
const actualProvider = response.model?.split("/")[0] || void 0;
|
|
344
472
|
return {
|
|
345
473
|
content,
|
|
346
474
|
toolCalls,
|
|
347
475
|
images,
|
|
348
|
-
|
|
349
|
-
|
|
476
|
+
providerTools,
|
|
477
|
+
finishReason: mapChatFinishReason(choice?.finish_reason),
|
|
478
|
+
usage: transformChatUsage(response.usage, actualProvider),
|
|
350
479
|
metadata: {
|
|
351
480
|
model: response.model,
|
|
352
481
|
provider: "openrouter",
|
|
@@ -355,373 +484,271 @@ function transformResponse(response) {
|
|
|
355
484
|
}
|
|
356
485
|
};
|
|
357
486
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
if (details.length === 0 && itemAny.encrypted_content) {
|
|
372
|
-
details.push({ type: "reasoning.encrypted", text: itemAny.encrypted_content });
|
|
373
|
-
}
|
|
374
|
-
if (itemAny.content) {
|
|
375
|
-
if (typeof itemAny.content === "string") {
|
|
376
|
-
details.push({ type: "reasoning.text", text: itemAny.content });
|
|
377
|
-
} else if (Array.isArray(itemAny.content)) {
|
|
378
|
-
for (const part of itemAny.content) {
|
|
379
|
-
if (part.text) {
|
|
380
|
-
details.push({ type: "reasoning.text", text: part.text });
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (itemAny.type === "message" && itemAny.content && Array.isArray(itemAny.content)) {
|
|
387
|
-
for (const part of itemAny.content) {
|
|
388
|
-
if (part.type === "reasoning" || part.type === "reasoning_text") {
|
|
389
|
-
details.push({ type: "reasoning.text", text: part.text || "" });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
if (details.length === 0 && streamedReasoningContent) {
|
|
396
|
-
details.push({ type: "reasoning.text", text: streamedReasoningContent });
|
|
397
|
-
}
|
|
398
|
-
return details.length > 0 ? details : void 0;
|
|
399
|
-
}
|
|
400
|
-
function buildCreateParams(request) {
|
|
401
|
-
const { input, instructions } = transformMessages(request.messages);
|
|
402
|
-
const params = {
|
|
403
|
-
model: request.model,
|
|
404
|
-
input,
|
|
405
|
-
store: false
|
|
406
|
-
// Always stateless
|
|
487
|
+
function createChatStreamState() {
|
|
488
|
+
return {
|
|
489
|
+
content: "",
|
|
490
|
+
toolCalls: /* @__PURE__ */ new Map(),
|
|
491
|
+
images: [],
|
|
492
|
+
reasoningContent: "",
|
|
493
|
+
hasContent: false,
|
|
494
|
+
hasReasoning: false,
|
|
495
|
+
finishReason: null,
|
|
496
|
+
reasoningDetails: [],
|
|
497
|
+
webSearchCitations: []
|
|
407
498
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
499
|
+
}
|
|
500
|
+
function accumulateReasoningDetail(state, detail) {
|
|
501
|
+
const idx = detail.index ?? 0;
|
|
502
|
+
const textContent = detail.text || detail.summary || "";
|
|
503
|
+
const detailType = detail.type === "reasoning.summary" ? "summary" : "text";
|
|
504
|
+
const existing = state.reasoningDetails.find(
|
|
505
|
+
(d) => d.type === detailType && d._index === idx
|
|
506
|
+
);
|
|
507
|
+
if (existing && existing.text !== void 0) {
|
|
508
|
+
existing.text += textContent;
|
|
509
|
+
} else {
|
|
510
|
+
state.reasoningDetails.push({
|
|
511
|
+
type: detailType,
|
|
512
|
+
text: textContent,
|
|
513
|
+
format: detail.format,
|
|
514
|
+
_index: idx
|
|
515
|
+
});
|
|
419
516
|
}
|
|
420
|
-
|
|
421
|
-
|
|
517
|
+
}
|
|
518
|
+
function processChatStreamChunk(chunk, state) {
|
|
519
|
+
const chunks = [];
|
|
520
|
+
const choice = chunk.choices?.[0];
|
|
521
|
+
const delta = choice?.delta;
|
|
522
|
+
if (!delta && !choice?.finish_reason && !chunk.usage) {
|
|
523
|
+
return chunks;
|
|
422
524
|
}
|
|
423
|
-
if (
|
|
424
|
-
|
|
525
|
+
if (delta?.content) {
|
|
526
|
+
state.hasContent = true;
|
|
527
|
+
state.content += delta.content;
|
|
528
|
+
chunks.push({ type: "content-delta", delta: delta.content });
|
|
529
|
+
}
|
|
530
|
+
const reasoningText = delta?.reasoning ?? delta?.reasoning_content;
|
|
531
|
+
if (reasoningText) {
|
|
532
|
+
state.hasReasoning = true;
|
|
533
|
+
state.reasoningContent += reasoningText;
|
|
534
|
+
chunks.push({ type: "reasoning-delta", delta: reasoningText });
|
|
535
|
+
}
|
|
536
|
+
if (delta?.reasoning_details && Array.isArray(delta.reasoning_details)) {
|
|
537
|
+
for (const detail of delta.reasoning_details) {
|
|
538
|
+
if (detail.type === "reasoning.text") {
|
|
539
|
+
accumulateReasoningDetail(state, detail);
|
|
540
|
+
} else if (detail.type === "reasoning.summary") {
|
|
541
|
+
accumulateReasoningDetail(state, detail);
|
|
542
|
+
} else if (detail.type === "reasoning.encrypted") {
|
|
543
|
+
state.reasoningDetails.push({
|
|
544
|
+
type: "encrypted",
|
|
545
|
+
id: detail.id,
|
|
546
|
+
data: detail.data,
|
|
547
|
+
format: detail.format
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (delta?.tool_calls) {
|
|
553
|
+
for (const tc of delta.tool_calls) {
|
|
554
|
+
const index = tc.index;
|
|
555
|
+
const existing = state.toolCalls.get(index);
|
|
556
|
+
if (tc.id && tc.function?.name) {
|
|
557
|
+
state.toolCalls.set(index, {
|
|
558
|
+
id: tc.id,
|
|
559
|
+
name: tc.function.name,
|
|
560
|
+
arguments: tc.function.arguments || ""
|
|
561
|
+
});
|
|
562
|
+
chunks.push({
|
|
563
|
+
type: "tool-call-start",
|
|
564
|
+
id: tc.id,
|
|
565
|
+
name: tc.function.name
|
|
566
|
+
});
|
|
567
|
+
} else if (existing && tc.function?.arguments) {
|
|
568
|
+
existing.arguments += tc.function.arguments;
|
|
569
|
+
chunks.push({
|
|
570
|
+
type: "tool-call-delta",
|
|
571
|
+
id: existing.id,
|
|
572
|
+
argumentsDelta: tc.function.arguments
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (delta?.images && delta.images.length > 0) {
|
|
578
|
+
for (const img of delta.images) {
|
|
579
|
+
const index = state.images.length;
|
|
580
|
+
const providerImage = extractImageFromUrl(img.image_url.url, index);
|
|
581
|
+
state.images.push(providerImage);
|
|
582
|
+
chunks.push({
|
|
583
|
+
type: "image-done",
|
|
584
|
+
index,
|
|
585
|
+
image: providerImage
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const webSearchCitations = extractWebSearchCitations(delta?.annotations);
|
|
590
|
+
if (webSearchCitations.length > 0) {
|
|
591
|
+
state.webSearchCitations.push(...webSearchCitations);
|
|
592
|
+
}
|
|
593
|
+
if (choice?.finish_reason) {
|
|
594
|
+
state.finishReason = choice.finish_reason;
|
|
595
|
+
if (state.hasContent) {
|
|
596
|
+
chunks.push({ type: "content-done" });
|
|
597
|
+
}
|
|
598
|
+
if (state.hasReasoning) {
|
|
599
|
+
chunks.push({ type: "reasoning-done" });
|
|
600
|
+
}
|
|
601
|
+
for (const tc of state.toolCalls.values()) {
|
|
602
|
+
let parsedArgs = {};
|
|
603
|
+
try {
|
|
604
|
+
parsedArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
chunks.push({
|
|
608
|
+
type: "tool-call-done",
|
|
609
|
+
id: tc.id,
|
|
610
|
+
arguments: parsedArgs
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (chunk.usage) {
|
|
615
|
+
chunks.push(...providerToolDoneChunksFromServerToolUseOrCitations(
|
|
616
|
+
chunk.usage,
|
|
617
|
+
state.webSearchCitations,
|
|
618
|
+
chunk.id
|
|
619
|
+
));
|
|
620
|
+
const actualProvider = chunk.model?.split("/")[0] || void 0;
|
|
621
|
+
const reasoningDetails = state.reasoningDetails.length > 0 ? state.reasoningDetails.map(({ _index, ...d }) => d) : void 0;
|
|
622
|
+
chunks.push({
|
|
623
|
+
type: "finish",
|
|
624
|
+
finishReason: mapChatFinishReason(state.finishReason),
|
|
625
|
+
usage: transformChatUsage(chunk.usage, actualProvider),
|
|
626
|
+
reasoningDetails
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
return chunks;
|
|
630
|
+
}
|
|
631
|
+
function parseChatStreamEvent(jsonStr) {
|
|
632
|
+
try {
|
|
633
|
+
return JSON.parse(jsonStr);
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function* parseChatSSEStream(response, state) {
|
|
639
|
+
const reader = response.body?.getReader();
|
|
640
|
+
if (!reader) {
|
|
641
|
+
throw new Error("No response body");
|
|
642
|
+
}
|
|
643
|
+
const decoder = new TextDecoder();
|
|
644
|
+
let buffer = "";
|
|
645
|
+
try {
|
|
646
|
+
while (true) {
|
|
647
|
+
const { done, value } = await reader.read();
|
|
648
|
+
if (done) break;
|
|
649
|
+
buffer += decoder.decode(value, { stream: true });
|
|
650
|
+
const lines = buffer.split("\n");
|
|
651
|
+
buffer = lines.pop() || "";
|
|
652
|
+
for (const line of lines) {
|
|
653
|
+
const trimmed = line.trim();
|
|
654
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
655
|
+
if (trimmed.startsWith("data: ")) {
|
|
656
|
+
const data = trimmed.slice(6);
|
|
657
|
+
if (data === "[DONE]") continue;
|
|
658
|
+
const chunk = parseChatStreamEvent(data);
|
|
659
|
+
if (chunk) {
|
|
660
|
+
const providerChunks = processChatStreamChunk(chunk, state);
|
|
661
|
+
for (const c of providerChunks) {
|
|
662
|
+
yield c;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (buffer.trim()) {
|
|
669
|
+
const trimmed = buffer.trim();
|
|
670
|
+
if (trimmed.startsWith("data: ")) {
|
|
671
|
+
const data = trimmed.slice(6);
|
|
672
|
+
if (data !== "[DONE]") {
|
|
673
|
+
const chunk = parseChatStreamEvent(data);
|
|
674
|
+
if (chunk) {
|
|
675
|
+
const providerChunks = processChatStreamChunk(chunk, state);
|
|
676
|
+
for (const c of providerChunks) {
|
|
677
|
+
yield c;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
if (state.finishReason && !state.toolCalls.size) {
|
|
684
|
+
}
|
|
685
|
+
} finally {
|
|
686
|
+
reader.releaseLock();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function buildChatParams(request) {
|
|
690
|
+
const messages = transformChatMessages(request.messages);
|
|
691
|
+
const params = {
|
|
692
|
+
model: request.model,
|
|
693
|
+
messages
|
|
694
|
+
};
|
|
695
|
+
if (request.tools && request.tools.length > 0) {
|
|
696
|
+
params.tools = transformChatTools(request.tools, request.providerOptions);
|
|
697
|
+
const toolChoice = transformChatToolChoice(request.toolChoice);
|
|
698
|
+
if (toolChoice !== void 0) {
|
|
699
|
+
params.tool_choice = toolChoice;
|
|
700
|
+
}
|
|
701
|
+
if (request.parallelToolCalls !== void 0) {
|
|
702
|
+
params.parallel_tool_calls = request.parallelToolCalls;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (request.maxOutputTokens !== void 0) {
|
|
706
|
+
params.max_tokens = request.maxOutputTokens;
|
|
707
|
+
}
|
|
708
|
+
if (request.temperature !== void 0) {
|
|
709
|
+
params.temperature = request.temperature;
|
|
425
710
|
}
|
|
426
711
|
if (request.topP !== void 0) {
|
|
427
|
-
params.
|
|
712
|
+
params.top_p = request.topP;
|
|
428
713
|
}
|
|
429
714
|
if (request.reasoning?.level !== void 0) {
|
|
430
715
|
const effortMap = {
|
|
431
716
|
10: "minimal",
|
|
432
|
-
// Basic reasoning
|
|
433
717
|
33: "low",
|
|
434
|
-
// Light reasoning
|
|
435
718
|
66: "medium",
|
|
436
|
-
// Balanced reasoning
|
|
437
719
|
100: "high"
|
|
438
|
-
// Deep reasoning
|
|
439
720
|
};
|
|
440
721
|
const effort = effortMap[request.reasoning.level];
|
|
441
722
|
if (effort) {
|
|
442
|
-
params.reasoning = {
|
|
443
|
-
effort
|
|
444
|
-
};
|
|
723
|
+
params.reasoning = { effort };
|
|
445
724
|
}
|
|
446
725
|
}
|
|
447
726
|
if (request.responseFormat) {
|
|
448
727
|
if (request.responseFormat.type === "json") {
|
|
449
728
|
if (request.responseFormat.schema) {
|
|
450
|
-
params.
|
|
451
|
-
|
|
452
|
-
|
|
729
|
+
params.response_format = {
|
|
730
|
+
type: "json_schema",
|
|
731
|
+
json_schema: {
|
|
453
732
|
name: "response",
|
|
454
733
|
schema: request.responseFormat.schema,
|
|
455
734
|
strict: true
|
|
456
735
|
}
|
|
457
736
|
};
|
|
458
737
|
} else {
|
|
459
|
-
params.
|
|
460
|
-
format: { type: "json_object" }
|
|
461
|
-
};
|
|
738
|
+
params.response_format = { type: "json_object" };
|
|
462
739
|
}
|
|
463
740
|
}
|
|
464
741
|
}
|
|
465
|
-
|
|
466
|
-
|
|
742
|
+
const safeOptions = splitOpenRouterProviderOptions(
|
|
743
|
+
request.providerOptions
|
|
744
|
+
);
|
|
745
|
+
if (safeOptions) {
|
|
746
|
+
Object.assign(params, safeOptions);
|
|
467
747
|
}
|
|
468
748
|
return params;
|
|
469
749
|
}
|
|
470
|
-
function
|
|
471
|
-
return {
|
|
472
|
-
toolCalls: /* @__PURE__ */ new Map(),
|
|
473
|
-
reasoningContent: "",
|
|
474
|
-
hasContent: false,
|
|
475
|
-
hasReasoning: false,
|
|
476
|
-
currentItemId: null,
|
|
477
|
-
imageGenerations: /* @__PURE__ */ new Map()
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
function processStreamEvent(event, state) {
|
|
481
|
-
const chunks = [];
|
|
482
|
-
switch (event.type) {
|
|
483
|
-
// Text content streaming
|
|
484
|
-
case "response.output_text.delta":
|
|
485
|
-
state.hasContent = true;
|
|
486
|
-
chunks.push({ type: "content-delta", delta: event.delta });
|
|
487
|
-
break;
|
|
488
|
-
case "response.output_text.done":
|
|
489
|
-
break;
|
|
490
|
-
// Reasoning streaming - OpenRouter uses 'response.reasoning.delta'
|
|
491
|
-
// Note: SDK types may not include reasoning events, so we check dynamically
|
|
492
|
-
case "response.reasoning_text.delta":
|
|
493
|
-
state.hasReasoning = true;
|
|
494
|
-
state.reasoningContent += event.delta;
|
|
495
|
-
chunks.push({ type: "reasoning-delta", delta: event.delta });
|
|
496
|
-
break;
|
|
497
|
-
case "response.reasoning_text.done":
|
|
498
|
-
break;
|
|
499
|
-
// Function call and image generation streaming
|
|
500
|
-
case "response.output_item.added":
|
|
501
|
-
if (event.item.type === "function_call") {
|
|
502
|
-
const callId = event.item.callId || event.item.call_id || "";
|
|
503
|
-
const name = event.item.name || "";
|
|
504
|
-
state.toolCalls.set(callId, {
|
|
505
|
-
id: callId,
|
|
506
|
-
name,
|
|
507
|
-
arguments: ""
|
|
508
|
-
});
|
|
509
|
-
chunks.push({
|
|
510
|
-
type: "tool-call-start",
|
|
511
|
-
id: callId,
|
|
512
|
-
name
|
|
513
|
-
});
|
|
514
|
-
}
|
|
515
|
-
if (event.item.type === "image_generation_call") {
|
|
516
|
-
const itemAny = event.item;
|
|
517
|
-
const imageId = itemAny.id || itemAny.call_id || itemAny.callId || `img-${state.imageGenerations.size}`;
|
|
518
|
-
state.imageGenerations.set(imageId, {
|
|
519
|
-
id: imageId,
|
|
520
|
-
data: "",
|
|
521
|
-
status: "in_progress"
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
case "response.output_item.done": {
|
|
526
|
-
const itemAny = event.item;
|
|
527
|
-
if (itemAny.type === "image_generation_call" && itemAny.result) {
|
|
528
|
-
const imageId = itemAny.id || itemAny.call_id || itemAny.callId || "";
|
|
529
|
-
const imageData = itemAny.result;
|
|
530
|
-
let mediaType = "image/png";
|
|
531
|
-
if (imageData.startsWith("data:")) {
|
|
532
|
-
const match = imageData.match(/^data:([^;,]+)/);
|
|
533
|
-
if (match) mediaType = match[1];
|
|
534
|
-
}
|
|
535
|
-
const existing = state.imageGenerations.get(imageId);
|
|
536
|
-
if (existing) {
|
|
537
|
-
existing.data = imageData;
|
|
538
|
-
existing.status = "completed";
|
|
539
|
-
}
|
|
540
|
-
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
541
|
-
chunks.push({
|
|
542
|
-
type: "image-done",
|
|
543
|
-
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
544
|
-
image: {
|
|
545
|
-
id: imageId || void 0,
|
|
546
|
-
data: imageData,
|
|
547
|
-
mediaType,
|
|
548
|
-
revisedPrompt: itemAny.revised_prompt || itemAny.revisedPrompt || void 0
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
break;
|
|
553
|
-
}
|
|
554
|
-
case "response.function_call_arguments.delta": {
|
|
555
|
-
const itemId = event.itemId || event.item_id || "";
|
|
556
|
-
const deltaToolCall = Array.from(state.toolCalls.values()).find(
|
|
557
|
-
(tc) => tc.id === itemId
|
|
558
|
-
);
|
|
559
|
-
if (deltaToolCall) {
|
|
560
|
-
deltaToolCall.arguments += event.delta;
|
|
561
|
-
chunks.push({
|
|
562
|
-
type: "tool-call-delta",
|
|
563
|
-
id: deltaToolCall.id,
|
|
564
|
-
argumentsDelta: event.delta
|
|
565
|
-
});
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
case "response.function_call_arguments.done": {
|
|
570
|
-
const itemId = event.itemId || event.item_id || "";
|
|
571
|
-
const doneToolCall = Array.from(state.toolCalls.values()).find(
|
|
572
|
-
(tc) => tc.id === itemId
|
|
573
|
-
);
|
|
574
|
-
if (doneToolCall) {
|
|
575
|
-
let parsedArgs = {};
|
|
576
|
-
try {
|
|
577
|
-
parsedArgs = doneToolCall.arguments ? JSON.parse(doneToolCall.arguments) : {};
|
|
578
|
-
} catch {
|
|
579
|
-
}
|
|
580
|
-
chunks.push({
|
|
581
|
-
type: "tool-call-done",
|
|
582
|
-
id: doneToolCall.id,
|
|
583
|
-
arguments: parsedArgs
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
break;
|
|
587
|
-
}
|
|
588
|
-
// Response completion
|
|
589
|
-
case "response.completed": {
|
|
590
|
-
if (state.hasContent) {
|
|
591
|
-
chunks.push({ type: "content-done" });
|
|
592
|
-
}
|
|
593
|
-
if (state.hasReasoning) {
|
|
594
|
-
chunks.push({ type: "reasoning-done" });
|
|
595
|
-
}
|
|
596
|
-
const completedProvider = event.response.model?.split("/")[0] || void 0;
|
|
597
|
-
const reasoningDetails = extractReasoningDetails(event.response.output, state.reasoningContent);
|
|
598
|
-
const generatedImages = extractImages(event.response.output);
|
|
599
|
-
if (generatedImages && generatedImages.length > 0) {
|
|
600
|
-
for (let i = 0; i < generatedImages.length; i++) {
|
|
601
|
-
const img = generatedImages[i];
|
|
602
|
-
const alreadyEmitted = img.id && state.imageGenerations.get(img.id)?.status === "completed";
|
|
603
|
-
if (!alreadyEmitted) {
|
|
604
|
-
chunks.push({
|
|
605
|
-
type: "image-done",
|
|
606
|
-
index: i,
|
|
607
|
-
image: img
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
chunks.push({
|
|
613
|
-
type: "finish",
|
|
614
|
-
finishReason: mapFinishReason(event.response),
|
|
615
|
-
usage: transformUsage(event.response.usage, completedProvider),
|
|
616
|
-
responseId: event.response.id,
|
|
617
|
-
// Used to fetch generation metadata after stream
|
|
618
|
-
reasoningDetails
|
|
619
|
-
});
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
case "response.failed":
|
|
623
|
-
chunks.push({
|
|
624
|
-
type: "error",
|
|
625
|
-
error: event.response.error?.message || "Response generation failed",
|
|
626
|
-
code: event.response.error?.code
|
|
627
|
-
});
|
|
628
|
-
break;
|
|
629
|
-
case "response.incomplete": {
|
|
630
|
-
if (state.hasContent) {
|
|
631
|
-
chunks.push({ type: "content-done" });
|
|
632
|
-
}
|
|
633
|
-
if (state.hasReasoning) {
|
|
634
|
-
chunks.push({ type: "reasoning-done" });
|
|
635
|
-
}
|
|
636
|
-
const incompleteProvider = event.response.model?.split("/")[0] || void 0;
|
|
637
|
-
const incompleteReasoningDetails = extractReasoningDetails(event.response.output, state.reasoningContent);
|
|
638
|
-
const incompleteImages = extractImages(event.response.output);
|
|
639
|
-
if (incompleteImages && incompleteImages.length > 0) {
|
|
640
|
-
for (let i = 0; i < incompleteImages.length; i++) {
|
|
641
|
-
const img = incompleteImages[i];
|
|
642
|
-
const alreadyEmitted = img.id && state.imageGenerations.get(img.id)?.status === "completed";
|
|
643
|
-
if (!alreadyEmitted) {
|
|
644
|
-
chunks.push({
|
|
645
|
-
type: "image-done",
|
|
646
|
-
index: i,
|
|
647
|
-
image: img
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
chunks.push({
|
|
653
|
-
type: "finish",
|
|
654
|
-
finishReason: mapFinishReason(event.response),
|
|
655
|
-
usage: transformUsage(event.response.usage, incompleteProvider),
|
|
656
|
-
responseId: event.response.id,
|
|
657
|
-
// Used to fetch generation metadata after stream
|
|
658
|
-
reasoningDetails: incompleteReasoningDetails
|
|
659
|
-
});
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
// Handle reasoning and image events dynamically (SDK types may not include them)
|
|
663
|
-
default: {
|
|
664
|
-
const eventType = event.type;
|
|
665
|
-
const eventAny = event;
|
|
666
|
-
if (eventType === "response.reasoning.delta") {
|
|
667
|
-
state.hasReasoning = true;
|
|
668
|
-
state.reasoningContent += eventAny.delta || "";
|
|
669
|
-
chunks.push({ type: "reasoning-delta", delta: eventAny.delta || "" });
|
|
670
|
-
}
|
|
671
|
-
if (eventType === "response.image_generation_call.partial_image" || eventType === "response.image_generation.partial") {
|
|
672
|
-
const imageId = eventAny.item_id || eventAny.itemId || eventAny.id || "";
|
|
673
|
-
const partialData = eventAny.partial_image || eventAny.delta || eventAny.data || "";
|
|
674
|
-
const existing = state.imageGenerations.get(imageId);
|
|
675
|
-
if (existing) {
|
|
676
|
-
existing.data += partialData;
|
|
677
|
-
} else {
|
|
678
|
-
state.imageGenerations.set(imageId, {
|
|
679
|
-
id: imageId,
|
|
680
|
-
data: partialData,
|
|
681
|
-
status: "generating"
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
685
|
-
chunks.push({
|
|
686
|
-
type: "image-delta",
|
|
687
|
-
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
688
|
-
data: partialData
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
if (eventType === "response.image_generation_call.completed" || eventType === "response.image_generation.done") {
|
|
692
|
-
const imageId = eventAny.item_id || eventAny.itemId || eventAny.id || "";
|
|
693
|
-
const imageData = eventAny.result || eventAny.image || eventAny.data || "";
|
|
694
|
-
if (imageData) {
|
|
695
|
-
let mediaType = "image/png";
|
|
696
|
-
if (imageData.startsWith("data:")) {
|
|
697
|
-
const match = imageData.match(/^data:([^;,]+)/);
|
|
698
|
-
if (match) mediaType = match[1];
|
|
699
|
-
}
|
|
700
|
-
const existing = state.imageGenerations.get(imageId);
|
|
701
|
-
if (existing) {
|
|
702
|
-
existing.data = imageData;
|
|
703
|
-
existing.status = "completed";
|
|
704
|
-
}
|
|
705
|
-
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
706
|
-
chunks.push({
|
|
707
|
-
type: "image-done",
|
|
708
|
-
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
709
|
-
image: {
|
|
710
|
-
id: imageId || void 0,
|
|
711
|
-
data: imageData,
|
|
712
|
-
mediaType,
|
|
713
|
-
revisedPrompt: eventAny.revised_prompt || eventAny.revisedPrompt || void 0
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
break;
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
return chunks;
|
|
722
|
-
}
|
|
723
|
-
function createErrorChunk(error, code) {
|
|
724
|
-
return { type: "error", error, code };
|
|
750
|
+
function createChatErrorChunk(error, code) {
|
|
751
|
+
return { type: "error", error, code };
|
|
725
752
|
}
|
|
726
753
|
function isBase64Like(str) {
|
|
727
754
|
if (str.startsWith("data:")) return true;
|
|
@@ -736,7 +763,7 @@ function truncateBase64String(str, maxLength = 50) {
|
|
|
736
763
|
const preview = str.substring(0, maxLength);
|
|
737
764
|
return `${preview}...[truncated, ${str.length.toLocaleString()} chars]`;
|
|
738
765
|
}
|
|
739
|
-
function
|
|
766
|
+
function truncateChatBase64(obj, maxLength = 50) {
|
|
740
767
|
if (obj === null || obj === void 0) {
|
|
741
768
|
return obj;
|
|
742
769
|
}
|
|
@@ -747,206 +774,113 @@ function truncateBase64(obj, maxLength = 50) {
|
|
|
747
774
|
return obj;
|
|
748
775
|
}
|
|
749
776
|
if (Array.isArray(obj)) {
|
|
750
|
-
return obj.map((item) =>
|
|
777
|
+
return obj.map((item) => truncateChatBase64(item, maxLength));
|
|
751
778
|
}
|
|
752
779
|
if (typeof obj === "object") {
|
|
753
780
|
const result = {};
|
|
754
781
|
for (const [key, value] of Object.entries(obj)) {
|
|
755
|
-
result[key] =
|
|
782
|
+
result[key] = truncateChatBase64(value, maxLength);
|
|
756
783
|
}
|
|
757
784
|
return result;
|
|
758
785
|
}
|
|
759
786
|
return obj;
|
|
760
787
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
788
|
+
|
|
789
|
+
// src/responses-transformers.ts
|
|
790
|
+
function isRecord3(value) {
|
|
791
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
792
|
+
}
|
|
793
|
+
function transformContentPart(part) {
|
|
794
|
+
if (part.type === "text") {
|
|
795
|
+
return { type: "input_text", text: part.text };
|
|
796
|
+
}
|
|
797
|
+
if (part.type === "image") {
|
|
798
|
+
const data = part.data || "";
|
|
799
|
+
const imageUrl = data.startsWith("data:") ? data : `data:${part.mediaType || "image/png"};base64,${data}`;
|
|
800
|
+
return {
|
|
801
|
+
type: "input_image",
|
|
802
|
+
image_url: imageUrl,
|
|
803
|
+
detail: part.detail || "auto"
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (part.type === "image_url") {
|
|
807
|
+
const url = part.image_url?.url || "";
|
|
808
|
+
const detail = part.image_url?.detail || "auto";
|
|
809
|
+
return {
|
|
810
|
+
type: "input_image",
|
|
811
|
+
image_url: url,
|
|
812
|
+
detail
|
|
813
|
+
};
|
|
767
814
|
}
|
|
815
|
+
return {
|
|
816
|
+
type: "input_text",
|
|
817
|
+
text: `[File: ${part.filename || "file"}]`
|
|
818
|
+
};
|
|
768
819
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
throw new Error("No response body");
|
|
773
|
-
}
|
|
774
|
-
const decoder = new TextDecoder();
|
|
775
|
-
let buffer = "";
|
|
776
|
-
try {
|
|
777
|
-
while (true) {
|
|
778
|
-
const { done, value } = await reader.read();
|
|
779
|
-
if (done) break;
|
|
780
|
-
buffer += decoder.decode(value, { stream: true });
|
|
781
|
-
const lines = buffer.split("\n");
|
|
782
|
-
buffer = lines.pop() || "";
|
|
783
|
-
for (const line of lines) {
|
|
784
|
-
const trimmed = line.trim();
|
|
785
|
-
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
786
|
-
if (trimmed.startsWith("data: ")) {
|
|
787
|
-
const data = trimmed.slice(6);
|
|
788
|
-
if (data === "[DONE]") continue;
|
|
789
|
-
const event = parseRawStreamEvent(data);
|
|
790
|
-
if (event) {
|
|
791
|
-
const chunks = processStreamEvent(event, state);
|
|
792
|
-
for (const chunk of chunks) {
|
|
793
|
-
yield chunk;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
if (buffer.trim()) {
|
|
800
|
-
const trimmed = buffer.trim();
|
|
801
|
-
if (trimmed.startsWith("data: ")) {
|
|
802
|
-
const data = trimmed.slice(6);
|
|
803
|
-
if (data !== "[DONE]") {
|
|
804
|
-
const event = parseRawStreamEvent(data);
|
|
805
|
-
if (event) {
|
|
806
|
-
const chunks = processStreamEvent(event, state);
|
|
807
|
-
for (const chunk of chunks) {
|
|
808
|
-
yield chunk;
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
} finally {
|
|
815
|
-
reader.releaseLock();
|
|
820
|
+
function transformMessageContent(content) {
|
|
821
|
+
if (typeof content === "string") {
|
|
822
|
+
return content;
|
|
816
823
|
}
|
|
824
|
+
return content.map(transformContentPart);
|
|
817
825
|
}
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
if (
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
if (!response.ok) {
|
|
838
|
-
console.error(`Failed to fetch generation metadata: ${response.status}`);
|
|
839
|
-
return null;
|
|
840
|
-
}
|
|
841
|
-
const result = await response.json();
|
|
842
|
-
if (result.error) {
|
|
843
|
-
console.error("Generation metadata error:", result.error.message);
|
|
844
|
-
return null;
|
|
845
|
-
}
|
|
846
|
-
const data = result.data;
|
|
847
|
-
if (!data) {
|
|
848
|
-
return null;
|
|
826
|
+
function transformUserMessage(msg) {
|
|
827
|
+
const content = transformMessageContent(msg.content);
|
|
828
|
+
return {
|
|
829
|
+
role: "user",
|
|
830
|
+
content: typeof content === "string" ? content : content
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function transformAssistantMessage(msg) {
|
|
834
|
+
const items = [];
|
|
835
|
+
const reasoningDetails = msg.reasoningDetails;
|
|
836
|
+
if (reasoningDetails && reasoningDetails.length > 0) {
|
|
837
|
+
const summaryTexts = [];
|
|
838
|
+
let encryptedContent;
|
|
839
|
+
for (const detail of reasoningDetails) {
|
|
840
|
+
if (detail.type === "reasoning.text") {
|
|
841
|
+
summaryTexts.push({ type: "summary_text", text: detail.text });
|
|
842
|
+
} else if (detail.type === "reasoning.encrypted") {
|
|
843
|
+
encryptedContent = detail.text;
|
|
849
844
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
providerName: data.provider_name || "unknown",
|
|
858
|
-
latency: data.latency || data.generation_time,
|
|
859
|
-
model: data.model,
|
|
860
|
-
finishReason: data.finish_reason,
|
|
861
|
-
createdAt: data.created_at
|
|
845
|
+
}
|
|
846
|
+
if (summaryTexts.length > 0) {
|
|
847
|
+
const reasoningItem = {
|
|
848
|
+
type: "reasoning",
|
|
849
|
+
id: "",
|
|
850
|
+
summary: summaryTexts,
|
|
851
|
+
...encryptedContent ? { encrypted_content: encryptedContent } : {}
|
|
862
852
|
};
|
|
863
|
-
|
|
864
|
-
if (attempt < maxRetries - 1) {
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
console.error("Error fetching generation metadata:", error);
|
|
868
|
-
return null;
|
|
853
|
+
items.push(reasoningItem);
|
|
869
854
|
}
|
|
870
855
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
if (part.type === "image") {
|
|
880
|
-
const data = part.data || "";
|
|
881
|
-
const imageUrl = data.startsWith("data:") ? data : `data:${part.mediaType || "image/png"};base64,${data}`;
|
|
882
|
-
return {
|
|
883
|
-
type: "image_url",
|
|
884
|
-
image_url: {
|
|
885
|
-
url: imageUrl,
|
|
886
|
-
detail: part.detail || "auto"
|
|
887
|
-
}
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
if (part.type === "image_url") {
|
|
891
|
-
return {
|
|
892
|
-
type: "image_url",
|
|
893
|
-
image_url: {
|
|
894
|
-
url: part.image_url?.url || "",
|
|
895
|
-
detail: part.image_url?.detail || "auto"
|
|
896
|
-
}
|
|
856
|
+
if (msg.content) {
|
|
857
|
+
const outputMessage = {
|
|
858
|
+
type: "message",
|
|
859
|
+
role: "assistant",
|
|
860
|
+
id: "",
|
|
861
|
+
status: "completed",
|
|
862
|
+
content: [{ type: "output_text", text: msg.content, annotations: [] }]
|
|
897
863
|
};
|
|
864
|
+
items.push(outputMessage);
|
|
898
865
|
}
|
|
899
|
-
return {
|
|
900
|
-
type: "text",
|
|
901
|
-
text: `[File: ${part.filename || "file"}]`
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
function transformChatMessageContent(content) {
|
|
905
|
-
if (typeof content === "string") {
|
|
906
|
-
return content;
|
|
907
|
-
}
|
|
908
|
-
return content.map(transformChatContentPart);
|
|
909
|
-
}
|
|
910
|
-
function transformChatSystemMessage(msg) {
|
|
911
|
-
return {
|
|
912
|
-
role: "system",
|
|
913
|
-
content: msg.content
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
function transformChatUserMessage(msg) {
|
|
917
|
-
return {
|
|
918
|
-
role: "user",
|
|
919
|
-
content: transformChatMessageContent(msg.content)
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
function transformChatAssistantMessage(msg) {
|
|
923
|
-
const message = {
|
|
924
|
-
role: "assistant",
|
|
925
|
-
content: msg.content || null
|
|
926
|
-
};
|
|
927
866
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
867
|
+
for (const tc of msg.toolCalls) {
|
|
868
|
+
const functionCall = {
|
|
869
|
+
type: "function_call",
|
|
870
|
+
id: `fc_${tc.id}`,
|
|
871
|
+
// Unique output item ID (prefixed to distinguish from call_id)
|
|
872
|
+
call_id: tc.id,
|
|
932
873
|
name: tc.name,
|
|
933
|
-
arguments:
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (detail.type === "text") {
|
|
940
|
-
return { type: "reasoning.text", text: detail.text, format: detail.format };
|
|
941
|
-
} else if (detail.type === "summary") {
|
|
942
|
-
return { type: "reasoning.summary", summary: detail.text, format: detail.format };
|
|
943
|
-
}
|
|
944
|
-
return { type: "reasoning.encrypted", data: detail.data, id: detail.id, format: detail.format };
|
|
945
|
-
});
|
|
874
|
+
arguments: JSON.stringify(tc.arguments),
|
|
875
|
+
status: "completed"
|
|
876
|
+
// Mark as completed since this is from history
|
|
877
|
+
};
|
|
878
|
+
items.push(functionCall);
|
|
879
|
+
}
|
|
946
880
|
}
|
|
947
|
-
return
|
|
881
|
+
return items;
|
|
948
882
|
}
|
|
949
|
-
function
|
|
883
|
+
function transformToolMessage(msg) {
|
|
950
884
|
let output;
|
|
951
885
|
if (typeof msg.content === "string") {
|
|
952
886
|
output = msg.content;
|
|
@@ -962,13 +896,13 @@ function transformChatToolMessage(msg) {
|
|
|
962
896
|
output = JSON.stringify(msg.content);
|
|
963
897
|
}
|
|
964
898
|
const imageAttachments = msg.attachments?.filter((a) => a.type === "image" && a.data) || [];
|
|
965
|
-
const
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
899
|
+
const toolOutput = {
|
|
900
|
+
type: "function_call_output",
|
|
901
|
+
call_id: msg.toolCallId,
|
|
902
|
+
output: output || (imageAttachments.length > 0 ? "Success" : "")
|
|
969
903
|
};
|
|
970
904
|
if (imageAttachments.length === 0) {
|
|
971
|
-
return {
|
|
905
|
+
return { toolOutput };
|
|
972
906
|
}
|
|
973
907
|
const imageContent = [];
|
|
974
908
|
const toolName = msg.toolName || "the tool";
|
|
@@ -976,81 +910,76 @@ function transformChatToolMessage(msg) {
|
|
|
976
910
|
const descriptor = isSingle ? "the file" : `${imageAttachments.length} files`;
|
|
977
911
|
const verb = isSingle ? "is" : "are";
|
|
978
912
|
imageContent.push({
|
|
979
|
-
type: "
|
|
913
|
+
type: "input_text",
|
|
980
914
|
text: `Here ${verb} ${descriptor} from ${toolName}:`
|
|
981
915
|
});
|
|
982
916
|
for (const attachment of imageAttachments) {
|
|
983
917
|
const attachmentData = attachment.data || "";
|
|
984
918
|
const imageData = attachmentData.startsWith("data:") ? attachmentData : `data:${attachment.mediaType || "image/png"};base64,${attachmentData}`;
|
|
985
919
|
imageContent.push({
|
|
986
|
-
type: "
|
|
987
|
-
image_url:
|
|
988
|
-
|
|
989
|
-
detail: "auto"
|
|
990
|
-
}
|
|
920
|
+
type: "input_image",
|
|
921
|
+
image_url: imageData,
|
|
922
|
+
detail: "auto"
|
|
991
923
|
});
|
|
992
924
|
}
|
|
993
925
|
const syntheticUserMessage = {
|
|
994
926
|
role: "user",
|
|
995
927
|
content: imageContent
|
|
996
928
|
};
|
|
997
|
-
return {
|
|
929
|
+
return { toolOutput, syntheticUserMessage };
|
|
998
930
|
}
|
|
999
|
-
function
|
|
1000
|
-
|
|
931
|
+
function transformMessages(messages) {
|
|
932
|
+
let instructions;
|
|
933
|
+
const input = [];
|
|
1001
934
|
for (const msg of messages) {
|
|
1002
935
|
switch (msg.role) {
|
|
1003
936
|
case "system":
|
|
1004
|
-
|
|
937
|
+
instructions = instructions ? `${instructions}
|
|
938
|
+
|
|
939
|
+
${msg.content}` : msg.content;
|
|
940
|
+
input.push({
|
|
941
|
+
type: "message",
|
|
942
|
+
role: "system",
|
|
943
|
+
content: msg.content
|
|
944
|
+
});
|
|
1005
945
|
break;
|
|
1006
946
|
case "user":
|
|
1007
|
-
|
|
947
|
+
input.push(transformUserMessage(msg));
|
|
1008
948
|
break;
|
|
1009
949
|
case "assistant":
|
|
1010
|
-
|
|
950
|
+
input.push(...transformAssistantMessage(msg));
|
|
1011
951
|
break;
|
|
1012
952
|
case "tool": {
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
if (syntheticUserMessage) {
|
|
1016
|
-
|
|
953
|
+
const result = transformToolMessage(msg);
|
|
954
|
+
input.push(result.toolOutput);
|
|
955
|
+
if (result.syntheticUserMessage) {
|
|
956
|
+
input.push(result.syntheticUserMessage);
|
|
1017
957
|
}
|
|
1018
958
|
break;
|
|
1019
959
|
}
|
|
1020
960
|
}
|
|
1021
961
|
}
|
|
1022
|
-
return
|
|
962
|
+
return { input, instructions };
|
|
1023
963
|
}
|
|
1024
|
-
function
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
parameters = {
|
|
1029
|
-
...inputParams,
|
|
1030
|
-
additionalProperties: false
|
|
1031
|
-
};
|
|
1032
|
-
} else {
|
|
1033
|
-
parameters = {
|
|
1034
|
-
type: "object",
|
|
1035
|
-
properties: {},
|
|
1036
|
-
required: [],
|
|
1037
|
-
additionalProperties: false
|
|
1038
|
-
};
|
|
964
|
+
function transformTool(tool, providerOptions) {
|
|
965
|
+
if (isOpenRouterProviderTool(tool)) {
|
|
966
|
+
const parameters2 = getServerToolParameters(providerOptions, tool.function.name);
|
|
967
|
+
return toOpenRouterServerTool(tool.function.name, parameters2);
|
|
1039
968
|
}
|
|
969
|
+
const inputParams = tool.function.parameters;
|
|
970
|
+
const parameters = normalizeStrictToolParameters(inputParams);
|
|
1040
971
|
return {
|
|
1041
972
|
type: "function",
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
strict: true
|
|
1047
|
-
}
|
|
973
|
+
name: tool.function.name,
|
|
974
|
+
description: tool.function.description || void 0,
|
|
975
|
+
parameters,
|
|
976
|
+
strict: true
|
|
1048
977
|
};
|
|
1049
978
|
}
|
|
1050
|
-
function
|
|
1051
|
-
return tools.map(
|
|
979
|
+
function transformTools(tools, providerOptions) {
|
|
980
|
+
return tools.map((tool) => transformTool(tool, providerOptions));
|
|
1052
981
|
}
|
|
1053
|
-
function
|
|
982
|
+
function transformToolChoice(choice) {
|
|
1054
983
|
if (choice === "auto") {
|
|
1055
984
|
return "auto";
|
|
1056
985
|
}
|
|
@@ -1061,44 +990,123 @@ function transformChatToolChoice(choice) {
|
|
|
1061
990
|
return "required";
|
|
1062
991
|
}
|
|
1063
992
|
if (typeof choice === "object" && "name" in choice) {
|
|
1064
|
-
return { type: "function",
|
|
993
|
+
return { type: "function", name: choice.name };
|
|
1065
994
|
}
|
|
1066
|
-
return
|
|
995
|
+
return "auto";
|
|
1067
996
|
}
|
|
1068
|
-
function
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
case "length":
|
|
997
|
+
function mapFinishReason(response) {
|
|
998
|
+
if (response.status === "failed") {
|
|
999
|
+
return "error";
|
|
1000
|
+
}
|
|
1001
|
+
if (response.status === "incomplete") {
|
|
1002
|
+
if (response.incompleteDetails?.reason === "max_output_tokens") {
|
|
1075
1003
|
return "length";
|
|
1076
|
-
|
|
1004
|
+
}
|
|
1005
|
+
if (response.incompleteDetails?.reason === "content_filter") {
|
|
1077
1006
|
return "content_filter";
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const hasToolCalls = response.output.some(
|
|
1010
|
+
(item) => item.type === "function_call"
|
|
1011
|
+
);
|
|
1012
|
+
if (hasToolCalls) {
|
|
1013
|
+
return "tool_calls";
|
|
1082
1014
|
}
|
|
1015
|
+
return "stop";
|
|
1083
1016
|
}
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1086
|
-
|
|
1017
|
+
function extractTextContent(output) {
|
|
1018
|
+
const textParts = [];
|
|
1019
|
+
for (const item of output) {
|
|
1020
|
+
if (item.type === "message" && item.role === "assistant") {
|
|
1021
|
+
for (const content of item.content) {
|
|
1022
|
+
if (content.type === "output_text") {
|
|
1023
|
+
textParts.push(content.text);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1087
1027
|
}
|
|
1088
|
-
return
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1028
|
+
return textParts.length > 0 ? textParts.join("") : null;
|
|
1029
|
+
}
|
|
1030
|
+
function extractResponseWebSearchCitations(output) {
|
|
1031
|
+
const citations = [];
|
|
1032
|
+
for (const item of output) {
|
|
1033
|
+
if (item.type !== "message" || !isRecord3(item) || !Array.isArray(item.content)) {
|
|
1034
|
+
continue;
|
|
1093
1035
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1036
|
+
for (const content of item.content) {
|
|
1037
|
+
if (!isRecord3(content)) continue;
|
|
1038
|
+
const contentRecord = content;
|
|
1039
|
+
citations.push(...extractWebSearchCitations(contentRecord.annotations));
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return citations;
|
|
1100
1043
|
}
|
|
1101
|
-
function
|
|
1044
|
+
function extractToolCalls(output) {
|
|
1045
|
+
const toolCalls = [];
|
|
1046
|
+
for (const item of output) {
|
|
1047
|
+
if (item.type === "function_call") {
|
|
1048
|
+
let parsedArgs = {};
|
|
1049
|
+
try {
|
|
1050
|
+
parsedArgs = item.arguments ? JSON.parse(item.arguments) : {};
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
toolCalls.push({
|
|
1054
|
+
id: item.callId,
|
|
1055
|
+
name: item.name,
|
|
1056
|
+
arguments: parsedArgs
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return toolCalls.length > 0 ? toolCalls : void 0;
|
|
1061
|
+
}
|
|
1062
|
+
function extractImages(output) {
|
|
1063
|
+
const images = [];
|
|
1064
|
+
for (const item of output) {
|
|
1065
|
+
const itemAny = item;
|
|
1066
|
+
if (itemAny.type === "image_generation_call" && itemAny.result) {
|
|
1067
|
+
const imageData = itemAny.result;
|
|
1068
|
+
const isDataUrl = imageData.startsWith("data:");
|
|
1069
|
+
const isHttpUrl = imageData.startsWith("http://") || imageData.startsWith("https://");
|
|
1070
|
+
let mediaType = "image/png";
|
|
1071
|
+
if (isDataUrl) {
|
|
1072
|
+
const match = imageData.match(/^data:([^;,]+)/);
|
|
1073
|
+
if (match) {
|
|
1074
|
+
mediaType = match[1];
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
images.push({
|
|
1078
|
+
id: itemAny.id || void 0,
|
|
1079
|
+
data: imageData,
|
|
1080
|
+
mediaType,
|
|
1081
|
+
// Include revised prompt if available
|
|
1082
|
+
revisedPrompt: itemAny.revised_prompt || itemAny.revisedPrompt || void 0
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
if (itemAny.type === "message" && itemAny.content && Array.isArray(itemAny.content)) {
|
|
1086
|
+
for (const content of itemAny.content) {
|
|
1087
|
+
if (content.type === "output_image" || content.type === "image") {
|
|
1088
|
+
const imageData = content.image || content.data || content.url || content.image_url;
|
|
1089
|
+
if (imageData) {
|
|
1090
|
+
let mediaType = content.media_type || content.mediaType || "image/png";
|
|
1091
|
+
if (typeof imageData === "string" && imageData.startsWith("data:")) {
|
|
1092
|
+
const match = imageData.match(/^data:([^;,]+)/);
|
|
1093
|
+
if (match) {
|
|
1094
|
+
mediaType = match[1];
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
images.push({
|
|
1098
|
+
id: content.id || void 0,
|
|
1099
|
+
data: imageData,
|
|
1100
|
+
mediaType
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return images.length > 0 ? images : void 0;
|
|
1108
|
+
}
|
|
1109
|
+
function transformUsage(usage, actualProvider) {
|
|
1102
1110
|
if (!usage) {
|
|
1103
1111
|
return {
|
|
1104
1112
|
promptTokens: 0,
|
|
@@ -1106,62 +1114,39 @@ function transformChatUsage(usage, actualProvider) {
|
|
|
1106
1114
|
totalTokens: 0
|
|
1107
1115
|
};
|
|
1108
1116
|
}
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
const totalTokens = usage.total_tokens ||
|
|
1117
|
+
const inputTokens = usage.native_tokens_prompt || usage.nativeTokensPrompt || usage.input_tokens || usage.inputTokens || 0;
|
|
1118
|
+
const outputTokens = usage.native_tokens_completion || usage.nativeTokensCompletion || usage.output_tokens || usage.outputTokens || 0;
|
|
1119
|
+
const totalTokens = usage.total_tokens || usage.totalTokens || 0;
|
|
1120
|
+
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens || usage.outputTokensDetails?.reasoningTokens;
|
|
1121
|
+
const cachedTokens = usage.input_tokens_details?.cached_tokens || usage.inputTokensDetails?.cachedTokens;
|
|
1112
1122
|
return {
|
|
1113
|
-
promptTokens,
|
|
1114
|
-
completionTokens,
|
|
1115
|
-
totalTokens,
|
|
1116
|
-
reasoningTokens
|
|
1117
|
-
cachedTokens
|
|
1123
|
+
promptTokens: inputTokens,
|
|
1124
|
+
completionTokens: outputTokens,
|
|
1125
|
+
totalTokens: totalTokens || inputTokens + outputTokens,
|
|
1126
|
+
reasoningTokens,
|
|
1127
|
+
cachedTokens,
|
|
1118
1128
|
cost: usage.cost,
|
|
1119
1129
|
provider: actualProvider
|
|
1120
1130
|
};
|
|
1121
1131
|
}
|
|
1122
|
-
function
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
return {
|
|
1134
|
-
id: `image_${index}`,
|
|
1135
|
-
data: url,
|
|
1136
|
-
mediaType: "image/png"
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
return {
|
|
1140
|
-
id: `image_${index}`,
|
|
1141
|
-
data: url,
|
|
1142
|
-
mediaType: "image/png"
|
|
1143
|
-
// Default; actual type unknown without fetching
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
function extractChatImages(images) {
|
|
1147
|
-
if (!images || images.length === 0) {
|
|
1148
|
-
return void 0;
|
|
1149
|
-
}
|
|
1150
|
-
return images.map((img, index) => extractImageFromUrl(img.image_url.url, index));
|
|
1151
|
-
}
|
|
1152
|
-
function transformChatResponse(response) {
|
|
1153
|
-
const choice = response.choices?.[0];
|
|
1154
|
-
const message = choice?.message;
|
|
1155
|
-
const content = message?.content || null;
|
|
1156
|
-
const toolCalls = extractChatToolCalls(message?.tool_calls);
|
|
1157
|
-
const images = extractChatImages(message?.images);
|
|
1132
|
+
function transformResponse(response) {
|
|
1133
|
+
const content = extractTextContent(response.output);
|
|
1134
|
+
const toolCalls = extractToolCalls(response.output);
|
|
1135
|
+
const images = extractImages(response.output);
|
|
1136
|
+
const webSearchCitations = extractResponseWebSearchCitations(response.output);
|
|
1137
|
+
const providerTools = providerToolResultsFromServerToolUseOrCitations(
|
|
1138
|
+
response.usage,
|
|
1139
|
+
webSearchCitations,
|
|
1140
|
+
response.id
|
|
1141
|
+
);
|
|
1158
1142
|
const actualProvider = response.model?.split("/")[0] || void 0;
|
|
1159
1143
|
return {
|
|
1160
1144
|
content,
|
|
1161
1145
|
toolCalls,
|
|
1162
1146
|
images,
|
|
1163
|
-
|
|
1164
|
-
|
|
1147
|
+
providerTools,
|
|
1148
|
+
finishReason: mapFinishReason(response),
|
|
1149
|
+
usage: transformUsage(response.usage, actualProvider),
|
|
1165
1150
|
metadata: {
|
|
1166
1151
|
model: response.model,
|
|
1167
1152
|
provider: "openrouter",
|
|
@@ -1170,148 +1155,431 @@ function transformChatResponse(response) {
|
|
|
1170
1155
|
}
|
|
1171
1156
|
};
|
|
1172
1157
|
}
|
|
1173
|
-
function
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1158
|
+
function extractReasoningDetails(output, streamedReasoningContent) {
|
|
1159
|
+
const details = [];
|
|
1160
|
+
if (output && Array.isArray(output)) {
|
|
1161
|
+
for (const item of output) {
|
|
1162
|
+
const itemAny = item;
|
|
1163
|
+
if (itemAny.type === "reasoning") {
|
|
1164
|
+
if (itemAny.summary && Array.isArray(itemAny.summary)) {
|
|
1165
|
+
for (const step of itemAny.summary) {
|
|
1166
|
+
if (typeof step === "string") {
|
|
1167
|
+
details.push({ type: "reasoning.text", text: step });
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
if (details.length === 0 && itemAny.encrypted_content) {
|
|
1172
|
+
details.push({ type: "reasoning.encrypted", text: itemAny.encrypted_content });
|
|
1173
|
+
}
|
|
1174
|
+
if (itemAny.content) {
|
|
1175
|
+
if (typeof itemAny.content === "string") {
|
|
1176
|
+
details.push({ type: "reasoning.text", text: itemAny.content });
|
|
1177
|
+
} else if (Array.isArray(itemAny.content)) {
|
|
1178
|
+
for (const part of itemAny.content) {
|
|
1179
|
+
if (part.text) {
|
|
1180
|
+
details.push({ type: "reasoning.text", text: part.text });
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (itemAny.type === "message" && itemAny.content && Array.isArray(itemAny.content)) {
|
|
1187
|
+
for (const part of itemAny.content) {
|
|
1188
|
+
if (part.type === "reasoning" || part.type === "reasoning_text") {
|
|
1189
|
+
details.push({ type: "reasoning.text", text: part.text || "" });
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
if (details.length === 0 && streamedReasoningContent) {
|
|
1196
|
+
details.push({ type: "reasoning.text", text: streamedReasoningContent });
|
|
1197
|
+
}
|
|
1198
|
+
return details.length > 0 ? details : void 0;
|
|
1184
1199
|
}
|
|
1185
|
-
function
|
|
1186
|
-
const
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
state.reasoningDetails.push({
|
|
1196
|
-
type: detailType,
|
|
1197
|
-
text: textContent,
|
|
1198
|
-
format: detail.format,
|
|
1199
|
-
_index: idx
|
|
1200
|
-
});
|
|
1200
|
+
function buildCreateParams(request) {
|
|
1201
|
+
const { input, instructions } = transformMessages(request.messages);
|
|
1202
|
+
const params = {
|
|
1203
|
+
model: request.model,
|
|
1204
|
+
input,
|
|
1205
|
+
store: false
|
|
1206
|
+
// Always stateless
|
|
1207
|
+
};
|
|
1208
|
+
if (instructions) {
|
|
1209
|
+
params.instructions = instructions;
|
|
1201
1210
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1211
|
+
if (request.tools && request.tools.length > 0) {
|
|
1212
|
+
params.tools = transformTools(request.tools, request.providerOptions);
|
|
1213
|
+
if (request.toolChoice !== void 0) {
|
|
1214
|
+
params.toolChoice = transformToolChoice(request.toolChoice);
|
|
1215
|
+
}
|
|
1216
|
+
if (request.parallelToolCalls !== void 0) {
|
|
1217
|
+
params.parallelToolCalls = request.parallelToolCalls;
|
|
1218
|
+
}
|
|
1209
1219
|
}
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1212
|
-
state.content += delta.content;
|
|
1213
|
-
chunks.push({ type: "content-delta", delta: delta.content });
|
|
1220
|
+
if (request.maxOutputTokens !== void 0) {
|
|
1221
|
+
params.maxOutputTokens = request.maxOutputTokens;
|
|
1214
1222
|
}
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
state.hasReasoning = true;
|
|
1218
|
-
state.reasoningContent += reasoningText;
|
|
1219
|
-
chunks.push({ type: "reasoning-delta", delta: reasoningText });
|
|
1223
|
+
if (request.temperature !== void 0) {
|
|
1224
|
+
params.temperature = request.temperature;
|
|
1220
1225
|
}
|
|
1221
|
-
if (
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1226
|
+
if (request.topP !== void 0) {
|
|
1227
|
+
params.topP = request.topP;
|
|
1228
|
+
}
|
|
1229
|
+
if (request.reasoning?.level !== void 0) {
|
|
1230
|
+
const effortMap = {
|
|
1231
|
+
10: "minimal",
|
|
1232
|
+
// Basic reasoning
|
|
1233
|
+
33: "low",
|
|
1234
|
+
// Light reasoning
|
|
1235
|
+
66: "medium",
|
|
1236
|
+
// Balanced reasoning
|
|
1237
|
+
100: "high"
|
|
1238
|
+
// Deep reasoning
|
|
1239
|
+
};
|
|
1240
|
+
const effort = effortMap[request.reasoning.level];
|
|
1241
|
+
if (effort) {
|
|
1242
|
+
params.reasoning = {
|
|
1243
|
+
effort
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (request.responseFormat) {
|
|
1248
|
+
if (request.responseFormat.type === "json") {
|
|
1249
|
+
if (request.responseFormat.schema) {
|
|
1250
|
+
params.text = {
|
|
1251
|
+
format: {
|
|
1252
|
+
type: "json_schema",
|
|
1253
|
+
name: "response",
|
|
1254
|
+
schema: request.responseFormat.schema,
|
|
1255
|
+
strict: true
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
} else {
|
|
1259
|
+
params.text = {
|
|
1260
|
+
format: { type: "json_object" }
|
|
1261
|
+
};
|
|
1234
1262
|
}
|
|
1235
1263
|
}
|
|
1236
1264
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1265
|
+
const providerOptions = splitOpenRouterProviderOptions(
|
|
1266
|
+
request.providerOptions
|
|
1267
|
+
);
|
|
1268
|
+
if (providerOptions) {
|
|
1269
|
+
Object.assign(params, providerOptions);
|
|
1270
|
+
}
|
|
1271
|
+
return params;
|
|
1272
|
+
}
|
|
1273
|
+
function createStreamState() {
|
|
1274
|
+
return {
|
|
1275
|
+
toolCalls: /* @__PURE__ */ new Map(),
|
|
1276
|
+
reasoningContent: "",
|
|
1277
|
+
hasContent: false,
|
|
1278
|
+
hasReasoning: false,
|
|
1279
|
+
currentItemId: null,
|
|
1280
|
+
imageGenerations: /* @__PURE__ */ new Map()
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function processStreamEvent(event, state) {
|
|
1284
|
+
const chunks = [];
|
|
1285
|
+
switch (event.type) {
|
|
1286
|
+
// Text content streaming
|
|
1287
|
+
case "response.output_text.delta":
|
|
1288
|
+
state.hasContent = true;
|
|
1289
|
+
chunks.push({ type: "content-delta", delta: event.delta });
|
|
1290
|
+
break;
|
|
1291
|
+
case "response.output_text.done":
|
|
1292
|
+
break;
|
|
1293
|
+
// Reasoning streaming - OpenRouter uses 'response.reasoning.delta'
|
|
1294
|
+
// Note: SDK types may not include reasoning events, so we check dynamically
|
|
1295
|
+
case "response.reasoning_text.delta":
|
|
1296
|
+
state.hasReasoning = true;
|
|
1297
|
+
state.reasoningContent += event.delta;
|
|
1298
|
+
chunks.push({ type: "reasoning-delta", delta: event.delta });
|
|
1299
|
+
break;
|
|
1300
|
+
case "response.reasoning_text.done":
|
|
1301
|
+
break;
|
|
1302
|
+
// Function call and image generation streaming
|
|
1303
|
+
case "response.output_item.added":
|
|
1304
|
+
if (event.item.type === "function_call") {
|
|
1305
|
+
const callId = event.item.callId || event.item.call_id || "";
|
|
1306
|
+
const name = event.item.name || "";
|
|
1307
|
+
state.toolCalls.set(callId, {
|
|
1308
|
+
id: callId,
|
|
1309
|
+
name,
|
|
1310
|
+
arguments: ""
|
|
1246
1311
|
});
|
|
1247
1312
|
chunks.push({
|
|
1248
1313
|
type: "tool-call-start",
|
|
1249
|
-
id:
|
|
1250
|
-
name
|
|
1314
|
+
id: callId,
|
|
1315
|
+
name
|
|
1251
1316
|
});
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1317
|
+
}
|
|
1318
|
+
if (event.item.type === "image_generation_call") {
|
|
1319
|
+
const itemAny = event.item;
|
|
1320
|
+
const imageId = itemAny.id || itemAny.call_id || itemAny.callId || `img-${state.imageGenerations.size}`;
|
|
1321
|
+
state.imageGenerations.set(imageId, {
|
|
1322
|
+
id: imageId,
|
|
1323
|
+
data: "",
|
|
1324
|
+
status: "in_progress"
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
break;
|
|
1328
|
+
case "response.output_item.done": {
|
|
1329
|
+
const itemAny = event.item;
|
|
1330
|
+
if (itemAny.type === "image_generation_call" && itemAny.result) {
|
|
1331
|
+
const imageId = itemAny.id || itemAny.call_id || itemAny.callId || "";
|
|
1332
|
+
const imageData = itemAny.result;
|
|
1333
|
+
let mediaType = "image/png";
|
|
1334
|
+
if (imageData.startsWith("data:")) {
|
|
1335
|
+
const match = imageData.match(/^data:([^;,]+)/);
|
|
1336
|
+
if (match) mediaType = match[1];
|
|
1337
|
+
}
|
|
1338
|
+
const existing = state.imageGenerations.get(imageId);
|
|
1339
|
+
if (existing) {
|
|
1340
|
+
existing.data = imageData;
|
|
1341
|
+
existing.status = "completed";
|
|
1342
|
+
}
|
|
1343
|
+
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
1344
|
+
chunks.push({
|
|
1345
|
+
type: "image-done",
|
|
1346
|
+
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
1347
|
+
image: {
|
|
1348
|
+
id: imageId || void 0,
|
|
1349
|
+
data: imageData,
|
|
1350
|
+
mediaType,
|
|
1351
|
+
revisedPrompt: itemAny.revised_prompt || itemAny.revisedPrompt || void 0
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
case "response.function_call_arguments.delta": {
|
|
1358
|
+
const itemId = event.itemId || event.item_id || "";
|
|
1359
|
+
const deltaToolCall = Array.from(state.toolCalls.values()).find(
|
|
1360
|
+
(tc) => tc.id === itemId
|
|
1361
|
+
);
|
|
1362
|
+
if (deltaToolCall) {
|
|
1363
|
+
deltaToolCall.arguments += event.delta;
|
|
1254
1364
|
chunks.push({
|
|
1255
1365
|
type: "tool-call-delta",
|
|
1256
|
-
id:
|
|
1257
|
-
argumentsDelta:
|
|
1366
|
+
id: deltaToolCall.id,
|
|
1367
|
+
argumentsDelta: event.delta
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case "response.function_call_arguments.done": {
|
|
1373
|
+
const itemId = event.itemId || event.item_id || "";
|
|
1374
|
+
const doneToolCall = Array.from(state.toolCalls.values()).find(
|
|
1375
|
+
(tc) => tc.id === itemId
|
|
1376
|
+
);
|
|
1377
|
+
if (doneToolCall) {
|
|
1378
|
+
let parsedArgs = {};
|
|
1379
|
+
try {
|
|
1380
|
+
parsedArgs = doneToolCall.arguments ? JSON.parse(doneToolCall.arguments) : {};
|
|
1381
|
+
} catch {
|
|
1382
|
+
}
|
|
1383
|
+
chunks.push({
|
|
1384
|
+
type: "tool-call-done",
|
|
1385
|
+
id: doneToolCall.id,
|
|
1386
|
+
arguments: parsedArgs
|
|
1258
1387
|
});
|
|
1259
1388
|
}
|
|
1389
|
+
break;
|
|
1260
1390
|
}
|
|
1391
|
+
// Response completion
|
|
1392
|
+
case "response.completed": {
|
|
1393
|
+
if (state.hasContent) {
|
|
1394
|
+
chunks.push({ type: "content-done" });
|
|
1395
|
+
}
|
|
1396
|
+
if (state.hasReasoning) {
|
|
1397
|
+
chunks.push({ type: "reasoning-done" });
|
|
1398
|
+
}
|
|
1399
|
+
const completedProvider = event.response.model?.split("/")[0] || void 0;
|
|
1400
|
+
const reasoningDetails = extractReasoningDetails(event.response.output, state.reasoningContent);
|
|
1401
|
+
const generatedImages = extractImages(event.response.output);
|
|
1402
|
+
if (generatedImages && generatedImages.length > 0) {
|
|
1403
|
+
for (let i = 0; i < generatedImages.length; i++) {
|
|
1404
|
+
const img = generatedImages[i];
|
|
1405
|
+
const alreadyEmitted = img.id && state.imageGenerations.get(img.id)?.status === "completed";
|
|
1406
|
+
if (!alreadyEmitted) {
|
|
1407
|
+
chunks.push({
|
|
1408
|
+
type: "image-done",
|
|
1409
|
+
index: i,
|
|
1410
|
+
image: img
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
chunks.push(...providerToolDoneChunksFromServerToolUseOrCitations(
|
|
1416
|
+
event.response.usage,
|
|
1417
|
+
extractResponseWebSearchCitations(event.response.output),
|
|
1418
|
+
event.response.id
|
|
1419
|
+
));
|
|
1420
|
+
chunks.push({
|
|
1421
|
+
type: "finish",
|
|
1422
|
+
finishReason: mapFinishReason(event.response),
|
|
1423
|
+
usage: transformUsage(event.response.usage, completedProvider),
|
|
1424
|
+
responseId: event.response.id,
|
|
1425
|
+
// Used to fetch generation metadata after stream
|
|
1426
|
+
reasoningDetails
|
|
1427
|
+
});
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
case "response.failed":
|
|
1431
|
+
chunks.push({
|
|
1432
|
+
type: "error",
|
|
1433
|
+
error: event.response.error?.message || "Response generation failed",
|
|
1434
|
+
code: event.response.error?.code
|
|
1435
|
+
});
|
|
1436
|
+
break;
|
|
1437
|
+
case "response.incomplete": {
|
|
1438
|
+
if (state.hasContent) {
|
|
1439
|
+
chunks.push({ type: "content-done" });
|
|
1440
|
+
}
|
|
1441
|
+
if (state.hasReasoning) {
|
|
1442
|
+
chunks.push({ type: "reasoning-done" });
|
|
1443
|
+
}
|
|
1444
|
+
const incompleteProvider = event.response.model?.split("/")[0] || void 0;
|
|
1445
|
+
const incompleteReasoningDetails = extractReasoningDetails(event.response.output, state.reasoningContent);
|
|
1446
|
+
const incompleteImages = extractImages(event.response.output);
|
|
1447
|
+
if (incompleteImages && incompleteImages.length > 0) {
|
|
1448
|
+
for (let i = 0; i < incompleteImages.length; i++) {
|
|
1449
|
+
const img = incompleteImages[i];
|
|
1450
|
+
const alreadyEmitted = img.id && state.imageGenerations.get(img.id)?.status === "completed";
|
|
1451
|
+
if (!alreadyEmitted) {
|
|
1452
|
+
chunks.push({
|
|
1453
|
+
type: "image-done",
|
|
1454
|
+
index: i,
|
|
1455
|
+
image: img
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
chunks.push(...providerToolDoneChunksFromServerToolUseOrCitations(
|
|
1461
|
+
event.response.usage,
|
|
1462
|
+
extractResponseWebSearchCitations(event.response.output),
|
|
1463
|
+
event.response.id
|
|
1464
|
+
));
|
|
1465
|
+
chunks.push({
|
|
1466
|
+
type: "finish",
|
|
1467
|
+
finishReason: mapFinishReason(event.response),
|
|
1468
|
+
usage: transformUsage(event.response.usage, incompleteProvider),
|
|
1469
|
+
responseId: event.response.id,
|
|
1470
|
+
// Used to fetch generation metadata after stream
|
|
1471
|
+
reasoningDetails: incompleteReasoningDetails
|
|
1472
|
+
});
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
// Handle reasoning and image events dynamically (SDK types may not include them)
|
|
1476
|
+
default: {
|
|
1477
|
+
const eventType = event.type;
|
|
1478
|
+
const eventAny = event;
|
|
1479
|
+
if (eventType === "response.reasoning.delta") {
|
|
1480
|
+
state.hasReasoning = true;
|
|
1481
|
+
state.reasoningContent += eventAny.delta || "";
|
|
1482
|
+
chunks.push({ type: "reasoning-delta", delta: eventAny.delta || "" });
|
|
1483
|
+
}
|
|
1484
|
+
if (eventType === "response.image_generation_call.partial_image" || eventType === "response.image_generation.partial") {
|
|
1485
|
+
const imageId = eventAny.item_id || eventAny.itemId || eventAny.id || "";
|
|
1486
|
+
const partialData = eventAny.partial_image || eventAny.delta || eventAny.data || "";
|
|
1487
|
+
const existing = state.imageGenerations.get(imageId);
|
|
1488
|
+
if (existing) {
|
|
1489
|
+
existing.data += partialData;
|
|
1490
|
+
} else {
|
|
1491
|
+
state.imageGenerations.set(imageId, {
|
|
1492
|
+
id: imageId,
|
|
1493
|
+
data: partialData,
|
|
1494
|
+
status: "generating"
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
1498
|
+
chunks.push({
|
|
1499
|
+
type: "image-delta",
|
|
1500
|
+
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
1501
|
+
data: partialData
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1504
|
+
if (eventType === "response.image_generation_call.completed" || eventType === "response.image_generation.done") {
|
|
1505
|
+
const imageId = eventAny.item_id || eventAny.itemId || eventAny.id || "";
|
|
1506
|
+
const imageData = eventAny.result || eventAny.image || eventAny.data || "";
|
|
1507
|
+
if (imageData) {
|
|
1508
|
+
let mediaType = "image/png";
|
|
1509
|
+
if (imageData.startsWith("data:")) {
|
|
1510
|
+
const match = imageData.match(/^data:([^;,]+)/);
|
|
1511
|
+
if (match) mediaType = match[1];
|
|
1512
|
+
}
|
|
1513
|
+
const existing = state.imageGenerations.get(imageId);
|
|
1514
|
+
if (existing) {
|
|
1515
|
+
existing.data = imageData;
|
|
1516
|
+
existing.status = "completed";
|
|
1517
|
+
}
|
|
1518
|
+
const imageIndex = Array.from(state.imageGenerations.keys()).indexOf(imageId);
|
|
1519
|
+
chunks.push({
|
|
1520
|
+
type: "image-done",
|
|
1521
|
+
index: imageIndex >= 0 ? imageIndex : state.imageGenerations.size - 1,
|
|
1522
|
+
image: {
|
|
1523
|
+
id: imageId || void 0,
|
|
1524
|
+
data: imageData,
|
|
1525
|
+
mediaType,
|
|
1526
|
+
revisedPrompt: eventAny.revised_prompt || eventAny.revisedPrompt || void 0
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
break;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return chunks;
|
|
1535
|
+
}
|
|
1536
|
+
function createErrorChunk(error, code) {
|
|
1537
|
+
return { type: "error", error, code };
|
|
1538
|
+
}
|
|
1539
|
+
function isBase64Like2(str) {
|
|
1540
|
+
if (str.startsWith("data:")) return true;
|
|
1541
|
+
if (str.length > 200) {
|
|
1542
|
+
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
1543
|
+
return base64Pattern.test(str.substring(0, 200));
|
|
1544
|
+
}
|
|
1545
|
+
return false;
|
|
1546
|
+
}
|
|
1547
|
+
function truncateBase64String2(str, maxLength = 50) {
|
|
1548
|
+
if (str.length <= maxLength) return str;
|
|
1549
|
+
const preview = str.substring(0, maxLength);
|
|
1550
|
+
return `${preview}...[truncated, ${str.length.toLocaleString()} chars]`;
|
|
1551
|
+
}
|
|
1552
|
+
function truncateBase64(obj, maxLength = 50) {
|
|
1553
|
+
if (obj === null || obj === void 0) {
|
|
1554
|
+
return obj;
|
|
1261
1555
|
}
|
|
1262
|
-
if (
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const providerImage = extractImageFromUrl(img.image_url.url, index);
|
|
1266
|
-
state.images.push(providerImage);
|
|
1267
|
-
chunks.push({
|
|
1268
|
-
type: "image-done",
|
|
1269
|
-
index,
|
|
1270
|
-
image: providerImage
|
|
1271
|
-
});
|
|
1556
|
+
if (typeof obj === "string") {
|
|
1557
|
+
if (isBase64Like2(obj)) {
|
|
1558
|
+
return truncateBase64String2(obj, maxLength);
|
|
1272
1559
|
}
|
|
1560
|
+
return obj;
|
|
1273
1561
|
}
|
|
1274
|
-
if (
|
|
1275
|
-
|
|
1276
|
-
if (state.hasContent) {
|
|
1277
|
-
chunks.push({ type: "content-done" });
|
|
1278
|
-
}
|
|
1279
|
-
if (state.hasReasoning) {
|
|
1280
|
-
chunks.push({ type: "reasoning-done" });
|
|
1281
|
-
}
|
|
1282
|
-
for (const tc of state.toolCalls.values()) {
|
|
1283
|
-
let parsedArgs = {};
|
|
1284
|
-
try {
|
|
1285
|
-
parsedArgs = tc.arguments ? JSON.parse(tc.arguments) : {};
|
|
1286
|
-
} catch {
|
|
1287
|
-
}
|
|
1288
|
-
chunks.push({
|
|
1289
|
-
type: "tool-call-done",
|
|
1290
|
-
id: tc.id,
|
|
1291
|
-
arguments: parsedArgs
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1562
|
+
if (Array.isArray(obj)) {
|
|
1563
|
+
return obj.map((item) => truncateBase64(item, maxLength));
|
|
1294
1564
|
}
|
|
1295
|
-
if (
|
|
1296
|
-
const
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
usage: transformChatUsage(chunk.usage, actualProvider),
|
|
1302
|
-
reasoningDetails
|
|
1303
|
-
});
|
|
1565
|
+
if (typeof obj === "object") {
|
|
1566
|
+
const result = {};
|
|
1567
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1568
|
+
result[key] = truncateBase64(value, maxLength);
|
|
1569
|
+
}
|
|
1570
|
+
return result;
|
|
1304
1571
|
}
|
|
1305
|
-
return
|
|
1572
|
+
return obj;
|
|
1306
1573
|
}
|
|
1307
|
-
function
|
|
1574
|
+
function parseRawStreamEvent(jsonStr) {
|
|
1308
1575
|
try {
|
|
1309
|
-
|
|
1576
|
+
const event = JSON.parse(jsonStr);
|
|
1577
|
+
return event;
|
|
1310
1578
|
} catch {
|
|
1311
1579
|
return null;
|
|
1312
1580
|
}
|
|
1313
1581
|
}
|
|
1314
|
-
async function*
|
|
1582
|
+
async function* parseSSEStream(response, state) {
|
|
1315
1583
|
const reader = response.body?.getReader();
|
|
1316
1584
|
if (!reader) {
|
|
1317
1585
|
throw new Error("No response body");
|
|
@@ -1331,11 +1599,11 @@ async function* parseChatSSEStream(response, state) {
|
|
|
1331
1599
|
if (trimmed.startsWith("data: ")) {
|
|
1332
1600
|
const data = trimmed.slice(6);
|
|
1333
1601
|
if (data === "[DONE]") continue;
|
|
1334
|
-
const
|
|
1335
|
-
if (
|
|
1336
|
-
const
|
|
1337
|
-
for (const
|
|
1338
|
-
yield
|
|
1602
|
+
const event = parseRawStreamEvent(data);
|
|
1603
|
+
if (event) {
|
|
1604
|
+
const chunks = processStreamEvent(event, state);
|
|
1605
|
+
for (const chunk of chunks) {
|
|
1606
|
+
yield chunk;
|
|
1339
1607
|
}
|
|
1340
1608
|
}
|
|
1341
1609
|
}
|
|
@@ -1346,118 +1614,74 @@ async function* parseChatSSEStream(response, state) {
|
|
|
1346
1614
|
if (trimmed.startsWith("data: ")) {
|
|
1347
1615
|
const data = trimmed.slice(6);
|
|
1348
1616
|
if (data !== "[DONE]") {
|
|
1349
|
-
const
|
|
1350
|
-
if (
|
|
1351
|
-
const
|
|
1352
|
-
for (const
|
|
1353
|
-
yield
|
|
1617
|
+
const event = parseRawStreamEvent(data);
|
|
1618
|
+
if (event) {
|
|
1619
|
+
const chunks = processStreamEvent(event, state);
|
|
1620
|
+
for (const chunk of chunks) {
|
|
1621
|
+
yield chunk;
|
|
1354
1622
|
}
|
|
1355
1623
|
}
|
|
1356
1624
|
}
|
|
1357
1625
|
}
|
|
1358
1626
|
}
|
|
1359
|
-
if (state.finishReason && !state.toolCalls.size) {
|
|
1360
|
-
}
|
|
1361
1627
|
} finally {
|
|
1362
1628
|
reader.releaseLock();
|
|
1363
1629
|
}
|
|
1364
1630
|
}
|
|
1365
|
-
function
|
|
1366
|
-
const
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const toolChoice = transformChatToolChoice(request.toolChoice);
|
|
1374
|
-
if (toolChoice !== void 0) {
|
|
1375
|
-
params.tool_choice = toolChoice;
|
|
1376
|
-
}
|
|
1377
|
-
if (request.parallelToolCalls !== void 0) {
|
|
1378
|
-
params.parallel_tool_calls = request.parallelToolCalls;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
if (request.maxOutputTokens !== void 0) {
|
|
1382
|
-
params.max_tokens = request.maxOutputTokens;
|
|
1383
|
-
}
|
|
1384
|
-
if (request.temperature !== void 0) {
|
|
1385
|
-
params.temperature = request.temperature;
|
|
1386
|
-
}
|
|
1387
|
-
if (request.topP !== void 0) {
|
|
1388
|
-
params.top_p = request.topP;
|
|
1389
|
-
}
|
|
1390
|
-
if (request.reasoning?.level !== void 0) {
|
|
1391
|
-
const effortMap = {
|
|
1392
|
-
10: "minimal",
|
|
1393
|
-
33: "low",
|
|
1394
|
-
66: "medium",
|
|
1395
|
-
100: "high"
|
|
1396
|
-
};
|
|
1397
|
-
const effort = effortMap[request.reasoning.level];
|
|
1398
|
-
if (effort) {
|
|
1399
|
-
params.reasoning = { effort };
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
if (request.responseFormat) {
|
|
1403
|
-
if (request.responseFormat.type === "json") {
|
|
1404
|
-
if (request.responseFormat.schema) {
|
|
1405
|
-
params.response_format = {
|
|
1406
|
-
type: "json_schema",
|
|
1407
|
-
json_schema: {
|
|
1408
|
-
name: "response",
|
|
1409
|
-
schema: request.responseFormat.schema,
|
|
1410
|
-
strict: true
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
} else {
|
|
1414
|
-
params.response_format = { type: "json_object" };
|
|
1631
|
+
async function fetchGenerationMetadata(apiKey, generationId, baseUrl = "https://openrouter.ai/api/v1", signal) {
|
|
1632
|
+
const url = `${baseUrl}/generation?id=${encodeURIComponent(generationId)}`;
|
|
1633
|
+
const maxRetries = 3;
|
|
1634
|
+
const delays = [500, 1e3, 2e3];
|
|
1635
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1636
|
+
try {
|
|
1637
|
+
if (attempt > 0) {
|
|
1638
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt - 1]));
|
|
1415
1639
|
}
|
|
1640
|
+
const response = await fetch(url, {
|
|
1641
|
+
method: "GET",
|
|
1642
|
+
headers: {
|
|
1643
|
+
"Authorization": `Bearer ${apiKey}`
|
|
1644
|
+
},
|
|
1645
|
+
signal
|
|
1646
|
+
});
|
|
1647
|
+
if (response.status === 404 && attempt < maxRetries - 1) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (!response.ok) {
|
|
1651
|
+
console.error(`Failed to fetch generation metadata: ${response.status}`);
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
const result = await response.json();
|
|
1655
|
+
if (result.error) {
|
|
1656
|
+
console.error("Generation metadata error:", result.error.message);
|
|
1657
|
+
return null;
|
|
1658
|
+
}
|
|
1659
|
+
const data = result.data;
|
|
1660
|
+
if (!data) {
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
return {
|
|
1664
|
+
id: data.id || generationId,
|
|
1665
|
+
totalCost: data.total_cost || 0,
|
|
1666
|
+
promptTokens: data.tokens_prompt || 0,
|
|
1667
|
+
completionTokens: data.tokens_completion || 0,
|
|
1668
|
+
nativePromptTokens: data.native_tokens_prompt,
|
|
1669
|
+
nativeCompletionTokens: data.native_tokens_completion,
|
|
1670
|
+
providerName: data.provider_name || "unknown",
|
|
1671
|
+
latency: data.latency || data.generation_time,
|
|
1672
|
+
model: data.model,
|
|
1673
|
+
finishReason: data.finish_reason,
|
|
1674
|
+
createdAt: data.created_at
|
|
1675
|
+
};
|
|
1676
|
+
} catch (error) {
|
|
1677
|
+
if (attempt < maxRetries - 1) {
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
console.error("Error fetching generation metadata:", error);
|
|
1681
|
+
return null;
|
|
1416
1682
|
}
|
|
1417
1683
|
}
|
|
1418
|
-
|
|
1419
|
-
const { _metadata, ...safeOptions } = request.providerOptions;
|
|
1420
|
-
Object.assign(params, safeOptions);
|
|
1421
|
-
}
|
|
1422
|
-
return params;
|
|
1423
|
-
}
|
|
1424
|
-
function createChatErrorChunk(error, code) {
|
|
1425
|
-
return { type: "error", error, code };
|
|
1426
|
-
}
|
|
1427
|
-
function isBase64Like2(str) {
|
|
1428
|
-
if (str.startsWith("data:")) return true;
|
|
1429
|
-
if (str.length > 200) {
|
|
1430
|
-
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
1431
|
-
return base64Pattern.test(str.substring(0, 200));
|
|
1432
|
-
}
|
|
1433
|
-
return false;
|
|
1434
|
-
}
|
|
1435
|
-
function truncateBase64String2(str, maxLength = 50) {
|
|
1436
|
-
if (str.length <= maxLength) return str;
|
|
1437
|
-
const preview = str.substring(0, maxLength);
|
|
1438
|
-
return `${preview}...[truncated, ${str.length.toLocaleString()} chars]`;
|
|
1439
|
-
}
|
|
1440
|
-
function truncateChatBase64(obj, maxLength = 50) {
|
|
1441
|
-
if (obj === null || obj === void 0) {
|
|
1442
|
-
return obj;
|
|
1443
|
-
}
|
|
1444
|
-
if (typeof obj === "string") {
|
|
1445
|
-
if (isBase64Like2(obj)) {
|
|
1446
|
-
return truncateBase64String2(obj, maxLength);
|
|
1447
|
-
}
|
|
1448
|
-
return obj;
|
|
1449
|
-
}
|
|
1450
|
-
if (Array.isArray(obj)) {
|
|
1451
|
-
return obj.map((item) => truncateChatBase64(item, maxLength));
|
|
1452
|
-
}
|
|
1453
|
-
if (typeof obj === "object") {
|
|
1454
|
-
const result = {};
|
|
1455
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
1456
|
-
result[key] = truncateChatBase64(value, maxLength);
|
|
1457
|
-
}
|
|
1458
|
-
return result;
|
|
1459
|
-
}
|
|
1460
|
-
return obj;
|
|
1684
|
+
return null;
|
|
1461
1685
|
}
|
|
1462
1686
|
|
|
1463
1687
|
// src/vendor-icons/google.svg
|
|
@@ -1594,6 +1818,40 @@ var OpenRouterProvider = class _OpenRouterProvider {
|
|
|
1594
1818
|
constructor(config) {
|
|
1595
1819
|
this.config = config;
|
|
1596
1820
|
}
|
|
1821
|
+
static SERVER_TOOL_RESULT = {
|
|
1822
|
+
status: "success",
|
|
1823
|
+
result: "Handled by OpenRouter"
|
|
1824
|
+
};
|
|
1825
|
+
static TOOLS = {
|
|
1826
|
+
web_search: defineTool({
|
|
1827
|
+
description: "Search the web for up-to-date information using OpenRouter server-side web search",
|
|
1828
|
+
args: z.object({}),
|
|
1829
|
+
execute: async () => _OpenRouterProvider.SERVER_TOOL_RESULT,
|
|
1830
|
+
executionMode: "provider",
|
|
1831
|
+
executionProvider: "openrouter"
|
|
1832
|
+
}),
|
|
1833
|
+
web_fetch: defineTool({
|
|
1834
|
+
description: "Fetch and extract content from URLs using OpenRouter server-side web fetch",
|
|
1835
|
+
args: z.object({}),
|
|
1836
|
+
execute: async () => _OpenRouterProvider.SERVER_TOOL_RESULT,
|
|
1837
|
+
executionMode: "provider",
|
|
1838
|
+
executionProvider: "openrouter"
|
|
1839
|
+
}),
|
|
1840
|
+
datetime: defineTool({
|
|
1841
|
+
description: "Get the current date and time using OpenRouter server-side datetime",
|
|
1842
|
+
args: z.object({}),
|
|
1843
|
+
execute: async () => _OpenRouterProvider.SERVER_TOOL_RESULT,
|
|
1844
|
+
executionMode: "provider",
|
|
1845
|
+
executionProvider: "openrouter"
|
|
1846
|
+
}),
|
|
1847
|
+
image_generation: defineTool({
|
|
1848
|
+
description: "Generate images using OpenRouter server-side image generation",
|
|
1849
|
+
args: z.object({}),
|
|
1850
|
+
execute: async () => _OpenRouterProvider.SERVER_TOOL_RESULT,
|
|
1851
|
+
executionMode: "provider",
|
|
1852
|
+
executionProvider: "openrouter"
|
|
1853
|
+
})
|
|
1854
|
+
};
|
|
1597
1855
|
/**
|
|
1598
1856
|
* Determine which API to use for a request.
|
|
1599
1857
|
* Checks request-level providerOptions first, then falls back to config.
|
|
@@ -1616,9 +1874,44 @@ var OpenRouterProvider = class _OpenRouterProvider {
|
|
|
1616
1874
|
}
|
|
1617
1875
|
return this.client;
|
|
1618
1876
|
}
|
|
1877
|
+
applyConfiguredProvidersToResponses(params) {
|
|
1878
|
+
if (this.config.providers && this.config.providers.length > 0) {
|
|
1879
|
+
params.provider = { only: this.config.providers };
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
hasOpenRouterServerTools(params) {
|
|
1883
|
+
return params.tools?.some(isOpenRouterServerTool) ?? false;
|
|
1884
|
+
}
|
|
1885
|
+
toSdkResponsesRequest(params) {
|
|
1886
|
+
const { tools, ...rest } = params;
|
|
1887
|
+
if (!tools) {
|
|
1888
|
+
return rest;
|
|
1889
|
+
}
|
|
1890
|
+
const sdkTools = tools.filter(
|
|
1891
|
+
(tool) => !isOpenRouterServerTool(tool)
|
|
1892
|
+
);
|
|
1893
|
+
return {
|
|
1894
|
+
...rest,
|
|
1895
|
+
tools: sdkTools
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
async throwOpenRouterFetchError(response) {
|
|
1899
|
+
const errorText = await response.text();
|
|
1900
|
+
let errorMessage = `OpenRouter API error: ${response.status}`;
|
|
1901
|
+
try {
|
|
1902
|
+
const errorJson = JSON.parse(errorText);
|
|
1903
|
+
errorMessage = errorJson.error?.message || errorJson.message || errorMessage;
|
|
1904
|
+
} catch {
|
|
1905
|
+
errorMessage = errorText || errorMessage;
|
|
1906
|
+
}
|
|
1907
|
+
throw new ProviderError(errorMessage, "invalid_request", response.status);
|
|
1908
|
+
}
|
|
1619
1909
|
supportsModel(_modelId) {
|
|
1620
1910
|
return true;
|
|
1621
1911
|
}
|
|
1912
|
+
getTools(_modelId) {
|
|
1913
|
+
return { ..._OpenRouterProvider.TOOLS };
|
|
1914
|
+
}
|
|
1622
1915
|
/**
|
|
1623
1916
|
* Get the icon for a model as a data URI.
|
|
1624
1917
|
* Extracts the AI lab/organization from the model ID prefix and returns
|
|
@@ -1788,14 +2081,33 @@ var OpenRouterProvider = class _OpenRouterProvider {
|
|
|
1788
2081
|
* Generate using the Responses API (beta).
|
|
1789
2082
|
*/
|
|
1790
2083
|
async generateWithResponses(request) {
|
|
1791
|
-
const
|
|
2084
|
+
const apiKey = this.config.apiKey;
|
|
2085
|
+
const baseUrl = this.config.baseUrl || "https://openrouter.ai/api/v1";
|
|
1792
2086
|
try {
|
|
1793
2087
|
const params = buildCreateParams(request);
|
|
1794
|
-
|
|
1795
|
-
|
|
2088
|
+
this.applyConfiguredProvidersToResponses(params);
|
|
2089
|
+
if (this.hasOpenRouterServerTools(params)) {
|
|
2090
|
+
const response2 = await fetch(`${baseUrl}/responses`, {
|
|
2091
|
+
method: "POST",
|
|
2092
|
+
headers: {
|
|
2093
|
+
"Content-Type": "application/json",
|
|
2094
|
+
"Authorization": `Bearer ${apiKey}`
|
|
2095
|
+
},
|
|
2096
|
+
body: JSON.stringify({
|
|
2097
|
+
...params,
|
|
2098
|
+
stream: false
|
|
2099
|
+
}),
|
|
2100
|
+
signal: request.signal
|
|
2101
|
+
});
|
|
2102
|
+
if (!response2.ok) {
|
|
2103
|
+
await this.throwOpenRouterFetchError(response2);
|
|
2104
|
+
}
|
|
2105
|
+
const data = await response2.json();
|
|
2106
|
+
return transformResponse(data);
|
|
1796
2107
|
}
|
|
2108
|
+
const client = await this.getClient();
|
|
1797
2109
|
const response = await client.beta.responses.send({
|
|
1798
|
-
...params,
|
|
2110
|
+
...this.toSdkResponsesRequest(params),
|
|
1799
2111
|
stream: false
|
|
1800
2112
|
});
|
|
1801
2113
|
return transformResponse(response);
|
|
@@ -1890,9 +2202,7 @@ var OpenRouterProvider = class _OpenRouterProvider {
|
|
|
1890
2202
|
const baseUrl = this.config.baseUrl || "https://openrouter.ai/api/v1";
|
|
1891
2203
|
try {
|
|
1892
2204
|
const params = buildCreateParams(request);
|
|
1893
|
-
|
|
1894
|
-
params.provider = { only: this.config.providers };
|
|
1895
|
-
}
|
|
2205
|
+
this.applyConfiguredProvidersToResponses(params);
|
|
1896
2206
|
const response = await fetch(`${baseUrl}/responses`, {
|
|
1897
2207
|
method: "POST",
|
|
1898
2208
|
headers: {
|
|
@@ -2050,52 +2360,52 @@ var OpenRouterProvider = class _OpenRouterProvider {
|
|
|
2050
2360
|
};
|
|
2051
2361
|
|
|
2052
2362
|
// src/providerOptions.ts
|
|
2053
|
-
import { z } from "zod";
|
|
2054
|
-
var percentileSchema =
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
p50:
|
|
2058
|
-
p75:
|
|
2059
|
-
p90:
|
|
2060
|
-
p99:
|
|
2363
|
+
import { z as z2 } from "zod";
|
|
2364
|
+
var percentileSchema = z2.union([
|
|
2365
|
+
z2.number(),
|
|
2366
|
+
z2.object({
|
|
2367
|
+
p50: z2.number().optional(),
|
|
2368
|
+
p75: z2.number().optional(),
|
|
2369
|
+
p90: z2.number().optional(),
|
|
2370
|
+
p99: z2.number().optional()
|
|
2061
2371
|
})
|
|
2062
2372
|
]);
|
|
2063
|
-
var providerRoutingSchema =
|
|
2373
|
+
var providerRoutingSchema = z2.object({
|
|
2064
2374
|
/** Provider slugs to try in order (e.g., ['anthropic', 'openai']) */
|
|
2065
|
-
order:
|
|
2375
|
+
order: z2.array(z2.string()).optional(),
|
|
2066
2376
|
/** Allow fallback to other providers if preferred unavailable (default: true) */
|
|
2067
|
-
allow_fallbacks:
|
|
2377
|
+
allow_fallbacks: z2.boolean().optional(),
|
|
2068
2378
|
/** Only use providers that support all parameters in the request */
|
|
2069
|
-
require_parameters:
|
|
2379
|
+
require_parameters: z2.boolean().optional(),
|
|
2070
2380
|
/** Control data storage policies: 'allow' or 'deny' */
|
|
2071
|
-
data_collection:
|
|
2381
|
+
data_collection: z2.enum(["allow", "deny"]).optional(),
|
|
2072
2382
|
/** Restrict to Zero Data Retention endpoints only */
|
|
2073
|
-
zdr:
|
|
2383
|
+
zdr: z2.boolean().optional(),
|
|
2074
2384
|
/** Restrict to these providers only (exclusive list) */
|
|
2075
|
-
only:
|
|
2385
|
+
only: z2.array(z2.string()).optional(),
|
|
2076
2386
|
/** Skip these providers */
|
|
2077
|
-
ignore:
|
|
2387
|
+
ignore: z2.array(z2.string()).optional(),
|
|
2078
2388
|
/** Sort providers by price, throughput, or latency */
|
|
2079
|
-
sort:
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
by:
|
|
2083
|
-
partition:
|
|
2389
|
+
sort: z2.union([
|
|
2390
|
+
z2.enum(["price", "throughput", "latency"]),
|
|
2391
|
+
z2.object({
|
|
2392
|
+
by: z2.enum(["price", "throughput", "latency"]),
|
|
2393
|
+
partition: z2.enum(["model", "none"]).optional()
|
|
2084
2394
|
})
|
|
2085
2395
|
]).optional(),
|
|
2086
2396
|
/** Maximum price constraints (hard limit - request fails if no provider meets threshold) */
|
|
2087
|
-
max_price:
|
|
2397
|
+
max_price: z2.object({
|
|
2088
2398
|
/** Max price per million prompt tokens */
|
|
2089
|
-
prompt:
|
|
2399
|
+
prompt: z2.number().optional(),
|
|
2090
2400
|
/** Max price per million completion tokens */
|
|
2091
|
-
completion:
|
|
2401
|
+
completion: z2.number().optional(),
|
|
2092
2402
|
/** Max price per request */
|
|
2093
|
-
request:
|
|
2403
|
+
request: z2.number().optional(),
|
|
2094
2404
|
/** Max price per image */
|
|
2095
|
-
image:
|
|
2405
|
+
image: z2.number().optional()
|
|
2096
2406
|
}).optional(),
|
|
2097
2407
|
/** Allowed quantization levels */
|
|
2098
|
-
quantizations:
|
|
2408
|
+
quantizations: z2.array(z2.enum([
|
|
2099
2409
|
"int4",
|
|
2100
2410
|
"int8",
|
|
2101
2411
|
"fp4",
|
|
@@ -2111,9 +2421,16 @@ var providerRoutingSchema = z.object({
|
|
|
2111
2421
|
/** Maximum latency in seconds (soft preference) */
|
|
2112
2422
|
preferred_max_latency: percentileSchema.optional(),
|
|
2113
2423
|
/** Restrict to models allowing text distillation */
|
|
2114
|
-
enforce_distillable_text:
|
|
2424
|
+
enforce_distillable_text: z2.boolean().optional()
|
|
2115
2425
|
}).passthrough();
|
|
2116
|
-
var
|
|
2426
|
+
var serverToolParametersSchema = z2.record(z2.string(), z2.unknown());
|
|
2427
|
+
var serverToolsSchema = z2.object({
|
|
2428
|
+
web_search: serverToolParametersSchema.optional(),
|
|
2429
|
+
web_fetch: serverToolParametersSchema.optional(),
|
|
2430
|
+
datetime: serverToolParametersSchema.optional(),
|
|
2431
|
+
image_generation: serverToolParametersSchema.optional()
|
|
2432
|
+
}).partial();
|
|
2433
|
+
var openrouterProviderOptions = z2.object({
|
|
2117
2434
|
/** Provider routing configuration */
|
|
2118
2435
|
provider: providerRoutingSchema.optional(),
|
|
2119
2436
|
/**
|
|
@@ -2125,7 +2442,14 @@ var openrouterProviderOptions = z.object({
|
|
|
2125
2442
|
*
|
|
2126
2443
|
* @default false
|
|
2127
2444
|
*/
|
|
2128
|
-
useResponsesApi:
|
|
2445
|
+
useResponsesApi: z2.boolean().optional(),
|
|
2446
|
+
/**
|
|
2447
|
+
* Parameters for OpenRouter provider-executed server tools.
|
|
2448
|
+
*
|
|
2449
|
+
* Tool availability is controlled by model `providerTools`; this object only
|
|
2450
|
+
* supplies per-tool OpenRouter options.
|
|
2451
|
+
*/
|
|
2452
|
+
serverTools: serverToolsSchema.optional()
|
|
2129
2453
|
}).passthrough();
|
|
2130
2454
|
|
|
2131
2455
|
// src/index.ts
|