Imported from piccuss/claude_multi_provider_proxy (
AGENTS.md). Install upstream withnpx skills add piccuss/claude_multi_provider_proxy. Copyright stays with the author.
AGENTS.md
本文件供 AI 编码代理阅读,假设读者对本项目一无所知。文档与代码注释以中文为准。
项目概述
claude-code-proxy(v1.0.0):零依赖的本地 HTTP 代理,把多家服务商的多把 API Key 组成号池,按会话粘性与用量均衡自动选路,让 Claude Code 通过切换模型路由到不同上游。
代理不做任何协议格式转换(上下游均为原生 Anthropic Messages API),只做五件事:
- 校验调用方 Key(请求头 API Key 必须在
auth.request_keys名单内,否则 401); - 按请求体
model字段找到候选 Key 集(同一模型可挂多把 Key); - 按号池路由策略选 Key:同一会话固定同一把 Key(保 prompt 缓存),新会话按剩余额度加权随机,用量比例差超阈值自动再均衡;
- 替换
model为上游真实模型编码、替换鉴权头为该 Key 的真实 API Key,原样透传(含 SSE 流); - 上游报错时自动熔断该 Key 并无感切换下一把(仅响应未开始传输前)。
另拦截两类无需转发的请求:/v1/messages/count_tokens(本地估算 token 数直接返回)与 max_tokens ≤ 1 的探测请求(返回 mock message),避免 Claude Desktop 报连接超时。
技术栈
- Node.js >= 18(
package.json的engines硬性要求),纯标准库,零 npm 依赖——package.json中不存在也不允许出现dependencies/devDependencies。 - CommonJS 模块(每个文件以
'use strict';开头,require/module.exports)。 - 无构建步骤、无 lint 配置。统计页面是同目录下的零依赖 HTML 文件(
src/stats/*.html),由路由原样读出。
常用命令
npm test # 全部测试(node --test tests/*.test.js),当前 108 个用例
node --test tests/router.test.js # 跑单个测试文件
npm start # 前台启动(需先 cp config.json.example config.json 并填入真实 Key)
npm run start:bg # 后台启动:detached 子进程,日志 data/proxy.log,PID data/proxy.pid
npm run kill / restart / status # 停止(先 POST /admin/shutdown 优雅停机,超时按 PID 强杀)/ 重启 / 状态
PROXY_PORT=3457 npm run start:bg # 覆盖端口起第二实例,与在跑服务隔离(start/kill 需用同一值)
PROXY_DEBUG_USAGE=1 npm start # 调试模式:把上游原始响应落盘到 data/debug/,排查 usage 解析
默认监听 http://127.0.0.1:3456。部署形态就是本地常驻进程:前台 npm start 或后台 npm run start:bg(daemon.js 负责 detached spawn、健康检查 /api/stats、PID 管理),无容器/CI 配置。
已知怪癖:daemon.js kill 的优雅停机用 auth.local_api_key 做鉴权;若配置只有 auth.request_keys 而无旧版 local_api_key,优雅停机会 401 并回退为按 PID 强杀(不影响数据,30s 定时刷盘兜底)。
架构:一条请求管线
src/server.js 的 createServer 是唯一编排点,所有请求按严格顺序穿过以下环节(改动时注意顺序敏感):
- 统计路由(
stats/stats-routes.js+stats/pool-routes.js+stats/request-key-routes.js)—/stats、/stats/pool、/stats/pool/usage、/stats/request-keys及对应/api/*在 auth 之前拦截返回,本地直连无需鉴权; - auth(
middleware/auth.js)— 按config._requestKeys(Map: api_key → code)校验x-api-key/authorization: Bearer,通过后挂req._requestKeyCode; - logger(
middleware/logger.js)— 重写res.end打结构化日志(读req._upstreamId/req._modelName); - 优雅停机(
server.js内联)—POST /admin/shutdown,仅当注入options.onShutdown时存在; - 本地拦截,不转发上游:
/v1/messages/count_tokens按 chars/4 估算直接返回;max_tokens ≤ 1的/v1/messages返回 mock message(这两个分支req._upstreamId = 'mock');非/v1/*路径 404; - 号池路由(
router.js+pool/key-picker.js)—resolveCandidates按body.model查config._modelMap得候选集,pickCandidate做硬门槛(enabled/冷却/日限额/月限额)→ 会话绑定命中 → 再均衡 → 策略选择; - 改写(
transformer.js)— 替换 model 为上游真实编码,生成该 Key 的鉴权头(支持每 Key 独立的key_header_name/key_prefix/headers); - 透传(
proxy.js)—base_url路径前缀与请求路径拼接后转发,proxyRes.pipe(tap).pipe(clientRes)回流 SSE;onSettle回调上报结果分类(ok / passthrough / retryable),retryable 且未吐字节时 server.js 换 Key 重试,候选全部失败则回放最后一个上游错误。
号池耗尽时返回 Anthropic 标准 429(key pool exhausted,message 只含状态计数——冷却数、日/月超限数分开计——与恢复时间:单 Key 取所有触发原因最晚解除时刻,整池取最早恢复者,不含任何 Key 标识)。
跨模块隐式契约(改一个文件前先看这些)
config._pool/config._modelMap:由config-loader.js的applyPool构建;_modelMap是 model → [{ key, realModel }] 多值映射,config 声明顺序即 fill-first 优先级;集成测试用applyPool构造同形对象。config._requestKeys:由applyAuth构建(Map: api_key → code),来源auth.request_keys[]+ 旧版auth.local_api_key(归入 code(default));server.js对缺失的手工 config 现场兜底构建。upstream._proxyHeaders:server.js调用transformRequest后挂在 upstream 对象上,proxy.js读取注入。upstream._keyId/upstream._requestKeyCode:server.js选定 Key 后挂上,proxy.js的采集回调据此归属号池用量与调用方审计用量。req._upstreamId/req._modelName:路由后附加(值为 Key id),logger.js读取;mock 拦截分支值为'mock'。data/usage.jsonv2 结构:keyId → 模型名 →YYYY-MM-DD→ 指标,外加request_records(调用方 code → 日期 → 指标);v1 旧文件首载自动迁移到(legacy)桶;周/月窗口查询时实时聚合。data/sessions.json:PoolState的会话绑定表(sessionId →{key_id, last_seen},version 1),30s 刷盘;测试注入poolStateFile: null走纯内存。- sessionId 提取顺序:
body.metadata.user_id→x-session-id头 → null(匿名不写绑定)。
号池路由算法(src/pool/)
权威说明见 docs/plans/2026-07-24-key-pool-routing-design.md。要点:
- session-sticky(默认):同 sessionId 固定同 Key;新会话按有效剩余额度(日/月两个已配置维度中较紧的剩余,见
key-picker.js的effectiveRemaining)加权随机;候选集内有效用量比例(usedRatio,取日/月中较耗尽者)spread >rebalance_threshold(默认 0.2)且过冷却期(默认 600s)时迁移绑定,空闲会话优先;要求所有启用 Key 至少配日限额或月限额之一,否则拒绝启动。 - 其余策略:
fill-first(按声明顺序用满再切)、round-robin、least-used(有效用量比例最小者)。 - 硬门槛:enabled、不在冷却期、当日用量 < 日限额(若配置)、当月用量 < 月限额(若配置)。
- 熔断(
PoolState.recordFailure):401/403 → 固定 1 小时;429/5xx/网络/超时 → 指数退避 30s×2ⁿ 封顶 30 分钟;其他 4xx 客户端错误透传不惩罚。成功一次清零连败。 - 并发超限是有意取舍:usage 流结束才统计,派发前只查一次,不做预留额度。
Token 统计子系统(src/stats/)
- 采集:
proxy.js在响应流中插入PassThroughtap,流结束后调usage-collector.js的parseUsage;采集全程 try/catch,异常仅console.warn,绝不影响主链路。 - 解析口径(
usage-collector.js顶部注释是权威说明):SSE 从message_start/message_delta提取;output_tokens只取message_delta最终值;兼容字段变体(如cache_creation_input_tokens/cache_creation_tokens)。 - 存储:
usage-store.js单例,内存聚合为准,30s 定时 + 每 20 条 + 进程退出时刷盘(tmp 文件 rename,version: 2);落盘路径可用PROXY_USAGE_FILE覆盖(测试隔离用)。 - 展示:
/api/stats按模型跨 Key 汇总;/api/pool/stats池级判断(健康/紧张/耗尽,日/月两个维度合计与剩余比例)+ 单 Key 当日与当月状态(today/month同构,状态按较耗尽维度判定);/api/pool/usage按 Key 5 窗口(今天/本周/本月/上周/上月,ISO 周、本地时区)并附带日/月限额;/api/request-keys/usage按调用方 code 5 窗口(审计,已删除 code 的历史保留标记)。页面零依赖 HTML,均不含任何 Key 材料。
代码组织
├── package.json # 仅 scripts/engines,无依赖字段
├── config.json.example # 配置模板(不含密钥)
├── config.json # 实际配置(gitignored,含密钥,勿读取外泄)
├── daemon.js # 后台运行管理(start/kill/restart/status,跨平台)
├── src/
│ ├── index.js # 入口:loadConfig + createServer + 信号优雅退出
│ ├── server.js # HTTP 服务编排(管线顺序见上,唯一编排点)
│ ├── config-loader.js # 配置加载、校验、旧格式转换、环境变量覆盖
│ ├── router.js # model → 候选 Key 集
│ ├── pool/
│ │ ├── key-picker.js # 硬门槛 + 会话绑定 + 加权随机 + 再均衡
│ │ └── pool-state.js # Key 熔断状态 + 会话绑定表(落盘 data/sessions.json)
│ ├── transformer.js # 请求改写(model 替换 + 鉴权头生成)
│ ├── proxy.js # HTTP/HTTPS 透传、SSE 回流、错误分类、usage tap
│ ├── middleware/
│ │ ├── auth.js # 调用方 Key 校验(401)
│ │ ├── logger.js # 结构化请求日志
│ │ └── error-handler.js# 兜底 500
│ └── stats/
│ ├── stats-routes.js # /stats + /api/stats(按模型)
│ ├── pool-routes.js # /stats/pool、/stats/pool/usage 及 API
│ ├── request-key-routes.js # /stats/request-keys 及 API(调用方审计)
│ ├── usage-store.js # 用量存储单例(v2,刷盘)
│ ├── usage-collector.js # SSE/JSON usage 解析
│ └── *.html # 4 个零依赖统计页面
├── tests/ # 9 个测试文件,node:test
├── docs/plans/ # 设计文档(路由算法等权威说明)
└── docs/prototypes/ # 统计页面 HTML 原型
不可违反的设计约束(安全相关)
- 零依赖:
package.json不得出现dependencies/devDependencies。 - 零协议转换:不解析 Messages API body 结构(路由只读
model、metadata.user_id顶层标量),上下游都是原生 Anthropic 格式。 - SSE 直接 pipe:主链路严禁缓冲完整响应;usage 采集只能通过旁路 tap;可重试错误的响应体很小(非 SSE),缓冲是例外且合法。
- Header 安全:转发前必须删除
x-api-key、authorization、host,再注入上游真实 Key,防止本地 Key 泄露到上游。 - Key 不出配置文件:页面/API/错误信息只输出
provider、id、调用方code(含掩码片段也不行);config-loader 校验报错必须用maskKey遮蔽 api_key。 - 代理层错误一律包装为 Anthropic 标准错误格式(
proxy.js的sendAnthropicError:{type:'error', error:{type, message}})。 config.json、data/、CLAUDE.md均在.gitignore中,不入库、勿提交。
配置要点(config.json,模板见 config.json.example)
- auth.request_keys[]:调用方 Key 名单,
{code, api_key}均必填且各自唯一;名单外请求 401;旧版auth.local_api_key兼容(归入(default)),两者并存时合并。 - pool.keys[]:
id(唯一)、provider、base_url、api_key必填;可选daily_limit_tokens(null=不限)、monthly_limit_tokens(null=不限,按本地时区自然月统计,与日限额可只配其一或日+月双控)、enabled(默认 true)、key_header_name、key_prefix、models(对外模型名 → 上游真实编码)、headers。 - 同一模型名可挂在多把 Key 上(号池核心,不再报 duplicate)。
- 环境变量覆盖:端口
PROXY_PORT;Key 本体PROXY_API_KEY_<KEY_ID>(ID 大写、-转_)。 - 旧格式兼容:
upstreams[]自动转换为号池 Key(不限额、fill-first)并打印弃用警告;与pool并存报错。 - base_url 路径拼接:
base_url路径前缀与客户端请求路径 join(proxy.js中实现)。
测试约定
node:test+node:assert/strict,无任何测试框架依赖;命令npm test(即node --test tests/*.test.js)。- 集成测试(
tests/integration.test.js)用port: 0让系统分配端口,配置经applyPool构造,号池状态走纯内存(poolStateFile: null,避免写data/sessions.json与注册 exit 钩子),并起内存 mock 上游验证透传与故障转移。 - 新增 upstream/号池行为优先在
tests/integration.test.js加端到端断言;选择算法单测在tests/key-picker.test.js;存储/解析/路由各有对应单测文件。 - 最近一次验证:108 个用例全部通过。
设计文档
docs/plans/ 下的设计文档是权威说明,改动对应子系统前先读:
2026-07-29-monthly-limit-design.md— 日/月双维度限额(配置、路由口径、展示改造)2026-07-24-key-pool-routing-design.md— 号池路由算法(熔断、再均衡细节)2026-07-24-key-pool-overall-design.md— 号池改造整体方案2026-07-17-token-usage-stats-design.md— Token 统计模块口径与决策