This commit is contained in:
2026-08-23 02:12:08 +08:00
commit 5987b2a1f2
19 changed files with 4174 additions and 0 deletions
+281
View File
@@ -0,0 +1,281 @@
# 国产 AI Provider 的怪癖
> 采集于 2026-08-12。
>
> **可信度标注**:本文件里 ✅ 表示来自厂商官方文档页面;⚠️ 表示来自搜索结果聚合或第三方资料,**落地前需要用真实请求验证**。国产厂商文档改版频繁(本次采集就遇到 moonshot.cn → kimi.com、volcengine.com → docs.volcengine.com 两处 301),且很多"历史怪癖"在新版本里已被修掉,不要照抄老博客。
国产 API 的怪癖**和欧美三家不是一个类型**。OpenAI/Anthropic/DeepSeek 的坑主要在**语义层**thinking 怎么回传、tool_result 放哪);国产的坑主要在**协议层和运维层**:鉴权方式五花八门、HTTP 200 里藏业务错误、流式默认全量返回、内容安全是一等公民。
---
## 0. 跨厂商的共性怪癖(最重要)
这五条是中转站架构里必须提前留出的抽象层,逐个厂商去补会很痛苦。
### 0.1 HTTP 200 内嵌业务错误 ⚠️→✅
**MiniMax 是典型**:无论成功失败都返回 **HTTP 200**,真正的状态在 body 的 `base_resp.status_code`0 = 成功)。
```json
{"base_resp": {"status_code": 1002, "status_msg": "rate limit"}}
```
对中转站的直接后果:**只看 HTTP 状态码的限流检测会完全失效**——限流返回的是 200 而不是 429。重试逻辑、熔断器、计费统计全都要能读 body。
建议:网关的上游响应处理层做成"HTTP 状态码 + body 业务码"双通道判定,而不是假设 HTTP 语义可靠。
### 0.2 内容安全是一等公民
欧美三家只有 `content_filter` / `refusal` 一个笼统信号,国产普遍把内容安全做成结构化字段:
| 厂商 | 表现形式 |
| --- | --- |
| 智谱 GLM ✅ | `finish_reason: "sensitive"`(OpenAI 枚举里没有这个值) |
| MiniMax ✅ | `input_sensitive` / `output_sensitive` 布尔,外加 `input_sensitive_type` / `output_sensitive_type`17 的违规分类),还有 `mask_sensitive_info` 参数 |
`finish_reason: "sensitive"` 会直接**打破 OpenAI 客户端的枚举校验**——严格的 SDK(尤其是 Go/Rust 这类强类型的)会反序列化失败。中转站必须把它映射成 `content_filter`,同时用扩展字段保留原值。
### 0.3 鉴权方式没有一个统一的
| 方式 | 厂商 |
| --- | --- |
| `Authorization: Bearer <key>` | 智谱(现行)✅、Kimi ✅、豆包/火山方舟 ✅、腾讯混元兼容端点 ✅、DeepSeek |
| Bearer + 额外 header | 百度千帆 v2`Bearer bce-v3/ALTAK-...` + `appid` header)⚠️ |
| JWT 签名(key 形如 `{id}.{secret}`,需自行签发带 exp 的 token) | 智谱(历史方式,现在仍支持)⚠️ |
| `access_token` 放 query string(用 API Key + Secret Key 先换取) | 百度千帆 v1 ⚠️ |
| TC3-HMAC-SHA256 云厂商签名 | 腾讯混元原生接口 ✅ |
| HMAC-SHA256 签名拼进 **WebSocket URL** | 讯飞星火原生接口 ✅ |
| `AK:SK` 拼接格式 | 讯飞星火 OpenAI 兼容端点 ⚠️ |
中转站的凭据存储不能假设"一个 key 一个字符串"——至少要支持 (key, secret, appid, region) 四元组,且签名逻辑要能按 provider 插拔。
### 0.4 流式默认可能是全量累积
见 §1.2 的阿里 `incremental_output`。这是**最容易在生产上炸**的一条:不设参数时每个 SSE chunk 返回的是**从头累积的完整文本**,直接按 OpenAI 语义拼接会得到指数级重复的内容。
### 0.5 非标参数塞 `extra_body`
国产厂商的 OpenAI 兼容模式普遍保留一批自有参数(思考开关、联网搜索、重复惩罚),官方推荐用 OpenAI SDK 的 `extra_body` 透传。中转站需要一个**参数白名单 + 透传通道**,而不是严格按 OpenAI schema 校验后丢弃未知字段。
---
## 1. 阿里 · 通义千问 / 百炼(DashScope)✅
> 来源:https://help.aliyun.com/zh/model-studio/use-qwen-by-calling-api 、https://help.aliyun.com/zh/model-studio/qwen-api-via-dashscope 、https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-chat-completions
### 1.1 四套接口并存
百炼同时提供**四种**入口,这本身就是中转站的最佳参考:
1. **OpenAI 兼容 Chat Completions** — 迁移成本最低
2. **OpenAI 兼容 Responses** — 内置联网搜索、代码解释器、网页提取,自动管理对话历史
3. **Anthropic 兼容 Messages** — 支持 thinking 和工具调用
4. **DashScope 原生** — "提供最完整的功能集和参数支持"
**功能集不等价**:原生接口是超集。做中转站时若只对接 OpenAI 兼容模式,会拿不到部分能力。
### 1.2 `incremental_output`:最大的坑 ⚠️
DashScope 原生流式下:
| 取值 | 行为 |
| --- | --- |
| `true` | 每个 chunk 只含**新增**内容(OpenAI 语义) |
| `false`(部分场景默认) | 每个 chunk 返回**从头累积的完整文本** |
如果按 OpenAI 的"delta 累加"逻辑处理 `incremental_output=false` 的流,输出会变成 `我我是我是通我是通义…` 这样的雪球。
**中转站务必显式传 `incremental_output=true`,不要依赖默认值。**
### 1.3 原生接口是嵌套结构
```jsonc
// DashScope 原生
POST /api/v1/services/aigc/text-generation/generation
{
"model": "qwen-plus",
"input": { "messages": [...] }, // ← 嵌套
"parameters": { "temperature": 0.7, "result_format": "message" }
}
// 响应:output.choices[].message ← 比 OpenAI 多一层 output
```
`result_format` 取值 `text` / `message`——**取 `text` 时响应结构完全不同**(不是 choices 数组)。中转站应固定用 `message`
### 1.4 其他
- **原生 HTTP 流式必须加 header `X-DashScope-SSE: enable`** ⚠️,只设 `stream` 参数不生效。
- **base_url 按地域 + workspace 分片** ✅:`https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1`region 含 cn-beijing / ap-southeast-1 / cn-hongkong / eu-central-1 / ap-northeast-1,另有 `https://dashscope-us.aliyuncs.com/compatible-mode/v1`。中转站的上游配置不能是单一 URL 常量。
- OpenAI 兼容模式下需走 `extra_body` 的非标参数 ✅:`enable_thinking``thinking_budget``top_k``repetition_penalty`0.012.0)、`enable_search` / `search_options``preserve_thinking` / `clear_thinking``enable_code_interpreter``tool_stream``vl_high_resolution_images`
- **QwQ 等思考模型需要流式输出** ✅,非流式体验会因长推理阶段而劣化。
- `seed` 范围 `[0, 2³¹-1]`,与 OpenAI 的 int64 范围不同 ⚠️。
---
## 2. 智谱 GLMBigModel)✅
> 来源:https://docs.bigmodel.cn/api-reference/模型-api/对话补全
端点:`POST https://open.bigmodel.cn/api/paas/v4/chat/completions`(注意路径是 `/api/paas/v4/`,不是 `/v1/`
| 怪癖 | 详情 |
| --- | --- |
| **`do_sample`** | 布尔,默认 `true`。为 `false` 时走贪心解码,**`temperature` / `top_p` 全部失效**。OpenAI 无此参数——中转站收到 `temperature: 0` 时应考虑翻译成 `do_sample: false` 而不是直接透传 |
| **`temperature` 范围 `[0.0, 1.0]`** | 不是 OpenAI 的 0~2,超出需 clamp |
| **`top_p` 范围 `[0.01, 1.0]`,默认 0.95** | 下界不是 0,`top_p: 0` 会被拒 |
| **`finish_reason: "sensitive"`** | 内容被安全拦截。**不在 OpenAI 枚举内**,会打破严格客户端 |
| `thinking` | `{"type": "enabled"\|"disabled", "clear_thinking": bool}`,仅 GLM-4.5+ |
| `reasoning_content` | 思维链输出字段(DeepSeek 风格的事实标准) |
| 工具上限 | 最多 128 个 function,另支持 web_search / 知识库检索 / MCP 工具 |
| 鉴权 | 现行推荐 `Authorization: Bearer <api_key>`;**历史 JWT 方式仍支持** ⚠️——API Key 形如 `{id}.{secret}`,需按点号拆分后签发含 `api_key`/`exp`/`timestamp` 的 JWT |
默认模型为 `glm-5.2`。流式同样以 `data: [DONE]` 结束。
---
## 3. 月之暗面 KimiMoonshot)✅
> **已拆分为独立文件:[kimi.md](./kimi.md)**2026-08-13 全量重采集,**并用真实 API key 实测校验**)。本节仅保留跨厂商对比时用得上的摘要,细节以那边为准——尤其 §14 列出了 10 条被实测推翻的官方文档结论。
>
> ⚠️ **域名分层**:文档站/控制台 `platform.moonshot.cn` → `platform.kimi.com`301),但 **API 数据面仍是 `api.moonshot.cn`**,没有迁到 `api.kimi.com`。另有国际站 `platform.kimi.ai` / `api.moonshot.ai`**账号与 key 与国内站完全隔离**。
| 怪癖 | 详情 |
| --- | --- |
| **采样参数硬 400**(实测) | K3 / K2.7-code 把 `temperature`(1.0) / `top_p`(0.95) / `n`(1) / 两个 penalty 写死,**传其他值 400 而不是静默忽略**(实测 `invalid temperature: only 1 is allowed for this model`)。而 moonshot-v1 的 `temperature` 默认是 **0**、上限是 **1**OpenAI 是 2),且 **`n>1` 要求 temperature>0**(隐藏耦合,文档未提) |
| **同参数两入口行为相反**(实测) | 同一个 `temperature=0.7`OpenAI 入口 **400**,自家 Anthropic 入口 **200 静默忽略**。但 `tool_choice` 的限制两边一致——**是选择性的不一致**,不能"打不通就换入口" |
| **流式 `usage` 位置可控**(实测) | 不传 `stream_options.include_usage` 时藏在 **`choices[0].usage`**OpenAI SDK 的 `chunk.usage` 恒为 `None`,会漏计费);**传 `include_usage:true` 则回到顶层 + `choices:[]`,与 OpenAI 一致**。中转站补这个参数即可,无需改写响应 |
| **Partial Mode(前缀续写)** | 在 messages 末尾加 `{"role": "assistant", "content": "...", "partial": true}`,模型从该前缀继续。**字段名是 `partial`DeepSeek 叫 `prefix`** —— 同一能力三家三个名字(另见 Anthropic 的 prefill,且 Claude 4.6+ 已禁用) |
| **`n` 最多 5** | 且**仅 Moonshot V1 系列支持**,新模型固定为 1 |
| 思考模型分裂成两套参数 | K3 用顶层 `reasoning_effort``low`/`high`/`max`,默认最贵的 `max`);K2.6/K2.5 用 `thinking` 对象;K2.7-code 用 `thinking` 但只接受 `"enabled"`(实测关闭 → 400)。⚠️ **文档称 K3 恒开不可关,实测 `thinking:{"type":"disabled"}` 有效**`prompt_tokens` 88→21,思考模式带约 67 token 隐藏 system prompt)。⚠️ `reasoning_effort``"banana"` 也返回 200——**枚举校验形同虚设,非法值静默跑默认的 max 档** |
| `reasoning_content` 明文回传 | 明文(非 Anthropic 那种加密 signature)。⚠️ **文档称 K3/K2.7-code 多轮缺失会报错,实测不报错**200 正常继续) |
| `tool_choice` 与思考互斥 | 指定具体函数 + 思考开启 → **400 `tool_choice 'specified' is incompatible with thinking enabled`**(两个入口一致)。⚠️ **但关闭思考后再指定函数 → 200 且模型不调用工具、直接幻觉出假数据**——静默错误比 400 更危险,不要把关思考当变通方案 |
| tool_call id 可预测 | 格式是 **`{函数名}_{序号}`**(如 `get_weather_0`),不是 OpenAI 的随机串。多轮同名函数会**重复 id**,用它做去重键会误判 |
| 多模态**不收 http URL** | 只接受 base64 data URI 和 `ms://<file_id>`。与 OpenAI/Anthropic 的硬性差异 |
| 动态工具加载 | 工具可声明在 `{"role":"system","tools":[...]}`**无 `content` 字段**)里,位置决定可见轮次。仅 K3 支持,其他模型报 `tokenization failed` |
| `finish_reason` | `stop` / `length` / `tool_calls`,与 OpenAI 一致,**没有 `sensitive`**——内容安全走 400 `content_filter`,比多数国产厂商干净 |
| 错误体缺字段 + 三种格式 | OpenAI 入口只有 `error.type`/`error.message`**无 `code`/`param`**);Anthropic 入口多 `request_id` 和顶层 `type`**路径写错会掉到网关层**,返回 `{"code":5,"error":"url.not_found","message":"没找到对象","ua":"curl/8.7.1",...}`——连 `error` 字段的类型都从 object 变成 string |
| 三种 429 语义相反 | `engine_overloaded_error`(退避重试)/ `exceeded_current_quota_error`(欠费,重试无用)/ `rate_limit_reached_error`(降并发)。只看状态码的通用重试会在欠费时打爆上游 |
| **错误信息泄漏账号标识** | 429 的 message 原文含 `Your account org-xxx<ak-xxx> request reached organization max RPM: 3`——**org id 和 key 标识直接暴露**。响应头另有 `msh-org-id`/`msh-uid`/`msh-project-id`。中转站原样透传 = 把自己的账号广播给所有下游用户 |
| 无 `x-ratelimit-*` 头 | 只在 429 时给 `retry-after` / `x-retry-after` / `msh-cooldown-seconds`(同一信息重复三份)。**成功时无法得知剩余配额**,配额管理只能本地计数 |
| **两个产品两套栈** | `platform``api.moonshot.cn`key `sk-`)与 **Kimi For Coding**`api.kimi.com/coding`key `sk-kimi-`**key 互不通用**,模型名(`kimi-k3` vs `k3`)、Anthropic 入口路径、限流、错误语义、tool_call id 格式全不同。⚠️ **coding 侧几乎不校验 `model`**:传 `gpt-4`/空字符串都返回 200 并原样回显,只有带方括号的名字被拒(且返回 **401** 而非 404)。详见 [kimi.md](./kimi.md) §17 |
无状态 API,多轮需自行拼接历史。
**context caching 已查证(原"需另行查证"结论作废)**:早期的独立 cache 对象 API 已被移除,**现行是全自动前缀缓存**——无需创建或引用 cache id。⚠️ 文档称"prompt > 256 token 才命中"**实测 21 token 也完整命中**。命中量同时报在 `usage.cached_tokens`Kimi 私有)和 `usage.prompt_tokens_details.cached_tokens`(OpenAI 兼容,**中转站直接用后者即可**);未命中时字段不出现而非返回 0。K3 命中 ¥2/1M、未命中 ¥20/1M,**10 倍价差**意味着中转站任何对 prompt 前缀的改写都会带来 10 倍成本。
---
## 4. 字节 · 豆包 / 火山方舟(Ark)✅
> 来源:https://doubao.apifox.cn/265892759e0 、搜索结果
- Base URL`https://ark.cn-beijing.volces.com/api/v3`OpenAI SDK 直接可用。
- **`model` 字段的历史怪癖** ⚠️:早期必须先在控制台创建"推理接入点"拿到 **Endpoint ID(形如 `ep-2024...`** 填进 `model`,而不是模型名。现在也支持直接填模型名(如 `doubao-pro-32k-240615`)。
中转站的模型别名表必须允许"一个逻辑模型名 → 一个 ep-xxx 字符串"的映射,且**不同用户的 ep id 不同**(接入点是账号级资源)。这意味着模型路由表可能需要**按租户隔离**,而不是全局共享一张表。
- usage 中含缓存相关明细字段。
- ⚠️ 抓取到的 OpenAPI 规范页**未列出 thinking 参数和完整限制**,深度思考模型(doubao-1.5-thinking 等)的参数需另查。
---
## 5. 百度 · 文心 / 千帆 ⚠️
> 来源:https://ai.baidu.com/ai-doc/WENXINWORKSHOP/0m2vwrjwsv2 通告,✅)+ 搜索结果(v1 约束,⚠️)
百度是"新旧两代差异最大"的一家。
### v1(历史,仍在运行)的经典怪癖 ⚠️
- **每个模型一个独立 URL**,模型名在路径里,没有 `model` 参数。
- **鉴权用 `access_token` 放 query string**:先用 API Key + Secret Key 调 `grant_type=client_credentials` 换取。
- **`messages` 必须是奇数条**。
- **role 必须 user / assistant 严格交替**:奇数位是 `user`(或 `function`),偶数位是 `assistant`,且**第一条不能是 `function`**。
- `system` 是**独立顶层字段**,不能放进 messages(和 Anthropic 一样)。
这套约束意味着**任何带连续同角色消息的 OpenAI 请求都无法直通 v1**,中转站必须做消息合并(连续 user 合并成一条)。
### v2(现行,已 OpenAI 兼容)✅
- 端点统一为 `https://qianfan.baidubce.com/v2/chat/completions`,用 `model` 参数选模型。
- 鉴权改为 **IAM Bearer Token**`Authorization: Bearer bce-v3/ALTAK-...`,并需要额外的 `appid` header ⚠️。
- 官方称"完全兼容 OpenAI SDK"。
- v1 在过渡期内继续可用,TPM 配额在两个版本间共享。
**中转站建议直接对接 v2**,但要注意 v2 是否仍保留 messages 交替约束——通告未明确说明,需实测验证。
---
## 6. 腾讯混元 ✅
> 来源:https://cloud.tencent.com/document/product/1729/111007
两套接口差异极大:
### 原生接口
- **腾讯云 TC3-HMAC-SHA256 签名**(云厂商通用签名 v3),需要多步 HMAC 派生密钥(SecretDate → SecretService → SecretSigning)。POST 支持最大 10MB。
- ⚠️ 字段命名是 **PascalCase 大驼峰**`Messages` / `Role` / `Content`),不是 OpenAI 的 snake_case。这是腾讯云全平台 API 的统一风格。
### OpenAI 兼容接口(推荐)
- Base URL`https://api.hunyuan.cloud.tencent.com/v1`
- 直接用 API Key + `Authorization: Bearer`OpenAI SDK 无需改代码。
**中转站结论**:走兼容端点,不要碰 TC3 签名——那套签名实现成本高且容易在时钟偏移、URL 编码细节上出错。
---
## 7. MiniMax ✅
> 来源:https://platform.minimax.io/docs/api-reference/text-post
| 怪癖 | 详情 |
| --- | --- |
| **端点路径非标** | `https://api.minimax.io/v1/text/chatcompletion_v2`**不是** `/v1/chat/completions` |
| **HTTP 200 + `base_resp`** | 业务错误也返回 200,状态在 `base_resp.status_code`0 = 成功) |
| 业务状态码 | `1002` 限流 / `1004` 鉴权失败 / `1008` 余额不足 / `1039` token 超限 / `2013` 参数无效 |
| 内容安全字段 | `input_sensitive``output_sensitive``input_sensitive_type` / `output_sensitive_type`17 分类)、`mask_sensitive_info` |
| `temperature` 范围 | 01,非 OpenAI 的 02 |
| `top_p` 默认 | 0.95 |
| `response_format` | JSON schema **仅 MiniMax-Text-01 支持** |
| 特有响应字段 | `reasoning_content``audio_content``correct_rate`JSON schema 模式下的置信度) |
| usage | 单独统计 `reasoning_tokens` |
`base_resp` 这一条值得再强调:**限流不返回 429**,任何依赖 HTTP 状态码的重试/熔断中间件在 MiniMax 上都是失效的。
---
## 8. 讯飞星火 ✅
> 来源:https://www.xfyun.cn/doc/spark/ 系列页面
- **原生接口是 WebSocket,不是 HTTP** ✅。鉴权方式是用 APPID + APIKey + APISecret 做 HMAC-SHA256,把签名**拼进 WebSocket 连接 URL** 里。这在中转站架构里是完全异类——需要独立的连接管理、心跳、重连逻辑,无法复用 HTTP 连接池。
- **OpenAI 兼容 HTTP 端点**`https://spark-api-open.xf-yun.com/v1`,用 APIPassword 认证;最新的 Spark-X2 还支持 `AK:SK` 拼接格式 ⚠️。
**中转站结论**:只对接 OpenAI 兼容 HTTP 端点。WebSocket 原生接口的接入成本远高于收益,除非需要它独有的能力(如实时语音)。
---
## 9. 给中转站的设计清单
从上面这些怪癖倒推,网关需要的抽象层:
1. **双通道错误判定** — HTTP 状态码 + body 业务码(MiniMax `base_resp`)。不能假设 HTTP 语义可靠。
2. **可插拔签名器** — Bearer / JWT 自签 / query token / TC3-HMAC / WebSocket 签名 URL 至少五种。凭据模型要支持 (key, secret, appid, region) 而非单字符串。
3. **流式规范化层** — 统一处理"全量累积 vs 增量"(阿里 `incremental_output`)、`[DONE]` 有无、SSE header 要求(`X-DashScope-SSE`)。**这一层是国产接入的核心价值**。
4. **finish_reason 枚举映射**`sensitive``content_filter`,并用扩展字段保留原值,避免打破强类型客户端。
5. **非标参数透传白名单**`extra_body` 风格的旁路通道,而不是按 OpenAI schema 严格校验后丢弃。
6. **租户级模型路由表** — 火山方舟的 Endpoint ID 是账号级资源,模型别名映射不能全局共享。
7. **多地域 base_url** — 阿里按 region + workspace 分片,上游地址不能是常量。
8. **消息结构归一化** — 连续同角色消息合并(百度 v1 交替约束、Anthropic 单 system),作为通用前置处理。
---
## 10. 未覆盖 / 待验证
- **零一万物 Yi、百川 Baichuan、阶跃星辰 StepFun、商汤日日新** — 未收集。
- **火山方舟深度思考模型的 thinking 参数** — 官方 OpenAPI 页未列出,需另查。
- ~~**Kimi 的 context caching**~~ — ✅ 已查证(2026-08-13):早期的独立 cache 对象 API 已移除,现为全自动前缀缓存,见 [kimi.md](./kimi.md) §10。Kimi 的其余待验证项已转入 [kimi.md](./kimi.md) §14。
- **百度 v2 是否仍保留 messages 奇数/交替约束** — 通告未明说,**必须实测**。
- 所有标 ⚠️ 的条目 — 来自搜索结果聚合,建议用真实请求逐条验证后再固化进代码。