@ar-ai/brink-cli 0.0.0-stage → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -2
- package/package.json +18 -4
- package/support/activity.js +102 -0
- package/support/brand.js +110 -0
- package/support/bridge-stream.js +109 -0
- package/support/brink.js +275 -0
- package/support/chat-bridge.js +1093 -0
- package/support/client-setup.js +620 -0
- package/support/messages-bridge.js +317 -0
- package/support/messages-request.js +442 -0
- package/support/model-catalog.js +200 -0
- package/support/monitor.js +443 -0
- package/support/proxy.js +480 -0
- package/support/setup-ui.js +333 -0
- package/support/terminal-ui.js +630 -0
- package/support/transport.js +485 -0
- package/support/usage.js +144 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require("crypto");
|
|
4
|
+
const { buildMessagesRequest } = require("./messages-request");
|
|
5
|
+
const { cleanHeaders, requestWithRetries, upstreamFailure } = require("./transport");
|
|
6
|
+
const { failure, writeChunk, sseEvents, decodedStream } = require("./bridge-stream");
|
|
7
|
+
const { observeActivity } = require("./activity");
|
|
8
|
+
|
|
9
|
+
const UPSTREAM = new URL("https://agentrouter.org/v1/responses");
|
|
10
|
+
|
|
11
|
+
function messageUsage(usage) {
|
|
12
|
+
if (!usage) return {};
|
|
13
|
+
const result = {};
|
|
14
|
+
const cached = usage.input_tokens_details?.cached_tokens;
|
|
15
|
+
if (Number.isFinite(usage.input_tokens))
|
|
16
|
+
result.input_tokens = Math.max(0, usage.input_tokens - (cached || 0));
|
|
17
|
+
if (Number.isFinite(cached)) result.cache_read_input_tokens = cached;
|
|
18
|
+
if (Number.isFinite(usage.output_tokens)) result.output_tokens = usage.output_tokens;
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function errorBody(error) {
|
|
23
|
+
return { type: "error", error: {
|
|
24
|
+
type: error.status === 400 ? "invalid_request_error" :
|
|
25
|
+
error.status === 401 ? "authentication_error" :
|
|
26
|
+
error.status === 403 ? "permission_error" :
|
|
27
|
+
error.status === 429 ? "rate_limit_error" : "api_error",
|
|
28
|
+
message: error.message,
|
|
29
|
+
} };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class MessagesWriter {
|
|
33
|
+
constructor(res, payload, tracker) {
|
|
34
|
+
this.res = res;
|
|
35
|
+
this.streaming = payload.stream === true;
|
|
36
|
+
this.showThinking = payload.thinking?.display !== "omitted";
|
|
37
|
+
this.tracker = tracker;
|
|
38
|
+
this.items = new Map();
|
|
39
|
+
this.blocks = new Map();
|
|
40
|
+
this.terminal = null;
|
|
41
|
+
this.refused = false;
|
|
42
|
+
this.toolNames = new Set((payload.tools || []).map(tool => tool.name));
|
|
43
|
+
this.message = { id: `msg_${randomUUID().replace(/-/g, "")}`, type: "message",
|
|
44
|
+
role: "assistant", model: payload.model, content: [], stop_reason: null,
|
|
45
|
+
stop_sequence: null, usage: {} };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async event(type, fields = {}) {
|
|
49
|
+
if (this.streaming)
|
|
50
|
+
await writeChunk(this.res, `event: ${type}\ndata: ${JSON.stringify({ type, ...fields })}\n\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async start() {
|
|
54
|
+
await this.event("message_start", { message: { ...this.message,
|
|
55
|
+
usage: { input_tokens: 0, output_tokens: 0 } } });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async block(key, content) {
|
|
59
|
+
let state = this.blocks.get(key);
|
|
60
|
+
if (state) return state;
|
|
61
|
+
state = { index: this.message.content.length, content, text: "", closed: false };
|
|
62
|
+
this.blocks.set(key, state);
|
|
63
|
+
this.message.content.push(content);
|
|
64
|
+
await this.event("content_block_start", { index: state.index, content_block: { ...content } });
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async append(state, text, final = false) {
|
|
69
|
+
if (typeof text !== "string") throw failure("AgentRouter returned non-text content");
|
|
70
|
+
if (final) {
|
|
71
|
+
if (!text.startsWith(state.text)) throw failure("AgentRouter changed already-streamed content");
|
|
72
|
+
text = text.slice(state.text.length);
|
|
73
|
+
}
|
|
74
|
+
if (!text) return;
|
|
75
|
+
if (state.closed) throw failure("AgentRouter changed a completed content block");
|
|
76
|
+
state.text += text;
|
|
77
|
+
const kind = state.content.type;
|
|
78
|
+
if (kind === "text") state.content.text = state.text;
|
|
79
|
+
else if (kind === "thinking") state.content.thinking = state.text;
|
|
80
|
+
await this.event("content_block_delta", { index: state.index, delta:
|
|
81
|
+
kind === "tool_use" ? { type: "input_json_delta", partial_json: text } :
|
|
82
|
+
kind === "thinking" ? { type: "thinking_delta", thinking: text } :
|
|
83
|
+
{ type: "text_delta", text } });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async close(state) {
|
|
87
|
+
if (state.closed) return;
|
|
88
|
+
if (state.content.type === "tool_use") {
|
|
89
|
+
let input;
|
|
90
|
+
try { input = JSON.parse(state.text); } catch {
|
|
91
|
+
throw failure("AgentRouter returned incomplete or invalid tool arguments", 502, "incomplete_stream");
|
|
92
|
+
}
|
|
93
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
94
|
+
throw failure("AgentRouter tool arguments must be a JSON object");
|
|
95
|
+
state.content.input = input;
|
|
96
|
+
}
|
|
97
|
+
state.closed = true;
|
|
98
|
+
await this.event("content_block_stop", { index: state.index });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async item(index, item, final = false) {
|
|
102
|
+
if (!item || !Number.isInteger(index)) throw failure("AgentRouter returned an invalid output item");
|
|
103
|
+
const previous = this.items.get(index);
|
|
104
|
+
if (previous && (previous.type !== item.type || (previous.id && item.id && previous.id !== item.id)))
|
|
105
|
+
throw failure("AgentRouter changed an output item");
|
|
106
|
+
this.items.set(index, item);
|
|
107
|
+
if (item.type === "function_call") {
|
|
108
|
+
if (!item.call_id || !this.toolNames.has(item.name))
|
|
109
|
+
throw failure("AgentRouter returned an unknown tool or missing call ID");
|
|
110
|
+
const state = await this.block(`${index}:tool`, {
|
|
111
|
+
type: "tool_use", id: item.call_id, name: item.name, input: {},
|
|
112
|
+
});
|
|
113
|
+
if (state.content.id !== item.call_id || state.content.name !== item.name)
|
|
114
|
+
throw failure("AgentRouter changed a tool call identity");
|
|
115
|
+
if (final || item.arguments) await this.append(state, item.arguments || "", true);
|
|
116
|
+
if (final) await this.close(state);
|
|
117
|
+
} else if (item.type === "message" || item.type === "reasoning") {
|
|
118
|
+
if (item.type === "message" && item.role !== "assistant")
|
|
119
|
+
throw failure("AgentRouter returned a non-assistant output message");
|
|
120
|
+
const parts = item.type === "reasoning" ? item.summary : item.content;
|
|
121
|
+
for (const [partIndex, part] of (parts || []).entries())
|
|
122
|
+
await this.part(index, partIndex, part, final);
|
|
123
|
+
} else {
|
|
124
|
+
throw failure(`Unsupported Responses output type: ${item.type}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async part(index, partIndex, part, final = false) {
|
|
129
|
+
const thinking = part?.type === "summary_text";
|
|
130
|
+
if (thinking && !this.showThinking) return;
|
|
131
|
+
if (!thinking && !["output_text", "refusal"].includes(part?.type))
|
|
132
|
+
throw failure(`Unsupported Responses content type: ${part?.type}`);
|
|
133
|
+
const refused = part.type === "refusal";
|
|
134
|
+
this.refused ||= refused;
|
|
135
|
+
const state = await this.block(`${index}:${thinking ? "summary" : "text"}:${partIndex}`,
|
|
136
|
+
thinking ? { type: "thinking", thinking: "", signature: "" } : { type: "text", text: "" });
|
|
137
|
+
await this.append(state, (refused ? part.refusal : part.text) || "", true);
|
|
138
|
+
if (final) await this.close(state);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async consume(value) {
|
|
142
|
+
if (!value || typeof value !== "object") throw failure("Invalid Responses payload");
|
|
143
|
+
if (this.terminal) throw failure("AgentRouter emitted data after its terminal response");
|
|
144
|
+
const type = value.type;
|
|
145
|
+
if (type === "error" || type === "response.failed" || value.error) {
|
|
146
|
+
const error = value.error || value.response?.error || value;
|
|
147
|
+
throw failure(typeof error === "string" ? error : error.message || "AgentRouter response failed",
|
|
148
|
+
502, error.code || "upstream_error");
|
|
149
|
+
}
|
|
150
|
+
observeActivity(value, this.tracker, { summaries: this.showThinking });
|
|
151
|
+
if (type === "response.output_item.added" || type === "response.output_item.done") {
|
|
152
|
+
await this.item(value.output_index, value.item, type.endsWith(".done"));
|
|
153
|
+
} else if (type === "response.content_part.added" || type === "response.content_part.done") {
|
|
154
|
+
await this.part(value.output_index, value.content_index, value.part, type.endsWith(".done"));
|
|
155
|
+
} else if (type === "response.reasoning_summary_part.added" || type === "response.reasoning_summary_part.done") {
|
|
156
|
+
await this.part(value.output_index, value.summary_index, value.part, type.endsWith(".done"));
|
|
157
|
+
} else if (["response.output_text.delta", "response.refusal.delta", "response.reasoning_summary_text.delta"].includes(type)) {
|
|
158
|
+
const thinking = type === "response.reasoning_summary_text.delta";
|
|
159
|
+
if (thinking && !this.showThinking) return;
|
|
160
|
+
this.refused ||= type === "response.refusal.delta";
|
|
161
|
+
const key = `${value.output_index}:${thinking ? "summary" : "text"}:${thinking ? value.summary_index : value.content_index}`;
|
|
162
|
+
const state = await this.block(key, thinking ? { type: "thinking", thinking: "", signature: "" } : { type: "text", text: "" });
|
|
163
|
+
await this.append(state, value.delta);
|
|
164
|
+
} else if (type === "response.function_call_arguments.delta" || type === "response.function_call_arguments.done") {
|
|
165
|
+
const state = this.blocks.get(`${value.output_index}:tool`);
|
|
166
|
+
if (!state) throw failure("AgentRouter emitted arguments before the tool identity");
|
|
167
|
+
await this.append(state, value.delta ?? value.arguments, type.endsWith(".done"));
|
|
168
|
+
} else if (["response.output_text.done", "response.refusal.done", "response.reasoning_summary_text.done"].includes(type)) {
|
|
169
|
+
const thinking = type === "response.reasoning_summary_text.done";
|
|
170
|
+
await this.part(value.output_index, thinking ? value.summary_index : value.content_index,
|
|
171
|
+
thinking ? { type: "summary_text", text: value.text } :
|
|
172
|
+
type === "response.refusal.done" ? { type: "refusal", refusal: value.refusal } :
|
|
173
|
+
{ type: "output_text", text: value.text });
|
|
174
|
+
} else if (type === "response.completed" || type === "response.incomplete" || value.object === "response") {
|
|
175
|
+
const response = value.response || value;
|
|
176
|
+
if (!["completed", "incomplete"].includes(response.status))
|
|
177
|
+
throw failure(response.error?.message || "AgentRouter response did not complete", 502, "incomplete_stream");
|
|
178
|
+
if (!Array.isArray(response.output)) throw failure("AgentRouter response is missing output");
|
|
179
|
+
for (const [index, item] of response.output.entries()) await this.item(index, item, true);
|
|
180
|
+
const incomplete = response.status === "incomplete";
|
|
181
|
+
const reason = response.incomplete_details?.reason;
|
|
182
|
+
if (incomplete && !["max_output_tokens", "content_filter"].includes(reason))
|
|
183
|
+
throw failure("AgentRouter response is incomplete", 502, "incomplete_stream");
|
|
184
|
+
this.message.stop_reason = incomplete ? reason === "max_output_tokens" ? "max_tokens" : "refusal" :
|
|
185
|
+
this.message.content.some(part => part.type === "tool_use") ? "tool_use" :
|
|
186
|
+
this.refused ? "refusal" : "end_turn";
|
|
187
|
+
this.message.usage = messageUsage(response.usage);
|
|
188
|
+
if (response.usage) this.tracker?.usage(response.usage);
|
|
189
|
+
if (incomplete) this.tracker?.fail("AgentRouter reported an incomplete response (generation limit or content restriction)");
|
|
190
|
+
this.terminal = response.status;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async finish() {
|
|
195
|
+
if (!this.terminal) throw failure("AgentRouter stream ended before its terminal response", 502, "incomplete_stream");
|
|
196
|
+
for (const state of this.blocks.values()) await this.close(state);
|
|
197
|
+
await this.event("message_delta", { delta: { stop_reason: this.message.stop_reason, stop_sequence: null }, usage: this.message.usage });
|
|
198
|
+
if (this.terminal === "completed") this.tracker?.complete();
|
|
199
|
+
await this.event("message_stop");
|
|
200
|
+
return this.message;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function readJSON(stream) {
|
|
205
|
+
const chunks = [];
|
|
206
|
+
let bytes = 0;
|
|
207
|
+
for await (const chunk of stream) {
|
|
208
|
+
bytes += chunk.length;
|
|
209
|
+
if (bytes > 32 * 1024 * 1024) throw failure("AgentRouter JSON response exceeds 32 MiB");
|
|
210
|
+
chunks.push(chunk);
|
|
211
|
+
}
|
|
212
|
+
try { return JSON.parse(Buffer.concat(chunks).toString("utf8")); }
|
|
213
|
+
catch { throw failure("AgentRouter returned invalid Responses JSON"); }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function bridgeMessages(req, res, payload, { agent, tracker } = {}) {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
let result;
|
|
219
|
+
let stream;
|
|
220
|
+
let writer;
|
|
221
|
+
let passthrough = false;
|
|
222
|
+
let cancelled = false;
|
|
223
|
+
const destroyUpstream = () => {
|
|
224
|
+
controller.abort();
|
|
225
|
+
stream?.destroy();
|
|
226
|
+
result?.stream.destroy();
|
|
227
|
+
result?.response.destroy();
|
|
228
|
+
result?.request.destroy();
|
|
229
|
+
};
|
|
230
|
+
const cancel = () => {
|
|
231
|
+
if (res.writableFinished) return;
|
|
232
|
+
cancelled = true;
|
|
233
|
+
destroyUpstream();
|
|
234
|
+
};
|
|
235
|
+
req.once("aborted", cancel);
|
|
236
|
+
res.once("close", cancel);
|
|
237
|
+
res.once("error", cancel);
|
|
238
|
+
try {
|
|
239
|
+
const request = buildMessagesRequest(payload);
|
|
240
|
+
if (payload.output_config?.effort === "max")
|
|
241
|
+
tracker?.note("info", "Mapped Messages max effort to Responses xhigh");
|
|
242
|
+
const body = JSON.stringify(request);
|
|
243
|
+
const headers = cleanHeaders(req.headers);
|
|
244
|
+
if (!headers.authorization && typeof headers["x-api-key"] === "string")
|
|
245
|
+
headers.authorization = `Bearer ${headers["x-api-key"]}`;
|
|
246
|
+
for (const key of ["host", "content-length", "content-encoding", "expect", "accept-encoding", "x-api-key", "anthropic-version", "anthropic-beta"])
|
|
247
|
+
delete headers[key];
|
|
248
|
+
headers.host = UPSTREAM.host;
|
|
249
|
+
headers["content-type"] = "application/json";
|
|
250
|
+
headers["content-length"] = Buffer.byteLength(body);
|
|
251
|
+
headers["accept-encoding"] = "identity";
|
|
252
|
+
headers.accept = request.stream ? "text/event-stream" : "application/json";
|
|
253
|
+
if (cancelled || req.aborted || res.destroyed) return;
|
|
254
|
+
result = await requestWithRetries(UPSTREAM, { method: "POST", headers, agent }, body, {
|
|
255
|
+
signal: controller.signal, tracker,
|
|
256
|
+
clientOwnsRetries: req.headers["x-agentrouter-retry-owner"] === "client",
|
|
257
|
+
});
|
|
258
|
+
const status = result.response.statusCode || 502;
|
|
259
|
+
tracker?.upstream(status);
|
|
260
|
+
if (status < 200 || status >= 300 || result.retryReason) {
|
|
261
|
+
tracker?.fail(upstreamFailure(result.retryReason ? null : status, { code: result.retryReason }));
|
|
262
|
+
passthrough = true;
|
|
263
|
+
res.writeHead(status, cleanHeaders(result.response.headers));
|
|
264
|
+
for await (const chunk of result.stream) await writeChunk(res, chunk);
|
|
265
|
+
res.end();
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
stream = decodedStream(result.response, result.stream);
|
|
269
|
+
writer = new MessagesWriter(res, payload, tracker);
|
|
270
|
+
const responseHeaders = cleanHeaders(result.response.headers);
|
|
271
|
+
for (const key of ["content-length", "content-encoding", "etag"])
|
|
272
|
+
delete responseHeaders[key];
|
|
273
|
+
responseHeaders["cache-control"] = "no-cache";
|
|
274
|
+
responseHeaders["x-accel-buffering"] = "no";
|
|
275
|
+
responseHeaders["content-type"] = writer.streaming ? "text/event-stream; charset=utf-8" : "application/json";
|
|
276
|
+
if (writer.streaming) {
|
|
277
|
+
res.writeHead(200, responseHeaders);
|
|
278
|
+
res.flushHeaders?.();
|
|
279
|
+
await writer.start();
|
|
280
|
+
}
|
|
281
|
+
if (String(result.response.headers["content-type"] || "").includes("text/event-stream")) {
|
|
282
|
+
for await (const event of sseEvents(stream)) {
|
|
283
|
+
if (event.data.trim() === "[DONE]") continue;
|
|
284
|
+
let value;
|
|
285
|
+
try { value = JSON.parse(event.data); }
|
|
286
|
+
catch { throw failure("AgentRouter returned malformed Responses SSE JSON"); }
|
|
287
|
+
if (!value.type && event.event) value.type = event.event;
|
|
288
|
+
await writer.consume(value);
|
|
289
|
+
}
|
|
290
|
+
} else await writer.consume(await readJSON(stream));
|
|
291
|
+
const message = await writer.finish();
|
|
292
|
+
if (writer.streaming) res.end();
|
|
293
|
+
else {
|
|
294
|
+
res.writeHead(200, responseHeaders);
|
|
295
|
+
res.end(JSON.stringify(message));
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (!cancelled && !res.destroyed && !res.writableEnded)
|
|
299
|
+
tracker?.fail(upstreamFailure(null, error));
|
|
300
|
+
destroyUpstream();
|
|
301
|
+
if (cancelled || res.destroyed || res.writableEnded) return;
|
|
302
|
+
if (passthrough) { res.destroy(); return; }
|
|
303
|
+
if (res.headersSent) {
|
|
304
|
+
try { await writer.event("error", { error: errorBody(error).error }); res.end(); }
|
|
305
|
+
catch { res.destroy(); }
|
|
306
|
+
} else {
|
|
307
|
+
res.writeHead(error.status || 502, { "content-type": "application/json" });
|
|
308
|
+
res.end(JSON.stringify(errorBody(error)));
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
req.off("aborted", cancel);
|
|
312
|
+
res.off("close", cancel);
|
|
313
|
+
res.off("error", cancel);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = { bridgeMessages };
|