@wacry/codeks-bridge 0.0.0-stage → 0.0.1
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/auth.d.ts +2 -0
- package/dist/auth.js +42 -0
- package/dist/backends/anthropic.d.ts +8 -0
- package/dist/backends/anthropic.js +309 -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 +205 -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 +167 -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 +72 -0
- package/dist/tools.d.ts +33 -0
- package/dist/tools.js +182 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.js +1 -0
- package/dist/upstream.d.ts +3 -0
- package/dist/upstream.js +84 -0
- package/dist/writer.d.ts +95 -0
- package/dist/writer.js +430 -0
- package/docs/backends-anthropic.md +61 -0
- package/docs/backends-deepseek-chat.md +75 -0
- package/docs/backends-responses.md +33 -0
- package/docs/compaction.md +176 -0
- package/docs/config.md +231 -0
- package/docs/design.md +93 -0
- package/docs/inspect.md +138 -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 +55 -0
- package/examples/config.json +35 -0
- package/package.json +46 -4
- package/ui/api.js +64 -0
- package/ui/app.css +375 -0
- package/ui/app.js +489 -0
- package/ui/detail.js +682 -0
- package/ui/diff.js +281 -0
- package/ui/dom.js +258 -0
- package/ui/events.js +351 -0
- package/ui/favicon.svg +1 -0
- package/ui/index.html +58 -0
- package/ui/labels.js +101 -0
- package/ui/list.js +180 -0
- package/ui/structure.js +285 -0
- package/ui/tree.js +158 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { encodeCompaction } from "./carrier.js";
|
|
2
|
+
import { splitCompactionPrefix } from "./compaction-history.js";
|
|
3
|
+
import { BridgeError, ResponsesIncompleteError, ResponsesStreamError } from "./errors.js";
|
|
4
|
+
import { newId } from "./ids.js";
|
|
5
|
+
import { withCaptureLabel } from "./inspect/capture.js";
|
|
6
|
+
import { HANDOFF_PROMPT, SUMMARIZATION_PROMPT } from "./prompts.js";
|
|
7
|
+
import { ResponseWriter } from "./writer.js";
|
|
8
|
+
export function isCompactionRequest(request) {
|
|
9
|
+
return Array.isArray(request.input) && request.input.some((item) => item.type === "compaction_trigger");
|
|
10
|
+
}
|
|
11
|
+
function compactionInput(request) {
|
|
12
|
+
if (!Array.isArray(request.input))
|
|
13
|
+
throw new BridgeError("Compaction requires an input array.");
|
|
14
|
+
const triggers = request.input.filter((item) => item.type === "compaction_trigger");
|
|
15
|
+
if (triggers.length !== 1 || request.input.at(-1)?.type !== "compaction_trigger") {
|
|
16
|
+
throw new BridgeError("Compaction requires exactly one trigger at the end of input.");
|
|
17
|
+
}
|
|
18
|
+
return request.input.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
/** Keep the shared prefix intact, including tools and any signed reasoning. */
|
|
21
|
+
export function summaryRequest(request) {
|
|
22
|
+
const input = compactionInput(request);
|
|
23
|
+
const result = {
|
|
24
|
+
...request,
|
|
25
|
+
input: [...input, { type: "message", role: "user", content: [{ type: "input_text", text: SUMMARIZATION_PROMPT }] }],
|
|
26
|
+
stream: true,
|
|
27
|
+
};
|
|
28
|
+
// A conversation's structured output schema does not describe a handoff summary.
|
|
29
|
+
delete result.text;
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function handoffRecipe(context, prefix) {
|
|
33
|
+
// The handoff rebuilds input and transport mode; a summary is not a structured answer.
|
|
34
|
+
const { input: _input, stream: _stream, text: _text, ...parameters } = context.request;
|
|
35
|
+
parameters.reasoning = { ...parameters.reasoning, effort: parameters.reasoning?.effort ?? context.route.profile.default_effort };
|
|
36
|
+
return structuredClone({ prefix, parameters, responsesLite: context.responsesLite === true });
|
|
37
|
+
}
|
|
38
|
+
/** Rephrase the checkpoint using the exact history retained by the client. */
|
|
39
|
+
export function handoffRequest(recipe, retained, checkpoint) {
|
|
40
|
+
return {
|
|
41
|
+
...recipe.parameters,
|
|
42
|
+
input: [...recipe.prefix, ...retained, checkpoint,
|
|
43
|
+
{ type: "message", role: "user", content: [{ type: "input_text", text: HANDOFF_PROMPT }] }],
|
|
44
|
+
stream: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function collect(context, backend) {
|
|
48
|
+
const writer = new ResponseWriter({ write() { } }, { model: context.request.model, showSummary: false });
|
|
49
|
+
let result;
|
|
50
|
+
try {
|
|
51
|
+
result = await backend(context, writer);
|
|
52
|
+
}
|
|
53
|
+
catch (cause) {
|
|
54
|
+
// Auxiliary output and usage must not become the client's replacement history.
|
|
55
|
+
if (cause instanceof ResponsesIncompleteError) {
|
|
56
|
+
throw new BridgeError(cause.message, "invalid_prompt", 400, undefined, { upstream: { body: cause.response }, cause });
|
|
57
|
+
}
|
|
58
|
+
if (cause instanceof ResponsesStreamError && cause.response) {
|
|
59
|
+
const { output: _output, usage: _usage, ...response } = cause.response;
|
|
60
|
+
throw new ResponsesStreamError(cause.error, { ...response, output: [] }, cause.event, cause.headers);
|
|
61
|
+
}
|
|
62
|
+
throw cause;
|
|
63
|
+
}
|
|
64
|
+
context.signal.throwIfAborted();
|
|
65
|
+
writer.complete(result.usage);
|
|
66
|
+
return { output: writer.outputItems, usage: result.usage, endTurn: result.endTurn };
|
|
67
|
+
}
|
|
68
|
+
function summaryText({ output, endTurn }) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
for (const value of output) {
|
|
71
|
+
const item = value;
|
|
72
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") {
|
|
73
|
+
throw new BridgeError("Compaction returned a tool call instead of a summary. No tools were executed.");
|
|
74
|
+
}
|
|
75
|
+
if (item.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content))
|
|
76
|
+
continue;
|
|
77
|
+
for (const part of item.content) {
|
|
78
|
+
if (part.type === "refusal")
|
|
79
|
+
throw new BridgeError("Compaction was refused.");
|
|
80
|
+
if (part.type === "output_text" && typeof part.text === "string")
|
|
81
|
+
parts.push(part.text);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (endTurn === false)
|
|
85
|
+
throw new BridgeError("Compaction did not finish its response.");
|
|
86
|
+
const summary = parts.join("\n").trim();
|
|
87
|
+
if (!summary)
|
|
88
|
+
throw new BridgeError("Compaction returned an empty summary.");
|
|
89
|
+
return summary;
|
|
90
|
+
}
|
|
91
|
+
export async function collectHandoff(context, backend) {
|
|
92
|
+
// Generating text from the source's own native state is independent of whether
|
|
93
|
+
// that route normally chooses to replay native checkpoints in ordinary turns.
|
|
94
|
+
const route = { ...context.route, compaction: { ...context.route.compaction, native_replay: true } };
|
|
95
|
+
const result = await withCaptureLabel("handoff", () => collect({ ...context, route, compacting: true }, backend));
|
|
96
|
+
return { summary: summaryText(result), usage: result.usage };
|
|
97
|
+
}
|
|
98
|
+
function nativeItem(output) {
|
|
99
|
+
const items = output.filter((value) => {
|
|
100
|
+
const item = value;
|
|
101
|
+
return item.type === "compaction" || item.type === "compaction_summary";
|
|
102
|
+
});
|
|
103
|
+
if (items.length !== 1 || typeof items[0]?.encrypted_content !== "string" || !items[0].encrypted_content) {
|
|
104
|
+
throw new BridgeError("Native compaction must return exactly one nonempty compaction item.");
|
|
105
|
+
}
|
|
106
|
+
return items[0];
|
|
107
|
+
}
|
|
108
|
+
/** Publish a complete native checkpoint or portable text summary. */
|
|
109
|
+
export async function compact(context, backend) {
|
|
110
|
+
const input = compactionInput(context.request);
|
|
111
|
+
context.signal.throwIfAborted();
|
|
112
|
+
const item = (carrier) => ({
|
|
113
|
+
type: "compaction", id: newId("cmp"), encrypted_content: encodeCompaction(carrier),
|
|
114
|
+
});
|
|
115
|
+
if (context.route.compaction.mode === "summary") {
|
|
116
|
+
const result = await withCaptureLabel("summary", () => collect({
|
|
117
|
+
...context, compacting: true, request: summaryRequest(context.request),
|
|
118
|
+
}, backend));
|
|
119
|
+
return { item: item({ summary: summaryText(result) }), usage: result.usage };
|
|
120
|
+
}
|
|
121
|
+
const native = await withCaptureLabel("native", () => collect({
|
|
122
|
+
...context, compacting: true, request: { ...context.request, stream: true },
|
|
123
|
+
}, backend));
|
|
124
|
+
if (native.endTurn === false)
|
|
125
|
+
throw new BridgeError("Compaction did not finish its response.");
|
|
126
|
+
const { prefix } = splitCompactionPrefix(input);
|
|
127
|
+
return {
|
|
128
|
+
item: item({
|
|
129
|
+
native: { source: context.route.origin, format: context.route.profile.native_compaction_format, data: nativeItem(native.output) },
|
|
130
|
+
handoff: handoffRecipe(context, prefix),
|
|
131
|
+
}),
|
|
132
|
+
usage: native.usage,
|
|
133
|
+
};
|
|
134
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BridgeConfig, ResolvedModel } from "./types.ts";
|
|
2
|
+
export declare function validateConfig(value: unknown): BridgeConfig;
|
|
3
|
+
export declare function loadConfig(path: string): Promise<BridgeConfig>;
|
|
4
|
+
export declare function resolveModel(config: BridgeConfig, slug: string): ResolvedModel;
|
|
5
|
+
/** Select one executor; the caller owns the generation mode, the executor owns replay. */
|
|
6
|
+
export declare function resolveCompactionModel(config: BridgeConfig, caller: ResolvedModel): ResolvedModel;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { modelProfile } from "./profiles.js";
|
|
4
|
+
import { getCatalogEntry } from "./model-catalog.js";
|
|
5
|
+
import { BridgeError } from "./errors.js";
|
|
6
|
+
function object(value) {
|
|
7
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function fail(message) { throw new BridgeError(`Invalid configuration: ${message}`); }
|
|
10
|
+
function strings(value) {
|
|
11
|
+
return Array.isArray(value) && value.every(v => typeof v === "string" && v.length > 0);
|
|
12
|
+
}
|
|
13
|
+
const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
14
|
+
/** Record provenance, never policy or credentials, in portable history. */
|
|
15
|
+
function origin(config, model) {
|
|
16
|
+
const connection = hash([model.upstream, config.upstreams[model.upstream].base_url.replace(/\/+$/, "")]);
|
|
17
|
+
const { adapter } = getCatalogEntry(model.catalog);
|
|
18
|
+
return { connection, model: model.model, adapter, id: hash([connection, adapter, model.model]) };
|
|
19
|
+
}
|
|
20
|
+
export function validateConfig(value) {
|
|
21
|
+
if (!object(value) || !object(value.upstreams) || !object(value.models))
|
|
22
|
+
fail("upstreams and models must be objects");
|
|
23
|
+
if (!Object.keys(value.models).length)
|
|
24
|
+
fail("at least one model is required");
|
|
25
|
+
if (value.host !== undefined && (typeof value.host !== "string" || !value.host))
|
|
26
|
+
fail("host must be a string");
|
|
27
|
+
if (value.port !== undefined && (!Number.isInteger(value.port) || value.port < 0 || value.port > 65535))
|
|
28
|
+
fail("port must be 0..65535");
|
|
29
|
+
if (value.token_env !== undefined && (typeof value.token_env !== "string" || !value.token_env))
|
|
30
|
+
fail("token_env must name an environment variable");
|
|
31
|
+
if (value.compaction_cache_dir !== undefined && (typeof value.compaction_cache_dir !== "string" || !value.compaction_cache_dir.trim()))
|
|
32
|
+
fail("compaction_cache_dir must be a nonempty path");
|
|
33
|
+
if (value.inspect !== undefined) {
|
|
34
|
+
const inspect = value.inspect;
|
|
35
|
+
if (!object(inspect))
|
|
36
|
+
fail("inspect must be an object");
|
|
37
|
+
if (inspect.capture !== undefined && typeof inspect.capture !== "boolean")
|
|
38
|
+
fail("inspect.capture must be boolean");
|
|
39
|
+
if (inspect.max_mib !== undefined && (!Number.isSafeInteger(inspect.max_mib) || inspect.max_mib <= 0))
|
|
40
|
+
fail("inspect.max_mib must be a positive integer");
|
|
41
|
+
}
|
|
42
|
+
for (const [name, up] of Object.entries(value.upstreams)) {
|
|
43
|
+
if (!object(up))
|
|
44
|
+
fail(`upstream ${name}: expected a connection object`);
|
|
45
|
+
let url;
|
|
46
|
+
try {
|
|
47
|
+
url = new URL(up.base_url);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
fail(`upstream ${name}: invalid base_url`);
|
|
51
|
+
}
|
|
52
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
53
|
+
fail(`upstream ${name}: base_url must be an HTTP(S) URL without credentials, query, or fragment`);
|
|
54
|
+
if (up.api_key_env !== undefined && (typeof up.api_key_env !== "string" || !up.api_key_env))
|
|
55
|
+
fail(`upstream ${name}: invalid api_key_env`);
|
|
56
|
+
if (up.api_key_env && up.api_key_command)
|
|
57
|
+
fail(`upstream ${name}: choose api_key_env or api_key_command`);
|
|
58
|
+
if (up.api_key_command !== undefined && (!Array.isArray(up.api_key_command) || !up.api_key_command.length || up.api_key_command.some((arg) => typeof arg !== "string")))
|
|
59
|
+
fail(`upstream ${name}: api_key_command must be a nonempty argv array`);
|
|
60
|
+
if (up.stream_idle_timeout_ms !== undefined && (!Number.isSafeInteger(up.stream_idle_timeout_ms) || up.stream_idle_timeout_ms <= 0))
|
|
61
|
+
fail(`upstream ${name}: invalid stream_idle_timeout_ms`);
|
|
62
|
+
if (up.auth_header !== undefined && (typeof up.auth_header !== "string" || !/^[\w-]+$/.test(up.auth_header)))
|
|
63
|
+
fail(`upstream ${name}: invalid auth_header`);
|
|
64
|
+
if (up.headers !== undefined && (!object(up.headers) || Object.values(up.headers).some(v => typeof v !== "string" || /[\r\n]/.test(v))))
|
|
65
|
+
fail(`upstream ${name}: invalid headers`);
|
|
66
|
+
if (up.options !== undefined && !object(up.options))
|
|
67
|
+
fail(`upstream ${name}: options must be an object`);
|
|
68
|
+
}
|
|
69
|
+
for (const [slug, model] of Object.entries(value.models)) {
|
|
70
|
+
if (!slug || !object(model) || typeof model.model !== "string" || !model.model || typeof model.upstream !== "string" || !Object.hasOwn(value.upstreams, model.upstream))
|
|
71
|
+
fail(`model ${slug}: model and an existing upstream are required`);
|
|
72
|
+
if (typeof model.catalog !== "string" || !model.catalog)
|
|
73
|
+
fail(`model ${slug}: catalog is required`);
|
|
74
|
+
if (model.expose !== undefined && typeof model.expose !== "boolean")
|
|
75
|
+
fail(`model ${slug}: expose must be boolean`);
|
|
76
|
+
for (const field of ["context_window", "max_output_tokens"]) {
|
|
77
|
+
if (model[field] !== undefined && (!Number.isSafeInteger(model[field]) || model[field] <= 0))
|
|
78
|
+
fail(`model ${slug}: ${field} must be a positive integer`);
|
|
79
|
+
}
|
|
80
|
+
if (model.options !== undefined && !object(model.options))
|
|
81
|
+
fail(`model ${slug}: options must be an object`);
|
|
82
|
+
if (model.input_modalities !== undefined && (!strings(model.input_modalities) || !model.input_modalities.length || model.input_modalities.some((m) => !["text", "image"].includes(m))))
|
|
83
|
+
fail(`model ${slug}: input_modalities must contain text or image`);
|
|
84
|
+
if (model.reasoning !== undefined) {
|
|
85
|
+
const r = model.reasoning;
|
|
86
|
+
if (!object(r))
|
|
87
|
+
fail(`model ${slug}: reasoning must be an object`);
|
|
88
|
+
if (r.default_effort !== undefined && (typeof r.default_effort !== "string" || !r.default_effort))
|
|
89
|
+
fail(`model ${slug}: invalid default_effort`);
|
|
90
|
+
if (r.effort_map !== undefined && (!object(r.effort_map) || !Object.keys(r.effort_map).length || Object.entries(r.effort_map).some(([key, native]) => !key || typeof native !== "string" || !native)))
|
|
91
|
+
fail(`model ${slug}: effort_map must map effort names to upstream values`);
|
|
92
|
+
}
|
|
93
|
+
const profile = modelProfile(model);
|
|
94
|
+
if (Object.hasOwn(profile.effort_map, "ultra") && !profile.multi_agent_effort)
|
|
95
|
+
fail(`model ${slug}: ultra must map to the same upstream effort as an ordinary level`);
|
|
96
|
+
if (!Object.hasOwn(profile.effort_map, profile.default_effort))
|
|
97
|
+
fail(`model ${slug}: default_effort must appear in effort_map`);
|
|
98
|
+
if (model.replay !== undefined) {
|
|
99
|
+
if (!object(model.replay))
|
|
100
|
+
fail(`model ${slug}: replay must be an object`);
|
|
101
|
+
for (const field of ["reasoning_from", "native_compaction_from"]) {
|
|
102
|
+
if (model.replay[field] !== undefined && !strings(model.replay[field]))
|
|
103
|
+
fail(`model ${slug}: ${field} must list source model aliases`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (model.compaction !== undefined) {
|
|
107
|
+
const c = model.compaction;
|
|
108
|
+
if (!object(c))
|
|
109
|
+
fail(`model ${slug}: compaction must be an object`);
|
|
110
|
+
if (c.mode !== undefined && !["summary", "native"].includes(c.mode))
|
|
111
|
+
fail(`model ${slug}: compaction.mode must be summary or native`);
|
|
112
|
+
if (c.model !== undefined && (typeof c.model !== "string" || !Object.hasOwn(value.models, c.model)))
|
|
113
|
+
fail(`model ${slug}: compaction.model must name an existing model alias`);
|
|
114
|
+
if (c.native_replay !== undefined && typeof c.native_replay !== "boolean")
|
|
115
|
+
fail(`model ${slug}: compaction.native_replay must be boolean`);
|
|
116
|
+
if (!profile.native_compaction_format && c.native_replay === true)
|
|
117
|
+
fail(`model ${slug}: adapter has no native compaction representation for replay`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// References are directional. Validate decodability, not provider compatibility.
|
|
121
|
+
const config = value;
|
|
122
|
+
for (const [slug, model] of Object.entries(config.models)) {
|
|
123
|
+
const target = modelProfile(model);
|
|
124
|
+
for (const [field, format] of [["reasoning_from", "reasoning_format"], ["native_compaction_from", "native_compaction_format"]]) {
|
|
125
|
+
for (const name of model.replay?.[field] ?? []) {
|
|
126
|
+
if (!Object.hasOwn(config.models, name))
|
|
127
|
+
fail(`model ${slug}: unknown replay source ${name}`);
|
|
128
|
+
const source = modelProfile(config.models[name]);
|
|
129
|
+
if (!target[format] || source[format] !== target[format])
|
|
130
|
+
fail(`model ${slug}: cannot decode ${field} source ${name}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
for (const slug of Object.keys(config.models)) {
|
|
135
|
+
const executor = resolveCompactionModel(config, resolveModel(config, slug));
|
|
136
|
+
if (executor.compaction.mode === "native" && !executor.profile.native_compaction_format)
|
|
137
|
+
fail(`model ${slug}: compaction model ${executor.slug} has no native compaction representation`);
|
|
138
|
+
}
|
|
139
|
+
return config;
|
|
140
|
+
}
|
|
141
|
+
export async function loadConfig(path) {
|
|
142
|
+
return validateConfig(JSON.parse(await readFile(path, "utf8")));
|
|
143
|
+
}
|
|
144
|
+
export function resolveModel(config, slug) {
|
|
145
|
+
if (typeof slug !== "string" || !Object.hasOwn(config.models, slug))
|
|
146
|
+
throw new BridgeError("Unknown model. Use GET /v1/models to list exposed models.");
|
|
147
|
+
const model = config.models[slug];
|
|
148
|
+
const profile = modelProfile(model);
|
|
149
|
+
const source = origin(config, model);
|
|
150
|
+
const allowed = (names = []) => new Set([source.id, ...names.map(name => origin(config, config.models[name]).id)]);
|
|
151
|
+
return {
|
|
152
|
+
slug, config: model, upstream: config.upstreams[model.upstream], profile, origin: source,
|
|
153
|
+
replay: { reasoning: allowed(model.replay?.reasoning_from), native_compaction: allowed(model.replay?.native_compaction_from) },
|
|
154
|
+
compaction: {
|
|
155
|
+
mode: model.compaction?.mode ?? (profile.native_compaction_format ? "native" : "summary"),
|
|
156
|
+
native_replay: model.compaction?.native_replay ?? !!profile.native_compaction_format,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/** Select one executor; the caller owns the generation mode, the executor owns replay. */
|
|
161
|
+
export function resolveCompactionModel(config, caller) {
|
|
162
|
+
const slug = caller.config.compaction?.model;
|
|
163
|
+
if (slug === undefined || slug === caller.slug)
|
|
164
|
+
return caller;
|
|
165
|
+
const executor = resolveModel(config, slug);
|
|
166
|
+
return { ...executor, compaction: { ...executor.compaction, mode: caller.compaction.mode } };
|
|
167
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type HeaderRecord } from "./headers.ts";
|
|
2
|
+
export interface UpstreamFailure {
|
|
3
|
+
status?: number;
|
|
4
|
+
body: unknown;
|
|
5
|
+
}
|
|
6
|
+
/** Native Responses HTTP failures retain their transport-level meaning. */
|
|
7
|
+
export declare class UpstreamHttpError extends Error {
|
|
8
|
+
readonly status: number;
|
|
9
|
+
readonly body: string;
|
|
10
|
+
readonly headers: Headers;
|
|
11
|
+
constructor(status: number, body: string, headers: Headers);
|
|
12
|
+
}
|
|
13
|
+
/** A Responses failure belongs to the SSE protocol, not HTTP classification. */
|
|
14
|
+
export declare class ResponsesStreamError extends Error {
|
|
15
|
+
readonly error: Record<string, unknown>;
|
|
16
|
+
readonly response?: Record<string, any>;
|
|
17
|
+
readonly event?: Record<string, any>;
|
|
18
|
+
readonly headers?: Headers;
|
|
19
|
+
constructor(error: Record<string, unknown>, response?: Record<string, any>, event?: Record<string, any>, headers?: Headers);
|
|
20
|
+
}
|
|
21
|
+
/** An incomplete response has its own client termination and usage semantics. */
|
|
22
|
+
export declare class ResponsesIncompleteError extends Error {
|
|
23
|
+
readonly response: Record<string, any>;
|
|
24
|
+
readonly event?: Record<string, any>;
|
|
25
|
+
constructor(response: Record<string, any>, event?: Record<string, any>);
|
|
26
|
+
}
|
|
27
|
+
/** Client retry classification is separate from the unmodified upstream failure. */
|
|
28
|
+
export declare class BridgeError extends Error {
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly status: number;
|
|
31
|
+
readonly headers?: HeaderRecord;
|
|
32
|
+
readonly upstream?: UpstreamFailure;
|
|
33
|
+
constructor(message: string, code?: string, status?: number, headers?: HeaderRecord, options?: {
|
|
34
|
+
upstream?: UpstreamFailure;
|
|
35
|
+
cause?: unknown;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Keep native fields; only supplement missing retry advice from HTTP headers. */
|
|
39
|
+
export declare function responsesStreamError(payload: unknown, headers?: Headers, event?: Record<string, any>): ResponsesStreamError;
|
|
40
|
+
/** Translate non-Responses protocols into errors understood by the client. */
|
|
41
|
+
export declare function upstreamError(status: number | undefined, payload: unknown, headers?: Headers): BridgeError;
|
|
42
|
+
/** Pick only diagnostics; fetch options and credential-command output must not escape. */
|
|
43
|
+
export declare function transportError(cause: unknown): BridgeError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { forwardResponseHeaders } from "./headers.js";
|
|
2
|
+
/** Native Responses HTTP failures retain their transport-level meaning. */
|
|
3
|
+
export class UpstreamHttpError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
body;
|
|
6
|
+
headers;
|
|
7
|
+
constructor(status, body, headers) {
|
|
8
|
+
super(`Upstream returned HTTP ${status}`);
|
|
9
|
+
this.name = "UpstreamHttpError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
this.headers = headers;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** A Responses failure belongs to the SSE protocol, not HTTP classification. */
|
|
16
|
+
export class ResponsesStreamError extends Error {
|
|
17
|
+
error;
|
|
18
|
+
response;
|
|
19
|
+
event;
|
|
20
|
+
headers;
|
|
21
|
+
constructor(error, response, event, headers) {
|
|
22
|
+
super(typeof error.message === "string" ? error.message : "The upstream response failed");
|
|
23
|
+
this.name = "ResponsesStreamError";
|
|
24
|
+
this.error = error;
|
|
25
|
+
this.response = response;
|
|
26
|
+
this.event = event;
|
|
27
|
+
this.headers = headers;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** An incomplete response has its own client termination and usage semantics. */
|
|
31
|
+
export class ResponsesIncompleteError extends Error {
|
|
32
|
+
response;
|
|
33
|
+
event;
|
|
34
|
+
constructor(response, event) {
|
|
35
|
+
super(`The upstream response is incomplete: ${String(response.incomplete_details?.reason ?? "unknown")}`);
|
|
36
|
+
this.name = "ResponsesIncompleteError";
|
|
37
|
+
this.response = response;
|
|
38
|
+
this.event = event;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Client retry classification is separate from the unmodified upstream failure. */
|
|
42
|
+
export class BridgeError extends Error {
|
|
43
|
+
code;
|
|
44
|
+
status;
|
|
45
|
+
headers;
|
|
46
|
+
upstream;
|
|
47
|
+
constructor(message, code = "invalid_prompt", status = 400, headers, options) {
|
|
48
|
+
super(message, options);
|
|
49
|
+
this.name = "BridgeError";
|
|
50
|
+
this.code = code;
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.headers = headers;
|
|
53
|
+
this.upstream = options?.upstream;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function record(value) {
|
|
57
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
function retryAfter(value) {
|
|
60
|
+
if (value instanceof Headers)
|
|
61
|
+
return value.get("retry-after") ?? undefined;
|
|
62
|
+
for (const [name, content] of Object.entries(record(value) ?? {})) {
|
|
63
|
+
if (name.toLowerCase() === "retry-after" && (typeof content === "string" || typeof content === "number"))
|
|
64
|
+
return String(content);
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Keep native fields; only supplement missing retry advice from HTTP headers. */
|
|
69
|
+
export function responsesStreamError(payload, headers, event) {
|
|
70
|
+
const envelope = record(payload);
|
|
71
|
+
const response = envelope?.error !== undefined && (envelope.object === "response" || typeof envelope.status === "string" ||
|
|
72
|
+
(event?.type === "response.failed" && event.response !== undefined)) ? envelope : undefined;
|
|
73
|
+
let error = record(envelope?.error ?? payload) ?? {};
|
|
74
|
+
const delay = retryAfter(headers);
|
|
75
|
+
if (delay !== undefined && retryAfter(error.headers) === undefined) {
|
|
76
|
+
error = { ...error, headers: { ...record(error.headers), "retry-after": delay } };
|
|
77
|
+
}
|
|
78
|
+
return new ResponsesStreamError(error, response ? { ...response, error } : undefined, event, headers);
|
|
79
|
+
}
|
|
80
|
+
/** Translate non-Responses protocols into errors understood by the client. */
|
|
81
|
+
export function upstreamError(status, payload, headers) {
|
|
82
|
+
const envelope = record(payload);
|
|
83
|
+
const raw = envelope?.error ?? payload;
|
|
84
|
+
const error = record(raw);
|
|
85
|
+
const message = typeof error?.message === "string" ? error.message
|
|
86
|
+
: typeof raw === "string" && raw.length ? raw
|
|
87
|
+
: status !== undefined ? `Upstream returned HTTP ${status}` : "The upstream response failed";
|
|
88
|
+
const kind = [error?.code, error?.type].filter((value) => typeof value === "string").join(" ");
|
|
89
|
+
let code;
|
|
90
|
+
if (/context_length|too_many_tokens/.test(kind) || /prompt is too long|maximum context length|too many tokens/i.test(message))
|
|
91
|
+
code = "context_length_exceeded";
|
|
92
|
+
else if (/quota|billing|credit|balance/.test(kind) || /insufficient (quota|credit|balance)/i.test(message))
|
|
93
|
+
code = "insufficient_quota";
|
|
94
|
+
else if (status === 429 || /rate_limit|slow_down/.test(kind))
|
|
95
|
+
code = "rate_limit_exceeded";
|
|
96
|
+
else if (status === 503 || status === 529 || /overload/.test(kind))
|
|
97
|
+
code = "server_is_overloaded";
|
|
98
|
+
else if (/stream_error/.test(kind))
|
|
99
|
+
code = "stream_error";
|
|
100
|
+
else if (/invalid|authentication|permission|not_found/.test(kind))
|
|
101
|
+
code = "invalid_prompt";
|
|
102
|
+
else
|
|
103
|
+
code = status !== undefined && status < 500 ? "invalid_prompt" : "upstream_error";
|
|
104
|
+
const mappedStatus = code === "rate_limit_exceeded" || code === "insufficient_quota" ? 429
|
|
105
|
+
: code === "server_is_overloaded" ? 503
|
|
106
|
+
: code === "invalid_prompt" || code === "context_length_exceeded" ? 400 : 502;
|
|
107
|
+
const delay = retryAfter(headers) ?? retryAfter(error?.headers) ?? retryAfter(envelope?.headers);
|
|
108
|
+
const forwarded = headers ? forwardResponseHeaders(headers) : {};
|
|
109
|
+
if (delay !== undefined)
|
|
110
|
+
forwarded["retry-after"] = delay;
|
|
111
|
+
return new BridgeError(message, code, status ?? mappedStatus, Object.keys(forwarded).length ? forwarded : undefined, {
|
|
112
|
+
upstream: { ...(status !== undefined ? { status } : {}), body: payload },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function transportFailure(value, seen = new Set()) {
|
|
116
|
+
const error = record(value);
|
|
117
|
+
const result = { message: typeof error?.message === "string" ? error.message : typeof value === "string" ? value : "Upstream request failed" };
|
|
118
|
+
if (seen.has(value))
|
|
119
|
+
return result;
|
|
120
|
+
seen.add(value);
|
|
121
|
+
if (typeof error?.name === "string")
|
|
122
|
+
result.name = error.name;
|
|
123
|
+
if (typeof error?.code === "string")
|
|
124
|
+
result.code = error.code;
|
|
125
|
+
if (error?.cause !== undefined)
|
|
126
|
+
result.cause = transportFailure(error.cause, seen);
|
|
127
|
+
if (Array.isArray(error?.errors))
|
|
128
|
+
result.errors = error.errors.map((cause) => transportFailure(cause, seen));
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function transportMessage(error) {
|
|
132
|
+
const own = error.code ? `${error.message} (${error.code})` : error.message;
|
|
133
|
+
return [own, ...(error.cause ? [transportMessage(error.cause)] : []), ...(error.errors?.map(transportMessage) ?? [])].filter(Boolean).join(": ");
|
|
134
|
+
}
|
|
135
|
+
/** Pick only diagnostics; fetch options and credential-command output must not escape. */
|
|
136
|
+
export function transportError(cause) {
|
|
137
|
+
if (cause instanceof BridgeError)
|
|
138
|
+
return cause;
|
|
139
|
+
const failure = transportFailure(cause);
|
|
140
|
+
return new BridgeError(transportMessage(failure), "upstream_error", 502, undefined, { cause: failure });
|
|
141
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
2
|
+
export type HeaderRecord = Record<string, string | string[]>;
|
|
3
|
+
export declare function incomingHeaders(input: IncomingHttpHeaders): Headers;
|
|
4
|
+
/** Credentials belong to the selected upstream, never to the bridge client. */
|
|
5
|
+
export declare function forwardRequestHeaders(input?: Headers, authHeader?: string): Headers;
|
|
6
|
+
export declare function forwardResponseHeaders(input: Headers, rewritten?: boolean): HeaderRecord;
|
package/dist/headers.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const HOP_BY_HOP = ["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"];
|
|
2
|
+
/** Each HTTP leg owns its framing; metadata is forwarded unless it conflicts. */
|
|
3
|
+
function withoutTransportHeaders(input) {
|
|
4
|
+
const headers = new Headers(input);
|
|
5
|
+
const connection = (headers.get("connection") ?? "").split(",").map(name => name.trim()).filter(Boolean);
|
|
6
|
+
for (const name of [...connection, ...HOP_BY_HOP, "content-length", "content-encoding", "content-md5", "digest", "content-digest", "repr-digest"])
|
|
7
|
+
headers.delete(name);
|
|
8
|
+
return headers;
|
|
9
|
+
}
|
|
10
|
+
export function incomingHeaders(input) {
|
|
11
|
+
const headers = new Headers();
|
|
12
|
+
for (const [name, value] of Object.entries(input)) {
|
|
13
|
+
for (const item of Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
14
|
+
headers.append(name, item);
|
|
15
|
+
}
|
|
16
|
+
return headers;
|
|
17
|
+
}
|
|
18
|
+
/** Credentials belong to the selected upstream, never to the bridge client. */
|
|
19
|
+
export function forwardRequestHeaders(input, authHeader) {
|
|
20
|
+
const headers = withoutTransportHeaders(input);
|
|
21
|
+
for (const name of ["host", "accept-encoding", "authorization", "x-api-key", "api-key", "cookie", ...(authHeader ? [authHeader] : [])])
|
|
22
|
+
headers.delete(name);
|
|
23
|
+
return headers;
|
|
24
|
+
}
|
|
25
|
+
export function forwardResponseHeaders(input, rewritten = true) {
|
|
26
|
+
const headers = withoutTransportHeaders(input);
|
|
27
|
+
// The catalog belongs to the bridge even when the selected upstream also
|
|
28
|
+
// serves Codex. Rewritten bodies need their own content type.
|
|
29
|
+
headers.delete("x-models-etag");
|
|
30
|
+
if (rewritten) {
|
|
31
|
+
headers.delete("content-type");
|
|
32
|
+
headers.delete("etag");
|
|
33
|
+
}
|
|
34
|
+
const result = Object.fromEntries(headers);
|
|
35
|
+
const cookies = headers.getSetCookie();
|
|
36
|
+
if (cookies.length)
|
|
37
|
+
result["set-cookie"] = cookies;
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type CompactionCarrier, type StatePayload } from "./carrier.ts";
|
|
2
|
+
import type { ResolvedModel, ResponsesRequest } from "./types.ts";
|
|
3
|
+
/** Replay policy for decoded state. The inspector reports the same decisions. */
|
|
4
|
+
export declare function replaysReasoning(carrier: StatePayload, route: ResolvedModel): boolean;
|
|
5
|
+
export declare function restoresNativeCompaction(carrier: CompactionCarrier, route: ResolvedModel): boolean;
|
|
6
|
+
/** Selects model-specific opaque state without changing the stored history. */
|
|
7
|
+
export declare function prepareHistory(input: ResponsesRequest["input"], route: ResolvedModel): Record<string, any>[];
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { decodeCompaction, decodeReasoning } from "./carrier.js";
|
|
2
|
+
import { BridgeError } from "./errors.js";
|
|
3
|
+
import { SUMMARY_PREFIX } from "./prompts.js";
|
|
4
|
+
import { translateToolHistory } from "./tool-history.js";
|
|
5
|
+
/** Replay policy for decoded state. The inspector reports the same decisions. */
|
|
6
|
+
export function replaysReasoning(carrier, route) {
|
|
7
|
+
return route.replay.reasoning.has(carrier.source.id) && carrier.format === route.profile.reasoning_format;
|
|
8
|
+
}
|
|
9
|
+
export function restoresNativeCompaction(carrier, route) {
|
|
10
|
+
return !!carrier.native && route.compaction.native_replay &&
|
|
11
|
+
route.replay.native_compaction.has(carrier.native.source.id) &&
|
|
12
|
+
carrier.native.format === route.profile.native_compaction_format;
|
|
13
|
+
}
|
|
14
|
+
/** Selects model-specific opaque state without changing the stored history. */
|
|
15
|
+
export function prepareHistory(input, route) {
|
|
16
|
+
if (typeof input === "string") {
|
|
17
|
+
return [{ type: "message", role: "user", content: [{ type: "input_text", text: input }] }];
|
|
18
|
+
}
|
|
19
|
+
if (input === undefined)
|
|
20
|
+
return [];
|
|
21
|
+
if (!Array.isArray(input))
|
|
22
|
+
throw new BridgeError("input must be a string or an array of items");
|
|
23
|
+
const result = [];
|
|
24
|
+
for (const original of input) {
|
|
25
|
+
if (!original || typeof original !== "object" || Array.isArray(original)) {
|
|
26
|
+
throw new BridgeError("Each input item must be an object");
|
|
27
|
+
}
|
|
28
|
+
const item = structuredClone(original);
|
|
29
|
+
if (item.type === "bridge_reasoning") {
|
|
30
|
+
throw new BridgeError("Internal reasoning items cannot be supplied as request history");
|
|
31
|
+
}
|
|
32
|
+
if (item.type === "item_reference") {
|
|
33
|
+
throw new BridgeError("Stored item references are unsupported; send the complete conversation items");
|
|
34
|
+
}
|
|
35
|
+
if (item.type === "reasoning") {
|
|
36
|
+
const carrier = decodeReasoning(item.encrypted_content);
|
|
37
|
+
if (carrier && replaysReasoning(carrier, route)) {
|
|
38
|
+
result.push({ type: "bridge_reasoning", state: carrier, summary: item.summary ?? [] });
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (item.type === "agent_message" && route.profile.api !== "responses") {
|
|
43
|
+
if (typeof item.author !== "string" || typeof item.recipient !== "string" || !Array.isArray(item.content)) {
|
|
44
|
+
throw new BridgeError("An agent message requires author, recipient, and content");
|
|
45
|
+
}
|
|
46
|
+
if (item.encrypted_content || item.content.some((part) => part?.type === "encrypted_content")) {
|
|
47
|
+
throw new BridgeError("Encrypted agent messages are unsupported on this backend; use plaintext agent messages");
|
|
48
|
+
}
|
|
49
|
+
for (const part of item.content) {
|
|
50
|
+
if (!["input_text", "input_image"].includes(part?.type)) {
|
|
51
|
+
throw new BridgeError(`Unsupported agent message content: ${String(part?.type)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
result.push({
|
|
55
|
+
type: "message", role: "user",
|
|
56
|
+
content: [
|
|
57
|
+
{ type: "input_text", text: `Agent message from ${JSON.stringify(item.author)} to ${JSON.stringify(item.recipient)}:\n` },
|
|
58
|
+
...item.content,
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (item.type === "compaction" || item.type === "compaction_summary") {
|
|
64
|
+
const carrier = decodeCompaction(item.encrypted_content);
|
|
65
|
+
if (!carrier) {
|
|
66
|
+
throw new BridgeError("This compaction item has no portable summary or handoff recipe; use history created through this bridge");
|
|
67
|
+
}
|
|
68
|
+
if (restoresNativeCompaction(carrier, route)) {
|
|
69
|
+
result.push(carrier.native.data);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
if (!carrier.summary)
|
|
73
|
+
throw new BridgeError("Deferred compaction summary has not been materialized");
|
|
74
|
+
result.push({
|
|
75
|
+
type: "message",
|
|
76
|
+
role: "user",
|
|
77
|
+
content: [{ type: "input_text", text: `${SUMMARY_PREFIX}\n\n${carrier.summary}` }],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
result.push(item);
|
|
83
|
+
}
|
|
84
|
+
return route.profile.api === "responses" ? result : translateToolHistory(result);
|
|
85
|
+
}
|
package/dist/ids.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function newId(prefix: string): string;
|
package/dist/ids.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createBridgeServer } from "./server.ts";
|
|
2
|
+
export { loadConfig, validateConfig, resolveModel } from "./config.ts";
|
|
3
|
+
export { buildCatalog, configSnippet } from "./catalog.ts";
|
|
4
|
+
export { BridgeError } from "./errors.ts";
|
|
5
|
+
export type { BridgeConfig, UpstreamConfig, ModelConfig } from "./types.ts";
|
package/dist/index.js
ADDED