折腾笔记

折腾笔记

把 CodeBuddy 的订阅接成 OpenAI 兼容接口|9router 内置 OAuth 通道的部署与原理

2026-09-30

起因

手里有几个 AI 客户端的订阅,CodeBuddy 是其中一个。平时在它自家的 IDE 里写代码还行,但一旦想接到别的地方用 —— 比如让 DeepSeek Harness、Claude Code 或者 Cherry Studio 也能使上它背后的那些模型 —— 就有点费劲了:它只提供自家的协议,不是 OpenAI 兼容的那套。

之前有一阵子我是靠一个自己写的转换器顶着的。原理很土:从本机已登录的客户端里把凭据抠出来,再拿这份凭据去请求上游,把结果翻译成 OpenAI 格式吐出来。能用,但一直不太稳。

这次索性换了个思路重做了一遍,结果比原来干净得多。记录一下过程和原理,照这个思路,别人的环境应该也能复现。

先说结论:现在长什么样

现在的架构是「一个本地网关 + 一堆只认这个网关的客户端」:

整体链路

  • 客户端(DeepSeek Harness、Claude Code、Cursor、Cherry Studio,任何支持 OpenAI 协议的都行)统统只连本地一个地址:http://127.0.0.1:20128/v1
  • 中间跑的是 9router,一个现成的开源项目,用 Docker 起一个容器就行
  • 9router 里配一个「CodeBuddy CN」的 provider,登录走 OAuth,登录态由它自己保管和刷新
  • 真正的模型请求由 9router 带着 OAuth 凭据转发到 CodeBuddy 的官方网关

对外,9router 把 CodeBuddy 的模型统一暴露成带 cbcn/ 前缀的名字,比如 cbcn/hy3、cbcn/glm-5.2。客户端写 cbcn/ 开头的模型名,请求就会被路由过去。

为什么不再自己写转换器

这里是我踩得最深的一个坑,值得单独说。

我最早那套自建转换器,本质上是在「偷」客户端已经登录好的凭据。问题出在:OAuth 的 refresh token 是会轮换的 —— 上游在你刷新的时候会给你发一个新的 refresh token,同时把旧的作废。

于是只要有两套程序同时持有同一个账号的登录态,就会互相踢:A 刷新一下,B 手里那份就过期了,表现为 B 突然报 401;B 再重新登录,A 又坏了。来回横跳,而且很难一眼看出原因,因为单独测每个程序都是好的。

为什么放弃自建桥

换成只用 9router 之后,登录态就只有一份了,问题自然消失。顺带还少了:

  • 一个要自己维护和更新的容器
  • 一套要自己写的 token 刷新逻辑
  • 一堆上游接口变了就得跟着改的适配代码

这些活儿 9router 都替你干了,而且它是跟着上游升级的,不是我一个人对着变化的接口改。

动手前需要准备什么

  • 一台能跑 Docker 的机器(我是在 Mac 上跑的,Linux 一样)
  • 一个已经能正常用的 CodeBuddy 账号(有额度的那种)
  • 大概十分钟

不需要的东西:不需要把账号密码交出去,不需要额外的服务器,不需要域名和证书 —— 全部跑在本机 127.0.0.1 上。

四步部署

四步部署

第一步:起容器

关键就三个参数:端口、数据目录、重启策略。数据目录一定要挂出来,OAuth 登录态就存在里面,不挂的话容器一重建就得重新登录。

docker run -d --name 9router --restart unless-stopped \
  -p 20128:20128 \
  -v ./9router-data:/app/data \
  -e DATA_DIR=/app/data \
  -e REQUIRE_API_KEY=true \
  -e INITIAL_PASSWORD='自己设一个控制台密码' \
  -e BASE_URL=http://localhost:20128 \
  decolua/9router:latest

几个我踩过的点:

  • --restart unless-stopped 很重要。不然电脑重启之后服务就没了,而你第二天早上坐到桌前只会困惑「昨天还好好的,怎么客户端全连不上了」。
  • INITIAL_PASSWORD 建议一开始就设。不设的话控制台会提示你「默认密码必须修改」,而在容器视角下浏览器访问会被判定为远程访问,改密码那步会比较别扭。
  • REQUIRE_API_KEY=true 打开,这样 /v1 必须带 key 才能用。反正只监听本机,但多一层没坏处。

第二步:连上游账号

浏览器打开控制台:

http://127.0.0.1:20128/dashboard

进去之后找到 Providers,选择 CodeBuddy CN,点 Connect,会弹出一个页面让你完成登录。登录完回来,连接状态应该是正常的。

这一步之后,登录态就归容器管了。容器内部会自己保存 access token,并且在快过期的时候用 refresh token 自动续期。你之后不用再管它,也不用再登录第二次。

第三步:发一个本地 API Key

在控制台的 Endpoint / API Keys 里新建一个 key,形如 sk-xxxxxxxx。

这个 key 只对 9router 本身有效,跟上游没有任何关系 —— 换句话说,就算它泄露了,别人也只能通过你这台机器转发请求,拿不到你的上游凭据(当然本地服务开在公网上另说,别这么干)。

第四步:把客户端指过来

以 OpenAI 兼容的客户端为例:

配置项 值
base_url http://127.0.0.1:20128/v1
API Key 上一步生成的那个 sk-…
模型名 cbcn/ 开头的,如 cbcn/glm-5.2

模型名必须带 cbcn/ 前缀,这是 9router 用来判断「这个请求该转发给哪个 provider」的依据。写错了会直接告诉你找不到模型。

怎么确认它真的通了

别急着开客户端,先用三条命令确认链路完好:

验证部署

# 1. 容器活着吗
docker ps --filter name=9router

# 2. 服务健康吗
curl -s http://127.0.0.1:20128/api/health
# => {"ok":true}

# 3. 用本地 key 能不能列出模型
curl -s http://127.0.0.1:20128/v1/models \
  -H "Authorization: Bearer $KEY"

第三条返回的列表里,cbcn/ 前缀的那批就是你实际能用的 CodeBuddy 模型。

最后再实打实发一句话:

curl -s http://127.0.0.1:20128/v1/chat/completions \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"model":"cbcn/hy3","messages":[{"role":"user","content":"用一句话说明什么是反向代理"}]}'

我测的时候返回里带了 "credit": 0 —— 用的那个模型当时在免费档,确实不扣积分,跟官方标的费率对得上。这个字段挺有用的,可以用它来核对「它说免费是不是真的免费」。

顺手做的一件事:把模型费率同步下来

CodeBuddy 的模型列表和计费倍率是放在上游的目录接口里的,会变,而且不同模型的「消耗速度」差得挺远。与其手抄一遍,不如写个小脚本定期从接口拉:

  • 从 9router 的 /v1/models 拿「实际能被服务的模型」(cbcn/ 那批)
  • 从上游的目录接口拿每个模型的倍率和标签(是不是限时免费、有没有夜间折扣)
  • 两边一交集,生成一份表格,顺便把费率写进客户端的模型名里

倍率的含义是「消耗速度」:x0.79 表示按 0.79 倍速扣积分,x0.00 就是不扣。

模型倍率

拿它当参考,日常用法就很清楚了:

  • 随便聊两句、跑点不重要的活:用最便宜的那档
  • 正经干活:用中间档,性价比最好
  • 特别难的推理:再上贵的

但真正烧积分的其实不是模型贵不贵,而是会话长不长。 一个几十万 token 的长上下文请求,就算是便宜模型,乘下来也顶得上几十个短请求。所以想省积分,/compact 和及时开新会话比换模型有效得多。

接进 DeepSeek Harness

如果你也在用 DSH,它的好处是配置改完热加载,不用重启。

一份可参考的路由配置长这样(apiKey 走凭据引用,不写在配置里):

- id: llm-pi-ai
  name: "@deepseek-ai/dsh-llm-pi-ai"
  config:
    providers:
      codebuddy:
        displayName: CodeBuddy CN (9router)
        api: openai-completions
        baseURL: http://127.0.0.1:20128/v1
        apiKeyEnv: MY_ROUTER_API_KEY
        models:
          - id: cbcn/glm-5.2
            name: "GLM-5.2 · x0.79"
            contextWindow: 950000
            maxTokens: 64000
          - id: cbcn/hy3
            name: "Hy3 · 免费 x0.00"
            contextWindow: 182400
            maxTokens: 64000
          # …其余模型同理

密钥单独放在凭据文件里,引用名字对上就行:

# ~/.credentials.yaml
refs:
  MY_ROUTER_API_KEY: sk-你的本地key

改完新开一个会话,在模型选择器里就能看到这些模型了。

有一个小提醒:会话标题生成这类后台辅助调用也会走默认模型,如果你把默认模型设成了收费档,那这部分也会悄悄消耗积分。想省的话,把这种后台调用单独指到免费的模型上。

几个可能踩到的坑

模型列表里出现两批前缀。 除了 cbcn/,可能还有 cbai/ 开头的。那不是同一个后端 —— cbai 是 CodeBuddy 国际站,cbcn 才是国内的。用不上就在控制台把那条连接断开,列表会干净很多。

突然大面积 401。 大概率是上游登录态失效了。回到控制台 Providers 页面重新 Connect 一次即可。如果反复出现,检查一下是不是别的地方(比如另一个客户端、另一个脚本)也在用同一个账号的登录态 —— 就是前面说的「多套会话互相踢」问题。

报内容审核类的错误。 这是上游的合规拦截,换措辞或者换模型就行,反复重试和折腾账号都没用。

容器重建后要重新登录。 十有八九是数据目录没挂对。检查 -v 那一行,以及容器里是否真的写进了 /app/data。挂对了的话,docker rm -f 再重新 run,登录态是还在的。

几个要说清楚的注意事项

  1. 花的是你自己账号的积分。 余额在控制台能看。别拿它去跑什么奇怪的大批量任务,然后把额度跑光了来问怎么少了一半。

  2. 不要给同一个账号接第二套 OAuth 会话。 这是本文反复强调的一点,因为它真的会导致随机 401,而且排查起来很费时间。

  3. 本地 API Key 不要外传。 虽然它不能直接换到你的上游凭据,但它能白嫖你的额度。

  4. 这件事的性质自己心里要有数。 把桌面端/订阅客户端的登录态拿给第三方客户端用,属于上游服务条款里没说清楚的地带,存在被限流或者风控的可能。适合自己小范围折腾,不适合对外提供服务或者转售。

小结

整个方案的核心其实就一句话:把登录态的持有权收敛到唯一一个程序里。

明白了这一点,很多现象就都解释得通了 —— 为什么以前会随机 401、为什么数据目录必须挂出来、为什么本地 key 和上游凭据是两回事、为什么换了客户端甚至换了机器都不用重新登录。

剩下的事情,交给一个现成的网关去做就好了,没必要自己写。


这篇里的路径、端口都是默认值,实际用的时候按自己环境改。模型列表和费率会变,以控制台为准。