@ar-ai/brink-cli 0.0.0-stage → 1.0.6

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,1093 @@
1
+ "use strict";
2
+
3
+ const { randomUUID, createHash } = require("crypto");
4
+ const { failure, writeChunk, sseEvents, decodedStream } = require("./bridge-stream");
5
+ const { cleanHeaders, requestWithRetries, upstreamFailure } = require("./transport");
6
+ const { observeActivity } = require("./activity");
7
+
8
+ const UPSTREAM = new URL("https://agentrouter.org/v1/chat/completions");
9
+
10
+
11
+ function invalid(message) {
12
+ throw failure(message, 400, "invalid_request_error");
13
+ }
14
+
15
+ function identifier(prefix) {
16
+ return `${prefix}_${randomUUID().replace(/-/g, "")}`;
17
+ }
18
+
19
+ function convertContent(content, context) {
20
+ if (typeof content === "string") return content;
21
+ if (!Array.isArray(content))
22
+ invalid(`${context} must contain text or a content array`);
23
+ const parts = content.map((part) => {
24
+ if (!part || typeof part !== "object")
25
+ invalid(`Invalid content in ${context}`);
26
+ if (["input_text", "output_text", "text"].includes(part.type)) {
27
+ if (typeof part.text !== "string")
28
+ invalid(`Non-string text in ${context}`);
29
+ return { type: "text", text: part.text };
30
+ }
31
+ if (part.type === "refusal" && typeof part.refusal === "string") {
32
+ return { type: "text", text: part.refusal };
33
+ }
34
+ if (part.type === "input_image" || part.type === "image_url") {
35
+ const source = part.image_url;
36
+ const url = typeof source === "string" ? source : source && source.url;
37
+ if (typeof url !== "string" || !url)
38
+ invalid(
39
+ `Images in ${context} require image_url; file IDs cannot be resolved by the Chat bridge`,
40
+ );
41
+ const image_url = { url };
42
+ const detail = part.detail || (source && source.detail);
43
+ if (detail) image_url.detail = detail;
44
+ return { type: "image_url", image_url };
45
+ }
46
+ invalid(`Unsupported ${context} content type: ${part.type || "unknown"}`);
47
+ });
48
+ return parts.every((part) => part.type === "text")
49
+ ? parts.map((part) => part.text).join("")
50
+ : parts;
51
+ }
52
+
53
+ function buildRequest(payload, tracker) {
54
+ if (!payload || typeof payload !== "object" || Array.isArray(payload))
55
+ invalid("Expected a Responses request object");
56
+ if (typeof payload.model !== "string" || !payload.model)
57
+ invalid("model is required");
58
+ if (payload.previous_response_id || payload.conversation)
59
+ invalid(
60
+ "The Chat bridge requires full input history; previous_response_id and conversation are not supported",
61
+ );
62
+ if (payload.background)
63
+ invalid("Background Responses are not supported by the Chat bridge");
64
+ const byKey = new Map();
65
+ const byWireName = new Map();
66
+ const tools = [];
67
+ const keyFor = (name, namespace) => `${namespace || ""}\0${name}`;
68
+
69
+ function register(name, namespace, kind, definition, namespaceDescription) {
70
+ if (typeof name !== "string" || !name)
71
+ invalid("Every tool requires a name");
72
+ if (namespace != null && (typeof namespace !== "string" || !namespace))
73
+ invalid("Invalid tool namespace");
74
+ const key = keyFor(name, namespace);
75
+ const existing = byKey.get(key);
76
+ if (existing) {
77
+ if (existing.kind !== kind) invalid(`Conflicting tool types for ${name}`);
78
+ if (definition && existing.definition)
79
+ invalid(`Duplicate tool definition: ${name}`);
80
+ return existing;
81
+ }
82
+ const qualified = namespace ? `${namespace}.${name}` : name;
83
+ let wireName = qualified;
84
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(wireName) || byWireName.has(wireName)) {
85
+ wireName = `${qualified.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40)}_${createHash("sha256").update(key).digest("hex").slice(0, 20)}`;
86
+ }
87
+ if (byWireName.has(wireName)) invalid(`Tool name collision: ${qualified}`);
88
+ const descriptor = { name, namespace, kind, wireName, definition };
89
+ byKey.set(key, descriptor);
90
+ byWireName.set(wireName, descriptor);
91
+ if (definition) {
92
+ const fn = { name: wireName };
93
+ const descriptions = [
94
+ namespaceDescription,
95
+ definition.description,
96
+ ].filter((value) => typeof value === "string" && value);
97
+ if (kind === "custom") {
98
+ descriptions.push(
99
+ "This is a freeform tool. Supply its entire raw input as the single string property input, without JSON-encoding that string a second time.",
100
+ );
101
+ if (definition.format && definition.format.type !== "text")
102
+ descriptions.push(
103
+ `Required freeform input format: ${JSON.stringify(definition.format)}`,
104
+ );
105
+ fn.parameters = {
106
+ type: "object",
107
+ properties: {
108
+ input: {
109
+ type: "string",
110
+ description: "The complete raw freeform tool input.",
111
+ },
112
+ },
113
+ required: ["input"],
114
+ additionalProperties: false,
115
+ };
116
+ fn.strict = true;
117
+ } else {
118
+ fn.parameters = definition.parameters || {
119
+ type: "object",
120
+ properties: {},
121
+ };
122
+ if (definition.strict != null) fn.strict = definition.strict;
123
+ }
124
+ if (descriptions.length) fn.description = descriptions.join("\n\n");
125
+ tools.push({ type: "function", function: fn });
126
+ }
127
+ return descriptor;
128
+ }
129
+
130
+ function addTool(tool, namespace, description) {
131
+ if (!tool || typeof tool !== "object") invalid("Invalid tool definition");
132
+ if (tool.type === "web_search" || tool.type === "web_search_preview") {
133
+ tracker?.note(
134
+ "warn",
135
+ "Claude Chat bridge omits the unsupported hosted web-search tool advertisement",
136
+ );
137
+ return;
138
+ }
139
+ if (tool.type === "namespace") {
140
+ if (namespace) invalid("Nested tool namespaces are not supported");
141
+ if (
142
+ typeof tool.name !== "string" ||
143
+ !tool.name ||
144
+ !Array.isArray(tool.tools)
145
+ )
146
+ invalid("Invalid namespace tool definition");
147
+ for (const child of tool.tools)
148
+ addTool(child, tool.name, tool.description);
149
+ return;
150
+ }
151
+ if (!["function", "custom", "freeform"].includes(tool.type))
152
+ invalid(
153
+ `Unsupported Chat bridge tool type: ${tool.type || "unknown"}. Use a function or custom tool instead of hosted Responses tools.`,
154
+ );
155
+ const definition =
156
+ tool.type === "function" && tool.function ? tool.function : tool;
157
+ register(
158
+ definition.name,
159
+ namespace || definition.namespace,
160
+ tool.type === "function" ? "function" : "custom",
161
+ definition,
162
+ description,
163
+ );
164
+ }
165
+ if (payload.tools != null && !Array.isArray(payload.tools))
166
+ invalid("tools must be an array");
167
+ for (const tool of payload.tools || []) addTool(tool);
168
+
169
+ function lookup(name, namespace, kind, replay = false) {
170
+ let found = byKey.get(keyFor(name, namespace));
171
+ if (!found && !namespace) {
172
+ const matches = [...byKey.values()].filter(
173
+ (entry) =>
174
+ entry.name === name ||
175
+ (entry.namespace && `${entry.namespace}.${entry.name}` === name),
176
+ );
177
+ if (matches.length === 1) found = matches[0];
178
+ }
179
+ if (found && kind && found.kind !== kind)
180
+ invalid(`Tool call type does not match definition: ${name}`);
181
+ if (found) return found;
182
+ if (replay) return register(name, namespace, kind);
183
+ invalid(
184
+ `Unknown tool in tool_choice: ${namespace ? `${namespace}.` : ""}${name}`,
185
+ );
186
+ }
187
+
188
+ const messages = [];
189
+ const seenCalls = new Set();
190
+ const answeredCalls = new Set();
191
+ let pendingImages = [];
192
+ function flushImages() {
193
+ if (pendingImages.length)
194
+ messages.push({ role: "user", content: pendingImages });
195
+ pendingImages = [];
196
+ }
197
+ function appendInput(input, instruction = false) {
198
+ if (typeof input === "string") {
199
+ flushImages();
200
+ messages.push({ role: instruction ? "system" : "user", content: input });
201
+ return;
202
+ }
203
+ const items = Array.isArray(input) ? input : [input];
204
+ for (const item of items) {
205
+ if (!item || typeof item !== "object")
206
+ invalid("Invalid Responses input item");
207
+ // Opaque backend reasoning is never converted into visible messages.
208
+ if (item.type === "reasoning") continue;
209
+ const output =
210
+ item.type === "function_call_output" ||
211
+ item.type === "custom_tool_call_output" ||
212
+ item.type === "freeform_call_output";
213
+ if (!output) flushImages();
214
+ if (item.type === "message" || (!item.type && item.role)) {
215
+ if (!["system", "developer", "user", "assistant"].includes(item.role))
216
+ invalid(`Unsupported message role: ${item.role}`);
217
+ const role = item.role === "developer" ? "system" : item.role;
218
+ if (instruction && role !== "system")
219
+ invalid(
220
+ "instructions must contain only system or developer messages",
221
+ );
222
+ messages.push({
223
+ role,
224
+ content: convertContent(item.content, `${item.role} message`),
225
+ });
226
+ } else if (
227
+ ["function_call", "custom_tool_call", "freeform_call"].includes(
228
+ item.type,
229
+ )
230
+ ) {
231
+ if (instruction) invalid("Tool calls are not valid instructions");
232
+ const kind = item.type === "function_call" ? "function" : "custom";
233
+ const descriptor = lookup(item.name, item.namespace, kind, true);
234
+ if (
235
+ typeof item.call_id !== "string" ||
236
+ !item.call_id ||
237
+ seenCalls.has(item.call_id)
238
+ )
239
+ invalid("Tool calls require unique call_id values");
240
+ seenCalls.add(item.call_id);
241
+ const argument = kind === "custom" ? item.input : item.arguments;
242
+ if (typeof argument !== "string")
243
+ invalid(
244
+ `Tool ${item.name} requires string ${kind === "custom" ? "input" : "arguments"}`,
245
+ );
246
+ const call = {
247
+ id: item.call_id,
248
+ type: "function",
249
+ function: {
250
+ name: descriptor.wireName,
251
+ arguments:
252
+ kind === "custom"
253
+ ? JSON.stringify({ input: argument })
254
+ : argument,
255
+ },
256
+ };
257
+ let assistant = messages[messages.length - 1];
258
+ if (!assistant || assistant.role !== "assistant") {
259
+ assistant = { role: "assistant", content: null };
260
+ messages.push(assistant);
261
+ }
262
+ (assistant.tool_calls ||= []).push(call);
263
+ } else if (output) {
264
+ if (instruction) invalid("Tool outputs are not valid instructions");
265
+ if (!seenCalls.has(item.call_id) || answeredCalls.has(item.call_id))
266
+ invalid(
267
+ `Tool output ${item.call_id || "(missing call_id)"} requires its preceding tool call in input history`,
268
+ );
269
+ answeredCalls.add(item.call_id);
270
+ const content = convertContent(item.output, "tool output");
271
+ if (typeof content === "string") {
272
+ messages.push({ role: "tool", tool_call_id: item.call_id, content });
273
+ } else {
274
+ // Chat tool messages permit text only. Lift images into a labelled
275
+ // user message after all adjacent tool replies, keeping call linkage.
276
+ const text = [];
277
+ for (const part of content) {
278
+ if (part.type === "text") text.push(part.text);
279
+ else {
280
+ const label = `[Image returned by tool call ${item.call_id}]`;
281
+ text.push(label);
282
+ pendingImages.push({ type: "text", text: label }, part);
283
+ }
284
+ }
285
+ messages.push({
286
+ role: "tool",
287
+ tool_call_id: item.call_id,
288
+ content: text.join(""),
289
+ });
290
+ }
291
+ } else {
292
+ invalid(`Unsupported Responses input item: ${item.type || "unknown"}`);
293
+ }
294
+ }
295
+ }
296
+ if (payload.instructions != null) appendInput(payload.instructions, true);
297
+ if (payload.input != null) appendInput(payload.input);
298
+ flushImages();
299
+ if (!messages.length) invalid("input or instructions must contain a message");
300
+ const request = {
301
+ model: payload.model,
302
+ messages,
303
+ stream: payload.stream === true,
304
+ };
305
+ if (request.stream) request.stream_options = { include_usage: true };
306
+ for (const key of [
307
+ "temperature",
308
+ "top_p",
309
+ "parallel_tool_calls",
310
+ "user",
311
+ "seed",
312
+ "frequency_penalty",
313
+ "presence_penalty",
314
+ "stop",
315
+ ]) {
316
+ if (payload[key] != null) request[key] = payload[key];
317
+ }
318
+ if (payload.max_output_tokens != null)
319
+ request.max_tokens = payload.max_output_tokens;
320
+ if (tools.length) request.tools = tools;
321
+ const choice = payload.tool_choice;
322
+ if (typeof choice === "string") {
323
+ if (!["auto", "none", "required"].includes(choice))
324
+ invalid(`Unsupported tool_choice: ${choice}`);
325
+ if (tools.length || choice !== "auto") request.tool_choice = choice;
326
+ } else if (choice != null) {
327
+ if (choice.type === "allowed_tools") {
328
+ if (
329
+ !Array.isArray(choice.tools) ||
330
+ !["auto", "required"].includes(choice.mode)
331
+ )
332
+ invalid("Invalid allowed_tools choice");
333
+ const allowed = new Set(
334
+ choice.tools.map(
335
+ (entry) =>
336
+ lookup(
337
+ entry.name,
338
+ entry.namespace,
339
+ entry.type === "custom" || entry.type === "freeform"
340
+ ? "custom"
341
+ : "function",
342
+ ).wireName,
343
+ ),
344
+ );
345
+ request.tools = tools.filter((tool) => allowed.has(tool.function.name));
346
+ if (!request.tools.length)
347
+ invalid("allowed_tools must select at least one available tool");
348
+ request.tool_choice = choice.mode;
349
+ } else if (["function", "custom", "freeform"].includes(choice.type)) {
350
+ const selected = lookup(
351
+ choice.name || (choice.function && choice.function.name),
352
+ choice.namespace,
353
+ choice.type === "function" ? "function" : "custom",
354
+ );
355
+ request.tool_choice = {
356
+ type: "function",
357
+ function: { name: selected.wireName },
358
+ };
359
+ } else invalid(`Unsupported tool_choice type: ${choice.type}`);
360
+ }
361
+ const format = payload.text && payload.text.format;
362
+ if (format && format.type !== "text") {
363
+ if (format.type === "json_object")
364
+ request.response_format = { type: "json_object" };
365
+ else if (format.type === "json_schema") {
366
+ const json_schema = { name: format.name, schema: format.schema };
367
+ if (format.description != null)
368
+ json_schema.description = format.description;
369
+ if (format.strict != null) json_schema.strict = format.strict;
370
+ request.response_format = { type: "json_schema", json_schema };
371
+ } else invalid(`Unsupported text format: ${format.type}`);
372
+ }
373
+ return { request, byWireName };
374
+ }
375
+
376
+ // Decode the one-property custom-tool envelope incrementally, including JSON
377
+ // escapes split between arbitrary Chat argument fragments.
378
+ class CustomInputDecoder {
379
+ constructor() {
380
+ this.prefix = "";
381
+ this.started = false;
382
+ this.closed = false;
383
+ this.escape = false;
384
+ this.unicode = null;
385
+ this.trailing = "";
386
+ this.pendingSurrogate = "";
387
+ this.value = "";
388
+ }
389
+
390
+ feed(fragment) {
391
+ let cursor = 0;
392
+ if (!this.started) {
393
+ this.prefix += fragment;
394
+ const match = /^\s*\{\s*"input"\s*:\s*"/.exec(this.prefix);
395
+ if (!match) return "";
396
+ fragment = this.prefix.slice(match[0].length);
397
+ this.prefix = "";
398
+ this.started = true;
399
+ }
400
+ let delta = this.pendingSurrogate;
401
+ this.pendingSurrogate = "";
402
+ for (; cursor < fragment.length; cursor++) {
403
+ const char = fragment[cursor];
404
+ if (this.closed) {
405
+ this.trailing += fragment.slice(cursor);
406
+ break;
407
+ }
408
+ if (this.unicode !== null) {
409
+ if (!/[0-9a-fA-F]/.test(char))
410
+ throw failure("Invalid Unicode escape in custom tool input");
411
+ this.unicode += char;
412
+ if (this.unicode.length === 4) {
413
+ delta += String.fromCharCode(parseInt(this.unicode, 16));
414
+ this.unicode = null;
415
+ }
416
+ } else if (this.escape) {
417
+ this.escape = false;
418
+ if (char === "u") this.unicode = "";
419
+ else {
420
+ const escapes = {
421
+ '"': '"',
422
+ "\\": "\\",
423
+ "/": "/",
424
+ b: "\b",
425
+ f: "\f",
426
+ n: "\n",
427
+ r: "\r",
428
+ t: "\t",
429
+ };
430
+ if (!(char in escapes))
431
+ throw failure("Invalid escape in custom tool input");
432
+ delta += escapes[char];
433
+ }
434
+ } else if (char === "\\") this.escape = true;
435
+ else if (char === '"') this.closed = true;
436
+ else {
437
+ if (char.charCodeAt(0) < 32)
438
+ throw failure("Unescaped control character in custom tool input");
439
+ delta += char;
440
+ }
441
+ }
442
+ if (delta.length && /[\uD800-\uDBFF]/.test(delta[delta.length - 1])) {
443
+ this.pendingSurrogate = delta.slice(-1);
444
+ delta = delta.slice(0, -1);
445
+ }
446
+ this.value += delta;
447
+ return delta;
448
+ }
449
+
450
+ finish(raw) {
451
+ let parsed;
452
+ try {
453
+ parsed = JSON.parse(raw);
454
+ } catch {
455
+ throw failure("Upstream custom tool arguments are not valid JSON");
456
+ }
457
+ if (
458
+ !parsed ||
459
+ typeof parsed.input !== "string" ||
460
+ Object.keys(parsed).length !== 1 ||
461
+ !this.closed ||
462
+ this.pendingSurrogate ||
463
+ parsed.input !== this.value
464
+ ) {
465
+ throw failure(
466
+ "Upstream custom tool arguments must contain exactly one string property: input",
467
+ );
468
+ }
469
+ return this.value;
470
+ }
471
+ }
472
+
473
+ function responseUsage(usage) {
474
+ if (!usage) return null;
475
+ const input = usage.prompt_tokens ?? usage.input_tokens ?? 0;
476
+ const output = usage.completion_tokens ?? usage.output_tokens ?? 0;
477
+ return {
478
+ input_tokens: input,
479
+ input_tokens_details: {
480
+ cached_tokens:
481
+ usage.prompt_tokens_details?.cached_tokens ??
482
+ usage.input_tokens_details?.cached_tokens ??
483
+ 0,
484
+ },
485
+ output_tokens: output,
486
+ output_tokens_details: {
487
+ reasoning_tokens:
488
+ usage.completion_tokens_details?.reasoning_tokens ??
489
+ usage.output_tokens_details?.reasoning_tokens ??
490
+ 0,
491
+ },
492
+ total_tokens: usage.total_tokens ?? input + output,
493
+ };
494
+ }
495
+
496
+
497
+ class ResponseWriter {
498
+ constructor(res, payload, catalog, streaming, tracker) {
499
+ this.res = res;
500
+ this.catalog = catalog;
501
+ this.streaming = streaming;
502
+ this.tracker = tracker;
503
+ this.sequence = 0;
504
+ this.states = [];
505
+ this.calls = new Map();
506
+ this.message = null;
507
+ this.reasoning = null;
508
+ this.finishReason = null;
509
+ this.sawChoice = false;
510
+ this.response = {
511
+ id: identifier("resp"),
512
+ object: "response",
513
+ created_at: Math.floor(Date.now() / 1000),
514
+ status: "in_progress",
515
+ error: null,
516
+ incomplete_details: null,
517
+ model: payload.model,
518
+ output: [],
519
+ usage: null,
520
+ parallel_tool_calls: payload.parallel_tool_calls ?? true,
521
+ tool_choice: payload.tool_choice ?? "auto",
522
+ tools: payload.tools || [],
523
+ instructions: payload.instructions ?? null,
524
+ max_output_tokens: payload.max_output_tokens ?? null,
525
+ temperature: payload.temperature ?? null,
526
+ top_p: payload.top_p ?? null,
527
+ text: payload.text || { format: { type: "text" } },
528
+ truncation: "disabled",
529
+ metadata: payload.metadata || {},
530
+ store: false,
531
+ };
532
+ }
533
+
534
+ async event(type, fields) {
535
+ // Chat reasoning_content is raw provider reasoning, not a public summary.
536
+ // Display only its presence; preserve the existing client protocol unchanged.
537
+ observeActivity({ type, ...fields }, this.tracker, { summaries: false });
538
+ if (!this.streaming) return;
539
+ await writeChunk(
540
+ this.res,
541
+ `event: ${type}\ndata: ${JSON.stringify({ type, sequence_number: this.sequence++, ...fields })}\n\n`,
542
+ );
543
+ }
544
+
545
+ async start() {
546
+ await this.event("response.created", { response: this.response });
547
+ await this.event("response.in_progress", { response: this.response });
548
+ }
549
+
550
+ async add(item) {
551
+ const state = { item, index: this.states.length };
552
+ this.states.push(state);
553
+ this.response.output.push(item);
554
+ await this.event("response.output_item.added", {
555
+ output_index: state.index,
556
+ item,
557
+ });
558
+ return state;
559
+ }
560
+
561
+ fields(state) {
562
+ return { item_id: state.item.id, output_index: state.index };
563
+ }
564
+
565
+ async textDelta(text, refusal = false) {
566
+ if (typeof text !== "string")
567
+ throw failure("Unsupported non-text upstream assistant content");
568
+ if (!text) return;
569
+ if (!this.message) {
570
+ this.message = await this.add({
571
+ id: identifier("msg"),
572
+ type: "message",
573
+ role: "assistant",
574
+ status: "in_progress",
575
+ content: [],
576
+ });
577
+ }
578
+ const state = this.message;
579
+ const kind = refusal ? "refusal" : "output_text";
580
+ let contentIndex = state.item.content.findIndex(
581
+ (part) => part.type === kind,
582
+ );
583
+ if (contentIndex < 0) {
584
+ contentIndex = state.item.content.length;
585
+ const part = refusal
586
+ ? { type: "refusal", refusal: "" }
587
+ : { type: "output_text", text: "", annotations: [], logprobs: [] };
588
+ state.item.content.push(part);
589
+ await this.event("response.content_part.added", {
590
+ ...this.fields(state),
591
+ content_index: contentIndex,
592
+ part,
593
+ });
594
+ }
595
+ state.item.content[contentIndex][refusal ? "refusal" : "text"] += text;
596
+ await this.event(`response.${kind}.delta`, {
597
+ ...this.fields(state),
598
+ content_index: contentIndex,
599
+ delta: text,
600
+ ...(refusal ? {} : { logprobs: [] }),
601
+ });
602
+ }
603
+
604
+ async reasoningDelta(text) {
605
+ if (typeof text !== "string")
606
+ throw failure("Unsupported non-text upstream reasoning_content");
607
+ if (!text) return;
608
+ if (!this.reasoning) {
609
+ this.reasoning = await this.add({
610
+ id: identifier("rs"),
611
+ type: "reasoning",
612
+ status: "in_progress",
613
+ summary: [],
614
+ });
615
+ const part = { type: "summary_text", text: "" };
616
+ this.reasoning.item.summary.push(part);
617
+ await this.event("response.reasoning_summary_part.added", {
618
+ ...this.fields(this.reasoning),
619
+ summary_index: 0,
620
+ part,
621
+ });
622
+ }
623
+ this.reasoning.item.summary[0].text += text;
624
+ await this.event("response.reasoning_summary_text.delta", {
625
+ ...this.fields(this.reasoning),
626
+ summary_index: 0,
627
+ delta: text,
628
+ });
629
+ }
630
+
631
+ async exposeCall(call, final = false) {
632
+ if (call.state) return;
633
+ const descriptor = this.catalog.get(call.name);
634
+ // Names can arrive in separate deltas. Never guess a custom/function kind
635
+ // from a partial name or expose a name that is still being accumulated.
636
+ if (!descriptor) {
637
+ if (final)
638
+ throw failure(
639
+ `Upstream returned an unknown tool: ${call.name || "(missing name)"}`,
640
+ );
641
+ return;
642
+ }
643
+ if (
644
+ !final &&
645
+ (!call.arguments ||
646
+ [...this.catalog.keys()].some(
647
+ (name) => name !== call.name && name.startsWith(call.name),
648
+ ))
649
+ )
650
+ return;
651
+ call.descriptor = descriptor;
652
+ const item = {
653
+ id: identifier(descriptor.kind === "custom" ? "ctc" : "fc"),
654
+ type: descriptor.kind === "custom" ? "custom_tool_call" : "function_call",
655
+ // A bridge-owned ID remains stable even if Chat fragments its ID.
656
+ // Follow-up requests replay this ID unchanged in calls and outputs.
657
+ call_id: identifier("call"),
658
+ name: descriptor.name,
659
+ status: "in_progress",
660
+ ...(descriptor.namespace ? { namespace: descriptor.namespace } : {}),
661
+ ...(descriptor.kind === "custom" ? { input: "" } : { arguments: "" }),
662
+ };
663
+ call.state = await this.add(item);
664
+ if (descriptor.kind === "custom") call.decoder = new CustomInputDecoder();
665
+ await this.argumentDelta(call, call.arguments);
666
+ }
667
+
668
+ async argumentDelta(call, fragment) {
669
+ if (!fragment) return;
670
+ const custom = call.descriptor.kind === "custom";
671
+ const delta = custom ? call.decoder.feed(fragment) : fragment;
672
+ if (!delta) return;
673
+ call.state.item[custom ? "input" : "arguments"] += delta;
674
+ await this.event(
675
+ custom
676
+ ? "response.custom_tool_call_input.delta"
677
+ : "response.function_call_arguments.delta",
678
+ { ...this.fields(call.state), delta },
679
+ );
680
+ }
681
+
682
+ async toolDelta(tool, fallbackIndex) {
683
+ const index = tool.index ?? fallbackIndex;
684
+ if (!Number.isInteger(index) || index < 0)
685
+ throw failure("Upstream tool call is missing a valid index");
686
+ if (tool.type && tool.type !== "function")
687
+ throw failure(`Unsupported upstream tool call type: ${tool.type}`);
688
+ let call = this.calls.get(index);
689
+ if (!call) {
690
+ call = { name: "", arguments: "", upstreamId: "", state: null };
691
+ this.calls.set(index, call);
692
+ }
693
+ if (tool.id != null) {
694
+ if (typeof tool.id !== "string")
695
+ throw failure("Invalid upstream tool call ID");
696
+ call.upstreamId += tool.id;
697
+ }
698
+ const fn = tool.function || {};
699
+ if (fn.name != null) {
700
+ if (typeof fn.name !== "string")
701
+ throw failure("Invalid upstream tool name");
702
+ if (call.state && fn.name)
703
+ throw failure(
704
+ "Upstream changed a tool name after streaming its arguments",
705
+ );
706
+ call.name += fn.name;
707
+ }
708
+ const fragment = fn.arguments ?? "";
709
+ if (typeof fragment !== "string")
710
+ throw failure("Invalid upstream tool arguments");
711
+ call.arguments += fragment;
712
+ if (call.state) await this.argumentDelta(call, fragment);
713
+ else await this.exposeCall(call);
714
+ }
715
+
716
+ async consume(chunk, complete = false) {
717
+ // AgentRouter emits JSON null for Anthropic block boundary events.
718
+ // They carry no Chat delta; finish_reason and [DONE] still remain required.
719
+ if (chunk === null && !complete) return;
720
+ if (!chunk || typeof chunk !== "object")
721
+ throw failure("Invalid Chat response payload");
722
+ if (chunk.error) {
723
+ const message =
724
+ typeof chunk.error === "string"
725
+ ? chunk.error
726
+ : chunk.error.message || "AgentRouter returned an error";
727
+ throw failure(message, 502, chunk.error.code || "upstream_error");
728
+ }
729
+ if (chunk.usage) {
730
+ this.response.usage = responseUsage(chunk.usage);
731
+ this.tracker?.usage(chunk.usage);
732
+ }
733
+ if (!Array.isArray(chunk.choices))
734
+ throw failure("Chat response has no choices array");
735
+ if (chunk.choices.length > 1)
736
+ throw failure(
737
+ "Multiple Chat choices cannot be represented as one Responses output",
738
+ );
739
+ for (const choice of chunk.choices) {
740
+ if (choice.index != null && choice.index !== 0)
741
+ throw failure("Unexpected Chat choice index");
742
+ if (this.finishReason)
743
+ throw failure("Upstream emitted a choice after its finish_reason");
744
+ this.sawChoice = true;
745
+ const delta = complete ? choice.message : choice.delta;
746
+ if (!delta || typeof delta !== "object")
747
+ throw failure("Chat choice is missing assistant content");
748
+ if (delta.role && delta.role !== "assistant")
749
+ throw failure(`Unexpected upstream role: ${delta.role}`);
750
+ if (delta.reasoning_content != null)
751
+ await this.reasoningDelta(delta.reasoning_content);
752
+ if (delta.content != null) {
753
+ if (typeof delta.content === "string")
754
+ await this.textDelta(delta.content);
755
+ else if (Array.isArray(delta.content)) {
756
+ for (const part of delta.content) {
757
+ if (part.type === "text" || part.type === "output_text")
758
+ await this.textDelta(part.text);
759
+ else if (part.type === "refusal")
760
+ await this.textDelta(part.refusal, true);
761
+ else
762
+ throw failure(
763
+ `Unsupported upstream assistant content: ${part.type}`,
764
+ );
765
+ }
766
+ } else throw failure("Unsupported upstream assistant content");
767
+ }
768
+ if (delta.refusal != null) await this.textDelta(delta.refusal, true);
769
+ if (delta.audio || delta.function_call)
770
+ throw failure(
771
+ "Unsupported upstream audio or legacy function_call output",
772
+ );
773
+ if (delta.tool_calls != null) {
774
+ if (!Array.isArray(delta.tool_calls))
775
+ throw failure("Invalid upstream tool_calls");
776
+ for (let i = 0; i < delta.tool_calls.length; i++)
777
+ await this.toolDelta(delta.tool_calls[i], complete ? i : undefined);
778
+ }
779
+ if (choice.finish_reason != null) {
780
+ if (
781
+ !["stop", "tool_calls", "length", "content_filter"].includes(
782
+ choice.finish_reason,
783
+ )
784
+ )
785
+ throw failure(
786
+ `Unsupported upstream finish_reason: ${choice.finish_reason}`,
787
+ );
788
+ this.finishReason = choice.finish_reason;
789
+ }
790
+ }
791
+ }
792
+
793
+ async finish() {
794
+ if (!this.sawChoice || !this.finishReason)
795
+ throw failure(
796
+ "Upstream stream ended without a finish_reason",
797
+ 502,
798
+ "incomplete_stream",
799
+ );
800
+ const incomplete =
801
+ this.finishReason === "length" || this.finishReason === "content_filter";
802
+ if (this.finishReason === "tool_calls" && !this.calls.size)
803
+ throw failure("Upstream finished with tool_calls but returned no calls");
804
+ for (const call of this.calls.values()) {
805
+ await this.exposeCall(call, true);
806
+ if (!incomplete && call.descriptor.kind === "custom")
807
+ call.decoder.finish(call.arguments);
808
+ if (!incomplete && call.descriptor.kind === "function") {
809
+ try {
810
+ JSON.parse(call.arguments);
811
+ } catch {
812
+ throw failure(
813
+ `Invalid JSON arguments for tool ${call.descriptor.name}`,
814
+ );
815
+ }
816
+ }
817
+ }
818
+ // Validate every call before publishing any executable output_item.done.
819
+ for (const state of this.states) {
820
+ const item = state.item;
821
+ item.status = incomplete ? "incomplete" : "completed";
822
+ if (item.type === "message") {
823
+ for (
824
+ let content_index = 0;
825
+ content_index < item.content.length;
826
+ content_index++
827
+ ) {
828
+ const part = item.content[content_index];
829
+ const fields = { ...this.fields(state), content_index };
830
+ if (part.type === "output_text")
831
+ await this.event("response.output_text.done", {
832
+ ...fields,
833
+ text: part.text,
834
+ logprobs: [],
835
+ });
836
+ else
837
+ await this.event("response.refusal.done", {
838
+ ...fields,
839
+ refusal: part.refusal,
840
+ });
841
+ await this.event("response.content_part.done", { ...fields, part });
842
+ }
843
+ } else if (item.type === "reasoning") {
844
+ const fields = { ...this.fields(state), summary_index: 0 };
845
+ await this.event("response.reasoning_summary_text.done", {
846
+ ...fields,
847
+ text: item.summary[0].text,
848
+ });
849
+ await this.event("response.reasoning_summary_part.done", {
850
+ ...fields,
851
+ part: item.summary[0],
852
+ });
853
+ } else if (item.type === "custom_tool_call") {
854
+ await this.event("response.custom_tool_call_input.done", {
855
+ ...this.fields(state),
856
+ input: item.input,
857
+ });
858
+ } else {
859
+ await this.event("response.function_call_arguments.done", {
860
+ ...this.fields(state),
861
+ name: item.name,
862
+ arguments: item.arguments,
863
+ });
864
+ }
865
+ await this.event("response.output_item.done", {
866
+ output_index: state.index,
867
+ item,
868
+ });
869
+ }
870
+ this.response.status = incomplete ? "incomplete" : "completed";
871
+ this.response.completed_at = Math.floor(Date.now() / 1000);
872
+ this.response.end_turn = !this.calls.size;
873
+ this.response.output_text = this.response.output
874
+ .filter((item) => item.type === "message")
875
+ .flatMap((item) => item.content)
876
+ .filter((part) => part.type === "output_text")
877
+ .map((part) => part.text)
878
+ .join("");
879
+ if (incomplete)
880
+ this.response.incomplete_details = {
881
+ reason:
882
+ this.finishReason === "length"
883
+ ? "max_output_tokens"
884
+ : "content_filter",
885
+ };
886
+ await this.event(
887
+ incomplete ? "response.incomplete" : "response.completed",
888
+ { response: this.response },
889
+ );
890
+ if (incomplete)
891
+ this.tracker?.fail(
892
+ "AgentRouter reported an incomplete response (generation limit or content restriction)",
893
+ );
894
+ else this.tracker?.complete();
895
+ return this.response;
896
+ }
897
+
898
+ async fail(error) {
899
+ this.response.status = "failed";
900
+ this.response.error = {
901
+ code: error.code || "bridge_error",
902
+ message: error.message || "AgentRouter stream failed",
903
+ };
904
+ for (const item of this.response.output) item.status = "incomplete";
905
+ await this.event("error", { ...this.response.error, param: null });
906
+ await this.event("response.failed", { response: this.response });
907
+ }
908
+ }
909
+
910
+
911
+
912
+ async function readJSON(stream) {
913
+ const chunks = [];
914
+ for await (const chunk of stream) chunks.push(chunk);
915
+ try {
916
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
917
+ } catch {
918
+ throw failure("AgentRouter returned invalid Chat Completions JSON");
919
+ }
920
+ }
921
+
922
+ async function bridgeResponses(req, res, payload, { agent, tracker } = {}) {
923
+ let upstreamRequest;
924
+ let upstreamResponse;
925
+ let upstreamStream;
926
+ const controller = new AbortController();
927
+ let stream;
928
+ let writer;
929
+ let cancelled = false;
930
+ let passthrough = false;
931
+ const cancel = () => {
932
+ if (res.writableFinished) return;
933
+ cancelled = true;
934
+ controller.abort();
935
+ upstreamStream?.destroy();
936
+ upstreamRequest?.destroy();
937
+ upstreamResponse?.destroy();
938
+ if (stream && stream !== upstreamResponse) stream.destroy();
939
+ };
940
+ req.once("aborted", cancel);
941
+ res.once("close", cancel);
942
+ res.once("error", cancel);
943
+ try {
944
+ const { request, byWireName } = buildRequest(payload, tracker);
945
+ const body = JSON.stringify(request);
946
+ const headers = cleanHeaders(req.headers);
947
+ for (const key of [
948
+ "host",
949
+ "content-length",
950
+ "content-encoding",
951
+ "expect",
952
+ "accept-encoding",
953
+ ])
954
+ delete headers[key];
955
+ headers.host = UPSTREAM.host;
956
+ headers["content-type"] = "application/json";
957
+ headers["content-length"] = Buffer.byteLength(body);
958
+ headers["accept-encoding"] = "identity";
959
+ headers.accept ??= request.stream
960
+ ? "text/event-stream"
961
+ : "application/json";
962
+ if (cancelled || req.aborted || res.destroyed) return;
963
+ const result = await requestWithRetries(
964
+ UPSTREAM,
965
+ { method: "POST", headers, agent },
966
+ body,
967
+ {
968
+ signal: controller.signal,
969
+ tracker,
970
+ clientOwnsRetries: req.headers["x-agentrouter-retry-owner"] === "client",
971
+ },
972
+ );
973
+ upstreamRequest = result.request;
974
+ upstreamResponse = result.response;
975
+ upstreamStream = result.stream;
976
+ const status = upstreamResponse.statusCode || 502;
977
+ tracker?.upstream(status);
978
+ if (status < 200 || status >= 300 || result.retryReason) {
979
+ tracker?.fail(upstreamFailure(result.retryReason ? null : status, { code: result.retryReason }));
980
+ passthrough = true;
981
+ res.writeHead(status, cleanHeaders(upstreamResponse.headers));
982
+ for await (const chunk of upstreamStream) await writeChunk(res, chunk);
983
+ res.end();
984
+ return;
985
+ }
986
+ stream = decodedStream(upstreamResponse, upstreamStream);
987
+ writer = new ResponseWriter(
988
+ res,
989
+ payload,
990
+ byWireName,
991
+ request.stream,
992
+ tracker,
993
+ );
994
+ const responseHeaders = cleanHeaders(upstreamResponse.headers);
995
+ for (const key of ["content-length", "content-encoding", "etag"])
996
+ delete responseHeaders[key];
997
+ responseHeaders["cache-control"] = "no-cache";
998
+ responseHeaders["x-accel-buffering"] = "no";
999
+ responseHeaders["openai-model"] = payload.model;
1000
+ if (request.stream) {
1001
+ responseHeaders["content-type"] = "text/event-stream; charset=utf-8";
1002
+ res.writeHead(200, responseHeaders);
1003
+ res.flushHeaders?.();
1004
+ await writer.start();
1005
+ const contentType = String(
1006
+ upstreamResponse.headers["content-type"] || "",
1007
+ );
1008
+ if (contentType.includes("application/json")) {
1009
+ await writer.consume(await readJSON(stream), true);
1010
+ } else {
1011
+ let done = false;
1012
+ for await (const event of sseEvents(stream)) {
1013
+ if (done) continue;
1014
+ if (event.data.trim() === "[DONE]") {
1015
+ done = true;
1016
+ // Consume through HTTP EOF so the shared agent can reuse this
1017
+ // socket; breaking the async iterator would destroy it.
1018
+ continue;
1019
+ }
1020
+ let chunk;
1021
+ try {
1022
+ chunk = JSON.parse(event.data);
1023
+ } catch {
1024
+ throw failure("AgentRouter returned malformed SSE JSON");
1025
+ }
1026
+ if (event.event === "error" && !chunk.error)
1027
+ throw failure(
1028
+ chunk.message || "AgentRouter returned a stream error",
1029
+ 502,
1030
+ chunk.code || "upstream_error",
1031
+ );
1032
+ await writer.consume(chunk);
1033
+ }
1034
+ if (!done)
1035
+ throw failure(
1036
+ "Upstream SSE ended before [DONE]",
1037
+ 502,
1038
+ "incomplete_stream",
1039
+ );
1040
+ }
1041
+ await writer.finish();
1042
+ res.end();
1043
+ } else {
1044
+ await writer.consume(await readJSON(stream), true);
1045
+ const response = await writer.finish();
1046
+ responseHeaders["content-type"] = "application/json";
1047
+ res.writeHead(200, responseHeaders);
1048
+ res.end(JSON.stringify(response));
1049
+ }
1050
+ } catch (error) {
1051
+ if (!cancelled && !res.destroyed && !res.writableEnded)
1052
+ tracker?.fail(upstreamFailure(null, error));
1053
+ controller.abort();
1054
+ upstreamStream?.destroy();
1055
+ upstreamRequest?.destroy();
1056
+ upstreamResponse?.destroy();
1057
+ if (stream && stream !== upstreamResponse) stream.destroy();
1058
+ if (cancelled || res.destroyed || res.writableEnded) return;
1059
+ if (passthrough) {
1060
+ res.destroy();
1061
+ return;
1062
+ }
1063
+ if (res.headersSent) {
1064
+ if (writer && writer.streaming) {
1065
+ try {
1066
+ await writer.fail(error);
1067
+ res.end();
1068
+ } catch {
1069
+ res.destroy();
1070
+ }
1071
+ } else res.destroy();
1072
+ } else {
1073
+ res.writeHead(error.status || 502, {
1074
+ "content-type": "application/json",
1075
+ });
1076
+ res.end(
1077
+ JSON.stringify({
1078
+ error: {
1079
+ type: error.status === 400 ? "invalid_request_error" : "api_error",
1080
+ code: error.code || "bridge_error",
1081
+ message: error.message,
1082
+ },
1083
+ }),
1084
+ );
1085
+ }
1086
+ } finally {
1087
+ req.off("aborted", cancel);
1088
+ res.off("close", cancel);
1089
+ res.off("error", cancel);
1090
+ }
1091
+ }
1092
+
1093
+ module.exports = { bridgeResponses };