客户端接入与网关差异
本章讲两件事:各客户端怎么连上网关;连上之后,网关的行为和直连上游有什么不同。后者只讲你会观察到什么、怎么应对,不讲背后实现。
客户端接入
通用规则:把客户端的 base_url 指向网关,API key 填你在网关签发的访问密钥,模型名填你在网关配的名字。
OpenAI SDK(Python / Node)
base_url = http://<host>:7890/v1
api_key = <你的访问密钥>curl:
curl http://localhost:7890/v1/chat/completions \
-H "Authorization: Bearer <你的访问密钥>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}]}'Embeddings / Images / Audio / Rerank 同理,完整路径见 端点清单。
Anthropic SDK
base_url = http://<host>:7890
api_key = <你的访问密钥> # 通过 x-api-key 头发送curl:
curl http://localhost:7890/v1/messages \
-H "x-api-key: <你的访问密钥>" \
-H "anthropic-version: 2023-06-01" \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet","max_tokens":1024,"messages":[{"role":"user","content":"hi"}]}'Anthropic SDK 默认把路径拼成
/v1/messages,所以 base_url 填到根(不带/v1)。
Google Gemini SDK
api_key = <你的访问密钥> # 通过 x-goog-api-key 头发送
base_url = http://<host>:7890curl:
curl "http://localhost:7890/v1beta/models/gemini-2.0-flash:generateContent" \
-H "x-goog-api-key: <你的访问密钥>" \
-H "Content-Type: application/json" \
-d '{"contents":[{"parts":[{"text":"hi"}]}]}'DashScope
两种接法:
- 透传:
POST /v1/services/{*rest},请求体原样发给 DashScope 上游。 - 翻译:
POST /v1/chat/completions,网关把 OpenAI 格式翻译成 DashScope 格式。
任意协议透传
当客户端协议与目标协议相同、你想完全绕过翻译时:
curl http://localhost:7890/v3/<模型名>/<任意后续路径> \
-H "Authorization: Bearer <你的访问密钥>" \
-d '{ ...原样请求体... }'/v3/{model}/{*rest} 把请求体原样转发到该模型配置的目标上游。
Cherry Studio 等第三方客户端
填法:
- API 地址:
http://<host>:7890/v1(OpenAI 兼容) - API Key:网关访问密钥
- 模型:填网关里配的模型名
部分客户端按 model id 推断是否支持 function calling。若工具调用不生效,检查模型名是否落在客户端的能力识别规则内,或在客户端侧手动开启。
网关与原生 API 的差异
直连上游 vs 经网关,调用方会观察到这些不同。
长时间推理时收到 :keep-alive 注释行
流式请求中,如果上游推理时间较长,网关会周期性发送 SSE 注释行 :keep-alive 保持连接活跃,防止中间代理因空闲超时断开。
- 这是 SSE 注释(以
:开头),不是数据事件,SSE 客户端标准实现会自动忽略,无需特殊处理。 - 如果你手写 SSE 解析,跳过以
:开头的行即可。
上游流异常时收到 _gateway_warning 字段
当上游流在传输中途异常中断时,网关不会直接报错打断你的对话,而是:
关闭尚未结束的内容块;
在 SSE 流里插入一个
_gateway_warning扩展字段,告诉你中断原因(含reason/detail/last_finish_reason/timestamp);发送正常的终止序列,让客户端能正常收尾。
_gateway_warning是网关的协议扩展字段(带下划线前缀)。解析流时如果看到它,说明这次响应中途出了问题,可据此记录或告警。设计目的是避免 Claude Code 这类客户端因流异常而中断整段对话。
错误码与重试
经网关时你会遇到这些状态码(完整速查见 错误码):
| 状态码 | 含义 | 你的处理 |
|---|---|---|
| 429 | 被限流(每密钥/每 IP/上游) | 看 Retry-After 头等待后重试;无该头则按响应体提示 |
| 503 | 服务过载/不可用/并发达上限 | 退避后重试 |
| 504 | 超时 | 退避后重试 |
| 502 | 上游故障 | 网关通常已自动故障转移到其他节点;持续 502 检查上游配置 |
所有 429 响应均为 JSON(
{"error":{...}});仅每 IP 限流不带Retry-After头。
上游故障时自动转移到其他节点
如果你的模型配了负载均衡(见 负载均衡字段),上游某节点故障时,网关会自动把请求重试到其他节点。你会观察到:
- 响应可能来自不同于上一次的节点(正常现象)。
- 如果所有节点都不可用,返回 503。
单上游(非负载均衡)不会跨节点转移,上游故障直接返回 502。
跨协议工具调用
当客户端协议和上游协议不同、且请求带工具调用时:
- 网关自动翻译
tools/tool_choice/tool_calls等字段。 tool_call_id在跨协议往返中必须保留原值——你在多轮工具对话里回传的tool_call_id要和网关给你的完全一致,否则上游无法匹配。- Anthropic 面向客户端:响应里的
tool_useid 由网关合成为全局唯一的toolu_id(不透传上游原始 id),客户端原样回显即可。 - Anthropic 面向客户端:请求历史里的
tool_useid 必须全局唯一,重复时网关回400 invalid_request_error(tool_use ids must be unique),与 Anthropic 官方 API 一致。 - 流式工具调用期间,网关持续转发增量片段,保证长工具调用时始终有事件流动。
思考签名(signature / encrypted_content)经网关封装
部分厂商(Anthropic、OpenAI、Google 等)在多轮对话里下发加密的思考签名/密文,下轮要原样带回,且只有颁发它的那个账号能解开。经网关时你观察到的差异:
- 同协议直通也不再裸透传:即使客户端协议和上游协议相同(如 Anthropic ⇄ Anthropic、OpenAI Responses ⇄ OpenAI 官方),签名也会被网关封装成不透明的网关信封——你拿到的仍是一个不透明字符串,照常原样回传即可,无需解析。
- 中途切换模型/上游/账号:旧签名对新的处理方是无法解开的外来密文,网关会自动剥离(thinking 保留可读文本、工具调用历史保留),而不是把它送去解不开它的账号导致上游 400。剥离只影响思考的加密接续,不影响对话内容。
- 升级过渡:升级前下发的旧格式签名,只要模型没变仍照常工作;模型变了则同样按剥离处理。
store 参数与响应取回
Responses 协议的 store 请求参数控制这一轮是否被保存。只要网关配有数据库存储后端(STORAGE_MODE 为 sqlite / postgresql,即有持久化后端),无论上游是什么协议,Responses 存储都由网关集中承载:历史接龙(previous_response_id)由网关消费,轮次落网关存储,响应 id 重写为网关自己的 resp_{id}。你在多轮对话中途切换模型不会断链——接龙、取回、删除都在网关侧闭环。
store缺省是 true。不传就按 true 处理,网关照常落库。store: false只是不落库:历史合并照常进行,响应也照常返回。之后如果有请求拿这个没落库的 id 当previous_response_id,网关查不到历史,会静默降级为只用当前轮上下文,不报错。GET /v1/responses/{id}取回:id 带不带resp_前缀都行。网关存储的轮次,响应包含id、object:"response"、status、model、output(逐字输出项)、previous_response_id(若有)、created_at,不含input。刚结束流式响应后立刻 GET,输出可能还在补写,此时status为in_progress,稍后重试即为completed。DELETE /v1/responses/{id}删除:成功返回{id, object:"response.deleted", deleted:true}。只删这一轮,不级联;删掉链条中间的一轮之后,它的后代轮仍能取回,但后续请求的历史重建会静默丢掉被删祖先的历史。- GET/DELETE 也能触达上游侧存储:轮次存在上游(网关未集中承载、原生 Responses 透传到上游,或 background 轮)时,网关按属主登记把请求代理到当初存它的那个上游,响应是上游的完整原生形状。
- 只能操作自己的数据:无论轮次在网关存储还是上游侧,都只认当初存它的那把 access key——查不到、归属不符,一律返回 404(不暴露别人的数据是否存在)。
- 归属信息是随轮次一起存的;没有归属信息的旧轮次无法取回或删除(404)。
- 与 background 共存:
background: true的轮次豁免集中化——响应 id 是上游的真实 id(它是事后取结果的凭证),轮次不落网关存储。拿这个 id GET 取结果时,网关经属主代理从上游取回真实完成态;previous_response_id接龙到这类轮次时,该轮及其后继在上游侧延续,引用回更早的网关轮次时自动切回网关侧接龙。
常见问题
Q:客户端报「stream 超时」或连接断开? 检查客户端和网关之间是否有反向代理,其空闲超时是否短于网关 keep-alive 间隔。调大反代超时,或调小 STREAMING_KEEPALIVE_SECONDS(默认 15 秒)。
Q:响应里混进了 _gateway_warning,是上游返回的吗? 不是,是网关加的。说明上游流中途断了。看里面的 reason/detail 判断是否需重试。
Q:流式请求被全局并发限制挡了,返回 503? 网关有全局并发上限(server.max_global_concurrency,默认 1000;流式另有 streaming.max_concurrent_streams,默认 200)。高峰期短暂 503 属正常,退避重试即可;上限可调(TOML 字段,见 配置参考 → 仅配置文件可配的字段),如需调高联系管理员。
