cmc-proxy
Health Warn
- No license — Repository has no license file
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Fail
- child_process — Shell command execution capability in build.js
- execSync — Synchronous shell command execution in build.js
- fs.rmSync — Destructive file system operation in build.js
- fs module — File system access in build.js
- exec() — Shell command execution in goat-prices.js
- fs module — File system access in goat-prices.js
- network request — Outbound network request in goat-prices.js
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Reverse-proxy a GOAT/commandcode AI subscription to localhost for Claude Code & Codex — OpenAI/Anthropic/Responses tri-protocol conversion, unified model routing, auto failover rotation & prefix-cache-friendly transforms. Zero-dependency single file (Node.js 18+).
cmc-proxy — commandcode GOAT 订阅本地反代网关
把 commandcode 的 Provider API 反代到本机 127.0.0.1:5411,让本地 Claude Code 和 Codex 直接接入你的 GOAT 订阅。大部分其他 Agent (Pi,zcode等)本身可以直连,无需此代理。当然加一层可以更智能些,比如图片请求可以自动路由,或上游失败时自动切换,还可以看到一些token用量、缓存命中的统计——我喜欢看 tps,快慢自知容易心里踏实。
单笔消费成本/消耗额度也可以看了,区分峰谷期计算(时间颜色和^符号),运行前 node goat-prices.js 先拉取一下价格,并确保 "modelCatalog": "goat-prices.json" 配置项存在。
- 极简单文件
proxy.js,零第三方依赖,无需安装 npm 包,仅需 Node.js ≥ 18(内置fetch/ReadableStream),node proxy.js即可启动(都知道要反代了,想必node早就配备了)。 - 代码安全,不含任何后门或远程调用,所有请求仅在本机转发到 commandcode 上游;基本上是 AI 代写,使用前可以让 AI 再审查一遍。本身 AI 写一个不难,难的是如何避坑以及设计实用功能,github 找了一圈没找到合适的,所以自己让 AI 帮忙写了一个。遇到的坑包括:CC改历史、变换版本后缀、Codex只支持Responses、鬼知道什么时候配置了记忆生成导致token额外消耗等。反正这里暂时解决了。
- 同时提供 OpenAI 兼容(
/v1/chat/completions)、Anthropic 兼容(/v1/messages,供 Claude Code)与 Responses 兼容(/v1/responses,供 Codex)三类端点;看上去opencode-go套餐也可以拿来改一下EndPoint(upstream和apikey) 直接用,可能需要适应性修改,目前停了 ocgo 套餐,不去折腾了。 - 协议转换:GOAT 订阅不含任何 Claude 模型,Claude Code 请求自动
Anthropic → OpenAI转换;Codex 的 Responses 请求自动Responses → Chat Completions转换(上游只有 chat/completions);流式 + 工具调用全链路支持 - 统一模型决策
pickModel:modelMap显式映射 → 上游模型目录解析(resolveModel)→ 按请求类型回退默认(文本defaultModels[0]/ 带图defaultVisionModels[0]) - 失败轮换
switchOnFail(支持布尔或{text, image}):失败 1 次即切换 +failTTL冷却(只对回退到默认的模型生效,用户显式指定模型失败不冷却、下次仍从它开始);文本请求按defaultModels、带图请求按defaultVisionModels轮换(带图请求 400 也轮换,图片不支持的报错就是 400) - 多模态:
visionAutoRoute(2026-9-6新增)在带图请求决策出"判定不支持视觉"的模型时前置改走defaultVisionModels[0](免上游 400错误请求/200静默盲视);因为最近调deepseek v4 flash时已经从之前的400变成了200,导致仅靠失败轮换失效;tool_result内嵌图片抽出注入同轮 user 消息透传;cleanHistoryImages可在本轮无新图时清理历史图片,让请求安全回流纯文本模型;REQ 行img=N(新M)标记 + 会话标签@前缀 - 前缀缓存优化:注入提醒剥离、易变计数器取整、
cache_control透传、会话缓存亲和——同会话 Claude Code / Codex 的上游前缀缓存可稳定在 95%+;RES 行内置前缀分叉探测(pfx~标记)可定位缓存失效来源。此类信息主要用于调试,正常使用请忽略。 - Codex 新协议全兼容:
custom(apply_patch freeform)/tool_search(延迟工具发现)/namespace工具组 / 顶层function_call历史等新形态全链路支持 - 会话级访问日志:按
x-claude-code-session-id/session-id/thread-id稳定归因,两行日志(REQ/RES)配对 + 缓存命中率 / 生成速度 / 前缀分叉 / 累计用量统计 - 结构化 JSONL 请求日志(
config.jsonjsonlLog):把与 REQ/RES 两行同源的当次请求数据在 RES 输出时写一条 JSON 到requests.jsonl,供离线分析(独立于CMC_LOGGING_FILE分级落盘) - 支持流式 SSE 透传、token 用量上报、模型列表过滤、分级请求落盘(环境变量
CMC_LOGGING_FILE)
以下内容主要由AI维护
文件说明
cmc-proxy/
├── proxy.js # 主程序(反代 + 协议转换,单文件)
├── config.json # 配置:端口、API Key、模型映射(gitignore,不入库)
├── config.example.json # 配置模板
├── build.js # 构建脚本:导出 release 版本
├── start.bat # Windows 启动脚本
├── start.sh # macOS/Linux 启动脚本
├── schemas.md # 四阶段数据格式与样例参考(Local/Upstream 请求与响应)
├── release/ # build 产物(gitignore,由 build.js 生成)
└── README.md
构建 Release
release/ 目录是构建产物(已 gitignore),由 build.js 生成,不要手动编辑:
node build.js # 导出到 release/,版本号自动取 git tag/commit
node build.js --version v1.0.1 # 指定版本号
node build.js --out dist # 自定义输出目录
产物包含 proxy.js、config.example.json、start.bat、start.sh、README.md 与 VERSION.txt(版本 / 构建时间 / git commit / 文件校验和)。不含 config.json(含私有 apiKey)。拿到 release 后:
- 复制
config.example.json为config.json; - 填入你的 apiKey,按需调整端口 / 模型映射;
start.bat(Windows)或./start.sh(macOS/Linux)启动。
快速开始
确保已安装 Node.js ≥ 18(
node -v验证)。检查
config.json:apiKey已填入你的 GOAT API Key;确认端口为5411。启动:
# Windows start.bat # macOS / Linux ./start.sh # 或直接 node proxy.js命令行参数:
node proxy.js [--port 5411] [--host 127.0.0.1] [--config config.json]
(--config支持绝对路径,或相对 proxy.js 所在目录的相对路径)验证:
curl http://127.0.0.1:5411/health应返回{"ok":true,...}。
启动后打印 banner 与配置摘要,可据此确认生效的模型与开关:
==========================================================
cmc-proxy 已启动
监听地址 : http://127.0.0.1:5411
上游端点 : https://api.commandcode.ai/provider
默认模型 : deepseek/deepseek-v4-flash(默认) → deepseek/deepseek-v4-flash-vision-exp → …
视觉模型 : xiaomi/mimo-v2.5(默认) → z-ai/glm-5.3-flash → Qwen/Qwen3.8-27B
失败轮换 : 对象 {text:true, image:true} (失败1次即切换 + 30s 冷却)
历史图清理 : 开启 (无新图请求时剥离历史图, 回流请求指定模型)
tool结果图 : 保留 (注入 user 消息透传)
----------------------------------------------------------
Claude Code 接入: export ANTHROPIC_BASE_URL=http://localhost:5411
Codex 接入: base_url = http://localhost:5411/v1 (wire_api = responses)
==========================================================
[17:00:12.334] 刷新上游模型列表成功: 62 条 (17:00:12.334)
Claude Code 接入
Claude Code 走 Anthropic 协议。设置环境变量后启动 claude:
# Windows (PowerShell)
$env:ANTHROPIC_BASE_URL="http://localhost:5411"
$env:ANTHROPIC_AUTH_TOKEN="sk-local-any-value"
# 不设置 ANTHROPIC_MODEL 时使用默认模型(defaultModels[0])
claude
# macOS / Linux
export ANTHROPIC_BASE_URL=http://localhost:5411
export ANTHROPIC_AUTH_TOKEN=sk-local-any-value
claude
ANTHROPIC_AUTH_TOKEN填任意值即可,反代会替换成真实 Key。
也可以在 Claude Code 里用/model选择反代/v1/models返回的模型,或直接输入不带前缀的模型名(如deepseek-v4-flash、qwen3.8-max),反代会自动匹配到对应的上游模型。
Codex 接入
Codex CLI(≥ 0.84)只支持 Responses API(wire_api = "chat" 已移除)。编辑 ~/.codex/config.toml:
model_provider = "cmdc-goat"
model = "deepseek-v4-flash"
[model_providers.cmdc-goat]
name = "CommandCode GOAT"
base_url = "http://localhost:5411/v1"
wire_api = "responses"
env_key = "CMD_GOAT_KEY"
然后:
export CMD_GOAT_KEY=sk-local-any-value
codex
或使用 OpenAI 兼容环境变量:
export OPENAI_BASE_URL=http://localhost:5411/v1
export OPENAI_API_KEY=sk-local-any-value
export OPENAI_MODEL=deepseek-v4-flash
base_url需要带/v1后缀。Codex 会请求POST /v1/responses;cmc-proxy 内置 Responses ↔ Chat Completions 协议转换(因为 commandcode 上游只提供 chat/completions 端点),流式与工具调用均支持。
模型说明
GOAT 订阅不包含 Claude 全系(Sonnet 需 Pro、Opus 需 Provider),也不含 GPT-5.5 及以下。实测可用的模型(2026-08):
| 模型 ID | 视觉 | 说明 |
|---|---|---|
deepseek/deepseek-v4-flash |
❌ 纯文本(带图必 400) | 默认模型,DeepSeek V4 Flash,速度快性价比高 |
deepseek/deepseek-v4-flash-vision-exp |
✅ | DeepSeek V4 Flash Vision(实验版,支持视觉) |
deepseek/deepseek-v4-pro |
未实测 | DeepSeek V4 Pro |
z-ai/glm-5.3-flash |
✅ | 智谱 GLM-5.3 Flash |
Qwen/Qwen3.8-27B |
✅ | 阿里 Qwen 3.8 27B |
xiaomi/mimo-v2.5 |
✅ | 小米 MiMo V2.5 |
gpt-5.6-sol |
未实测 | GPT 编码/智能体能力强,Codex 系 |
moonshotai/Kimi-K2.7-Code / moonshotai/Kimi-K3 |
未实测 | Kimi 编码系 |
zai-org/GLM-5.3 / zai-org/GLM-5.2 |
未实测 | 智谱 GLM |
Qwen/Qwen3.8-Max / Qwen/Qwen3.7-Flash |
未实测 | 阿里 Qwen |
MiniMaxAI/MiniMax-M3 |
未实测 | MiniMax |
xai/grok-4.6 |
未实测 | Grok |
xiaomi/mimo-v2.5-pro |
未实测 | 小米,限时高折扣 |
tencent/hy3-paid |
未实测 | 腾讯 |
- 完整列表:
curl http://127.0.0.1:5411/v1/models(已过滤blockedModels,?raw=1看全量)。上游模型目录 60s 缓存,启动时会预热并打印刷新上游模型列表成功: N 条。 - 换模型:文本请求改
config.json的defaultModels(数组,第一个为文本默认模型),带图请求改defaultVisionModels;或在modelMap里把特定模型名映射到目标模型后重启。 - GOAT 按订阅额度计费,模型实际可用性以上游返回为准。
- 图片 token 成本实测:
deepseek-v4-flash-vision-exp一张 1920×1080 ≈ 370 prompt tokens(非分辨率线性)。
模型决策(pickModel)
所有转发路径共用同一个决策函数,顺序固定:
请求带 model?
├─ 没带 → 按请求类型取默认: 文本 defaultModels[0], 带图 defaultVisionModels[0]
└─ 带了 → modelMap 显式映射 (命中即用, 不区分请求类型, 优先级最高, 置空即关闭)
└─ 未命中 → config.resolveModel (默认 true)
├─ true → resolveModel(): 上游模型目录匹配, 命中即用; 未命中按请求类型回退默认
└─ false → 原样向上游请求 (不解析不回退)
带图判定(
isImage)按路径不同:
路径 isImage判定/v1/messages原始请求体含图片块( countImagesDeep(body) > 0);开启cleanHistoryImages且本轮无新图时,历史图剥离后重新判定为文本/v1/chat/completions请求体含图片块( image_url/input_image/ Anthropicimage块)/v1/responses转换后的 chat messages 含 image_urlpart
/v1/messages会决策两次:先按原始请求体判断是否需要直连上游/messages(Claude 模型),再在历史图片清理之后按最终请求类型重新决策转换路径用的模型。
defaultVisionModels 为空时,带图请求回退到 defaultModels[0]。
失败轮换(switchOnFail)
config.json 的 switchOnFail 是轮换总闸,支持三种写法:
{ "switchOnFail": true } // 文本/带图统一开启
{ "switchOnFail": false } // 关闭(默认):失败原样返回
{ "switchOnFail": { "text": true, "image": true } } // 分别控制
开启后的精确语义(proxy.js 的 upstreamFetchRotate):
- 轮换列表按请求类型:文本请求 →
defaultModels,带图请求 →defaultVisionModels。去重后候选只剩 1 个时(典型情况:该类型列表只有 1 个模型,且首个候选就是它)不轮换;banner 在两个列表都不足 2 个时会显示"不适用 (列表仅一个模型, 不轮换)"。 - 候选序列:
pickModel决策出的模型(尊重客户端显式意图,含blockedModels中的模型)作为首个候选,随后按类型列表顺序补全并去重。每次尝试都会重写请求体里的model字段——上游实际收到的模型跟着变。 - 失败 1 次即切换:不再区分"首个模型/后续模型",失败立刻换下一个候选。
- TTL 冷却(只对回退到默认的模型生效):失败模型进入冷却(
config.failTTL,默认 30000ms),冷却期内跨请求也跳过该模型并打印模型 X 冷却中, 跳过;成功即清除冷却。failTTL: 0关闭冷却。用户显式指定的模型(经modelMap/ 目录解析命中,未落到回退点)不冷却——不去猜测用户指定模型的能力(如纯文本模型收到带图请求),失败即轮换,下次请求仍从它开始;只有未带 model 或指定模型解析失败、回退到defaultForType的模型(defaultModels[0]/defaultVisionModels[0])失败才进入冷却。默认列表里的后续候选(轮换到的 index ≥ 1)无论何种情况都照常冷却。 - 哪些失败才轮换:
403 / 404 / 408 / 429 / 500 / 502 / 503 / 504与网络层错误(含首字节超时)。400 / 401 / 413 / 422等(请求体非法、key 无效、body 超限)换任何模型结果都一样,不轮换、直接透传,避免一个失败放大成 N 个。两类 400 例外:(a) 带图请求的 400(图片不支持的报错就是 400);(b) 状态/能力错配类 400 —— 错误体匹配reasoning_content ... must be passed back/thinking mode/does not support image|vision等(换模型确实可解,见「reasoning / thinking 桥接」),日志打印上游 400 (<model>), 状态错配轮换 → <next>。为判断是否属于 (b),400 的响应体会被读取一次用于匹配;不轮换时用读到的文本重建 Response 原样透传(body 不丢失)。注意任何非 2xx 都会记入冷却表(用户显式指定模型除外),不轮换的那几类也会让该模型冷却一个 TTL。 - 成功即停:任一候选返回 2xx 即用该响应继续原有流程(流式转换/透传),并把 RES 行的
model=更新为实际生效的模型(reasoningBridge的回填在候选切换后依然生效——注入字段被所有候选接受)。 - 全部试完:把最后一次尝试的上游响应(状态码 + body)或错误透传给客户端,客户端看到的是真实收尾结果。每次将被轮换掉的失败尝试都会先补一条 RES 形态的行(与 REQ 行配对,不必等到最终结果):
[ts] S1#2 <status> POST /v1/messages model=<model> try=i/N took=<该次耗时> [code=... msg=...]—— 网络层失败记502(调用方就此收尾会返回的状态)并带code=/msg=摘要,HTTP 失败记上游真实状态码(错误体摘要留在随后的[cmc-proxy]动作行)。随后打印动作行:上游 <status> (<model>), 轮换 → <next> (i/N): <错误体摘要>或上游请求失败 (<model>): code=<错误码> msg=<错误信息>, 轮换 → <next> (i/N)。错误码/信息来自e.cause链(Node fetch 只抛fetch failed,真正原因是ECONNREFUSED/ENOTFOUND/UND_ERR_CONNECT_TIMEOUT等),首字节超时/客户端断开分别标注FIRST_BYTE_TIMEOUT/CLIENT_ABORT;502 收尾与客户端错误体同样带code=/msg=摘要,便于定位。终态失败不再补尝试行(由 RES 行负责),避免重复。 - 全部在冷却期:不再发起请求,直接把冷却错误交给调用方(502 收尾)。
- 客户端断开立即终止:不再重试,也不计失败。
- 边界:轮换只覆盖响应头阶段的失败(拿到响应头之后、向客户端写任何字节之前,重试是安全的);中途断流不可重试(字节已发出)。重试期间全程持有该会话串行锁,同会话后续请求会排队。
- 没有活动模型指针:默认模型恒为
defaultModels[0]/defaultVisionModels[0],一次成功的轮换不会改变后续请求的默认模型;跨请求规避已死模型只靠 TTL 冷却。
代价须知:最坏耗时 ≈ 候选数 × 各自的首字节超时(firstByteTimeout,默认 120s),5 个模型的配置最坏可拖数分钟。通常配较小的 firstByteTimeout(如 15~30s)使用。
钉死单模型的语义请只保留
defaultModels中的一个模型。旧配置中的defaultModel字段仍兼容(视为单元素列表),imageCapableModels自动迁移为defaultVisionModels。switchOnFail: false时单次请求不轮换,但回退到默认的模型失败仍会记入冷却表(onRequestFail),成功清除;用户显式指定的模型不记入。
模型匹配规则
客户端请求的模型名按以下顺序解析(proxy.js 的 pickModel / resolveModel):
- 显式映射表
modelMap:claude-*、gpt-5.x-codex等已内置映射,命中即用、不区分请求类型。 - 上游模型目录匹配(
resolveModel,需config.resolveModel !== false),按序尝试:- 精确匹配;
- 大小写不敏感匹配;
- 去 provider 前缀按裸名匹配(
deepseek-v4-flash→deepseek/deepseek-v4-flash、qwen3.8-max→Qwen/Qwen3.8-Max); - 去
[*]后缀匹配(deepseek-v4-flash[1m]→deepseek/deepseek-v4-flash,视为同模型的不同上下文窗口变体)。
- 无任何匹配 → 按请求类型回退默认(文本
defaultModels[0]/ 带图defaultVisionModels[0])。
因此本地客户端(尤其 Claude Code)可以直接用不带前缀的模型名,例如 /model 输入 deepseek-v4-flash、kimi-k2.7-code 等,都会被自动映射。
多模态(图片)支持
上游不支持在 tool 消息里带图(实测 400,vision 模型同样拒绝),也不支持纯文本模型处理图片。cmc-proxy 的处理:
| 开关(config.json) | 默认 | 作用 |
|---|---|---|
toolResultImages |
true |
tool_result 内嵌的图片块抽出,注入同轮末尾的 user 消息:先插一条文本 part [tool_result <id> 附带的图片],再接 image_url part。false 时丢弃(折叠为 [image] 占位符) |
visionAutoRoute |
true |
带图请求前置路由:决策出的模型判定不支持视觉(判定为 modelCatalog 的 vision 字段与 defaultVisionModels 白名单的并集:在名单内 / catalog vision:true 视为支持),则不发该模型、改走 defaultVisionModels[0],免上游 400 / 静默盲视(200 假装看不见)。false = 保持现状(先按原模型发,失败靠 switchOnFail 轮换) |
cleanHistoryImages |
false |
仅 /v1/messages 转换链路:本轮(最后一条 user 消息)无新图时,把该消息之前所有图片块(含 tool_result 内嵌)原位替换为占位文本 [历史图片已清理]。历史里的图上游同样 400,剥离后请求可安全回流纯文本模型,带图路由随之只看新图——会话不再被历史图片钉死在视觉模型 |
- 判定"是否支持视觉"时
modelCatalog与defaultVisionModels是并集关系:defaultVisionModels是用户显式声明"带图用它"(白名单,优先信任),modelCatalog提供 catalog 里非白名单模型的权威vision字段;两者都不覆盖时视为不支持。 - 替换是确定性的:同一段历史每轮剥出逐字节一致的结果,不破坏前缀缓存(代价是历史图片内容对模型不可见)。
- 纯文本路径(无图请求)逐字节保持旧行为,
toolResultImages=false时文本也完全不变。 - 日志侧:REQ 行显示
img=N(新M)——N为请求体中的图片块总数,新M为最后一条 user 消息(本轮)中的新图数;本轮有新图时会话标签加@前缀(@S3#3)。 - 已知限制:文本链路的历史图占位符是
[历史图片已清理],而tool_result折叠用的占位符是[image]。带图轮次的图在下一轮变成占位文本时,tool 消息内容与上一轮不同([image]→[历史图片已清理])、其后的 user 消息里注入的真图也消失,RES 行会触发一次pfx~N分叉(缓存断一次)。统一两处占位符只能让 tool 消息跨轮字节一致(分叉点后移一条消息),消除不了这次缓存断——真图从上下文消失是剥离历史图的固有代价。
reasoning / thinking 桥接(DeepSeek 工具循环 400)
DeepSeek 系 thinking 模型有一条硬要求:当历史里出现 assistant(tool_calls) 且对话以 tool 结果结尾(即模型正在续写工具循环)时,messages 里每一条 assistant(tool_calls) 都必须回传非空 reasoning_content,否则上游直接 400:
The `reasoning_content` in the thinking mode must be passed back to the API.
实测边界(deepseek/deepseek-v4.1-flash):
| 历史形态 | 结果 |
|---|---|
user, a(call), tool(尾 = tool 结果) |
❌ 400 |
user, a(call), tool, user(尾 = user,循环已闭合) |
✅ 200(无要求) |
两条 a(call),只给最后一条带 reasoning |
❌ 400(每一条都要) |
两条 a(call),只给第一条带 reasoning |
❌ 400 |
reasoning_content: "" 空串 |
❌ 400(等同没带) |
全部带(reasoning_content / reasoning / reasoning_details 任一形态) |
✅ 200 |
Anthropic 协议里这条要求对应 thinking 块({type:"thinking", thinking, signature}),但转换层此前出站和入站两个方向都把它丢掉了:客户端拿不到 thinking(无从回传),上游拿不到 reasoning(直接 400)——表现为"工具循环第一跳就 400,日志 rt:N 明明有思考量"。修复分三层:
只有一个开关(两个方向是同一件事,拆成多个键只会费解):
"reasoningBridge": true // 默认:出站回填 + 入站 thinking 透传,不截断
"reasoningBridge": false // 关闭(恢复旧行为:工具循环会 400)
"reasoningBridge": { "maxChars": 500 } // 细调:每条回填最多 500 字符
"reasoningBridge": { "passthrough": false } // 细调:只修 400,不下发 thinking 块
| 字段 | 默认 | 作用 |
|---|---|---|
| (布尔值) | true |
开/关总闸。开启后:出站每条 assistant(tool_calls) 都补非空 reasoning_content,取值优先级 ① 客户端 thinking 块原文 → ② 会话级缓存(键 = tool_call_id,上游返回时记录)→ ③ 占位串(无条件回填,不按"本轮是否以 tool 结尾"分支,保证同一历史消息跨请求逐字节一致、不制造前缀分叉);入站客户端请求了 thinking(body.thinking.type !== "disabled")时把上游 reasoning 还原为 thinking 块(thinking_delta + 合成 signature_delta)。未请求 thinking 的会话逐字节零变化 |
maxChars |
0(不截断) |
回填 reasoning 的字符上限:控制回填文本占用上下文窗口的体积(见下「上下文体积」)。截断确定性,不破坏前缀缓存 |
passthrough |
true |
false = 只做出站回填(修 400)、不下发 thinking 块。入库的 thinking 块只进客户端历史 |
placeholder |
"(reasoning omitted)" |
无真内容可回填时的兜底串(上游只要求非空)。配 "" = 关闭兜底(宁可 400 也不伪造) |
入站方向为什么不需要单独开关:客户端自己的
thinking设置已经决定要不要 thinking(CC 2.1.241 实测默认发{"type":"adaptive","display":"omitted"}),反代只负责“要就给”,无需再加一个维度。真实 CC 端到端验证(2.1.241,2026-09):CC 请求 thinking 时,反代下发的
thinking块(含合成签名cmc-…)被 CC 接受(无报错、exit 0),且 CC 会在下一轮原样回传该块(thinking_msgs_in_request=1)—— 回填因此优先拿到客户端原文,占位串只是最后兜底。
- 会话缓存:上游每次返回的 reasoning 都按响应里的
tool_call_id记入session.reasoning(FIFO 上限 128 条)。因此即使客户端不回传 thinking 块(未开 thinking / 历史被压缩改写 / 反代重启后旧的轮次),也能用真实思考文本回填,而不是占位串。 - 签名:真实
signature是 Anthropic 的不透明凭据(本链路上游非 Anthropic,不校验),用思考文本的 sha1 生成确定性签名(同文本同签名),不抽签、不破坏前缀缓存。 - Codex/Responses 链路:同样处理——
reasoning条目文本附着到紧随的function_call对应的 assistant 消息;出站同样回填(Codex 侧暂不下发 reasoning 条目)。 - 仅带
tool_calls的 assistant 轮次回填(纯文本轮次无此要求,不注入)。 - 哪些模型会强制:对目录里全部 70 个模型跑同一探针(
assistant(tool_calls)+ 尾 = tool 结果 + 不回传 reasoning),只有 2 个报该 400:deepseek/deepseek-v4-flash、deepseek/deepseek-v4.1-flash。同系的deepseek-v4-flash-vision-exp/deepseek-v4-flash-fast/deepseek-v4-pro以及z-ai/glm-5.3-flash、MiniMaxAI/*、Qwen/*、moonshotai/*、xai/grok-*、thinkingmachines/*等均不要求(尾部为user时更是完全不要求,任何模型都不查)。 - 兼容性:反向回归同样跑了一遍 —— 给这 42 个能正常响应的模型注入
reasoning_content,全部仍 200,无一因多出该字段而报错(它不是 OpenAI 标准字段,但本上游网关统一接受)。这正是"无条件回填"能安全落地的依据(见上文:按轮次条件性回填会制造pfx~N前缀分叉)。
上下文体积(本方案的真实代价)
会变大。 回填文本随历史进入每一轮请求,稳态下的净增量 ≈ 该会话内所有 tool-call 轮次的 reasoning tokens 之和。用实测数据(requests.jsonl 的 S3 会话,103 个请求):
| 指标 | 值 |
|---|---|
会话累计 rt(思考 tokens) |
51,781(均值 513 / 中位 90 / 最大 3881) |
| 末次请求输入 | in 53,882 + cr 19,712 = 73,594 tokens |
| 全量回填后输入 | ≈ 125,375 tokens(+70%) |
费用影响几乎可以忽略,因为增量落在缓存读一侧:
| 口径 | 单价 | 51,781 tokens 成本 |
|---|---|---|
缓存读(cr) |
$0.003/Mtok |
$0.00016 |
| 全价输入 | $0.15/Mtok |
$0.0078 |
增加的那段前缀是稳定的,除了"首次出现的那一轮"(当作新增 token 计一次全价),其余轮次都是缓存读(比全价输入便宜 50 倍)。所以这是上下文窗口占用问题,不是账单问题。
控制手段(按代价从低到高):
reasoningMaxChars: 500(建议起点)—— 每条回填截到 500 字符,保留思考要点、把增量压到一两个数量级以下;截断确定性 → 缓存不受影响。reasoningMaxChars: 20左右 —— 增量几乎归零,只保留"非空"语义。reasoningBridge: false—— 完全不回填,但工具循环会重新 400(仅在你不用 thinking 模型时考虑)。
副作用提醒:客户端不回传 thinking 块时(未开 thinking),回填依赖会话缓存;缓存是 FIFO 有界(128 条
tool_call_id)的,超过后最早的条目被逐出,那些旧消息的回填值会从真思考文本退化为占位串 → 该位置发生一次pfx~N、那一次请求全价重读一遍前缀。实际影响是一次的(逐出后永久稳定),长会话(>128 个带工具调用的轮次)才可能遇到。客户端回传 thinking 时(开了thinkingPassthrough且 CC 请求了 thinking)不受此影响——取值优先客户端原文。
配置项速查
| 键 | 默认 | 说明 |
|---|---|---|
port |
5411 |
监听端口(可用 --port 覆盖) |
host |
127.0.0.1 |
监听地址(可用 --host 覆盖) |
upstream |
https://api.commandcode.ai/provider |
上游地址(尾部 / 自动去除) |
apiKey |
— | 必填。GOAT API Key;环境变量 CMDC_API_KEY 优先 |
switchOnFail |
false |
轮换总闸:true / false / {text, image}(单布尔统一取值) |
failTTL |
30000 |
失败模型冷却毫秒数,0 = 不冷却 |
defaultModels |
["deepseek/deepseek-v4-flash"] |
文本请求轮换列表,第一个为文本默认模型(旧 defaultModel 视为单元素) |
defaultVisionModels |
[] |
带图请求轮换列表,第一个为视觉默认模型(旧 imageCapableModels 自动迁移);为空时带图请求回退 defaultModels[0] |
modelMap |
{} |
显式模型映射(key = 客户端请求名,value = 上游模型名),优先级最高,置空即关闭 |
resolveModel |
true |
modelMap 未命中时是否做目录解析 + 回退默认;false = 原样向上游请求 |
modelCatalog |
未配置(无默认) | 模型参数数据文件路径(相对 proxy.js 目录),仅显式配置时加载;存在且解析成功时用于计算单次请求的额度,文件缺失/解析失败静默跳过 |
blockedModels |
[] |
从 /v1/models 列表隐藏(避免客户端误选);转发时不拦截,命中只打印一次性告警 |
cleanHistoryImages |
false |
本轮无新图时把历史图片块替换为 [历史图片已清理](见「多模态」) |
reasoningBridge |
true |
reasoning/thinking 桥接总开关:true/false,或对象细调 {maxChars, passthrough, placeholder}(见「reasoning / thinking 桥接」) |
toolResultImages |
true |
tool_result 内嵌图片保留并注入后续 user 消息;false 折叠为 [image] |
visionAutoRoute |
true |
带图请求前置路由:模型判定不支持视觉则改走 defaultVisionModels[0](见「多模态」) |
jsonlLog |
关闭 | 结构化 JSONL 请求日志:true 写 requests.jsonl,字符串为自定义路径;false/缺省关闭(见「结构化请求日志 (JSONL)」) |
jsonlRotateDays |
30 |
JSONL 按日切分保留天数:当日写 requests.jsonl,跨日归档 requests-YYYY-MM-DD.jsonl,过期清理;0/false 关闭切分(可用 --jsonlRotateDays 覆盖) |
serializeSessionRequests |
true |
同会话上游请求串行化 |
firstByteTimeout |
120000 |
上游响应头超时 ms,0 关闭 |
stripSystemReminders |
true |
剥离 messages 里注入的 system 提醒 |
stabilizeCounters |
true |
<total_tokens> 计数就近取整到百万 |
cacheControlPassthrough |
true |
Anthropic cache_control → OpenAI content part |
cacheAffinity |
true |
按会话注入 user / prompt_cache_key |
配置模板见
config.example.json(键序与config.json对齐,参数前带_xxx说明注释键)。
环境变量
| 变量 | 默认 | 作用 |
|---|---|---|
CMDC_API_KEY |
未设置 | 优先于 config.json 的 apiKey |
CMC_LOGGING_FILE |
0 |
请求落盘分级(写入 fulllog.log,见「缓存优化」表):0/未设置 关闭;1 严重事件;2 1 + 前缀分叉请求;3 全部模型类请求 |
CMC_STATS_EVERY |
10 |
每 N 个请求打印一次 TOD/ALL 累计统计 |
CMC_DEBUG |
未设置 | 设为 1:把上游原始流逐 chunk 打到 stderr([DBG-UP-messages] / [DBG-UP-responses],每条截 300 字符) |
CMC_DEBUG_PAYLOAD |
未设置 | 设为 1:打印本地请求的完整 headers、bodyKeys 与 body 原文(超 8000B 截断) |
NO_COLOR |
未设置 | 设置后日志无色(输出非 TTY 时也自动无色) |
缓存优化(前缀缓存友好转换)
DeepSeek 等上游的前缀缓存要求同会话请求的消息前缀逐字节一致。反代在转换层做了以下保障,默认全部开启:
| 开关(config.json) | 默认 | 作用 |
|---|---|---|
stripSystemReminders |
true |
整条剥离 Claude Code 2.1.251+ 注入到 messages 里的 role:"system" 提醒(<total_tokens> 配额计数、任务工具催促)——数值随轮次回溯改写,是前缀缓存失效的元凶。仅提示性内容,不影响编码能力,CC 下一轮会重新注入;顶层 system(系统提示词 + CLAUDE.md)原样保留 |
stabilizeCounters |
true |
文本中 <total_tokens>N tokens left</total_tokens> 计数就近取整到 100 万(14977212 → 15000000),使回溯改写前后字节一致;作为剥离的兜底(防计数出现在其他位置) |
cacheControlPassthrough |
true |
Anthropic cache_control 标记透传为 OpenAI content part(对支持显式缓存的后端生效,已验证不影响本上游的缓存键) |
cacheAffinity |
true |
按会话注入稳定 user / prompt_cache_key(Claude Code 取会话 UUID,Codex 透传其自带值),便于上游按会话做缓存路由 |
CMC_LOGGING_FILE(环境变量) |
未设置 | 请求落盘分级:0/未设置 关闭;1 严重事件(上游失败/超时、客户端中途断开);2 1 + 前缀分叉时落盘该请求(client/upstream 双 body);3 全部模型类请求。写入 fulllog.log,分叉/失败条目带标注;日志会持续增长,注意清理 |
serializeSessionRequests |
true |
同会话上游请求串行发送:CC 的会话标题探测请求(4KB,自动起名)与主请求毫秒级并发到达时,中转侧出现过主请求长时间无响应头悬挂;串行化规避并发,排队中的请求若客户端断开会立即出队 |
firstByteTimeout |
120000 |
上游超过该毫秒数未返回响应头时主动中止并返回 502(0 关闭)。替代 undici 隐性的 300s 黑盒超时,悬挂请求 2 分钟内可见明确错误 |
配套的前缀分叉探测:RES 行在检测到与该会话上一次请求相比前缀发生变化时输出红色标记——pfx~N(第 N 条消息变化,索引含 system)、pfx~tools(工具定义变化)、pfx~params(顶层参数变化,附字段名)、pfx<N(历史变短,压缩);纯追加(健康)不输出。同时打印分叉内容预览(旧/新 JSON 各起一行,便于对齐比较;总预算 140 字符——分叉点在 100 字符内则头部截断 140(140 含尾部 …),分叉点在 100 之外则头 49 字符 + … + 自分叉点前 20 字符起的 90 字符定位子串(前 20 + 后 70,三段合计恰为 140);分叉起点至行尾以橙色高亮,其余红色),可直接看出客户端改写了什么。分叉基线按 tools 哈希分桶(最多 4 桶),主请求与并发小探测请求互不污染。前缀纯追加时同会话缓存应稳定在 95%+;若出现 pfx~ 标记即该位置之后本轮必然后缀缓存失效。
原理
Claude Code ──Anthropic /v1/messages───────▶ cmc-proxy:5411
Codex ──────Responses /v1/responses───────▶ │
OpenAI 客户端 ──chat /v1/chat/completions──▶ │ 模型决策 + 协议转换 + Key 注入 + 失败轮换
▼
https://api.commandcode.ai/provider/v1/*
| 端点 | 方法 | 行为 |
|---|---|---|
/、/health |
GET | 健康检查,返回 {ok, service, upstream, port} |
/v1/models |
GET | 上游模型列表(按 blockedModels 过滤,?raw=1 看全量),60s 缓存 |
/v1/messages/count_tokens |
POST | 本地估算 input_tokens(ceil(JSON长度/4),最小为 1),不转发上游 |
/v1/messages |
POST | 模型判定为 Claude 系(claude-*)→ 原样透传上游 /messages(预留 Pro/Provider 直连真实 Claude);否则 → Anthropic → OpenAI 转换后发上游 /chat/completions,响应转回 Anthropic(含 tool_use / input_json_delta 事件) |
/v1/chat/completions |
POST | 原样透传上游 /chat/completions,仅做模型决策与 Key 注入 |
/v1/responses |
POST | Responses → Chat Completions 转换后发上游,响应转回 Responses SSE 事件(response.created → output_text.delta / function_call_arguments.delta → response.completed),含工具调用 |
其他 /v1/* |
任意 | 通配原样透传(注入 Key,redirect: follow)。不参与模型决策、轮换与会话串行化 |
| 其他路径 | 任意 | 404 {error:{message:"Not found: …", type:"invalid_request_error"}} |
各阶段数据格式与样例见 schemas.md。
访问日志
每个请求打印两行日志:REQ 行记录本地客户端发来的请求,RES 行记录外部上游返回的结果,便于对照感知请求与返回。时间后跟 S会话#请求 标签(如 S1#10 = 1 号会话的第 10 个请求),按稳定会话标识区分本地 agent 进程并自增分配;REQ 与 RES 两行标签相同即同一请求的请求与响应。标签按请求轮换取色,同一请求两行同色以便配对,相邻请求颜色轮转更易区分;REQ 字样恒为青色:
[20:15:55.877] S3#1 REQ POST /v1/messages model=deepseek-v4-flash src=127.0.0.1:54321 ua=claude-cli/2.0.0 stream=1 body=186.5KB
[20:15:58.232] S3#1 200 POST /v1/messages model=deepseek/deepseek-v4-flash took=2.35s out=1736B in:1234 out:567 rt:480 cr:890 cw:0 ch:87% cost=$0.011571 credit=0.013500 ts:241.3/s
[20:15:58.822]@S3#2 REQ POST /v1/messages model=mimo-v2.5 src=127.0.0.1:54321 ua=claude-cli/2.0.0 stream=1 img=2(新1) body=321.4KB
[20:16:00.510]@S3#2 200 POST /v1/messages took=1.69s out=567B qwait:1.2s in:987 out:45 cr:0 cw:0 ch:40% ^cost=$0.026786 ^credit=0.031250 gap:10 ts:26.6/s
[20:16:30.000] S5#7 REQ* POST /v1/chat/completions model=gpt-5.6-sol src=127.0.0.1:48721 ua=codex/1.0.0 stream=0 body=88B
[20:17:30.000] S5#7 502 POST /v1/chat/completions took=60.01s out=112B pfx~3
字段说明:
| 字段 | 含义 |
|---|---|
REQ / RES |
本地请求到达 / 外部返回完成。REQ*(红色 *)表示该请求到达时同会话已有在途请求,正在串行队列中等待发送上游。行首时间戳默认暗灰,高峰时段改亮红(醒目,按该请求实际转发的模型是否在其 offPeak.windows 内判定,见 ^cost: / ^credit:) |
S会话#请求 |
标签 = 会话编号 + 请求编号(如 S1#10)。会话编号优先按 x-claude-code-session-id(Claude Code 每个会话唯一的 UUID)→ session-id(Codex 0.147+ 每请求携带,兼容旧版 session_id 下划线头名)→ thread-id(Codex 对话线程,codex resume 后不变)→ 回退 src:端口 + ua 近似区分,同一会话跨 TCP 重连不换号;请求编号为会话内自增的请求计数器,两行编号相同即同一请求的请求行与响应行,用于在并发/交错日志中配对 REQ 与 RES。仅 model 类请求(/v1/messages、/v1/chat/completions、/v1/responses)计入会话,非 model 请求(健康检查等)无标签。本轮带新图时标签前加 @(如 @S3#2,紧贴时间戳不空格) |
status / method / path |
HTTP 状态码、方法与路径(RES 行含状态码) |
src |
客户端 IP:源端口(仅 REQ 行;端口用于区分同 ua 的不同进程/连接) |
ua |
客户端 User-Agent(claude-cli/* 即 Claude Code,codex/* 即 Codex) |
model |
仅 REQ 行显示本地请求的模型名(如 deepseek-v4-flash);RES 行只在实际转发的模型名与本地名不同时显示转发名(如 deepseek/deepseek-v4-flash),字符串相同时省略 —— 两行对照即知映射关系;轮换发生时显示最终生效的模型。按模型名字符串哈希着色:同模型恒同色、不同模型尽量异色,扫日志时可按颜色快速归类模型 |
stream |
是否为流式请求(1 流式 / 0 非流式,仅 REQ 行) |
img |
请求体中检测到的图片块数(Anthropic image 块 + OpenAI image_url / input_image part),仅 REQ 行。/v1/messages 链路额外显示 新M —— 最后一条 user 消息(本轮)中的新图数,如 img=2(新1) |
body |
请求体大小(仅 REQ 行;用于区分两条请求是否完全相同:工具循环的请求体会递增,客户端重试的请求体一致) |
took |
上游耗时:从真正发往上游起算到响应完成(排队等待不计入,与 provider 侧 API 耗时对齐,仅 RES 行) |
qwait: |
排队等待时长:同会话串行化时在本请求之前等待的时间,超过 0.5s 才显示;took + qwait ≈ 总耗时 |
out |
响应输出字节数(仅 RES 行,含 res.end() 直写的 body) |
in: / out: |
输入 / 输出 tokens。in: 为净输入(已扣除缓存命中部分,即按原价计费的量;流式与非流式、转换与透传路径均解析;上游未返回时不显示) |
rt: |
思考 tokens(reasoning_tokens,DeepSeek 系常见,已包含在 out: 中,仅上游返回时出现) |
cr: / cw: |
缓存读取(cached_tokens) / 缓存写入(cache_creation_input_tokens) tokens,命中缓存可大幅省钱。in: + cr: = 总输入 |
^cost: / ^credit: / cost: / credit: |
单次请求成本与额度消耗,位于 ch: 之后、cost: 在前。cost:(橙色)为美元成本(按模型目录 priceUsdPerMTok 牌价直接计算,6 位小数);credit:(黄色)为额度 = cost × plan.credits ÷ 模型 monthlyCredits(6 位小数)。两者不保持固定比例:不同模型 monthlyCredits 不同。仅当配置 modelCatalog 且模型已收录、本次解析到 usage 时输出。^cost: / ^credit:(前缀带 ^)表示当前 UTC 时刻处于该模型 offPeak.windows 高峰窗口(已按 peakUsdPerMTok 覆盖 input/output/cacheRead 牌价;高峰仅工作日生效——周一~周五按 UTC 计,周末整天按错峰价)。同一判定也作用于 REQ/RES 行首时间戳:高峰时该行时间戳由暗灰改亮红,可扫一眼日志行首即知该请求是否按高峰价计费 |
gap: |
低缓存间隔:仅当本次缓存命中率 cr ÷ (in + cr) < 50% 时输出,值为本次与该会话最近一次低缓存命中请求的请求序号差(会话内序号只对有 usage 的请求递增)。会话内首次低缓存只记录基准、不输出 gap |
pfx~ |
前缀分叉标记(红色,详见「缓存优化」章节):pfx~N 该会话上一次请求的第 N 条消息与本次不同(索引含 system,pfx~1 即首条 user 消息)、pfx~tools 工具定义变化、pfx~params 顶层参数变化(附字段名)、pfx<N 历史变短(压缩)。分叉处之后本轮必然无法命中前缀缓存;纯追加(健康)不输出,同时会单独打印多行分叉内容预览(分叉段橙色高亮) |
ch: / ts: |
会话累计缓存命中率与滚动生成速度(见「用量统计」),仅在本次解析到 usage 时追加 |
ABT |
客户端中途断开时单独打印的告警行:[cmc-proxy]S3#1 ABT POST /v1/messages 客户端断开 (已等待 12.3s, 未收到完整响应)。断开会联动中止上游请求,同时按 CMC_LOGGING_FILE>=1 落盘 |
流式请求上游默认不返回 usage,需请求体带
stream_options: {"include_usage": true}—— Claude Code / Codex 转换路径已自动带上;直接调用/v1/chat/completions的客户端需自行加该参数才能在日志中看到用量。
用量统计
1. 滚动统计(每次请求输出)
每次请求解析到 usage(即 RES 行输出了 in:/out:/rt:/cr:/cw:)时,在行末追加 ch(会话累计缓存命中率)+ ts(最近 1 / 10 / 50 次请求的滚动速度)。无 usage 的请求(如健康检查、未返回用量的透传)不追加:
[20:16:30.000] S3#1 200 POST /v1/messages took=2.35s out=1736B in:1234 out:567 rt:480 cr:890 cw:0 ch:87% ts:33/s,40/s,50/s
ch:为会话累计命中率(该会话所有有 usage 请求的cr ÷ (in + cr),最多带 1 位小数,如ch:98.7%),单值输出——同会话正常工作时应稳定在 95%+;若长期偏低,结合pfx~标记定位前缀分叉来源ts:值个数随历史请求数变化:1 次显示 1 值 → 2–10 次显示 2 值 → ≥11 次显示 3 值(第 1 个 = 最近 1 次,第 2 个 = 最近 10 次,第 3 个 = 最近 50 次,仅计入有 usage 的请求)gap:出现在ch之前:仅当本次缓存命中率 < 50% 时输出,值为与同会话最近一次低缓存命中请求的序号差(首次低缓存只记录基准、不输出)
| 字段 | 含义 |
|---|---|
gap |
两次 cachehit < 50% 请求的序号差(会话内、仅计有 usage 的请求),如 gap:10 表示距上一次低缓存命中间隔了 10 次有效请求。红色高亮,便于快速定位低缓存频率 |
ch |
会话累计缓存命中率 = cr ÷ (in + cr),波段色:<60 红 / 60–79 橙 / 80–89 黄 / 90–94 绿 / ≥95 亮绿 |
ts |
生成速度(输出 tokens/s,输出含思考量,滚动窗口),波段色:<20 红 / 20–39 橙 / 40–59 黄 / 60–79 绿 / ≥80 亮绿 |
波段色按逗号分段:逗号跟随其后的数值一起着色(如
ts:33/s、,40/s、,50/s各自独立着色)。
2. TOD / ALL 累计(每 10 个请求打印)
[20:16:30.000] STATS TOD req:25 in:56.3K out:12.4K rt:9.1K cr:98.7K cw:0 ch:63% cost:$7.220000 credit:8.420000 avg:0.336800 ts:210.1/s
[20:16:30.000] STATS ALL req:25 in:56.3K out:12.4K rt:9.1K cr:98.7K cw:0 ch:63% cost:$7.220000 credit:8.420000 avg:0.336800 ts:210.1/s
| 行 | 范围 | 说明 |
|---|---|---|
TOD |
当天累计(按自然日) | 跨天自动先打印上日汇总 |
ALL |
进程启动以来累计 | 当天启动时与 TOD 一致,自动省略 |
in:/out:/rt:/cr:/cw:含义与 RES 行相同;数字超 1K/1M 自动缩写cost:累计成本(USD,橙色,6 位小数),credit:累计额度消耗(黄色,6 位小数),avg:单次请求平均额度 =credit ÷ req(6 位小数);顺序为cost:在前、credit:次之,均位于ch:之后(与 RES 行一致)- 额度计算口径:成本 = 按模型目录
priceUsdPerMTok牌价直接算;额度 = 成本 ×plan.credits÷ 模型monthlyCredits。未配置modelCatalog/ 模型未收录 /monthlyCredits缺失时不累计(无该字段) - 打印频率可用环境变量
CMC_STATS_EVERY调整(默认 10) - 统计为内存态,进程重启后清零
结构化请求日志 (JSONL)
独立于 CMC_LOGGING_FILE 分级落盘的结构化请求日志:把与终端 REQ/RES 两行同源的当前次请求数据,在 RES 输出时组织成一条 JSON 追加到 JSONL(JSON Lines)文件,便于后续用 jq/脚本按会话、模型、缓存命中、成本做离线分析。每条记录除当次请求信息外,附带 RES 行尾展示的滚动派生值快照(res.ch 会话累计命中率、res.ts 窗口 1 速度),省得离线重算;TOD/ALL 累计不入档。
- 开关:
config.json的jsonlLog(默认关闭)。true写入requests.jsonl(proxy.js 目录);值为字符串则视为路径(相对 proxy.js 目录,自动建目录),如"log/requests.jsonl"。未配置/false不生成文件 - 按日切分:
jsonlRotateDays(默认30,config.json或 CLI--jsonlRotateDays)——当日记录写热文件requests.jsonl;检测到跨日(按本地时区)时把热文件整体改名为requests-YYYY-MM-DD.jsonl归档并开新热文件,热文件名恒定、只含当日。归档保留 N 天,启动/跨日时自动清理过期归档(仅删本机制命名的requests-*.jsonl)。0/false关闭切分,保持原单一无限追加、不清理。自定义jsonlLog为非requests.jsonl文件名时不做切分(视为精确文件) - 触发:仅 model 类请求(
/v1/messages、/v1/chat/completions、/v1/responses,即带S会话#请求标签的请求)在响应完成(RES 输出)时写一条;健康检查、/v1/models、通配透传不写;客户端中途断开(ABT)不触发响应完成,不写 - 格式:每行一个 JSON 对象(UTF-8),追加式写入,写入顺序 = 完成顺序;
*.jsonl已加入.gitignore。切分后历史日文件(requests-YYYY-MM-DD.jsonl)仍可被vislog下拉选中离线分析
记录字段(与终端行的对应关系):
| JSON 字段 | 含义(对照终端) |
|---|---|
ts |
响应完成时间,UTC ISO(toISOString()),跨时区分析用 |
session.id / session.req |
标签 S会话#请求(如 S1#10) |
session.key / keyType |
底层稳定会话标识与其类型(cc/cx/cx-thread/src),跨重启可聚合同一 agent 会话 |
session.cc/cxSession/thread |
对应请求头 x-claude-code-session-id / session-id / thread-id |
http.status/method/path |
RES 行状态码 / 方法 / 路径 |
http.src/srcPort/ua |
REQ 行 src= 的 IP / 源端口 / User-Agent |
req.model |
REQ 行 model= —— 客户端请求的模型 |
req.stream |
REQ 行 stream=(1 流式 / 0 非流式 / null 未知) |
req.img / req.imgNew |
REQ 行 img=N(新M);imgNew 仅 /v1/messages 链路计算,其余为 null |
req.bodyBytes |
REQ 行 body=(字节数) |
req.queued |
REQ 行 REQ* —— 到达时同会话已有在途请求,串行队列等待 |
req.dispatchAt |
真正发往上游的时刻(UTC ISO;排队等待不计入 ms) |
res.model / modelChanged |
RES 行 model= —— 实际转发模型(轮换后为最终生效);是否与请求模型不同 |
res.peak |
高峰窗口(工作日 UTC):终端 ^cost/^credit/时间戳亮红同口径的布尔 |
res.outBytes |
RES 行 out=(响应字节数) |
res.ms / qwaitMs |
RES 行 took= / qwait:(ms;took + qwait ≈ 总耗时) |
res.usage |
RES 行 in:/out:/rt:/cr:/cw:;本次未解析到 usage 为 null(同 RES 行不显示) |
res.cost / credit |
RES 行 cost= / credit=(0 / 未收录模型为 0) |
res.ch |
会话累计缓存命中率 %(同 RES 行尾 ch:,会话累计口径);本次无 usage 为 null |
res.ts |
最近 1 次生成速度 tokens/s(同 RES 行尾 ts: 窗口 1);本次无 usage 为 null |
res.lowCache / gap |
RES 行 gap: —— 本次缓存命中率 <50% 时为 true 并给出与上次低缓存的序号差 |
res.pfx |
RES 行 pfx~N/pfx~tools/pfx~params/pfx<N(纯追加健康时为 null) |
示例记录:
{
"ts": "2026-09-06T06:20:15.622Z",
"event": "request",
"session": {
"id": 3,
"req": 10,
"key": "cc:cc-4f8a...",
"keyType": "cc",
"cc": "cc-4f8a...",
"cxSession": null,
"thread": null
},
"http": {
"status": 200,
"method": "POST",
"path": "/v1/messages",
"src": "127.0.0.1",
"srcPort": 54321,
"ua": "claude-cli/2.0.0"
},
"req": {
"model": "deepseek-v4-flash",
"stream": 1,
"img": 0,
"imgNew": null,
"bodyBytes": 190964,
"queued": false,
"dispatchAt": "2026-09-06T06:20:13.200Z"
},
"res": {
"model": "deepseek/deepseek-v4-flash",
"modelChanged": true,
"peak": false,
"outBytes": 1736,
"ms": 2350,
"qwaitMs": 0,
"usage": { "in": 1234, "out": 567, "rt": 480, "cr": 890, "cw": 0 },
"cost": 0.011571,
"credit": 0.0135,
"lowCache": false,
"gap": null,
"pfx": null
}
}
常见问题
端口被占用
Get-NetTCPConnection -LocalPort 5411 -State Listen | Stop-Process
或改 config.json 的 port。
请求报 MODEL_NOT_IN_PLAN — 该模型 GOAT 订阅不可用(403)。此错误会进入轮换(开启 switchOnFail 时当场换下一个模型)并让该模型冷却 failTTL;也可以手动调整 defaultModels / defaultVisionModels 或把该模型从 blockedModels 里排除。
带图请求拿到空/瞎编回答或 400 — 上游纯文本模型(如 deepseek-v4-flash)对图片的行为是 400 拒绝 或 200 静默盲视(实测两种都有,不同供应商/网关形态不一)。默认开启 visionAutoRoute:带图请求决策出的模型若判定不支持视觉,会前置改走 defaultVisionModels[0],不再把图片发给它。visionAutoRoute: false 时退回"先发原模型 + switchOnFail.image 轮换"的旧行为。历史图片会把会话一直钉在视觉模型上——先剥后送可用 cleanHistoryImages: true。
想用真正的 Claude 模型 — 需要升级 Pro/Provider 计划;升级后在 modelMap 中把 claude-* 映射为真实 Claude 模型名(如 claude-sonnet-4-6)即可直连上游 /messages(isClaudeModel() 判定)。
一条回复被拆成两截(同一消息出现两个圆点) — Claude Code 里 assistant 回复显示为「头 1-3 个字符 + 剩余全文」两段、各带一个圆点,拼起来正好是完整句子。原因是 StreamConverter 文本块路径用 !textBlock 判断「文本块是否已建」,而首个内容 chunk 建立的文本块索引是 0(合法值)——!0 为真导致第 2 个内容 chunk 误建第二个文本块,把一条消息拆成两个 text content block(工具块路径用的是 === undefined,无此问题)。判断块是否已建必须用 == null(配合 null 哨兵)而非 !x。此 bug 已随新核心修复;若仍复现,确认运行的是修复后的 proxy.js / release/proxy.js 并已重启进程。
流式输出卡住 — 检查网络到 api.commandcode.ai 的连通性;可设 CMC_DEBUG=1 启动查看上游原始流([DBG-UP-*]),或调小 firstByteTimeout 让悬挂请求更快失败。
缓存命中率低 — 看 RES 行的 pfx~ 标记与分叉预览:pfx~1/pfx~2 等早期消息分叉说明客户端在改写历史(升级 Claude Code 版本后常见);pfx~tools 工具定义变化;无标记但 cr 仍低则多为上游/中转侧行为。可设 CMC_LOGGING_FILE=2 落盘分叉请求对照排查(3 为全部请求);相关开关见「缓存优化」章节。
安全提醒
config.json 中的 apiKey 是你的订阅凭据,不要提交到公开仓库。建议:
config.json
proxy.log
fulllog.log
服务默认只监听 127.0.0.1,如需局域网共享请改 config.json 的 host 为 0.0.0.0 并自行加鉴权。
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found