客户端接入指南
把 workbuddy2api-panel 网关接进各种客户端 / SDK / Agent 框架的实操文档。
上游 CodeBuddy 的接口均为非公开接口,本文只描述本网关暴露给客户端的接口,以及各客户端的具体配置方法。
目录
- 0. 一句话总览
- 1. 支持的协议范围(先看清边界)
- 2. 快速自检
- 3. 模型选择
- 4. 支持的请求参数(实测)
- 5. Agent 场景:工具调用
- 6. 会话粘性
- 7. 各客户端配置
- 8. 错误处理
- 9. 响应里的 usage 字段
- 10. 运行日志与排错
- 11. 两个接入地址
- 12. 常见问题
- 13. 安全提醒
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/completions | POST | Bearer | 唯一的对话端点,流式 / 非流式 |
/v1/models | GET | Bearer | 模型列表(含上下文长度、输出上限、思考档位、倍率) |
/status | GET | Bearer | 账号池状态(账号数 / 冷却 / 积分 / 粘性会话 / 模型锁) |
/healthz | GET | 无 | 健康检查,healthy>0 返回 200,否则 503 |
/panel/ | GET | Bearer | Web 管理面板 |
不支持的端点(实测返回 404):
| 端点 | 说明 |
|---|---|
/v1/messages | Anthropic 原生协议 → Claude Code 接不上 |
/v1/responses | OpenAI Responses API → Codex CLI 接不上 |
/v1/completions | 旧版补全 |
/v1/embeddings | 向量化 |
需要接 Claude Code / Codex,得在前面再叠一层协议转换(claude-code-router、one-api、litellm等),把/v1/messages或/v1/responses转成/v1/chat/completions。
鉴权细节(实测)
- 头格式:
Authorization: Bearer <api_key> Bearer大小写敏感——bearer xxx返回 401- 不接受裸 key、
api-key:(Azure 风格)、x-api-key:(Anthropic 风格)头,全部 401 /healthz恒无鉴权;其余端点api_key非空时强制校验
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:auto | 256K | 32000 | — | 默认模型,由上游自动路由 |
cn:deepseek-v4-flash | 1M | 50000 | x0.17 | 快,便宜,日常首选 |
cn:deepseek-v4.1-flash | 1M | 393216 | x0.11 | 更新版 flash |
cn:deepseek-v4-pro | 1M | 393216 | x0.51 | 旗舰,长输出 |
cn:glm-5.3 | 1M | 131072 | x0.79 | 强推理 |
cn:glm-5.3-flash | 1M | 131072 | x0.06 | 最便宜 |
cn:glm-5.2 | 1M | 131072 | x0.79 | 有夜间折扣 |
cn:glm-5.1 | 200K | 48000 | x0.79 | |
cn:glm-5v-turbo | 200K | 64000 | x0.71 | 视觉模型 |
cn:kimi-k3-1 | 1M | 1048576 | x1.62 | 超长输出,贵 |
cn:kimi-k2.8-preview | 1M | 64000 | x0.77 | |
cn:kimi-k2.7 / cn:kimi-k2.6 | 256K | 32000 | x0.57 / x0.52 | |
cn:minimax-m3 | 512K | 524288 | x0.25 | |
cn:space-bunny | 1M | 128000 | x0.08 | 五档思考 |
cn:hy3 | 192K | 64000 | x0.00 | 限时免费 |
cn:hy3-x | 192K | 64000 | x0.05 | |
cn:hy4-preview / cn:hy4-preview-f | 960K / 1M | 64000 | x0.29 |
模型 ID 带不带 cn: 前缀都能用(实测两者都返回 200),但建议带上——多账号池扩到国际版后,前缀用于区分区域(cn: / 全球版前缀)。
积分保底机制:config.json 里 pool.credit_floor = 100,当账号积分低于 100 时,高价模型会被自动跳过(比如 kimi-k3-1 一次调用可能打穿保底)。若某模型一直返回 no_healthy_account,多半是余额触底,去面板充值或等签到恢复。
思考档位
带 supports_reasoning 的模型支持 reasoning_effort:
| 模型 | 默认档 | 可选档 |
|---|---|---|
cn:space-bunny | max | low / medium / high / xhigh / max |
cn:deepseek-v4-* | high | low / high / xhigh(pro/flash 还有 max) |
cn:glm-5.3* | high | low / high / max |
cn:glm-5.2 | high | high / xhigh |
cn:kimi-* / minimax-m3 / glm-5.1 | medium | 单档或 medium |
cn:hy3* / cn:auto | high | low / 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):
metadata.conversationId(推荐)conversation_id(snake_case,也认)X-Conversation-ID头- 都没有时自动派生 —— 用
system + 首条 user 消息哈希(d-前缀)
所以:什么键都不传也能享受粘性,通用 OpenAI 客户端不用做任何事。
主动传(更稳):
{
"model": "cn:deepseek-v4-flash",
"messages": [...],
"metadata": {"conversationId": "my-session-001"}
}
| 配置项 | 默认值 | 说明 |
|---|---|---|
session_sticky.enabled | true | 开关 |
session_sticky.ttl | 30m | 内存 TTL |
session_sticky.gc_interval | 5m | GC 周期 |
Redis 模式(本项目已启用 upstash 指向本机 redis db2)下粘性绑定持久化 7 天,进程重启不丢——这是用 Redis 的主要收益之一。
查看当前粘性会话数:GET /status 的 sticky_sessions 字段。
7. 各客户端配置
7.1 Cline / Roo Code(VS Code 插件)
设置里选 "OpenAI Compatible":
| 字段 | 值 |
|---|---|
| Base URL | https://wb.cangnian.icu/v1 |
| API Key | 7f704d79...(你的 key) |
| Model ID | cn: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:
- API 地址:
https://wb.cangnian.icu/v1 - API 密钥:你的 key
- 再手动添加模型:点「模型」→「添加」,逐个填
cn:deepseek-v4-flash等(Cherry 不会自动拉取第三方模型列表)
7.4 NextChat / LobeChat / Open WebUI / ChatGPT-Next-Web
共同思路:自定义 OpenAI 接口地址,填 https://wb.cangnian.icu/v1 + key,然后手工添加模型 ID。
- Open WebUI:设置 → Connections → OpenAI → 填 Base URL(要带
/v1),保存后它会自动拉模型列表 - LobeChat:服务商选「OpenAI」,填入
OPENAI_PROXY_URL=https://wb.cangnian.icu/v1 - NextChat:设置 → 自定义接口,填地址与 key,
model里手动加
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 Code | POST /v1/messages(Anthropic 原生) | ❌ 404 |
| Codex CLI | POST /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 状态:
| 场景 | HTTP | error.code | 上游 code | 说明 |
|---|---|---|---|---|
| key 缺失 / 错误 | 401 | invalid_api_key | — | 检查 Authorization: Bearer 头 |
| 请求体畸形 JSON | 400 | bad_params | 11101 | 不罚账号,但会换号重试 |
messages 类型错(传字符串) | 400 | bad_params | 11101 | cannot unmarshal string into ... []chat.Message |
多模态块 type 不认识 | 400 | bad_params | 11101 | unsupported content type at index 0: xxx |
| 模型不存在 | 400 | model_unavailable | 11102 | gateway_hint 字段会说明是池限制还是上游拒绝 |
缺 model 字段 | 400 | model_unavailable | 11102 | 上游报 model [] service info not found |
⚠️ messages 为空 / 缺失 | 503 | no_healthy_account | 11133 | 易误判——看着像"没账号了",其实是参数问题 |
| 全部账号冷却 / 积分耗尽 | 503 | no_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_tokens | Anthropic 风格缓存字段(上游返回时透传) |
缓存命中对成本影响很大——这也是会话粘性(§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" |
src=是客户端 IP(取X-Forwarded-For首段 →X-Real-IP→ TCP 对端)ua=是客户端 User-Agent(截断 200 字节)- 可以按 IP / UA / 模型 / 账号 / 请求 ID 筛选
想知道"到底是谁在接",看 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. 安全提醒
- api_key 等同账号额度,泄露等于额度被白用。公网部署务必配 HTTPS(已配好)+ 定期换 key。
- 面板与
/v1/*共用同一 key,浏览器 localStorage 会记住——公用机器上记得清。 auths/*.json里存的是明文 accessToken / refreshToken,服务器上该目录权限是700。不要把这个目录同步到任何地方。- 网关自身只提供明文 HTTP,必须置于 HTTPS 反代之后(本部署已如此)。