@bolt-foundry/gambit 0.7.0 → 0.8.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.
@@ -0,0 +1,595 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import OpenAI from "openai";
3
+ 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("");
78
+ }
79
+ function normalizeMessage(content) {
80
+ const toolCalls = content.tool_calls ??
81
+ undefined;
82
+ return {
83
+ role: content.role,
84
+ content: typeof content.content === "string"
85
+ ? content.content
86
+ : Array.isArray(content.content)
87
+ ? content.content
88
+ .map((c) => (typeof c === "string" ? c : ""))
89
+ .join("")
90
+ : "",
91
+ name: content.name,
92
+ tool_call_id: content.tool_call_id,
93
+ tool_calls: toolCalls && toolCalls.length > 0 ? toolCalls : undefined,
94
+ };
95
+ }
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,
112
+ },
113
+ ...items,
114
+ ];
115
+ }
116
+ return items;
117
+ }
118
+ function messagesFromResponseItems(items) {
119
+ const messages = [];
120
+ for (const item of items) {
121
+ if (item.type !== "message")
122
+ continue;
123
+ if (item.role === "tool") {
124
+ if (!item.tool_call_id)
125
+ continue;
126
+ const content = contentPartsToText(item.content) ?? "";
127
+ messages.push({
128
+ role: "tool",
129
+ content,
130
+ tool_call_id: item.tool_call_id,
131
+ });
132
+ continue;
133
+ }
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 } : {}),
141
+ });
142
+ continue;
143
+ }
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 } : {}),
150
+ });
151
+ continue;
152
+ }
153
+ const content = contentPartsToText(item.content) ?? "";
154
+ messages.push({
155
+ role: item.role,
156
+ content,
157
+ ...(item.name ? { name: item.name } : {}),
158
+ });
159
+ }
160
+ return messages;
161
+ }
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;
168
+ };
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;
181
+ }
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 },
187
+ }
188
+ : "auto";
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+ export function createOpenRouterProvider(opts) {
194
+ 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
+ };
240
+ return {
241
+ 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.");
269
+ }
270
+ return terminalResponse;
271
+ }
272
+ return await client.responses.create(request);
273
+ }
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
+ if (input.stream) {
280
+ if (debugStream) {
281
+ logger.log(`[stream-debug] requesting stream model=${input.model} messages=${messages.length} tools=${input.tools?.length ?? 0}`);
282
+ }
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
+ },
335
+ });
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
+ let finishReason = null;
361
+ const contentParts = [];
362
+ let contentPartStarted = false;
363
+ const toolCallMap = new Map();
364
+ let chunkCount = 0;
365
+ let streamedChars = 0;
366
+ for await (const chunk of stream) {
367
+ chunkCount++;
368
+ const choice = chunk.choices[0];
369
+ const fr = choice.finish_reason;
370
+ if (fr === "stop" || fr === "tool_calls" || fr === "length" ||
371
+ fr === null) {
372
+ finishReason = fr ?? finishReason;
373
+ }
374
+ const delta = choice.delta;
375
+ 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
+ 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
+ });
394
+ streamedChars += delta.content.length;
395
+ }
396
+ else if (Array.isArray(delta.content)) {
397
+ const chunkStr = delta.content
398
+ .map((c) => (typeof c === "string" ? c : ""))
399
+ .join("");
400
+ 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
+ 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
+ });
419
+ streamedChars += chunkStr.length;
420
+ }
421
+ }
422
+ for (const tc of delta.tool_calls ?? []) {
423
+ const idx = tc.index ?? 0;
424
+ const existing = toolCallMap.get(idx) ??
425
+ {
426
+ id: tc.id,
427
+ function: { name: tc.function?.name, arguments: "" },
428
+ };
429
+ if (!existing.id) {
430
+ existing.id = tc.id ??
431
+ crypto.randomUUID().replace(/-/g, "").slice(0, 24);
432
+ }
433
+ if (tc.function?.name)
434
+ existing.function.name = tc.function.name;
435
+ if (tc.function?.arguments) {
436
+ 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
+ }
444
+ toolCallMap.set(idx, existing);
445
+ }
446
+ }
447
+ if (debugStream) {
448
+ logger.log(`[stream-debug] completed stream chunks=${chunkCount} streamedChars=${streamedChars} finishReason=${finishReason}`);
449
+ }
450
+ const tool_calls = Array.from(toolCallMap.values()).map((tc) => ({
451
+ id: tc.id ?? crypto.randomUUID().replace(/-/g, "").slice(0, 24),
452
+ type: "function",
453
+ function: {
454
+ name: tc.function.name ?? "",
455
+ arguments: tc.function.arguments,
456
+ },
457
+ }));
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
+ const message = normalizeMessage({
472
+ role: "assistant",
473
+ content: text.length > 0 ? text : null,
474
+ tool_calls,
475
+ });
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
+ }
528
+ return {
529
+ ...responseResource,
530
+ };
531
+ }
532
+ const response = await client.chat.completions.create({
533
+ model: input.model,
534
+ messages: messages,
535
+ tools: input.tools,
536
+ tool_choice: toolChoice,
537
+ stream: false,
538
+ ...requestParams,
539
+ });
540
+ const choice = response.choices[0];
541
+ const message = choice.message;
542
+ 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";
558
+ 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,
582
+ usage: response.usage
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
+ promptTokens: response.usage.prompt_tokens ?? 0,
588
+ completionTokens: response.usage.completion_tokens ?? 0,
589
+ totalTokens: response.usage.total_tokens ?? 0,
590
+ }
591
+ : undefined,
592
+ };
593
+ },
594
+ };
595
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAY5C,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,2BAA2B,CAAC;AAuanC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CA8wExC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/src/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAY5C,OAAO,KAAK,EAEV,aAAa,EAKd,MAAM,2BAA2B,CAAC;AAogBnC;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,aAAa,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CA60ExC"}