API中转返回200仍报错?检查choices和usage字段
很多接口会把自己描述成“OpenAI 兼容”但真正接入后才发现兼容的含义可能只覆盖了 URL 形式或者只在最简单的请求上返回了 200。客户端一旦依赖choices[0].message.content、usage.total_tokens或错误响应中的固定字段隐藏的不一致就会变成运行时故障。这篇文章不做供应商排名也不把一次成功请求包装成完整兼容性证明。我们把问题收窄成一个可以在本地完成的任务给定一个请求 JSON 和一个响应 JSON用一份最小契约检查器验证“客户端真正依赖的字段是否存在、类型是否正确、列表是否为空”。通过样例和失败样例都放在文章配套目录里读者可以先离线验证再把同一套检查器接到自己的测试环境。先用一分钟定位是不是字段兼容问题适用场景是你已经把中转站提供的Base URL、API Key 和模型名填进客户端请求也返回 HTTP 200但程序仍然出现KeyError: choices、NoneType、用量统计为空或 SDK 无法读取消息内容。先不要改业务代码保存一次原始 JSON 响应再检查客户端最常读取的三个位置python3-mjson.tool response.json python3 contract_check.py request.json response.json成功信号是脚本输出PASS response contract并以exit0结束如果输出明确指向response.choices[0].message.content或response.usage.total_tokens说明问题在响应字段契约不是“网络已经通了就一定兼容”。如果 HTTP 状态本身是 401、404 或 429应先按鉴权、路径/模型名和限流分别处理不要混进字段兼容问题。实测结果概览图中结果来自本文配套的本地 fixture通过样例返回 exit0缺少 message.content 的失败样例返回 exit1。两条路径都由同一检查器实际执行失败结果没有被改写成成功。先定义“兼容”的检查范围OpenAI API 的官方参考页把 Chat Completions 描述为对一组消息发起POST /chat/completions并列出ChatCompletion响应对象。JSON Schema 官方教程则把 schema 解释为对 JSON 实例的结构、约束和类型描述。把这两份资料放在一起得到一个很实用的判断兼容性不是一句产品描述而是一组可以被验证的字段约束。本文先检查四个层次请求体是否是对象model是否为非空字符串messages是否为非空数组。每条消息是否有允许的role和字符串形式的content。响应体是否包含非空的choices首个 choice 是否有整数index和 assistant 消息。usage是否提供三个整数prompt_tokens、completion_tokens、total_tokens。这不是对完整 API 的覆盖。工具调用、图像输入、流式事件、音频、多候选和供应商自定义字段都可能需要额外契约。好处是先把业务代码最依赖的最小面固定下来失败时能知道究竟是哪一层不满足。准备一个不带真实凭据的检查器下面的检查器只使用 Python 标准库。它不请求网络不读取环境变量中的 Key也不把响应内容发送到第三方服务。将它保存为contract_check.py再传入请求文件和响应文件importjsonimportsysfrompathlibimportPathdeffail(path,message):raiseValueError(f{path}:{message})defrequire_object(value,path):ifnotisinstance(value,dict):fail(path,expected object)defrequire_string(value,path):ifnotisinstance(value,str)ornotvalue.strip():fail(path,expected non-empty string)defrequire_int(value,path):ifnotisinstance(value,int)orisinstance(value,bool):fail(path,expected integer)defcheck_request(data):require_object(data,request)require_string(data.get(model),request.model)messagesdata.get(messages)ifnotisinstance(messages,list)ornotmessages:fail(request.messages,expected non-empty list)allowed_roles{system,developer,user,assistant}forindex,messageinenumerate(messages):pathfrequest.messages[{index}]require_object(message,path)ifmessage.get(role)notinallowed_roles:fail(f{path}.role,unsupported role)require_string(message.get(content),f{path}.content)defcheck_response(data):require_object(data,response)require_string(data.get(id),response.id)ifdata.get(object)!chat.completion:fail(response.object,expected chat.completion)choicesdata.get(choices)ifnotisinstance(choices,list)ornotchoices:fail(response.choices,expected non-empty list)firstchoices[0]require_object(first,response.choices[0])require_int(first.get(index),response.choices[0].index)messagefirst.get(message)require_object(message,response.choices[0].message)ifmessage.get(role)!assistant:fail(response.choices[0].message.role,expected assistant)require_string(message.get(content),response.choices[0].message.content)usagedata.get(usage)require_object(usage,response.usage)forkeyin(prompt_tokens,completion_tokens,total_tokens):require_int(usage.get(key),fresponse.usage.{key})defmain():iflen(sys.argv)!3:print(usage: python3 contract_check.py REQUEST.json RESPONSE.json)return2requestjson.loads(Path(sys.argv[1]).read_text(encodingutf-8))responsejson.loads(Path(sys.argv[2]).read_text(encodingutf-8))try:check_request(request)print(PASS request contract)check_response(response)print(PASS response contract)print(SUMMARY contractpass)return0except(ValueError,json.JSONDecodeError)asexc:print(fFAIL{exc})print(SUMMARY contractfail)return1if__name____main__:raiseSystemExit(main())这里有两个刻意的选择。第一检查器把布尔值排除在整数之外因为 Python 中bool是int的子类直接使用isinstance(value, int)容易放过错误数据。第二检查器只验证客户端明确依赖的字段不会因为服务端增加了额外字段就失败。契约测试应该约束必需行为而不是把供应商扩展字段全部锁死。先跑通过路径再跑失败路径将下面的请求作为request.json。这只是脱敏 fixture不含真实用户问题、模型 Key 或线上数据{model:demo-model,messages:[{role:user,content:Return one short sentence.}]}对应的响应 fixture 可以是{id:chatcmpl-demo-001,object:chat.completion,created:1700000000,model:demo-model,choices:[{index:0,message:{role:assistant,content:A short sentence.},finish_reason:stop}],usage:{prompt_tokens:8,completion_tokens:4,total_tokens:12}}执行模板如下。/v1前缀和真实模型名应由读者替换本文没有用真实 Key 执行这条线上请求因此下面只展示“把线上响应保存后再检查”的方法BASE_URLhttps://example.com/v1curl-sS$BASE_URL/chat/completions\-HContent-Type: application/json\-HAuthorization: Bearer$OPENAI_API_KEY\-drequest.jsonresponse.json python3 contract_check.py request.json response.json本次实际执行的是本地 fixturePASS request contract PASS response contract SUMMARY contractpass然后把message.content删除模拟一个 HTTP 请求已经返回、但客户端真正需要的字段不存在的响应。实际失败信号是FAIL response.choices[0].message.content: expected non-empty string SUMMARY contractfail这比只检查 HTTP 200 更有价值。200 只能说明网络层和服务端愿意返回一个响应不能说明响应能被下游代码消费。失败路径应进入测试报告而不是在业务代码里继续用response[choices][0]触发一个难定位的KeyError或TypeError。如何区分鉴权失败和契约失败契约测试最好分成两段。第一段验证鉴权和连通性状态码、响应头、错误对象是否符合你们的错误处理约定。第二段只对成功响应执行字段契约检查。这样401/403 不会被错误地报告为“响应格式不兼容”而是明确归类为凭据、权限或环境问题。我在 2026-07-17 对 CODELINK 的公开/v1/models做了无凭据只读检查页面返回API_KEY_REQUIRED并说明需要在 Authorization、x-api-key或x-goog-api-key中提供认证信息。这次结果只证明鉴权边界。由于没有获得测试 Key本文不把它写成成功调用也不推断当前模型列表、性能或稳定性。失败排查顺序建议按下面顺序排查避免一上来就改 SDK状态码先看清楚。401/403 先查环境变量是否加载、Key 是否属于当前环境、权限是否足够429 需要记录限流信息5xx 则保留请求时间和服务端错误 ID。再检查 JSON 顶层。用python3 -m json.tool response.json确认响应不是 HTML 错误页或网关文本。再执行字段契约。确认choices非空首个消息角色和内容存在usage字段类型正确。最后才看扩展能力。如果基础契约通过再单独为流式、工具调用、图像和供应商扩展增加测试不把所有能力混在一次失败里。安全边界和适用范围不要把真实 Key 写进 fixture、截图、命令历史或文章正文。示例只使用demo-model和固定文本真实请求通过环境变量读取 Key并且只在后端或受控终端执行。文章配套文件不应包含请求头、真实用户输入、响应中的个人信息或可复用的邀请参数。契约测试不能证明模型回答质量也不能证明长时间稳定性、价格或并发上限。它解决的是更基础的问题当客户端要读取某个字段时接口是否返回了符合约定的结构。把这个检查放进 CI 或发布前手工清单能够让“兼容”从宣传语变成可比较、可回归的测试结果。本文的测试环境之一是作者维护的 CODELINK API 中转服务。这个关系披露不改变教程结论删除该服务名称后契约检查器、fixture 和排错方法仍然可以独立运行。总结OpenAI 兼容 API 的第一步不是选择哪个 SDK而是写出你真正依赖的最小契约。用 JSON Schema 的思路描述结构再用一份不依赖第三方包的检查器执行通过和失败都保存下来至少可以回答三个问题请求是否满足前置约束、响应是否真的能被客户端消费、失败属于鉴权问题还是结构问题。当这三件事有了可重复的成功信号和失败路径后续再扩展流式事件、工具调用和性能测试才不会把不同层次的问题混成一句“接口不兼容”。

相关新闻

Mocode-1B-Model

Mocode-1B-Model

Mocode-1B-Model 模型描述 Mocode 统一模型,集成 7 个 embeddingMLP 子模型,采用 GGUF v3 格式封装为单一文件。 子模型 前缀模型名任务类别数deepseekDeepSeekQualityClassifier数据质量分类2disclawDisclawClassifier法律任务分类10intentTrainedI…

2026/10/18 10:52:30 阅读更多 →
DeepSeek从抽象数学的角度对“如果生物神经网络(Biological Neural Network BNN)的数学模型是一个基于群节点的拓扑网络”做的解读以及两个思路

DeepSeek从抽象数学的角度对“如果生物神经网络(Biological Neural Network BNN)的数学模型是一个基于群节点的拓扑网络”做的解读以及两个思路

DeepSeek从抽象数学的角度对基于群节点的拓扑网络做的解读提问1:基于群节点的拓扑网络因为具有超级巨大的状态空间,因此它可以通过数学建模解决一个类似于哲学方面的问题,就是全体所有的人类,包括过去所有的人类、现在所有的人类&…

2026/10/19 15:42:29 阅读更多 →
3步掌握暗黑破坏神2存档编辑的终极开源工具指南

3步掌握暗黑破坏神2存档编辑的终极开源工具指南

3步掌握暗黑破坏神2存档编辑的终极开源工具指南 【免费下载链接】d2s-editor 项目地址: https://gitcode.com/gh_mirrors/d2/d2s-editor 你是否曾经在暗黑破坏神2中花费数小时刷装备却一无所获?或者因为一个错误的加点而不得不重新开始整个角色?…

2026/10/19 20:50:13 阅读更多 →

最新新闻

蓝牙AOA定位网关上报数据格式全解析:从字段含义到兼容排查

蓝牙AOA定位网关上报数据格式全解析:从字段含义到兼容排查

做蓝牙AOA定位的项目,拿到网关以后大家第一件事往往不是调算法,而是先搞清楚一个问题:网关上报的数据到底长什么样。很多项目在“跑通Demo”和“精准定位”之间反复横跳,根子就出在把上报数据格式理解错了,或者压根没理…

2026/10/20 7:15:57 阅读更多 →
分类模型评估指标详解:从混淆矩阵到PR/ROC曲线,告别准确率陷阱

分类模型评估指标详解:从混淆矩阵到PR/ROC曲线,告别准确率陷阱

1. 为什么在真实项目里不能只用准确率——先看清评价指标的作用1.1 一个看起来很漂亮的99.5%准确率我在刚开始做分类模型的时候,也犯过一个挺典型的错误:模型上线前汇报效果,习惯性先说“准确率到了多少”。有一次做欺诈交易检测,…

2026/10/20 7:15:57 阅读更多 →
为什么MySQL默认事务隔离级别是RR?InnoDB、MVCC与主从复制深度拆解

为什么MySQL默认事务隔离级别是RR?InnoDB、MVCC与主从复制深度拆解

做 DBA 或者后端开发久了,一定会被问过这个问题:为什么 MySQL 默认事务隔离级别是 RR(REPEATABLE READ,可重复读),而不是像 Oracle、PostgreSQL 那样用 RC(READ COMMITTED,读已提交&…

2026/10/20 7:15:57 阅读更多 →
STM32 SDIO+DMA双缓冲实战:高速稳定SD卡读写方案

STM32 SDIO+DMA双缓冲实战:高速稳定SD卡读写方案

简介:本资源是一份面向STM32嵌入式开发者的SD卡底层驱动实战工程,聚焦SDIO接口在1bit模式下结合DMA与中断的高效读写实现,适用于F4系列MCU(如STM32F407)的存储扩展开发与课程实验。工程完整包含HAL库驱动层、时钟与SDI…

2026/10/20 7:15:57 阅读更多 →
百度离线地图V3.0内网部署实践:从瓦片到坐标系的完整指南

百度离线地图V3.0内网部署实践:从瓦片到坐标系的完整指南

简介:基于百度地图JavaScript API V3.0构建的离线地图示例,面向无外网、弱网环境下的Web前端或GIS开发人员。压缩包内共1174个文件,以1093张JPG地图切片为主体,按缩放层级切分,配合初始化脚本、功能扩展模块、HTML示例…

2026/10/20 7:15:57 阅读更多 →
Pallas引擎如何看穿AI写作?从降AI到人机协同的正确路径

Pallas引擎如何看穿AI写作?从降AI到人机协同的正确路径

先说结论:2026年了,还在问“怎么降AI”的人,思路本身就反了。真正的答案不是研究怎么骗过Pallas引擎,而是研究为什么它盯上你。我把国内外主流AIGC检测工具,包括知网近期升级的Pallas引擎思路做了拆解,结合…

2026/10/20 7:14:56 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →