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