OpenRouter 的批量 API:什么任务适合走批量

2026-08-18

有一类活儿是这样的:手上攒了一堆文案要打标签,或者要给一个知识库里的段落批量生成摘要,跑完之后你也不会盯着屏幕等——今晚提交,明天上班看结果就行。这种活儿如果按同步接口一条一条发,你就得自己写并发控制、自己处理重试、自己把结果和输入对回去。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_imageinput_file 片段,以及 Anthropic 形状的 imagedocument 块。在 /v1/chat/completions 上,校验还会拒绝那些通过 modalitiesaudioimage_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 体里 endpointmodel 必须序列化在 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

除此之外还有 failedexpiredcancellingcancelled。文档明确点出终态一共四个:completedfailedexpiredcancelled,并要求你一直轮询到批次进入终态为止。注意 cancelling 不是终态——它是取消过程中的中间态,写状态机时别把它当结束条件。

request_counts 里是这批的总数,以及已完成、已失败的条数:

{
  "total": 100,
  "completed": 98,
  "failed": 2
}

这里是本文最该记住的一处设计:没有单独的结果下载端点。文档原话是,批次进行中、或者失败、过期、被取消时,resultsnull;批次完成后,results 会作为一个数组内联在同一个响应里返回。所以你不需要去找什么 output file id,拿到 completed 的那次 GET 响应本身就带着全部结果。

结果与输入的对应关系靠 custom_id。每一条结果里,responseerror 恰好只有一个会被填上:

{
  "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 天删除,与上游的批处理保留窗口一致;需要的结果请在窗口到期前下载走。这个窗口以官方文档最新说明为准。

六、怎么验证你配对了

按顺序确认这四件事,基本就不会在半路上打转:

  1. 提交这一步:看 HTTP 状态码是不是 202,响应里的 status 是不是 "validating"id 有没有拿到。如果收到 400,先回头查 JSON 里 endpointmodel 是不是排在 requests 前面——文档把这个顺序问题和 400 直接对上了。
  2. 轮询这一步:拿 id 打 GET,确认状态在往前走,并且你的循环退出条件覆盖了四个终态,而不是只判断 completed;只判断 completed 的循环遇到 failedexpired 会一直转下去。
  3. 数量对账:批次到终态后,比对 request_counts 里的 total 与你提交的条数,再看 completedfailed 的分布。有 failed 时不要整批重跑,逐条看结果里的 error
  4. 逐条落库:遍历 results,按 custom_id 回连你自己的记录;每条只取 responseerror 中被填上的那一个。要报某次生成的问题,就留存 response.body.id

最后提醒一句常识性的:这一页的端点路径里带着 beta,字段与状态集合也可能随平台调整,写死之前请对照官方文档最新内容再核一遍。


本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。 该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。 该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单; 价格、额度与限流的具体数值请以官方定价页与用量说明为准。

安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。