Results
The output and error file formats. One JSONL line per request, joined back to your inputs by custom_id.
When a batch reaches completed (or expired with partial work), it produces
up to two result files, referenced on the batch object:
output_file_id: successful requests, one line each.error_file_id: failed requests, one line each. Present only if at least one request failed.
Download either with the Files content endpoint:
curl https://inference.openrelay.inc/v1/files/file-batch_9f8e7d6c5b4a-output/content \
-H "Authorization: Bearer $OPENRELAY_API_KEY" \
-o results.jsonlCLI: orl files content get file-batch_9f8e7d6c5b4a-output -o results.jsonl.
Result line shape
Every line in both files is one result object, matching the OpenAI batch output shape:
| Field | Type | Description |
|---|---|---|
id | string | A per-result id (batch_req_<custom_id>). |
custom_id | string | The custom_id from the matching input request. Use it to join results back to inputs. |
response | object or null | On success: { status_code, body }, where body is the full endpoint response (e.g. a chat completion). Null on failure. |
error | object or null | On failure: { code, message, status_code, body }. Null on success. |
Exactly one of response and error is set on each line. The result files are
not ordered to match the input file; always join by custom_id.
Output file line (success)
{
"id": "batch_req_req-1",
"custom_id": "req-1",
"response": {
"status_code": 200,
"body": {
"id": "chatcmpl-abc123",
"object": "chat.completion",
"model": "openrelay/gpt-oss-120b",
"choices": [
{ "index": 0, "message": { "role": "assistant", "content": "Red, blue, and yellow." }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 17, "completion_tokens": 8, "total_tokens": 25 }
}
},
"error": null
}Output file line, tool-calling record
A record submitted with openrelay.tool_config
whose model actually tool-called carries one extra field in response.body:
{
"id": "batch_req_req-2",
"custom_id": "req-2",
"response": {
"status_code": 200,
"body": {
"id": "chatcmpl-def456",
"object": "chat.completion",
"model": "openrelay/gpt-oss-120b",
"choices": [
{ "index": 0, "message": { "role": "assistant", "content": "The invoice total is $4,210." }, "finish_reason": "stop" }
],
"usage": { "prompt_tokens": 2140, "completion_tokens": 96, "total_tokens": 2236 },
"openrelay": { "tool_rounds": 3 }
}
},
"error": null
}| Field | Type | Description |
|---|---|---|
openrelay | object | Namespace for OpenRelay response extensions. Ignore any key you do not recognize: this object is additive, and new keys may appear without a breaking change. The request side uses the same namespace: body.openrelay.tool_config is where you ask for tool calling. |
openrelay.tool_rounds | integer | How many tool round-trips the worker ran for this record, so the answer came from tool_rounds + 1 model turns. |
Where it appears:
- Only on output lines, inside
response.body, and only when at least one tool round ran. Anopenrelay.tool_configrecord whose model answered immediately is byte-for-byte identical to a record with notool_configat all. - Never on error lines, and never on a record without
openrelay.tool_config. - Alongside a
usageobject that is summed across every turn of the record, which is what you are billed for.tool_roundsis how you tell a legitimately expensive agentic record from an anomalous one.
The field is on the response body only. openrelay.tool_config itself is
stripped before every model invocation and never appears in either result
file, and an openrelay object left empty by that strip is removed with it.
Request and response now share one namespace word: you ask under
body.openrelay, and you read the answer's metadata under
response.body.openrelay. A record still carrying the retired bare top-level
tool_config fails with tool_config_moved; see the migration note in
Tool calling in a batch.
Error file line (failure)
{
"id": "batch_req_req-7",
"custom_id": "req-7",
"response": null,
"error": {
"code": "invalid_request_error",
"message": "messages: field required",
"status_code": 400
}
}A request whose 24-hour window elapsed before it ran gets an error line with
code: "batch_expired". A record that fails validation (for example one with
"stream": true, which is not allowed in a batch) also lands in the error
file rather than failing the whole batch.
Reconciling usage
Each successful response.body carries its own usage object, billed exactly
like an online request but at the batch rate. For a record that tool-called,
that usage is the sum across all tool_rounds + 1 turns, so it is the whole
record's cost in one place rather than the last turn's. The batch object also
exposes a rolled-up usage (input_tokens, output_tokens, cost_nano_usd)
so you can read total spend without summing every line. See
Pricing & billing.