将 Command Code API 转换为 OpenAI / Anthropic 兼容接口的反代代理。单文件,零外部依赖。
逐条对齐官方 npm 包源码(command-code@1.53.1;dist/cli.mjs 只是压缩、没有混淆)。上游 npm 走到更高版本时代理只打漂移告警,不会静默改版本号(见反检测)。
完整功能:OpenAI Chat Completions / Responses API(/v1/responses) + Anthropic Messages API | 流式/非流式输出 | 工具调用 (tool_use) | 多模态图片输入 | 推理强度 (reasoning_effort) | 动态模型列表 | 缓存命中指标 | 设备指纹伪装(per-key 绑定、自动刷新)| x-api-key 鉴权(Anthropic SDK)| 客户端断连检测(上游中止)| 零输出 → 429 自动重试 | 连续超时 → 429 自动重试 | 隐私保护日志
社区: Linux.do — 一个友好的中文技术社区。
npm start # 启动(仓库自带 config.json,监听 http://0.0.0.0:3050)
npm run dev # watch 模式(文件修改自动重启)API Key 通过 Authorization 请求头(Anthropic SDK 可用 x-api-key)传入,无需配置到文件中。Key 必须以 user_ 开头(自动匹配任意前缀,如 Bearer token_user_xxx):
curl http://127.0.0.1:3050/v1/chat/completions \
-H "Authorization: Bearer user_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"model":"deepseek/deepseek-v4-flash","messages":[{"role":"user","content":"hi"}]}'commandcode/
├── config.json # 端口 / 日志路径等
├── LICENSE # MIT License
├── package.json # npm start / npm run dev
├── proxy.mjs # 单文件核心代理(~1900 行)
├── Dockerfile # 容器构建文件(node:22-alpine)
├── docker-compose.yml # 容器编排
├── .dockerignore # 构建上下文排除规则
├── .github/
│ └── workflows/
│ └── docker-publish.yml # release 分支 / v* tag → GHCR 多架构(latest + release)
├── captured-requests/ # CLI 抓包数据(协议逆向参考)
├── README.md # 英文文档
└── README_zh.md # 本文档(中文)
| 字段 | 默认值 | 说明 |
|---|---|---|
port |
3000 |
监听端口(仓库自带 config.json 为 3050) |
host |
0.0.0.0 |
监听地址 |
apiBase |
https://api.commandcode.ai |
CC API 地址 |
projectSlug |
cc-proxy |
x-project-slug header |
apiKey |
"" |
可选兜底 API Key(请求也可通过 header 传入) |
logFile |
"" |
日志文件路径(空=仅控制台) |
logLevel |
info |
日志级别 |
useProviderModels |
true |
从 Provider API 动态拉取模型列表 |
modelRefreshIntervalMs |
300000 |
模型列表缓存刷新间隔(5min) |
zdr |
false |
请求 Command Code 使用 ZDR-only 路由 |
cliMode |
agent |
信封 mode。上游枚举:agent / learning / custom-agent / custom-agent-create / title-gen / tool-desc / compact / vision |
cliSessionMode |
interactive |
lifecycle 元数据里的 mode(另一个枚举:interactive / non-interactive) |
fingerprintSalt |
"" |
设备指纹的盐。成批换设备身份就用它(同一个 key 永远报同一台设备) |
deviceProjectDir |
"" |
伪装的项目目录(空则用内置 C:\Users\dev\projects\app);改了 = 所有账号换一台设备 |
emptySystemPlaceholder |
true |
无 system prompt 时发空格占位,阻止上游注入约 7.5K token 默认提示词(#17) |
| 变量 | 默认 | 说明 |
|---|---|---|
PORT |
3000(自带 config.json 为 3050) |
监听端口 → port |
HOST |
0.0.0.0 |
监听地址 → host |
CC_API_BASE |
https://api.commandcode.ai |
上游地址 → apiBase |
CC_UPSTREAM_PROXY |
空 | 让发往 CC 上游的请求走 HTTP 代理(仅 http:// CONNECT),见下文「上游代理」→ upstreamProxy |
PROJECT_SLUG |
cc-proxy |
x-project-slug → projectSlug |
LOG_FILE |
空 | 日志文件 → logFile(同步写,见其它注意事项) |
CC_USE_PROVIDER_MODELS |
true |
动态拉取模型列表 → useProviderModels |
CMD_ZDR |
关 | 1 开启 ZDR-only 路由 → zdr |
CC_CLI_MODE |
agent |
信封 mode → cliMode |
CC_CLI_SESSION_MODE |
interactive |
lifecycle 元数据的 mode → cliSessionMode |
CC_FINGERPRINT_SALT |
空 | 设备指纹盐 → fingerprintSalt |
CC_DEVICE_PROJECT_DIR |
空 | 伪装的项目目录 → deviceProjectDir |
CC_EMPTY_SYSTEM_PLACEHOLDER |
true |
无 system prompt 时发空格占位;false 关掉 → emptySystemPlaceholder |
CC_MAX_BODY_MB |
100 |
请求体上限(MB),超限返回 413 |
CC_STREAM_IDLE_MS |
30000 |
流式上游读空闲超时,见上游空闲超时 |
CC_NONSTREAM_IDLE_MS |
90000 |
非流式上游读空闲超时(同上) |
CC_MAX_INFLIGHT |
0(不限) |
进程内在途请求上限,超限 503,见在途上限 |
CC_CLIENT_DRAIN_TIMEOUT_MS |
空(禁用) | 下游背压阻塞超过该毫秒数就断开该客户端,见僵死连接 |
CC_KEEPALIVE_TIMEOUT_MS |
65000 |
后端 keep-alive 时长(headersTimeout 自动 +1s)。必须大于反代侧的 keepalive_timeout,见 keep-alive 时序 |
开启后,代理会在 Command Code 生成请求以及 fingerprint/lifecycle 初始化请求中附加
x-cmd-zdr: 1。npm 版本检查和代理自己的 /provider/v1/models 模型目录请求不会附加该
header。该开关只是请求 Command Code 使用 ZDR-only 路由,实际数据留存和上游可用性仍由上游服务决定。
请求体上限:独立于 config.json —— 超过 100MB 的请求会被拒绝并返回 HTTP 413(连接保持可排空,不会直接 reset)。可用 CC_MAX_BODY_MB(正整数,单位 MB)覆盖。
⚠️ 内存放大:请求体在转发到上游前会存在多份副本,实测峰值 ≈ body 大小 × 5.1~7.4(7MB→+52MB、20MB→+116MB;被413拒绝的请求只要 ×1.05)。因此默认CC_MAX_BODY_MB=100意味着单个请求最坏可吃 ~550MB,且该上限是每请求的、不是全局的。详见内存与部署。
让代理发往 Command Code 的请求走本地 HTTP 代理 —— 用于出口地区调整,或排查风控 403 时做 IP 维度对照。
{ "upstreamProxy": "http://127.0.0.1:7890" }CC_UPSTREAM_PROXY=http://127.0.0.1:7890 npm start- 作用于
/alpha/generate、/alpha/fingerprint/record、/alpha/lifecycle-events与/provider/v1/models。 - 不影响本地监听、
/health与 npm 版本检查。 - 仅支持
http://(CONNECT)代理。实现方式是自建 CONNECT 隧道 +node:https复用同一 socket,不新增任何依赖,Node 18+ 即可用。 - 每个上游请求各自建立一条隧道连接。TLS 为端到端:证书按目标主机名校验,绝不针对代理降级。
- 指纹/lifecycle 预请求也走代理是刻意的:若它们直连而上游生成走代理,同一账号会从两个不同 IP 注册 —— 正是你想避免的那种矛盾。
- 代理地址里带账号密码(
http://user:pass@host:port)时,日志只保留host:port,不打印口令。
Node 原生
fetch不读HTTPS_PROXY/HTTP_PROXY。官方环境变量路线需要 Node ≥ 22.21 / 24.5 且设NODE_USE_ENV_PROXY=1;本选项两者都不需要。
OpenAI Chat Completions 兼容。支持流式和非流式、工具调用、多模态图片输入、推理强度。
请求体参数:
| 参数 | 必填 | 说明 |
|---|---|---|
model |
是 | 模型 ID(见模型列表) |
messages |
是 | 对话消息,支持 system/user/assistant/tool 角色 |
max_tokens |
否 | 最大生成 token(默认 64000) |
stream |
否 | 是否 SSE 流式(默认 false) |
temperature |
否 | 采样温度(0-2) |
reasoning_effort |
否 | 推理强度 low/medium/high/max |
tools |
否 | 工具定义(OpenAI function calling 格式) |
tool_choice |
否 | 工具选择策略 |
parallel_tool_calls |
否 | 是否允许并行工具调用 |
简单请求:
{
"model": "deepseek/deepseek-v4-flash",
"messages": [{ "role": "user", "content": "hello" }],
"stream": true
}多模态图片输入(需 vision 模型):
{
"model": "xiaomi/mimo-v2.5",
"messages": [{
"role": "user",
"content": [
{ "type": "text", "text": "描述这张图片" },
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
]
}]
}工具调用:
{
"model": "deepseek/deepseek-v4-flash",
"messages": [...],
"tools": [{
"type": "function",
"function": { "name": "get_weather", "description": "...", "parameters": {...} }
}],
"tool_choice": "auto"
}流式响应(SSE):
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"思考过程"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"}}]}
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"prompt_tokens_details":{"cached_tokens":8}}}
data: [DONE]
非流式响应(含缓存命中):
{
"id": "chatcmpl-xxx",
"object": "chat.completion",
"created": 1234567890,
"model": "deepseek/deepseek-v4-flash",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello!",
"reasoning_content": "The user said hello, I should respond."
},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 7558,
"completion_tokens": 42,
"total_tokens": 7600,
"prompt_tokens_details": { "cached_tokens": 7552 }
}
}Anthropic Messages API 兼容端点。支持流式和非流式、工具调用。
请求体:
{
"model": "claude-sonnet-4-6",
"max_tokens": 1000,
"system": "你是一个有用的助手。",
"messages": [
{ "role": "user", "content": "hello" }
],
"stream": true
}Anthropic 协议差异(自动转换):
| 概念 | Anthropic 原始格式 | 转换说明 |
|---|---|---|
| System prompt | 顶层 system 字段 |
自动转为 OpenAI system message |
| 消息内容 | content 数组(text/tool_use/tool_result) |
自动映射为对应角色 |
| 工具结果 | user 消息中的 tool_result 块 |
自动转为 role: "tool" |
| 工具定义 | input_schema |
自动映射为 parameters |
tool_choice |
{type:"auto"/"any"/"tool"} |
any→required,tool→function 对象 |
| 推理强度 | thinking.budget_tokens |
自动映射为 reasoning_effort(≥10000→high, ≥5000→medium, ≥2000→low) |
| 停止原因 | end_turn/max_tokens/tool_use |
自动映射为 stop/length/tool_calls |
| Token 用量 | input_tokens/output_tokens + 缓存 |
透传,缓存字段映射为 Anthropic 格式 |
流式响应(SSE,Anthropic 格式):
event: message_start
data: {"type":"message_start","message":{"id":"msg_xxx","type":"message","role":"assistant","content":[],"model":"...","usage":{"input_tokens":0,"output_tokens":0}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":10,"cache_read_input_tokens":0,"input_tokens":100}}
event: message_stop
data: {"type":"message_stop"}
非流式响应:
{
"id": "msg_xxx",
"type": "message",
"role": "assistant",
"model": "deepseek/deepseek-v4-flash",
"content": [{ "type": "text", "text": "Hello!" }],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 7558,
"output_tokens": 42,
"cache_read_input_tokens": 7552,
"cache_creation_input_tokens": null
}
}OpenAI Responses API(Codex、以及新版 OpenAI SDK 用的那套)。
请求侧做转译:input(消息数组,item 可省略 type)、instructions、max_output_tokens、temperature、top_p、reasoning、tools、tool_choice 都会映射进 CC 信封;响应按 Responses 形状返回(object: "response"、output 数组、usage、status)。流式为 SSE:
response.created / response.in_progress / response.output_item.added|done / response.content_part.added|done / response.output_text.delta|done / response.reasoning_summary_text.delta|done / response.function_call_arguments.delta|done,收尾是 response.completed(被 max_output_tokens 截断时为 response.incomplete,出错为 response.failed)。
- 无状态:
previous_response_id不支持,传了直接400—— 每轮把完整input发过来即可(代理不存会话历史)。 - 错误体是 Responses 风格:
{"error":{"message":...,"type":...}}。 - 与
/v1/chat/completions共用同一套上游调用、缓存断点与空闲看门狗。
curl http://127.0.0.1:3050/v1/responses \
-H "Authorization: Bearer user_xxxxxxxxx" -H "Content-Type: application/json" \
-d '{"model":"deepseek/deepseek-v4-flash","input":[{"role":"user","content":[{"type":"input_text","text":"hi"}]}]}'返回可用模型列表。优先从 Provider API 动态拉取(5min 缓存),失败回退硬编码列表。
健康检查。返回 OK。
代理自己产生的:
| HTTP | 场景 |
|---|---|
400 |
请求体不是合法 JSON、input 为空、用了不支持的 previous_response_id |
401 |
缺 API Key / 格式不对(Key 必须以 user_ 开头;通过 Authorization: Bearer 或 x-api-key 传入) |
404 |
路径不存在 |
413 |
请求体超过 CC_MAX_BODY_MB(连接保持可排空,不会直接 reset) |
429 |
零输出 token、流空闲超时(30s 流式 / 90s 非流式)、或上游限流映射 —— 都带 Retry-After,SDK 自动退避重试;连续 3 次超时后提示压缩上下文 |
502 |
CC 上游错误(fetch failed 这类连接层失败也走这里) |
503 |
开了 CC_MAX_INFLIGHT 且超过在途上限(type: server_busy) |
上游 CC 状态码的映射(CC_STATUS_MAP,未列出的按 502 upstream_error):
| 上游 | 下游 |
|---|---|
400 → 400 invalid_request_error |
401 → 401 authentication_error |
402 → 429 rate_limit_error(付费失败按限流处理) |
403 → 401 authentication_error |
404 → 404 not_found |
422 → 400 invalid_request_error |
429 → 429 rate_limit_error(带 retry_after: 30) |
500 / 502 → 502 upstream_error |
503 → 503 temporarily_unavailable |
其它 → 502 upstream_error |
上游错误体里的机器可读分类(error.code,如 BAD_REQUEST / USAGE_EXCEEDED)会透传到下游错误体的 error.code。
代理访问 GET /v1/models 会返回实时模型列表。以下为常见模型参考,完整列表以实际接口返回为准——各模型套餐可参考 Command Code Pricing。
| 模型 ID | 提供商 |
|---|---|
claude-sonnet-4-6 / claude-opus-4-8 / claude-opus-4-7 / claude-haiku-4-5-20251001 |
Anthropic |
gpt-5.5 / gpt-5.4 / gpt-5.4-mini / gpt-5.3-codex |
OpenAI |
deepseek/deepseek-v4-pro / deepseek/deepseek-v4-flash |
DeepSeek |
moonshotai/Kimi-K2.6 / moonshotai/Kimi-K2.5 |
Kimi |
zai-org/GLM-5.1 / zai-org/GLM-5 |
GLM |
MiniMaxAI/MiniMax-M3 / MiniMaxAI/MiniMax-M2.7 / MiniMaxAI/MiniMax-M2.5 |
MiniMax |
Qwen/Qwen3.7-Max / Qwen/Qwen3.6-Max-Preview / Qwen/Qwen3.6-Plus |
Qwen |
stepfun/Step-3.7-Flash / stepfun/Step-3.5-Flash |
Step |
xiaomi/mimo-v2.5-pro / xiaomi/mimo-v2.5 |
Xiaomi(支持图片输入) |
google/gemini-3.5-flash / google/gemini-3.1-flash-lite |
Gemini |
⚠️ 部分模型(如deepseek-v4-flash、claude-sonnet-4-6)不支持图片输入。如需多模态请用xiaomi/mimo-v2.5、Kimi-K2.5等 vision 模型。
from openai import OpenAI
client = OpenAI(
api_key="user_xxxxxxxxx",
base_url="http://127.0.0.1:3050/v1",
)
response = client.chat.completions.create(
model="deepseek/deepseek-v4-flash",
messages=[{"role": "user", "content": "hello"}],
stream=True,
)
for chunk in response:
print(chunk.choices[0].delta.content or "", end="")curl http://127.0.0.1:3050/v1/chat/completions \
-H "Authorization: Bearer user_xxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v4-flash",
"messages": [{"role": "user", "content": "hello"}],
"stream": true
}'在 Cursor 设置中添加 Custom Provider:
- API Base URL:
http://127.0.0.1:3050/v1 - API Key:
user_xxxxxxxxx - Model: 从模型列表中选择
import anthropic
client = anthropic.Anthropic(
api_key="user_xxxxxxxxx",
base_url="http://127.0.0.1:3050",
)
message = client.messages.create(
model="deepseek/deepseek-v4-flash",
max_tokens=1000,
system="You are helpful.",
messages=[{"role": "user", "content": "hello"}],
)
print(message.content[0].text)Anthropic SDK 通过 x-api-key 头鉴权——代理已原生支持(无需 Authorization 头)。
{
"provider": "openai-compatible",
"baseUrl": "http://127.0.0.1:3050/v1",
"apiKey": "user_xxxxxxxxx"
}基于对官方 CLI 网络流量的分析(版本号从 npm registry 动态拉取),实现了以下兼容适配:
| 机制 | 实现 |
|---|---|
| 设备指纹 | 每个 Key 首次请求前发送 POST /alpha/fingerprint/record;信号值(Windows MachineGuid 形状、真实形状的 MAC、DESKTOP-xxxxxx 主机名)由 API key 确定性派生,并按 CLI 的算法哈希 —— 同一个 key 永远报告同一台设备:重启、内存回收、多实例都一致(用 CC_FINGERPRINT_SALT 成批换身份) |
| 生命周期声明 | Key 初始化时与指纹并行发送 POST /alpha/lifecycle-events(cli_session_exists,metadata {sessionId, cliVersion, mode, os}) |
| 按 Key 分 Session | 每个 API Key 独立 session,12h 过期 + 1h 随机抖动 |
| 协议版本号 | x-command-code-version 报实际实现的协议版本(当前 1.53.1);npm 上有新版本只打漂移告警,不会静默改版本号 |
| CLI 信封格式 | 9 键:config / memory / taste / skills / permissionMode / threadId / mode / promptCache / params |
| OpenTelemetry | traceparent (W3C Trace Context) |
| 环境标识 | x-cli-environment: production、x-taste-learning: "false"、User-Agent: cli |
| Project Slug | x-project-slug = slugify(DEVICE_PROFILE.projectDir),与 config.workingDir 同源(默认 C:\Users\dev\projects\app,用 CC_DEVICE_PROJECT_DIR 改) |
| 设备档案单一真源 | 指纹 / config.environment / config.workingDir / x-project-slug / lifecycle 的 os 共用同一份 DEVICE_PROFILE(win32 / x64)—— 既不会自相矛盾("指纹说 win32、环境说 linux"),也不把宿主真实平台、Node 版本、cwd 交给上游 |
| 思考强度 | reasoning_effort 透传 (low/medium/high/max) |
| API Key 格式验证 | 对 Authorization: Bearer 或 x-api-key 用正则 user_[a-zA-Z0-9_-]+ 提取,自动清理多余路径/前缀,sk-xxx 等非 user_ 格式拒 |
| 流式超时保护 | 流式 30s、非流式 90s → 429 + SDK 自动重试 |
| 连续超时阈值 | 连续 3 次超时后才提示压缩上下文 |
| 零输出防护 | outputTokens=0 → 429 rate_limit_error(SDK 自动重试,反异常计费) |
| 上游中止 | 客户端断连 + 全部错误路径 AbortController 打断 CC |
| 隐私保护日志 | 日志不含 API Key 片段、错误 body、stack trace |
{
"config": {
"workingDir": "C:\\project",
"date": "2026-06-07",
"environment": "win32",
"structure": [],
"isGitRepo": false,
"currentBranch": "",
"mainBranch": "",
"gitStatus": "",
"recentCommits": []
},
"memory": null,
"taste": null,
"skills": null,
"permissionMode": "standard",
"params": {
"model": "deepseek/deepseek-v4-flash",
"messages": [...],
"max_tokens": 64000,
"stream": true,
"reasoning_effort": "max"
}
}config.environment / config.workingDir 都取自 DEVICE_PROFILE(不是宿主真实值),skills 发 null(不是空串)。
条件字段:system(从 system 消息提取)、temperature、reasoning_effort、tools(映射为 CC input_schema 格式)、tool_choice、parallel_tool_calls。给了 prompt_cache_key(或客户端自带 cache_control 断点)时,断点会落在 system 的最后一块上 —— 缓存按前缀计,system 正是最前那段前缀。
CLI 发送图片的格式:
{
"role": "user",
"content": [
{ "type": "image", "image": "data:image/jpeg;base64,..." },
{ "type": "text", "text": "图里写了什么" }
]
}代理收到 OpenAI image_url 格式后自动转为上述 CC 格式透传。
GitHub Actions 会把多架构镜像(linux/amd64 + linux/arm64)推到 GitHub Container Registry:
| 标签 | 来源 | 说明 |
|---|---|---|
:release |
release 分支 |
跟随发布分支 |
:latest |
release 分支 或 v* tag |
与 :release 同一个 digest。"只在打 tag 时更新"是已修掉的旧行为 —— 它曾让用 :latest 的人长期停在旧版本、新端点表现为 404(#28) |
docker pull ghcr.io/maxeaglet/commandcode-proxy:release
docker run -d --name cc-proxy -p 3050:3050 -e PORT=3050 ghcr.io/maxeaglet/commandcode-proxy:release镜像为公共可见,拉取无需登录。升级后请确认 digest 真的变了(docker inspect --format '{{index .RepoDigests 0}}'),别假设本地缓存就是新版本。
docker compose up -d代理将在 http://0.0.0.0:3050 监听。通过 PROXY_PORT 自定义主机端口:
PROXY_PORT=13050 docker compose up -ddocker build -t commandcode-proxy:latest .
docker run -d -p 3050:3050 -e PORT=3050 commandcode-proxy:latestnpm run docker:build:multi容器相关的只有两个,其余全部见上面的环境变量总表:
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
3050 |
容器内监听端口 |
PROXY_PORT |
3050 |
主机映射端口(仅 compose) |
默认关闭(CC_MAX_INFLIGHT 未设置 = 不限制并发),既有行为不变。
本项目定位是纯反代层,并发控制属于下游 —— 按 IP / 按 key 的限流请用反向代理(见内存与部署里的 limit_conn)。
本项不是那套方案的替代品,只为「不挂反代裸跑」(Dockerfile 与 npm start 都支持这种用法)提供一个进程内、仅全局的兜底:
CC_MAX_INFLIGHT=32 npm start # 最多同时处理 32 个请求超限时快速返回 503 + Retry-After: 5 + type: server_busy —— OpenAI / Anthropic 官方 SDK 认得这个组合会自动退避重试,而不是拿到连接被重置。/health 与 / 不计入、也不受限制,避免探活与编排器因业务繁忙收到 503。
为什么需要它:内存 = 在途数 × (0.13MB + 5.5 × body_MB)。CC_MAX_BODY_MB 只管住单请求量级,乘数无人管 —— 默认 100MB 时 N 个并发最坏可达 N × 550MB。
⚠️ 开启本项不等于内存安全:32 × 550MB 仍远超小机器容量。要拿到硬性上界,需同时下调CC_MAX_BODY_MB。
两个上游读空闲看门狗,超时后返回 429(带 retry_after)让 SDK 自动重试:
| 环境变量 | 默认 | 作用于 |
|---|---|---|
CC_STREAM_IDLE_MS |
30000 |
流式请求 |
CC_NONSTREAM_IDLE_MS |
90000 |
非流式请求 |
语义:只计「reader.read() 的等待时间」,每收到一个 chunk 就重置 —— 不是整个请求的总时长。
只要上游在持续吐流就不会触发,哪怕单个请求已经跑了几十分钟。
默认值与官方 CLI 不一致,这是已知取舍(#19):
官方 CLI 对上游没有任何 idle timeout —— 反编译 command-code@1.50.0 可见所有 createApiClient({ baseUrl }) 调用点都未传 timeout,实测 700+ 秒的停顿可正常完成。
本代理保留 30s 是为了兜住真正死掉的连接;代价是推理模型的长思考停顿可能被误杀。
若遇到「429 Response timeout」「zero output tokens」且日志里 elapsedMs ≈ 30000、bytesReceived = 0,
说明是看门狗误杀了 prefill / 首 token 阶段的正常停顿 —— 调大即可:
CC_STREAM_IDLE_MS=300000 npm start # 5 分钟
⚠️ 误杀的成本不止一次失败:被 abort 后返回429 + retry_after,SDK 会自动重试, 而重试等于完整重发整个上下文,长会话下每次误杀都要重付一次全量 prefill。
数据来自 issue #20 的实测复现(Node v24,loopback mock 上游)。
单请求内存开销的经验公式:
RSS ≈ 70 MB + 在途请求数 × (0.13 MB + 5.5 × body_MB)
res.write() 返回 false(socket 写缓冲超过 highWaterMark)时会暂停读取上游,响应不再在内存中无界堆积:
| 场景(200MB 上游流,客户端发完请求即停止读取) | 峰值 RSS 增量 |
|---|---|
| 修复前 | +586 MB(66 → 652 MB) |
| 修复后 | +4 MB(背压一路传回上游,上游只吐出 ~8MB 即停住) |
这不只是恶意客户端问题 —— 弱网/移动端、客户端卡在工具执行、客户端已放弃但 TCP 还没发 RST,都会触发。
body 在转发到上游前同时存在多份副本:chunks[] / Buffer.concat / utf8 字符串 / JSON.parse 对象树 / buildCcRequest 重建对象树 / JSON.stringify 序列化体。
| body | 上限 | 峰值增量 | 结果 |
|---|---|---|---|
| 7 MB | 100 MB | +52 MB(7.4×) | 200 |
| 20 MB | 100 MB | +116 MB(5.8×) | 200 |
| 20 MB | 8 MB | +21 MB(1.05×) | 413 |
启动时若隐含最坏峰值 ≥ 500MB,日志会输出 warn 提示。上限是按请求的,proxy 自身没有在途限流 —— 公网部署必须在反向代理层补上。
client_max_body_size 在 nginx 拒绝时,body 根本不会进入 Node 进程:
map $http_authorization $cc_key { default $http_authorization; "" $http_x_api_key; }
map "" $cc_global_key { default "global"; }
limit_conn_zone $binary_remote_addr zone=cc_ip:10m;
limit_conn_zone $cc_key zone=cc_key:10m;
limit_conn_zone $cc_global_key zone=cc_global:10m;
location /v1/ {
client_max_body_size 4m; # 需 <= CC_MAX_BODY_MB
limit_conn cc_ip 8;
limit_conn cc_key 4;
limit_conn cc_global 32; # 这一项就是内存天花板
limit_conn_status 429;
proxy_pass http://127.0.0.1:3050;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 300s; # 需大于 30s 的流空闲超时
}
⚠️ keep-alive 时序:上面proxy_set_header Connection ""让 nginx 与后端保持长连接,此时反代侧的upstream { keepalive_timeout ...; }必须小于后端的CC_KEEPALIVE_TIMEOUT_MS(默认 65s)。反了的话, 反代会复用一条后端已经 FIN 掉的连接去写 POST 请求体,吃EPIPE(nginx 日志里是sendfile() failed (32: Broken pipe)); 而 POST 非幂等、nginx 默认不重试 —— 客户端直接拿到 502。
背压生效后,客户端既不读也不断开时该请求会连带上游连接一直挂着。实测残留在途成本:
| 僵死连接数 | RSS 增量 | 上游连接持有 |
|---|---|---|
| 1 | +5 MB | 1 |
| 10 | +45 MB | 10 |
| 50 | +248 MB | 50(永久持有) |
特性是有界、不泄漏、客户端断开即回收(RSS 曲线完全持平),但连接数本身无上限。
默认不处理,因为僵死客户端与「卡在工具执行的合法客户端」在协议层无法区分;且官方 CLI 对上游没有任何 idle timeout(见 #19),贸然加超时会重蹈「误杀健康请求」。
需要封顶时启用(opt-in):
# 下游持续阻塞超过 60s 才断开,正常客户端只要在推进 drain 就不会触发
CC_CLIENT_DRAIN_TIMEOUT_MS=60000 npm start启用后实测(50 个僵死连接):上游连接持有数由 50(永久)→ 0,且丢弃后不会继续抽干上游。
更稳妥的封顶仍在反向代理层(limit_conn),因为只有它知道该部署能承受多少并发。
logFile是同步写(appendFileSync),公网负载下会阻塞事件循环 —— 建议保持留空,从 stdout 收集。- systemd 兜底:配
MemoryMax=与NODE_OPTIONS=--max-old-space-size=,让超限杀掉 proxy 而不是sshd/nginx。 - 多账号 + 多实例:
sessionStore/keyStateStore是进程内Map。同一个 API key 打到两个实例会得到两个不同 session 与两个不同设备指纹,上游会看到「一个账号在多台机器上」。横向扩展请按 API key 做一致性哈希(hash $cc_key consistent),不要轮询。
本项目仅供学习和研究使用。
- 非官方:本项目与 Command Code 无任何关联,非官方产品。
- 个人使用:使用者应自行承担所有责任。请遵守 Command Code 服务条款。
- API Key:本项目不会收集、上传或泄露你的 API Key。Key 通过每次请求的
Authorization: Bearer <key>或x-api-key头传入,日志中不记录;config.json中的可选apiKey字段仅作本地兜底,不会离开你的机器。 - 合规性:协议基于对本地 CLI 网络流量的被动观察,未对服务端进行任何未授权访问、破解或篡改。
- 账号风险:建议和正常 CLI 使用频率保持一致,超高并发调用可能触发风控。
# 带 watch 模式启动(文件修改自动重启)
npm run dev