本地翻译代理是 a4api 打通「任意服务商」的关键组件:Claude Code 只说 Anthropic 协议、Codex 说 OpenAI Responses 协议,而国内常用的 OpenAI 兼容上游大多只提供 Chat Completions 接口,三者对不上。代理在本机把一种协议实时翻译成另一种并流式转发,让 Claude Code 能用任意 OpenAI 兼容 API、让 Codex 能接智谱等仅有 Chat Completions 的上游、让 dsh 经由统一透传端点规避上游分片格式缺陷。本文章介绍代理的三条翻译链路、网络与鉴权设计、进程生命周期,以及历次迭代沉淀的上游兼容性加固。
1. 为什么需要翻译代理
三个目标工具各自「说」的协议不同,上游提供的协议也参差不齐:
| 角色 | 使用的协议 | 说明 |
|---|---|---|
| Claude Code | Anthropic /v1/messages |
官方仅原生支持 Anthropic 协议 |
| Codex | OpenAI /responses |
Responses 接口较新,多数国内上游未提供 |
| dsh | OpenAI /chat/completions |
最普及的接口形态 |
| 多数上游 | 仅 Chat Completions | DeepSeek、智谱 v4、OpenRouter 等 |
代理据此提供三条链路,全部收敛到同一个本机端口:
Anthropic messages] -->|翻译| P[本地翻译代理
127.0.0.1] B[Codex
Responses] -->|翻译| P C[dsh
Chat Completions] -->|透传+清洗| P P --> T[翻译为
Chat Completions] T --> U[OpenAI 兼容上游
DeepSeek/智谱/OpenRouter等] D[Codex 直连场景
原生 Responses 上游] -.->|不经代理| U style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px style C fill:#ffecd6,stroke:#e67e22,stroke-width:2px style P fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style T fill:#fdebd0,stroke:#b7950b,stroke-width:2px style U fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style D fill:#fadbd8,stroke:#c0392b,stroke-width:2px
唯一的例外是原生支持 Responses 的上游(当前为 DeepSeek 官方):Codex 直连上游,完全不经代理,见服务商管理与预置模板。
2. 网络与鉴权
代理的安全边界刻意收得很紧:
| 设计点 | 取值 | 说明 |
|---|---|---|
| 监听地址 | 仅 127.0.0.1 |
不对外网开放,局域网内其他设备无法访问 |
| 端口范围 | 17890 - 17899 |
固定小范围内自动挑选空闲端口 |
| 鉴权 token | secrets.token_urlsafe(24) 随机生成 |
每次启动重新生成,写入目标配置供工具携带 |
| 鉴权方式 | x-api-key 头或 Authorization: Bearer <token> |
二选一,与 token 不一致返回 401 |
切换时写入配置的是代理鉴权 token 而不是真实 API Key——真实 Key 只保存在加密数据库中、由代理在转发前解密使用,不会落盘到任何明文配置里。
3. 端点一览
| 端点 | 方法 | 服务对象 | 行为 |
|---|---|---|---|
/v1/messages |
POST | Claude Code | 把 Anthropic 请求翻译为 Chat Completions 转发,流式响应回译成 SSE |
/responses |
POST | Codex | 把 Responses 请求翻译为 Chat Completions 转发,供智谱等非原生上游使用 |
/chat/completions |
POST | dsh | 透传到上游,并对流式分片做 tool_calls null 归一清洗 |
/v1/api/version |
GET | 内部握手 | 返回能力版本与特性列表,用于复用前校验 |
/v1/proxy/refresh |
POST | 内部调用 | 通知代理立即按数据库当前生效配置刷新上游地址与密钥 |
4. 进程模型与生命周期
4.1 独立进程
代理以独立子进程运行:安装版通过 a4api.exe --proxy 拉起,开发模式为 python -m backend.app.proxy_standalone。无窗口、输出静默,主程序退出后仍然存活——这正是「关闭 a4api 后 Claude Code 还能继续用」的原因。
4.2 启动与复用流程
切换到需要代理的配置方案时,主程序执行「确保代理就绪」:
proxy.json} B -->|是| C{端口存活且有
token?} C -->|是| D{能力握手通过?
version>=2 且支持 Responses} D -->|是| E[复用现有代理
并通知刷新上游] D -->|否| F[结束旧构建进程
清理状态文件] C -->|否| F B -->|否| G[拉起独立代理进程] F --> G G --> H[等待就绪
最多 20 秒] H --> E E --> I[返回 base_url
与 token 供写配置] style A fill:#e8f4f8,stroke:#1a6b8a,stroke-width:2px style B fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style C fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style D fill:#ebdef0,stroke:#8e44ad,stroke-width:2px style E fill:#d5f5e3,stroke:#27ae60,stroke-width:2px style F fill:#fadbd8,stroke:#c0392b,stroke-width:2px style G fill:#ffecd6,stroke:#e67e22,stroke-width:2px style H fill:#fdebd0,stroke:#b7950b,stroke-width:2px style I fill:#d6eaf8,stroke:#1a6b8a,stroke-width:2px
几个值得展开的细节:
- 状态文件:代理启动后把端口、PID 与 token 写入数据目录的
proxy.json;主程序凭它与代理建立联系。 - 能力握手:端口活着不代表能用——早期构建只有 Anthropic 翻译、没有 Responses 端点,Codex 会拿到 404。复用前必须查询
/v1/api/version,要求能力版本不低于 2 且特性列表包含 Responses 支持,否则视为旧构建,强制结束后重新拉起。 - 陈旧状态清理:状态文件可能因崩溃而残留过期信息,此时按监听端口反查实际占用进程并处理,避免误判。
4.3 上游信息的更新
代理自己持有真实上游地址与解密后的 API Key:每次切换后主程序调用内部刷新端点,让代理立即从数据库读取「当前生效配置」并更新上游,避免短暂使用旧上游。代理同时每 3 秒轮询一次数据库,一旦发现当前生效配置不再需要代理,便自动退出。
4.4 何时需要代理
| 当前生效配置 | 是否运行代理 |
|---|---|
| Anthropic 类型服务商 + Claude 目标 | 否,直连上游 |
| OpenAI 类型 + 目标含 Claude | 是 |
| OpenAI 类型 + 目标含 Codex(上游非原生 Responses) | 是 |
| OpenAI 原生 Responses + 仅 Codex 目标 | 否,直连上游 |
| OpenAI 类型 + 目标含 dsh | 是,始终经代理透传 |
手动观测与干预走后端接口:查询运行状态的 /api/v1/switch/proxy/status、停止代理的 /api/v1/switch/proxy/stop。
5. 上游兼容性加固
真实世界的上游实现五花八门,代理在 0.1.x 的迭代中沉淀了一批针对性兼容处理:
| 加固项 | 解决的问题 | 引入版本 |
|---|---|---|
| developer 角色映射为 system | 部分 Codex 请求的角色字段被上游拒绝(400) | 0.1.1 |
| description 字段类型修正 | 工具描述被误转字符串导致上游拒绝 | 0.1.0 期间 |
| 残缺 tool_call 对话过滤 | 中断留下的残缺工具调用历史引发上游 400 | 0.1.2 |
| tool 响应消息重排 | 工具结果顺序错乱导致 Codex 重连上游 400 | 0.1.3 |
| 上游 429 退避透传 | 限流响应连同 Retry-After 头原样回传,客户端正确退避 | 0.1.3 |
| 工具 schema 清洗 | LLama 系推理服务不认识的 schema 键被裁剪 | 0.1.0 起 |
| tool_calls null 归一 | opencode zen 等上游用 null 填充后续分片,dsh 适配器把工具名覆盖为空报 unknown tool | 0.1.5 |
这些处理全部内建于代理内部,对三个工具透明;对应回归测试覆盖在项目的测试体系中,详见项目总览与快速开始的源码仓库说明。
6. 安全与隐私
代理处理的是完整对话流量,隐私设计上做了三层约束:
- 密钥不落盘:真实 API Key 只存在于 DPAPI 加密的数据库中,代理转发前才解密;写入各工具配置文件的只有随机鉴权 token。
- 日志默认静默:正常日志只记时间、级别与摘要,不记录任何请求或响应内容。
- 调试日志显式开关:仅在设置环境变量
A4API_PROXY_DEBUG时才记录完整请求头、请求体与上游响应,用于排障后应关闭。
7. 常见问题
问:关闭 a4api 主程序后,Claude Code 还能继续用吗?
答:能。代理是独立进程,主程序退出不影响它;只要数据库里的生效配置仍是需要代理的方案,它会一直驻留并在后台完成翻译转发。
问:界面提示无法访问本地代理地址怎么办?
答:多为残留了旧版本代理进程——能力握手会识别这种情况并自动清理重启,若仍失败,可调用停止接口结束代理后重新切换一次配置;再不行请附上 ~/.a4api/logs/a4api.log 排查。
问:怎么彻底停掉代理?
答:两种方式:把生效配置切换到不需要代理的方案(如 Anthropic 类型直连 Claude Code),代理会在数秒内自行退出;或直接调用停止接口强杀进程并清理状态文件。
问:端口范围只有 10 个,被占满了怎么办?
答:正常场景极难发生——代理只在需要时运行一个实例。若确实出现,先排查是否有异常进程占用 17890 至 17899 端口段;停止代理接口会顺带清理陈旧的占用者。
举手提问