@bolt-foundry/gambit 0.8.0 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +82 -2
  2. package/README.md +31 -9
  3. package/esm/gambit/simulator-ui/dist/bundle.js +4744 -4360
  4. package/esm/gambit/simulator-ui/dist/bundle.js.map +4 -4
  5. package/esm/gambit/simulator-ui/dist/favicon.ico +0 -0
  6. package/esm/mod.d.ts +7 -3
  7. package/esm/mod.d.ts.map +1 -1
  8. package/esm/mod.js +5 -1
  9. package/esm/src/cli_utils.d.ts +3 -2
  10. package/esm/src/cli_utils.d.ts.map +1 -1
  11. package/esm/src/cli_utils.js +43 -27
  12. package/esm/src/openai_compat.d.ts +63 -0
  13. package/esm/src/openai_compat.d.ts.map +1 -0
  14. package/esm/src/openai_compat.js +277 -0
  15. package/esm/src/providers/google.d.ts +16 -0
  16. package/esm/src/providers/google.d.ts.map +1 -0
  17. package/esm/src/providers/google.js +352 -0
  18. package/esm/src/providers/ollama.d.ts +17 -0
  19. package/esm/src/providers/ollama.d.ts.map +1 -0
  20. package/esm/src/providers/ollama.js +509 -0
  21. package/esm/src/providers/openrouter.d.ts +14 -1
  22. package/esm/src/providers/openrouter.d.ts.map +1 -1
  23. package/esm/src/providers/openrouter.js +460 -463
  24. package/esm/src/server.d.ts +4 -0
  25. package/esm/src/server.d.ts.map +1 -1
  26. package/esm/src/server.js +623 -164
  27. package/esm/src/trace.d.ts.map +1 -1
  28. package/esm/src/trace.js +3 -6
  29. package/package.json +2 -2
  30. package/script/gambit/simulator-ui/dist/bundle.js +4744 -4360
  31. package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
  32. package/script/gambit/simulator-ui/dist/favicon.ico +0 -0
  33. package/script/mod.d.ts +7 -3
  34. package/script/mod.d.ts.map +1 -1
  35. package/script/mod.js +9 -3
  36. package/script/src/cli_utils.d.ts +3 -2
  37. package/script/src/cli_utils.d.ts.map +1 -1
  38. package/script/src/cli_utils.js +42 -26
  39. package/script/src/openai_compat.d.ts +63 -0
  40. package/script/src/openai_compat.d.ts.map +1 -0
  41. package/script/src/openai_compat.js +281 -0
  42. package/script/src/providers/google.d.ts +16 -0
  43. package/script/src/providers/google.d.ts.map +1 -0
  44. package/script/src/providers/google.js +359 -0
  45. package/script/src/providers/ollama.d.ts +17 -0
  46. package/script/src/providers/ollama.d.ts.map +1 -0
  47. package/script/src/providers/ollama.js +551 -0
  48. package/script/src/providers/openrouter.d.ts +14 -1
  49. package/script/src/providers/openrouter.d.ts.map +1 -1
  50. package/script/src/providers/openrouter.js +461 -463
  51. package/script/src/server.d.ts +4 -0
  52. package/script/src/server.d.ts.map +1 -1
  53. package/script/src/server.js +623 -164
  54. package/script/src/trace.d.ts.map +1 -1
  55. package/script/src/trace.js +3 -6
  56. package/esm/src/compat/openai.d.ts +0 -2
  57. package/esm/src/compat/openai.d.ts.map +0 -1
  58. package/esm/src/compat/openai.js +0 -1
  59. package/script/src/compat/openai.d.ts +0 -2
  60. package/script/src/compat/openai.d.ts.map +0 -1
  61. package/script/src/compat/openai.js +0 -5
@@ -36,84 +36,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.OPENROUTER_PREFIX = void 0;
39
40
  exports.createOpenRouterProvider = createOpenRouterProvider;
40
41
  const dntShim = __importStar(require("../../_dnt.shims.js"));
41
42
  const openai_1 = __importDefault(require("openai"));
43
+ const gambit_core_1 = require("@bolt-foundry/gambit-core");
42
44
  const logger = console;
43
- function contentToChatCompletionContent(content) {
44
- if (typeof content === "string" || content === null)
45
- return content;
46
- const textParts = [];
47
- const out = [];
48
- const flushText = () => {
49
- if (!textParts.length)
50
- return;
51
- out.push({ type: "text", text: textParts.join("") });
52
- textParts.length = 0;
53
- };
54
- for (const part of content) {
55
- switch (part.type) {
56
- case "input_image":
57
- if (part.image_url) {
58
- flushText();
59
- out.push({
60
- type: "image_url",
61
- image_url: {
62
- url: part.image_url,
63
- detail: part.detail,
64
- },
65
- });
66
- }
67
- break;
68
- case "input_file": {
69
- const label = part.file_url ?? part.filename;
70
- if (label)
71
- textParts.push(`[file] ${label}`);
72
- break;
73
- }
74
- case "input_video":
75
- textParts.push(`[video] ${part.video_url}`);
76
- break;
77
- case "refusal":
78
- textParts.push(part.refusal);
79
- break;
80
- case "input_text":
81
- case "output_text":
82
- case "text":
83
- case "summary_text":
84
- case "reasoning_text":
85
- textParts.push(part.text);
86
- break;
87
- }
88
- }
89
- if (!out.length)
90
- return textParts.join("");
91
- flushText();
92
- return out;
93
- }
94
- function contentPartsToText(content) {
95
- if (typeof content === "string" || content === null)
96
- return content;
97
- return content.map((part) => {
98
- switch (part.type) {
99
- case "input_text":
100
- case "output_text":
101
- case "text":
102
- case "summary_text":
103
- case "reasoning_text":
104
- return part.text;
105
- case "refusal":
106
- return part.refusal;
107
- case "input_file": {
108
- const label = part.file_url ?? part.filename;
109
- return label ? `[file] ${label}` : "";
110
- }
111
- case "input_video":
112
- return `[video] ${part.video_url}`;
113
- default:
114
- return "";
115
- }
116
- }).join("");
45
+ exports.OPENROUTER_PREFIX = "openrouter/";
46
+ function normalizeOpenRouterModel(model) {
47
+ return model.startsWith(exports.OPENROUTER_PREFIX)
48
+ ? model.slice(exports.OPENROUTER_PREFIX.length)
49
+ : model;
117
50
  }
118
51
  function normalizeMessage(content) {
119
52
  const toolCalls = content.tool_calls ??
@@ -132,273 +65,465 @@ function normalizeMessage(content) {
132
65
  tool_calls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined,
133
66
  };
134
67
  }
135
- function normalizeInputItems(input, instructions) {
136
- const items = typeof input === "string"
137
- ? [
138
- {
139
- type: "message",
140
- role: "user",
141
- content: input,
142
- },
143
- ]
144
- : input ?? [];
145
- if (instructions) {
146
- return [
147
- {
148
- type: "message",
149
- role: "system",
150
- content: instructions,
68
+ function safeJson(input) {
69
+ try {
70
+ const parsed = JSON.parse(input);
71
+ if (parsed && typeof parsed === "object") {
72
+ return parsed;
73
+ }
74
+ }
75
+ catch {
76
+ // fall through
77
+ }
78
+ return {};
79
+ }
80
+ function isAsyncIterable(value) {
81
+ return Boolean(value &&
82
+ typeof value === "object" &&
83
+ Symbol.asyncIterator in value);
84
+ }
85
+ function mapUsage(usage) {
86
+ if (!usage)
87
+ return undefined;
88
+ return {
89
+ promptTokens: usage.input_tokens ?? 0,
90
+ completionTokens: usage.output_tokens ?? 0,
91
+ totalTokens: usage.total_tokens ?? 0,
92
+ };
93
+ }
94
+ function mapStatus(status) {
95
+ if (!status)
96
+ return undefined;
97
+ if (status === "completed")
98
+ return "completed";
99
+ if (status === "in_progress" || status === "queued")
100
+ return "in_progress";
101
+ return "failed";
102
+ }
103
+ function mapError(error) {
104
+ if (!error)
105
+ return undefined;
106
+ return { code: error.code, message: error.message };
107
+ }
108
+ function mapTools(tools) {
109
+ if (!tools || tools.length === 0)
110
+ return undefined;
111
+ return tools.map((tool) => ({
112
+ type: "function",
113
+ name: tool.function.name,
114
+ description: tool.function.description ?? null,
115
+ parameters: normalizeToolParameters(tool.function.parameters),
116
+ strict: false,
117
+ }));
118
+ }
119
+ function normalizeToolParameters(parameters) {
120
+ const normalized = structuredClone(parameters ?? {});
121
+ if (normalized.type !== "object") {
122
+ return normalized;
123
+ }
124
+ if (normalized.properties === undefined) {
125
+ normalized.properties = {};
126
+ }
127
+ const props = normalized.properties;
128
+ if (props && typeof props === "object" && !Array.isArray(props)) {
129
+ const requiredKeys = Array.isArray(normalized.required)
130
+ ? normalized.required.filter((key) => typeof key === "string" && key in props)
131
+ : [];
132
+ for (const [key, value] of Object.entries(props)) {
133
+ if (!value || typeof value !== "object" || Array.isArray(value))
134
+ continue;
135
+ if (!("type" in value))
136
+ continue;
137
+ if (value.type === "object" && value.additionalProperties !== false) {
138
+ props[key] = {
139
+ ...value,
140
+ additionalProperties: false,
141
+ };
142
+ }
143
+ }
144
+ if (requiredKeys.length > 0) {
145
+ normalized.required = requiredKeys;
146
+ }
147
+ }
148
+ const additional = normalized.additionalProperties;
149
+ if (additional !== false) {
150
+ normalized.additionalProperties = false;
151
+ }
152
+ return normalized;
153
+ }
154
+ function appendSyntheticTools(tools, input) {
155
+ const needed = new Set();
156
+ for (const item of input) {
157
+ if (item.type !== "function_call")
158
+ continue;
159
+ if (item.name === gambit_core_1.GAMBIT_TOOL_CONTEXT || item.name === gambit_core_1.GAMBIT_TOOL_INIT) {
160
+ needed.add(item.name);
161
+ }
162
+ }
163
+ for (const name of needed) {
164
+ if (tools.some((tool) => tool.name === name))
165
+ continue;
166
+ tools.push({
167
+ type: "function",
168
+ name,
169
+ description: "Synthetic Gambit context payload.",
170
+ parameters: {
171
+ type: "object",
172
+ properties: {},
173
+ additionalProperties: false,
174
+ required: [],
151
175
  },
152
- ...items,
153
- ];
176
+ strict: false,
177
+ });
154
178
  }
155
- return items;
156
179
  }
157
- function messagesFromResponseItems(items) {
158
- const messages = [];
180
+ function mapToolChoice(toolChoice) {
181
+ if (!toolChoice)
182
+ return undefined;
183
+ if (toolChoice === "auto" || toolChoice === "required")
184
+ return toolChoice;
185
+ return { type: "function", name: toolChoice.function.name };
186
+ }
187
+ function mapOpenAIOutputItem(item) {
188
+ const itemType = item.type;
189
+ if (itemType === "message") {
190
+ const message = item;
191
+ const content = [];
192
+ for (const part of message.content ?? []) {
193
+ if (part.type === "output_text") {
194
+ content.push({ type: "output_text", text: part.text });
195
+ }
196
+ }
197
+ if (content.length === 0)
198
+ return null;
199
+ return {
200
+ type: "message",
201
+ role: "assistant",
202
+ content,
203
+ id: message.id,
204
+ };
205
+ }
206
+ if (itemType === "function_call") {
207
+ const call = item;
208
+ return {
209
+ type: "function_call",
210
+ call_id: call.call_id,
211
+ name: call.name,
212
+ arguments: call.arguments,
213
+ id: call.id,
214
+ };
215
+ }
216
+ return null;
217
+ }
218
+ function normalizeOpenAIResponse(response) {
219
+ const outputItems = (response.output ?? [])
220
+ .map(mapOpenAIOutputItem)
221
+ .filter((item) => Boolean(item));
222
+ return {
223
+ id: response.id,
224
+ object: "response",
225
+ model: response.model,
226
+ created: response.created_at,
227
+ status: mapStatus(response.status ?? undefined),
228
+ output: outputItems,
229
+ usage: mapUsage(response.usage),
230
+ error: mapError(response.error),
231
+ };
232
+ }
233
+ function toOpenAIInputItems(items) {
234
+ const mapped = [];
159
235
  for (const item of items) {
160
- if (item.type !== "message")
161
- continue;
162
- if (item.role === "tool") {
163
- if (!item.tool_call_id)
236
+ if (item.type === "message") {
237
+ const isAssistant = item.role === "assistant";
238
+ const content = item.content
239
+ .map((part) => {
240
+ if (part.type === "output_text") {
241
+ return {
242
+ type: "output_text",
243
+ text: part.text,
244
+ };
245
+ }
246
+ if (part.type === "input_text") {
247
+ return {
248
+ type: isAssistant ? "output_text" : "input_text",
249
+ text: part.text,
250
+ };
251
+ }
252
+ return null;
253
+ })
254
+ .filter((part) => Boolean(part));
255
+ if (content.length === 0)
164
256
  continue;
165
- const content = contentPartsToText(item.content) ?? "";
166
- messages.push({
167
- role: "tool",
257
+ mapped.push({
258
+ type: "message",
259
+ role: item.role,
168
260
  content,
169
- tool_call_id: item.tool_call_id,
261
+ id: item.id,
170
262
  });
171
263
  continue;
172
264
  }
173
- if (item.role === "assistant") {
174
- const content = contentPartsToText(item.content);
175
- messages.push({
176
- role: "assistant",
177
- content,
178
- ...(item.name ? { name: item.name } : {}),
179
- ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
265
+ if (item.type === "function_call") {
266
+ mapped.push({
267
+ type: "function_call",
268
+ call_id: item.call_id,
269
+ name: item.name,
270
+ arguments: item.arguments,
271
+ id: item.id,
180
272
  });
181
273
  continue;
182
274
  }
183
- if (item.role === "user") {
184
- const content = contentToChatCompletionContent(item.content) ?? "";
185
- messages.push({
186
- role: "user",
187
- content,
188
- ...(item.name ? { name: item.name } : {}),
275
+ if (item.type === "function_call_output") {
276
+ mapped.push({
277
+ type: "function_call_output",
278
+ call_id: item.call_id,
279
+ output: item.output,
280
+ id: item.id,
189
281
  });
282
+ }
283
+ }
284
+ return mapped;
285
+ }
286
+ function chatMessagesToResponseItems(messages) {
287
+ const items = [];
288
+ for (const message of messages) {
289
+ if (message.role === "tool") {
290
+ if (message.tool_call_id &&
291
+ typeof message.content === "string") {
292
+ items.push({
293
+ type: "function_call_output",
294
+ call_id: message.tool_call_id,
295
+ output: message.content,
296
+ });
297
+ }
190
298
  continue;
191
299
  }
192
- const content = contentPartsToText(item.content) ?? "";
193
- messages.push({
194
- role: item.role,
195
- content,
196
- ...(item.name ? { name: item.name } : {}),
197
- });
300
+ if (message.role === "system" || message.role === "user" ||
301
+ message.role === "assistant") {
302
+ const content = [];
303
+ if (typeof message.content === "string" && message.content.length > 0) {
304
+ content.push({
305
+ type: message.role === "assistant" ? "output_text" : "input_text",
306
+ text: message.content,
307
+ });
308
+ }
309
+ if (content.length > 0) {
310
+ items.push({
311
+ type: "message",
312
+ role: message.role,
313
+ content,
314
+ });
315
+ }
316
+ }
317
+ if (message.role === "assistant" && message.tool_calls) {
318
+ for (const call of message.tool_calls) {
319
+ items.push({
320
+ type: "function_call",
321
+ call_id: call.id,
322
+ name: call.function.name,
323
+ arguments: call.function.arguments,
324
+ });
325
+ }
326
+ }
327
+ }
328
+ return items;
329
+ }
330
+ function responseItemsToChat(items) {
331
+ const textParts = [];
332
+ const toolCalls = [];
333
+ const messageToolCalls = [];
334
+ for (const item of items) {
335
+ if (item.type === "message" && item.role === "assistant") {
336
+ for (const part of item.content) {
337
+ if (part.type === "output_text") {
338
+ textParts.push(part.text);
339
+ }
340
+ }
341
+ }
342
+ if (item.type === "function_call") {
343
+ toolCalls.push({
344
+ id: item.call_id,
345
+ name: item.name,
346
+ args: safeJson(item.arguments),
347
+ });
348
+ messageToolCalls.push({
349
+ id: item.call_id,
350
+ type: "function",
351
+ function: { name: item.name, arguments: item.arguments },
352
+ });
353
+ }
198
354
  }
199
- return messages;
355
+ const content = textParts.length > 0 ? textParts.join("") : null;
356
+ const message = {
357
+ role: "assistant",
358
+ content,
359
+ tool_calls: messageToolCalls.length > 0 ? messageToolCalls : undefined,
360
+ };
361
+ return {
362
+ message,
363
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
364
+ };
200
365
  }
201
- function applyRequestParams(input, params) {
202
- const out = { ...params };
203
- const setParam = (key, value) => {
204
- if (value === undefined || out[key] !== undefined)
205
- return;
206
- out[key] = value;
366
+ async function createResponse(client, request, onStreamEvent) {
367
+ const baseParams = {
368
+ model: normalizeOpenRouterModel(request.model),
369
+ input: toOpenAIInputItems(request.input),
370
+ instructions: request.instructions,
371
+ tools: undefined,
372
+ tool_choice: mapToolChoice(request.tool_choice),
373
+ stream: request.stream,
374
+ max_output_tokens: request.max_output_tokens,
375
+ metadata: request.metadata,
207
376
  };
208
- setParam("temperature", input.temperature);
209
- setParam("top_p", input.top_p);
210
- setParam("frequency_penalty", input.frequency_penalty);
211
- setParam("presence_penalty", input.presence_penalty);
212
- setParam("max_tokens", input.max_output_tokens);
213
- setParam("top_logprobs", input.top_logprobs);
214
- setParam("parallel_tool_calls", input.parallel_tool_calls);
215
- if (input.tool_choice !== undefined && out.tool_choice === undefined) {
216
- if (typeof input.tool_choice === "string") {
217
- out.tool_choice = input.tool_choice === "required"
218
- ? "auto"
219
- : input.tool_choice;
377
+ const mappedTools = mapTools(request.tools) ?? [];
378
+ appendSyntheticTools(mappedTools, request.input);
379
+ if (mappedTools.length > 0) {
380
+ baseParams.tools = mappedTools;
381
+ }
382
+ const params = { ...(request.params ?? {}), ...baseParams };
383
+ const debugResponses = dntShim.Deno.env.get("GAMBIT_DEBUG_RESPONSES") === "1";
384
+ let responseOrStream;
385
+ try {
386
+ responseOrStream = await client.responses.create(params);
387
+ }
388
+ catch (err) {
389
+ if (debugResponses) {
390
+ logger.error("[responses-debug] request", params);
391
+ if (err instanceof openai_1.default.APIError) {
392
+ logger.error("[responses-debug] error", err.error);
393
+ }
394
+ else {
395
+ logger.error("[responses-debug] error", err);
396
+ }
220
397
  }
221
- else if (input.tool_choice.type === "function") {
222
- out.tool_choice = input.tool_choice.name
223
- ? {
224
- type: "function",
225
- function: { name: input.tool_choice.name },
398
+ throw err;
399
+ }
400
+ if (request.stream &&
401
+ isAsyncIterable(responseOrStream)) {
402
+ let completed = null;
403
+ for await (const event of responseOrStream) {
404
+ if (!event || typeof event !== "object" || !("type" in event)) {
405
+ continue;
406
+ }
407
+ switch (event.type) {
408
+ case "response.created": {
409
+ const mapped = normalizeOpenAIResponse(event.response);
410
+ onStreamEvent?.({ type: "response.created", response: mapped });
411
+ break;
412
+ }
413
+ case "response.output_text.delta":
414
+ onStreamEvent?.({
415
+ type: "response.output_text.delta",
416
+ output_index: event.output_index,
417
+ delta: event.delta,
418
+ item_id: event.item_id,
419
+ });
420
+ break;
421
+ case "response.output_text.done":
422
+ onStreamEvent?.({
423
+ type: "response.output_text.done",
424
+ output_index: event.output_index,
425
+ text: event.text,
426
+ item_id: event.item_id,
427
+ });
428
+ break;
429
+ case "response.output_item.added": {
430
+ const item = mapOpenAIOutputItem(event.item);
431
+ if (item) {
432
+ onStreamEvent?.({
433
+ type: "response.output_item.added",
434
+ output_index: event.output_index,
435
+ item,
436
+ });
437
+ }
438
+ break;
439
+ }
440
+ case "response.output_item.done": {
441
+ const item = mapOpenAIOutputItem(event.item);
442
+ if (item) {
443
+ onStreamEvent?.({
444
+ type: "response.output_item.done",
445
+ output_index: event.output_index,
446
+ item,
447
+ });
448
+ }
449
+ break;
450
+ }
451
+ case "response.completed": {
452
+ completed = normalizeOpenAIResponse(event.response);
453
+ onStreamEvent?.({ type: "response.completed", response: completed });
454
+ break;
455
+ }
456
+ case "response.failed": {
457
+ const error = mapError(event.response?.error ?? undefined);
458
+ onStreamEvent?.({
459
+ type: "response.failed",
460
+ error: error ?? {},
461
+ });
462
+ break;
226
463
  }
227
- : "auto";
464
+ default:
465
+ break;
466
+ }
228
467
  }
468
+ if (completed)
469
+ return completed;
470
+ throw new Error("OpenRouter responses stream ended without completion.");
229
471
  }
230
- return out;
472
+ return normalizeOpenAIResponse(responseOrStream);
231
473
  }
232
474
  function createOpenRouterProvider(opts) {
233
475
  const debugStream = dntShim.Deno.env.get("GAMBIT_DEBUG_STREAM") === "1";
234
- const envFlag = dntShim.Deno.env.get("OPENROUTER_USE_RESPONSES");
235
- const useResponses = opts.useResponses !== undefined
236
- ? opts.useResponses
237
- : envFlag === "0"
238
- ? false
239
- : envFlag === "1"
240
- ? true
241
- : true;
242
- const client = new openai_1.default({
243
- apiKey: opts.apiKey,
244
- baseURL: opts.baseURL ?? "https://openrouter.ai/api/v1",
245
- defaultHeaders: {
246
- "HTTP-Referer": opts.referer ?? "https://gambit.local",
247
- "X-Title": opts.title ?? "Gambit CLI",
248
- },
249
- });
250
- const openResponseEventTypes = new Set([
251
- "response.output_text.delta",
252
- "response.output_text.done",
253
- "response.output_item.added",
254
- "response.output_item.done",
255
- "response.content_part.added",
256
- "response.content_part.done",
257
- "response.function_call_arguments.delta",
258
- "response.function_call_arguments.done",
259
- "response.refusal.delta",
260
- "response.refusal.done",
261
- "response.reasoning.delta",
262
- "response.reasoning.done",
263
- "response.reasoning_summary_text.delta",
264
- "response.reasoning_summary_text.done",
265
- "response.reasoning_summary_part.added",
266
- "response.reasoning_summary_part.done",
267
- "response.created",
268
- "response.queued",
269
- "response.in_progress",
270
- "response.failed",
271
- "response.incomplete",
272
- "response.completed",
273
- "error",
274
- ]);
275
- const buildResponsesRequest = (input) => {
276
- const { params: _params, state: _state, onStreamEvent: _onStreamEvent, ...request } = input;
277
- return request;
278
- };
476
+ const client = (opts.client ??
477
+ new openai_1.default({
478
+ apiKey: opts.apiKey,
479
+ baseURL: opts.baseURL ?? "https://openrouter.ai/api/v1",
480
+ defaultHeaders: {
481
+ "HTTP-Referer": opts.referer ?? "https://gambit.local",
482
+ "X-Title": opts.title ?? "Gambit CLI",
483
+ },
484
+ }));
279
485
  return {
280
486
  async responses(input) {
281
- if (useResponses) {
282
- const request = buildResponsesRequest(input);
283
- if (input.stream) {
284
- let sequence = 0;
285
- let terminalResponse;
286
- const stream = await client.responses.create({
287
- ...request,
288
- stream: true,
289
- });
290
- for await (const event of stream) {
291
- if (!event || !event.type)
292
- continue;
293
- if (openResponseEventTypes.has(event.type)) {
294
- const streamEvent = event;
295
- input.onStreamEvent?.({
296
- ...streamEvent,
297
- sequence_number: streamEvent.sequence_number ?? ++sequence,
298
- });
299
- }
300
- if (event.type === "response.completed" ||
301
- event.type === "response.failed" ||
302
- event.type === "response.incomplete") {
303
- terminalResponse = event.response;
304
- }
305
- }
306
- if (!terminalResponse) {
307
- throw new Error("OpenRouter responses stream ended without terminal response.");
487
+ return await createResponse(client, input.request, input.onStreamEvent);
488
+ },
489
+ async chat(input) {
490
+ const params = input.params ?? {};
491
+ if (opts.enableResponses) {
492
+ const response = await createResponse(client, {
493
+ model: normalizeOpenRouterModel(input.model),
494
+ input: chatMessagesToResponseItems(input.messages),
495
+ tools: input.tools,
496
+ stream: input.stream,
497
+ params,
498
+ }, (event) => {
499
+ if (event.type === "response.output_text.delta") {
500
+ input.onStreamText?.(event.delta);
308
501
  }
309
- return terminalResponse;
310
- }
311
- return await client.responses.create(request);
502
+ });
503
+ const mapped = responseItemsToChat(response.output);
504
+ return {
505
+ message: mapped.message,
506
+ finishReason: mapped.toolCalls ? "tool_calls" : "stop",
507
+ toolCalls: mapped.toolCalls,
508
+ usage: response.usage,
509
+ };
312
510
  }
313
- const items = normalizeInputItems(input.input, input.instructions ?? null);
314
- const messages = messagesFromResponseItems(items);
315
- const requestParams = applyRequestParams(input, input.params ?? {});
316
- const toolChoice = requestParams.tool_choice ?? "auto";
317
- delete requestParams.tool_choice;
318
511
  if (input.stream) {
319
512
  if (debugStream) {
320
- logger.log(`[stream-debug] requesting stream model=${input.model} messages=${messages.length} tools=${input.tools?.length ?? 0}`);
513
+ logger.log(`[stream-debug] requesting stream model=${input.model} messages=${input.messages.length} tools=${input.tools?.length ?? 0}`);
321
514
  }
322
- const responseId = crypto.randomUUID();
323
- const createdAt = Math.floor(Date.now() / 1000);
324
- const itemId = crypto.randomUUID().replace(/-/g, "").slice(0, 24);
325
- let sequence = 0;
326
- const emit = (event) => {
327
- input.onStreamEvent?.({
328
- ...event,
329
- sequence_number: event.sequence_number ?? ++sequence,
330
- });
331
- };
332
- const responseSkeletonBase = {
333
- id: responseId,
334
- object: "response",
335
- created_at: createdAt,
336
- model: input.model,
337
- previous_response_id: input.previous_response_id ?? null,
338
- instructions: input.instructions ?? null,
339
- tool_choice: input.tool_choice,
340
- truncation: input.truncation,
341
- parallel_tool_calls: input.parallel_tool_calls,
342
- text: input.text,
343
- max_output_tokens: input.max_output_tokens,
344
- max_tool_calls: input.max_tool_calls,
345
- store: input.store,
346
- background: input.background,
347
- service_tier: input.service_tier,
348
- metadata: input.metadata,
349
- safety_identifier: input.safety_identifier,
350
- prompt_cache_key: input.prompt_cache_key,
351
- tools: input.tools,
352
- output: [],
353
- };
354
- emit({
355
- type: "response.queued",
356
- response: { ...responseSkeletonBase, status: "queued" },
357
- });
358
- const responseSkeleton = {
359
- ...responseSkeletonBase,
360
- status: "in_progress",
361
- };
362
- emit({ type: "response.created", response: responseSkeleton });
363
- emit({ type: "response.in_progress", response: responseSkeleton });
364
- emit({
365
- type: "response.output_item.added",
366
- output_index: 0,
367
- item: {
368
- type: "message",
369
- id: itemId,
370
- status: "in_progress",
371
- role: "assistant",
372
- content: [],
373
- },
515
+ const stream = await client.chat.completions.create({
516
+ model: normalizeOpenRouterModel(input.model),
517
+ messages: input
518
+ .messages,
519
+ tools: input
520
+ .tools,
521
+ tool_choice: "auto",
522
+ stream: true,
523
+ ...params,
374
524
  });
375
- let stream = null;
376
- try {
377
- stream = await client.chat.completions.create({
378
- model: input.model,
379
- messages: messages,
380
- tools: input.tools,
381
- tool_choice: toolChoice,
382
- stream: true,
383
- ...requestParams,
384
- });
385
- }
386
- catch (err) {
387
- const message = err instanceof Error ? err.message : String(err);
388
- emit({ type: "error", error: { code: "openrouter_error", message } });
389
- emit({
390
- type: "response.failed",
391
- response: {
392
- ...responseSkeleton,
393
- status: "failed",
394
- error: { code: "openrouter_error", message },
395
- },
396
- });
397
- throw err;
398
- }
399
525
  let finishReason = null;
400
526
  const contentParts = [];
401
- let contentPartStarted = false;
402
527
  const toolCallMap = new Map();
403
528
  let chunkCount = 0;
404
529
  let streamedChars = 0;
@@ -412,24 +537,8 @@ function createOpenRouterProvider(opts) {
412
537
  }
413
538
  const delta = choice.delta;
414
539
  if (typeof delta.content === "string") {
415
- if (!contentPartStarted) {
416
- emit({
417
- type: "response.content_part.added",
418
- item_id: itemId,
419
- output_index: 0,
420
- content_index: 0,
421
- part: { type: "output_text", text: "" },
422
- });
423
- contentPartStarted = true;
424
- }
425
540
  contentParts.push(delta.content);
426
- emit({
427
- type: "response.output_text.delta",
428
- item_id: itemId,
429
- output_index: 0,
430
- content_index: 0,
431
- delta: delta.content,
432
- });
541
+ input.onStreamText?.(delta.content);
433
542
  streamedChars += delta.content.length;
434
543
  }
435
544
  else if (Array.isArray(delta.content)) {
@@ -437,24 +546,8 @@ function createOpenRouterProvider(opts) {
437
546
  .map((c) => (typeof c === "string" ? c : ""))
438
547
  .join("");
439
548
  if (chunkStr) {
440
- if (!contentPartStarted) {
441
- emit({
442
- type: "response.content_part.added",
443
- item_id: itemId,
444
- output_index: 0,
445
- content_index: 0,
446
- part: { type: "output_text", text: "" },
447
- });
448
- contentPartStarted = true;
449
- }
450
549
  contentParts.push(chunkStr);
451
- emit({
452
- type: "response.output_text.delta",
453
- item_id: itemId,
454
- output_index: 0,
455
- content_index: 0,
456
- delta: chunkStr,
457
- });
550
+ input.onStreamText?.(chunkStr);
458
551
  streamedChars += chunkStr.length;
459
552
  }
460
553
  }
@@ -465,20 +558,12 @@ function createOpenRouterProvider(opts) {
465
558
  id: tc.id,
466
559
  function: { name: tc.function?.name, arguments: "" },
467
560
  };
468
- if (!existing.id) {
469
- existing.id = tc.id ??
470
- crypto.randomUUID().replace(/-/g, "").slice(0, 24);
471
- }
561
+ if (tc.id)
562
+ existing.id = tc.id;
472
563
  if (tc.function?.name)
473
564
  existing.function.name = tc.function.name;
474
565
  if (tc.function?.arguments) {
475
566
  existing.function.arguments += tc.function.arguments;
476
- emit({
477
- type: "response.function_call_arguments.delta",
478
- item_id: existing.id,
479
- output_index: 0,
480
- delta: tc.function.arguments,
481
- });
482
567
  }
483
568
  toolCallMap.set(idx, existing);
484
569
  }
@@ -494,135 +579,48 @@ function createOpenRouterProvider(opts) {
494
579
  arguments: tc.function.arguments,
495
580
  },
496
581
  }));
497
- for (const call of tool_calls) {
498
- emit({
499
- type: "response.function_call_arguments.done",
500
- item_id: call.id,
501
- output_index: 0,
502
- arguments: call.function.arguments,
503
- });
504
- }
505
- const text = contentParts.length ? contentParts.join("") : "";
506
- const outputPart = {
507
- type: "output_text",
508
- text,
509
- };
510
582
  const message = normalizeMessage({
511
583
  role: "assistant",
512
- content: text.length > 0 ? text : null,
584
+ content: contentParts.length ? contentParts.join("") : null,
513
585
  tool_calls,
514
586
  });
515
- const outputItem = {
516
- type: "message",
517
- id: itemId,
518
- status: "completed",
519
- role: message.role,
520
- content: text.length > 0 ? [outputPart] : null,
521
- name: message.name,
522
- tool_call_id: message.tool_call_id,
523
- tool_calls: message.tool_calls,
524
- };
525
- if (contentPartStarted && text.length > 0) {
526
- emit({
527
- type: "response.output_text.done",
528
- item_id: itemId,
529
- output_index: 0,
530
- content_index: 0,
531
- text,
532
- });
533
- emit({
534
- type: "response.content_part.done",
535
- item_id: itemId,
536
- output_index: 0,
537
- content_index: 0,
538
- part: outputPart,
539
- });
540
- }
541
- emit({
542
- type: "response.output_item.done",
543
- output_index: 0,
544
- item: outputItem,
545
- });
546
- const completedAt = Math.floor(Date.now() / 1000);
547
- const status = finishReason === "length" ? "incomplete" : "completed";
548
- const responseResource = {
549
- ...responseSkeleton,
550
- completed_at: completedAt,
551
- status,
552
- output: [outputItem],
553
- finishReason: finishReason ?? "stop",
554
- };
555
- if (status === "incomplete") {
556
- emit({
557
- type: "response.incomplete",
558
- response: responseResource,
559
- });
560
- }
561
- else {
562
- emit({
563
- type: "response.completed",
564
- response: responseResource,
565
- });
566
- }
587
+ const toolCalls = tool_calls.length > 0
588
+ ? tool_calls.map((tc) => ({
589
+ id: tc.id,
590
+ name: tc.function.name,
591
+ args: safeJson(tc.function.arguments),
592
+ }))
593
+ : undefined;
567
594
  return {
568
- ...responseResource,
595
+ message,
596
+ finishReason: finishReason ?? "stop",
597
+ toolCalls,
569
598
  };
570
599
  }
571
600
  const response = await client.chat.completions.create({
572
- model: input.model,
573
- messages: messages,
574
- tools: input.tools,
575
- tool_choice: toolChoice,
601
+ model: normalizeOpenRouterModel(input.model),
602
+ messages: input
603
+ .messages,
604
+ tools: input
605
+ .tools,
606
+ tool_choice: "auto",
576
607
  stream: false,
577
- ...requestParams,
608
+ ...params,
578
609
  });
579
610
  const choice = response.choices[0];
580
611
  const message = choice.message;
581
612
  const normalizedMessage = normalizeMessage(message);
582
- const responseId = response.id ?? crypto.randomUUID();
583
- const createdAt = Math.floor(Date.now() / 1000);
584
- const outputItem = {
585
- type: "message",
586
- id: crypto.randomUUID().replace(/-/g, "").slice(0, 24),
587
- status: "completed",
588
- role: normalizedMessage.role,
589
- content: normalizedMessage.content,
590
- name: normalizedMessage.name,
591
- tool_call_id: normalizedMessage.tool_call_id,
592
- tool_calls: normalizedMessage.tool_calls,
593
- };
594
- const finishReason = choice.finish_reason ??
595
- "stop";
596
- const status = finishReason === "length" ? "incomplete" : "completed";
613
+ const toolCalls = message.tool_calls?.map((tc) => ({
614
+ id: tc.id,
615
+ name: tc.function.name,
616
+ args: safeJson(tc.function.arguments),
617
+ }));
597
618
  return {
598
- id: responseId,
599
- object: "response",
600
- created_at: createdAt,
601
- completed_at: createdAt,
602
- status,
603
- model: input.model,
604
- previous_response_id: input.previous_response_id ?? null,
605
- instructions: input.instructions ?? null,
606
- tool_choice: input.tool_choice,
607
- truncation: input.truncation,
608
- parallel_tool_calls: input.parallel_tool_calls,
609
- text: input.text,
610
- max_output_tokens: input.max_output_tokens,
611
- max_tool_calls: input.max_tool_calls,
612
- store: input.store,
613
- background: input.background,
614
- service_tier: input.service_tier,
615
- metadata: input.metadata,
616
- safety_identifier: input.safety_identifier,
617
- prompt_cache_key: input.prompt_cache_key,
618
- tools: input.tools,
619
- output: [outputItem],
620
- finishReason,
619
+ message: normalizedMessage,
620
+ finishReason: (choice.finish_reason ?? "stop"),
621
+ toolCalls,
621
622
  usage: response.usage
622
623
  ? {
623
- input_tokens: response.usage.prompt_tokens ?? 0,
624
- output_tokens: response.usage.completion_tokens ?? 0,
625
- total_tokens: response.usage.total_tokens ?? 0,
626
624
  promptTokens: response.usage.prompt_tokens ?? 0,
627
625
  completionTokens: response.usage.completion_tokens ?? 0,
628
626
  totalTokens: response.usage.total_tokens ?? 0,