指南/OpenAI API 兼容性
“兼容 OpenAI API”到底兼容到哪一层
把 OpenAI SDK 的 base_url 换掉,第一条 Chat Completions 返回了文字,通常就会被标成「兼容 OpenAI API」。这只验证了认证、路径和最简单的文本请求。生产代码依赖的参数语义、工具调用、流式终止、错误对象与用量字段,仍可能完全不同。
「能调用」是最低一层
OpenAI API 兼容通常指兼容 Chat Completions 的请求和响应外形。SDK 负责序列化 JSON、发送认证头,再把返回对象解析成 choices[0].message.content。只测一句「你好」,覆盖不到开发者消息、多模态内容、结构化输出、工具调用、流式事件或缓存明细。
官方兼容层本身也没有承诺完整等价。Anthropic 把自己的 OpenAI SDK compatibility定位为模型测试和比较用途,并建议需要 PDF、引用、思考与 Prompt Caching 时改用原生 Claude API。Google 的集成指南也写明,OpenAI SDK 路径适合优先统一 schema、又不依赖 File API 等 Gemini 专属能力的场景。
判断兼容性,应先写清应用依赖哪部分契约。一个只发纯文本的批处理程序,与一个依赖严格工具参数、缓存计费和实时流式的 Agent,要求差很多。
第一层:端点、认证与基础响应
这层检查最容易:SDK 能否连接,认证失败是否返回可识别错误,模型名是否按预期解析,纯文本请求是否得到 choices、message 和 finish_reason。
还要记录响应里的模型标识和请求 ID。兼容服务可能接受 OpenAI 风格模型名,再映射到自己的模型;仅看请求中的 model 无法知道响应实际声明了什么。请求 ID 则是和服务方排查问题时唯一能精确定位一笔调用的线索。
第二层:参数是执行了,还是被收下后丢掉
HTTP 200 只能证明请求格式被接受。参数可能生效、被换算、被限制,也可能静默忽略。
Anthropic 的官方兼容文档给了可核对的例子:
temperature只支持 0 到 1,大于 1 的值会被限制为 1;n必须是 1。logprobs、seed、presence_penalty、frequency_penalty、reasoning_effort等字段会被忽略。- 音频输入不受支持,会从输入中移除。
- 多数不受支持字段不会触发错误。
静默忽略比明确报错更难处理。调用方会以为 seed 已提供可复现性,或者 strict 已保证工具参数符合 JSON Schema,直到某个分支拿到意外结果。
契约测试要为每个依赖参数设计可观测结果。比如把 n 设为 2,确认服务是拒绝、返回两个候选,还是悄悄改成 1;发送一个文档明确列为不支持的字段,确认团队是否接受「忽略」这种行为。
第三层:消息角色与顺序
字段名相同,消息语义也可能变。OpenAI Chat Completions 允许 system 和 developer 角色出现在消息序列中。Claude 原生 Messages 只使用一个起始 system prompt,所以 Anthropic 兼容层会把会话各处的 system 和 developer 消息抽出,用换行拼接后放到对话开头。
这会改变原始顺序。若应用在对话中途插入新的 developer 消息,用来覆盖某一阶段的行为,兼容层的拼接结果与 OpenAI 原请求已经不是同一段上下文。
测试时准备一组能暴露优先级和顺序的消息,不要只测单个 system prompt。保存服务实际收到的请求日志时,也要区分「客户端发送体」与「兼容层转换后的上游请求」;只有前者无法解释行为差异。
第四层:结构化输出与工具调用
工具调用能返回一个对象,不代表 schema 约束已经执行。Anthropic 兼容层会忽略 function calling 的 strict,官方文档明确说工具参数不保证符合给定 schema;response_format 也会被忽略。需要强 schema 时,Anthropic 要求使用原生 Structured Outputs。
一组有效的工具契约测试至少包含:
- 一个有必填字段、枚举和
additionalProperties: false的工具,检查返回参数能否通过本地校验。 - 一次并行工具调用,检查多个
tool_calls的 ID、索引和参数增量能否正确配对。 - 工具结果回传后的第二轮请求,检查角色、调用 ID 和终止原因有没有丢失。
- 一个故意无法满足的 schema,确认服务返回错误、拒绝还是自由文本。
无论服务宣称是否支持 strict,应用都应在执行工具前本地校验参数。兼容层、SDK 版本或模型切换都会改变边界,应用能确定的是本地校验结果。
第五层:流式事件与结束条件
Chat Completions 的流式响应常以 delta 和 [DONE] 结束;OpenAI Responses API 则有 response.created、response.output_text.delta、response.completed 和 error 等带类型事件。两者已经不是同一套事件模型。
兼容服务可能只实现 Chat Completions,也可能把自己的原生事件转换成 OpenAI 风格 chunk。测试要检查 UTF-8 字符是否在边界处损坏、工具参数是否能跨 chunk 累积、终止标记是否存在、连接中断能否被识别为失败。只要业务使用流式响应,就应单独阅读流式响应返回 200,为什么仍然可能失败。
第六层:错误、状态码与响应头
错误 JSON 外形相似,错误分类未必等价。Anthropic 表示其兼容层维持 OpenAI 风格错误格式,但详细错误消息不会相同,只适合日志和调试。部分速率限制头与 request-id 受支持,openai-processing-ms 则始终为空。
因此代码不要按错误消息文本做分支。应优先使用 HTTP 状态码、稳定的错误类型、retry-after 和请求 ID,并为 401、429、服务端错误、超时与流内错误分别建测试。错误消息可以展示给排查者,不能充当机器契约;定位错误来自客户端、中转层还是上游,可按 AI API 错误排查手册留证。
第七层:usage 与计费明细
很多兼容层只填 prompt_tokens、completion_tokens、total_tokens 三个总数。Anthropic 兼容层把 usage.prompt_tokens_details 与 usage.completion_tokens_details 始终留空,同时不支持 Prompt Caching。这种响应足够显示一个总量,却无法验证缓存读写或思考用量。
原生 API 之间的包含关系也不同,不能因为兼容层换成 OpenAI 字段名,就按 OpenAI 原生语义理解。三家的详细对照见OpenAI、Claude、Gemini 的 Token 为什么不能直接横向比较;缓存验证见 Prompt Cache 到底命中了没有。
把兼容性写成一张契约表
上线前把业务依赖的能力列成表,每一行都要有请求样例、期望响应和失败处理:
| 能力 | 至少验证什么 | 不兼容时怎么办 |
|---|---|---|
| 基础文本 | 模型、内容、终止原因、请求 ID | 阻止上线 |
| 参数 | 业务依赖字段是否生效,未知字段怎样处理 | 删除依赖或改用原生 API |
| 消息 | system/developer 的位置和优先级 | 重写 prompt 结构 |
| 工具 | schema、本地校验、并行调用、结果回传 | 执行前拒绝无效参数 |
| 流式 | 事件顺序、终止、错误和中断 | 丢弃不完整结果或安全续传 |
| 用量 | 总数、缓存和思考明细 | 单独标记为不可核对 |
同一套测试要对官方 OpenAI 端点和目标兼容端点各跑一次。结果不同不自动等于兼容端点有问题;有些差异来自厂商明确记录的设计。要做的判断是:这些差异是否违反当前应用的契约。
兼容性不是一个布尔值。把它拆成可运行的契约测试,服务或模型升级后重新执行,才能知道变的是哪一层。
LinkyMonitor 用固定协议探针记录字段、终止原因和用量结构;兼容层升级后哪一项开始漂移,可以回到原始响应核对。