外挂在官方 Codex CLI 上的本地小代理:单静态二进制(Go,CGO_ENABLED=0)、零外部服务依赖、
不改 ~/.codex/config.toml、不装证书。
默认观察两个问题:① 上游声明的模型是否匹配请求;② STATE 是否符合本地预设格式。
可选开启 M2 STATE 维护,在后台采集、复验并注入票据。格式符合或模型声明匹配都不等于回答能力得到保证。
其余统计量只进审计台账(~/.codexproxy/audit.jsonl)与 cpx report。
| 平台 | 文件 |
|---|---|
| Ubuntu / Debian x86_64 | cpx-0.1.1-linux-amd64.tar.gz |
| Ubuntu / Debian ARM64 | cpx-0.1.1-linux-arm64.tar.gz |
| macOS Apple Silicon | cpx-0.1.1-darwin-arm64.tar.gz |
| macOS Intel(amd64) | cpx-0.1.1-darwin-amd64.tar.gz |
下载页:https://github.com/LyleLiu666/CodexProxy/releases/latest(四个包的 tar.gz + SHA256SUMS)。
文件名里的 darwin 就是 macOS(内核名):Darwin arm64 = Apple Silicon,Darwin x86_64 = Intel,
Linux x86_64 / Linux aarch64 = linux-amd64 / linux-arm64。用 uname -s 与 uname -m 核对即可。
已发布的 0.1.1 包自带 5 个文件:cpx(单文件静态二进制,8–9 MB)、README.md、linux.md、model-check.md、
check-model.py(离线体检脚本,使用不需要它,见 §6.8)。校验:shasum -a 256 -c SHA256SUMS。
从源码构建见 §6.3。
tar -xzf cpx-0.1.1-linux-amd64.tar.gz
cd cpx-0.1.1-linux-amd64
./cpx install --add-path # 装到 ~/.local/bin,并把该目录幂等写进 shell 启动文件
source ~/.profile # Ubuntu/bash;zsh 用 ~/.zshrc;或直接重开一个 SSH 窗口
cpx version # 确认可用(source 后仍 command not found → 执行 hash -r)cpx install 只写两处:~/.local/bin/cpx 和 shell 启动文件里的一行 PATH;不用 sudo、不碰系统目录、可重复执行。
真实使用需要官方 codex CLI 且已 codex login(npm i -g @openai/codex;可选依赖见 §6.1)。
不需要 codex 也能安装与自检。
从源码装 / 升级(macOS 与 Linux 通用):make install —— 构建当前平台 + 装到 ~/.local/bin + 报告 PATH 状态。
cpx tui # ← 主推:进入 codex TUI,下方挂 2 行实时状态面板(需要 tmux)
cpx # 不带面板:等价于 codex(参数原样透传)
cpx selftest # 建议首次跑一次:内置假上游自证整条链路,不需要 codex / 登录 / 联网cpx tui需要 tmux:sudo apt install tmux(macOS:brew install tmux)。没装时它会打印安装命令并 停止本次启动,不会静默降级;此时直接用cpx,功能不受影响。- 已在 tmux 内 → 直接上下分屏;不在 tmux 内 → 自动建会话并 attach(
Ctrl-b d分离)。 - 环境排查:
cpx doctor(codex 路径、登录态、审计目录、监听、代理路由)。
面板(cpx tui 下方 2 行,独立窗格,不与 TUI 抢终端):
当前 turn 4 · 模型 ✔ · STATE 格式 292 字符 ✔
累计 · 模型正确 4 次 / 错误 0 次 · STATE 格式 292 字符符合 4 次 / 312 字符不符 0 次
STATE 显示实际字符数,默认要求为 292;✔ / ✗ 是格式检查结果,— 表示没有凭证或无法判断(不计入正确 / 错误)。
模型按请求计数,凭证按检查次数计数(请求携带与服务端返回分别计数);第一行优先显示本次服务端返回的凭证。
退出摘要(每个会话一行;有降级/错误时多一行"明细"):
cpx: 本次会话 s-1a2b3c4d:2 请求 / 1 turns,模型 ⚠ 被替换 1 次(gpt-6-astra → gpt-5.6-sol),STATE ✔ 格式符合(见过 292 字符×2);客户端携带 1 次 / 未携带 1 次 · 上游签发新凭证 1 张
cpx: 明细:cpx report --session s-1a2b3c4d(实时:cpx watch)
事后报告(cpx report,按 turn 归组):
STATE 格式: ✔ 格式符合
见过的长度: 292 字符×2
来源: 客户端携带 1 次 / 未携带 1 次 · 上游签发新凭证 1 张 · 客户端带的是上游此前签发的凭证
三者同一套口径:先说结论(模型声明是否一致、STATE 格式是否符合规则),再给可直接核对的证据;判不过的凭证按原因分类 (非法 base64 / 封装不识别 / 签发时间越界 / 长度不符 / 长度非标准),不把格式检查当成上游有效性验证。
| 命令 | 说明 |
|---|---|
cpx |
主形态:包裹运行 codex(参数原样透传) |
cpx tui |
TUI + 下方 2 行状态面板(tmux 分屏) |
cpx report |
事后报告:按 turn 归组(--session/--json/--fail-on-degrade) |
cpx watch [路径] |
实时跟读审计日志(一行一条;--raw 原始 JSONL) |
cpx status |
当前会话状态块(--session <id> 只认本会话,--follow 持续刷新) |
cpx selftest |
一条命令自证:内置假上游跑完整链路(无需 codex / 登录 / 联网) |
cpx doctor |
自检:codex / 登录态 / 审计目录 / 监听 / 代理路由 |
cpx install [--add-path] |
把自己装到 ~/.local/bin(无需 sudo) |
cpx serve |
只启动常驻代理(IDE 插件等自行指向端口) |
cpx version |
版本 |
| 依赖 | 用途 | 安装 |
|---|---|---|
| codex CLI | 真实使用(cpx 是它的代理层) | npm i -g @openai/codex,首次 codex login |
| tmux | cpx tui 的实时状态面板 |
sudo apt install tmux / brew install tmux |
| libnotify-bin(Linux) | 降级桌面通知(notify_on_mismatch) |
sudo apt install libnotify-bin |
~/.codexproxy/config.toml:
listen = "127.0.0.1:0" # 默认随机端口
upstream_base = "https://chatgpt.com"
audit_enabled = true
audit_log = "~/.codexproxy/audit.jsonl"
status_file = "~/.codexproxy/status.json" # cpx status / tmux 面板读取
notify_on_mismatch = false # 首次降级发一条 macOS 通知(cpx tui 默认开)
[ticket] # M2 实验特性,默认关
enabled = false
allow_without_ticket = false # 无已复验 STATE 时拒绝请求;显式设 true 才原样转发
models = ["gpt-6-astra", "gpt-5.6-sol"]
target_length = 292
ttl_seconds = 3600
refresh_before_seconds = 600
min_probe_interval_seconds = 60
cooldown_seconds = 300
harvest_proxy_url = "" # 可选采集代理;复验和业务仍用原出口环境变量覆盖:CPX_CONFIG、CPX_AUDIT_LOG、CPX_STATUS_FILE、CPX_UPSTREAM_BASE、CPX_LISTEN、
CPX_CODEX_BIN、CPX_NOTIFY、CPX_SESSION(由 cpx tui 注入,面板与主进程共享会话 id)。
- 文件:
~/.codexproxy/audit.jsonl(目录 0700、文件 0600),只追加,绝不含 token / 凭证原文 - 字段:
time、path、session、seq、thread、turn、turn_ordinal、request_kind、turn_started_at、client_ticket、upstream_ticket、request_model、upstream_model、upstream_model_mismatch、upstream_header_model(CLI 自己使用的openai-model响应头)、upstream_model_conflict(头/体自述不一致)、service_tier、stream、status、duration_ms、upstream_request_id、error、client_closed(客户端读到终局事件后自行断流——SSE 的正常行为,不计入转发错误)、client_ticket_len/client_ticket_ok/upstream_ticket_len/upstream_ticket_ok(凭证只记「长度 + 形状判定」)、client_ticket_blocks/client_ticket_issued_unix/client_ticket_id/client_ticket_shape(上游侧为同名的upstream_ticket_*) (凭证只记长度、块数、签发时间、形状原因和一个短哈希;凭证原文绝不落盘) - 判定语义:响应模型为空 → 不判定;大小写不敏感;终态事件(
response.completed/failed/incomplete/cancelled/done)声明的模型优先 - 错误语义:上游 HTTP 4xx/5xx 记为请求错误;上游流在客户端仍在读时被切断记为传输错误并中止下游响应;客户端主动断开(codex
拿到终局事件就挂断、用户 Ctrl-C)记为
client_closed,不计入错误 - 凭证语义(纯被动监控):
x-codex-turn-state是「57 字节头(版本字节 + 大端 unix 秒签发时间)- N 个 16 字节块」的封装,长度由块数推出:10 块 = 292 字符(默认规则)、11 块 = 312、
12 块 = 332。验收看「封装可解析且块数 ==
ticket.target_length对应块数」 (docs/design.md§5.3);判不过的凭证按原因分类(base64/envelope/timestamp/blocks/length)。历史长度/前缀兼容规则标记为legacy,只用于被动观察,不可用于主动发布票据
- N 个 16 字节块」的封装,长度由块数推出:10 块 = 292 字符(默认规则)、11 块 = 312、
12 块 = 332。验收看「封装可解析且块数 ==
- 格式结论:所有交付面都先说「STATE ✔ 格式符合」或「STATE ✗ 格式不符(原因)」,再给证据(见过的长度、
来源、最近一张的短哈希与签发时间)。
cpx watch每条请求带[STATE 格式 ✔ 292 字符 a1b2c3d4]或[STATE 格式 ✗ 长度不符(312 字符) a1b2c3d4] - 状态文件:
status_file按会话隔离(~/.codexproxy/status-<会话>.json,0600、原子写), 面板与cpx status都读它;cpx status不带参数时读最新的一个 - 定位到具体请求:
cpx report(默认只列降级/错误行,--all展开、--json给脚本、--fail-on-degrade给 CI、--session all看历史);cpx watch可边跑边看
- 退出码:
cpx返回 codex 的退出码;被信号杀死时按 shell 惯例映射为128+N(Ctrl-C → 130,SIGTERM → 143)。 - 监听:默认只绑回环。
listen配成非回环(如0.0.0.0:8788)会被拒绝——那等于把你的账号借给任何能访问该端口的人;确需如此请显式CPX_ALLOW_REMOTE=1(仍会打出警告)。 - 上游地址:
upstream_base必须带http:///https://,否则加载配置时就报错(而不是每个请求 502)。 - 压缩响应:gzip 响应体会被解压后只用于审计观测,转发给 codex 的字节完全不变;压缩的 SSE 流会标记为"体未观测",而不是假装"没有矛盾"。
- 净化:模型名/档位/请求 id 等上游可控字段在渲染前会剥离控制字符并截断——说谎的上游无法向你的终端注入转义序列。
- 审计日志:解析时跳过损坏/超长(>4 MiB)行并计数,单行异常不会吞掉整份报告。
- 凭证检查(被动):只读已经流过的请求/响应(不采集、不注入、不重放);分类失败只影响那一行的显示,转发字节完全不变。
- 默认行为:M2 关闭时不新增探测或注入;开启后会增加后台探测,并可能向原本没有 STATE 的请求注入已复验票据。
cpx [codex 参数...]
├─ 启动 127.0.0.1 随机端口上的进程内代理
├─ spawn codex,临时注入(不落盘):
│ -c model_provider=cpx
│ -c model_providers.cpx.base_url="http://127.0.0.1:<port>/backend-api/codex"
│ -c model_providers.cpx.requires_openai_auth=true (复用 codex login 登录态)
│ -c model_providers.cpx.supports_websockets=false
│ + 环境变量 NO_PROXY=127.0.0.1,localhost
├─ 请求:codex ──HTTP──▶ cpx ──▶ https://chatgpt.com/backend-api/codex/...
│ · SSE 逐帧透传(不缓冲、不改语义),tee 观测
│ · 回源自动走 HTTPS_PROXY 环境变量或 macOS 系统代理(scutil)
└─ codex 退出 → 打印本次会话审计摘要
M0 实测发现:macOS 的系统代理会劫持(本机)loopback 请求,且 reqwest 不遵守例外列表, 因此 cpx 必须给 codex 子进程注入
NO_PROXY;反之 cpx 回源 chatgpt.com 时必须自己走系统代理。 细节见m0/experiment1.md。-c features.respect_system_proxy=false实测无效(见m0/experiment1.md追加实验)。 CLI 侧行为考证(turn-state 契约、openai-model头、provider schema):docs/codex-source-notes.md。
make build # 生成 dist/cpx
make install # 本机装好:构建当前平台 → ~/.local/bin → 报告 PATH 状态(macOS/Linux 通用)
make cross # darwin/arm64+amd64、linux/arm64+amd64、windows/amd64(CGO_ENABLED=0)
make release # → dist/release/cpx-<版本>-<平台>.tar.gz ×4 + SHA256SUMS
# 有 ssh 直达时,一条命令推到远端并自检(自动识别远端系统/架构:linux 与 macOS 都支持)
make push HOST=user@ubuntu
make push HOST=user@macbook DIR=bin # 换目录
# 没有 ssh 直达时手工三步(以 Ubuntu 为例)
scp dist/cpx-linux-amd64 user@ubuntu:/tmp/cpx
ssh user@ubuntu 'chmod +x /tmp/cpx && /tmp/cpx install --add-path'
ssh user@ubuntu 'source ~/.profile && cpx doctor'单个二进制约 8–9 MB、静态链接,Ubuntu 20.04+ 直接跑(不需要 glibc 版本对齐、不需要运行时依赖)。
macOS 侧同样:下载包里的 darwin-arm64/amd64 二进制与本机 make install 装的是同一个(Mach-O、无外部依赖),
升级就是再跑一次 make install,或解压新的 darwin 包执行 ./cpx install。
Linux 细节(systemd 常驻、代理环境变量、通知、容器 E2E 实录)见 docs/linux.md。
M2 已在当前源码实现:采集候选 STATE → 日常业务出口复验 → 内存缓存 → 未携带 STATE 的请求注入 → 提前续期与异常作废。
开启方式及完整边界见 M2 使用说明。已发布的 0.1.1 安装包不因此自动获得此功能;当前源码构建标识为 0.2.0-dev。
- 默认拒绝首次采集中、采集失败或票据过期时的新请求(503);显式设置
allow_without_ticket = true才会在无票据时原样转发。 - 已有 CLI STATE 不覆盖、不因票据空窗而拒绝,也不自动重放请求。
- 默认 60 分钟 TTL、提前 10 分钟续期、采集最低间隔 60 秒、失败冷却 300 秒。
- 票据不落盘;按账号、OAuth token、模型与业务出口配置隔离。
- 面板、watch、report 和退出摘要记录 M2 实际动作;没有注入时不宣称已生效。
- 当前验证为受控上游自动化,真实账号上的模型路由、能力和并发收益仍待验证。
已有独立对照探针(会消耗账号配额):
make probe
./dist/cpx-probe -mode harvest -n 5 -out m0/probe-harvest.jsonl
./dist/cpx-probe -mode ab -n 20 -out m0/probe-ab.jsonl采集成功要求 HTTP 成功、完整成功响应模型匹配、头体不矛盾及严格 STATE 格式校验。
A/B 交替执行带 STATE / 不带 STATE,记录完整成功且模型匹配的次数、耗时和错误;没有采到可用候选时返回非零退出码。
独立探针使用日常出口;可选 harvest_proxy_url 用于 M2 后台维护。
check-model.py 与 cpx 一起打进安装包(在解压目录里),用来体检"这台机器、这个账号、这条链路"。
日常不需要它——面板 / 报告 / 摘要已经回答了两个业务问题。
# 在解压出来的包目录里:只跑本地链路自检(不联网、不用账号、无副作用)
python3 check-model.py --binary ./cpx --offline
# 在源码仓库里:
python3 scripts/check-model.py --binary dist/cpx --offline去掉 --offline 会额外跑一份内置遗传学题目(默认 3 轮、gpt-5.6-sol / medium),
会消耗已登录账号的配额,只在"刚换网络 / 刚换账号 / 怀疑固定被降级"时用:
python3 check-model.py --archive cpx-0.1.1-linux-amd64.tar.gz # 需要 codex 已登录标准答案 46.24,答出 48 只标记"答题异常",不据此断言换模型或凭证失效;每轮独立审计、答案与汇总都落盘。
只需 Python 标准库。详细用法与判定口径见 docs/model-check.md。
- 只监听
127.0.0.1;不修改用户配置、不装根证书、不接管系统流量。 - 日志与报错不含
Authorization、access_token、凭证原文。 - STATE 采集/注入默认关闭;实验请求使用自己的账号并消耗配额。本项目与 OpenAI 无任何关联。
- 被动格式检查与 M2 主动维护分别记录;长度/封装规则不是上游有效性或模型能力的证明。
- loopback 端口可被同机其他进程访问(等于借用你的账号转发)——介意时用
cpx wrapper形态(默认随机端口 + 生命周期跟随进程)。
| 阶段 | 状态 |
|---|---|
| M0-1 集成链路验证 | ✅ 通过(m0/experiment1.md) |
| M0-2 铸凭证 / M0-3 A/B 增益 | ⏳ 待用真实账号运行 dist/cpx-probe |
| M1 wrapper + 审计 MVP | ✅ 本仓库 |
| M2 STATE 维护 | 当前源码已实现,默认关闭;受控测试通过,真实收益待验证 |
| M3 三平台发布、doctor/watch、文档 | 部分(doctor/watch 已可用) |
make test # go test ./...
make check # go test -race + go vet + Python 端到端
make cross # 五平台交叉编译
e2e/linux/run.sh # 真 Ubuntu 容器端到端(需 docker;无 codex 登录也能跑)平台:Linux/Ubuntu 用法见 docs/linux.md(含 systemd、代理环境变量、容器 E2E 实录)。
自动检测说明:docs/model-check.md;降级可视化设计:docs/ux-degradation-report.md;
M2:docs/m2.md;整体设计:docs/design.md;CLI 行为考证:docs/codex-source-notes.md。
目录:cmd/cpx(CLI)、cmd/cpx-probe(M0 探针)、e2e/linux(Linux 端到端)、
internal/{config,audit,proxy,upstream,wrapper,auth,ticket,tmux,tty}。
许可:MIT。机制参考 sub2api(LGPL-3.0-or-later)但未复制代码。