客户端接入指南

把 workbuddy2api-panel 网关接进各种客户端 / SDK / Agent 框架的实操文档。

上游 CodeBuddy 的接口均为非公开接口,本文只描述本网关暴露给客户端的接口,以及各客户端的具体配置方法。

目录


0. 一句话总览

网关对外暴露 OpenAI 兼容的 Chat Completions 协议。任何支持「自定义 OpenAI base_url」的客户端都能接入,配置三件套:

base_url = https://wb.cangnian.icu/v1
api_key  = <你的 api_key>
model    = cn:deepseek-v4-flash     # 或下方模型表里任意 id
本部署的 api_key:7f704d79b3df61ecd948da826fd89095bc299f09afbe681b
面板里可改(「配置 → 服务 → API 密钥」),改完热生效无需重启。

1. 支持的协议范围(先看清边界)

网关是同协议中转站,不是协议转换器——因为上游 copilot.tencent.com 本身就说 OpenAI Chat Completions。

端点方法鉴权说明
/v1/chat/completionsPOSTBearer唯一的对话端点,流式 / 非流式
/v1/modelsGETBearer模型列表(含上下文长度、输出上限、思考档位、倍率)
/statusGETBearer账号池状态(账号数 / 冷却 / 积分 / 粘性会话 / 模型锁)
/healthzGET无健康检查,healthy>0 返回 200,否则 503
/panel/GETBearerWeb 管理面板

不支持的端点(实测返回 404):

端点说明
/v1/messagesAnthropic 原生协议 → Claude Code 接不上
/v1/responsesOpenAI Responses API → Codex CLI 接不上
/v1/completions旧版补全
/v1/embeddings向量化
需要接 Claude Code / Codex,得在前面再叠一层协议转换(claude-code-router、one-api、litellm 等),把 /v1/messages 或 /v1/responses 转成 /v1/chat/completions。

鉴权细节(实测)


2. 快速自检

接任何客户端之前,先用 curl 确认链路通:

KEY="7f704d79b3df61ecd948da826fd89095bc299f09afbe681b"
BASE="https://wb.cangnian.icu/v1"

# 1) 模型列表
curl -s -H "Authorization: Bearer $KEY" $BASE/models | head -c 300

# 2) 非流式对话
curl -s -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"cn:deepseek-v4-flash","stream":false,
       "messages":[{"role":"user","content":"只回两个字:通了"}]}' \
  $BASE/chat/completions

# 3) 流式对话
curl -N -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"model":"cn:deepseek-v4-flash","stream":true,
       "messages":[{"role":"user","content":"数到三"}]}' \
  $BASE/chat/completions

三条都出内容,说明网关、账号池、上游都正常,剩下只是客户端配置问题。


3. 模型选择

调 GET /v1/models 拿实时列表。当前池内可用(19 个,前缀 cn:):

模型 ID上下文最大输出倍率说明
cn:auto256K32000—默认模型,由上游自动路由
cn:deepseek-v4-flash1M50000x0.17快,便宜,日常首选
cn:deepseek-v4.1-flash1M393216x0.11更新版 flash
cn:deepseek-v4-pro1M393216x0.51旗舰,长输出
cn:glm-5.31M131072x0.79强推理
cn:glm-5.3-flash1M131072x0.06最便宜
cn:glm-5.21M131072x0.79有夜间折扣
cn:glm-5.1200K48000x0.79
cn:glm-5v-turbo200K64000x0.71视觉模型
cn:kimi-k3-11M1048576x1.62超长输出,贵
cn:kimi-k2.8-preview1M64000x0.77
cn:kimi-k2.7 / cn:kimi-k2.6256K32000x0.57 / x0.52
cn:minimax-m3512K524288x0.25
cn:space-bunny1M128000x0.08五档思考
cn:hy3192K64000x0.00限时免费
cn:hy3-x192K64000x0.05
cn:hy4-preview / cn:hy4-preview-f960K / 1M64000x0.29

模型 ID 带不带 cn: 前缀都能用(实测两者都返回 200),但建议带上——多账号池扩到国际版后,前缀用于区分区域(cn: / 全球版前缀)。

积分保底机制:config.json 里 pool.credit_floor = 100,当账号积分低于 100 时,高价模型会被自动跳过(比如 kimi-k3-1 一次调用可能打穿保底)。若某模型一直返回 no_healthy_account,多半是余额触底,去面板充值或等签到恢复。

思考档位

带 supports_reasoning 的模型支持 reasoning_effort:

模型默认档可选档
cn:space-bunnymaxlow / medium / high / xhigh / max
cn:deepseek-v4-*highlow / high / xhigh(pro/flash 还有 max)
cn:glm-5.3*highlow / high / max
cn:glm-5.2highhigh / xhigh
cn:kimi-* / minimax-m3 / glm-5.1medium单档或 medium
cn:hy3* / cn:autohighlow / high

请求里加 "reasoning_effort": "low" 可提速降本。

关思考:传 "reasoning_effort": "off"(实测返回 200)。但不是所有模型都能关——只有 /v1/models 里 can_disable_thinking: true 的模型才真的关得掉:

可关思考的模型
cn:deepseek-v4-flash / cn:deepseek-v4-pro / cn:deepseek-v4.1-flash
cn:glm-5.3 / cn:glm-5.3-flash / cn:glm-5.2
cn:kimi-k2.8-preview

其余模型的 can_disable_thinking 为空(即 false),传 off 也不会生效。

另外注意 only_reasoning: true 的模型(cn:auto、cn:hy3、cn:hy3-x、cn:hy4-preview*、cn:kimi-k3-1、cn:minimax-m3、cn:space-bunny、cn:glm-5v-turbo、cn:glm-5.1、cn:kimi-k2.7/k2.6 等)是纯推理模型,无论如何都会思考。

响应里的思维链在 message.reasoning_content(非流式)或 delta.reasoning_content(流式),与正文分离,客户端不认这个字段可直接忽略。


4. 支持的请求参数(实测)

参数支持备注
model / messages / stream✅必填
max_tokens✅显式优先
max_completion_tokens✅别名,网关自动翻译成 max_tokens(新客户端只发这个,不会丢)
temperature / top_p✅
stop / n✅
frequency_penalty / presence_penalty✅
seed✅
user✅
logprobs✅
response_format✅{"type":"json_object"} 实测通过
tools / tool_choice✅完整支持,见 §5
reasoning_effort✅见上
stream_options.include_usage✅流式末帧带 usage
多模态 image_url✅需用视觉模型(cn:glm-5v-turbo),支持 base64 data URL
developer 角色✅自动归一到 system
conversation_id / metadata.conversationId✅会话粘性键,见 §6

请求体无大小上限(client_max_body_size 0,网关侧也不设限)——多图长上下文不会撞 413,实测 1.5 MB 请求体返回 200。

README:690 写的「请求体上限 8 MiB」是过时表述。代码事实:server.max_body_mb 配置项已被移除(cmd/server/config.go 无该字段),internal/server/handler.go:513-521 注释明写「请求体无大小上限(max_body_mb 已移除,对齐上游)」,走 io.ReadAll(r.Body) 完整读入。

5. Agent 场景:工具调用

支持完整的 OpenAI function calling,实测:

curl -s -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{
  "model": "cn:deepseek-v4-flash",
  "stream": false,
  "messages": [{"role":"user","content":"北京天气怎么样?用工具查"}],
  "tools": [{
    "type": "function",
    "function": {
      "name": "get_weather",
      "description": "查询天气",
      "parameters": {"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}
    }
  }],
  "tool_choice": "auto"
}' $BASE/chat/completions

返回:

{
  "choices": [{
    "finish_reason": "tool_calls",
    "message": {
      "content": "",
      "tool_calls": [{
        "id": "call_00_3WyjLpBR3rGA7PkxSgHj0481",
        "index": 0,
        "type": "function",
        "function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}
      }]
    }
  }]
}

⚠️ 一个已知坑:tools 字段在中转链路里丢失

上游(DeepSeek 系)的工具调用原生语法是带全角竖线的标记文本,只有当请求里真的带上了 tools 字段时,上游才会把它解析成结构化的 tool_calls。

但在实际链路里,tools 声明丢失是常态(协议转换丢字段、一次性 exec 探测、auto-review 复核回合等)。一旦丢了,模型仍然想调工具,标记就以纯文本落进 content。

网关对此做了兜底:识别出 content 里的原生标记,还原成结构化 tool_calls,流式与非流式都覆盖。命中时一帧会拆成「正文帧 + N 个调用帧 + 收尾帧」,finish_reason: stop 改写为 tool_calls。

所以即使你的客户端没传 tools,仍可能收到 tool_calls——客户端要能处理这种"意外"的工具调用。反过来,如果你的客户端只认结构化 tool_calls 而不认正文里的标记,网关的还原逻辑正好救你一命(Codex 这类客户端就是靠这个才不把工具标记当正文写进历史)。


6. 会话粘性

为什么重要:网关背后是多账号池。如果同一个对话的请求被轮流分到不同账号,上游会看到"每一轮都是新对话",导致上下文丢失。会话粘性把同一个对话固定绑到一个账号上(顺带保住上游的 prompt cache)。

绑定键的优先级(internal/session/ids.go):

  1. metadata.conversationId(推荐)
  2. conversation_id(snake_case,也认)
  3. X-Conversation-ID 头
  4. 都没有时自动派生 —— 用 system + 首条 user 消息哈希(d- 前缀)

所以:什么键都不传也能享受粘性,通用 OpenAI 客户端不用做任何事。

主动传(更稳):

{
  "model": "cn:deepseek-v4-flash",
  "messages": [...],
  "metadata": {"conversationId": "my-session-001"}
}
配置项默认值说明
session_sticky.enabledtrue开关
session_sticky.ttl30m内存 TTL
session_sticky.gc_interval5mGC 周期

Redis 模式(本项目已启用 upstash 指向本机 redis db2)下粘性绑定持久化 7 天,进程重启不丢——这是用 Redis 的主要收益之一。

查看当前粘性会话数:GET /status 的 sticky_sessions 字段。


7. 各客户端配置

7.1 Cline / Roo Code(VS Code 插件)

设置里选 "OpenAI Compatible":

字段值
Base URLhttps://wb.cangnian.icu/v1
API Key7f704d79...(你的 key)
Model IDcn:deepseek-v4-flash
上下文长度手动填 1000000(或按模型表填)
支持图片视觉模型才勾(cn:glm-5v-turbo)

Cline 会发 tools,走原生 function calling,无需额外配置。

7.2 Continue(VS Code / JetBrains)

~/.continue/config.json:

{
  "models": [
    {
      "title": "WorkBuddy DeepSeek",
      "provider": "openai",
      "model": "cn:deepseek-v4-flash",
      "apiBase": "https://wb.cangnian.icu/v1",
      "apiKey": "7f704d79b3df61ecd948da826fd89095bc299f09afbe681b",
      "contextLength": 1000000,
      "useLegacyCompletionsEndpoint": false
    }
  ]
}

useLegacyCompletionsEndpoint: false 很重要——设 true 会去打 /v1/completions(本网关不支持)。

7.3 Cherry Studio

设置 → 模型服务 → 添加提供商 → 类型选 OpenAI:

7.4 NextChat / LobeChat / Open WebUI / ChatGPT-Next-Web

共同思路:自定义 OpenAI 接口地址,填 https://wb.cangnian.icu/v1 + key,然后手工添加模型 ID。

7.5 Python(openai SDK)

from openai import OpenAI

client = OpenAI(
    base_url="https://wb.cangnian.icu/v1",
    api_key="7f704d79b3df61ecd948da826fd89095bc299f09afbe681b",
)

# 非流式
r = client.chat.completions.create(
    model="cn:deepseek-v4-flash",
    messages=[{"role": "user", "content": "你好"}],
)
print(r.choices[0].message.content)

# 流式
for chunk in client.chat.completions.create(
    model="cn:deepseek-v4-flash",
    messages=[{"role": "user", "content": "数到三"}],
    stream=True,
):
    d = chunk.choices[0].delta
    if getattr(d, "reasoning_content", None):
        print("[think]", d.reasoning_content, end="")
    if d.content:
        print(d.content, end="")

# 工具调用
tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "查询天气",
        "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
    },
}]
r = client.chat.completions.create(
    model="cn:deepseek-v4-flash",
    messages=[{"role": "user", "content": "北京天气"}],
    tools=tools,
)
print(r.choices[0].message.tool_calls)

reasoning_content 是非标字段,openai SDK 会落在 delta.model_extra 里(新版本可能直接属性访问),取不到也不影响。

7.6 Node.js

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://wb.cangnian.icu/v1",
  apiKey: "7f704d79b3df61ecd948da826fd89095bc299f09afbe681b",
});

const r = await client.chat.completions.create({
  model: "cn:deepseek-v4-flash",
  messages: [{ role: "user", content: "你好" }],
});
console.log(r.choices[0].message.content);

7.7 curl / 任意 HTTP 客户端

见 §2。注意:stream: true 时 nginx 侧已 proxy_buffering off、超时 3600s,长流不会被掐。

7.8 Claude Code / Codex CLI(不能直连)

这两个走的是非 Chat Completions 协议:

客户端它请求的端点本网关
Claude CodePOST /v1/messages(Anthropic 原生)❌ 404
Codex CLIPOST /v1/responses(OpenAI Responses API)❌ 404

接法:前面叠一层协议转换,以 one-api / new-api 为例:

Claude Code ──(Anthropic 协议)──> one-api ──(OpenAI 协议)──> wb.cangnian.icu/v1 ──> CodeBuddy

在 one-api 里新建渠道,类型选 OpenAI,Base URL 填 https://wb.cangnian.icu/v1,key 填你的 key;然后把 Claude Code 的 ANTHROPIC_BASE_URL 指向 one-api 的地址。

也可以用 claude-code-router(更轻,纯本地转发 + 协议转换)或 litellm。

8. 错误处理

网关返回的是标准 OpenAI 错误结构:

{
  "error": {
    "code": "invalid_api_key",
    "message": "missing or invalid API key",
    "type": "api_error"
  }
}

实测各错误的 code(error.code)与 HTTP 状态:

场景HTTPerror.code上游 code说明
key 缺失 / 错误401invalid_api_key—检查 Authorization: Bearer 头
请求体畸形 JSON400bad_params11101不罚账号,但会换号重试
messages 类型错(传字符串)400bad_params11101cannot unmarshal string into ... []chat.Message
多模态块 type 不认识400bad_params11101unsupported content type at index 0: xxx
模型不存在400model_unavailable11102gateway_hint 字段会说明是池限制还是上游拒绝
缺 model 字段400model_unavailable11102上游报 model [] service info not found
⚠️ messages 为空 / 缺失503no_healthy_account11133易误判——看着像"没账号了",其实是参数问题
全部账号冷却 / 积分耗尽503no_healthy_account—去面板看账号池状态
模型级限流429—6004该模型超限,换模型立即可用

注意 messages 为空返回 503 而不是 400 —— 这是上游把空 messages 归类成 11133(参数被模型提供方拒绝),网关透传了上游的分类。排错时看到 503 别急着怀疑账号池,先确认 messages 里至少有一条消息。

注意:error.message 里嵌的是上游原文 JSON 字符串(含上游自己的 requestId、extError、displayMsg 中英双语),便于排查。客户端只需展示 error.message 即可。

429 与账号池的关系:429 通常是模型级限流(code 6004),不是整个账号废掉——网关会记住触发模型,同一账号改用其他模型视为可用。


9. 响应里的 usage 字段

网关返回的 usage 比 OpenAI 标准多几个字段,标准字段也都在:

{
  "prompt_tokens": 31,
  "completion_tokens": 10,
  "total_tokens": 41,
  "completion_tokens_details": {
    "reasoning_tokens": 10,
    "cached_tokens": 0,
    "audio_tokens": 0
  },
  "prompt_tokens_details": { "cached_tokens": 0, "reasoning_tokens": 0 },
  "credit": 0,
  "completion_thinking_tokens": 10,
  "prompt_cache_hit_tokens": 0,
  "prompt_cache_miss_tokens": 31,
  "cache_creation_input_tokens": 0,
  "cache_read_input_tokens": 0,
  "cached_tokens": 0
}
字段含义
credit本次调用扣除的积分(非标字段)
completion_thinking_tokens思维链占用的 token 数
prompt_cache_hit_tokens / prompt_cache_miss_tokens上游 prompt cache 命中 / 未命中
cache_read_input_tokens / cache_creation_input_tokensAnthropic 风格缓存字段(上游返回时透传)

缓存命中对成本影响很大——这也是会话粘性(§6,固定账号)除了保上下文之外的第二个价值:同一账号才有机会命中上游 prompt cache。

客户端用 openai SDK 时,非标字段会落在 response.usage.model_extra(或直接被丢弃),不影响正常使用。


10. 运行日志与排错

面板 「运行日志 → 请求记录」 会逐条显示:

| #001 | 22:40:54 | cn:deepseek-v4-pro | sync | 200 | 蝉时雨(4da5ee6d) | 22.9tok/s | total=1.9s | src=127.0.0.1 ua="curl/7.81.0" |

想知道"到底是谁在接",看 ua= 列即可。这个功能由 logging.request_client_info 控制(默认开启,可在面板关)。

curl 自检时看不到真实客户端 IP——因为 src=127.0.0.1(网关本机)。经 nginx 反代过来的请求会显示真实客户端 IP(vhost 里已设 X-Real-IP / X-Forwarded-For)。


11. 三个入口地址

当前部署了两条并行反代,任选其一:

入口地址说明
子域(推荐)https://wb.cangnian.icu/v1独立 vhost + 独立 LE 证书,自动续签路径干净
主域路径https://cangnian.icu/v1挂在 memories 站点的 extension 上,不用新域名

两者功能等价,都指向 127.0.0.1:7863,都支持流式与长流(proxy_read_timeout 3600s)。

辅助页面

页面地址说明
网关着陆页https://wb.cangnian.icu/身份页 + 实时服务状态(调 /healthz 显示可用账号数)+ 三个入口卡片
本文档https://wb.cangnian.icu/docs/ 或 https://cangnian.icu/docs/本文件渲染版(自包含 HTML,明暗主题跟随系统,左侧目录)
管理面板https://wb.cangnian.icu/panel/账号池 / 用量 / 任务 / 配置 / 日志

这三个页面无需鉴权(静态文件 + 无敏感信息,/healthz 本身就无鉴权)。

从面板进文档:管理面板左侧栏底部「接入文档」、顶栏「接入文档」按钮,两处都直链 /docs/(新标签页打开)。所以进了面板就随时能翻这份文档。

服务状态自检:

curl -s https://wb.cangnian.icu/healthz
# {"healthy":1,"realm_servable":{"cn":true,"global":false},"service":"workbuddy2api","total":1}

healthy 是可用账号数;total 是账号总数。healthy == 0 时会返回 503(无账号可用)。


12. 常见问题

Q:浏览器打开 https://wb.cangnian.icu/v1 是 404,是不是坏了?

不是。/v1 是 base URL 不是网页——网关没有 /v1/ 的 HTML 页面,OpenAI 兼容网关(DeepSeek、OpenRouter 等)全都这样。

现在打开 https://wb.cangnian.icu/v1(不带斜杠)会 302 跳到着陆页 https://wb.cangnian.icu/,从那能看到面板与文档入口;/v1/(带斜杠)仍是 404,属正常。

Q:返回 no_healthy_account,但 /healthz 显示有账号?

账号可能因积分低于保底(100) 或该模型专属冷却被跳过。去面板「账号池」看色条与状态标签(可用 / 限流冷却 / 积分冷却 / 熔断 / 连败降权 / 已禁用),或看「模型锁池」表。

Q:为什么有时候 content 是空的,但 tool_calls 有值?

正常——模型决定调工具,不产出正文。

Q:为什么响应里有 reasoning_content?

上游 DeepSeek 系模型的思维链。它与正文分开,客户端不认可忽略;认的话能在界面上展示"思考过程"。

Q:能不能并发调?

能。但要注意:pool.max_in_flight = 3(单账号在途上限)、pool.max_in_flight_global = 2(全局上限)。高并发下会排队或触发换号,吞吐受账号数限制。

Q:stream: false 会慢吗?

会略慢——网关内部仍向上游发流式请求,然后在本地聚合成单个响应返回(这样能拿到 reasoning_content 与 usage)。对延迟敏感就用 stream: true。

Q:模型 ID 前面的 cn: 是什么?

区域前缀,表示走国内版 CodeBuddy(www.codebuddy.cn)。账号池扩到国际版后会出现其他前缀。


13. 安全提醒