OpenAI 兼容网关的参数透传验证 (Verifying Parameter Passthrough in OpenAI-Compatible Gateways)
目标
拿到一个自称「OpenAI 兼容」的网关(中转站、校园平台、自建代理),在不看服务端代码的前提下判断:它到底把请求原样转给了上游,还是做了删改。
这件事直接决定你能拿它干什么。删改过的网关聊天没问题,但做不了 Agent。
为什么不能只看它「能不能回话」
能回话 ≠ 透传。套壳网关的常见做法是:接下请求 → 自己挑几个字段转发 → 拿到响应 → 重新拼一个 JSON 返回。中间丢什么、改什么,从外面看不出来。
真正的判据在边界情况上:只有当你传一个「正常聊天用不到、但 Agent 必须用」的参数,才能试出它转不转。
核心手法:用 YES/NO 问出参数有没有到达模型
这是整篇最有用的一招。不要观察模型行为,直接问模型。
把要验证的参数放进去,然后让模型回答一个只有「参数真的到了」才会出现的答案。
以验证 tools 参数为例:
curl -s -X POST "$GATEWAY/v1/chat/completions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"model": "your-model",
"stream": false,
"max_tokens": 200,
"messages": [{
"role": "user",
"content": "I have registered a function named get_weather in this request's tools parameter. Look at your available tools. Is get_weather available to you? Answer exactly: YES or NO."
}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}}
}
}]
}
JSON读到 NO 就说明参数被网关吞了。 连问三次都是 NO,可以定性。
这个方法的好处:模型的回答是一个确定的布尔值,不依赖它怎么「演」。而观察行为会被误导 —— 见下。
反例:为什么不能看模型「有没有调工具」
被吞了 tools 的网关,表现极具迷惑性:
- 有时模型拒绝:「我没有 get_weather 工具」
- 有时模型编一段调用格式当普通文本吐出来
后者最容易骗人。以 DeepSeek 为例,它的原生工具调用格式用一对特殊标记包裹(形如 <全角竖线><全角竖线>DSML<全角竖线><全角竖线> 加 calls / invoke / parameter 标签)。
这段标记本该由服务端的 tool parser 读掉、翻译成 OpenAI 标准的 tool_calls 字段再返回。网关没开这个 parser 时,标记会原样漏进 content。
关键判断:漏出来的标记不是「工具调用成功」,恰恰是「工具调用失败」。 因为模型是在没有工具定义的情况下凭训练记忆猜了一个格式出来 —— 我实测时 prompt 里提到了函数名,模型就自己编了一段。
三者对照:
| 现象 | 真实含义 |
|---|---|
tool_calls 字段有值 | 真透传,工具调用可用 |
tool_calls 字段不存在,content 里有原始标记 | 网关吞了 tools,模型在瞎猜 |
| 模型说「我没有这个工具」 | 同上 |
补充:tool_calls 字段缺失 ≠ 值为 null
实测中容易搞混:网关返回的 JSON 里完全没有 tool_calls 这个键,而不是 "tool_calls": null。
用 Python 解析时 msg.get('tool_calls') 两种情况都返回 None,看不出区别。要判断真相得看原始 JSON 文本:
grep -c 'tool_calls' raw-response.json # 0 表示字段根本不存在配套验证项
同一个手法可以验其它参数。挑几个「正常聊天用不到、Agent 或高级用法才要」的:
| 验证项 | 怎么试 | 透传的表现 |
|---|---|---|
tools | 上面的 YES/NO 问法 | 模型答 YES |
tool_choice | 设为 {"type":"function","function":{"name":"x"}}(强制调用) | 返回 tool_calls,而不是自由文本 |
logprobs | 传 "logprobs": true | 响应里出现 logprobs 对象,不是 null |
| 非法参数 | 传 "temperature": 99 | 报 400;静默接受也是被吞的信号 |
| 响应信封 | 看顶层字段 | 完整透传会带 system_fingerprint |
| 模型注册表 | 调上游的真名(如 deepseek-chat) | 透传能过;套壳网关会说「模型不存在」 |
stream 默认值 | 不传 stream | OpenAI 规范默认 false;默认 true 是自研实现的特征 |
判据总结:任何一项被吞,这个网关就不该拿来做 Agent。
一个容易误判的信号:token 数和内容长度对不上
有时会看到 usage.completion_tokens 远大于 content 的长度(例如 2600 tokens 但只有 31 个字符)。
别急着下「被吞了工具调用」的结论。 常见原因是推理模型的思维链(reasoning / thinking tokens)被网关丢弃,但 token 照算。
区分方法:去掉 tools 参数再测一次同样的问法。如果 token 数依然对不上,那就是思维链,与工具调用无关。
我实测时正是如此:去掉 tools 后,同样生成了 419 个 token 而只有 221 个字符的内容 —— 说明网关本来就在丢思考过程。
完整流程
GET /v1/models或POST /v1/chat/completions探活,确认鉴权方式- 传一个不存在的模型名,看报错格式(能反推它是不是套壳)
- 跑 YES/NO 探针验
tools - 逐个补验上表里的其它参数
- 看响应的顶层字段和
id格式(自研 id 是套壳的强信号)
下次如何识别
- 客户端报「模型不承认工具存在」「工具调用格式乱码」→ 先跑 YES/NO 探针,别先怀疑模型。
- 看到
content里出现原始特殊标记(DSML 之类)→ 网关的 tool parser 没开,在服务端修,客户端无解。 - 只有
chat/completions能用、completions(FIM 补全)404 → 这个网关只实现了 chat 一条路。 - 想确认「是不是真上游」→ 看它吐不吐上游专有的东西。DeepSeek 的 DSML 标记就是强证据:套壳的模型不会吐别家的私有格式。
相关概念
- MCP 远程端点手动探测 (Manually Probing MCP Endpoints) —— 探测 MCP 端点本身的四步法
- MCP (Model Context Protocol)
- 平台无法传请求头时用 nginx 注入鉴权 (Injecting Auth Headers via nginx for Header-Blind Clients) —— 网关不支持鉴权头时的绕法
- 模型降智检测与果汁数 (Model Degradation Detection & Juice Number) —— 同属「验真」家族
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/AI apps