CompactGate 是一个给 Codex CLI 和 Claude Code 用的本地路由与协议适配代理。
它的作用很简单:
- 普通请求继续发到你的主上游
- 只有 compact 请求单独分流
- 必要时自动改写 compact 模型名
- Codex Responses 与 Claude Messages 可以接入不同格式的兼容上游
- 你还能在本地页面里看配置、健康状态和最近日志
如果你现在还不清楚 “compact 请求” 是什么,也没关系。你只要记住:
CompactGate 让 Codex 在“正常对话”和“压缩上下文”这两类请求上,可以走不同的上游。
很多时候你会遇到下面几种情况:
- 你的主上游能跑正常对话,但不支持 compact 模型。
- 你想把 compact 请求单独走另一家兼容 OpenAI API 的服务。
- 你想保留原来的 Codex 使用方式,只在中间加一层本地代理。
- 你想在本地看最近请求都走到了哪条路由,并保留完整请求 / 响应正文方便排查。
- 你的客户端和上游分别使用 Responses、Messages 或 Chat Completions,需要在代理层转换。
CompactGate 就是为这个场景做的。
接入后,链路会变成这样:
Codex -> CompactGate -> 你的上游服务
CompactGate 会按规则转发:
普通 /v1/* 请求 -> primary 主上游
/v1/responses/compact -> compact 策略
/v1/responses + request_kind: "compaction" -> compact 策略
/v1/responses + input[].type: "compaction_trigger" -> primary 上游策略(逻辑记录为 remote_v2)
新版 Codex 的上下文压缩有三种线上的请求形态:专用 /responses/compact 的 Remote V1、带 compaction_trigger 且仍走普通 /responses 的 Remote V2,以及由 x-codex-turn-metadata 标记为 request_kind: "compaction" 的本地摘要压缩。CompactGate 会识别三种形态;Remote V2 复用 primary 模型、凭据和协议,不要求上游提供 compact 模型。Responses 上游原生转发,Anthropic Messages 转成原生 compaction,Chat 上游转成普通摘要请求并返回 CompactGate 签名状态。没有这些精确信号的普通请求仍走 primary。
Remote V1 与 Remote V2 不是同一路径换了名字:
| 项目 | Remote V1 | Remote V2 |
|---|---|---|
| 请求入口 | /v1/responses/compact |
/v1/responses |
| 真实触发信号 | 专用 compact 路径 | input[].type: "compaction_trigger" 或 responses_compaction_v2 元数据 |
| 实际上游 | split 时走独立 compact.base_url |
始终复用 Primary 上游与协议 |
| 模型策略 | 可改写为 compact 模型 | 保留 Primary 模型和 reasoning 策略 |
| 状态处理 | 支持响应归一化、桥接和短期去重 | Responses 保留 provider state;Messages/Chat 使用签名 cg1_ 可移植摘要 |
| 日志 | route=compact、compaction_mode=remote_v1 |
route=compact、compaction_mode=remote_v2 |
OpenAI Codex 官方 0.140.0 发布说明把 Remote Compaction V2 改为默认启用,因此 CompactGate 把 0.140.0 记作“V2 默认起点”,而不是“V1 被删除”的硬切换点。账号灰度、配置、桌面内置 CLI 和二开版本仍可能改变实际行为,所以 Studio 按“实际请求 > 历史观测 > 本机版本基线”的顺序展示协议;所有兼容路径始终保留。
服务启动时会执行一次有超时保护的 codex --version,之后每 6 小时刷新本机版本。真实请求里的 User-Agent 更权威,例如 codex-tui/0.144.1-cometix 会拆成原始版本 0.144.1-cometix、官方数字基线 0.144.1 和二开变体 cometix;如果它实际发送 V1,Studio 仍显示 V1,不会因为数字基线较新而强制标成 V2。
你只需要把 Codex 的 base_url 改成 CompactGate,本地代理就会替你处理剩下的事情。
npm installcp compactgate.example.json compactgate.json先至少改这几个值:
primary.base_url:你的主上游地址compact.base_url:你的 compact 上游地址primary.api_key_env:主上游密钥环境变量名compact.api_key_env:compact 上游密钥环境变量名
如果你暂时只想走一套上游,也可以把:
"upstream_mode": "primary"这样 compact 请求也会走主上游。
export PRIMARY_API_KEY="你的主上游密钥"
export COMPACT_API_KEY="你的 compact 上游密钥"如果你不想用环境变量,也可以先启动服务,再去 Studio 页面里直接保存 API Key。
npm run build
npm start启动后打开:
http://127.0.0.1:7865/
这里就是 CompactGate Studio。
构建后可以直接用一次性启动器,不修改 ~/.codex/config.toml:
node dist/server/cli.js agent codex -- --model gpt-5.5
node dist/server/cli.js agent codex --profile PROFILE_ID -- --model gpt-5.5通过 npm link 或包管理器安装 bin 后,可将 node dist/server/cli.js 换成
compactgate。--url 可覆盖默认的 http://127.0.0.1:7865,-- 后的参数
原样传给 Codex。
把 Codex 的 OpenAI 兼容 base_url 指向 CompactGate:
model_provider = "compactgate"
model = "gpt-5.5"
[model_providers.compactgate]
name = "OpenAI"
wire_api = "responses"
requires_openai_auth = true
base_url = "http://127.0.0.1:7865/v1"这里有一个非常重要的点:
name = "OpenAI"
wire_api = "responses"这两项不要随便改,尤其是 name 必须保持 "OpenAI",否则 Codex 可能不会启用预期的 Responses 和远程压缩能力。不同 Codex 版本不一定都调用 /v1/responses/compact,也可能使用上面另外两种压缩请求形态。
同一个启动器通过进程内 --settings 指向 CompactGate,不写
~/.claude/settings.json:
node dist/server/cli.js agent claude
node dist/server/cli.js agent claude --profile PROFILE_ID -- --model sonnet启动器把 ANTHROPIC_BASE_URL 设为 http://127.0.0.1:7865/anthropic;指定
profile 时,再通过 ANTHROPIC_CUSTOM_HEADERS 注入请求级
x-compactgate-profile。--url 与 -- 的语义和 Codex 启动器一致。
下面是一个最常见的配置例子:
{
"listen": "127.0.0.1:7865",
"primary": {
"base_url": "https://primary.example/v1",
"api_key_env": "PRIMARY_API_KEY",
"upstream_protocol": "openai_responses",
"model_override": "",
"reasoning_effort": "",
"state_domain_id": ""
},
"compact": {
"base_url": "https://compact.example/v1",
"api_key_env": "COMPACT_API_KEY",
"upstream_protocol": "openai_responses",
"upstream_mode": "split",
"model_mode": "linked",
"model_template": "{model}-openai-compact",
"model_override": ""
},
"timeouts": {
"primary_ms": 120000,
"compact_ms": 900000
},
"logging": {
"redact_body": true,
"persist_body": false,
"keep_recent": 200,
"capture_dir": null,
"capture_body_max_bytes": 1048576,
"capture_dir_max_bytes": 21474836480,
"max_database_bytes": 1073741824
},
"primary_failover": {
"auto_schedule": true,
"state_portability": "recover_on_error"
}
}Codex 的 primary.base_url 和 compact.base_url 表示完整上游 API 根,例如 https://host/v1 或 https://host/api/paas/v4。CompactGate 会精确移除客户端请求开头的 /v1,再把剩余端点拼到该 API 根,因此不会因拼接额外生成 /v1/v1/responses,也不会破坏 /v4 这类供应商版本路径。
Claude 的 claude.primary.base_url 表示上游主机或供应商前缀,例如 https://api.anthropic.com 或 https://host/anthropic。/v1/messages 会追加到该前缀;如果 base URL 已以完整 /v1 段结尾,则拼接边界只保留一个 /v1。Claude Messages POST 返回 404 时,CompactGate 不会猜测并重试其他路径。
主路由与压缩路由都保存明确的上游协议。可选值为:
openai_responses:OpenAI Responses APIanthropic_messages:Anthropic Messages APIopenai_chat:OpenAI Chat Completions API
旧配置不需要手工迁移:Codex 的 primary / compact 默认保持
openai_responses,Claude 的 claude.primary / claude.compact 默认保持
anthropic_messages。同协议请求继续直通,只有协议不同时才转换请求和响应。
| 客户端入口 | Responses 上游 | Messages 上游 | Chat 上游 |
|---|---|---|---|
Codex /v1/responses |
直通 | 转换 | 转换 |
Codex /v1/responses/compact |
直通 | 原生或可移植压缩转换 | 明确拒绝 |
Claude /anthropic/v1/messages |
转换 | 直通 | 转换 |
Claude /anthropic/v1/messages/count_tokens |
转换为 /v1/responses/input_tokens |
直通 | 返回 501 |
协议转换覆盖文本、图片、自定义 function tools、tool result、JSON/SSE、usage 和错误响应。Responses 与 Messages 之间还会保留可移植的 reasoning/compaction 状态;无法安全移植的 provider-private 状态会明确失败。Chat 路径只承诺无状态的 文本、图片和 function tool 交互;reasoning、compaction、structured output、 provider tools 和多 choice 不会被静默丢弃,而是在请求发送前或流解析时返回明确错误。
Claude Code 的手工摘要提示仍按现有规则走 claude.primary,CompactGate 不根据
提示词猜测压缩流量;claude.compact 继续作为独立配置、凭据和 profile 元数据保存。
logging.keep_recent 控制 Studio 首屏和 /api/logs/recent 默认返回多少条日志,范围是 1 到 2000,不是 SQLite 日志保留上限。SQLite 请求日志默认最多占用 1 GiB;超过后先清空历史正文并保留元数据,仍超限时才按时间顺序删除最早的元数据行。
推荐使用分离存储:保持 logging.persist_body = false,再设置 logging.capture_dir。每个代理请求会写入一份独立 JSON,单段正文默认最多 1 MiB,受管抓包文件合计默认最多 20 GiB。目录超限时只删除最旧抓包,SQLite 元数据继续保留并把 capture_status 标为 purged。把 capture_dir 热更新为 null 可停止新抓包;COMPACTGATE_CAPTURE_DIR 和 COMPACTGATE_CAPTURE_BODY_MAX_BYTES 仍优先于配置文件。
logging.redact_body 是为旧配置保留的兼容字段,当前不会改变 SQLite 正文或抓包正文;鉴权头、Cookie 头和常见凭据查询参数始终单独脱敏。Studio 不提供这个无效开关,保存其他日志设置时也不会重写它。
最重要的字段只有这些:
普通请求走的主上游。
primary.model_override 非空时覆盖请求模型;primary.reasoning_effort 可设为 low、medium、high、xhigh 或 max,仅对 Responses API 写入 reasoning.effort。两者留空都表示保留客户端请求。旧配置中的 none 仍会按原值转发,但不会出现在 Studio 下拉选项中;需要取消覆盖时应改为“跟随请求”。
primary.state_domain_id 用来声明该 profile 的 provider 状态域。保存的 profile 留空时按 profile ID 隔离;没有 profile 的直连配置才回退到上游 URL origin。只有两个 profile 明确配置相同值时,CompactGate 才把它们视为可直接共享 encrypted reasoning/compaction 状态。
旧对话切换 profile 后失败的根因不是路由仍指向旧 host,而是请求正文携带了旧上游签发的 provider-private state。welfare 抓包样本包含 59 个 reasoning item(其中 15 个 encrypted_content 为 null 或格式无效)、1 个 compaction item 和 51 组工具调用;原实现切换 host 后仍把这些状态逐字节发给新上游,而新对话没有这批外域状态,所以新对话可以成功。CPA/CLIProxyAPI 只验证 GPT reasoning 密文的外层格式,不能证明新上游可解密;sub2api 还会从不可变正文重建每次尝试、跨 passthrough 边界删除整个 reasoning,并在明确的 400 invalid_encrypted_content 后定向重试。CompactGate 组合这些边界,并额外处理 compaction、工具配对、状态域绑定和流式响应提交点。
off:不做旧会话迁移。recover_on_error:默认值。第一次始终把当前请求原样发给选中的 profile;只有明确的可修复 400,或具有目标健康和状态域证据的兼容性失败,才从原始请求体派生恢复请求。旧配置值compatibility_first和domain_aware在加载时归一化为此模式。
明确 400 invalid_encrypted_content 或 previous_response_not_found 直接执行一次错误专用修复。修复后仍返回同一明确 400 时,若跨域证据成立仍可继续 strict。welfare 一类泛化失败必须同时满足:请求含 provider-owned state、目标 profile 最近 15 分钟有同模型/端点的无状态成功,以及已知状态域不匹配;来源未知时还要求 10 分钟内出现两次相同失败。恢复链为原始请求、CPA 低损清理、严格跨域清理;错误专用修复按错误位置插入。所有请求固定在当前 profile/host,最多发送 4 个不同 body,且每次都从同一份不可变请求体派生。只有完整成功响应才更新持久绑定。
通过 POST /api/config/profiles/apply 手动应用 Codex profile 是一次权威切换:CompactGate 会清除旧的进程内会话 stickiness,并强制下一次 Primary 选择新 profile,即使 auto_schedule=true。自动调度内部同步 active profile 不触发该强制逻辑。
compact 请求走的上游。
CompactGate 只在以下精确信号出现时标记为压缩流量:路径是 /v1/responses/compact(remote_v1);/v1/responses 的 Codex turn metadata 中 request_kind 是 compaction(local,实现为 responses_compaction_v2 时为 remote_v2);或者 input 中存在 type: "compaction_trigger"(remote_v2)。body 中的 client_metadata 优先于兼容请求头。压缩成功后的普通 /v1/responses 后续 turn 即使携带 type: "compaction" 和 V2 beta 标记,也保持普通 primary 日志;Responses provider state 原样保留,CompactGate 签名状态可还原为摘要消息。它不会根据提示词、模型名或 token 数猜测请求用途。Remote V2 的真实压缩动作仍使用 Primary 上游。
可选值:
split:local/Remote V1 compact 请求走compact.base_urlprimary:local/Remote V1 compact 请求也走primary.base_url
Remote V2 始终走 primary 上游,不受此项切换,也不会改写为 compact 模型。
可选值:
linked:按模板改写模型名custom:无论原模型是什么,都改成固定模型
当 model_mode = "linked" 时使用。
比如:
{model}-openai-compact
如果原模型是:
gpt-5.5
那么 compact 请求会被改写成:
gpt-5.5-openai-compact
当 model_mode = "custom" 时使用。
例如你可以固定改成:
my-compact-model
primary、compact、claude.primary、claude.compact 都支持:
{
"extra_headers": {
"x-provider-feature": "enabled"
},
"proxy_url": "http://user:pass@127.0.0.1:8080"
}extra_headers 会随 profile 保存,但禁止认证、Cookie、长度和 hop-by-hop 头;
值不会从 GET /api/config 返回,抓包也按配置头名动态脱敏。proxy_url 仅接受
HTTP CONNECT 代理,显式配置优先于环境代理且非法值直接失败,不会静默直连。
claude.scene_map 可为 long_context、background、web_search、thinking、
image 和 default 指定 profile_id 与可选 model。long_context_bytes 使用
UTF-8 文本字节数判断,图片 base64 不计入。优先级固定为:显式请求 profile、
长上下文、后台任务、Web 搜索、思考、图片、默认。
Codex 与 Claude 请求都可携带 x-compactgate-profile: PROFILE_ID 做单次精确选择;
仅 loopback 客户端可用,转发前会删除,并且不会修改 active profile、健康度或粘性。
打开 http://127.0.0.1:7865/ 后,你可以直接:
- 修改主上游和 compact 上游地址
- 为 Codex/Claude 的主路由和压缩路由选择上游协议
- 切换
split/primary模式 - 切换 linked / custom 模型改写方式
- 从已保存的 Primary/Claude 上游按需拉取模型并选择思考强度,或保留自定义兼容模型
- 直接保存 API Key
- 预览某条请求会走哪条路由
- 查看健康状态
- 实时查看最近日志
配置页六个 Tab 分别使用 /config/profiles、/config/routes、/config/model、/config/logging、/config/preview 和 /config/portable,可通过浏览器前进、后退回顾切换历史。
模型目录不会在页面加载时自动联网。CompactGate 会根据已保存的 base URL 生成 OpenAI-compatible /v1/models 或 /models 候选;仅当端点返回 404/405 时尝试下一条路径。Primary 使用 Bearer 凭据,Claude 保留 Anthropic 兼容鉴权头。修改 URL 或凭据草稿后需先保存,再拉取对应上游目录。
日志是实时刷新的,使用的是 SSE。
默认情况下,日志会记录这些信息:
- 路由类型
- 状态码
- 模型映射
- 实际发送给 Primary Responses 上游的
reasoning.effort - 上游主机
- 耗时
- request id
- 可选的客户端请求体
- 可选的实际上游请求体
- 可选的上游响应体
元数据始终持久化到本地 SQLite。只有 logging.persist_body = true 时正文才进入 SQLite;推荐保持关闭并通过有界抓包目录按需诊断。Studio 日志列表不返回正文或本机抓包路径,展开详情后也只有点击“查看抓包”才会加载原始内容。抓包查看器会对“客户端请求 → 上游请求”和“上游响应 → 客户端响应”做最多 200 项的结构化 JSON 对比;正文截断或不是 JSON 时明确标为不可比较。压缩日志还保存实现名、请求 compaction/trigger 数和响应 compaction 数,便于定位协议漂移。
请求元数据会持久化到 SQLite,不按页面展示数量自动删除。数据库文件、WAL 和 SHM 侧写文件合计默认上限为 1 GiB。超过上限时,CompactGate 先清空四段历史正文并把 body_status 标为 purged;回收后仍超限才删除最早的元数据行。维护任务执行 SQLite checkpoint/vacuum 回收磁盘空间。
默认文件位置是:
compactgate-logs.sqlite
日志库路径固定由配置文件路径派生。例如 compactgate.json 对应同目录的 compactgate-logs.sqlite。
如果你需要把单次请求写成独立 JSON 文件,包含脱敏后的请求头和正文,可以临时打开调试捕获:
COMPACTGATE_CAPTURE_DIR=/path/to/captures npm start也可以在 Studio 的“配置 → 日志存储”中选择“分离存储”并设置目录。默认不开启抓包。
如果你想自己对接页面或脚本,可以用这些接口:
查看服务和上下游状态。
查看当前运行配置。
注意:这个接口不会返回明文 API Key。
导出完整配置。
这是本地可移植备份,会保留配置中直填的 API Key;GET /api/config 才是用于页面展示的脱敏配置。
每次配置持久化前还会在同目录创建 0600 版本备份并保留最近 10 份:
GET /api/config/backups:列出备份元数据POST /api/config/backups/restore:传{"backup_id":"...","confirm":true}恢复DELETE /api/config/backups:传相同确认结构删除
恢复前会先校验 JSON 和完整配置合同;失败不会改变内存或当前配置文件。
热更新配置并写回磁盘,不需要重启。
预览一条请求最终会怎么路由、怎么改模型。
传 {"model":"MODEL"} 对当前 compact 路径执行一次真实、30 秒上限、512 KiB
响应上限的原生压缩探测。探测不会写日志、bridge、去重、profile 健康或故障转移
状态;Chat 上游直接返回不支持,网络/上游错误通过 supported=false 返回。
分页读取日志。默认返回 logging.keep_recent 条;历史记录可以用 limit 和 offset 继续读取。
可以加筛选:
?route=primary
?route=compact
?host=api.example.com
?limit=200&offset=200
按响应头 x-compactgate-request-id 查询单条元数据日志、正文状态与抓包生命周期状态。本机抓包路径和正文内容不会返回。不存在返回 404;旧数据库中若有重复请求 ID,返回 409 且不会删除历史日志。
按需读取受管抓包。写入中返回 202,从未保存返回 404,已清理或文件丢失返回 410,重复请求 ID 返回 409。
下载同一条受管抓包的 JSON 文件,不暴露本机路径。
请求体必须包含 { "confirm": true }。清空 SQLite 中四段历史正文、保留元数据行,并返回清理条数和清理前后数据库大小。
SSE 实时事件流。
它会推送两类事件:
snapshot:当前配置、健康状态、当前日志页log:一条新完成的代理日志
启动后端开发模式:
npm run dev如果你还想单独跑前端开发服务器,再开一个终端:
npx vite --host 127.0.0.1 --port 5173运行检查:
npm test
npm run build可以。新版 Codex 还可能把压缩请求发到普通 /v1/responses:本地压缩由 x-codex-turn-metadata 中的 request_kind: "compaction" 标识,Remote V2 由 input[].type: "compaction_trigger" 标识。CompactGate 会把它们记录为 compact 逻辑流量,但 Remote V2 实际复用 primary 上游与协议,不走 /responses/compact,也不需要 compact 模型。
如果日志仍显示为 primary,再检查 Codex 配置里是不是:
name = "OpenAI"
wire_api = "responses"去 Studio 页面里看:
compact.upstream_mode是不是你想要的值compact.base_url配得对不对
也可以用 POST /api/test-route 预览。
不需要。
这只影响 local/Remote V1。直接把:
"upstream_mode": "primary"这样 compact 请求也走主上游。
默认 logging.persist_body = false,SQLite 只保存可检索元数据,不保存客户端请求体、实际上游请求体、上游响应体或客户端响应体。兼容模式可打开 persist_body,但数据库达到容量上限时会优先清理这些正文。
需要原始请求和响应时,推荐配置有大小上限的独立抓包目录:
COMPACTGATE_CAPTURE_DIR=/path/to/captures请确认你运行的是新版 CompactGate。
这类报错通常不是超时。常见现象是 Codex 提示:
Stream disconnected before completion: stream closed before response.completed
如果 primary 上游收到原始 type: "compaction",但不能验证其中的 encrypted_content,上游可能会返回 invalid_encrypted_content,并且响应流里没有 Codex 需要的 response.completed 事件,最终就会显示上面的断流错误。
如果日志同时显示 status=502 与 Client disconnected before upstream response completed.,需要继续查看 upstream_status、stream_terminal_event、client_disconnect_phase 和 stream_outcome。Remote V2 或 Remote V1 在已经收到 response.completed/[DONE] 后,Codex CLI 可能先关闭连接而上游 HTTP 流尚未发出 end;新版会保留真实上游状态和已缓冲响应,并记录 stream_outcome=success、client_disconnect_phase=after_terminal,不再把它当作压缩失败。终止事件之前关闭仍记录为客户端取消或未完成流;真实上游 5xx 仍保留为上游错误。
日志中的 response_model 只表示上游响应正文明确返回的模型。Remote V2 的 response.compaction 可能不携带 model,此时 response_model 保持为空,effective_response_model 则在成功时使用 target_model,并由 response_model_source=target_fallback 标记为“目标模型推断”。Studio 主展示“有效响应模型”,同时单独显示“上游声明模型”,不会把推断值伪装成上游字段;只有明确读到上游模型时来源才是 upstream,失败、取消或未完成流则为 unavailable。超出流观察器 payload 上限的完整 response.completed 仍会按事件名识别,并记录 stream_oversized_event_count 供诊断。
当前版本在 compact.upstream_mode = "split" 时,会把成功 compact 响应里的可读 summary 状态记录下来。下一次包含同一段 encrypted_content 的普通 /v1/responses 请求会继续走 primary,但 CompactGate 会先把可读 compact 状态转换成 assistant summary message,再转发给 primary。
这个修复支持英文和中文等 Unicode 可读摘要,可以避免把可读摘要误当成不可解密的 compact 加密状态发给 primary。如果 compact 上游只返回不可读的加密状态,CompactGate 不能解密它,primary 仍可能无法恢复这段压缩上下文。
如果你刚更新代码,请重新执行构建并重启 CompactGate,让运行中的 dist/server/main.js 加载新版本。