最近我做了一个小工具,叫cc-router。它解决的问题其实很具体:我想继续用ClaudeCode的体验,但后端模型和服务我想自己选。比如你已经有:OpenAI兼容网关OpenRoutervLLM公司内部代理或者你自己的模型服务这时候你会发现一件很现实的事:不是模型
最近我做了一个小工具,叫 cc-router。
它解决的问题其实很具体:
我想继续用 Claude Code 的体验,但后端模型和服务我想自己选。
比如你已经有:
这时候你会发现一件很现实的事:
不是模型不能用,而是接口对不上。
Claude Code 这一侧期待的是 Anthropic 风格接口, 但很多现成服务提供的是 OpenAI-compatible 接口。
于是我做了一个本地小 router,专门干这个事:
前面接 Claude Code,后面接 OpenAI 兼容接口,中间负责把请求和响应翻译一下。
这篇文章不讲套话,我就按真实做项目的思路,聊聊我是怎么想、怎么拆、怎么避坑的。
cc-router 到底是干嘛的?一句话解释:
它是一个本地协议转换代理。
Claude Code 发请求给它, 它再把请求转成 OpenAI Chat Completions 的格式发到上游; 等上游回来了,它再翻译回 Claude Code 能认的格式。
所以它做的不是简单“转发”,而是:
你可以把它理解成一个“翻译官”。
前面说 Anthropic 这套话,后面说 OpenAI-compatible 那套话,
cc-router 在中间负责把两边都哄明白。
原因其实不复杂。
我很喜欢 Claude Code 这种工作方式。
它不是单纯聊天,而是真的能进工程流:
这种体验一旦顺手了,真的不太想换。
但问题是,现实世界里的模型后端并不总是那么统一。
你可能会遇到这些情况:
这时候就很容易卡住。
因为你会发现:
工具体验和模型提供方,很多时候是强绑定的。
而我做 cc-router,本质上就是想把这件事拆开。
工具还是那个工具,后端归我自己决定。
这件事对工程师来说,其实挺有吸引力的。
一开始很多人会觉得,这类工具不就是一个反向代理吗?
真不是。
普通代理解决的是:
请求从 A 转发到 B。
但 cc-router 要解决的是:
A 和 B 说的根本不是一种协议,你得在中间翻译。
比如看起来都是“聊天接口”,但细节差异一点也不少:
system 的表达方式不同messages.content 组织方式不同所以这个项目最核心的地方,从来都不是 HTTP 转发。
而是:
协议映射。
这个认识非常重要。
因为一旦你把它当“协议翻译器”来设计,很多决策都会自然清晰:
这种项目特别容易自我膨胀。
最常见的心路历程是这样的:
“既然都做 router 了,那顺手把 tool use 做了吧。” “多模态以后肯定也要支持,不如现在先设计进去。” “JSON mode 和 structured outputs 一起做是不是更完整?” “provider-specific 兼容也先预留吧。”
结果就是:
第一版还没跑通,你已经先把自己设计崩了。
所以我这次刻意控制范围,只做最小闭环。
先完成下面这件事:
/v1/messages你别小看这九步。
只要这条链路跑通,cc-router 就已经是个能工作的东西了。
很多项目最后做不成,不是技术上做不到,而是第一版想做太多,把问题空间一下子放大了。
所以如果你也想自己做一个类似的工具,我非常建议:
先做最短可用链路,不要一上来做全量兼容。
这种中间层项目,如果目录乱了,后面真的会很痛苦。
我比较喜欢的结构大概是这样:
src/
├── main.rs
├── config.rs
├── server.rs
├── error.rs
├── routes/
│ ├── mod.rs
│ ├── health.rs
│ └── messages.rs
├── upstream/
│ ├── mod.rs
│ └── openai_compat.rs
└── types/
├── mod.rs
├── anthropic.rs
└── openai.rs
这个结构不花哨,但够用,而且清楚。
config.rs就专门管配置。
这种工具很多问题都不是业务 bug,而是配置错了。
所以像这些东西,都适合集中处理:
这层单独拆出来,后面排查问题会轻松很多。
routes/messages.rs负责接 Anthropic 风格请求。
注意,这层最好不要写成一个巨长 handler, 把“接请求、验参数、调上游、拼 SSE、处理错误”全堆在一起。
那样第一版写起来也许快,后面看就会想骂自己。
upstream/openai_compat.rs这一层就是“上游适配器”。
专门负责:
以后你想再接别的后端,基本也是从这里扩。
types/anthropic.rs 和 types/openai.rs这层我很建议认真建。
很多人做这种项目,图快,全程 serde_json::Value 起飞。
短期很爽,长期很痛苦。
协议转换最怕的是“我以为这个字段一定有”。 有了明确类型,很多问题会更早暴露出来,也更容易做稳定映射。
我的经验是:
这类项目里,类型不是累赘,是安全带。
如果只让我选一个“必须从第一天就考虑清楚”的点,我会选 模型映射。
因为 Claude Code 发来的模型名,通常是这种:
claude-4-6-sonnetclaude-4-6-opus但你后端能识别的,往往是别的名字:
gpt-5.4kimi-k2.5glm-5qwen3.5-plus你不做映射,中间层根本就没法用。
最省心的方式,就是把映射做成配置,例如:
export CC_ROUTER_MODEL_MAP='{
"claude-4-6-sonnet": "gpt-5.4",
"claude-4-6-opus": "gpt-5.4"
}'
这样做的好处很直接:
为什么我不建议写死?
因为这种工具存在的意义,本来就是“解耦”。 如果模型映射还写死在代码里,那你每次切 provider 都要重新改、重新编译,体验就很别扭。
一句话:
部署环境会变的东西,尽量不要写死。
如果只做非流式,其实整体难度还比较正常。
流程基本就是:
这条路径虽然也要处理字段差异,但至少它是“完整响应 → 完整响应”,心智负担没那么高。
真正麻烦的地方,是 stream: true。
因为这时候上游不是一次性给你结果, 而是源源不断给你 delta。
而 Anthropic 侧期待的 SSE 事件,大概是这种节奏:
message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop问题在于,OpenAI-compatible 上游通常不是这套表达方式。
所以你不能简单把上游流原样转发回来。
你得在中间自己重组语义。
也就是说,你不是在“转字节流”,你是在“模拟一条 Anthropic 风格消息是怎么一点点生成出来的”。
这个事情最容易低估。
因为它看起来只是 SSE,实际上你在处理中间状态:
如果这些节奏没理顺,就算字最后都出来了,客户端体验也会怪。
所以我的建议很明确:
先把非流式走通,再做流式。
先支持纯文本 delta,再考虑复杂内容块。
别在 streaming 还没搞定的时候,就开始设计一个“未来支持万物”的超级事件状态机。
那很容易把自己做进去。
这个我感触特别深。
很多项目成功路径都写得挺顺,一旦出错,就开始甩原始异常、吐 provider 自己的报错、返回格式前后不一致。
技术上也不是不能跑,但用起来很差。
因为如果你对外提供的是 Anthropic-compatible 接口, 那成功时像 Anthropic,失败时也应该尽量像 Anthropic。
比如错误返回至少应该统一成类似:
{
"error": {
"type": "api_error",
"message": "upstream request failed: ..."
}
}
为什么这件事很重要?
因为中间层不是单纯通一下就完了。 它应该帮上层“收敛复杂度”。
如果上游报错长什么样、语义是什么、状态码怎么解释,全都原样甩给客户端,那这个 router 的价值就打折了。
我一直觉得,一个中间层项目是不是像样,不看它最顺的那条路,看它报错时有没有人话。
成功大家都会,失败时还能不能让人快速定位问题,才看得出有没有认真做。
这类工具很容易越做配置越多。
最后变成作者看着很灵活,用户看着很窒息。
我自己的偏好是:配置项够用就行,不要追求“看起来特别全”。
最基础的几项,其实已经够覆盖主要场景:
export CC_ROUTER_UPSTREAM_BASE_URL="https://api.openai.com"
export CC_ROUTER_UPSTREAM_API_KEY="sk-..."
export CC_ROUTER_UPSTREAM_MODEL="gpt-5.4"
export CC_ROUTER_LISTEN_ADDR="127.0.0.1:8787"
export CC_ROUTER_MODEL_MAP='{"claude-4-6-sonnet":"gpt-5.4"}'
然后 Claude Code 只要指向本地:
export ANTHROPIC_BASE_URL="http://127.0.0.1:8787"
claude
这时候整条链路一眼就懂:
cc-router 接请求cc-router 转给你指定的 OpenAI-compatible 上游用户根本不需要理解你内部怎么组织模块,也不需要学一套新的复杂抽象。
一个好用的基础工具,真的不是靠配置项数量取胜的。
而是:
第一次上手就能跑起来。
如果从零再来一遍,我依然会先盯住这三个点:
GET /health这个接口虽然简单,但非常值。
它的价值不在业务,而在排障。
很多时候问题根本不是协议转换,而是服务没起来、端口没监听、环境变量没读到。 有了 health check,你能更快判断问题落在哪一层。
POST /v1/messages 非流式这是最短主链路。
只要这条通了,说明整体架子基本立住了。
Anthropic 请求 → 转换 → 上游调用 → 响应映射 → 返回
这条链一旦走通,你心里会踏实很多。
POST /v1/messages 流式这是使用体验的关键。
没有流式,也不是不能用。 但只要你在命令行场景里用过一次,就会知道流式有多重要。
尤其是 Claude Code 这种交互工具, 用户对“有没有持续往外出字”是非常敏感的。
content 永远只是字符串这是最容易想当然的地方。
Anthropic 风格的 content 更像内容块数组;
OpenAI-compatible 这边,不同 provider 兼容得也不完全一样。
所以哪怕第一版只支持 text-only, 也最好在建模时承认一个事实:
content本质上是结构化内容,不是永远简单 string。
这不是过度设计,而是给后面留活路。
上游可能返回:
stoplengthcontent_filtertool_calls但 Anthropic 侧理解的是另一套语义,比如:
end_turnmax_tokens这东西最烦的地方在于: 错了不一定直接报错,但行为会变怪。
这种“表面上能跑,实际上语义不对”的问题,比直接 500 还难受。
我很建议从第一天就把日志打清楚,至少要能看到:
因为你迟早会遇到这种情况:
“看起来都对,但就是没回。”
这时候日志如果只有一句 request failed,基本等于白打。
说实话,这种工具不用 Rust 也能做。
Go 可以,Node.js 也可以。 如果只是验证 idea,脚本语言起手完全合理。
我最后用 Rust,不是为了“性能吹牛”,而是因为它确实适合这种场景:
这种项目的核心,不是业务逻辑有多复杂,而是协议边界很多。
Rust 会逼你把字段、状态和边界条件想清楚。 这对中间层项目是好事。
SSE、异步 HTTP、状态维护,这些东西写起来虽然没有脚本语言那么随手,但边界会更清楚。
本地工具做成单 binary,体验真的很好。
拿来就跑,不折腾运行时环境,这点在工具型项目里很加分。
当然,语言只是实现手段。 别把“选型”变成拖延的理由。
真正重要的还是那句老话:
先把最短可用路径做出来。
做完回头看,我觉得有几条原则特别值得保留。
不要上来就碰多模态、tool use、structured outputs。
先把聊天主链路做稳,这已经够难了。
不要在 handler 里直接把 Anthropic 请求手搓成 OpenAI 请求。
最好先转成一套内部统一结构,再分别适配上下游。 这样以后你想接第二种 provider,不至于全拆。
外部协议可以复杂,内部流转尽量保持清晰。
否则最后你不是做了一个 router, 而是做了一团谁都不想碰的胶水层。
这是特别容易掉进去的坑。
很多项目不是做不出来,而是提前抽象太多,把自己先绕晕了。
cc-router,我觉得做到这些就够发第一版了如果只是出第一版,我觉得做到下面这些就已经很不错:
GET /healthPOST /v1/messages而下面这些,我会明确放到后续版本:
不是不能做,而是没必要在第一版一起做。
基础设施项目最忌讳的,就是“看起来很全,但核心链路并不稳”。
表面上看,cc-router 只是一个小代理。
但从工程角度看,它真正做的事情其实是:
把工具体验和模型后端拆开。
一旦中间有了这样一层 router,你能做的事情就一下子多了很多:
我觉得这才是最有意思的地方。
不是“我又造了一个轮子”, 而是:
原来绑死的一条链路,被拆开了。
这种能力,对工程师来说通常比多一个 feature 更值钱。
如果你也想自己做一个 cc-router,我的建议就一句话:
别一上来把它当成一个大而全的 AI 网关。先把它当成一个只解决眼前问题的协议翻译器。
先把消息链路跑通, 先把流式做稳, 先把错误处理做好, 先让 Claude Code 真能接上你自己的后端。
剩下的能力,完全可以慢慢补。
因为大多数时候,真正难的不是写代码。
而是你能不能忍住,不在第一版里把自己做复杂。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!