@wacry/codeks-bridge 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/backends/anthropic.js +6 -2
- package/dist/backends/chat.js +15 -4
- package/dist/backends/responses.js +5 -3
- package/dist/carrier.js +1 -6
- package/dist/compaction-handoff.js +3 -1
- package/dist/config.js +10 -2
- package/dist/history.d.ts +4 -0
- package/dist/history.js +31 -3
- package/dist/index.d.ts +1 -1
- package/dist/inspect/view.d.ts +2 -1
- package/dist/inspect/view.js +14 -17
- package/dist/server.d.ts +2 -2
- package/dist/server.js +15 -4
- package/dist/types.d.ts +17 -1
- package/dist/upstream.d.ts +1 -1
- package/dist/upstream.js +13 -7
- package/dist/writer.d.ts +2 -0
- package/dist/writer.js +14 -3
- package/docs/backends-responses.md +1 -1
- package/docs/compaction.md +4 -2
- package/docs/config.md +24 -3
- package/docs/design.md +3 -1
- package/docs/embedding.md +37 -0
- package/docs/inspect.md +2 -4
- package/docs/reasoning.md +3 -3
- package/docs/releasing.md +1 -1
- package/docs/responses-stream.md +1 -1
- package/docs/testing.md +3 -0
- package/package.json +2 -2
- package/ui/labels.js +5 -7
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 安装和使用
|
|
8
8
|
|
|
9
|
-
需要 Node.js ≥
|
|
9
|
+
需要 Node.js ≥ 20.19 和已安装的 codeks。
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
npm install -g @wacry/codeks-bridge
|
|
@@ -29,7 +29,7 @@ codeks-bridge config-snippet --config /absolute/path/bridge.json
|
|
|
29
29
|
## 使用边界
|
|
30
30
|
|
|
31
31
|
- 上游错误保留原始详情交给客户端;bridge 不自动换端点、重试或更换压缩方式。
|
|
32
|
-
-
|
|
32
|
+
- bridge 封装的 reasoning 默认只回放给原目标,原生压缩与 reasoning 的跨目标复用分别配置。无法识别的历史状态默认透传,也可配置[过滤或报错](docs/config.md#未识别的历史状态);实际兼容性由上游决定。
|
|
33
33
|
- GPT 默认使用 `compaction.mode: "native"`:先做原生加密压缩,需要文字表示时再向原来源复述并缓存。其他模型默认使用标准文字压缩,也可通过 [`compaction.model`](docs/config.md#压缩模式与执行模型) 委托常用 GPT 执行原生压缩,直接复用已有模型配置;见 [GPT+Claude 双模型示例](examples/gpt-claude.json)。
|
|
34
34
|
- 历史由 codeks 保存;按需复述的成功结果另存本地缓存。封装只是编码,文字 reasoning、摘要及复述配方可读。
|
|
35
35
|
- Lark/regex 转为工具描述约束。图片理解由模型支持,客户端配置关闭网页搜索和图片生成。
|
|
@@ -38,4 +38,4 @@ codeks-bridge config-snippet --config /absolute/path/bridge.json
|
|
|
38
38
|
|
|
39
39
|
`serve` 加 `--capture` 后打开日志里的 `/inspect/` 地址,可以逐条查看 codeks 请求、发往上游的请求、上游原始响应和写回 codeks 的响应,以及 reasoning 与压缩状态的回放决定。记录只在内存中,也可以在页面上开关捕获、清空或导出。详见[调试检查器](docs/inspect.md)。
|
|
40
40
|
|
|
41
|
-
[配置与模型目录](docs/config.md) · [整体设计](docs/design.md) · [Reasoning](docs/reasoning.md) · [压缩](docs/compaction.md) · [工具](docs/tools.md) · [调试检查器](docs/inspect.md) · [验证](docs/testing.md) · [发布](docs/releasing.md)
|
|
41
|
+
[配置与模型目录](docs/config.md) · [嵌入现有服务](docs/embedding.md) · [整体设计](docs/design.md) · [Reasoning](docs/reasoning.md) · [压缩](docs/compaction.md) · [工具](docs/tools.md) · [调试检查器](docs/inspect.md) · [验证](docs/testing.md) · [发布](docs/releasing.md)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { encodeReasoning } from "../carrier.js";
|
|
2
2
|
import { resolveClaudeOptions } from "../adapter-options.js";
|
|
3
3
|
import { BridgeError, upstreamError } from "../errors.js";
|
|
4
|
-
import { prepareHistory } from "../history.js";
|
|
4
|
+
import { isOpaqueStateItem, prepareHistory } from "../history.js";
|
|
5
5
|
import { reasoningEffort, reasoningSummaryRequested } from "../profiles.js";
|
|
6
6
|
import { parseJsonSse } from "../sse.js";
|
|
7
7
|
import { safeCallId } from "../tools.js";
|
|
@@ -114,6 +114,10 @@ export function buildAnthropicRequest(context) {
|
|
|
114
114
|
type: "tool_addition", tool: { type: "tool_definition", definition },
|
|
115
115
|
})));
|
|
116
116
|
}
|
|
117
|
+
else if (isOpaqueStateItem(item)) {
|
|
118
|
+
// Keep unknown state opaque. The endpoint decides whether it accepts it.
|
|
119
|
+
append("assistant", [item]);
|
|
120
|
+
}
|
|
117
121
|
else if (item.type === "bridge_reasoning") {
|
|
118
122
|
const block = item.state.data;
|
|
119
123
|
if (!block || !["thinking", "redacted_thinking"].includes(block.type)) {
|
|
@@ -249,7 +253,7 @@ function validateUsage(value, required) {
|
|
|
249
253
|
}
|
|
250
254
|
export const runAnthropic = async (context, writer) => {
|
|
251
255
|
const { body, tools, betas, continuationBefore } = buildAnthropicRequest(context);
|
|
252
|
-
const response = await upstreamFetch(context.route, "/messages", body, context.signal, context.requestHeaders, { "anthropic-beta": betas });
|
|
256
|
+
const response = await upstreamFetch(context.route, "/messages", body, context.signal, context.requestHeaders, { "anthropic-beta": betas }, context.fetch);
|
|
253
257
|
context.onAccepted?.(response.headers);
|
|
254
258
|
if (!response.body)
|
|
255
259
|
throw new BridgeError("Messages returned an empty stream.", "upstream_error", 502);
|
package/dist/backends/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { encodeReasoning } from "../carrier.js";
|
|
2
2
|
import { BridgeError, transportError, upstreamError } from "../errors.js";
|
|
3
|
-
import { prepareHistory } from "../history.js";
|
|
3
|
+
import { isOpaqueStateItem, prepareHistory } from "../history.js";
|
|
4
4
|
import { parseJsonSse } from "../sse.js";
|
|
5
5
|
import { buildToolSet, normalizeToolDeclarations } from "../tools.js";
|
|
6
6
|
import { upstreamFetch } from "../upstream.js";
|
|
@@ -48,7 +48,15 @@ function convertHistory(context, tools, reasoningField) {
|
|
|
48
48
|
if (context.request.instructions)
|
|
49
49
|
messages.push({ role: "system", content: context.request.instructions });
|
|
50
50
|
for (const item of prepareHistory(context.request.input, context.route)) {
|
|
51
|
-
if (item
|
|
51
|
+
if (isOpaqueStateItem(item)) {
|
|
52
|
+
flushImages();
|
|
53
|
+
// Do not invent a text or reasoning_content interpretation for opaque data.
|
|
54
|
+
const a = getAssistant();
|
|
55
|
+
if (typeof a.content === "string")
|
|
56
|
+
a.content = a.content ? [{ type: "text", text: a.content }] : [];
|
|
57
|
+
a.content.push(item);
|
|
58
|
+
}
|
|
59
|
+
else if (item.type === "bridge_reasoning") {
|
|
52
60
|
flushImages();
|
|
53
61
|
// A reasoning item starts the next assistant response, even if the
|
|
54
62
|
// previous response ended without a tool or user message.
|
|
@@ -65,7 +73,10 @@ function convertHistory(context, tools, reasoningField) {
|
|
|
65
73
|
throw new BridgeError("Chat assistant history cannot contain images.");
|
|
66
74
|
const a = getAssistant();
|
|
67
75
|
const text = messageContent(parts);
|
|
68
|
-
|
|
76
|
+
if (Array.isArray(a.content))
|
|
77
|
+
a.content.push(...parts);
|
|
78
|
+
else
|
|
79
|
+
a.content += (a.content && text ? "\n" : "") + text;
|
|
69
80
|
}
|
|
70
81
|
else {
|
|
71
82
|
flushAssistant();
|
|
@@ -184,7 +195,7 @@ async function* chatEvents(body, signal) {
|
|
|
184
195
|
export async function runChat(context, writer, dialect) {
|
|
185
196
|
const { body, tools } = buildChatRequest(context, dialect);
|
|
186
197
|
const { reasoningField } = dialect;
|
|
187
|
-
const response = await upstreamFetch(context.route, "chat/completions", body, context.signal, context.requestHeaders);
|
|
198
|
+
const response = await upstreamFetch(context.route, "chat/completions", body, context.signal, context.requestHeaders, undefined, context.fetch);
|
|
188
199
|
context.onAccepted?.(response.headers);
|
|
189
200
|
if (!response.body)
|
|
190
201
|
throw new BridgeError("Chat upstream returned no stream.", "upstream_error", 502);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { encodeReasoning } from "../carrier.js";
|
|
2
2
|
import { BridgeError, ResponsesIncompleteError, responsesStreamError } from "../errors.js";
|
|
3
|
-
import { prepareHistory } from "../history.js";
|
|
3
|
+
import { isOpaqueStateItem, prepareHistory } from "../history.js";
|
|
4
4
|
import { reasoningEffort } from "../profiles.js";
|
|
5
5
|
import { parseJsonSse } from "../sse.js";
|
|
6
6
|
import { upstreamFetch } from "../upstream.js";
|
|
@@ -12,7 +12,9 @@ export function responsesBody({ request, route, compacting }) {
|
|
|
12
12
|
const input = prepareHistory(request.input, route).map((item) => {
|
|
13
13
|
// An assistant message id can reference a reasoning item that was filtered
|
|
14
14
|
// during a model switch or an agent fork. Full content needs neither id.
|
|
15
|
-
|
|
15
|
+
// Unrecognized state has no bridge provenance: forward its complete item.
|
|
16
|
+
if (!isOpaqueStateItem(item))
|
|
17
|
+
delete item.id;
|
|
16
18
|
if (item.type === "bridge_reasoning") {
|
|
17
19
|
if (typeof item.state.data !== "string")
|
|
18
20
|
throw new BridgeError("Invalid Responses reasoning data.");
|
|
@@ -90,7 +92,7 @@ function completedUsage(response) {
|
|
|
90
92
|
/** Transform only bridge-owned fields; native events keep their original shape. */
|
|
91
93
|
export const runResponses = async (context, writer) => {
|
|
92
94
|
writer.useNativeResponses();
|
|
93
|
-
const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined);
|
|
95
|
+
const response = await upstreamFetch(context.route, "/responses", responsesBody(context), context.signal, context.requestHeaders, context.responsesLite ? { "x-openai-internal-codex-responses-lite": "true" } : undefined, context.fetch);
|
|
94
96
|
context.onAccepted?.(response.headers);
|
|
95
97
|
const states = new Map();
|
|
96
98
|
const phases = new Map();
|
package/dist/carrier.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { BridgeError } from "./errors.js";
|
|
2
1
|
// Versioned, self-contained state stored in the client's encrypted_content field.
|
|
3
2
|
// These envelopes encode source facts and payloads, never replay policy.
|
|
4
3
|
const REASONING_PREFIX = "cxbr1:";
|
|
@@ -43,12 +42,8 @@ export function encodeReasoning(state) {
|
|
|
43
42
|
return encode(REASONING_PREFIX, state);
|
|
44
43
|
}
|
|
45
44
|
export function decodeReasoning(value) {
|
|
46
|
-
if (typeof value !== "string" || !value.startsWith(REASONING_PREFIX))
|
|
47
|
-
return undefined;
|
|
48
45
|
const decoded = decode(value, REASONING_PREFIX);
|
|
49
|
-
|
|
50
|
-
throw new BridgeError("Malformed bridge reasoning state envelope");
|
|
51
|
-
return decoded;
|
|
46
|
+
return statePayload(decoded) ? decoded : undefined;
|
|
52
47
|
}
|
|
53
48
|
export function encodeCompaction(carrier) {
|
|
54
49
|
if (!compactionCarrier(carrier))
|
|
@@ -7,7 +7,7 @@ import { collectHandoff, handoffRequest } from "./compaction.js";
|
|
|
7
7
|
import { splitCompactionPrefix } from "./compaction-history.js";
|
|
8
8
|
import { resolveModel } from "./config.js";
|
|
9
9
|
import { BridgeError } from "./errors.js";
|
|
10
|
-
import { restoresNativeCompaction } from "./history.js";
|
|
10
|
+
import { rejectUnrecognizedState, restoresNativeCompaction } from "./history.js";
|
|
11
11
|
function waitForTurn(previous, signal) {
|
|
12
12
|
signal.throwIfAborted();
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
@@ -78,6 +78,7 @@ export class CompactionHandoffStore {
|
|
|
78
78
|
const result = await collectHandoff({
|
|
79
79
|
request: { ...request, model: route.slug }, route, signal,
|
|
80
80
|
requestHeaders: context.requestHeaders,
|
|
81
|
+
fetch: context.fetch,
|
|
81
82
|
responsesLite: carrier.handoff.responsesLite,
|
|
82
83
|
}, this.backends[route.profile.id]);
|
|
83
84
|
signal.throwIfAborted();
|
|
@@ -101,6 +102,7 @@ export class CompactionHandoffStore {
|
|
|
101
102
|
}
|
|
102
103
|
/** Only checkpoint predecessors enter the export; later work stays on the target. */
|
|
103
104
|
async materialize(context) {
|
|
105
|
+
rejectUnrecognizedState(context.request.input, context.route);
|
|
104
106
|
if (!Array.isArray(context.request.input))
|
|
105
107
|
return context.request;
|
|
106
108
|
let changed = false;
|
package/dist/config.js
CHANGED
|
@@ -60,8 +60,10 @@ export function validateConfig(value) {
|
|
|
60
60
|
fail(`upstream ${name}: choose api_key_env or api_key_command`);
|
|
61
61
|
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")))
|
|
62
62
|
fail(`upstream ${name}: api_key_command must be a nonempty argv array`);
|
|
63
|
-
if (up.stream_idle_timeout_ms !== undefined && (!Number.isSafeInteger(up.stream_idle_timeout_ms) || up.stream_idle_timeout_ms <= 0))
|
|
63
|
+
if (up.stream_idle_timeout_ms !== undefined && up.stream_idle_timeout_ms !== null && (!Number.isSafeInteger(up.stream_idle_timeout_ms) || up.stream_idle_timeout_ms <= 0))
|
|
64
64
|
fail(`upstream ${name}: invalid stream_idle_timeout_ms`);
|
|
65
|
+
if (up.headers_timeout_ms !== undefined && up.headers_timeout_ms !== null && (!Number.isSafeInteger(up.headers_timeout_ms) || up.headers_timeout_ms <= 0))
|
|
66
|
+
fail(`upstream ${name}: invalid headers_timeout_ms`);
|
|
65
67
|
if (up.auth_header !== undefined && (typeof up.auth_header !== "string" || !/^[\w-]+$/.test(up.auth_header)))
|
|
66
68
|
fail(`upstream ${name}: invalid auth_header`);
|
|
67
69
|
if (up.headers !== undefined && (!object(up.headers) || Object.values(up.headers).some(v => typeof v !== "string" || /[\r\n]/.test(v))))
|
|
@@ -103,6 +105,9 @@ export function validateConfig(value) {
|
|
|
103
105
|
if (model.replay !== undefined) {
|
|
104
106
|
if (!object(model.replay))
|
|
105
107
|
fail(`model ${slug}: replay must be an object`);
|
|
108
|
+
if (model.replay.unrecognized !== undefined && !["passthrough", "drop", "error"].includes(model.replay.unrecognized)) {
|
|
109
|
+
fail(`model ${slug}: replay.unrecognized must be passthrough, drop, or error`);
|
|
110
|
+
}
|
|
106
111
|
for (const field of ["reasoning_from", "native_compaction_from"]) {
|
|
107
112
|
if (model.replay[field] !== undefined && !strings(model.replay[field]))
|
|
108
113
|
fail(`model ${slug}: ${field} must list source model aliases`);
|
|
@@ -155,7 +160,10 @@ export function resolveModel(config, slug) {
|
|
|
155
160
|
const allowed = (names = []) => new Set([source.id, ...names.map(name => origin(config, config.models[name]).id)]);
|
|
156
161
|
return {
|
|
157
162
|
slug, config: model, upstream: config.upstreams[model.upstream], profile, origin: source,
|
|
158
|
-
replay: {
|
|
163
|
+
replay: {
|
|
164
|
+
reasoning: allowed(model.replay?.reasoning_from), native_compaction: allowed(model.replay?.native_compaction_from),
|
|
165
|
+
unrecognized: model.replay?.unrecognized ?? "passthrough",
|
|
166
|
+
},
|
|
159
167
|
compaction: {
|
|
160
168
|
mode: model.compaction?.mode ?? (profile.native_compaction_format ? "native" : "summary"),
|
|
161
169
|
native_replay: model.compaction?.native_replay ?? !!profile.native_compaction_format,
|
package/dist/history.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type CompactionCarrier, type StatePayload } from "./carrier.ts";
|
|
2
2
|
import type { ResolvedModel, ResponsesRequest } from "./types.ts";
|
|
3
|
+
/** Raw Responses state items; decoded bridge reasoning uses an internal type. */
|
|
4
|
+
export declare function isOpaqueStateItem(item: Record<string, any>): boolean;
|
|
5
|
+
/** Reject explicit policy violations before even an auxiliary handoff can run. */
|
|
6
|
+
export declare function rejectUnrecognizedState(input: ResponsesRequest["input"], route: ResolvedModel): void;
|
|
3
7
|
/** Replay policy for decoded state. The inspector reports the same decisions. */
|
|
4
8
|
export declare function replaysReasoning(carrier: StatePayload, route: ResolvedModel): boolean;
|
|
5
9
|
export declare function restoresNativeCompaction(carrier: CompactionCarrier, route: ResolvedModel): boolean;
|
package/dist/history.js
CHANGED
|
@@ -2,6 +2,23 @@ import { decodeCompaction, decodeReasoning } from "./carrier.js";
|
|
|
2
2
|
import { BridgeError } from "./errors.js";
|
|
3
3
|
import { SUMMARY_PREFIX } from "./prompts.js";
|
|
4
4
|
import { translateToolHistory } from "./tool-history.js";
|
|
5
|
+
/** Raw Responses state items; decoded bridge reasoning uses an internal type. */
|
|
6
|
+
export function isOpaqueStateItem(item) {
|
|
7
|
+
return ["reasoning", "compaction", "compaction_summary"].includes(item.type);
|
|
8
|
+
}
|
|
9
|
+
/** Reject explicit policy violations before even an auxiliary handoff can run. */
|
|
10
|
+
export function rejectUnrecognizedState(input, route) {
|
|
11
|
+
if (route.replay.unrecognized !== "error" || !Array.isArray(input))
|
|
12
|
+
return;
|
|
13
|
+
for (const item of input) {
|
|
14
|
+
if (!item || !isOpaqueStateItem(item))
|
|
15
|
+
continue;
|
|
16
|
+
const carrier = item.type === "reasoning"
|
|
17
|
+
? decodeReasoning(item.encrypted_content) : decodeCompaction(item.encrypted_content);
|
|
18
|
+
if (!carrier)
|
|
19
|
+
throw new BridgeError(`Unrecognized ${item.type} history item; replay.unrecognized is set to error`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
5
22
|
/** Replay policy for decoded state. The inspector reports the same decisions. */
|
|
6
23
|
export function replaysReasoning(carrier, route) {
|
|
7
24
|
return route.replay.reasoning.has(carrier.source.id) && carrier.format === route.profile.reasoning_format;
|
|
@@ -20,7 +37,12 @@ export function prepareHistory(input, route) {
|
|
|
20
37
|
return [];
|
|
21
38
|
if (!Array.isArray(input))
|
|
22
39
|
throw new BridgeError("input must be a string or an array of items");
|
|
40
|
+
rejectUnrecognizedState(input, route);
|
|
23
41
|
const result = [];
|
|
42
|
+
const unrecognized = (item) => {
|
|
43
|
+
if (route.replay.unrecognized === "passthrough")
|
|
44
|
+
result.push(item);
|
|
45
|
+
};
|
|
24
46
|
for (const original of input) {
|
|
25
47
|
if (!original || typeof original !== "object" || Array.isArray(original)) {
|
|
26
48
|
throw new BridgeError("Each input item must be an object");
|
|
@@ -34,7 +56,9 @@ export function prepareHistory(input, route) {
|
|
|
34
56
|
}
|
|
35
57
|
if (item.type === "reasoning") {
|
|
36
58
|
const carrier = decodeReasoning(item.encrypted_content);
|
|
37
|
-
if (carrier
|
|
59
|
+
if (!carrier)
|
|
60
|
+
unrecognized(item);
|
|
61
|
+
else if (replaysReasoning(carrier, route)) {
|
|
38
62
|
result.push({ type: "bridge_reasoning", state: carrier, summary: item.summary ?? [] });
|
|
39
63
|
}
|
|
40
64
|
continue;
|
|
@@ -63,10 +87,14 @@ export function prepareHistory(input, route) {
|
|
|
63
87
|
if (item.type === "compaction" || item.type === "compaction_summary") {
|
|
64
88
|
const carrier = decodeCompaction(item.encrypted_content);
|
|
65
89
|
if (!carrier) {
|
|
66
|
-
|
|
90
|
+
unrecognized(item);
|
|
91
|
+
continue;
|
|
67
92
|
}
|
|
68
93
|
if (restoresNativeCompaction(carrier, route)) {
|
|
69
|
-
|
|
94
|
+
const native = carrier.native.data;
|
|
95
|
+
if (route.profile.api === "responses")
|
|
96
|
+
delete native.id;
|
|
97
|
+
result.push(native);
|
|
70
98
|
}
|
|
71
99
|
else {
|
|
72
100
|
if (!carrier.summary)
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { createBridgeServer } from "./server.ts";
|
|
|
2
2
|
export { loadConfig, validateConfig, resolveModel } from "./config.ts";
|
|
3
3
|
export { buildCatalog, configSnippet } from "./catalog.ts";
|
|
4
4
|
export { BridgeError } from "./errors.ts";
|
|
5
|
-
export type { BridgeConfig, UpstreamConfig, ModelConfig } from "./types.ts";
|
|
5
|
+
export type { BridgeConfig, UpstreamConfig, ModelConfig, BridgeServerOptions, ResponsesRequest } from "./types.ts";
|
package/dist/inspect/view.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface Annotation {
|
|
|
11
11
|
pointer: string;
|
|
12
12
|
event?: number;
|
|
13
13
|
kind: "reasoning" | "compaction";
|
|
14
|
-
decision: "replay" | "drop" | "native" | "summary" | "handoff" | "reject" | "output";
|
|
14
|
+
decision: "replay" | "passthrough" | "drop" | "native" | "summary" | "handoff" | "reject" | "output";
|
|
15
15
|
reason: string;
|
|
16
16
|
source: {
|
|
17
17
|
aliases: string[];
|
|
@@ -80,6 +80,7 @@ export declare function detail(exchange: Exchange): {
|
|
|
80
80
|
replay: {
|
|
81
81
|
reasoning_from: string[];
|
|
82
82
|
native_compaction_from: string[];
|
|
83
|
+
unrecognized: import("../types.ts").UnrecognizedStatePolicy;
|
|
83
84
|
};
|
|
84
85
|
responses_lite: boolean;
|
|
85
86
|
} | null;
|
package/dist/inspect/view.js
CHANGED
|
@@ -196,7 +196,10 @@ function routeView(inspector, route, responsesLite) {
|
|
|
196
196
|
native_compaction_format: route.profile.native_compaction_format ?? null,
|
|
197
197
|
compaction_mode: route.compaction.mode,
|
|
198
198
|
native_replay: route.compaction.native_replay,
|
|
199
|
-
replay: {
|
|
199
|
+
replay: {
|
|
200
|
+
reasoning_from: names(route.replay.reasoning), native_compaction_from: names(route.replay.native_compaction),
|
|
201
|
+
unrecognized: route.replay.unrecognized,
|
|
202
|
+
},
|
|
200
203
|
responses_lite: responsesLite ?? false,
|
|
201
204
|
};
|
|
202
205
|
}
|
|
@@ -217,15 +220,9 @@ function messageView(message) {
|
|
|
217
220
|
}
|
|
218
221
|
function reasoningInput(inspector, route, item, pointer) {
|
|
219
222
|
const base = { target: "client.request", pointer, kind: "reasoning" };
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
carrier = decodeReasoning(item.encrypted_content);
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
return { ...base, decision: "reject", reason: "malformed", source: null, format: null, decoded: null };
|
|
226
|
-
}
|
|
223
|
+
const carrier = decodeReasoning(item.encrypted_content);
|
|
227
224
|
if (!carrier)
|
|
228
|
-
return
|
|
225
|
+
return unrecognizedInput(base, route);
|
|
229
226
|
const replay = replaysReasoning(carrier, route);
|
|
230
227
|
const reason = replay
|
|
231
228
|
? carrier.source.id === route.origin.id ? "same_source" : "allowed_source"
|
|
@@ -236,7 +233,7 @@ function compactionInput(inspector, route, item, pointer) {
|
|
|
236
233
|
const base = { target: "client.request", pointer, kind: "compaction" };
|
|
237
234
|
const carrier = decodeCompaction(item.encrypted_content);
|
|
238
235
|
if (!carrier)
|
|
239
|
-
return
|
|
236
|
+
return unrecognizedInput(base, route);
|
|
240
237
|
const native = carrier.native;
|
|
241
238
|
const restored = restoresNativeCompaction(carrier, route);
|
|
242
239
|
const reason = restored ? "native_restored"
|
|
@@ -248,15 +245,15 @@ function compactionInput(inspector, route, item, pointer) {
|
|
|
248
245
|
source: native ? source(inspector, native.source) : null, format: native?.format ?? null, decoded: carrier,
|
|
249
246
|
};
|
|
250
247
|
}
|
|
248
|
+
function unrecognizedInput(base, route) {
|
|
249
|
+
return {
|
|
250
|
+
...base, decision: route.replay.unrecognized === "error" ? "reject" : route.replay.unrecognized,
|
|
251
|
+
reason: "unrecognized_state", source: null, format: null, decoded: null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
251
254
|
function outputNote(inspector, item, target) {
|
|
252
255
|
if (item?.type === "reasoning") {
|
|
253
|
-
|
|
254
|
-
try {
|
|
255
|
-
carrier = decodeReasoning(item.encrypted_content);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
256
|
+
const carrier = decodeReasoning(item.encrypted_content);
|
|
260
257
|
if (!carrier)
|
|
261
258
|
return undefined;
|
|
262
259
|
return { ...target, kind: "reasoning", decision: "output", reason: "output", source: source(inspector, carrier.source), format: carrier.format, decoded: carrier };
|
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type Server } from "node:http";
|
|
2
|
-
import type { BridgeConfig } from "./types.ts";
|
|
2
|
+
import type { BridgeConfig, BridgeServerOptions } from "./types.ts";
|
|
3
3
|
/** Creates a server without opening a socket, so callers control its lifecycle. */
|
|
4
|
-
export declare function createBridgeServer(value: BridgeConfig): Server;
|
|
4
|
+
export declare function createBridgeServer(value: BridgeConfig, options?: BridgeServerOptions): Server;
|
package/dist/server.js
CHANGED
|
@@ -71,7 +71,7 @@ function authenticated(req, token) {
|
|
|
71
71
|
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
72
72
|
}
|
|
73
73
|
/** Creates a server without opening a socket, so callers control its lifecycle. */
|
|
74
|
-
export function createBridgeServer(value) {
|
|
74
|
+
export function createBridgeServer(value, options = {}) {
|
|
75
75
|
const config = validateConfig(value);
|
|
76
76
|
const compactions = new CompactionHandoffStore(config, BACKENDS);
|
|
77
77
|
const catalog = JSON.stringify(buildCatalog(config));
|
|
@@ -124,7 +124,9 @@ export function createBridgeServer(value) {
|
|
|
124
124
|
}
|
|
125
125
|
res.setHeader("x-models-etag", catalogEtag);
|
|
126
126
|
const exchange = inspector.begin(req, res);
|
|
127
|
-
const
|
|
127
|
+
const original = await readRequest(req, config.request_max_mib ?? 512, exchange);
|
|
128
|
+
const requestHeaders = incomingHeaders(req.headers);
|
|
129
|
+
const request = await options.transformRequest?.(original, { headers: requestHeaders }) ?? original;
|
|
128
130
|
const caller = resolveModel(config, request.model);
|
|
129
131
|
const compacting = isCompactionRequest(request);
|
|
130
132
|
// Restore history for the actual consumer before materializing any handoffs.
|
|
@@ -139,7 +141,10 @@ export function createBridgeServer(value) {
|
|
|
139
141
|
exchange?.routed(route, responsesLite);
|
|
140
142
|
const backend = BACKENDS[route.profile.id];
|
|
141
143
|
const streaming = request.stream === true;
|
|
142
|
-
writer = new ResponseWriter(streaming ? res : { write() { } }, {
|
|
144
|
+
writer = new ResponseWriter(streaming ? res : { write() { } }, {
|
|
145
|
+
model: request.model, showSummary: reasoningSummaryRequested(request),
|
|
146
|
+
onEvent: options.onResponseEvent ? event => options.onResponseEvent(request, event) : undefined,
|
|
147
|
+
});
|
|
143
148
|
if (streaming)
|
|
144
149
|
startStream = () => {
|
|
145
150
|
if (res.headersSent || controller.signal.aborted)
|
|
@@ -155,7 +160,7 @@ export function createBridgeServer(value) {
|
|
|
155
160
|
};
|
|
156
161
|
else
|
|
157
162
|
writer.start();
|
|
158
|
-
const context = { request: executionRequest, route, signal: controller.signal, requestHeaders:
|
|
163
|
+
const context = { request: executionRequest, route, signal: controller.signal, requestHeaders, fetch: options.fetch, responsesLite, onAccepted: (headers) => {
|
|
159
164
|
for (const [name, value] of Object.entries(headers ? forwardResponseHeaders(headers) : {}))
|
|
160
165
|
res.setHeader(name, value);
|
|
161
166
|
startStream?.();
|
|
@@ -227,7 +232,13 @@ export function createBridgeServer(value) {
|
|
|
227
232
|
finally {
|
|
228
233
|
if (keepalive)
|
|
229
234
|
clearInterval(keepalive);
|
|
235
|
+
// A hook can fail after acceptance but before the backend reads the body.
|
|
236
|
+
controller.abort();
|
|
230
237
|
res.off("close", abort);
|
|
231
238
|
}
|
|
239
|
+
}).catch(() => {
|
|
240
|
+
// Failure delivery can itself throw, including from a terminal observer.
|
|
241
|
+
// Close only this response; never leave the async request unhandled.
|
|
242
|
+
res.destroy();
|
|
232
243
|
}));
|
|
233
244
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResponseWriter, Usage } from "./writer.ts";
|
|
2
2
|
export type Api = "responses" | "anthropic" | "chat";
|
|
3
3
|
export type AdapterId = "gpt-responses" | "claude-messages" | "deepseek-chat";
|
|
4
|
+
export type UnrecognizedStatePolicy = "passthrough" | "drop" | "error";
|
|
4
5
|
export interface ClaudeMessagesOptions {
|
|
5
6
|
tool_updates: "inline" | "top_level";
|
|
6
7
|
thinking_mismatch: "drop_block" | "error" | "upstream_default";
|
|
@@ -15,7 +16,9 @@ export interface UpstreamConfig {
|
|
|
15
16
|
/** An argv array. The command writes a short-lived credential to stdout. */
|
|
16
17
|
api_key_command?: string[];
|
|
17
18
|
auth_header?: string;
|
|
18
|
-
stream_idle_timeout_ms
|
|
19
|
+
/** Defaults to stream_idle_timeout_ms; null disables the wait-for-headers timer. */
|
|
20
|
+
headers_timeout_ms?: number | null;
|
|
21
|
+
stream_idle_timeout_ms?: number | null;
|
|
19
22
|
headers?: Record<string, string>;
|
|
20
23
|
options?: Record<string, unknown>;
|
|
21
24
|
adapters?: AdapterConfig;
|
|
@@ -36,6 +39,7 @@ export interface ModelConfig {
|
|
|
36
39
|
replay?: {
|
|
37
40
|
reasoning_from?: string[];
|
|
38
41
|
native_compaction_from?: string[];
|
|
42
|
+
unrecognized?: UnrecognizedStatePolicy;
|
|
39
43
|
};
|
|
40
44
|
compaction?: {
|
|
41
45
|
mode?: "summary" | "native";
|
|
@@ -87,6 +91,7 @@ export interface ResolvedModel {
|
|
|
87
91
|
replay: {
|
|
88
92
|
reasoning: ReadonlySet<string>;
|
|
89
93
|
native_compaction: ReadonlySet<string>;
|
|
94
|
+
unrecognized: UnrecognizedStatePolicy;
|
|
90
95
|
};
|
|
91
96
|
compaction: {
|
|
92
97
|
mode: "summary" | "native";
|
|
@@ -107,12 +112,23 @@ export interface ResponsesRequest {
|
|
|
107
112
|
stream?: boolean;
|
|
108
113
|
[key: string]: any;
|
|
109
114
|
}
|
|
115
|
+
export interface BridgeServerOptions {
|
|
116
|
+
/** Used by every upstream call, including compaction and handoffs. */
|
|
117
|
+
fetch?: typeof globalThis.fetch;
|
|
118
|
+
/** Runs once before routing. Return a replacement or change the request in place. */
|
|
119
|
+
transformRequest?: (request: ResponsesRequest, context: {
|
|
120
|
+
headers: Headers;
|
|
121
|
+
}) => ResponsesRequest | void | Promise<ResponsesRequest | void>;
|
|
122
|
+
/** Observes committed client response events, including non-streaming responses. */
|
|
123
|
+
onResponseEvent?: (request: ResponsesRequest, event: Record<string, any>) => void;
|
|
124
|
+
}
|
|
110
125
|
export interface BackendContext {
|
|
111
126
|
request: ResponsesRequest;
|
|
112
127
|
route: ResolvedModel;
|
|
113
128
|
signal: AbortSignal;
|
|
114
129
|
/** Client metadata; transport framing and credentials are replaced upstream. */
|
|
115
130
|
requestHeaders?: Headers;
|
|
131
|
+
fetch?: typeof globalThis.fetch;
|
|
116
132
|
compacting?: boolean;
|
|
117
133
|
/** Transport mode received from the client, including tool-free Lite requests. */
|
|
118
134
|
responsesLite?: boolean;
|
package/dist/upstream.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { ResolvedModel } from "./types.ts";
|
|
2
2
|
/** base_url includes any API version prefix; path is appended, never resolved from /. */
|
|
3
|
-
export declare function upstreamFetch(route: ResolvedModel, path: string, body: unknown, signal: AbortSignal, requestHeaders?: Headers, protocolHeaders?: Record<string, string | string[]
|
|
3
|
+
export declare function upstreamFetch(route: ResolvedModel, path: string, body: unknown, signal: AbortSignal, requestHeaders?: Headers, protocolHeaders?: Record<string, string | string[]>, fetch?: typeof globalThis.fetch): Promise<Response>;
|
package/dist/upstream.js
CHANGED
|
@@ -2,7 +2,7 @@ import { upstreamHeaders } from "./auth.js";
|
|
|
2
2
|
import { BridgeError, transportError, upstreamError, UpstreamHttpError } from "./errors.js";
|
|
3
3
|
import { captureUpstream } from "./inspect/capture.js";
|
|
4
4
|
/** base_url includes any API version prefix; path is appended, never resolved from /. */
|
|
5
|
-
export async function upstreamFetch(route, path, body, signal, requestHeaders, protocolHeaders) {
|
|
5
|
+
export async function upstreamFetch(route, path, body, signal, requestHeaders, protocolHeaders, fetch = globalThis.fetch) {
|
|
6
6
|
const url = route.upstream.base_url.replace(/\/+$/, "") + "/" + path.replace(/^\/+/, "");
|
|
7
7
|
const headers = await upstreamHeaders(route.upstream, route.profile.api, signal, requestHeaders);
|
|
8
8
|
for (const [name, value] of Object.entries(protocolHeaders ?? {})) {
|
|
@@ -16,13 +16,18 @@ export async function upstreamFetch(route, path, body, signal, requestHeaders, p
|
|
|
16
16
|
const call = captureUpstream(url, headers, payload);
|
|
17
17
|
const idle = new AbortController();
|
|
18
18
|
const combined = AbortSignal.any([signal, idle.signal]);
|
|
19
|
-
const timeoutMs = route.upstream.stream_idle_timeout_ms
|
|
19
|
+
const timeoutMs = route.upstream.stream_idle_timeout_ms === undefined ? 300_000 : route.upstream.stream_idle_timeout_ms;
|
|
20
20
|
let timer;
|
|
21
|
-
const reset = () => {
|
|
21
|
+
const reset = (duration = timeoutMs) => {
|
|
22
22
|
clearTimeout(timer);
|
|
23
|
-
timer =
|
|
23
|
+
timer = undefined;
|
|
24
|
+
if (duration === null)
|
|
25
|
+
return;
|
|
26
|
+
timer = setTimeout(() => idle.abort(new BridgeError("Upstream stream timed out", "stream_error", 504)), duration).unref();
|
|
24
27
|
};
|
|
25
|
-
|
|
28
|
+
const headersTimeout = route.upstream.headers_timeout_ms === undefined ? timeoutMs : route.upstream.headers_timeout_ms;
|
|
29
|
+
if (headersTimeout !== null)
|
|
30
|
+
reset(headersTimeout);
|
|
26
31
|
let response;
|
|
27
32
|
try {
|
|
28
33
|
response = await fetch(url, { method: "POST", headers, body: payload, signal: combined, redirect: "error" });
|
|
@@ -34,8 +39,9 @@ export async function upstreamFetch(route, path, body, signal, requestHeaders, p
|
|
|
34
39
|
throw combined.reason;
|
|
35
40
|
throw transportError(error);
|
|
36
41
|
}
|
|
37
|
-
// Downstream keepalives do not count as upstream progress.
|
|
38
|
-
//
|
|
42
|
+
// Downstream keepalives do not count as upstream progress. The body timer
|
|
43
|
+
// starts after headers, including when the header wait has no deadline.
|
|
44
|
+
reset();
|
|
39
45
|
call?.received(response);
|
|
40
46
|
if (response.body) {
|
|
41
47
|
const reader = response.body.getReader();
|
package/dist/writer.d.ts
CHANGED
|
@@ -40,10 +40,12 @@ export declare class ResponseWriter {
|
|
|
40
40
|
private nativeEnd;
|
|
41
41
|
private started;
|
|
42
42
|
private finished;
|
|
43
|
+
private onEvent?;
|
|
43
44
|
constructor(sink: Sink, opts: {
|
|
44
45
|
model: string;
|
|
45
46
|
showSummary?: boolean;
|
|
46
47
|
responseId?: string;
|
|
48
|
+
onEvent?: (event: Record<string, any>) => void;
|
|
47
49
|
});
|
|
48
50
|
get responseId(): string;
|
|
49
51
|
get nativeResponse(): Record<string, any> | undefined;
|
package/dist/writer.js
CHANGED
|
@@ -45,11 +45,13 @@ export class ResponseWriter {
|
|
|
45
45
|
nativeEnd;
|
|
46
46
|
started = false;
|
|
47
47
|
finished = false;
|
|
48
|
+
onEvent;
|
|
48
49
|
constructor(sink, opts) {
|
|
49
50
|
this.sink = sink;
|
|
50
51
|
this.model = opts.model;
|
|
51
52
|
this.showSummary = opts.showSummary ?? true;
|
|
52
53
|
this.id = opts.responseId ?? newId("resp");
|
|
54
|
+
this.onEvent = opts.onEvent;
|
|
53
55
|
}
|
|
54
56
|
get responseId() { return this.id; }
|
|
55
57
|
get nativeResponse() { return this.nativeEnd?.response; }
|
|
@@ -63,7 +65,16 @@ export class ResponseWriter {
|
|
|
63
65
|
return this.nextOutputIndex > 0 || this.calls > 0 || this.message !== undefined;
|
|
64
66
|
}
|
|
65
67
|
write(event, native = false) {
|
|
66
|
-
|
|
68
|
+
const emitted = native ? event : { sequence_number: this.seq++, ...event };
|
|
69
|
+
this.sink.write(formatSse(emitted));
|
|
70
|
+
try {
|
|
71
|
+
this.onEvent?.(emitted);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// The request's failure event must not re-enter a failed observer.
|
|
75
|
+
this.onEvent = undefined;
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
send(event, native = false) {
|
|
69
80
|
if (this.suffix)
|
|
@@ -117,8 +128,8 @@ export class ResponseWriter {
|
|
|
117
128
|
keepalive() {
|
|
118
129
|
if (!this.started || this.finished)
|
|
119
130
|
return;
|
|
120
|
-
//
|
|
121
|
-
this.write({ type: "keepalive" }
|
|
131
|
+
// Transport heartbeats bypass both the ordered suffix and the observer.
|
|
132
|
+
this.sink.write(formatSse({ ...(!this.native ? { sequence_number: this.seq++ } : {}), type: "keepalive" }));
|
|
122
133
|
}
|
|
123
134
|
/** Native Responses events retain their own ids, envelopes and metadata. */
|
|
124
135
|
useNativeResponses() { this.native = true; }
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
随后浅层合并上游和模型 `options`,后者覆盖前者,也可覆盖推导出的普通参数。最后由 bridge 固定 `model`、`input`、`tools`、`store: false`、`stream: true`,`include` 至少包含 `reasoning.encrypted_content`。上游状态由完整历史承载,不依赖服务器保存的会话。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
bridge 能识别的历史 reasoning 按允许来源选择,将 `responses.reasoning.v1` 的 data 还原为原生密文;原生压缩按独立策略选择。未识别状态由 `replay.unrecognized` 决定处理方式,默认保留整项,包括其 `id`。其余输入项移除顶层 `id`,工具配对的 `call_id` 保留,避免引用已过滤的 reasoning。图片、普通消息、工具结果和明文 agent message 保留原格式。[状态规则](reasoning.md)
|
|
14
14
|
|
|
15
15
|
## 流与工具交付
|
|
16
16
|
|
package/docs/compaction.md
CHANGED
|
@@ -163,7 +163,7 @@ cxbc1:<base64url(JSON)>
|
|
|
163
163
|
|
|
164
164
|
`summary` 检查点只保存 `summary` 文字字段。原生检查点的 `handoff` 包含 `prefix`、上述请求 `parameters` 和 `responsesLite` 标记,足以在后续请求提供的 `U` 上生成文字。原生 data 保留完整上游 item;来源指纹计算见 [reasoning](reasoning.md)。
|
|
165
165
|
|
|
166
|
-
每次普通请求或再次压缩前,bridge
|
|
166
|
+
每次普通请求或再次压缩前,bridge 对能识别的检查点按实际执行目标选表示;普通请求是工作模型,压缩请求是选定的压缩执行模型:
|
|
167
167
|
|
|
168
168
|
1. 有原生状态、开启 `native_replay`、来源允许、格式匹配:还原原生 item。
|
|
169
169
|
2. 其他情况已有文字摘要:将恢复前缀和文字摘要作为 user message。
|
|
@@ -171,7 +171,9 @@ cxbc1:<base64url(JSON)>
|
|
|
171
171
|
|
|
172
172
|
这是发送前的表示选择;第一种请求被上游拒绝时直接返回错误,不自动再试文字表示。生成复述必须读取源自身的 `K`,不受源路由普通续聊的 `native_replay: false` 限制。
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
恢复不改写客户端保存的载荷。按需复述只更新本次请求的副本,并通过本地缓存复用;切换模型再切回来,仍可使用原检查点。再次压缩后,新检查点取代旧检查点,不累积每个模型的检查点版本。
|
|
175
|
+
|
|
176
|
+
无法解码为 bridge carrier 的 `compaction` / `compaction_summary` 默认原样透传,不虚构来源、摘要或复述配方,也不触发 handoff。可用模型级 `replay.unrecognized` 改为过滤或报错,详见[未识别的历史状态](config.md#未识别的历史状态)。透传是否可用由上游裁决;过滤检查点会让本次请求失去其中承载的历史上下文。
|
|
175
177
|
|
|
176
178
|
恢复前缀逐字取自 Codex `rust-v0.162.1` 的 `codex-rs/prompts/templates/compact/summary_prefix.md`;handoff prompt 的许可归属见 `NOTICE`。外层是编码而非加密:文字摘要、请求前缀和参数可读,原生密文保持原样。
|
|
177
179
|
|
package/docs/config.md
CHANGED
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
| `api_key_env` | API key 环境变量名称 |
|
|
65
65
|
| `api_key_command` | 与 `api_key_env` 二选一;argv 数组,程序 stdout 返回当前凭据 |
|
|
66
66
|
| `auth_header` | 覆盖鉴权 header 名;默认 GPT/DeepSeek 用 `authorization: Bearer …`,Claude 用 `x-api-key` |
|
|
67
|
-
| `
|
|
67
|
+
| `headers_timeout_ms` | 等待上游响应头的最长时间;省略时沿用 `stream_idle_timeout_ms`,`null` 表示禁用 |
|
|
68
|
+
| `stream_idle_timeout_ms` | 收到上游响应头后,等待下一批字节的最长空闲时间,默认 300000 ms;`null` 表示禁用 |
|
|
68
69
|
| `headers` | 静态 header,覆盖同名客户端 header,例如 API 版本 |
|
|
69
70
|
| `options` | 原生请求参数,覆盖 adapter 推导出的参数 |
|
|
70
71
|
| `adapters` | 按 adapter ID 分组的桥接行为;供使用该连接的模型继承 |
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
| `max_output_tokens` | 覆盖目录的默认回答输出预算;请求中的显式值优先于模型配置 |
|
|
84
85
|
| `input_modalities` | 可选,覆盖条目中的 `text`/`image` 能力声明 |
|
|
85
86
|
| `reasoning` | 默认 effort 及其到原生值的映射 |
|
|
86
|
-
| `replay` |
|
|
87
|
+
| `replay` | reasoning、原生压缩的允许来源,以及未识别历史状态的处理方式 |
|
|
87
88
|
| `compaction` | 压缩方式、执行模型,以及本目标是否接收原生压缩 |
|
|
88
89
|
| `options` | 原生请求参数,覆盖上游级 options |
|
|
89
90
|
| `adapters` | 当前模型的桥接行为;逐字段覆盖上游级同名 adapter 设置 |
|
|
@@ -224,12 +225,32 @@ Claude 的 `native_replay` 仍默认为 `false`:委托生成不会让 Claude
|
|
|
224
225
|
}
|
|
225
226
|
```
|
|
226
227
|
|
|
227
|
-
|
|
228
|
+
bridge 能识别的自身状态默认允许恢复;两份列表增加其他允许来源,关系有方向且不传递。若也要接受 A 的 reasoning,把 A 加入 B 的 `reasoning_from`。这个例子的 B 自己只生成文字摘要,但仍可读取 A 的原生检查点:生成和恢复是独立能力。设置 `native_replay: false` 则已识别的压缩都使用文字表示,包括自身生成的检查点。
|
|
228
229
|
|
|
229
230
|
引用按连接、实际模型和 adapter 解析为来源身份,不绑定显示别名。加载配置时检查来源是否存在、源与目标的格式标识是否一致;每条请求恢复时再检查实际载荷。bridge 不探测密文是否被上游接受。上游拒绝被允许的状态时直接报错,不删掉状态再试。详见 [reasoning](reasoning.md) 和[压缩](compaction.md)。
|
|
230
231
|
|
|
231
232
|
连接身份包含 upstream ID 和去掉末尾斜杠的 URL,来源身份再包含 adapter 与实际模型。密钥、header 和 options 不参与身份。如果同一 URL 通过 header 或凭据切换到不兼容区域,使用不同 upstream ID;改显示别名不影响已保存状态。
|
|
232
233
|
|
|
234
|
+
### 未识别的历史状态
|
|
235
|
+
|
|
236
|
+
历史会话可能带有其他工具或旧版本保存的状态。模型级 `replay.unrecognized` 统一处理 `encrypted_content` 无法解码为相应 bridge carrier 的 `reasoning`、`compaction` 和 `compaction_summary` 项,包括未知前缀、缺少载荷或无法解析的封装:
|
|
237
|
+
|
|
238
|
+
| 值 | 行为 |
|
|
239
|
+
| --- | --- |
|
|
240
|
+
| `passthrough`(默认) | 保留整项及原始字段,交给上游解释 |
|
|
241
|
+
| `drop` | 仅从本次出站历史中过滤这些项 |
|
|
242
|
+
| `error` | 在发送上游请求前报错 |
|
|
243
|
+
|
|
244
|
+
例如,在需要明确拒绝此类历史的模型中配置:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{ "replay": { "unrecognized": "error" } }
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
GPT 将透传项保留在 Responses `input` 中,包括其 `id`;Claude/DeepSeek 将原项放入 assistant 消息的 content 数组,不把未知密文猜解为 thinking 或文字。这保留了原始数据,但不保证跨协议或跨模型兼容,上游可以拒绝;bridge 不自动重试或改用其他策略。
|
|
251
|
+
|
|
252
|
+
这些项没有可确认的来源与复述配方,因此不套用来源允许列表、不创建来源身份,也不触发按需复述。能解码的 bridge carrier 仍遵循原有来源、格式和压缩表示规则。三个选项都不改写客户端已保存的历史;`drop` 会减少本次请求的 reasoning 上下文,丢弃压缩检查点还可能失去早期对话的唯一表示。普通请求取工作模型的策略,委托压缩取实际执行模型的策略。
|
|
253
|
+
|
|
233
254
|
## 客户端目录
|
|
234
255
|
|
|
235
256
|
bridge 的 `GET /v1/models` 返回 Codex 专用的 `{"models": [...]}`,每项包含完整提示词、窗口、工具、多代理和 reasoning 元数据。它与经典 OpenAI 的 `{"object":"list","data":[...]}` 格式不同。仅列出配置中的别名,不暴露上游连接、实际部署名或凭据设置。
|
package/docs/design.md
CHANGED
|
@@ -52,7 +52,9 @@ Reasoning 载荷记录来源、格式和原始数据;压缩载荷保存文字
|
|
|
52
52
|
|
|
53
53
|
来源包括连接指纹、adapter 和实际模型,独立于显示别名和复用配置。连接凭据和 HTTP header 不写进载荷;复述配方保存源请求前缀及除输入、流式开关和文字输出约束外的请求参数,包括未知参数。外层编码不提供加密:文字 reasoning、签名、压缩摘要及请求前缀按普通聊天历史保护,按需复述缓存以 `0600` 文件权限保存。
|
|
54
54
|
|
|
55
|
-
每次请求按当前配置选取历史,不改写已保存载荷。
|
|
55
|
+
每次请求按当前配置选取历史,不改写已保存载荷。bridge 能识别的 reasoning 和原生压缩有独立的允许来源列表,默认只接受自身,关系有方向且不传递。修改配置即可改变已有历史的恢复方式;格式版本负责解释数据,不编码“永远隔离”这类策略。
|
|
56
|
+
|
|
57
|
+
未识别状态默认透传,因为历史会话可能来自其他客户端或版本,bridge 无法解码不等于上游不能使用。模型级 `replay.unrecognized` 也可显式选择过滤或报错;不为这类状态猜测来源或生成 handoff。它与已识别载荷的来源策略分开,保留未来协议扩展与用户选择。[配置与协议边界](config.md#未识别的历史状态)
|
|
56
58
|
|
|
57
59
|
明文 reasoning 使用可共享的文本格式;不同 adapter 将来可把同一格式恢复到各自字段。密文与签名保留各自原生结构,不把格式可解码等同于上游允许使用。
|
|
58
60
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 嵌入现有服务
|
|
2
|
+
|
|
3
|
+
npm 包可直接创建标准 Node HTTP server。运行已编译的包需要 Node.js ≥ 20.19;直接运行仓库里的 TypeScript 开发脚本需要 ≥ 22.18。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { createBridgeServer, loadConfig } from "@wacry/codeks-bridge";
|
|
7
|
+
|
|
8
|
+
const config = await loadConfig("/absolute/path/bridge.json");
|
|
9
|
+
const server = createBridgeServer(config, {
|
|
10
|
+
async transformRequest(request, { headers }) {
|
|
11
|
+
const model = headers.get("x-example-model");
|
|
12
|
+
if (model) request.model = model;
|
|
13
|
+
},
|
|
14
|
+
fetch: (input, init) => fetch(input, init),
|
|
15
|
+
onResponseEvent(request, event) {
|
|
16
|
+
if (event.type === "response.output_item.done") {
|
|
17
|
+
console.log(request.model, event.item.type);
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
server.listen(8123, "127.0.0.1");
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
第二个参数 `BridgeServerOptions` 可省略,不影响 CLI 和默认传输。各选项只作用于当前 server,不替换全局 fetch。
|
|
26
|
+
|
|
27
|
+
| 选项 | 调用边界 |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `transformRequest(request, { headers })` | 入口鉴权与 JSON 解析之后、模型路由和历史转换之前,主请求调用一次。可原位修改并返回 `void`,也可返回替换请求,支持 Promise。`headers` 是该请求的 HTTP headers。辅助压缩和按需复述不会再次调用它。 |
|
|
30
|
+
| `fetch` | 与标准 `fetch` 签名相同,承接全部模型请求,包括压缩、委托压缩及按需复述。可接入宿主的代理、凭据和传输策略;须保留传入的取消信号并返回标准 `Response`。 |
|
|
31
|
+
| `onResponseEvent(request, event)` | 同步观察主响应实际提交的标准 Responses 事件;`request` 是变换后的请求。缓冲后因失败丢弃的工具事件、辅助请求和 keepalive 不进入观察器。JSON 请求也产生同样的语义事件,其中每个已提交输出项有一次 `response.output_item.done`。 |
|
|
32
|
+
|
|
33
|
+
观察器应只读取参数,不修改请求或事件,也不返回 Promise。它在事件写出后同步执行;抛出的异常会进入现有请求错误处理,并停止该请求的后续观察,已经写出的事件无法撤回。如果交付终止错误时观察器自身抛错,bridge 关闭当前响应连接,不再尝试发送第二个错误;其他请求仍可继续。请求结束时取消仍在进行的上游调用,重试由客户端或宿主决定。原始上游帧及辅助调用的调试使用[检查器](inspect.md)。
|
|
34
|
+
|
|
35
|
+
宿主可通过 `AsyncLocalStorage` 在请求处理作用域中保存自己的关联信息,供 fetch 与观察器读取。bridge 不要求特定宿主元数据格式,也不管理宿主的重试。
|
|
36
|
+
|
|
37
|
+
如果宿主已有超时策略,分别设置连接的 `headers_timeout_ms` 和 `stream_idle_timeout_ms`;`null` 禁用对应阶段,省略保持默认行为,详见[连接配置](config.md#上游连接)。
|
package/docs/inspect.md
CHANGED
|
@@ -107,20 +107,18 @@ codeks-bridge serve --config /absolute/path/bridge.json --capture
|
|
|
107
107
|
| reasoning | `replay` | `allowed_source` | 来自配置允许的其他目标,本次回放 |
|
|
108
108
|
| reasoning | `drop` | `source_not_allowed` | 来源不在当前目标的允许列表,本次请求中过滤 |
|
|
109
109
|
| reasoning | `drop` | `format_mismatch` | 来源允许,但载荷格式与当前 adapter 不同 |
|
|
110
|
-
| reasoning | `drop` | `
|
|
111
|
-
| reasoning | `reject` | `malformed` | 有 bridge 前缀但无法解码,bridge 会拒绝请求 |
|
|
110
|
+
| reasoning / compaction | `passthrough` / `drop` / `reject` | `unrecognized_state` | 无法解码为 bridge carrier,按 `replay.unrecognized` 透传、过滤或拒绝 |
|
|
112
111
|
| compaction | `native` | `native_restored` | 恢复原生加密检查点 |
|
|
113
112
|
| compaction | `summary` | `no_native` | 检查点只有文字摘要 |
|
|
114
113
|
| compaction | `summary` / `handoff` | `native_replay_disabled` | 当前目标关闭了原生恢复 |
|
|
115
114
|
| compaction | `summary` / `handoff` | `source_not_allowed` | 原生检查点来源不在允许列表 |
|
|
116
115
|
| compaction | `summary` / `handoff` | `format_mismatch` | 原生检查点格式与当前 adapter 不同 |
|
|
117
|
-
| compaction | `reject` | `not_bridge_state` | 不是 bridge 生成的压缩项,bridge 会拒绝请求 |
|
|
118
116
|
|
|
119
117
|
`summary` 表示直接使用载荷中已有的文字,`handoff` 表示先取得文字交接:可以复用本地缓存,也可以向原来源生成。只有实际发起辅助请求时,上游调用列表才出现 `handoff`;这条状态标注本身不表示发生了额外 HTTP 请求。
|
|
120
118
|
|
|
121
119
|
响应侧标注 bridge 新生成的 reasoning 与压缩项(决定为 `output`)。流式响应指向 `response.output_item.done` 事件里的 `/item`,非流式响应指向 `/output/k`。只有文字摘要的压缩项没有原生部分,来源和格式为空;`native` 检查点的来源和格式取原生部分。
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
已识别载荷的标注附来源别名、部署名、adapter、来源指纹、载荷格式和解码后的 carrier;未识别状态不补造这些信息。`compaction_summary` 与 `compaction` 使用相同的压缩状态判断。`cxbr1:` 与 `cxbc1:` 只是编码,解码后可以看到文字 reasoning、压缩摘要和原生载荷;GPT 原生 `encrypted_content` 本身仍是密文,只能看到原样字符串。
|
|
124
122
|
|
|
125
123
|
## Claude 服务端输入处理
|
|
126
124
|
|
package/docs/reasoning.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reasoning 与状态复用
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
bridge 能识别的 reasoning 默认只恢复给产生它的目标,配置可以允许其他来源。是否允许复用和如何解码是两件事:来源策略由 `replay.reasoning_from` 决定,载荷的 `format` 告诉 adapter 如何还原内容。无法识别的历史状态默认原样透传,详见[未识别的历史状态](config.md#未识别的历史状态)。
|
|
4
4
|
|
|
5
5
|
## 显示与推理分开
|
|
6
6
|
|
|
@@ -61,7 +61,7 @@ B 可以接收自身及 A 的 reasoning;A 不会因此接收 B。A 接受 C
|
|
|
61
61
|
|
|
62
62
|
加载配置检查引用存在且源与目标的格式标识一致。两个 GPT 模型的 reasoning、同一 DeepSeek 模型的不同部署均可配置复用;每次请求恢复实际载荷,上游是否真正接受,交给实际请求验证。失败按上游错误返回,不删掉 reasoning 再发送。
|
|
63
63
|
|
|
64
|
-
`prepareHistory` 每次复制输入,恢复来源允许且格式匹配的 carrier
|
|
64
|
+
`prepareHistory` 每次复制输入,恢复来源允许且格式匹配的 carrier,其他已识别 carrier 只在本次请求中过滤。普通消息、工具调用、结果和图片继续保留。无法解码为 bridge carrier 的 reasoning 由 `replay.unrecognized` 决定透传、过滤或报错,不猜测来源,也不归属到当前目标。
|
|
65
65
|
|
|
66
66
|
例如默认 A → B → A 时,B 不接收 A 的 reasoning,切回 A 后仍可恢复。若后来配置 B 接受 A,无需重写历史,后续请求即可使用其中 A 的载荷。被新的压缩检查点取代的旧历史则不再可恢复。
|
|
67
67
|
|
|
@@ -69,6 +69,6 @@ B 可以接收自身及 A 的 reasoning;A 不会因此接收 B。A 接受 C
|
|
|
69
69
|
|
|
70
70
|
codeks 的明文 `agent_message` 在 GPT Responses 中保持原格式;Claude/DeepSeek 将其转成 user 消息,在内容开头标明 author、recipient,后面保留文本和图片。它不是 reasoning,不受 reasoning 来源列表控制。
|
|
71
71
|
|
|
72
|
-
GPT
|
|
72
|
+
GPT 出站请求使用 `store: false`,除原样透传的未识别状态项外,移除 item 顶层 `id`,保留工具配对的 `call_id`。这样普通消息不会继续通过服务器 item ID 引用已过滤的 reasoning;未识别状态的原始字段由上游解释。请求必须包含完整历史,不能只带 `item_reference` 或 `previous_response_id`。
|
|
73
73
|
|
|
74
74
|
压缩另用 `cxbc1:`,有独立的允许来源,保存文字摘要或按需复述配方。详见[压缩](compaction.md)。
|
package/docs/releasing.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 日常验证
|
|
6
6
|
|
|
7
|
-
GitHub Actions 工作流 `codeks-bridge-npm.yml` 只手动触发,不在每次提交时运行。默认 `publish=false
|
|
7
|
+
GitHub Actions 工作流 `codeks-bridge-npm.yml` 只手动触发,不在每次提交时运行。默认 `publish=false`:安装开发依赖、类型检查、测试、构建、公开信息检查,再生成 npm tarball,并在 Node.js 24 和最低运行时 20.19 上安装验证公开 API、类型、CLI、嵌入请求与 UI 资源。工作流保存实际测试过的 tarball,发布步骤直接使用同一份文件。
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
gh workflow run codeks-bridge-npm.yml --ref main
|
package/docs/responses-stream.md
CHANGED
|
@@ -87,4 +87,4 @@ GPT 保留原始 usage,包括提供的 cache write 等分项;DeepSeek 使用
|
|
|
87
87
|
|
|
88
88
|
按需复述的等待可能覆盖完整的辅助推理,客户端空闲超时应足以容纳该等待。
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
上游响应体空闲超时由 `upstreams.<id>.stream_idle_timeout_ms` 控制,默认 300 秒,也覆盖读取错误体。只有上游字节重置它,下游 keepalive 不延长它。等待响应头的时限由 `headers_timeout_ms` 控制,省略时沿用响应体空闲时限;收到响应头后重新开始响应体计时。任一字段设为 `null` 可禁用对应阶段的时限;两者均省略时保留原来的 300 秒行为。宿主已有传输重试或超时策略时,可据此分开配置,bridge 本身仍不重试。
|
package/docs/testing.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 验证
|
|
2
2
|
|
|
3
|
+
开发与直接运行 TypeScript 源码需要 Node.js ≥ 22.18;已发布 JavaScript 包支持 Node.js ≥ 20.19。
|
|
4
|
+
|
|
3
5
|
桥接器的测试只使用 Node 内置测试器和本地假上游,不需要模型密钥。普通测试不启动 codeks,也不编译其 Rust 项目。
|
|
4
6
|
|
|
5
7
|
```sh
|
|
@@ -25,6 +27,7 @@ npm run check
|
|
|
25
27
|
- 保留历史:按需复述使用客户端实际保留的历史而不再裁剪;源指令与工具声明恢复,检查点后的工作留给目标请求。
|
|
26
28
|
- 后端与历史:各 API 的消息、工具、图片、schema、thinking、usage 与错误转换;Claude 空工具结果、上下文边界停止及不完整工具不交付。
|
|
27
29
|
- 检查器:四段内容逐字节保存、只脱敏凭据 header、上游调用标签、标注与实际回放决定一致、捕获开关与结束时的软上限淘汰、清空释放数据且不打断请求、客户端断开、无 token 时的 loopback 与同源限制、token 鉴权、静态文件路径、导出格式和跨块 SSE 解析;Responses Lite 结构视图补全输出且保留原始事件。
|
|
30
|
+
- 嵌入:请求在路由前同步或异步变换;出站 fetch 按服务实例注入并覆盖三种 adapter、压缩及按需复述;观察器只接收主响应实际提交的 Responses 事件,流式与 JSON 保持一致,失败时未提交工具不外泄;响应头和响应体超时可分别禁用。
|
|
28
31
|
- Header 与错误:会话、路由及未知 header 默认透传;鉴权归所选上游,必要的传输字段移除;等待上游响应头时不发送 200;GPT HTTP status/body 与原生 SSE error 字段保留;辅助复述成功时返回目标 header,失败时返回该失败的 header;Claude/DeepSeek 继续转换协议;验证没有额外请求或自动降级。
|
|
29
32
|
|
|
30
33
|
这些测试检查协议和客户端行为;假上游不会验证模型是否理解提示词,也不会验证真实服务的缓存命中率、容量和区域之间的密文兼容性。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wacry/codeks-bridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Configurable local Responses API bridge for codeks with GPT, Claude Messages, and DeepSeek Chat adapters.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "David Zhang",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"NOTICE"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=20.19"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prepack": "npm run build",
|
package/ui/labels.js
CHANGED
|
@@ -14,10 +14,10 @@ export const KIND_LABEL = { turn: '轮次', compaction: '压缩', invalid: '无
|
|
|
14
14
|
export const OUTCOME_LABEL = { active: '进行中', completed: '完成', incomplete: '未完成', failed: '失败', error: '错误', aborted: '中止' };
|
|
15
15
|
export const END_LABEL = { active: '传输中', complete: '完整', cancelled: '已取消', error: '出错', unread: '终止事件后停止读取' };
|
|
16
16
|
|
|
17
|
-
const DECISION = { replay: '回放', drop: '丢弃', native: '恢复原生', summary: '文字摘要', handoff: '按需文字交接', reject: '拒绝', output: '输出' };
|
|
18
|
-
const DECISION_CLASS = { replay: 'ok', native: 'ok', drop: 'warn', summary: 'warn', handoff: 'warn', reject: 'bad', output: 'info' };
|
|
17
|
+
const DECISION = { replay: '回放', passthrough: '原样透传', drop: '丢弃', native: '恢复原生', summary: '文字摘要', handoff: '按需文字交接', reject: '拒绝', output: '输出' };
|
|
18
|
+
const DECISION_CLASS = { replay: 'ok', passthrough: 'info', native: 'ok', drop: 'warn', summary: 'warn', handoff: 'warn', reject: 'bad', output: 'info' };
|
|
19
19
|
// Verb shown before the reason text, per decision.
|
|
20
|
-
const VERB = { replay: '回放', drop: '丢弃', reject: '拒绝', summary: '改用文字摘要', handoff: '获取文字交接', native: '恢复原生压缩' };
|
|
20
|
+
const VERB = { replay: '回放', passthrough: '原样透传', drop: '丢弃', reject: '拒绝', summary: '改用文字摘要', handoff: '获取文字交接', native: '恢复原生压缩' };
|
|
21
21
|
|
|
22
22
|
const REASON = {
|
|
23
23
|
reasoning: {
|
|
@@ -25,8 +25,7 @@ const REASON = {
|
|
|
25
25
|
allowed_source: '来源在 replay 白名单内',
|
|
26
26
|
source_not_allowed: '来源未在 replay 白名单',
|
|
27
27
|
format_mismatch: '载荷格式与目标不匹配',
|
|
28
|
-
|
|
29
|
-
malformed: 'bridge 载荷损坏,无法解码',
|
|
28
|
+
unrecognized_state: '无法识别为 bridge reasoning 载荷,按 replay.unrecognized 处理',
|
|
30
29
|
},
|
|
31
30
|
compaction: {
|
|
32
31
|
native_restored: '',
|
|
@@ -34,8 +33,7 @@ const REASON = {
|
|
|
34
33
|
native_replay_disabled: '目标关闭了 native_replay',
|
|
35
34
|
source_not_allowed: '来源未在原生压缩白名单',
|
|
36
35
|
format_mismatch: '原生格式与目标不匹配',
|
|
37
|
-
|
|
38
|
-
malformed: 'bridge 检查点损坏,无法解码',
|
|
36
|
+
unrecognized_state: '无法识别为 bridge 压缩检查点,按 replay.unrecognized 处理',
|
|
39
37
|
},
|
|
40
38
|
};
|
|
41
39
|
|