@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.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/responses-transformers.ts
5
- function transformContentPart(part) {
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: "input_text", text: part.text };
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: "input_image",
14
- image_url: imageUrl,
15
- detail: part.detail || "auto"
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: "input_image",
23
- image_url: url,
24
- detail
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: "input_text",
179
+ type: "text",
29
180
  text: `[File: ${part.filename || "file"}]`
30
181
  };
31
182
  }
32
- function transformMessageContent(content) {
183
+ function transformChatMessageContent(content) {
33
184
  if (typeof content === "string") {
34
185
  return content;
35
186
  }
36
- return content.map(transformContentPart);
187
+ return content.map(transformChatContentPart);
37
188
  }
38
- function transformUserMessage(msg) {
39
- const content = transformMessageContent(msg.content);
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: typeof content === "string" ? content : content
198
+ content: transformChatMessageContent(msg.content)
43
199
  };
44
200
  }
45
- function transformAssistantMessage(msg) {
46
- const items = [];
47
- const reasoningDetails = msg.reasoningDetails;
48
- if (reasoningDetails && reasoningDetails.length > 0) {
49
- const summaryTexts = [];
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
- for (const tc of msg.toolCalls) {
80
- const functionCall = {
81
- type: "function_call",
82
- id: `fc_${tc.id}`,
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
- status: "completed"
88
- // Mark as completed since this is from history
89
- };
90
- items.push(functionCall);
91
- }
212
+ arguments: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments)
213
+ }
214
+ }));
92
215
  }
93
- return items;
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 transformToolMessage(msg) {
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 toolOutput = {
112
- type: "function_call_output",
113
- call_id: msg.toolCallId,
114
- output: output || (imageAttachments.length > 0 ? "Success" : "")
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 { toolOutput };
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: "input_text",
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: "input_image",
133
- image_url: imageData,
134
- detail: "auto"
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 { toolOutput, syntheticUserMessage };
276
+ return { toolMessage, syntheticUserMessage };
142
277
  }
143
- function transformMessages(messages) {
144
- let instructions;
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
- instructions = instructions ? `${instructions}
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
- input.push(transformUserMessage(msg));
286
+ result.push(transformChatUserMessage(msg));
160
287
  break;
161
288
  case "assistant":
162
- input.push(...transformAssistantMessage(msg));
289
+ result.push(transformChatAssistantMessage(msg));
163
290
  break;
164
291
  case "tool": {
165
- const result = transformToolMessage(msg);
166
- input.push(result.toolOutput);
167
- if (result.syntheticUserMessage) {
168
- input.push(result.syntheticUserMessage);
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 { input, instructions };
301
+ return result;
175
302
  }
176
- function transformTool(tool) {
177
- const inputParams = tool.function.parameters;
178
- let parameters;
179
- if (inputParams && typeof inputParams === "object") {
180
- parameters = {
181
- ...inputParams,
182
- additionalProperties: false
183
- };
184
- } else {
185
- parameters = {
186
- type: "object",
187
- properties: {},
188
- required: [],
189
- additionalProperties: false
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
- name: tool.function.name,
195
- description: tool.function.description || void 0,
196
- parameters,
197
- strict: true
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 transformTools(tools) {
201
- return tools.map(transformTool);
357
+ function transformChatTools(tools, providerOptions) {
358
+ return tools.map((tool) => transformChatTool(tool, providerOptions));
202
359
  }
203
- function transformToolChoice(choice) {
360
+ function transformChatToolChoice(choice) {
204
361
  if (choice === "auto") {
205
- return "auto";
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 "auto";
373
+ return void 0;
217
374
  }
218
- function mapFinishReason(response) {
219
- if (response.status === "failed") {
220
- return "error";
221
- }
222
- if (response.status === "incomplete") {
223
- if (response.incompleteDetails?.reason === "max_output_tokens") {
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 extractToolCalls(output) {
252
- const toolCalls = [];
253
- for (const item of output) {
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.length > 0 ? toolCalls : void 0;
268
- }
269
- function extractImages(output) {
270
- const images = [];
271
- for (const item of output) {
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
- return images.length > 0 ? images : void 0;
401
+ return {
402
+ id: tc.id,
403
+ name: tc.function.name,
404
+ arguments: parsedArgs
405
+ };
406
+ });
315
407
  }
316
- function transformUsage(usage, actualProvider) {
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 inputTokens = usage.native_tokens_prompt || usage.nativeTokensPrompt || usage.input_tokens || usage.inputTokens || 0;
325
- const outputTokens = usage.native_tokens_completion || usage.nativeTokensCompletion || usage.output_tokens || usage.outputTokens || 0;
326
- const totalTokens = usage.total_tokens || usage.totalTokens || 0;
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: inputTokens,
331
- completionTokens: outputTokens,
332
- totalTokens: totalTokens || inputTokens + outputTokens,
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 transformResponse(response) {
340
- const content = extractTextContent(response.output);
341
- const toolCalls = extractToolCalls(response.output);
342
- const images = extractImages(response.output);
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
- finishReason: mapFinishReason(response),
349
- usage: transformUsage(response.usage, actualProvider),
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 extractReasoningDetails(output, streamedReasoningContent) {
359
- const details = [];
360
- if (output && Array.isArray(output)) {
361
- for (const item of output) {
362
- const itemAny = item;
363
- if (itemAny.type === "reasoning") {
364
- if (itemAny.summary && Array.isArray(itemAny.summary)) {
365
- for (const step of itemAny.summary) {
366
- if (typeof step === "string") {
367
- details.push({ type: "reasoning.text", text: step });
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
- if (instructions) {
409
- params.instructions = instructions;
410
- }
411
- if (request.tools && request.tools.length > 0) {
412
- params.tools = transformTools(request.tools);
413
- if (request.toolChoice !== void 0) {
414
- params.toolChoice = transformToolChoice(request.toolChoice);
415
- }
416
- if (request.parallelToolCalls !== void 0) {
417
- params.parallelToolCalls = request.parallelToolCalls;
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
- if (request.maxOutputTokens !== void 0) {
421
- params.maxOutputTokens = request.maxOutputTokens;
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 (request.temperature !== void 0) {
424
- params.temperature = request.temperature;
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.topP = request.topP;
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.text = {
451
- format: {
452
- type: "json_schema",
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.text = {
460
- format: { type: "json_object" }
461
- };
738
+ params.response_format = { type: "json_object" };
462
739
  }
463
740
  }
464
741
  }
465
- if (request.providerOptions) {
466
- Object.assign(params, request.providerOptions);
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 createStreamState() {
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 truncateBase64(obj, maxLength = 50) {
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) => truncateBase64(item, maxLength));
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] = truncateBase64(value, maxLength);
782
+ result[key] = truncateChatBase64(value, maxLength);
756
783
  }
757
784
  return result;
758
785
  }
759
786
  return obj;
760
787
  }
761
- function parseRawStreamEvent(jsonStr) {
762
- try {
763
- const event = JSON.parse(jsonStr);
764
- return event;
765
- } catch {
766
- return null;
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
- async function* parseSSEStream(response, state) {
770
- const reader = response.body?.getReader();
771
- if (!reader) {
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
- async function fetchGenerationMetadata(apiKey, generationId, baseUrl = "https://openrouter.ai/api/v1", signal) {
819
- const url = `${baseUrl}/generation?id=${encodeURIComponent(generationId)}`;
820
- const maxRetries = 3;
821
- const delays = [500, 1e3, 2e3];
822
- for (let attempt = 0; attempt < maxRetries; attempt++) {
823
- try {
824
- if (attempt > 0) {
825
- await new Promise((resolve) => setTimeout(resolve, delays[attempt - 1]));
826
- }
827
- const response = await fetch(url, {
828
- method: "GET",
829
- headers: {
830
- "Authorization": `Bearer ${apiKey}`
831
- },
832
- signal
833
- });
834
- if (response.status === 404 && attempt < maxRetries - 1) {
835
- continue;
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
- return {
851
- id: data.id || generationId,
852
- totalCost: data.total_cost || 0,
853
- promptTokens: data.tokens_prompt || 0,
854
- completionTokens: data.tokens_completion || 0,
855
- nativePromptTokens: data.native_tokens_prompt,
856
- nativeCompletionTokens: data.native_tokens_completion,
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
- } catch (error) {
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
- return null;
872
- }
873
-
874
- // src/chat-completions-transformers.ts
875
- function transformChatContentPart(part) {
876
- if (part.type === "text") {
877
- return { type: "text", text: part.text };
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
- message.tool_calls = msg.toolCalls.map((tc) => ({
929
- id: tc.id,
930
- type: "function",
931
- function: {
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: typeof tc.arguments === "string" ? tc.arguments : JSON.stringify(tc.arguments)
934
- }
935
- }));
936
- }
937
- if (msg.reasoningDetails && msg.reasoningDetails.length > 0) {
938
- message.reasoning_details = msg.reasoningDetails.map((detail) => {
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 message;
881
+ return items;
948
882
  }
949
- function transformChatToolMessage(msg) {
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 toolMessage = {
966
- role: "tool",
967
- tool_call_id: msg.toolCallId,
968
- content: output || (imageAttachments.length > 0 ? "Success" : "")
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 { toolMessage };
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: "text",
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: "image_url",
987
- image_url: {
988
- url: imageData,
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 { toolMessage, syntheticUserMessage };
929
+ return { toolOutput, syntheticUserMessage };
998
930
  }
999
- function transformChatMessages(messages) {
1000
- const result = [];
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
- result.push(transformChatSystemMessage(msg));
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
- result.push(transformChatUserMessage(msg));
947
+ input.push(transformUserMessage(msg));
1008
948
  break;
1009
949
  case "assistant":
1010
- result.push(transformChatAssistantMessage(msg));
950
+ input.push(...transformAssistantMessage(msg));
1011
951
  break;
1012
952
  case "tool": {
1013
- const { toolMessage, syntheticUserMessage } = transformChatToolMessage(msg);
1014
- result.push(toolMessage);
1015
- if (syntheticUserMessage) {
1016
- result.push(syntheticUserMessage);
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 result;
962
+ return { input, instructions };
1023
963
  }
1024
- function transformChatTool(tool) {
1025
- const inputParams = tool.function.parameters;
1026
- let parameters;
1027
- if (inputParams && typeof inputParams === "object") {
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
- function: {
1043
- name: tool.function.name,
1044
- description: tool.function.description || void 0,
1045
- parameters,
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 transformChatTools(tools) {
1051
- return tools.map(transformChatTool);
979
+ function transformTools(tools, providerOptions) {
980
+ return tools.map((tool) => transformTool(tool, providerOptions));
1052
981
  }
1053
- function transformChatToolChoice(choice) {
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", function: { name: choice.name } };
993
+ return { type: "function", name: choice.name };
1065
994
  }
1066
- return void 0;
995
+ return "auto";
1067
996
  }
1068
- function mapChatFinishReason(finishReason) {
1069
- switch (finishReason) {
1070
- case "stop":
1071
- return "stop";
1072
- case "tool_calls":
1073
- return "tool_calls";
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
- case "content_filter":
1004
+ }
1005
+ if (response.incompleteDetails?.reason === "content_filter") {
1077
1006
  return "content_filter";
1078
- case "error":
1079
- return "error";
1080
- default:
1081
- return "stop";
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 extractChatToolCalls(toolCalls) {
1085
- if (!toolCalls || toolCalls.length === 0) {
1086
- return void 0;
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 toolCalls.map((tc) => {
1089
- let parsedArgs = {};
1090
- try {
1091
- parsedArgs = tc.function.arguments ? JSON.parse(tc.function.arguments) : {};
1092
- } catch {
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
- return {
1095
- id: tc.id,
1096
- name: tc.function.name,
1097
- arguments: parsedArgs
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 transformChatUsage(usage, actualProvider) {
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 promptTokens = usage.native_tokens_prompt || usage.prompt_tokens || 0;
1110
- const completionTokens = usage.native_tokens_completion || usage.completion_tokens || 0;
1111
- const totalTokens = usage.total_tokens || promptTokens + completionTokens;
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: usage.completion_tokens_details?.reasoning_tokens,
1117
- cachedTokens: usage.prompt_tokens_details?.cached_tokens,
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 extractImageFromUrl(url, index) {
1123
- if (url.startsWith("data:")) {
1124
- const match = url.match(/^data:([^;]+);base64,(.+)$/);
1125
- if (match) {
1126
- return {
1127
- id: `image_${index}`,
1128
- data: match[2],
1129
- // base64 data without prefix
1130
- mediaType: match[1]
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
- finishReason: mapChatFinishReason(choice?.finish_reason),
1164
- usage: transformChatUsage(response.usage, actualProvider),
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 createChatStreamState() {
1174
- return {
1175
- content: "",
1176
- toolCalls: /* @__PURE__ */ new Map(),
1177
- images: [],
1178
- reasoningContent: "",
1179
- hasContent: false,
1180
- hasReasoning: false,
1181
- finishReason: null,
1182
- reasoningDetails: []
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 accumulateReasoningDetail(state, detail) {
1186
- const idx = detail.index ?? 0;
1187
- const textContent = detail.text || detail.summary || "";
1188
- const detailType = detail.type === "reasoning.summary" ? "summary" : "text";
1189
- const existing = state.reasoningDetails.find(
1190
- (d) => d.type === detailType && d._index === idx
1191
- );
1192
- if (existing && existing.text !== void 0) {
1193
- existing.text += textContent;
1194
- } else {
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
- function processChatStreamChunk(chunk, state) {
1204
- const chunks = [];
1205
- const choice = chunk.choices?.[0];
1206
- const delta = choice?.delta;
1207
- if (!delta && !choice?.finish_reason && !chunk.usage) {
1208
- return chunks;
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 (delta?.content) {
1211
- state.hasContent = true;
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
- const reasoningText = delta?.reasoning ?? delta?.reasoning_content;
1216
- if (reasoningText) {
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 (delta?.reasoning_details && Array.isArray(delta.reasoning_details)) {
1222
- for (const detail of delta.reasoning_details) {
1223
- if (detail.type === "reasoning.text") {
1224
- accumulateReasoningDetail(state, detail);
1225
- } else if (detail.type === "reasoning.summary") {
1226
- accumulateReasoningDetail(state, detail);
1227
- } else if (detail.type === "reasoning.encrypted") {
1228
- state.reasoningDetails.push({
1229
- type: "encrypted",
1230
- id: detail.id,
1231
- data: detail.data,
1232
- format: detail.format
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
- if (delta?.tool_calls) {
1238
- for (const tc of delta.tool_calls) {
1239
- const index = tc.index;
1240
- const existing = state.toolCalls.get(index);
1241
- if (tc.id && tc.function?.name) {
1242
- state.toolCalls.set(index, {
1243
- id: tc.id,
1244
- name: tc.function.name,
1245
- arguments: tc.function.arguments || ""
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: tc.id,
1250
- name: tc.function.name
1314
+ id: callId,
1315
+ name
1251
1316
  });
1252
- } else if (existing && tc.function?.arguments) {
1253
- existing.arguments += tc.function.arguments;
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: existing.id,
1257
- argumentsDelta: tc.function.arguments
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 (delta?.images && delta.images.length > 0) {
1263
- for (const img of delta.images) {
1264
- const index = state.images.length;
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 (choice?.finish_reason) {
1275
- state.finishReason = choice.finish_reason;
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 (chunk.usage) {
1296
- const actualProvider = chunk.model?.split("/")[0] || void 0;
1297
- const reasoningDetails = state.reasoningDetails.length > 0 ? state.reasoningDetails.map(({ _index, ...d }) => d) : void 0;
1298
- chunks.push({
1299
- type: "finish",
1300
- finishReason: mapChatFinishReason(state.finishReason),
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 chunks;
1572
+ return obj;
1306
1573
  }
1307
- function parseChatStreamEvent(jsonStr) {
1574
+ function parseRawStreamEvent(jsonStr) {
1308
1575
  try {
1309
- return JSON.parse(jsonStr);
1576
+ const event = JSON.parse(jsonStr);
1577
+ return event;
1310
1578
  } catch {
1311
1579
  return null;
1312
1580
  }
1313
1581
  }
1314
- async function* parseChatSSEStream(response, state) {
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 chunk = parseChatStreamEvent(data);
1335
- if (chunk) {
1336
- const providerChunks = processChatStreamChunk(chunk, state);
1337
- for (const c of providerChunks) {
1338
- yield c;
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 chunk = parseChatStreamEvent(data);
1350
- if (chunk) {
1351
- const providerChunks = processChatStreamChunk(chunk, state);
1352
- for (const c of providerChunks) {
1353
- yield c;
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 buildChatParams(request) {
1366
- const messages = transformChatMessages(request.messages);
1367
- const params = {
1368
- model: request.model,
1369
- messages
1370
- };
1371
- if (request.tools && request.tools.length > 0) {
1372
- params.tools = transformChatTools(request.tools);
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
- if (request.providerOptions) {
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 client = await this.getClient();
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
- if (this.config.providers && this.config.providers.length > 0) {
1795
- params.provider = { only: this.config.providers };
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
- if (this.config.providers && this.config.providers.length > 0) {
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 = z.union([
2055
- z.number(),
2056
- z.object({
2057
- p50: z.number().optional(),
2058
- p75: z.number().optional(),
2059
- p90: z.number().optional(),
2060
- p99: z.number().optional()
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 = z.object({
2373
+ var providerRoutingSchema = z2.object({
2064
2374
  /** Provider slugs to try in order (e.g., ['anthropic', 'openai']) */
2065
- order: z.array(z.string()).optional(),
2375
+ order: z2.array(z2.string()).optional(),
2066
2376
  /** Allow fallback to other providers if preferred unavailable (default: true) */
2067
- allow_fallbacks: z.boolean().optional(),
2377
+ allow_fallbacks: z2.boolean().optional(),
2068
2378
  /** Only use providers that support all parameters in the request */
2069
- require_parameters: z.boolean().optional(),
2379
+ require_parameters: z2.boolean().optional(),
2070
2380
  /** Control data storage policies: 'allow' or 'deny' */
2071
- data_collection: z.enum(["allow", "deny"]).optional(),
2381
+ data_collection: z2.enum(["allow", "deny"]).optional(),
2072
2382
  /** Restrict to Zero Data Retention endpoints only */
2073
- zdr: z.boolean().optional(),
2383
+ zdr: z2.boolean().optional(),
2074
2384
  /** Restrict to these providers only (exclusive list) */
2075
- only: z.array(z.string()).optional(),
2385
+ only: z2.array(z2.string()).optional(),
2076
2386
  /** Skip these providers */
2077
- ignore: z.array(z.string()).optional(),
2387
+ ignore: z2.array(z2.string()).optional(),
2078
2388
  /** Sort providers by price, throughput, or latency */
2079
- sort: z.union([
2080
- z.enum(["price", "throughput", "latency"]),
2081
- z.object({
2082
- by: z.enum(["price", "throughput", "latency"]),
2083
- partition: z.enum(["model", "none"]).optional()
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: z.object({
2397
+ max_price: z2.object({
2088
2398
  /** Max price per million prompt tokens */
2089
- prompt: z.number().optional(),
2399
+ prompt: z2.number().optional(),
2090
2400
  /** Max price per million completion tokens */
2091
- completion: z.number().optional(),
2401
+ completion: z2.number().optional(),
2092
2402
  /** Max price per request */
2093
- request: z.number().optional(),
2403
+ request: z2.number().optional(),
2094
2404
  /** Max price per image */
2095
- image: z.number().optional()
2405
+ image: z2.number().optional()
2096
2406
  }).optional(),
2097
2407
  /** Allowed quantization levels */
2098
- quantizations: z.array(z.enum([
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: z.boolean().optional()
2424
+ enforce_distillable_text: z2.boolean().optional()
2115
2425
  }).passthrough();
2116
- var openrouterProviderOptions = z.object({
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: z.boolean().optional()
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