@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.
@@ -0,0 +1,442 @@
1
+ "use strict";
2
+
3
+ function invalid(message) {
4
+ throw Object.assign(new Error(message), {
5
+ status: 400,
6
+ code: "invalid_request_error",
7
+ });
8
+ }
9
+
10
+ function object(value, context) {
11
+ if (!value || typeof value !== "object" || Array.isArray(value))
12
+ invalid(`${context} must be an object`);
13
+ }
14
+
15
+ function fields(value, allowed, context) {
16
+ object(value, context);
17
+ for (const key of Object.keys(value)) {
18
+ if (!allowed.includes(key))
19
+ invalid(`${context}.${key} is not supported by the Messages-to-Responses bridge; remove it or use an Anthropic model`);
20
+ }
21
+ }
22
+
23
+ function text(value, context, nonempty = false) {
24
+ if (typeof value !== "string" || (nonempty && !value))
25
+ invalid(`${context} must be ${nonempty ? "a nonempty string" : "a string"}`);
26
+ }
27
+
28
+ function boolean(value, context) {
29
+ if (value != null && typeof value !== "boolean")
30
+ invalid(`${context} must be a boolean`);
31
+ }
32
+
33
+ function cacheControl(value, context) {
34
+ if (value == null) return;
35
+ fields(value, ["type", "ttl"], context);
36
+ if (value.type !== "ephemeral" ||
37
+ (value.ttl != null && !["5m", "1h"].includes(value.ttl)))
38
+ invalid(`${context} requires ephemeral caching with a 5m or 1h TTL`);
39
+ // Claude cache breakpoints/TTLs are provider billing hints, not Responses
40
+ // cache keys. Let the upstream manage caching rather than inventing a key.
41
+ }
42
+
43
+ function toolName(value, context) {
44
+ text(value, context, true);
45
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(value))
46
+ invalid(`${context} must use 1-64 letters, digits, underscores or hyphens for Responses; rename the client tool`);
47
+ }
48
+
49
+ function json(value, context) {
50
+ try {
51
+ const result = JSON.stringify(value);
52
+ if (result !== undefined) return result;
53
+ } catch {}
54
+ invalid(`${context} must contain JSON-serializable data`);
55
+ }
56
+
57
+ function url(value, context) {
58
+ text(value, context, true);
59
+ try {
60
+ const parsed = new URL(value);
61
+ if (["https:", "http:"].includes(parsed.protocol)) return value;
62
+ } catch {}
63
+ invalid(`${context} must be an absolute HTTP or HTTPS URL`);
64
+ }
65
+
66
+ function citations(value, context, configuration = false) {
67
+ if (value == null) return;
68
+ if (configuration) {
69
+ fields(value, ["enabled"], context);
70
+ boolean(value.enabled, `${context}.enabled`);
71
+ if (value.enabled !== true) return;
72
+ } else if (Array.isArray(value) && !value.length) return;
73
+ invalid(`${context} cannot retain Anthropic citation indexes in Responses; disable citations or supply plain text without citation annotations`);
74
+ }
75
+
76
+ function directCaller(value, context) {
77
+ if (value == null) return;
78
+ fields(value, ["type"], context);
79
+ if (value.type !== "direct")
80
+ invalid(`${context} requires a direct client tool call; server-side code execution is not supported`);
81
+ }
82
+
83
+ function convertImage(block, context) {
84
+ fields(block, ["type", "source", "cache_control", "transformations"], context);
85
+ cacheControl(block.cache_control, `${context}.cache_control`);
86
+ if (block.transformations != null) {
87
+ fields(block.transformations, ["oversized_image"], `${context}.transformations`);
88
+ if (block.transformations.oversized_image != null &&
89
+ block.transformations.oversized_image !== "downsize")
90
+ invalid(`${context}.transformations cannot enforce Anthropic image-size limits; resize the image before sending it`);
91
+ }
92
+ const source = block.source;
93
+ object(source, `${context}.source`);
94
+ if (source.type === "base64") {
95
+ fields(source, ["type", "data", "media_type"], `${context}.source`);
96
+ if (!["image/jpeg", "image/png", "image/gif", "image/webp"].includes(source.media_type))
97
+ invalid(`${context}.source.media_type must be a supported image type`);
98
+ text(source.data, `${context}.source.data`, true);
99
+ return { type: "input_image", image_url: `data:${source.media_type};base64,${source.data}`, detail: "auto" };
100
+ }
101
+ if (source.type === "url") {
102
+ fields(source, ["type", "url"], `${context}.source`);
103
+ return { type: "input_image", image_url: url(source.url, `${context}.source.url`), detail: "auto" };
104
+ }
105
+ invalid(`${context}.source type is not supported; Anthropic file IDs cannot be resolved by Responses, so provide a base64 image or URL`);
106
+ }
107
+
108
+ function convertDocument(block, context) {
109
+ fields(block, ["type", "source", "cache_control", "citations", "title", "context"], context);
110
+ cacheControl(block.cache_control, `${context}.cache_control`);
111
+ citations(block.citations, `${context}.citations`, true);
112
+ const parts = [];
113
+ const metadata = {};
114
+ for (const key of ["title", "context"]) {
115
+ if (block[key] != null) {
116
+ text(block[key], `${context}.${key}`);
117
+ metadata[key] = block[key];
118
+ }
119
+ }
120
+ if (Object.keys(metadata).length)
121
+ parts.push({ type: "input_text", text: `Document metadata: ${JSON.stringify(metadata)}` });
122
+ const source = block.source;
123
+ object(source, `${context}.source`);
124
+ if (source.type === "base64") {
125
+ fields(source, ["type", "data", "media_type"], `${context}.source`);
126
+ if (source.media_type !== "application/pdf")
127
+ invalid(`${context}.source.media_type must be application/pdf for a base64 document`);
128
+ text(source.data, `${context}.source.data`, true);
129
+ parts.push({
130
+ type: "input_file",
131
+ filename: block.title && /\.pdf$/i.test(block.title) ? block.title : `${block.title || "document"}.pdf`,
132
+ file_data: `data:application/pdf;base64,${source.data}`,
133
+ });
134
+ } else if (source.type === "url") {
135
+ fields(source, ["type", "url"], `${context}.source`);
136
+ parts.push({ type: "input_file", file_url: url(source.url, `${context}.source.url`) });
137
+ } else if (source.type === "text") {
138
+ fields(source, ["type", "data", "media_type"], `${context}.source`);
139
+ if (source.media_type !== "text/plain")
140
+ invalid(`${context}.source.media_type must be text/plain for a text document`);
141
+ text(source.data, `${context}.source.data`);
142
+ parts.push({ type: "input_text", text: source.data });
143
+ } else if (source.type === "content") {
144
+ fields(source, ["type", "content"], `${context}.source`);
145
+ if (typeof source.content === "string")
146
+ parts.push({ type: "input_text", text: source.content });
147
+ else {
148
+ if (!Array.isArray(source.content))
149
+ invalid(`${context}.source.content must be text or an array of text/image blocks`);
150
+ for (const [index, part] of source.content.entries()) {
151
+ const location = `${context}.source.content[${index}]`;
152
+ object(part, location);
153
+ if (!["text", "image"].includes(part.type))
154
+ invalid(`${location} must be a text or image block`);
155
+ parts.push(...convertContent(part, location));
156
+ }
157
+ }
158
+ } else {
159
+ invalid(`${context}.source type is not supported; Anthropic file IDs cannot be resolved by Responses, so provide document data or a URL`);
160
+ }
161
+ return parts;
162
+ }
163
+
164
+ function convertContent(block, context, tools) {
165
+ // This helper converts user/system input and tool-result content. Assistant
166
+ // history is converted to output blocks when its message is flushed below.
167
+ object(block, context);
168
+ if (block.type === "text") {
169
+ fields(block, ["type", "text", "cache_control", "citations"], context);
170
+ text(block.text, `${context}.text`);
171
+ cacheControl(block.cache_control, `${context}.cache_control`);
172
+ citations(block.citations, `${context}.citations`);
173
+ return [{ type: "input_text", text: block.text }];
174
+ }
175
+ if (block.type === "image") return [convertImage(block, context)];
176
+ if (block.type === "document") return convertDocument(block, context);
177
+ if (block.type === "tool_reference" && tools) {
178
+ fields(block, ["type", "tool_name", "cache_control"], context);
179
+ cacheControl(block.cache_control, `${context}.cache_control`);
180
+ toolName(block.tool_name, `${context}.tool_name`);
181
+ if (!tools.has(block.tool_name))
182
+ invalid(`${context} references a tool without a supplied definition; include it in tools`);
183
+ // Deferred client tools are advertised eagerly, so this reference is an
184
+ // ordinary discovery result rather than an unsupported hosted search item.
185
+ return [{ type: "input_text", text: `Available client tool: ${block.tool_name}` }];
186
+ }
187
+ invalid(`${context} has unsupported content type ${block.type || "(missing)"}; supply text, images, documents or direct client tool results instead of server-tool content`);
188
+ }
189
+
190
+ function convertTools(definitions) {
191
+ if (definitions == null) return [];
192
+ if (!Array.isArray(definitions)) invalid("tools must be an array");
193
+ const names = new Set();
194
+ return definitions.map((tool, index) => {
195
+ const context = `tools[${index}]`;
196
+ fields(tool, ["type", "name", "description", "input_schema", "cache_control", "defer_loading", "allowed_callers", "input_examples", "strict", "eager_input_streaming"], context);
197
+ if (tool.type != null && tool.type !== "custom")
198
+ invalid(`${context} uses a server/built-in tool type; supply a client function with input_schema instead`);
199
+ toolName(tool.name, `${context}.name`);
200
+ if (names.has(tool.name)) invalid(`Duplicate tool definition: ${tool.name}`);
201
+ names.add(tool.name);
202
+ object(tool.input_schema, `${context}.input_schema`);
203
+ if (tool.input_schema.type !== "object")
204
+ invalid(`${context}.input_schema must describe an object`);
205
+ cacheControl(tool.cache_control, `${context}.cache_control`);
206
+ boolean(tool.defer_loading, `${context}.defer_loading`);
207
+ boolean(tool.eager_input_streaming, `${context}.eager_input_streaming`);
208
+ boolean(tool.strict, `${context}.strict`);
209
+ if (tool.strict === true)
210
+ invalid(`${context}.strict cannot guarantee Anthropic schema enforcement; remove strict or use an Anthropic model`);
211
+ if (tool.allowed_callers != null &&
212
+ (!Array.isArray(tool.allowed_callers) || !tool.allowed_callers.length ||
213
+ tool.allowed_callers.some((caller) => caller !== "direct")))
214
+ invalid(`${context}.allowed_callers must allow only direct client calls; server-side execution is not supported`);
215
+ const converted = { type: "function", name: tool.name, parameters: tool.input_schema, strict: false };
216
+ if (tool.description != null) {
217
+ text(tool.description, `${context}.description`);
218
+ converted.description = tool.description;
219
+ }
220
+ if (tool.input_examples != null) {
221
+ if (!Array.isArray(tool.input_examples)) invalid(`${context}.input_examples must be an array`);
222
+ for (const example of tool.input_examples) object(example, `${context}.input_examples entry`);
223
+ if (tool.input_examples.length)
224
+ converted.description = `${converted.description ? `${converted.description}\n\n` : ""}Example inputs:\n${json(tool.input_examples, `${context}.input_examples`)}`;
225
+ }
226
+ // Responses has no Claude deferral/cache/streaming hints. Advertise every
227
+ // definition eagerly; the Responses transport determines streaming cadence.
228
+ return converted;
229
+ });
230
+ }
231
+
232
+ function convertReasoning(payload, request) {
233
+ const config = payload.output_config;
234
+ if (config != null) fields(config, ["effort", "format"], "output_config");
235
+ let effort = config?.effort;
236
+ if (effort != null && !["low", "medium", "high", "xhigh", "max"].includes(effort))
237
+ invalid("output_config.effort must be low, medium, high, xhigh or max");
238
+ // Fixed compatibility policy: Claude's max effort maps to Responses xhigh.
239
+ // The caller logs this mapping without adding non-API fields to the request.
240
+ if (effort === "max") effort = "xhigh";
241
+ const thinking = payload.thinking;
242
+ if (thinking != null) {
243
+ fields(thinking, ["type", "budget_tokens", "display"], "thinking");
244
+ if (!["adaptive", "enabled", "disabled"].includes(thinking.type))
245
+ invalid("thinking.type must be adaptive, enabled or disabled");
246
+ if (thinking.display != null && !["summarized", "omitted"].includes(thinking.display))
247
+ invalid("thinking.display must be summarized or omitted");
248
+ if (thinking.type !== "enabled" && thinking.budget_tokens != null)
249
+ invalid("thinking.budget_tokens requires thinking.type enabled");
250
+ if (thinking.type === "disabled") {
251
+ if (effort != null) invalid("thinking disabled conflicts with output_config.effort; remove the effort or enable thinking");
252
+ if (thinking.display != null) invalid("thinking.display requires enabled or adaptive thinking");
253
+ request.reasoning = { effort: "none" };
254
+ } else {
255
+ if (thinking.type === "enabled" &&
256
+ (!Number.isSafeInteger(thinking.budget_tokens) || thinking.budget_tokens < 1024 ||
257
+ (payload.max_tokens != null && thinking.budget_tokens >= payload.max_tokens)))
258
+ invalid("thinking.budget_tokens must be an integer of at least 1024 and less than max_tokens");
259
+ // Responses has effort levels, not a separate thinking token cap. The
260
+ // total cap remains max_output_tokens; explicit effort always wins.
261
+ request.reasoning = {
262
+ effort: effort ?? (thinking.type === "enabled" && thinking.budget_tokens < 8192 ? "medium" : "high"),
263
+ summary: "auto",
264
+ };
265
+ }
266
+ } else if (effort != null) request.reasoning = { effort, summary: "auto" };
267
+ if (config?.format != null) {
268
+ fields(config.format, ["type", "schema"], "output_config.format");
269
+ if (config.format.type !== "json_schema")
270
+ invalid("output_config.format must use json_schema");
271
+ object(config.format.schema, "output_config.format.schema");
272
+ request.text = { format: { type: "json_schema", name: "response", schema: config.format.schema, strict: true } };
273
+ }
274
+ }
275
+
276
+ function contextManagement(value) {
277
+ if (value == null) return;
278
+ fields(value, ["edits"], "context_management");
279
+ if (!Array.isArray(value.edits)) invalid("context_management.edits must be an array");
280
+ for (const [index, edit] of value.edits.entries()) {
281
+ const context = `context_management.edits[${index}]`;
282
+ fields(edit, ["type", "keep"], context);
283
+ if (edit.type !== "clear_thinking_20251015")
284
+ invalid(`${context} cannot be applied by Responses; perform context edits in the client before sending the request`);
285
+ if (edit.keep != null && edit.keep !== "all")
286
+ invalid(`${context}.keep must be all; perform finite-turn thinking clearing in the client before sending the request`);
287
+ // All public prior thinking is retained below. Provider-specific opaque
288
+ // signatures are never replayable, so no Responses edit is necessary.
289
+ }
290
+ }
291
+
292
+ function buildMessagesRequest(payload) {
293
+ fields(payload, ["model", "messages", "system", "stream", "max_tokens", "tools", "tool_choice", "thinking", "output_config", "context_management", "metadata", "cache_control", "temperature", "top_p", "top_k", "stop_sequences", "service_tier"], "request");
294
+ text(payload.model, "model", true);
295
+ if (!Array.isArray(payload.messages) || !payload.messages.length)
296
+ invalid("messages must be a nonempty array");
297
+ boolean(payload.stream, "stream");
298
+ if (payload.max_tokens != null && (!Number.isSafeInteger(payload.max_tokens) || payload.max_tokens <= 0))
299
+ invalid("max_tokens must be a positive integer; Responses does not support cache-only max_tokens=0 requests");
300
+ for (const [key, defaults] of [["temperature", [1]], ["top_p", [1]], ["top_k", [0, -1]]]) {
301
+ if (payload[key] != null && !defaults.includes(payload[key]))
302
+ invalid(`${key} is not supported with GPT reasoning; omit it or use its default value instead of disabling reasoning`);
303
+ }
304
+ if (payload.stop_sequences != null &&
305
+ (!Array.isArray(payload.stop_sequences) || payload.stop_sequences.length))
306
+ invalid("stop_sequences is not supported by Responses; remove custom stop sequences");
307
+ if (payload.metadata != null) object(payload.metadata, "metadata");
308
+ // Client metadata is deliberately not forwarded as OpenAI user identity.
309
+ cacheControl(payload.cache_control, "cache_control");
310
+ contextManagement(payload.context_management);
311
+ const tools = convertTools(payload.tools);
312
+ const toolNames = new Set(tools.map((tool) => tool.name));
313
+ const request = { model: payload.model, input: [], stream: payload.stream === true, store: false };
314
+ if (payload.max_tokens != null) request.max_output_tokens = payload.max_tokens;
315
+ if (payload.tools != null) request.tools = tools;
316
+ convertReasoning(payload, request);
317
+ if (payload.service_tier != null) {
318
+ if (!["auto", "standard_only"].includes(payload.service_tier))
319
+ invalid("service_tier must be auto or standard_only");
320
+ request.service_tier = payload.service_tier === "standard_only" ? "default" : "auto";
321
+ }
322
+ if (payload.tool_choice != null) {
323
+ const choice = payload.tool_choice;
324
+ fields(choice, ["type", "name", "disable_parallel_tool_use"], "tool_choice");
325
+ if (!["auto", "any", "tool", "none"].includes(choice.type))
326
+ invalid("tool_choice.type must be auto, any, tool or none");
327
+ boolean(choice.disable_parallel_tool_use, "tool_choice.disable_parallel_tool_use");
328
+ if (choice.disable_parallel_tool_use != null)
329
+ request.parallel_tool_calls = !choice.disable_parallel_tool_use;
330
+ if (choice.type === "tool") {
331
+ toolName(choice.name, "tool_choice.name");
332
+ if (!toolNames.has(choice.name)) invalid("tool_choice.name must identify a supplied client tool");
333
+ request.tool_choice = { type: "function", name: choice.name };
334
+ } else {
335
+ if (choice.name != null) invalid("tool_choice.name requires tool_choice.type tool");
336
+ if (choice.type === "any" && !tools.length) invalid("tool_choice any requires at least one client tool");
337
+ request.tool_choice = choice.type === "any" ? "required" : choice.type;
338
+ }
339
+ }
340
+ if (payload.system != null) {
341
+ let content;
342
+ if (typeof payload.system === "string") content = payload.system;
343
+ else {
344
+ if (!Array.isArray(payload.system)) invalid("system must be text or an array of text blocks");
345
+ content = payload.system.flatMap((block, index) => {
346
+ if (!block || block.type !== "text") invalid(`system[${index}] must be a text block`);
347
+ return convertContent(block, `system[${index}]`);
348
+ });
349
+ }
350
+ if (content.length) request.input.push({ role: "system", content });
351
+ }
352
+ const calls = new Set();
353
+ const answered = new Set();
354
+ for (const [messageIndex, message] of payload.messages.entries()) {
355
+ const context = `messages[${messageIndex}]`;
356
+ fields(message, ["role", "content"], context);
357
+ // Claude Code gateway mode also emits environment messages with role system.
358
+ if (!["user", "assistant", "system"].includes(message.role)) invalid(`${context}.role must be user, assistant or system`);
359
+ if (typeof message.content === "string") {
360
+ request.input.push({ role: message.role, content: message.content });
361
+ continue;
362
+ }
363
+ if (!Array.isArray(message.content) || !message.content.length)
364
+ invalid(`${context}.content must be text or a nonempty content array`);
365
+ let content = [];
366
+ const flush = () => {
367
+ if (content.length) {
368
+ if (message.role === "assistant") {
369
+ for (const part of content) {
370
+ if (part.type !== "input_text")
371
+ invalid(`${context}: Responses assistant history supports text only; images and documents must be user content or tool results`);
372
+ part.type = "output_text";
373
+ part.annotations = [];
374
+ }
375
+ }
376
+ request.input.push({ role: message.role, content });
377
+ content = [];
378
+ }
379
+ };
380
+ for (const [blockIndex, block] of message.content.entries()) {
381
+ const location = `${context}.content[${blockIndex}]`;
382
+ object(block, location);
383
+ if (block.type === "tool_use") {
384
+ if (message.role !== "assistant") invalid(`${location}: tool_use requires the assistant role`);
385
+ fields(block, ["type", "id", "name", "input", "cache_control", "caller"], location);
386
+ text(block.id, `${location}.id`, true);
387
+ toolName(block.name, `${location}.name`);
388
+ object(block.input, `${location}.input`);
389
+ if (calls.has(block.id)) invalid(`${location}.id duplicates a preceding tool call`);
390
+ cacheControl(block.cache_control, `${location}.cache_control`);
391
+ directCaller(block.caller, `${location}.caller`);
392
+ calls.add(block.id);
393
+ flush();
394
+ request.input.push({ type: "function_call", call_id: block.id, name: block.name, arguments: json(block.input, `${location}.input`) });
395
+ } else if (block.type === "tool_result") {
396
+ if (message.role !== "user") invalid(`${location}: tool_result requires the user role`);
397
+ fields(block, ["type", "tool_use_id", "content", "is_error", "cache_control"], location);
398
+ text(block.tool_use_id, `${location}.tool_use_id`, true);
399
+ if (!calls.has(block.tool_use_id) || answered.has(block.tool_use_id))
400
+ invalid(`${location} requires its preceding, unanswered tool_use in messages`);
401
+ boolean(block.is_error, `${location}.is_error`);
402
+ cacheControl(block.cache_control, `${location}.cache_control`);
403
+ let output = block.content ?? "";
404
+ if (typeof output !== "string") {
405
+ if (!Array.isArray(output)) invalid(`${location}.content must be text or a content array`);
406
+ output = output.flatMap((part, index) => convertContent(part, `${location}.content[${index}]`, toolNames));
407
+ }
408
+ // Responses has no is_error bit; preserve the tool failure as visible
409
+ // output without losing any original text/image/file ordering.
410
+ if (block.is_error === true) {
411
+ const marker = "[Client tool execution failed]";
412
+ output = typeof output === "string" ? `${marker}\n${output}` : [{ type: "input_text", text: marker }, ...output];
413
+ }
414
+ answered.add(block.tool_use_id);
415
+ flush();
416
+ request.input.push({ type: "function_call_output", call_id: block.tool_use_id, output });
417
+ } else if (block.type === "thinking" || block.type === "redacted_thinking") {
418
+ if (message.role !== "assistant") invalid(`${location}: thinking requires the assistant role`);
419
+ if (block.type === "thinking") {
420
+ fields(block, ["type", "thinking", "signature"], location);
421
+ text(block.thinking, `${location}.thinking`);
422
+ if (block.signature != null) text(block.signature, `${location}.signature`);
423
+ // Native reasoning input requires a backend-issued ID. Do not invent
424
+ // rs IDs or encrypted state: retain only the supplied public summary
425
+ // as labelled assistant text, in its original position.
426
+ if (block.thinking)
427
+ content.push({ type: "input_text", text: `Prior public reasoning summary:\n${block.thinking}` });
428
+ } else {
429
+ fields(block, ["type", "data"], location);
430
+ text(block.data, `${location}.data`);
431
+ // Anthropic's opaque redacted state cannot be decrypted or replayed
432
+ // by OpenAI and contains no public content to preserve.
433
+ }
434
+ } else content.push(...convertContent(block, location));
435
+ }
436
+ flush();
437
+ }
438
+ if (!request.input.length) invalid("messages must contain public content or a client tool call");
439
+ return request;
440
+ }
441
+
442
+ module.exports = { buildMessagesRequest };