在 OpenRouter 上要结构化输出:不是所有路由路径都支持
写业务代码的人对模型输出的要求往往很朴素:给我一个字段固定的 JSON,我 JSON.parse 完直接往下走。真到了接口上,麻烦通常不是”模型不会输出 JSON”,而是你昨天跑通的那套请求,今天同一个模型换了条路由路径就不灵了。在 OpenRouter 上这件事尤其容易发生,因为它把”模型”和”实际执行请求的 endpoint”拆成了两层,而结构化输出的支持与否,官方文档写明是按后者判定的。
这篇只讲一件具体的事:schema 怎么声明,以及不支持的时候会表现成什么样。
一、先看这个能力解决什么
OpenRouter 官方文档《Structured Outputs》页(openrouter.ai/docs/guides/features/structured-outputs)开头把这个能力的用途列成了四条:对模型响应强制执行指定的 JSON Schema 校验、拿到一致且类型安全的输出、避免解析错误与被凭空编出来的字段、简化应用里的响应处理逻辑。
再往上一层看,openrouter.ai/docs/api_reference/overview 那一页把 response_format 归纳成两种模式:
{ type: 'json_object' }——文档称之为基础 JSON 模式,模型会返回合法 JSON{ type: 'json_schema', json_schema: { ... } }——文档称之为严格 schema 模式,模型返回匹配你给定 schema 的 JSON
这两者的差别不是程度问题,是”只保证是 JSON”和”保证长成你要的样子”的差别。本文讲的结构化输出指后者。另外 openrouter.ai/docs/api_reference/parameters 那一页对 json_object 有一句提醒值得单独记住:使用 JSON 模式时,你还应当自己通过 system 或 user 消息指示模型产出 JSON。
二、前置条件:先确认这条路走得通
这一段最容易被跳过,但它恰好是这个能力最容易翻车的地方。
第一,模型层面要支持。 openrouter.ai/docs/guides/overview/models 那一页在讲模型信息里的 supported_parameters 数组时,把 structured_outputs 单列为一项,说明是”JSON schema enforcement”,同时 response_format 是另外一项。也就是说这两个键在模型元数据里是分开的,别把”支持 response_format”当成”支持 schema 强制”。openrouter.ai/docs/api_reference/parameters 里对 structured_outputs 这个键的定义写得更直白:布尔值,表示该模型是否能用 response_format 的 json_schema 返回结构化输出。
第二,也是最关键的一条——支持是按 endpoint 判定的,不只按模型判定。 《Structured Outputs》页原话的意思是:同一个模型可能由多个 provider 提供服务,其中只有部分 provider 支持结构化输出;而且 endpoint 的支持情况会随时间变化。文档给出的确认动作是,去该模型页面的 Providers 一节里看 structured_outputs 这个参数。
这一条解释了本文开头那个场景:模型没换、代码没改,只是这次请求被路由到了另一个 endpoint,行为就变了。文档还提到模型列表页支持按 supported_parameters=structured_outputs 过滤,但要注意那是模型级的筛选,粒度比 endpoint 粗。
如果你更习惯用程序而不是页面来确认,OpenRouter 的 TypeScript SDK 文档(openrouter.ai/docs/client-sdks/typescript/sdks/endpoints/README)里有一个 endpoints.list 方法,说明是”List all endpoints for a model”,入参是 author 和 slug。它返回什么字段官方文档在这一页没有逐一展开,但至少给了你一个把”这个模型现在有哪些 endpoint”程序化拉下来的入口。
第三,密钥。 文档里的示例统一用 <OPENROUTER_API_KEY> 这样的占位,请求头是 Authorization: Bearer <YOUR_API_KEY>。别把密钥写进代码库。
三、schema 到底怎么声明
《Structured Outputs》页写明:在请求里加 response_format 参数,type 设为 json_schema,json_schema 对象里放你的 schema。文档给的请求体原文如下:
{
"messages": [
{ "role": "user", "content": "What's the weather like in London?" }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "weather",
"strict": true,
"schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City or location name"
},
"temperature": {
"type": "number",
"description": "Temperature in Celsius"
},
"conditions": {
"type": "string",
"description": "Weather conditions description"
}
},
"required": ["location", "temperature", "conditions"],
"additionalProperties": false
}
}
}
}
拆开看,json_schema 这一层有三个键:name(这个 schema 的名字)、strict、以及真正的 schema 本体。schema 内部就是标准 JSON Schema 的写法——type、properties、required、additionalProperties。文档在”Best Practices”里给了两条建议,第一条就是给 schema 的属性加清楚的 description 来引导模型,上面示例里每个属性都带 description 不是装饰。
一个容易踩的差异:如果你用官方 TypeScript SDK 而不是裸 HTTP,字段名是驼峰的。文档的 SDK 示例里写的是 responseFormat 和 jsonSchema,而 Python 与 fetch 示例里是 response_format 和 json_schema。同一份文档里两种写法并存,照着哪段抄就用哪段的拼法,别混。
想要流式,《Structured Outputs》页说结构化输出同样支持流式响应,模型会流出合法的部分 JSON,拼完后构成符合你 schema 的完整响应;开启方式就是在请求里加 stream: true。
四、边界:strict: true 没有你以为的那么硬
这是全篇最需要照原文读的一段。文档”Best Practices”第二条建议设 strict: true,让那些具备原生 strict 模式的 provider 精确执行你的 schema,但紧接着写明:执行程度因 provider 而异——有的能保证输出符合 schema,有的会把你的 schema 转译成它自己的结构化输出格式,还有的只把它当作一个强提示,所以并非每个 endpoint 都能保证严格符合。文档还提示,strict 模式可能限制你能使用哪些 JSON Schema 特性,细节要看对应 provider 的文档。
换句话说,strict: true 是一个请求意图,不是一个跨 provider 的统一保证。你的解析代码仍然要能承受”字段缺了”或”多了个字段”。
那不支持的时候会怎样?文档”Error Handling”一节列了两种场景:其一,模型不支持结构化输出,请求会失败并返回一个指明缺乏支持的错误;其二,schema 本身不合法,模型会返回错误。文档在这一页没有给出这两种错误的具体错误码与错误体字段——官方文档没有说明这一点,所以别按猜测去写 if (error.code === ...) 的分支,按你实际收到的响应来写。
要让请求只落到支持结构化输出的 endpoint 上,《Structured Outputs》页给了三步:先在模型页确认该模型的 supported parameters;再在 provider preferences 里设 require_parameters: true;最后把 response_format 和 type: json_schema 放进请求里作为必需参数。
require_parameters 的语义在《Provider Routing》页(openrouter.ai/docs/guides/routing/provider-selection)的字段表里写明:布尔值,默认 false,含义是”只使用支持你请求中全部参数的 provider”。同一页展开解释了默认策略下的行为:不支持你请求中某些 LLM 参数的 provider 依然可能收到这个请求,只是会忽略它不认识的参数;把 require_parameters 设成 true 之后,请求就根本不会被路由到那个 provider。“参数被静默忽略”正是你拿到一个自由发挥的文本而不是报错的原因之一。
同一页还有一段”Default parameter preferences”值得单独拎出来:即使 require_parameters 是 false,也有一小组参数会作为软偏好参与 provider 选择,tools、response_format(文档明确括注包含结构化输出)和 verbosity 在列。如果某个模型的部分 provider 支持而另一部分不支持,请求只会路由到支持的那些;但如果这个模型的所有 provider 都不支持,请求仍然会被路由到该模型,参数被忽略——文档写明这个偏好永远不会把某个模型从候选列表里剔除。这解释了一个反直觉的现象:不加 require_parameters 时,OpenRouter 已经在替你偏好支持 response_format 的 provider 了,但这个偏好不是硬约束,兜不住”所有 provider 都不支持”的情况。
把这两页拼起来,一个同时带 schema 与硬约束的请求体大致是这个形状:
{
"messages": [{ "role": "user", "content": "Hello" }],
"provider": {
"require_parameters": true
},
"response_format": {
"type": "json_schema",
"json_schema": { "name": "weather", "strict": true, "schema": { } }
}
}
以上为按官方文档中的参数语义组合的示例,未经实测,以官方文档与 --help 的实际输出为准。(《Provider Routing》页那段 require_parameters 的原始示例配的是 response_format: { type: 'json_object' },json_schema 的部分来自《Structured Outputs》页。)
还有一个专门针对”格式不干净”的兜底:Response Healing 插件(openrouter.ai/docs/guides/features/plugins/response-healing)。它的启用方式是在请求里加 plugins 数组:
"plugins": [
{ "id": "response-healing" }
]
文档写明它在非流式请求且使用了 response_format 的 json_schema 或 json_object 时激活,能修补缺括号、尾逗号、键没加引号、被 markdown 代码块包住、JSON 前面混了一段说明文字这类问题。它的两条限制文档用 Warning 框标了出来:只作用于非流式请求;并非所有畸形 JSON 都能修复,特别是当响应被 max_tokens 截断时,插件修不了。也就是说,第三节里那个加 stream: true 的流式写法,和这个插件是互斥的,你得二选一。
五、怎么验证配对了
按上面的顺序,验证也分三层,别跳着做:
- 请求层:确认你发出去的 body 里
response_format.type确实是json_schema,且json_schema下有name与schema。用 SDK 时确认拼法是responseFormat/jsonSchema,用裸 HTTP 时是下划线。这一层错了,后面全白搭。 - 路由层:加上
provider.require_parameters: true再发一次。按文档语义,如果这次直接失败而不是返回一段自由文本,说明问题出在”被路由到了不支持的 endpoint”;如果加不加都正常,说明路由这一层不是瓶颈。 - 输出层:拿返回的
choices[0].message.content走一次你自己的 schema 校验(用 Ajv、Pydantic 之类的都行,这是通用做法,不是 OpenRouter 官方文档的内容)。别只看它是不是能JSON.parse——第四节说了,strict在不同 provider 上的执行程度不一样,能 parse 不等于字段齐。
关于环境变量,Linux / macOS 的 shell 里通常写 export OPENROUTER_API_KEY=<YOUR_API_KEY>,Windows PowerShell 里则是 $env:OPENROUTER_API_KEY = "<YOUR_API_KEY>",cmd 里是 set OPENROUTER_API_KEY=<YOUR_API_KEY>;这属于各自 shell 的通用差异,不是 OpenRouter 官方文档写的内容,官方示例只是统一用了 <OPENROUTER_API_KEY> 这样的占位。在 Windows 上还有一个和本文相关的实际问题:JSON 请求体里的引号在 cmd 与 PowerShell 里的转义规则各不相同,建议把请求体写进文件或直接用 SDK,别在命令行里手拼那一大段 schema。
最后提醒一句本文最想让你记住的事:这个平台上”支持结构化输出”从来不是一个模型的静态属性,文档自己写了 endpoint 的支持情况会随时间变化。所以把 require_parameters 这类硬约束写进代码,比把某个模型能用这件事记在脑子里可靠得多。该平台迭代频繁,以官方文档最新内容为准。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。