@wacry/codeks-bridge 0.0.0-stage → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +40 -2
- package/catalog/README.md +63 -0
- package/catalog/claude-opus-5-5.json +194 -0
- package/catalog/claude-sonnet-5-5.json +194 -0
- package/catalog/deepseek-flash.json +203 -0
- package/catalog/gpt-5.6-sol.json +152 -0
- package/catalog/gpt-6-astra.json +184 -0
- package/catalog/gpt-6-luna.json +178 -0
- package/catalog/gpt-6-sol.json +182 -0
- package/catalog/gpt-6.1-sol.json +187 -0
- package/dist/adapter-options.d.ts +5 -0
- package/dist/adapter-options.js +41 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +42 -0
- package/dist/backends/anthropic-tools.d.ts +13 -0
- package/dist/backends/anthropic-tools.js +41 -0
- package/dist/backends/anthropic.d.ts +9 -0
- package/dist/backends/anthropic.js +356 -0
- package/dist/backends/chat.d.ts +16 -0
- package/dist/backends/chat.js +284 -0
- package/dist/backends/deepseek.d.ts +6 -0
- package/dist/backends/deepseek.js +37 -0
- package/dist/backends/responses.d.ts +5 -0
- package/dist/backends/responses.js +227 -0
- package/dist/carrier.d.ts +22 -0
- package/dist/carrier.js +61 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +40 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +64 -0
- package/dist/compaction-handoff.d.ts +13 -0
- package/dist/compaction-handoff.js +126 -0
- package/dist/compaction-history.d.ts +7 -0
- package/dist/compaction-history.js +20 -0
- package/dist/compaction.d.ts +20 -0
- package/dist/compaction.js +134 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +170 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +141 -0
- package/dist/headers.d.ts +6 -0
- package/dist/headers.js +39 -0
- package/dist/history.d.ts +7 -0
- package/dist/history.js +85 -0
- package/dist/ids.d.ts +1 -0
- package/dist/ids.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/inspect/capture.d.ts +119 -0
- package/dist/inspect/capture.js +370 -0
- package/dist/inspect/http.d.ts +7 -0
- package/dist/inspect/http.js +149 -0
- package/dist/inspect/view.d.ts +239 -0
- package/dist/inspect/view.js +388 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +16 -0
- package/dist/profiles.d.ts +6 -0
- package/dist/profiles.js +63 -0
- package/dist/prompts.d.ts +5 -0
- package/dist/prompts.js +118 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +232 -0
- package/dist/sse.d.ts +12 -0
- package/dist/sse.js +93 -0
- package/dist/tool-history.d.ts +4 -0
- package/dist/tool-history.js +73 -0
- package/dist/tools.d.ts +35 -0
- package/dist/tools.js +193 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +3 -0
- package/dist/upstream.js +89 -0
- package/dist/writer.d.ts +95 -0
- package/dist/writer.js +430 -0
- package/docs/backends-anthropic.md +88 -0
- package/docs/backends-deepseek-chat.md +75 -0
- package/docs/backends-responses.md +33 -0
- package/docs/compaction.md +180 -0
- package/docs/config.md +271 -0
- package/docs/design.md +97 -0
- package/docs/inspect.md +148 -0
- package/docs/reasoning.md +72 -0
- package/docs/releasing.md +50 -0
- package/docs/responses-stream.md +90 -0
- package/docs/testing.md +177 -0
- package/docs/tools.md +68 -0
- package/examples/config.json +35 -0
- package/examples/gpt-claude.json +27 -0
- package/package.json +46 -4
- package/ui/api.js +64 -0
- package/ui/app.css +379 -0
- package/ui/app.js +489 -0
- package/ui/detail.js +713 -0
- package/ui/diff.js +281 -0
- package/ui/dom.js +258 -0
- package/ui/events.js +376 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +58 -0
- package/ui/labels.js +106 -0
- package/ui/list.js +180 -0
- package/ui/structure.js +285 -0
- package/ui/tree.js +158 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { AdapterId, ClaudeMessagesOptions, ResolvedModel } from "./types.ts";
|
|
2
|
+
/** Validate bridge-owned settings; native options stay open to upstream extensions. */
|
|
3
|
+
export declare function validateAdapterConfig(value: unknown, location: string, adapter?: AdapterId): void;
|
|
4
|
+
/** Model leaves override connection leaves; neither is part of state provenance. */
|
|
5
|
+
export declare function resolveClaudeOptions(route: Pick<ResolvedModel, "upstream" | "config">): ClaudeMessagesOptions;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BridgeError } from "./errors.js";
|
|
2
|
+
function object(value) {
|
|
3
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/** Validate bridge-owned settings; native options stay open to upstream extensions. */
|
|
6
|
+
export function validateAdapterConfig(value, location, adapter) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return;
|
|
9
|
+
function fail(message) {
|
|
10
|
+
throw new BridgeError(`Invalid configuration: ${location}.adapters: ${message}`);
|
|
11
|
+
}
|
|
12
|
+
if (!object(value))
|
|
13
|
+
fail("must be an object");
|
|
14
|
+
for (const [name, settings] of Object.entries(value)) {
|
|
15
|
+
if (name !== "claude-messages")
|
|
16
|
+
fail(`unknown adapter settings ${name}`);
|
|
17
|
+
if (adapter !== undefined && name !== adapter)
|
|
18
|
+
fail(`${name} does not match the model adapter ${adapter}`);
|
|
19
|
+
if (!object(settings))
|
|
20
|
+
fail(`${name} must be an object`);
|
|
21
|
+
for (const field of Object.keys(settings)) {
|
|
22
|
+
if (field !== "tool_updates" && field !== "thinking_mismatch")
|
|
23
|
+
fail(`unknown ${name} setting ${field}`);
|
|
24
|
+
}
|
|
25
|
+
if (settings.tool_updates !== undefined && !["inline", "top_level"].includes(settings.tool_updates)) {
|
|
26
|
+
fail(`${name}.tool_updates must be inline or top_level`);
|
|
27
|
+
}
|
|
28
|
+
if (settings.thinking_mismatch !== undefined && !["drop_block", "error", "upstream_default"].includes(settings.thinking_mismatch)) {
|
|
29
|
+
fail(`${name}.thinking_mismatch must be drop_block, error, or upstream_default`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Model leaves override connection leaves; neither is part of state provenance. */
|
|
34
|
+
export function resolveClaudeOptions(route) {
|
|
35
|
+
const upstream = route.upstream.adapters?.["claude-messages"];
|
|
36
|
+
const model = route.config.adapters?.["claude-messages"];
|
|
37
|
+
return {
|
|
38
|
+
tool_updates: model?.tool_updates ?? upstream?.tool_updates ?? "inline",
|
|
39
|
+
thinking_mismatch: model?.thinking_mismatch ?? upstream?.thinking_mismatch ?? "drop_block",
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { BridgeError } from "./errors.js";
|
|
4
|
+
import { forwardRequestHeaders } from "./headers.js";
|
|
5
|
+
const execute = promisify(execFile);
|
|
6
|
+
export async function upstreamHeaders(config, api, signal, clientHeaders) {
|
|
7
|
+
const headers = forwardRequestHeaders(clientHeaders, config.auth_header);
|
|
8
|
+
for (const [name, value] of Object.entries(config.headers ?? {}))
|
|
9
|
+
headers.set(name, value);
|
|
10
|
+
if (!headers.has("content-type"))
|
|
11
|
+
headers.set("content-type", "application/json");
|
|
12
|
+
if (api === "anthropic" && !headers.has("anthropic-version"))
|
|
13
|
+
headers.set("anthropic-version", "2023-06-01");
|
|
14
|
+
let credential;
|
|
15
|
+
if (config.api_key_env) {
|
|
16
|
+
credential = process.env[config.api_key_env]?.trim();
|
|
17
|
+
if (!credential)
|
|
18
|
+
throw new BridgeError(`Missing credential environment variable: ${config.api_key_env}`);
|
|
19
|
+
}
|
|
20
|
+
else if (config.api_key_command) {
|
|
21
|
+
const [command, ...args] = config.api_key_command;
|
|
22
|
+
try {
|
|
23
|
+
const result = await execute(command, args, { encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024, signal });
|
|
24
|
+
credential = result.stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
if (signal.aborted)
|
|
28
|
+
throw signal.reason;
|
|
29
|
+
// execFile errors include stdout/stderr. Never forward those to a client.
|
|
30
|
+
throw new BridgeError("Credential command failed");
|
|
31
|
+
}
|
|
32
|
+
if (!credential)
|
|
33
|
+
throw new BridgeError("Credential command returned an empty credential");
|
|
34
|
+
}
|
|
35
|
+
if (credential) {
|
|
36
|
+
if (/[\r\n]/.test(credential))
|
|
37
|
+
throw new BridgeError("Credential must be a single line");
|
|
38
|
+
const header = config.auth_header ?? (api === "anthropic" ? "x-api-key" : "authorization");
|
|
39
|
+
headers.set(header, header.toLowerCase() === "authorization" ? `Bearer ${credential}` : credential);
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ToolSet } from "../tools.ts";
|
|
2
|
+
import type { ResponsesRequest } from "../types.ts";
|
|
3
|
+
export interface AnthropicToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
input_schema: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
/** Keep later definitions at their original conversation boundary. */
|
|
9
|
+
export declare function buildAnthropicTools(request: ResponsesRequest, mode: "inline" | "top_level"): {
|
|
10
|
+
tools: ToolSet;
|
|
11
|
+
definitions: AnthropicToolDefinition[];
|
|
12
|
+
input: ResponsesRequest["input"];
|
|
13
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BridgeError } from "../errors.js";
|
|
2
|
+
import { buildToolSet, normalizeToolDeclarations } from "../tools.js";
|
|
3
|
+
function definition(tool) {
|
|
4
|
+
return { name: tool.flatName, description: tool.description, input_schema: tool.parameters };
|
|
5
|
+
}
|
|
6
|
+
/** Keep later definitions at their original conversation boundary. */
|
|
7
|
+
export function buildAnthropicTools(request, mode) {
|
|
8
|
+
if (mode === "top_level") {
|
|
9
|
+
const normalized = normalizeToolDeclarations(request);
|
|
10
|
+
const tools = buildToolSet(normalized.tools);
|
|
11
|
+
return { tools, definitions: tools.entries.map(definition), input: normalized.input };
|
|
12
|
+
}
|
|
13
|
+
const tools = buildToolSet(request.tools);
|
|
14
|
+
const initial = new Map(tools.entries.map((tool) => [tool.flatName, definition(tool)]));
|
|
15
|
+
if (!Array.isArray(request.input)) {
|
|
16
|
+
return { tools, definitions: [...initial.values()], input: request.input };
|
|
17
|
+
}
|
|
18
|
+
const input = [];
|
|
19
|
+
let prefix = true;
|
|
20
|
+
for (const item of request.input) {
|
|
21
|
+
if (item?.type === "additional_tools") {
|
|
22
|
+
if (!Array.isArray(item.tools))
|
|
23
|
+
throw new BridgeError("additional_tools requires a tools array.");
|
|
24
|
+
const definitions = tools.add(item.tools).map(definition);
|
|
25
|
+
if (prefix) {
|
|
26
|
+
for (const tool of definitions)
|
|
27
|
+
initial.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
else if (definitions.length) {
|
|
30
|
+
input.push({ ...item, tools: definitions });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
input.push(item);
|
|
35
|
+
if (!item || !["system", "developer"].includes(item.role) ||
|
|
36
|
+
(item.type !== undefined && item.type !== "message"))
|
|
37
|
+
prefix = false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { tools, definitions: [...initial.values()], input };
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolSet } from "../tools.ts";
|
|
2
|
+
import type { Backend, BackendContext } from "../types.ts";
|
|
3
|
+
/** Convert only request-owned copies; stored history remains unchanged. */
|
|
4
|
+
export declare function buildAnthropicRequest(context: BackendContext): {
|
|
5
|
+
body: Record<string, any>;
|
|
6
|
+
tools: ToolSet;
|
|
7
|
+
betas: string[];
|
|
8
|
+
};
|
|
9
|
+
export declare const runAnthropic: Backend;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { encodeReasoning } from "../carrier.js";
|
|
2
|
+
import { resolveClaudeOptions } from "../adapter-options.js";
|
|
3
|
+
import { BridgeError, upstreamError } from "../errors.js";
|
|
4
|
+
import { prepareHistory } from "../history.js";
|
|
5
|
+
import { reasoningEffort, reasoningSummaryRequested } from "../profiles.js";
|
|
6
|
+
import { parseJsonSse } from "../sse.js";
|
|
7
|
+
import { safeCallId } from "../tools.js";
|
|
8
|
+
import { upstreamFetch } from "../upstream.js";
|
|
9
|
+
import { buildAnthropicTools } from "./anthropic-tools.js";
|
|
10
|
+
const THINKING_BINDING_BETA = "thinking-binding-controls-2026-08-01";
|
|
11
|
+
const INLINE_TOOLS_BETA = "inline-tools-2026-09-15";
|
|
12
|
+
function contentBlocks(content) {
|
|
13
|
+
if (typeof content === "string")
|
|
14
|
+
return content ? [{ type: "text", text: content }] : [];
|
|
15
|
+
if (!Array.isArray(content))
|
|
16
|
+
throw new BridgeError("Messages content must be text or an array.");
|
|
17
|
+
return content.flatMap((part) => {
|
|
18
|
+
if (["input_text", "output_text", "text"].includes(part.type)) {
|
|
19
|
+
return part.text ? [{ type: "text", text: part.text }] : [];
|
|
20
|
+
}
|
|
21
|
+
if (part.type === "input_image" || part.type === "image_url") {
|
|
22
|
+
const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
|
|
23
|
+
if (typeof url !== "string")
|
|
24
|
+
throw new BridgeError("An image needs an image_url.");
|
|
25
|
+
const data = /^data:(image\/(?:jpeg|png|gif|webp));base64,([\s\S]+)$/.exec(url);
|
|
26
|
+
if (data)
|
|
27
|
+
return [{ type: "image", source: { type: "base64", media_type: data[1], data: data[2] } }];
|
|
28
|
+
if (/^https?:\/\//.test(url))
|
|
29
|
+
return [{ type: "image", source: { type: "url", url } }];
|
|
30
|
+
throw new BridgeError("Messages images must use an HTTP URL or a JPEG, PNG, GIF, or WebP base64 data URL.");
|
|
31
|
+
}
|
|
32
|
+
if (part.type === "refusal")
|
|
33
|
+
return [{ type: "text", text: String(part.refusal ?? "") }];
|
|
34
|
+
throw new BridgeError(`Unsupported Messages content type: ${String(part.type)}.`);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function parseArguments(value) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new BridgeError("A tool call contains invalid JSON arguments.");
|
|
44
|
+
}
|
|
45
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
46
|
+
throw new BridgeError("Messages tool arguments must be an object.");
|
|
47
|
+
}
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
function toolChoice(value, tools) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (value === "auto")
|
|
54
|
+
return { type: "auto" };
|
|
55
|
+
if (value === "none")
|
|
56
|
+
return { type: "none" };
|
|
57
|
+
if (value === "required")
|
|
58
|
+
return { type: "any" };
|
|
59
|
+
if (value && ["function", "custom"].includes(value.type) && typeof value.name === "string") {
|
|
60
|
+
const selected = tools.get(value.namespace, value.name);
|
|
61
|
+
if (!selected || selected.kind !== value.type) {
|
|
62
|
+
throw new BridgeError("The selected tool is not available on the Messages backend.");
|
|
63
|
+
}
|
|
64
|
+
return { type: "tool", name: selected.flatName };
|
|
65
|
+
}
|
|
66
|
+
throw new BridgeError("Unsupported Messages tool_choice.");
|
|
67
|
+
}
|
|
68
|
+
/** Convert only request-owned copies; stored history remains unchanged. */
|
|
69
|
+
export function buildAnthropicRequest(context) {
|
|
70
|
+
const { route } = context;
|
|
71
|
+
const request = context.request;
|
|
72
|
+
const settings = resolveClaudeOptions(route);
|
|
73
|
+
const { tools, definitions, input } = buildAnthropicTools(request, settings.tool_updates);
|
|
74
|
+
const system = request.instructions ? [{ type: "text", text: request.instructions }] : [];
|
|
75
|
+
const messages = [];
|
|
76
|
+
let additions = [];
|
|
77
|
+
let hasInlineTools = false;
|
|
78
|
+
const continueAssistant = () => {
|
|
79
|
+
const tail = messages.at(-1);
|
|
80
|
+
if (tail?.role === "assistant" && !tail.content.some(block => block.type === "tool_use")) {
|
|
81
|
+
messages.push({ role: "user", content: [{ type: "text", text: "Continue." }] });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const flushAdditions = () => {
|
|
85
|
+
if (!additions.length)
|
|
86
|
+
return;
|
|
87
|
+
// A persisted declaration also anchors this boundary on later replays.
|
|
88
|
+
continueAssistant();
|
|
89
|
+
if (messages.at(-1)?.role !== "user") {
|
|
90
|
+
throw new BridgeError("Inline tool declarations require a user or completed tool-result boundary.");
|
|
91
|
+
}
|
|
92
|
+
messages.push({ role: "system", content: additions });
|
|
93
|
+
additions = [];
|
|
94
|
+
hasInlineTools = true;
|
|
95
|
+
};
|
|
96
|
+
const append = (role, blocks) => {
|
|
97
|
+
if (!blocks.length)
|
|
98
|
+
return;
|
|
99
|
+
// Several reminders/results may share a user turn. Tool declarations take
|
|
100
|
+
// effect after that complete boundary, before the next assistant step.
|
|
101
|
+
if (role === "assistant")
|
|
102
|
+
flushAdditions();
|
|
103
|
+
const last = messages.at(-1);
|
|
104
|
+
if (last?.role === role)
|
|
105
|
+
last.content.push(...blocks);
|
|
106
|
+
else
|
|
107
|
+
messages.push({ role, content: blocks });
|
|
108
|
+
};
|
|
109
|
+
for (const item of prepareHistory(input, route)) {
|
|
110
|
+
if (item.type === "additional_tools") {
|
|
111
|
+
additions.push(...item.tools.map((definition) => ({
|
|
112
|
+
type: "tool_addition", tool: { type: "tool_definition", definition },
|
|
113
|
+
})));
|
|
114
|
+
}
|
|
115
|
+
else if (item.type === "bridge_reasoning") {
|
|
116
|
+
const block = item.state.data;
|
|
117
|
+
if (!block || !["thinking", "redacted_thinking"].includes(block.type)) {
|
|
118
|
+
throw new BridgeError("Invalid Messages reasoning carrier.");
|
|
119
|
+
}
|
|
120
|
+
append("assistant", [block]);
|
|
121
|
+
}
|
|
122
|
+
else if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
123
|
+
append("assistant", [{
|
|
124
|
+
type: "tool_use", id: safeCallId(item.call_id), name: tools.flatFor(item.namespace, item.name),
|
|
125
|
+
input: item.type === "custom_tool_call" ? { input: item.input } : parseArguments(item.arguments),
|
|
126
|
+
}]);
|
|
127
|
+
}
|
|
128
|
+
else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
129
|
+
const content = contentBlocks(item.output);
|
|
130
|
+
append("user", [{ type: "tool_result", tool_use_id: safeCallId(item.call_id), ...(content.length ? { content } : {}) }]);
|
|
131
|
+
}
|
|
132
|
+
else if (item.type === "message" || item.role) {
|
|
133
|
+
const blocks = contentBlocks(item.content);
|
|
134
|
+
if (item.role === "developer" || item.role === "system") {
|
|
135
|
+
if (blocks.some((block) => block.type !== "text"))
|
|
136
|
+
throw new BridgeError("System messages must contain text.");
|
|
137
|
+
if (!messages.length)
|
|
138
|
+
system.push(...blocks);
|
|
139
|
+
// Inline system roles have placement restrictions that arbitrary Responses
|
|
140
|
+
// history cannot satisfy. Keep reminders in order without rewriting a
|
|
141
|
+
// signed assistant prefix or moving new instructions to the beginning.
|
|
142
|
+
else
|
|
143
|
+
append("user", [{ type: "text", text: `<system-reminder>\n${blocks.map((block) => block.text).join("\n\n")}\n</system-reminder>` }]);
|
|
144
|
+
}
|
|
145
|
+
else if (item.role === "assistant" || item.role === "user") {
|
|
146
|
+
append(item.role, blocks);
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
throw new BridgeError(`Unsupported Messages role: ${String(item.role)}.`);
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
throw new BridgeError(`Unsupported Messages history item: ${String(item.type)}.`);
|
|
153
|
+
}
|
|
154
|
+
// A failed response can leave a complete thinking/text item in codeks history.
|
|
155
|
+
// Claude requires a user boundary to resume. Do not mask a missing tool result.
|
|
156
|
+
continueAssistant();
|
|
157
|
+
flushAdditions();
|
|
158
|
+
// Keep stable prefixes at 1h. The one-off compaction suffix uses 5m.
|
|
159
|
+
const cache = (block, ttl = "1h") => {
|
|
160
|
+
if (block)
|
|
161
|
+
block.cache_control = { type: "ephemeral", ttl };
|
|
162
|
+
};
|
|
163
|
+
cache(definitions.at(-1));
|
|
164
|
+
cache(system.at(-1));
|
|
165
|
+
const userMessages = messages.filter((message) => message.role === "user");
|
|
166
|
+
for (const message of userMessages.slice(-2)) {
|
|
167
|
+
cache(message.content.at(-1), context.compacting && message === userMessages.at(-1) ? "5m" : "1h");
|
|
168
|
+
}
|
|
169
|
+
const effort = reasoningEffort(route, request.reasoning?.effort);
|
|
170
|
+
const body = {
|
|
171
|
+
thinking: effort === "none" ? { type: "disabled" } : {
|
|
172
|
+
type: "adaptive", display: reasoningSummaryRequested(request) ? "summarized" : "omitted",
|
|
173
|
+
},
|
|
174
|
+
...(effort !== "none" ? { output_config: { effort } } : {}),
|
|
175
|
+
max_tokens: request.max_output_tokens ?? route.profile.max_output_tokens,
|
|
176
|
+
};
|
|
177
|
+
for (const name of ["temperature", "top_p"]) {
|
|
178
|
+
if (request[name] !== undefined)
|
|
179
|
+
body[name] = request[name];
|
|
180
|
+
}
|
|
181
|
+
let choice = toolChoice(request.tool_choice, tools);
|
|
182
|
+
if (tools.entries.length && typeof request.parallel_tool_calls === "boolean" && choice?.type !== "none") {
|
|
183
|
+
choice = { ...(choice ?? { type: "auto" }), disable_parallel_tool_use: !request.parallel_tool_calls };
|
|
184
|
+
}
|
|
185
|
+
if (choice)
|
|
186
|
+
body.tool_choice = choice;
|
|
187
|
+
const format = request.text?.format;
|
|
188
|
+
if (format !== undefined && format.type !== "text") {
|
|
189
|
+
if (format.type !== "json_schema")
|
|
190
|
+
throw new BridgeError(`Messages does not support text format ${String(format.type)}.`);
|
|
191
|
+
body.output_config = { ...body.output_config, format: { type: "json_schema", schema: format.schema } };
|
|
192
|
+
}
|
|
193
|
+
Object.assign(body, route.upstream.options, route.config.options, {
|
|
194
|
+
model: route.config.model, messages, stream: true,
|
|
195
|
+
});
|
|
196
|
+
// Native options remain authoritative. Modes such as between_tools do not
|
|
197
|
+
// support binding controls; do not change the caller's chosen thinking mode.
|
|
198
|
+
if (["adaptive", "enabled"].includes(body.thinking?.type) &&
|
|
199
|
+
settings.thinking_mismatch !== "upstream_default") {
|
|
200
|
+
body.thinking = {
|
|
201
|
+
...body.thinking,
|
|
202
|
+
block_binding: {
|
|
203
|
+
prefix_mismatch_behavior: settings.thinking_mismatch,
|
|
204
|
+
...body.thinking.block_binding,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (system.length)
|
|
209
|
+
body.system = system;
|
|
210
|
+
else
|
|
211
|
+
delete body.system;
|
|
212
|
+
if (definitions.length)
|
|
213
|
+
body.tools = definitions;
|
|
214
|
+
else
|
|
215
|
+
delete body.tools;
|
|
216
|
+
if (context.compacting) {
|
|
217
|
+
if (body.output_config) {
|
|
218
|
+
body.output_config = { ...body.output_config };
|
|
219
|
+
delete body.output_config.format;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return { body, tools, betas: [THINKING_BINDING_BETA, ...(hasInlineTools ? [INLINE_TOOLS_BETA] : [])] };
|
|
223
|
+
}
|
|
224
|
+
function tokenCount(value) {
|
|
225
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
226
|
+
throw new BridgeError("Messages stream has invalid or missing token usage.", "upstream_error", 502);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function validateUsage(value, required) {
|
|
231
|
+
for (const field of required)
|
|
232
|
+
tokenCount(value?.[field]);
|
|
233
|
+
for (const field of ["input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens"]) {
|
|
234
|
+
if (value?.[field] !== undefined)
|
|
235
|
+
tokenCount(value[field]);
|
|
236
|
+
}
|
|
237
|
+
if (value?.output_tokens_details?.thinking_tokens !== undefined)
|
|
238
|
+
tokenCount(value.output_tokens_details.thinking_tokens);
|
|
239
|
+
}
|
|
240
|
+
export const runAnthropic = async (context, writer) => {
|
|
241
|
+
const { body, tools, betas } = buildAnthropicRequest(context);
|
|
242
|
+
const response = await upstreamFetch(context.route, "/messages", body, context.signal, context.requestHeaders, { "anthropic-beta": betas });
|
|
243
|
+
context.onAccepted?.(response.headers);
|
|
244
|
+
if (!response.body)
|
|
245
|
+
throw new BridgeError("Messages returned an empty stream.", "upstream_error", 502);
|
|
246
|
+
const blocks = new Map();
|
|
247
|
+
let rawUsage = {};
|
|
248
|
+
let started = false;
|
|
249
|
+
let finalUsage = false;
|
|
250
|
+
let stopReason;
|
|
251
|
+
let stopped = false;
|
|
252
|
+
for await (const { json: event } of parseJsonSse(response.body)) {
|
|
253
|
+
if (event.type === "error")
|
|
254
|
+
throw upstreamError(undefined, event, response.headers);
|
|
255
|
+
if (event.type === "ping") {
|
|
256
|
+
writer.keepalive();
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (event.type === "message_start") {
|
|
260
|
+
validateUsage(event.message?.usage, ["input_tokens", "output_tokens"]);
|
|
261
|
+
started = true;
|
|
262
|
+
rawUsage = { ...rawUsage, ...event.message?.usage };
|
|
263
|
+
}
|
|
264
|
+
else if (event.type === "content_block_start") {
|
|
265
|
+
const value = { ...event.content_block };
|
|
266
|
+
if (!["text", "thinking", "redacted_thinking", "tool_use"].includes(value.type)) {
|
|
267
|
+
throw new BridgeError(`Unsupported Messages output block: ${String(value.type)}.`);
|
|
268
|
+
}
|
|
269
|
+
blocks.set(event.index, { value, json: "" });
|
|
270
|
+
if (value.type === "text" && value.text)
|
|
271
|
+
writer.textDelta(value.text);
|
|
272
|
+
if (value.type === "thinking")
|
|
273
|
+
writer.reasoningDelta(value.thinking ?? "");
|
|
274
|
+
}
|
|
275
|
+
else if (event.type === "content_block_delta") {
|
|
276
|
+
const block = blocks.get(event.index);
|
|
277
|
+
if (!block)
|
|
278
|
+
throw new BridgeError("Messages sent a delta before its block.", "upstream_error", 502);
|
|
279
|
+
const delta = event.delta;
|
|
280
|
+
if (delta.type === "text_delta") {
|
|
281
|
+
block.value.text = (block.value.text ?? "") + delta.text;
|
|
282
|
+
writer.textDelta(delta.text);
|
|
283
|
+
}
|
|
284
|
+
else if (delta.type === "thinking_delta") {
|
|
285
|
+
block.value.thinking = (block.value.thinking ?? "") + delta.thinking;
|
|
286
|
+
writer.reasoningDelta(delta.thinking);
|
|
287
|
+
}
|
|
288
|
+
else if (delta.type === "signature_delta") {
|
|
289
|
+
block.value.signature = (block.value.signature ?? "") + delta.signature;
|
|
290
|
+
}
|
|
291
|
+
else if (delta.type === "input_json_delta")
|
|
292
|
+
block.json += delta.partial_json;
|
|
293
|
+
else if (delta.type !== "citations_delta")
|
|
294
|
+
throw new BridgeError(`Unsupported Messages delta: ${String(delta.type)}.`);
|
|
295
|
+
}
|
|
296
|
+
else if (event.type === "content_block_stop") {
|
|
297
|
+
const block = blocks.get(event.index);
|
|
298
|
+
if (!block)
|
|
299
|
+
throw new BridgeError("Messages stopped an unknown block.", "upstream_error", 502);
|
|
300
|
+
if (block.value.type === "thinking" || block.value.type === "redacted_thinking") {
|
|
301
|
+
writer.endReasoning(encodeReasoning({ source: context.route.origin, format: context.route.profile.reasoning_format, data: block.value }));
|
|
302
|
+
}
|
|
303
|
+
else if (block.value.type === "tool_use") {
|
|
304
|
+
if (block.json)
|
|
305
|
+
block.value.input = parseArguments(block.json);
|
|
306
|
+
const call = block.value;
|
|
307
|
+
const tool = tools.resolve(call.name);
|
|
308
|
+
if (!tool)
|
|
309
|
+
throw new BridgeError(`Messages called an unknown tool: ${String(call.name)}.`);
|
|
310
|
+
if (tool.kind === "custom") {
|
|
311
|
+
if (typeof call.input?.input !== "string")
|
|
312
|
+
throw new BridgeError("A custom tool call needs a string input.");
|
|
313
|
+
writer.addCustomToolCall({ call_id: call.id, name: tool.name, namespace: tool.namespace, input: call.input.input });
|
|
314
|
+
}
|
|
315
|
+
else
|
|
316
|
+
writer.addFunctionCall({ call_id: call.id, name: tool.name, namespace: tool.namespace, arguments: JSON.stringify(call.input ?? {}) });
|
|
317
|
+
}
|
|
318
|
+
else if (block.value.type === "text")
|
|
319
|
+
writer.closeMessage();
|
|
320
|
+
blocks.delete(event.index);
|
|
321
|
+
}
|
|
322
|
+
else if (event.type === "message_delta") {
|
|
323
|
+
stopReason = event.delta?.stop_reason ?? stopReason;
|
|
324
|
+
validateUsage(event.usage, ["output_tokens"]);
|
|
325
|
+
finalUsage = true;
|
|
326
|
+
rawUsage = { ...rawUsage, ...event.usage };
|
|
327
|
+
}
|
|
328
|
+
else if (event.type === "message_stop")
|
|
329
|
+
stopped = true;
|
|
330
|
+
}
|
|
331
|
+
if (!started || !stopped || blocks.size || !stopReason) {
|
|
332
|
+
throw new BridgeError("Messages stream ended before the message was complete.", "upstream_error", 502);
|
|
333
|
+
}
|
|
334
|
+
if (stopReason === "model_context_window_exceeded") {
|
|
335
|
+
throw new BridgeError("Messages reached the model context window before completing the response.", "context_length_exceeded");
|
|
336
|
+
}
|
|
337
|
+
if (stopReason === "max_tokens" || stopReason === "refusal") {
|
|
338
|
+
throw new BridgeError(stopReason === "max_tokens" ? "Messages reached max_tokens before completing the response." : "Messages refused the request.");
|
|
339
|
+
}
|
|
340
|
+
if (!["end_turn", "stop_sequence", "tool_use", "pause_turn"].includes(stopReason)) {
|
|
341
|
+
throw new BridgeError(`Unsupported Messages stop reason: ${stopReason}.`);
|
|
342
|
+
}
|
|
343
|
+
if (!finalUsage)
|
|
344
|
+
throw new BridgeError("Messages stream ended without final token usage.", "upstream_error", 502);
|
|
345
|
+
const usage = {
|
|
346
|
+
input_tokens: tokenCount(rawUsage.input_tokens + (rawUsage.cache_read_input_tokens ?? 0) + (rawUsage.cache_creation_input_tokens ?? 0)),
|
|
347
|
+
cached_tokens: rawUsage.cache_read_input_tokens ?? 0,
|
|
348
|
+
cache_write_tokens: rawUsage.cache_creation_input_tokens ?? 0,
|
|
349
|
+
output_tokens: tokenCount(rawUsage.output_tokens),
|
|
350
|
+
...(rawUsage.output_tokens_details?.thinking_tokens != null ? { reasoning_tokens: rawUsage.output_tokens_details.thinking_tokens } : {}),
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
usage,
|
|
354
|
+
endTurn: stopReason !== "tool_use" && stopReason !== "pause_turn",
|
|
355
|
+
};
|
|
356
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BridgeError } from "../errors.ts";
|
|
2
|
+
import type { ToolSet } from "../tools.ts";
|
|
3
|
+
import type { BackendContext, BackendResult } from "../types.ts";
|
|
4
|
+
import type { ResponseWriter, Usage } from "../writer.ts";
|
|
5
|
+
/** A wire dialect supplies only the parts that differ from Chat Completions. */
|
|
6
|
+
export interface ChatDialect {
|
|
7
|
+
reasoningField: string;
|
|
8
|
+
requestParameters(context: BackendContext): Record<string, unknown>;
|
|
9
|
+
readUsage(value: any): Usage;
|
|
10
|
+
failureReason(reason: string): BridgeError | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildChatRequest(context: BackendContext, dialect: ChatDialect): {
|
|
13
|
+
body: Record<string, any>;
|
|
14
|
+
tools: ToolSet;
|
|
15
|
+
};
|
|
16
|
+
export declare function runChat(context: BackendContext, writer: ResponseWriter, dialect: ChatDialect): Promise<BackendResult>;
|