@wacry/codeks-bridge 0.0.4 → 0.0.6

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 CHANGED
@@ -29,7 +29,7 @@ codeks-bridge config-snippet --config /absolute/path/bridge.json
29
29
  ## 使用边界
30
30
 
31
31
  - 上游错误保留原始详情交给客户端;bridge 不自动换端点、重试或更换压缩方式。
32
- - Reasoning 默认只回放给原目标。原生压缩与 reasoning 的跨目标复用分别配置,实际兼容性由上游决定。
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 转为工具描述约束。图片理解由模型支持,客户端配置关闭网页搜索和图片生成。
package/catalog/README.md CHANGED
@@ -32,7 +32,7 @@
32
32
 
33
33
  Claude 和 DeepSeek 从 GPT-6.1 Sol 的完整条目开始,只做各自 JSON 中列出的修改:底层模型与展示信息、工作窗口、reasoning 档位,以及当前 adapter 尚未转换的参数和 GPT 专属的服务展示信息。提示词仅将首句的 `based on GPT-6` 换成 `based on Claude` 或 `based on DeepSeek`;`You are Codex`、`As Codex` 和其余客户端指令保留。`default_reasoning_summary` 保持源条目的 `none`,由用户在 codeks 中选择摘要展示方式。
34
34
 
35
- 预算默认值遵循与 GPT-6.1 Sol 模板对齐的维护原则:仅保留模板已有字段,或为上游必填参数补充默认值;公布的能力上限本身不要求新增默认配置。窗口字段沿用模板结构:三个第三方条目的 `context_window` 与 `max_context_window` 均为 `1000000`。压缩阈值保留模板的 `auto_compact_token_limit: null`,由客户端按窗口计算。Claude Messages 要求 `max_tokens`,因此两个 Claude 条目提供 `128000` 默认预算;GPT 和 DeepSeek 的输出预算由请求、用户配置或上游默认值决定。
35
+ 预算默认值遵循与 GPT-6.1 Sol 模板对齐的维护原则:仅保留模板已有字段,或为上游必填参数补充默认值;公布的能力上限本身不要求新增默认配置。窗口字段沿用模板结构:按用户选择,两个 Claude 条目的默认 `context_window` 为 `400000`,`max_context_window` 保留 `1000000`;DeepSeek Flash 的两个字段均为 `1000000`。压缩阈值保留模板的 `auto_compact_token_limit: null`,由客户端按窗口计算。Claude Messages 要求 `max_tokens`,因此两个 Claude 条目提供 `128000` 默认预算;GPT 和 DeepSeek 的输出预算由请求、用户配置或上游默认值决定。
36
36
 
37
37
  依据:[Claude 模型规格](https://platform.claude.com/docs/en/models/sonnet-5-5/overview)、[Messages 必填参数](https://platform.claude.com/docs/en/api/messages/create)、[DeepSeek 模型规格](https://api-docs.deepseek.com/quick_start/pricing/)。这些是第三方精调的依据,`source` 仍记录 Codex 模板来源。
38
38
 
@@ -9,7 +9,7 @@
9
9
  "adjustments": [
10
10
  "以 GPT-6.1 Sol 的完整条目为起点;保留 Code Mode Only、Responses Lite、多代理和工具元数据。",
11
11
  "将模型标识和展示信息改为 Claude Opus 5.5;instructions_template 仅将 based on GPT-6 改为 based on Claude,保留 Codex 代理身份和其余客户端提示词。",
12
- "context_window 和 max_context_window 设为 1000000;Messages API 必填 max_tokens,因此目录提供 max_output_tokens 默认值 128000,部署配置和请求可明确覆盖。",
12
+ "按用户选择将默认 context_window 设为 400000,max_context_window 保留 1000000;Messages API 必填 max_tokens,因此目录提供 max_output_tokens 默认值 128000,部署配置和请求可明确覆盖。",
13
13
  "默认 effort 为 medium;Opus 5.5 不支持关闭 thinking,目录仅列出 low 至 max 与 ultra;未列出的显式请求值继续由上游裁决。",
14
14
  "关闭尚未转换的 verbosity 和 original image detail 声明;保留普通图片输入。",
15
15
  "清空 GPT 专属的服务优先级、速度档位、推广和升级提示。",
@@ -55,7 +55,7 @@
55
55
  "requires_sandboxed_review": false,
56
56
  "auto_review_model_override": null,
57
57
  "model_specialty": null,
58
- "context_window": 1000000,
58
+ "context_window": 400000,
59
59
  "max_context_window": 1000000,
60
60
  "auto_compact_token_limit": null,
61
61
  "comp_hash": "3000",
@@ -9,7 +9,7 @@
9
9
  "adjustments": [
10
10
  "以 GPT-6.1 Sol 的完整条目为起点;保留 Code Mode Only、Responses Lite、多代理和工具元数据。",
11
11
  "将模型标识和展示信息改为 Claude Sonnet 5.5;instructions_template 仅将 based on GPT-6 改为 based on Claude,保留 Codex 代理身份和其余客户端提示词。",
12
- "context_window 和 max_context_window 设为 1000000;Messages API 必填 max_tokens,因此目录提供 max_output_tokens 默认值 128000,部署配置和请求可明确覆盖。",
12
+ "按用户选择将默认 context_window 设为 400000,max_context_window 保留 1000000;Messages API 必填 max_tokens,因此目录提供 max_output_tokens 默认值 128000,部署配置和请求可明确覆盖。",
13
13
  "默认 effort 为 medium;Sonnet 5.5 的最低 thinking 模式 between_tools 仍会在工具之间思考,不以 none 宣传;可用 options 显式选择,未列出的请求值继续由上游裁决。",
14
14
  "关闭尚未转换的 verbosity 和 original image detail 声明;保留普通图片输入。",
15
15
  "清空 GPT 专属的服务优先级、速度档位、推广和升级提示。",
@@ -55,7 +55,7 @@
55
55
  "requires_sandboxed_review": false,
56
56
  "auto_review_model_override": null,
57
57
  "model_specialty": null,
58
- "context_window": 1000000,
58
+ "context_window": 400000,
59
59
  "max_context_window": 1000000,
60
60
  "auto_compact_token_limit": null,
61
61
  "comp_hash": "3000",
@@ -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)) {
@@ -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.type === "bridge_reasoning") {
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
- a.content += (a.content && text ? "\n" : "") + text;
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();
@@ -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
- delete item.id;
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.");
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
- if (!statePayload(decoded))
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) => {
@@ -101,6 +101,7 @@ export class CompactionHandoffStore {
101
101
  }
102
102
  /** Only checkpoint predecessors enter the export; later work stays on the target. */
103
103
  async materialize(context) {
104
+ rejectUnrecognizedState(context.request.input, context.route);
104
105
  if (!Array.isArray(context.request.input))
105
106
  return context.request;
106
107
  let changed = false;
package/dist/config.js CHANGED
@@ -103,6 +103,9 @@ export function validateConfig(value) {
103
103
  if (model.replay !== undefined) {
104
104
  if (!object(model.replay))
105
105
  fail(`model ${slug}: replay must be an object`);
106
+ if (model.replay.unrecognized !== undefined && !["passthrough", "drop", "error"].includes(model.replay.unrecognized)) {
107
+ fail(`model ${slug}: replay.unrecognized must be passthrough, drop, or error`);
108
+ }
106
109
  for (const field of ["reasoning_from", "native_compaction_from"]) {
107
110
  if (model.replay[field] !== undefined && !strings(model.replay[field]))
108
111
  fail(`model ${slug}: ${field} must list source model aliases`);
@@ -155,7 +158,10 @@ export function resolveModel(config, slug) {
155
158
  const allowed = (names = []) => new Set([source.id, ...names.map(name => origin(config, config.models[name]).id)]);
156
159
  return {
157
160
  slug, config: model, upstream: config.upstreams[model.upstream], profile, origin: source,
158
- replay: { reasoning: allowed(model.replay?.reasoning_from), native_compaction: allowed(model.replay?.native_compaction_from) },
161
+ replay: {
162
+ reasoning: allowed(model.replay?.reasoning_from), native_compaction: allowed(model.replay?.native_compaction_from),
163
+ unrecognized: model.replay?.unrecognized ?? "passthrough",
164
+ },
159
165
  compaction: {
160
166
  mode: model.compaction?.mode ?? (profile.native_compaction_format ? "native" : "summary"),
161
167
  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 && replaysReasoning(carrier, route)) {
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
- throw new BridgeError("This compaction item has no portable summary or handoff recipe; use history created through this bridge");
90
+ unrecognized(item);
91
+ continue;
67
92
  }
68
93
  if (restoresNativeCompaction(carrier, route)) {
69
- result.push(carrier.native.data);
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)
@@ -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;
@@ -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: { reasoning_from: names(route.replay.reasoning), native_compaction_from: names(route.replay.native_compaction) },
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
- let carrier;
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 { ...base, decision: "drop", reason: "not_bridge_state", source: null, format: null, decoded: null };
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 { ...base, decision: "reject", reason: "not_bridge_state", source: null, format: null, decoded: null };
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
- let carrier;
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/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";
@@ -36,6 +37,7 @@ export interface ModelConfig {
36
37
  replay?: {
37
38
  reasoning_from?: string[];
38
39
  native_compaction_from?: string[];
40
+ unrecognized?: UnrecognizedStatePolicy;
39
41
  };
40
42
  compaction?: {
41
43
  mode?: "summary" | "native";
@@ -87,6 +89,7 @@ export interface ResolvedModel {
87
89
  replay: {
88
90
  reasoning: ReadonlySet<string>;
89
91
  native_compaction: ReadonlySet<string>;
92
+ unrecognized: UnrecognizedStatePolicy;
90
93
  };
91
94
  compaction: {
92
95
  mode: "summary" | "native";
@@ -30,7 +30,7 @@ Opus 5.5 始终启用 thinking;Sonnet 5.5 的最低模式是 `between_tools`
30
30
 
31
31
  未映射的显式 effort 仍交给上游判断;客户端显式请求 `none` 时,adapter 仍发送 `thinking.type: "disabled"`,由上游返回错误。目录精调不构成运行时能力预检。
32
32
 
33
- Opus / Sonnet 5.5 目录的默认及最大上下文窗口为 `1000000`,Messages API 要求提供 `max_tokens`,因此目录设置默认输出预算 `128000`,对应官方标准最大输出。输出预算依次取请求 `max_output_tokens`、模型配置、目录默认值,再接受上游和模型 options 的覆盖;这是允许的上限,不要求每次生成同样长度。[模型规格](https://platform.claude.com/docs/en/models/sonnet-5-5/overview)、[配置优先级](config.md)
33
+ Opus / Sonnet 5.5 目录的默认上下文窗口为 `400000`,最大上下文窗口为 `1000000`,Messages API 要求提供 `max_tokens`,因此目录设置默认输出预算 `128000`,对应官方标准最大输出。输出预算依次取请求 `max_output_tokens`、模型配置、目录默认值,再接受上游和模型 options 的覆盖;这是允许的上限,不要求每次生成同样长度。[模型规格](https://platform.claude.com/docs/en/models/sonnet-5-5/overview)、[配置优先级](config.md)
34
34
 
35
35
  完整 `thinking`/`redacted_thinking` block,包括签名或隐藏数据,存入 `anthropic.thinking.v1` carrier。请求按 `replay.reasoning_from` 恢复允许来源,签名完整保留;其他来源只在当前请求过滤。允许配对不意味着上游一定接受其签名。[Reasoning 策略](reasoning.md)
36
36
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  随后浅层合并上游和模型 `options`,后者覆盖前者,也可覆盖推导出的普通参数。最后由 bridge 固定 `model`、`input`、`tools`、`store: false`、`stream: true`,`include` 至少包含 `reasoning.encrypted_content`。上游状态由完整历史承载,不依赖服务器保存的会话。
12
12
 
13
- 历史 reasoning 按允许来源选择,将 `responses.reasoning.v1` 的 data 还原为原生密文;原生压缩按独立策略选择。每条输入项移除顶层 `id`,工具配对的 `call_id` 保留,避免引用已过滤的 reasoning。图片、普通消息、工具结果和明文 agent message 保留原格式。[状态规则](reasoning.md)
13
+ bridge 能识别的历史 reasoning 按允许来源选择,将 `responses.reasoning.v1` 的 data 还原为原生密文;原生压缩按独立策略选择。未识别状态由 `replay.unrecognized` 决定处理方式,默认保留整项,包括其 `id`。其余输入项移除顶层 `id`,工具配对的 `call_id` 保留,避免引用已过滤的 reasoning。图片、普通消息、工具结果和明文 agent message 保留原格式。[状态规则](reasoning.md)
14
14
 
15
15
  ## 流与工具交付
16
16
 
@@ -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
- 恢复不改写客户端保存的载荷。按需复述只更新本次请求的副本,并通过本地缓存复用;切换模型再切回来,仍可使用原检查点。再次压缩后,新检查点取代旧检查点,不累积每个模型的检查点版本。外部原生检查点若没有 bridge 摘要或复述配方,不能保证跨目标恢复,因此明确报错。
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
@@ -83,14 +83,14 @@
83
83
  | `max_output_tokens` | 覆盖目录的默认回答输出预算;请求中的显式值优先于模型配置 |
84
84
  | `input_modalities` | 可选,覆盖条目中的 `text`/`image` 能力声明 |
85
85
  | `reasoning` | 默认 effort 及其到原生值的映射 |
86
- | `replay` | 分别允许哪些来源的 reasoning、原生压缩 |
86
+ | `replay` | reasoning、原生压缩的允许来源,以及未识别历史状态的处理方式 |
87
87
  | `compaction` | 压缩方式、执行模型,以及本目标是否接收原生压缩 |
88
88
  | `options` | 原生请求参数,覆盖上游级 options |
89
89
  | `adapters` | 当前模型的桥接行为;逐字段覆盖上游级同名 adapter 设置 |
90
90
 
91
91
  `expose` 控制目录展示,不改变别名的路由、来源身份或访问权限。隐藏模型仍可用于压缩、handoff 和状态复用,显式按别名发送请求也仍可调用;inspector 保留其诊断记录。`config-snippet` 选择首个公开别名作为默认模型;全部隐藏时目录为空,该命令明确报错。
92
92
 
93
- 不设置窗口时,原条目的两个窗口字段各自保留。当前 GPT 条目为 `context_window: 272000`、`max_context_window: 872000`;Opus 5.5、Sonnet 5.5 和 DeepSeek Flash 的两个字段均为 `1000000`。压缩阈值沿用模板的 `auto_compact_token_limit: null`,由客户端按窗口计算。Claude Messages 必须提供 `max_tokens`,对应目录外层默认预算为 `128000`;GPT 和 DeepSeek 使用请求、模型配置或上游提供的默认值。原生 options 也可调整预算,实际限制由部署端点裁决。
93
+ 不设置窗口时,原条目的两个窗口字段各自保留。当前 GPT 条目为 `context_window: 272000`、`max_context_window: 872000`;Opus 5.5 和 Sonnet 5.5 为 `context_window: 400000`、`max_context_window: 1000000`;DeepSeek Flash 的两个字段均为 `1000000`。压缩阈值沿用模板的 `auto_compact_token_limit: null`,由客户端按窗口计算。Claude Messages 必须提供 `max_tokens`,对应目录外层默认预算为 `128000`;GPT 和 DeepSeek 使用请求、模型配置或上游提供的默认值。原生 options 也可调整预算,实际限制由部署端点裁决。
94
94
 
95
95
  请求先由 adapter 转换,再浅层合并 `upstream.options`、`model.options`;后者优先,嵌套对象整体替换。例如模型 `options.output_config` 会替换推导值和上游同名对象。由此可以显式调整 thinking budget、原生 effort 或端点专有字段。
96
96
 
@@ -224,12 +224,32 @@ Claude 的 `native_replay` 仍默认为 `false`:委托生成不会让 Claude
224
224
  }
225
225
  ```
226
226
 
227
- 自身状态默认允许恢复;两份列表增加其他允许来源,关系有方向且不传递。若也要接受 A 的 reasoning,把 A 加入 B 的 `reasoning_from`。这个例子的 B 自己只生成文字摘要,但仍可读取 A 的原生检查点:生成和恢复是独立能力。设置 `native_replay: false` 则所有压缩都使用文字表示,包括自身生成的检查点。
227
+ bridge 能识别的自身状态默认允许恢复;两份列表增加其他允许来源,关系有方向且不传递。若也要接受 A 的 reasoning,把 A 加入 B 的 `reasoning_from`。这个例子的 B 自己只生成文字摘要,但仍可读取 A 的原生检查点:生成和恢复是独立能力。设置 `native_replay: false` 则已识别的压缩都使用文字表示,包括自身生成的检查点。
228
228
 
229
229
  引用按连接、实际模型和 adapter 解析为来源身份,不绑定显示别名。加载配置时检查来源是否存在、源与目标的格式标识是否一致;每条请求恢复时再检查实际载荷。bridge 不探测密文是否被上游接受。上游拒绝被允许的状态时直接报错,不删掉状态再试。详见 [reasoning](reasoning.md) 和[压缩](compaction.md)。
230
230
 
231
231
  连接身份包含 upstream ID 和去掉末尾斜杠的 URL,来源身份再包含 adapter 与实际模型。密钥、header 和 options 不参与身份。如果同一 URL 通过 header 或凭据切换到不兼容区域,使用不同 upstream ID;改显示别名不影响已保存状态。
232
232
 
233
+ ### 未识别的历史状态
234
+
235
+ 历史会话可能带有其他工具或旧版本保存的状态。模型级 `replay.unrecognized` 统一处理 `encrypted_content` 无法解码为相应 bridge carrier 的 `reasoning`、`compaction` 和 `compaction_summary` 项,包括未知前缀、缺少载荷或无法解析的封装:
236
+
237
+ | 值 | 行为 |
238
+ | --- | --- |
239
+ | `passthrough`(默认) | 保留整项及原始字段,交给上游解释 |
240
+ | `drop` | 仅从本次出站历史中过滤这些项 |
241
+ | `error` | 在发送上游请求前报错 |
242
+
243
+ 例如,在需要明确拒绝此类历史的模型中配置:
244
+
245
+ ```json
246
+ { "replay": { "unrecognized": "error" } }
247
+ ```
248
+
249
+ GPT 将透传项保留在 Responses `input` 中,包括其 `id`;Claude/DeepSeek 将原项放入 assistant 消息的 content 数组,不把未知密文猜解为 thinking 或文字。这保留了原始数据,但不保证跨协议或跨模型兼容,上游可以拒绝;bridge 不自动重试或改用其他策略。
250
+
251
+ 这些项没有可确认的来源与复述配方,因此不套用来源允许列表、不创建来源身份,也不触发按需复述。能解码的 bridge carrier 仍遵循原有来源、格式和压缩表示规则。三个选项都不改写客户端已保存的历史;`drop` 会减少本次请求的 reasoning 上下文,丢弃压缩检查点还可能失去早期对话的唯一表示。普通请求取工作模型的策略,委托压缩取实际执行模型的策略。
252
+
233
253
  ## 客户端目录
234
254
 
235
255
  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
- 每次请求按当前配置选取历史,不改写已保存载荷。Reasoning 和原生压缩有独立的允许来源列表,默认只接受自身,关系有方向且不传递。修改配置即可改变已有历史的恢复方式;格式版本负责解释数据,不编码“永远隔离”这类策略。
55
+ 每次请求按当前配置选取历史,不改写已保存载荷。bridge 能识别的 reasoning 和原生压缩有独立的允许来源列表,默认只接受自身,关系有方向且不传递。修改配置即可改变已有历史的恢复方式;格式版本负责解释数据,不编码“永远隔离”这类策略。
56
+
57
+ 未识别状态默认透传,因为历史会话可能来自其他客户端或版本,bridge 无法解码不等于上游不能使用。模型级 `replay.unrecognized` 也可显式选择过滤或报错;不为这类状态猜测来源或生成 handoff。它与已识别载荷的来源策略分开,保留未来协议扩展与用户选择。[配置与协议边界](config.md#未识别的历史状态)
56
58
 
57
59
  明文 reasoning 使用可共享的文本格式;不同 adapter 将来可把同一格式恢复到各自字段。密文与签名保留各自原生结构,不把格式可解码等同于上游允许使用。
58
60
 
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` | `not_bridge_state` | 不是 bridge 封装的状态,例如直接来自其他 provider 的密文 |
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
- 每条标注附来源别名、部署名、adapter、来源指纹、载荷格式和解码后的 carrier。`cxbr1:` 与 `cxbc1:` 只是编码,解码后可以看到文字 reasoning、压缩摘要和原生载荷;GPT 原生 `encrypted_content` 本身仍是密文,只能看到原样字符串。
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
- Reasoning 默认只恢复给产生它的目标,配置可以允许其他来源。是否允许复用和如何解码是两件事:来源策略由 `replay.reasoning_from` 决定,载荷的 `format` 告诉 adapter 如何还原内容。
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,其他 reasoning 只在本次请求中过滤。普通消息、工具调用、结果和图片继续保留。没有来源包装的 reasoning 不归属到当前目标。
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 出站请求移除 item 顶层 `id`,保留工具配对的 `call_id`,使用 `store: false`。这样消息不会继续通过服务器 item ID 引用已过滤的 reasoning。请求必须包含完整历史,不能只带 `item_reference` 或 `previous_response_id`。
72
+ GPT 出站请求使用 `store: false`,除原样透传的未识别状态项外,移除 item 顶层 `id`,保留工具配对的 `call_id`。这样普通消息不会继续通过服务器 item ID 引用已过滤的 reasoning;未识别状态的原始字段由上游解释。请求必须包含完整历史,不能只带 `item_reference` 或 `previous_response_id`。
73
73
 
74
74
  压缩另用 `cxbc1:`,有独立的允许来源,保存文字摘要或按需复述配方。详见[压缩](compaction.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wacry/codeks-bridge",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",
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
- not_bridge_state: '不是 bridge 载荷(无 cxbr1 前缀)',
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
- not_bridge_state: '不是 bridge 检查点(无 cxbc1 前缀)',
38
- malformed: 'bridge 检查点损坏,无法解码',
36
+ unrecognized_state: '无法识别为 bridge 压缩检查点,按 replay.unrecognized 处理',
39
37
  },
40
38
  };
41
39