OpenRouter 的批量 API:什么任务适合走批量
有一类活儿是这样的:手上攒了一堆文案要打标签,或者要给一个知识库里的段落批量生成摘要,跑完之后你也不会盯着屏幕等——今晚提交,明天上班看结果就行。这种活儿如果按同步接口一条一条发,你就得自己写并发控制、自己处理重试、自己把结果和输入对回去。OpenRouter 的 Batch API 就是给这类场景准备的:官方文档《Batch API Quickstart》一开头写明,它适合「不需要即时响应」的工作,并且使用一个 24 小时的完成窗口,让你不必逐个调用去管理每一次请求。
这篇只讲一件具体的事:批量任务怎么提交、结果怎么拿回来。所有依据都来自 OpenRouter 官方文档的 openrouter.ai/docs/batch-quickstart 这一页。
一、先判断你的任务能不能走批量
在动手写代码之前,先看两条硬门槛,不然你会在提交阶段就被挡回来。
第一条:目前是纯文本的。 文档在 Limitations 一节写得很直接——Batch API 当前是 text-only。在 /v1/chat/completions、/v1/responses、/v1/messages 这三种形状上,校验会拒绝任何携带图像、音频、视频或文件内容片段的请求,其中点名了 Responses 形状的 input_image 与 input_file 片段,以及 Anthropic 形状的 image 与 document 块。在 /v1/chat/completions 上,校验还会拒绝那些通过 modalities、audio、image_config 要求非文本输出的请求。对 embeddings 来说,input 必须是字符串或 token 数组。文档给的处置办法只有一句:多模态请求改走同步 API。
第二条:结果是异步的,别按同步接口的心智去接。 文档特意加了一条提示:提交成功返回的是 202 Accepted,并且 status 是 "validating"。这意味着 OpenRouter 已经把这个批次持久化并排进了校验队列,不代表每一条请求都已经跑完。如果你的调用方在拿到 200 就去读结果,那这段代码需要改。
所以「什么任务适合走批量」的答案,按文档能核到的部分是:纯文本、可以容忍到次日、并且你愿意用轮询而不是等返回的任务。
二、前置条件
- 一个 OpenRouter API 密钥,按文档示例放在
Authorization: Bearer请求头里。 - 想清楚整批要用哪种 API 形状。
endpoint是批次级字段,一批只能用一种。 - 如果你配置了 BYOK 的 provider key,文档写明批次会像同步请求一样自动走它:由供应商直接向你收取推理费用,OpenRouter 只收 BYOK 费用,完成的批次会带上
usage.is_byok: true。这里有一个容易被漏掉的额外条件——文档写明 Google Vertex 需要在密钥上配置bucket,还需要额外的 IAM 权限,具体要求指向 BYOK 那一页的 Google Vertex 小节。 - 计费口径这一层,文档说明批量请求相对标准价另有折扣,同时提醒非 token 的计费项并没有统一打折(例如 web-search 调用按标准费率计费,prompt-caching 的费率随模型不同),并写明每个模型页上显示的定价才是准的。本文不写任何具体数值,请以官方定价页为准。
三、提交:三个必填字段,以及一个反直觉的顺序要求
提交端点是:
POST https://openrouter.ai/api/beta/batches
注意路径里带 beta 这一段,这是官方文档给出的写法,原样抄,别自作主张改成别的前缀。
请求体有三个必填的顶层字段:
| 字段 | 说明 |
|---|---|
endpoint | 这一批里每条请求使用的 API 形状,可选 /v1/chat/completions、/v1/responses、/v1/messages、/v1/embeddings |
model | 一个 OpenRouter 的 model slug,这个批次级的模型会应用到每一条请求上 |
requests | 非空数组,元素是 { custom_id, body }。custom_id 在同一批次内必须唯一,body 遵循所选 endpoint 的形状 |
有两处是文档里明说、但按常识很难想到的:
其一,字段的序列化顺序有要求。 文档用 Warning 框写明:JSON 体里 endpoint 和 model 必须序列化在 requests 之前。原因文档自述是——API 对请求做流式解析,这样才能在不缓冲的情况下接收非常大的 requests 数组;如果 requests 出现在前面,会返回 400。文档也说明该页上所有示例都已经按这个顺序写。这条对用 Python 字典或 JS 对象拼 JSON 的人尤其要留意:多数序列化器按插入顺序输出,所以你构造对象时的写法顺序就是最终的字节顺序。
其二,请求级的 model 只能省略或者写成一样的。 文档写明批次级 model 应用于每条请求,单条 body 可以省略 model 来继承批次级的值;但如果单条 body 自己写了 model,它必须与批次级的 model 一致,否则整个提交会被拒绝。也就是说,Batch API 不是让你在一批里混着跑多个模型的工具。
官方文档给出的 Shell 示例(原样抄录):
curl https://openrouter.ai/api/beta/batches \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d '{
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"requests": [
{
"custom_id": "req-0001",
"body": {
"messages": [
{
"role": "user",
"content": "Summarize OpenRouter in one sentence."
}
]
}
}
]
}'
示例里的 openai/gpt-4o 只是官方文档当时用的示例值,平台上有哪些模型随时在变,别把它当清单用。文档同时给了 Python(requests 库 + json.dumps)和 TypeScript(fetch + JSON.stringify)两个等价写法。
Windows 侧提醒(这一段是通用的命令行常识,不是 OpenRouter 官方文档内容):上面这条 curl 用单引号包住整段 JSON,这是 Linux/macOS 的 shell 写法。在 Windows 的 cmd 与 PowerShell 里,单引号的处理规则不一样,直接照抄大概率会解析失败;把 JSON 存成文件再用 -d @batch.json 传是更省事的做法,或者干脆用文档里的 Python / TypeScript 版本。另外环境变量的取法也不同:Linux/macOS 是 $OPENROUTER_API_KEY,PowerShell 里要写 $env:OPENROUTER_API_KEY。密钥请自己用占位替换(如 <YOUR_API_KEY>),别写进提交到仓库的脚本里。
提交成功后返回的是一个批次对象,里面带着后续要用的 ID:
{
"id": "batch_123",
"object": "batch",
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"completion_window": "24h",
"status": "validating",
"created_at": 1782097200,
"finalized_at": null,
"request_counts": {
"total": 1,
"completed": 0,
"failed": 0
},
"usage": null,
"results": null,
"error": null
}
completion_window 这个字段值得单独说一句:文档写明唯一支持的完成窗口就是 24h,没有别的档位可选。
四、取结果:轮询同一个 ID,结果内联返回
用批次 ID 查当前状态:
GET https://openrouter.ai/api/beta/batches/:id
curl https://openrouter.ai/api/beta/batches/batch_123 \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
文档给出的正常状态流转是:
validating → in_progress → finalizing → completed
除此之外还有 failed、expired、cancelling、cancelled。文档明确点出终态一共四个:completed、failed、expired、cancelled,并要求你一直轮询到批次进入终态为止。注意 cancelling 不是终态——它是取消过程中的中间态,写状态机时别把它当结束条件。
request_counts 里是这批的总数,以及已完成、已失败的条数:
{
"total": 100,
"completed": 98,
"failed": 2
}
这里是本文最该记住的一处设计:没有单独的结果下载端点。文档原话是,批次进行中、或者失败、过期、被取消时,results 是 null;批次完成后,results 会作为一个数组内联在同一个响应里返回。所以你不需要去找什么 output file id,拿到 completed 的那次 GET 响应本身就带着全部结果。
结果与输入的对应关系靠 custom_id。每一条结果里,response 和 error 恰好只有一个会被填上:
{
"id": "batch_req_123",
"custom_id": "req-0001",
"response": {
"status_code": 200,
"request_id": "request_123",
"body": {
"id": "gen-batch-1782097200-a1b2c3d4e5f6a7b8c9d0",
"object": "chat.completion",
"created": 1782097200,
"model": "openai/gpt-4o",
"choices": [ ]
}
},
"error": null
}
(上面的 choices 内容做了省略,完整示例见官方文档该页。)
这也解释了为什么 custom_id 必须在批内唯一:它是你唯一的回连键。提交前就把它和你自己的业务主键绑好,别指望靠数组下标对回去。
另外记一个排障用的字段:每条完成结果的 response.body.id 就是这条请求的 OpenRouter generation ID(形如 gen-batch-...)。文档写明,要反馈某次生成有问题,就复制这个 ID,通过 Report Feedback 的 By generation ID 流程提交;并注明生成级反馈适用于 /v1/chat/completions、/v1/responses、/v1/messages 这几种形状。
五、边界:文档明说不支持的部分
- 不能在一批里混形状。 文档写明一个批次里所有请求共用同一个顶层
endpoint,要混用不同 API 形状就分批提交。顺带一提,endpoint字段的表格里列了四个可选值(含/v1/embeddings),而后面「Use different API shapes」一节只列了前三种,embeddings 单独成节讲;那一节里文档写明的做法是把顶层endpoint设为/v1/embeddings,也就是说这两处讲的是同一个字段。 - embeddings 尚在铺开中。 文档原文是「Embeddings are rolling out on providers that support them」,即在支持的供应商上逐步开放,不是已经全量。同时写明:多模态输入、
input_type、以及provider偏好在 Batch API 上不支持,需要这些就走同步 API。embeddings 的input可以是单个字符串或字符串数组,数组时该条请求会在一次调用里嵌入每个字符串,返回的data里按index排序、每个字符串一个 embedding 对象。 - Google 模型对
response_format有额外的一致性要求。 文档写明:在 Google 模型上,一批里每条请求必须要求相同的response_format——要么全部省略,要么全部用json_object,要么全部用json_schema且 schema 相同。文档自述的原因是 Google 的批处理服务会为整批推导出一个输出 schema,因此互相冲突的请求会在那边失败。校验会让不一致的批次失败,并指出第一条冲突的请求;文档给的做法是按response_format、按 schema 各自分批。 - 不需要你自己传 JSONL。 文档写明请求是以内联 JSON 的
requests数组提交的,你不上传 JSONL 文件,JSONL 的持久化由 OpenRouter 内部处理。如果你手上的代码是按「先上传文件、再拿 file id 引用」的流程写的,迁移到这里时这就是主要的改动点:文件上传那一整段可以去掉,直接把数组拼进请求体。 - 结果有保留期。 文档在末尾提示,批次的输入与结果以 JSONL 制品的形式存放在 Google Cloud Storage,并在创建后 30 天删除,与上游的批处理保留窗口一致;需要的结果请在窗口到期前下载走。这个窗口以官方文档最新说明为准。
六、怎么验证你配对了
按顺序确认这四件事,基本就不会在半路上打转:
- 提交这一步:看 HTTP 状态码是不是
202,响应里的status是不是"validating",id有没有拿到。如果收到400,先回头查 JSON 里endpoint和model是不是排在requests前面——文档把这个顺序问题和400直接对上了。 - 轮询这一步:拿
id打 GET,确认状态在往前走,并且你的循环退出条件覆盖了四个终态,而不是只判断completed;只判断completed的循环遇到failed或expired会一直转下去。 - 数量对账:批次到终态后,比对
request_counts里的total与你提交的条数,再看completed与failed的分布。有failed时不要整批重跑,逐条看结果里的error。 - 逐条落库:遍历
results,按custom_id回连你自己的记录;每条只取response与error中被填上的那一个。要报某次生成的问题,就留存response.body.id。
最后提醒一句常识性的:这一页的端点路径里带着 beta,字段与状态集合也可能随平台调整,写死之前请对照官方文档最新内容再核一遍。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。