@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.
Files changed (99) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +40 -2
  4. package/catalog/README.md +63 -0
  5. package/catalog/claude-opus-5-5.json +194 -0
  6. package/catalog/claude-sonnet-5-5.json +194 -0
  7. package/catalog/deepseek-flash.json +203 -0
  8. package/catalog/gpt-5.6-sol.json +152 -0
  9. package/catalog/gpt-6-astra.json +184 -0
  10. package/catalog/gpt-6-luna.json +178 -0
  11. package/catalog/gpt-6-sol.json +182 -0
  12. package/catalog/gpt-6.1-sol.json +187 -0
  13. package/dist/auth.d.ts +2 -0
  14. package/dist/auth.js +42 -0
  15. package/dist/backends/anthropic.d.ts +8 -0
  16. package/dist/backends/anthropic.js +309 -0
  17. package/dist/backends/chat.d.ts +16 -0
  18. package/dist/backends/chat.js +284 -0
  19. package/dist/backends/deepseek.d.ts +6 -0
  20. package/dist/backends/deepseek.js +37 -0
  21. package/dist/backends/responses.d.ts +5 -0
  22. package/dist/backends/responses.js +205 -0
  23. package/dist/carrier.d.ts +22 -0
  24. package/dist/carrier.js +61 -0
  25. package/dist/catalog.d.ts +8 -0
  26. package/dist/catalog.js +40 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +64 -0
  29. package/dist/compaction-handoff.d.ts +13 -0
  30. package/dist/compaction-handoff.js +126 -0
  31. package/dist/compaction-history.d.ts +7 -0
  32. package/dist/compaction-history.js +20 -0
  33. package/dist/compaction.d.ts +20 -0
  34. package/dist/compaction.js +134 -0
  35. package/dist/config.d.ts +6 -0
  36. package/dist/config.js +167 -0
  37. package/dist/errors.d.ts +43 -0
  38. package/dist/errors.js +141 -0
  39. package/dist/headers.d.ts +6 -0
  40. package/dist/headers.js +39 -0
  41. package/dist/history.d.ts +7 -0
  42. package/dist/history.js +85 -0
  43. package/dist/ids.d.ts +1 -0
  44. package/dist/ids.js +4 -0
  45. package/dist/index.d.ts +5 -0
  46. package/dist/index.js +4 -0
  47. package/dist/inspect/capture.d.ts +119 -0
  48. package/dist/inspect/capture.js +370 -0
  49. package/dist/inspect/http.d.ts +7 -0
  50. package/dist/inspect/http.js +149 -0
  51. package/dist/inspect/view.d.ts +239 -0
  52. package/dist/inspect/view.js +388 -0
  53. package/dist/model-catalog.d.ts +18 -0
  54. package/dist/model-catalog.js +16 -0
  55. package/dist/profiles.d.ts +6 -0
  56. package/dist/profiles.js +63 -0
  57. package/dist/prompts.d.ts +5 -0
  58. package/dist/prompts.js +118 -0
  59. package/dist/server.d.ts +4 -0
  60. package/dist/server.js +232 -0
  61. package/dist/sse.d.ts +12 -0
  62. package/dist/sse.js +93 -0
  63. package/dist/tool-history.d.ts +4 -0
  64. package/dist/tool-history.js +72 -0
  65. package/dist/tools.d.ts +33 -0
  66. package/dist/tools.js +182 -0
  67. package/dist/types.d.ts +115 -0
  68. package/dist/types.js +1 -0
  69. package/dist/upstream.d.ts +3 -0
  70. package/dist/upstream.js +84 -0
  71. package/dist/writer.d.ts +95 -0
  72. package/dist/writer.js +430 -0
  73. package/docs/backends-anthropic.md +61 -0
  74. package/docs/backends-deepseek-chat.md +75 -0
  75. package/docs/backends-responses.md +33 -0
  76. package/docs/compaction.md +176 -0
  77. package/docs/config.md +231 -0
  78. package/docs/design.md +93 -0
  79. package/docs/inspect.md +138 -0
  80. package/docs/reasoning.md +72 -0
  81. package/docs/releasing.md +50 -0
  82. package/docs/responses-stream.md +90 -0
  83. package/docs/testing.md +177 -0
  84. package/docs/tools.md +55 -0
  85. package/examples/config.json +35 -0
  86. package/package.json +46 -4
  87. package/ui/api.js +64 -0
  88. package/ui/app.css +375 -0
  89. package/ui/app.js +489 -0
  90. package/ui/detail.js +682 -0
  91. package/ui/diff.js +281 -0
  92. package/ui/dom.js +258 -0
  93. package/ui/events.js +351 -0
  94. package/ui/favicon.svg +1 -0
  95. package/ui/index.html +58 -0
  96. package/ui/labels.js +101 -0
  97. package/ui/list.js +180 -0
  98. package/ui/structure.js +285 -0
  99. package/ui/tree.js +158 -0
@@ -0,0 +1,37 @@
1
+ import { BridgeError } from "../errors.js";
2
+ import { reasoningEffort } from "../profiles.js";
3
+ import { buildChatRequest, runChat } from "./chat.js";
4
+ const deepseek = {
5
+ reasoningField: "reasoning_content",
6
+ requestParameters({ request, route }) {
7
+ const effort = reasoningEffort(route, request.reasoning?.effort);
8
+ return effort === "none"
9
+ ? { thinking: { type: "disabled" } }
10
+ : { thinking: { type: "enabled" }, reasoning_effort: effort };
11
+ },
12
+ failureReason(reason) {
13
+ if (reason === "insufficient_system_resource" || reason === "aborted") {
14
+ return new BridgeError(`DeepSeek generation stopped with ${reason}.`, "server_is_overloaded", 503);
15
+ }
16
+ },
17
+ readUsage(value) {
18
+ const count = (value) => {
19
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
20
+ throw new BridgeError("Chat stream has no valid token usage.", "upstream_error", 502);
21
+ }
22
+ return value;
23
+ };
24
+ const cached = value?.prompt_cache_hit_tokens ?? value?.prompt_tokens_details?.cached_tokens;
25
+ const reasoning = value?.completion_tokens_details?.reasoning_tokens;
26
+ return {
27
+ input_tokens: count(value?.prompt_tokens),
28
+ output_tokens: count(value?.completion_tokens),
29
+ ...(cached !== undefined ? { cached_tokens: count(cached) } : {}),
30
+ ...(reasoning !== undefined ? { reasoning_tokens: count(reasoning) } : {}),
31
+ };
32
+ },
33
+ };
34
+ export function buildDeepSeekRequest(context) {
35
+ return buildChatRequest(context, deepseek);
36
+ }
37
+ export const runDeepSeek = (context, writer) => runChat(context, writer, deepseek);
@@ -0,0 +1,5 @@
1
+ import type { Backend, BackendContext } from "../types.ts";
2
+ /** Replay complete content, never a server-side item reference. */
3
+ export declare function responsesBody({ request, route, compacting }: BackendContext): Record<string, any>;
4
+ /** Transform only bridge-owned fields; native events keep their original shape. */
5
+ export declare const runResponses: Backend;
@@ -0,0 +1,205 @@
1
+ import { encodeReasoning } from "../carrier.js";
2
+ import { BridgeError, ResponsesIncompleteError, responsesStreamError } from "../errors.js";
3
+ import { prepareHistory } from "../history.js";
4
+ import { reasoningEffort } from "../profiles.js";
5
+ import { parseJsonSse } from "../sse.js";
6
+ import { upstreamFetch } from "../upstream.js";
7
+ /** Replay complete content, never a server-side item reference. */
8
+ export function responsesBody({ request, route, compacting }) {
9
+ if (request.previous_response_id) {
10
+ throw new BridgeError("previous_response_id is unsupported; send the complete conversation items");
11
+ }
12
+ const input = prepareHistory(request.input, route).map((item) => {
13
+ // An assistant message id can reference a reasoning item that was filtered
14
+ // during a model switch or an agent fork. Full content needs neither id.
15
+ delete item.id;
16
+ if (item.type === "bridge_reasoning") {
17
+ if (typeof item.state.data !== "string")
18
+ throw new BridgeError("Invalid Responses reasoning data.");
19
+ return { type: "reasoning", summary: item.summary, encrypted_content: item.state.data };
20
+ }
21
+ return item;
22
+ });
23
+ const body = {
24
+ ...(route.profile.max_output_tokens ? { max_output_tokens: route.profile.max_output_tokens } : {}),
25
+ ...request,
26
+ reasoning: { ...request.reasoning, effort: reasoningEffort(route, request.reasoning?.effort) },
27
+ ...route.upstream.options,
28
+ ...route.config.options,
29
+ model: route.config.model,
30
+ input,
31
+ tools: request.tools,
32
+ store: false,
33
+ stream: true,
34
+ };
35
+ body.include = [...new Set([...(Array.isArray(body.include) ? body.include : []), "reasoning.encrypted_content"])];
36
+ delete body.previous_response_id;
37
+ if (input.some((item) => item.type === "compaction_trigger")) {
38
+ // Native compaction has its own output budget requirements. An ordinary
39
+ // response limit can be smaller than its provider-enforced minimum.
40
+ delete body.max_output_tokens;
41
+ }
42
+ else if (compacting) {
43
+ delete body.text;
44
+ }
45
+ return body;
46
+ }
47
+ function usageFrom(value) {
48
+ const count = (value, field) => {
49
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
50
+ throw new BridgeError(`Missing or invalid Responses usage field: ${field}`, "upstream_error", 502);
51
+ }
52
+ return value;
53
+ };
54
+ const cached = value?.input_tokens_details?.cached_tokens;
55
+ const reasoning = value?.output_tokens_details?.reasoning_tokens;
56
+ const cacheWrite = value?.input_tokens_details?.cache_write_tokens;
57
+ return {
58
+ input_tokens: count(value?.input_tokens, "input_tokens"),
59
+ output_tokens: count(value?.output_tokens, "output_tokens"),
60
+ ...(cached !== undefined ? { cached_tokens: count(cached, "input_tokens_details.cached_tokens") } : {}),
61
+ ...(cacheWrite !== undefined ? { cache_write_tokens: count(cacheWrite, "input_tokens_details.cache_write_tokens") } : {}),
62
+ ...(reasoning !== undefined ? { reasoning_tokens: count(reasoning, "output_tokens_details.reasoning_tokens") } : {}),
63
+ };
64
+ }
65
+ /** Transform only bridge-owned fields; native events keep their original shape. */
66
+ export const runResponses = async (context, writer) => {
67
+ writer.useNativeResponses();
68
+ const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined);
69
+ context.onAccepted?.(response.headers);
70
+ const states = new Map();
71
+ const phases = new Map();
72
+ let pendingPhase;
73
+ let held = [];
74
+ function itemFor(item, index) {
75
+ const result = { ...item };
76
+ if (item.type === "reasoning" && typeof item.encrypted_content === "string" && item.encrypted_content) {
77
+ result.encrypted_content = encodeReasoning({
78
+ source: context.route.origin, format: context.route.profile.reasoning_format, data: item.encrypted_content,
79
+ });
80
+ }
81
+ if (item.type === "function_call") {
82
+ if (item.encrypted_function_args?.length) {
83
+ throw new BridgeError("Encrypted tool arguments are unsupported; use plaintext client tools");
84
+ }
85
+ result.encrypted_function_args ??= [];
86
+ }
87
+ if (item.type === "message" && item.phase === undefined) {
88
+ const phase = phases.get(index) ?? states.get(index)?.item.phase;
89
+ if (phase !== undefined)
90
+ result.phase = phase;
91
+ }
92
+ return result;
93
+ }
94
+ function responseFor(value) {
95
+ return {
96
+ ...value, model: context.request.model,
97
+ ...(Array.isArray(value.output) ? { output: value.output.map(itemFor) } : {}),
98
+ };
99
+ }
100
+ function releasePhase(phase) {
101
+ if (pendingPhase && phase !== undefined) {
102
+ pendingPhase.item.phase = phase;
103
+ phases.set(pendingPhase.index, phase);
104
+ }
105
+ pendingPhase = undefined;
106
+ const events = held;
107
+ held = [];
108
+ for (const event of events)
109
+ writer.nativeEvent(event);
110
+ }
111
+ function emit(event) {
112
+ if (event.type === "response.output_item.added" || event.type === "response.output_item.done") {
113
+ const index = event.output_index ?? 0;
114
+ if (pendingPhase && index !== pendingPhase.index && event.item.type !== "reasoning")
115
+ releasePhase("commentary");
116
+ if (event.type === "response.output_item.done" && event.item.type === "message" && event.item.phase === undefined) {
117
+ pendingPhase = { index, item: event.item };
118
+ }
119
+ }
120
+ if (pendingPhase)
121
+ held.push(event);
122
+ else
123
+ writer.nativeEvent(event);
124
+ }
125
+ function outputEvent(event) {
126
+ const index = event.output_index ?? 0;
127
+ const item = itemFor(event.item, index);
128
+ const state = states.get(index) ?? { item, added: false, done: false };
129
+ if (event.type === "response.output_item.done" && !state.added) {
130
+ emit({ type: "response.output_item.added", output_index: index, item });
131
+ state.added = true;
132
+ }
133
+ state.item = item;
134
+ state.added ||= event.type === "response.output_item.added";
135
+ state.done ||= event.type === "response.output_item.done";
136
+ states.set(index, state);
137
+ emit({ ...event, item });
138
+ }
139
+ function terminal(event) {
140
+ const result = event.response;
141
+ if (event.type === "response.failed" || result?.status === "failed" || result?.error) {
142
+ releasePhase();
143
+ throw responsesStreamError(responseFor(result), response.headers, event);
144
+ }
145
+ if (event.type === "response.incomplete" || result?.status === "incomplete") {
146
+ releasePhase();
147
+ throw new ResponsesIncompleteError(responseFor(result), event);
148
+ }
149
+ if (result?.status !== "completed") {
150
+ throw new BridgeError("The upstream did not return a completed response", "stream_error", 502);
151
+ }
152
+ const usage = usageFrom(result.usage);
153
+ // A whole JSON response, or a terminal-only stream, still needs item events
154
+ // because codeks persists the history from output_item.done.
155
+ for (const [index, item] of (result.output ?? []).entries()) {
156
+ if (!states.get(index)?.done)
157
+ outputEvent({ type: "response.output_item.done", output_index: index, item });
158
+ }
159
+ for (const state of states.values()) {
160
+ if (!state.done)
161
+ throw new BridgeError("The upstream completed with an unfinished output item", "stream_error", 502);
162
+ }
163
+ const hasCalls = [...states.values()].some(({ item }) => ["function_call", "custom_tool_call"].includes(item.type));
164
+ const endTurn = result.end_turn ?? !hasCalls;
165
+ if (pendingPhase) {
166
+ releasePhase(result.output?.[pendingPhase.index]?.phase ?? (endTurn ? "final_answer" : "commentary"));
167
+ }
168
+ writer.nativeEvent({ ...event, response: { ...responseFor(result), end_turn: endTurn } });
169
+ return { usage, endTurn };
170
+ }
171
+ if (response.headers.get("content-type")?.includes("application/json")) {
172
+ const result = await response.json();
173
+ if (result.status === "completed" || result.status === "incomplete") {
174
+ writer.nativeEvent({ type: "response.created", response: { ...result, model: context.request.model, status: "in_progress", output: [] } });
175
+ }
176
+ if (result.status === "incomplete") {
177
+ // An interrupted whole response has no earlier item events. Publish its
178
+ // partial history before the terminal event, still withholding any tool suffix.
179
+ for (const [index, item] of (result.output ?? []).entries()) {
180
+ outputEvent({ type: "response.output_item.done", output_index: index, item });
181
+ }
182
+ }
183
+ return terminal({ type: result.status === "incomplete" ? "response.incomplete" : "response.completed", response: result });
184
+ }
185
+ if (!response.body)
186
+ throw new BridgeError("The upstream returned an empty response", "stream_error", 502);
187
+ for await (const { json: event } of parseJsonSse(response.body)) {
188
+ switch (event.type) {
189
+ case "response.output_item.added":
190
+ case "response.output_item.done":
191
+ outputEvent(event);
192
+ break;
193
+ case "response.completed":
194
+ case "response.failed":
195
+ case "response.incomplete":
196
+ return terminal(event);
197
+ case "error":
198
+ releasePhase();
199
+ throw responsesStreamError(event.error ?? event, response.headers, event);
200
+ default:
201
+ emit(event.response ? { ...event, response: responseFor(event.response) } : event);
202
+ }
203
+ }
204
+ throw new BridgeError("The upstream stream ended before response.completed", "stream_error", 502);
205
+ };
@@ -0,0 +1,22 @@
1
+ import type { ResponsesRequest, StateOrigin } from "./types.ts";
2
+ export interface HandoffRecipe {
3
+ prefix: Record<string, any>[];
4
+ parameters: ResponsesRequest;
5
+ responsesLite: boolean;
6
+ }
7
+ export interface StatePayload {
8
+ source: StateOrigin;
9
+ format: string;
10
+ data: unknown;
11
+ }
12
+ export interface CompactionCarrier {
13
+ /** The prompt-based summary. Any model can read it. */
14
+ summary?: string;
15
+ native?: StatePayload;
16
+ /** Source request information needed to materialize a deferred summary. */
17
+ handoff?: HandoffRecipe;
18
+ }
19
+ export declare function encodeReasoning(state: StatePayload): string;
20
+ export declare function decodeReasoning(value: unknown): StatePayload | undefined;
21
+ export declare function encodeCompaction(carrier: CompactionCarrier): string;
22
+ export declare function decodeCompaction(value: unknown): CompactionCarrier | undefined;
@@ -0,0 +1,61 @@
1
+ import { BridgeError } from "./errors.js";
2
+ // Versioned, self-contained state stored in the client's encrypted_content field.
3
+ // These envelopes encode source facts and payloads, never replay policy.
4
+ const REASONING_PREFIX = "cxbr1:";
5
+ const COMPACTION_PREFIX = "cxbc1:";
6
+ function object(value) {
7
+ return value !== null && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ function statePayload(value) {
10
+ return object(value) && object(value.source) &&
11
+ [value.source.id, value.source.connection, value.source.model, value.source.adapter, value.format]
12
+ .every((field) => typeof field === "string" && field.length > 0) &&
13
+ Object.hasOwn(value, "data") && value.data !== undefined;
14
+ }
15
+ function compactionCarrier(value) {
16
+ if (!object(value) || (value.native !== undefined && !statePayload(value.native)))
17
+ return false;
18
+ if (value.summary !== undefined && (typeof value.summary !== "string" || !value.summary.trim()))
19
+ return false;
20
+ if (value.handoff !== undefined && (!object(value.handoff) ||
21
+ !Array.isArray(value.handoff.prefix) || !value.handoff.prefix.every(object) ||
22
+ !object(value.handoff.parameters) || typeof value.handoff.parameters.model !== "string" ||
23
+ typeof value.handoff.responsesLite !== "boolean"))
24
+ return false;
25
+ return value.summary !== undefined || (value.native !== undefined && value.handoff !== undefined);
26
+ }
27
+ function encode(prefix, value) {
28
+ return prefix + Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
29
+ }
30
+ function decode(value, prefix) {
31
+ if (typeof value !== "string" || !value.startsWith(prefix))
32
+ return undefined;
33
+ try {
34
+ return JSON.parse(Buffer.from(value.slice(prefix.length), "base64url").toString("utf8"));
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ export function encodeReasoning(state) {
41
+ if (!statePayload(state))
42
+ throw new TypeError("Invalid reasoning state envelope");
43
+ return encode(REASONING_PREFIX, state);
44
+ }
45
+ export function decodeReasoning(value) {
46
+ if (typeof value !== "string" || !value.startsWith(REASONING_PREFIX))
47
+ return undefined;
48
+ const decoded = decode(value, REASONING_PREFIX);
49
+ if (!statePayload(decoded))
50
+ throw new BridgeError("Malformed bridge reasoning state envelope");
51
+ return decoded;
52
+ }
53
+ export function encodeCompaction(carrier) {
54
+ if (!compactionCarrier(carrier))
55
+ throw new TypeError("Invalid compaction state envelope");
56
+ return encode(COMPACTION_PREFIX, carrier);
57
+ }
58
+ export function decodeCompaction(value) {
59
+ const decoded = decode(value, COMPACTION_PREFIX);
60
+ return compactionCarrier(decoded) ? decoded : undefined;
61
+ }
@@ -0,0 +1,8 @@
1
+ import type { BridgeConfig } from "./types.ts";
2
+ /** Codex bounds downloads from an explicit model_catalog_url to 1 MiB. */
3
+ export declare const MAX_HTTP_CATALOG_BYTES: number;
4
+ /** Serve client-exposed aliases, using their own reviewed client behavior. */
5
+ export declare function buildCatalog(config: BridgeConfig): {
6
+ models: Record<string, any>[];
7
+ };
8
+ export declare function configSnippet(config: BridgeConfig, catalogPath?: string): string;
@@ -0,0 +1,40 @@
1
+ import { modelProfile } from "./profiles.js";
2
+ import { BridgeError } from "./errors.js";
3
+ /** Codex bounds downloads from an explicit model_catalog_url to 1 MiB. */
4
+ export const MAX_HTTP_CATALOG_BYTES = 1024 * 1024;
5
+ function exposedModels(config) {
6
+ return Object.entries(config.models).filter(([, model]) => model.expose !== false);
7
+ }
8
+ /** Serve client-exposed aliases, using their own reviewed client behavior. */
9
+ export function buildCatalog(config) {
10
+ return { models: exposedModels(config).map(([slug, model], priority) => ({
11
+ ...modelProfile(model).model_info,
12
+ slug, display_name: slug, priority, visibility: "list", supported_in_api: true,
13
+ })) };
14
+ }
15
+ export function configSnippet(config, catalogPath) {
16
+ const first = exposedModels(config)[0]?.[0];
17
+ if (first === undefined)
18
+ throw new BridgeError("config-snippet requires at least one model with expose enabled");
19
+ const host = ["0.0.0.0", "::"].includes(config.host ?? "") ? "127.0.0.1" : config.host ?? "127.0.0.1";
20
+ const authority = host.includes(":") ? `[${host}]` : host;
21
+ const baseUrl = `http://${authority}:${config.port ?? 8787}/v1`;
22
+ const lines = [
23
+ `model = ${JSON.stringify(first)}`, 'model_provider = "bridge"',
24
+ ...(catalogPath ? [`model_catalog_json = ${JSON.stringify(catalogPath)}`] : []),
25
+ 'web_search = "disabled"', "", "[features]", "image_generation = false", "",
26
+ // Outside the reserved "collaboration" namespace, codeks sends multi-agent task messages as plaintext.
27
+ "[features.multi_agent_v2]", 'tool_namespace = "agents"', "",
28
+ "[model_providers.bridge]", 'name = "Local bridge"',
29
+ `base_url = ${JSON.stringify(baseUrl)}`, 'wire_api = "responses"',
30
+ ];
31
+ if (!catalogPath)
32
+ lines.push(`model_catalog_url = ${JSON.stringify(`${baseUrl}/models`)}`);
33
+ if (config.token_env)
34
+ lines.push(`env_key = ${JSON.stringify(config.token_env)}`);
35
+ // Codex requires API-key auth mode for provider catalog discovery, even on loopback.
36
+ else if (!catalogPath)
37
+ lines.push('experimental_bearer_token = "codeks-bridge-local"');
38
+ lines.push("", "[model_providers.bridge.capabilities]", 'remote_compaction = "v2"', "");
39
+ return lines.join("\n");
40
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { loadConfig } from "./config.js";
6
+ import { createBridgeServer } from "./server.js";
7
+ import { buildCatalog, configSnippet } from "./catalog.js";
8
+ const HELP = `Usage: codeks-bridge <command> --config <file> [options]
9
+
10
+ Commands:
11
+ serve Start the local Responses API server
12
+ catalog Export the client-visible model catalog for codeks
13
+ config-snippet Print the codeks provider configuration
14
+
15
+ Options:
16
+ --output <file> Write catalog JSON (otherwise stdout)
17
+ --catalog <file> Use a static catalog instead of HTTP discovery
18
+ --capture Start serve with inspector capture on
19
+ --help Show this help
20
+ `;
21
+ async function main() {
22
+ const { values, positionals } = parseArgs({ allowPositionals: true, options: {
23
+ config: { type: "string" }, output: { type: "string" },
24
+ catalog: { type: "string" }, capture: { type: "boolean" }, help: { type: "boolean" },
25
+ } });
26
+ if (values.help || positionals.length === 0) {
27
+ process.stdout.write(HELP);
28
+ return;
29
+ }
30
+ const command = positionals[0];
31
+ if (positionals.length !== 1 || !["serve", "catalog", "config-snippet"].includes(command))
32
+ throw new Error("Unknown command. Use --help.");
33
+ if (!values.config)
34
+ throw new Error("--config is required");
35
+ const config = await loadConfig(values.config);
36
+ if (command === "catalog") {
37
+ const text = JSON.stringify(buildCatalog(config), null, 2) + "\n";
38
+ if (values.output)
39
+ await writeFile(values.output, text, { mode: 0o600 });
40
+ else
41
+ process.stdout.write(text);
42
+ }
43
+ else if (command === "config-snippet") {
44
+ process.stdout.write(configSnippet(config, values.catalog ? resolve(values.catalog) : undefined));
45
+ }
46
+ else {
47
+ if (values.capture)
48
+ config.inspect = { ...config.inspect, capture: true };
49
+ const server = createBridgeServer(config);
50
+ server.on("error", error => { process.stderr.write(`Server error: ${error.message}\n`); process.exitCode = 1; });
51
+ server.listen(config.port ?? 8787, config.host ?? "127.0.0.1", () => {
52
+ const address = server.address();
53
+ const port = typeof address === "object" ? address?.port : config.port;
54
+ const host = config.host ?? "127.0.0.1";
55
+ const shown = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
56
+ process.stderr.write(`Listening on ${host}:${port}\n`);
57
+ process.stderr.write(`Inspector: http://${shown}:${port}/inspect/ (capture ${config.inspect?.capture ? "on" : "off"})\n`);
58
+ });
59
+ const stop = () => { server.close(); server.closeAllConnections(); };
60
+ process.once("SIGINT", stop);
61
+ process.once("SIGTERM", stop);
62
+ }
63
+ }
64
+ main().catch(error => { process.stderr.write(`${error instanceof Error ? error.message : "Command failed"}\n`); process.exitCode = 1; });
@@ -0,0 +1,13 @@
1
+ import type { AdapterId, Backend, BackendContext, BridgeConfig, ResponsesRequest } from "./types.ts";
2
+ /** Successful exports survive restarts; each caller owns only its own cancellation. */
3
+ export declare class CompactionHandoffStore {
4
+ readonly directory: string;
5
+ private readonly config;
6
+ private readonly backends;
7
+ private readonly pending;
8
+ constructor(config: BridgeConfig, backends: Record<AdapterId, Backend>);
9
+ private source;
10
+ private summary;
11
+ /** Only checkpoint predecessors enter the export; later work stays on the target. */
12
+ materialize(context: BackendContext): Promise<ResponsesRequest>;
13
+ }
@@ -0,0 +1,126 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { decodeCompaction, encodeCompaction } from "./carrier.js";
6
+ import { collectHandoff, handoffRequest } from "./compaction.js";
7
+ import { splitCompactionPrefix } from "./compaction-history.js";
8
+ import { resolveModel } from "./config.js";
9
+ import { BridgeError } from "./errors.js";
10
+ import { restoresNativeCompaction } from "./history.js";
11
+ function waitForTurn(previous, signal) {
12
+ signal.throwIfAborted();
13
+ return new Promise((resolve, reject) => {
14
+ const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
15
+ signal.addEventListener("abort", abort, { once: true });
16
+ previous.then(() => {
17
+ signal.removeEventListener("abort", abort);
18
+ if (signal.aborted)
19
+ reject(signal.reason);
20
+ else
21
+ resolve();
22
+ });
23
+ });
24
+ }
25
+ /** Successful exports survive restarts; each caller owns only its own cancellation. */
26
+ export class CompactionHandoffStore {
27
+ directory;
28
+ config;
29
+ backends;
30
+ pending = new Map();
31
+ constructor(config, backends) {
32
+ this.config = config;
33
+ this.backends = backends;
34
+ this.directory = config.compaction_cache_dir ?? join(process.env.XDG_CACHE_HOME || join(homedir(), ".cache"), "codeks-bridge", "compaction");
35
+ }
36
+ source(carrier) {
37
+ const preferred = carrier.handoff.parameters.model;
38
+ const aliases = [...new Set([preferred, ...Object.keys(this.config.models)])];
39
+ for (const alias of aliases) {
40
+ if (!Object.hasOwn(this.config.models, alias))
41
+ continue;
42
+ const route = resolveModel(this.config, alias);
43
+ if (route.origin.id === carrier.native.source.id)
44
+ return route;
45
+ }
46
+ throw new BridgeError("The original compaction source is not configured; restore its model and upstream to generate a handoff.");
47
+ }
48
+ async summary(carrier, request, context) {
49
+ const { signal } = context;
50
+ const key = createHash("sha256").update(JSON.stringify({
51
+ source: carrier.native.source,
52
+ request,
53
+ responsesLite: carrier.handoff.responsesLite,
54
+ })).digest("hex");
55
+ const previous = this.pending.get(key) ?? Promise.resolve();
56
+ let release;
57
+ const gate = new Promise(resolve => { release = resolve; });
58
+ const tail = previous.then(() => gate);
59
+ this.pending.set(key, tail);
60
+ void tail.then(() => { if (this.pending.get(key) === tail)
61
+ this.pending.delete(key); });
62
+ try {
63
+ await waitForTurn(previous, signal);
64
+ const path = join(this.directory, `${key}.json`);
65
+ try {
66
+ const cached = JSON.parse(await readFile(path, "utf8"));
67
+ if (typeof cached.summary !== "string" || !cached.summary.trim())
68
+ throw new BridgeError("Invalid cached compaction handoff.");
69
+ signal.throwIfAborted();
70
+ return cached.summary;
71
+ }
72
+ catch (error) {
73
+ if (error.code !== "ENOENT")
74
+ throw error;
75
+ }
76
+ signal.throwIfAborted();
77
+ const route = this.source(carrier);
78
+ const result = await collectHandoff({
79
+ request: { ...request, model: route.slug }, route, signal,
80
+ requestHeaders: context.requestHeaders,
81
+ responsesLite: carrier.handoff.responsesLite,
82
+ }, this.backends[route.profile.id]);
83
+ signal.throwIfAborted();
84
+ await mkdir(this.directory, { recursive: true, mode: 0o700 });
85
+ const temporary = join(this.directory, `.${key}.${randomUUID()}.tmp`);
86
+ try {
87
+ // Usage belongs to this export, never to the destination model's context.
88
+ await writeFile(temporary, JSON.stringify(result), { mode: 0o600, flag: "wx" });
89
+ await rename(temporary, path);
90
+ }
91
+ finally {
92
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT")
93
+ throw error; });
94
+ }
95
+ signal.throwIfAborted();
96
+ return result.summary;
97
+ }
98
+ finally {
99
+ release();
100
+ }
101
+ }
102
+ /** Only checkpoint predecessors enter the export; later work stays on the target. */
103
+ async materialize(context) {
104
+ if (!Array.isArray(context.request.input))
105
+ return context.request;
106
+ let changed = false;
107
+ const input = context.request.input;
108
+ const prepared = [...input];
109
+ for (let i = 0; i < input.length; i++) {
110
+ const item = input[i];
111
+ if (item.type !== "compaction" && item.type !== "compaction_summary")
112
+ continue;
113
+ const carrier = decodeCompaction(item.encrypted_content);
114
+ if (!carrier || carrier.summary !== undefined || restoresNativeCompaction(carrier, context.route))
115
+ continue;
116
+ if (!carrier.native || !carrier.handoff)
117
+ throw new BridgeError("This compaction checkpoint has no handoff recipe.");
118
+ const { history } = splitCompactionPrefix(input.slice(0, i));
119
+ const request = handoffRequest(carrier.handoff, history, item);
120
+ const summary = await this.summary(carrier, request, context);
121
+ prepared[i] = { ...item, encrypted_content: encodeCompaction({ ...carrier, summary }) };
122
+ changed = true;
123
+ }
124
+ return changed ? { ...context.request, input: prepared } : context.request;
125
+ }
126
+ }
@@ -0,0 +1,7 @@
1
+ type Item = Record<string, any>;
2
+ /** Save source instructions and Lite declarations, without saving old conversation turns. */
3
+ export declare function splitCompactionPrefix(input: readonly Item[]): {
4
+ prefix: Item[];
5
+ history: Item[];
6
+ };
7
+ export {};
@@ -0,0 +1,20 @@
1
+ const isMessage = (item) => item.type === "message" || (!item.type && typeof item.role === "string");
2
+ /** Save source instructions and Lite declarations, without saving old conversation turns. */
3
+ export function splitCompactionPrefix(input) {
4
+ const prefix = [];
5
+ const history = [];
6
+ let leading = true;
7
+ for (const item of input) {
8
+ if (item.type === "additional_tools") {
9
+ prefix.push(item);
10
+ }
11
+ else if (leading && isMessage(item) && ["system", "developer"].includes(item.role)) {
12
+ prefix.push(item);
13
+ }
14
+ else {
15
+ leading = false;
16
+ history.push(item);
17
+ }
18
+ }
19
+ return { prefix, history };
20
+ }
@@ -0,0 +1,20 @@
1
+ import type { HandoffRecipe } from "./carrier.ts";
2
+ import type { Backend, BackendContext, ResponsesRequest } from "./types.ts";
3
+ import type { Usage } from "./writer.ts";
4
+ type Item = Record<string, any>;
5
+ export interface CompactionResult {
6
+ item: Record<string, unknown>;
7
+ usage: Usage;
8
+ }
9
+ export declare function isCompactionRequest(request: ResponsesRequest): boolean;
10
+ /** Keep the shared prefix intact, including tools and any signed reasoning. */
11
+ export declare function summaryRequest(request: ResponsesRequest): ResponsesRequest;
12
+ /** Rephrase the checkpoint using the exact history retained by the client. */
13
+ export declare function handoffRequest(recipe: HandoffRecipe, retained: Item[], checkpoint: Item): ResponsesRequest;
14
+ export declare function collectHandoff(context: BackendContext, backend: Backend): Promise<{
15
+ summary: string;
16
+ usage: Usage;
17
+ }>;
18
+ /** Publish a complete native checkpoint or portable text summary. */
19
+ export declare function compact(context: BackendContext, backend: Backend): Promise<CompactionResult>;
20
+ export {};