Skip to content
Zabio is in beta: models, prices and features can change. Tell us what breaks at hello@zabio.ai.

API reference

This page lists every endpoint Zabio serves, field by field, with a runnable example for each in curl, Python and Node. The other pages explain the ideas: Models and routing for the three lines and how a job is classified, Chat completions for images, tools and structured output, Responses API and conversations for stored conversations, Errors for every error code. Come here when you need the exact shape of a request or a response.

Every field name, header and code on this page comes from the code. A machine readable version of the same routes is in openapi.yaml.

Host Use
https://api.zabio.ai/v1 default; resolves to the US region
https://api-us.zabio.ai/v1 the US region by name

The Python and Node examples below use the openai package with base_url set to the first host. Everything else in them is a normal SDK call.

from openai import OpenAI
client = OpenAI(base_url="https://api.zabio.ai/v1", api_key="insert your zabio.ai key here")
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.zabio.ai/v1", apiKey: "insert your zabio.ai key here" });

Send your API key as a bearer token. Keys look like zb_live_ followed by 43 letters and digits.

Authorization: Bearer zb_live_...

Zabio also reads the key from an x-api-key header when there is no Authorization header, for SDKs that send it that way. All /v1 routes need a key, including GET /v1/models, except the public ones: GET /v1/status, POST /v1/public/analyze, GET /v1/public/free-tier, GET /v1/public/plan, GET /v1/public/auto-models and GET /v1/public/catalog-export (the model tables of these docs, as the API serves them).

Status code When
401 invalid_api_key the key is missing, incorrect or revoked. The message says which. A missing key also returns WWW-Authenticate: Bearer realm="zabio"
403 organization_suspended the key is valid but its organization is suspended
403 insufficient_scope the key lacks the scope for this endpoint
503 auth_unavailable the key could not be checked right now

Keys carry scopes. New keys have all three.

Scope Endpoints
chat POST /v1/chat/completions and, together with conversations, POST /v1/responses
embeddings POST /v1/embeddings
conversations everything under /v1/responses and /v1/conversations

The version is the /v1 prefix in the path. There is no version header. Zabio adds fields to OpenAI’s objects (the zabio object on every response, zabio on model objects) rather than changing existing ones. Read the fields you need and ignore the rest.

Every response carries an x-request-id header of the form req_ followed by 32 hex characters. Zabio assigns it; an X-Request-Id you send is not reused. Quote it when you write to support.

The key’s two per minute buckets are reported on every response from /v1/chat/completions, /v1/embeddings and /v1/responses:

Header Meaning
x-ratelimit-limit-requests requests per minute allowed for this key (default 600)
x-ratelimit-remaining-requests requests left in the current minute
x-ratelimit-reset-requests time until the bucket is full again, as 12s or 500ms
x-ratelimit-limit-tokens tokens per minute allowed for this key (default 200000)
x-ratelimit-remaining-tokens tokens left. The bucket is charged with an estimate (input tokens plus max_tokens) at admission and refunded with the real count after the call
x-ratelimit-reset-tokens time until the token bucket is full again
Retry-After on 429, whole seconds to wait (at least 1). Also 5 on 502, 503 and 504 when a retry makes sense

Two more limits have their own headers:

Header Meaning
x-zabio-quota-scope, x-zabio-quota-limit, x-zabio-quota-remaining present when a spend limit applies. scope is key (the key’s own limit, over its reset period: a day, a week from Monday or a month from 00:00 UTC, or the key’s whole life) or member (the monthly quota of the member who created the key). Values are in micros (millionths of a US dollar)
x-ratelimit-limit-ip, x-ratelimit-remaining-ip, x-ratelimit-reset-ip the per address limit, checked before authentication. Sent only on the 429 ip_rate_limit_exceeded refusal, never on normal responses

The Free plan (see Billing, “The Free plan”) adds its own:

Header Meaning
x-zabio-plan free on every call served under the Free limits; absent on a paid call
x-ratelimit-limit-requests-day, x-ratelimit-remaining-requests-day, x-ratelimit-reset-requests-day the Free plan’s day: 100 successful requests, what is left, and the time to 00:00 UTC. Only with x-zabio-plan: free; on such calls x-ratelimit-*-requests is the Free minute bucket (20), which is tighter than the key’s own
x-zabio-free-limit on a 429 or 403 free_tier_limit, which limit was met: requests_per_day, requests_per_minute, daily_cost, input_tokens, max_tokens, members or api_keys
Header Meaning
x-zabio-vendor the vendor that served the call (anthropic, openai, google, deepseek, xai, mistral, together, typesafe); also set on vendor errors to name the vendor that failed. Absent on a call kept alive past 90 seconds (see Long calls): its headers went out before the vendor was chosen, and the body’s zabio object carries the vendor, the model and the routing
x-zabio-byok true when your organization’s own vendor key served the call
x-zabio-balance-micros your organization’s credit balance after the admission check, when a balance check ran

Zabio has no idempotency keys. A repeated POST is a new call and is billed again. Retry on 429, 502, 503 and 504 after Retry-After; do not retry a 200 you did not receive without checking your own records first.

Request bodies are limited to 32 MB (413 payload_too_large). Other limits are listed with each endpoint.

Every error is JSON in OpenAI’s shape, so SDKs raise their usual exception types:

{"error": {"message": "'temperature' must be between 0 and 2.", "type": "invalid_request_error", "param": "temperature", "code": "invalid_value"}}

type is invalid_request_error, permission_error, billing_error, rate_limit_error or server_error. param is present when a field is at fault. The full code list is on the Errors page; each endpoint below lists the codes specific to it.

Lists the four auto models followed by the catalog, priced for your organization. Keys with an allow list only see their models. No scope is needed beyond a valid key.

Response: {"object": "list", "data": [Model, ...]}. A Model is OpenAI’s model object plus a zabio object:

Field Type Notes
id string auto, auto/best, auto/balanced, auto/cheap or a catalog id
object string model
created integer the same launch timestamp for every model
owned_by string zabio for the auto models, else the vendor: anthropic, openai, google, deepseek, xai, mistral, together, typesafe
zabio.tier string the model’s class: fast, balanced, powerful, frontier. A label for the pages, not a routing rule. For the auto ids it is the line
zabio.auto_eligible boolean whether auto can pick it today: it has a skill score for at least one kind of work, or the earlier rule can choose it for a request of kind other (see Models and routing). It follows the daily skill data, the same list as GET /v1/public/auto-models
zabio.named_only boolean the opposite of auto_eligible: the model is reached by naming it; omitted when false
zabio.virtual boolean true on the auto models only
zabio.embedding boolean true on embedding models only
zabio.capabilities object chat models only: vision, tools, json_schema (booleans), context_window, max_output (tokens)
zabio.pricing object input, output, cache_read, cache_write in USD per million tokens with your plan’s fee included, currency (USD). cache_write is the 5 minute rate
zabio.residency string where inference may run, used by routing: global for most models, us for models served only from the United States (today the xAI models)
zabio.location string where the vendor processes the request, as shown on the Models page (for example United States, China); with zabio.label, the display name when the id alone is ambiguous
zabio.free_tier boolean true on the models of the free model list (see GET /v1/public/free-tier)
zabio.availability string available or locked: whether this key may name the model right now. locked only while the organization runs on the Free limits and the model is not on the free list; the auto ids are always available. Absent for a paid organization with credit

curl

Terminal window
curl https://api.zabio.ai/v1/models \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

models = client.models.list()
for m in models.data:
z = m.model_extra.get("zabio", {})
print(m.id, m.owned_by, z.get("auto_eligible"), (z.get("pricing") or {}).get("input"))

Node

const models = await client.models.list();
for (const m of models.data) console.log(m.id, m.owned_by, m.zabio.auto_eligible, m.zabio.pricing?.input);

Returns one model object. Virtual ids with a slash work: GET /v1/models/auto/cheap and the percent encoded GET /v1/models/auto%2Fcheap are both accepted.

Errors: 404 model_not_found when the id is unknown, switched off for your organization or missing from the key’s allow list.

curl

Terminal window
curl https://api.zabio.ai/v1/models/claude-sonnet-5 \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

m = client.models.retrieve("auto/cheap")
print(m.id, m.model_extra["zabio"])

Node

const m = await client.models.retrieve("auto/balanced");
console.log(m.id, m.zabio);

Public, no key: the free model list in force and the Free limits, for a pricing page or a client that wants to know what a Free account gets before signing up. Cache-Control: public, max-age=60. The fair-use limit is reported as existing, never with its amount. See Billing, “The Free plan”.

{
"models": ["deepseek-flash@together", "gemini-3.5-flash-lite", "gpt-5.4-nano", "gpt-oss-120b", "mistral-small-2603", "qwen3.5-9b"],
"limits": {"requests_per_day": 100, "requests_per_minute": 20, "max_input_tokens": 16000, "max_output_tokens": 2000, "fair_use_daily_limit": true}
}

Errors: 503 free_tier_unavailable when the list cannot be read. The same list marks each model in GET /v1/models as zabio.free_tier: true, and for a key on the Free limits zabio.availability says available or locked per model.

Terminal window
curl https://api.zabio.ai/v1/public/free-tier

Public, no key: the catalog ids auto can pick today, for a page or a client that wants to show them. A model is in the list when it has a skill score for at least one kind of work, or when the earlier rule can choose it for a request of kind other (see Models and routing). The list follows the daily skill data and routing’s own settings; as_of is the date of the skill data it was read from. Cache-Control: public, max-age=600. The same answer is zabio.auto_eligible in GET /v1/models.

{"object": "auto_models", "as_of": "2026-10-06T00:00:00Z", "models": ["claude-haiku-4-5", "deepseek-flash", "gpt-6-luna"]}
Terminal window
curl https://api.zabio.ai/v1/public/auto-models

One stateless chat turn in OpenAI’s shape. Needs the chat scope. Zabio stores nothing from these calls unless the key has body logging on.

Field Type Required Notes
model string no a catalog id or auto, auto/best, auto/balanced, auto/cheap. Omitted means auto
messages array yes 1 to 4096 messages with role system, developer, user, assistant or tool. At least one must be user, assistant or tool. system and developer texts are joined with a blank line into one system prompt
messages[].content string or array yes user messages accept text and image_url parts; the other roles accept text parts only. assistant messages may have content or tool_calls or both
messages[].tool_calls array no on assistant messages: {"id", "type": "function", "function": {"name", "arguments"}}. Invalid arguments JSON is replaced with {}
messages[].tool_call_id string on tool messages the id of the call being answered
tools array no up to 256 tools of type function with function.name, function.description, function.parameters (defaults to an empty object schema)
tool_choice string or object no auto, none, required or {"type": "function", "function": {"name": "..."}}. Needs tools unless it is none
parallel_tool_calls boolean no passed to the vendor where supported
max_tokens, max_completion_tokens integer no max_completion_tokens wins when both are present. Default 16000, capped at the model’s max_output
temperature number no 0 to 2
top_p number no 0 to 1
stop string or array no up to 4 sequences
stream boolean no server sent events, below
stream_options.include_usage boolean no accepted; the usage chunk is always sent
response_format object no {"type": "text"}, {"type": "json_object"} or {"type": "json_schema", "json_schema": {"name", "schema", "strict"}}; schema is required
reasoning_effort string no none, minimal, low, medium, high or xhigh; mapped onto the served model’s own control, see Reasoning levels
user string no forwarded as request metadata
metadata object no string values, forwarded as request metadata
store boolean no accepted and has no effect; chat completions store nothing
n integer no only 1; larger values are refused
seed, logit_bias, presence_penalty, frequency_penalty no accepted and ignored; reported in zabio.ignored_parameters when they carry a non default value
logprobs, top_logprobs, audio, prediction, web_search_options, non text modalities no refused with 400 unsupported_parameter
zabio object no Zabio’s options, next table
Field Type Values What it does
session_id string up to 128 characters groups calls so the routing decision is reused; echoed in the response
vendors array anthropic, openai, google, deepseek, xai, mistral, together only these vendors are candidates
fallback string same_vendor, cross_vendor, none what happens when an explicitly named model is unavailable. Default: the key’s fallback_policy
cache.ttl string 5m, 1h, none prompt cache lifetime where the vendor offers one. Caching is automatic on every call, to auto or to a named model, and none turns it off. Default: 5m (1h in a session whose turns are far apart), written on a long prompt’s second appearance (see cache.ttl); inside a batch, none, except that lines sharing a long prefix on an Anthropic model read it from a cache Zabio writes once for the batch
cache.breakpoints string auto, none none disables caching for the call and forces ttl to none
split string auto, off whether Zabio may split a big request under auto into an extract on a cheap model and an answer on the routed model, when that is cheaper. Default auto; off sends the request whole. Never on a named model. See Split and route on big inputs
deadline string a duration with units s, m or h (15m, 1h, 24h), up to 168h how long the answer may take. Absent or under 15 minutes: nothing changes. From 15 minutes up, the call may be served on the vendor’s flex tier where the model has one, the same model at about half the price, slower. Not a guarantee of a delivery time. See The flex tier

The same values can travel as headers, which win over the body: x-zabio-session-id, x-zabio-cache-ttl, x-zabio-fallback, x-zabio-split, x-zabio-deadline. There is no header for vendors or cache.breakpoints. Bad values return 400 invalid_value with param set to zabio.<field>.

A chat.completion object with exactly one choice:

Field Type Notes
id string chatcmpl- followed by 32 hex characters
object string chat.completion
created integer Unix seconds
model string the model that answered (same as zabio.model_served)
choices[0].index integer 0
choices[0].message object role assistant, content (string, or null when only tool calls were produced), tool_calls when present. arguments is always a JSON string, {} when empty
choices[0].finish_reason string stop, length, tool_calls or content_filter. A reasoning model that spends its whole output budget thinking returns an empty answer with length: raise max_tokens so it leaves room for the answer above the thinking, or lower reasoning_effort. A max_tokens above the served model’s maximum output is lowered to that limit
choices[0].logprobs null always null
usage object prompt_tokens (every input token, cached or not), completion_tokens, total_tokens, prompt_tokens_details.cached_tokens when any tokens were read from cache, and completion_tokens_details.reasoning_tokens when the vendor reports its thinking count
system_fingerprint null always null
zabio object next table

The same object is returned by /v1/chat/completions (in the body and in the final streaming chunk) and by /v1/responses. Fields with empty values are omitted.

Field Type Meaning
call_id string the call’s id, call_ and 32 hex characters: the same id Logs and the call list show, and the same hex as the response’s chatcmpl- id. Quote it to find the call later
vendor string who served the call: anthropic, openai, google, deepseek, xai, mistral, together or typesafe
model_served string the model that answered; differs from model you sent for auto and fallbacks
routing object line (the tier as you asked for it: auto, cheap, balanced or best), floor (the skill floor applied, none, middle or top, after a hard or high-stakes request moved it one step up and after any move to a stronger model; absent when the request was routed by the earlier rule, see Models and routing), task (the job as routing reads it), task_type (the kind of work, one of the tags in Kinds of work; absent when the classifier named none), difficulty (1 to 5), band (easy, medium, hard, frontier), stakes (normal or high), biased (true when low confidence raised the difficulty), cell (the knowledge used, task/band), confidence (0 to 1; 0 when no classifier ran), reason (the classifier’s sentence), trigger, classifier (who decided: typesafe/jev-1.13.0, or the text classifier it fell back to; absent on explicit models), effort (the reasoning setting applied with the model), effort_reason (why: the band’s evidence, high stakes, the best line, no evidence, or your explicit level) and sticky (present only when the session’s warm prompt cache decided the pick; names the model kept and the one cold pricing would have chosen, see Sessions and stickiness). Triggers: no_prior_decision, new_human_message, cache_hit, low_confidence_prior, long_idle, tool_failure_streak, customer_feedback, explicit, escalation. Absent when there is nothing to report
reasoning object requested (the level you sent, omitted when none) and applied (what the served model got: default, off, the vendor’s level name, budget N, on, always on or not supported)
cost_micros integer what you were billed, in millionths of a US dollar
cost object vendor_micros (provider list price), service_fee_micros, call_fee_micros (the fixed fee an auto request pays once, the price of automatic model choice; 0 on a named model), savings_fee_micros and classifier_micros (both always 0, kept so existing clients keep parsing them), direct_micros (the same call on your key’s baseline model with the same cache reads and writes at that model’s rates; the provider price itself on a named model), saved_micros (direct_micros minus cost_micros; can be negative; left out on an auto call while baseline.pending is true), baseline (on auto only: {pending, model} once your key has a baseline, or {pending: true, calls, calls_needed} while it is still under the 20 successful calls the baseline needs)
byok boolean present and true when your own vendor key served the call
cache object always present: read_tokens, write_tokens, ttl_applied (5m, 1h or "")
session_id string echoes the session id you sent. On /v1/responses it is the conversation id
escalated_from string line/model of the model whose answer auto replaced, for example cheap/gpt-5.4-nano: on the Responses API when that model asked for a stronger one, and on any request that is not streamed when its answer failed Zabio’s check
compacted boolean Responses API only: the stored history was summarized before this turn
vendor_stop_reason string the vendor’s own stop reason
ignored_parameters array request fields Zabio accepted and ignored
dropped_parameters array sampling parameters the model that answered does not take (temperature, top_p), left out of its request; also the x-zabio-dropped-params header. See dropped_parameters
attempts integer how many providers were tried before one answered (1 when the first worked)
decision object when jev-1.13.0 answered (named, or chosen by auto for a classification job, see Classification jobs on auto): one entry per property of your schema, in schema order. A choice: {"type": "choice", "probabilities": {value: p, ...}, "confidence"}; a boolean: {"type": "noul", "probability": p} (the probability of yes; true at 0.5 and above); a score: {"type": "score", "value": the weighted level, "probabilities": {level: p, ...}, "confidence"}. See Decision models
split object present while split and route is on (see Split and route on big inputs): applied (boolean, true when the answer came from an excerpt of your source), pattern (distill, once the split ran), reason (why the request was not split: named_model, opt_out, not_eligible, below_threshold, jev_no, jev_failed, price_check; empty on a split and on a fallback), estimate (unsplit_micros, split_micros; present once the gate passed), steps (a list of {role, vendor, model, input_tokens, output_tokens, micros, billed}; roles questions, extract, check, answer; micros is the call’s list price; billed is false for Zabio’s own costs and for every step of a fallback), excerpt (lines_kept, lines_total, tokens, verbatim_rate), check_p (the check’s probability), fallback (the guard that sent the request whole: extractor_declined, empty_extract, extract_too_big, low_verbatim, check_rejected, need_more_source, extract_failed, check_failed), saved_micros (on a split that held, what the split saved: the request sent whole to the answer’s model less what the split’s calls cost, at list price; separate from cost.saved_micros, and present while your key’s baseline is pending too). Empty fields are omitted
tier object the service tier of the call (see The flex tier): requested (standard, flex or batch, what your deadline mapped to), applied (what served the call) and fallback (capacity when flex was requested and the vendor refused it for capacity, so standard served and was billed; synchronous when a deadline of 24 hours or more asked for batch on a synchronous call, so flex or standard served; no_batch_tier on a background response whose model has no batch tier, and on a batch line at the standard price under zabio.allow_standard_price; decision on a batch line Jev answered at its own price, below the text pick’s batch price). Present on every response
plan object present on a call served under the Free limits (see Billing, “The Free plan”): tier (free), reason (free_plan, the organization never topped up; zero_balance, a paid organization at zero continuing on the free tier), paid_would_use (on auto, the model a paid account would have been given, when it differs from the one that answered) and topup_url. Absent on a paid call. Not to be confused with tier, the vendor processing tier

zabio.routing.task_type names the kind of work the classifier saw in an auto request, as one of these tags. The dashboard’s spend by kind of work and the Logs page show the name, and the dashboard’s CSV export of your calls carries the tag. The list can grow: treat a tag you do not know as a new kind of work, not as an error.

Tag Name
classification_tagging Classification
data:extraction Data Extraction
agent:workflow_execution Workflow Execution
roleplay_fiction Roleplay & Fiction
content_writing Content Writing
data:transformation Data Transformation
code:general_impl Code Generation
agent:multi_step_planning Multi-step Planning
qa_knowledge Q&A & Knowledge
translation Translation
conversational_reply Conversation
code:debugging Debugging
summarization Summarization
code:file_read_write File I/O
customer_support Customer Support
code:review_security Code Review
agent:tool_dispatch Tool Dispatch
code:shell_execution Shell Execution
agent:memory_extraction Memory Extraction
code:frontend_ui Frontend & UI
finance_trading Finance & Trading
research_report Research & Reports
agent:web_search Web Search
security_audit Security Audit
code:repo_scan Repo Scanning
math Math
code:devops_config DevOps & Config
code:sql_database SQL & Database
devops DevOps
other Other

With stream: true the status is 200 and the body is text/event-stream. Each event is one data: line holding a chat.completion.chunk with the same id, created and model as a unary response would have. Order:

  1. a chunk with delta: {"role": "assistant", "content": ""}
  2. content chunks with delta.content, and tool call chunks with delta.tool_calls[] carrying index, id, type and function.name on the first chunk of a call and function.arguments fragments afterwards
  3. a chunk with an empty delta and finish_reason
  4. one usage chunk with choices: [], the usage block and the zabio object
  5. data: [DONE]

Every chunk carries system_fingerprint: null, and every choice logprobs: null. If the vendor connection fails after the stream started, one event with {"error": {"message", "type": "server_error", "code"}} is sent, then data: [DONE]. Errors before the first chunk are ordinary JSON errors with their HTTP status.

A vendor attempt may run up to 600 seconds on powerful and frontier models (180 seconds on the others). To keep the connection alive that long, a call with no vendor answer after 90 seconds commits its 200 and writes one space every 20 seconds until the JSON body, which parsers read unchanged. A failure after that commit is delivered in the body as a chat.completion with choices: [] and an error object (message, type, code). Streams send an SSE comment (: keepalive) every 20 seconds while the vendor opens or is silent, and a failure before any data is data: {"error": {...}} then data: [DONE]. Set client timeouts to at least 600 seconds for powerful and frontier models. A disconnect cancels the vendor call and, for a call that was not streamed, costs nothing.

Status code When
400 missing_messages messages is absent or has no user, assistant or tool entry
400 too_many_messages, too_many_tools more than 4096 messages or 256 tools
400 invalid_value a field is out of range; param names it (temperature, top_p, max_tokens, stop, reasoning_effort, tool_choice, tools[i].type, messages[i].role, messages[i].content, response_format.type, response_format.json_schema, zabio.session_id, zabio.cache.ttl, zabio.cache.breakpoints, zabio.fallback, zabio.split, zabio.deadline)
400 unsupported_parameter n above 1, logprobs, top_logprobs, audio, prediction, web_search_options, a modalities entry other than text, the legacy function role, a content part type other than text and image_url, tools sent to a named model that cannot take them through Zabio (today gpt-6-astra)
400 invalid_image_url an image could not be fetched or is not allowed; param points at the part
400 byok_key_invalid your organization’s own vendor key was rejected by the vendor
400 upstream_invalid_request the vendor rejected the request; the message carries the vendor’s reason
400 unsupported_for_model the model you named cannot do what the request asks; param names the field. On jev-1.13.0: no json_schema, a free text or nested property, tools, images, tool results or stream: true; the message says what Jev answers and how to ask
402 insufficient_credits no credits left; only a named model served on your own vendor key skips this check, auto never does
403 insufficient_scope the key lacks chat
404 model_not_found unknown model, or not allowed for the key
429 rate_limit_exceeded, quota_exceeded, upstream_rate_limited see the headers above and Errors
502, 503, 504 upstream_error, no_provider_available, billing_unavailable, upstream_timeout see Errors

A unary call with auto, then reading what was served and what it cost.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"}
],
"zabio": {"session_id": "docs-demo"}
}'

Python

r = client.chat.completions.create(
model="auto",
messages=[
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why is the sky blue?"},
],
extra_body={"zabio": {"session_id": "docs-demo"}},
)
print(r.choices[0].message.content)
z = r.model_extra["zabio"]
print(z["model_served"], z["cost_micros"], z["routing"]["line"], z["routing"]["task"])

Node

const r = await client.chat.completions.create({
model: "auto",
messages: [
{ role: "system", content: "Answer in one sentence." },
{ role: "user", content: "Why is the sky blue?" },
],
zabio: { session_id: "docs-demo" },
});
console.log(r.choices[0].message.content);
console.log(r.zabio.model_served, r.zabio.cost_micros, r.zabio.routing?.line, r.zabio.routing?.task);

The same call streamed. The zabio object arrives in the last chunk, the one with an empty choices list.

curl

Terminal window
curl -N https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto/cheap", "messages": [{"role": "user", "content": "Count to five."}], "stream": true}'

Python

stream = client.chat.completions.create(
model="auto/cheap",
messages=[{"role": "user", "content": "Count to five."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\n", chunk.usage.total_tokens, chunk.model_extra.get("zabio"))

Node

const stream = await client.chat.completions.create({
model: "auto/cheap",
messages: [{ role: "user", content: "Count to five." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
if (chunk.usage) console.log("\n", chunk.usage.total_tokens, chunk.zabio);
}

A tool call round trip. The assistant answers with tool_calls; you run the function and send the result back as a tool message.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{
"model": "auto",
"messages": [{"role": "user", "content": "What is the weather in Lima? Use the tool."}],
"tools": [{"type": "function", "function": {"name": "get_weather", "description": "Weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}],
"tool_choice": "required"
}'

Python

tool = {"type": "function", "function": {"name": "get_weather", "description": "Weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}}}
messages = [{"role": "user", "content": "What is the weather in Lima? Use the tool."}]
first = client.chat.completions.create(model="auto", messages=messages, tools=[tool], tool_choice="required")
call = first.choices[0].message.tool_calls[0]
messages.append(first.choices[0].message.model_dump(exclude_none=True))
messages.append({"role": "tool", "tool_call_id": call.id, "content": "Sunny, 22C"})
second = client.chat.completions.create(model="auto", messages=messages, tools=[tool])
print(second.choices[0].message.content)

Node

const tools = [{ type: "function", function: { name: "get_weather", description: "Weather for a city",
parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] } } }];
const messages = [{ role: "user", content: "What is the weather in Lima? Use the tool." }];
const first = await client.chat.completions.create({ model: "auto", messages, tools, tool_choice: "required" });
const call = first.choices[0].message.tool_calls[0];
messages.push(first.choices[0].message, { role: "tool", tool_call_id: call.id, content: "Sunny, 22C" });
const second = await client.chat.completions.create({ model: "auto", messages, tools });
console.log(second.choices[0].message.content);

Embeddings in OpenAI’s shape. Models are always explicit; there is no auto. Needs the embeddings scope.

Field Type Required Notes
model string yes text-embedding-3-small, text-embedding-3-large or gemini-embedding-001
input string or array yes one string or 1 to 2048 strings
dimensions integer no passed to the vendor
encoding_format string no float (default) or base64 (little endian float32)
user string no accepted

The zabio request object does not apply here. Rate limit and quota headers are the same as for chat completions; the token bucket is charged with an estimate of one token per four characters.

Field Type Notes
object string list
data[] array {"object": "embedding", "index", "embedding"}; embedding is an array of numbers or a base64 string
model string the model that answered
usage object prompt_tokens and total_tokens (equal)
zabio object call_id, vendor, model_served, cost_micros, tokens_estimated. tokens_estimated is true for Google models, which do not report token counts; Zabio estimates and bills on the estimate
Status code When
400 missing_model model absent
400 invalid_value input empty or over 2048 items, or encoding_format not float or base64
400 byok_key_invalid your own vendor key was rejected
404 model_not_found not an embedding model, or not allowed for the key
503 no_provider_available the provider for this model is not configured on this server

curl

Terminal window
curl https://api.zabio.ai/v1/embeddings \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "text-embedding-3-small", "input": ["first text", "second text"]}'

Python

res = client.embeddings.create(model="text-embedding-3-small", input=["first text", "second text"])
print(len(res.data), len(res.data[0].embedding), res.model_extra["zabio"]["cost_micros"])

Node

const res = await client.embeddings.create({ model: "text-embedding-3-small", input: ["first text", "second text"] });
console.log(res.data.length, res.data[0].embedding.length, res.zabio.cost_micros);

One turn of a stored conversation in the OpenAI Responses API shape. Zabio assembles the history, routes the turn, stores the answer and returns it. Needs the conversations scope; the routing itself reuses the chat path.

Field Type Required Notes
model string no a catalog id or an auto model. Omitted means auto
input string or array yes a string is one user message. Items: {"type": "message", "role": "user" or "assistant" or "system" or "developer", "content": string or parts} (type may be omitted), {"type": "function_call_output", "call_id", "output"}, {"type": "function_call", "call_id", "name", "arguments"}. Content parts: input_text, text and output_text (all text), input_image with image_url
instructions string no stored with the turn as a developer item
previous_response_id string no resp_... to continue from. Pointing at an older response branches the conversation
conversation string or object no conv_... or {"id": "conv_..."} to continue from the conversation’s latest turn. With previous_response_id, both must belong to the same conversation
store boolean no default true. false computes the turn from the stored history and stores nothing; the response id cannot be retrieved afterwards
tools array no flat function tools: {"type": "function", "name", "description", "parameters", "strict"}
tool_choice string or object no auto, none, required or {"type": "function", "name"}; needs tools unless none
parallel_tool_calls boolean no passed to the vendor where supported
max_output_tokens integer no at least 1; default 4096
temperature number no 0 to 2
top_p number no above 0 and up to 1
text.format object no {"type": "text"}, {"type": "json_object"} or {"type": "json_schema", "name", "schema", "strict"}
reasoning.effort string no none, minimal, low, medium, high or xhigh, the same scale as reasoning_effort on chat completions
metadata object no up to 16 keys; keys up to 64 characters, values up to 512
stream boolean no server sent events, below
background boolean no default false. true returns the response at once with status queued and runs the turn later; poll GET /v1/responses/{id}. Needs store true and no stream. With a zabio.deadline of 24 hours or more on a model that can be batched, the turn joins your organization’s next vendor batch at the batch price; otherwise it runs as its own job at flex or standard and zabio.tier says why. See Background responses
include, truncation, user no accepted and ignored; listed in zabio.ignored_parameters
zabio object no next table

Everything from chat completions (session_id, vendors, fallback, cache, split, deadline) plus:

Field Type Values What it does
retention_days integer 0 to 365 retention of a conversation created by this call. 0 deletes it as soon as the response is returned. Ignored when the conversation already exists
compaction string auto (default), off off fails with context_length_exceeded instead of summarizing old history

session_id is overwritten: within a conversation the conversation id is the session, so zabio.session_id in the response is conv_.... The x-zabio-* headers work here too.

Field Type Notes
id string resp_ followed by 32 hex characters
object string response
created_at integer Unix seconds
status string completed, incomplete (hit max_output_tokens; incomplete_details.reason is max_output_tokens) or failed (streaming, after a vendor error; or a background response that failed). A background response is also queued and cancelled
background boolean true when the turn was created with background
error object or null on a failed or cancelled background response, {"code", "message"}
model string the model that answered
output[] array {"type": "message", "id": "msg_...", "status", "role": "assistant", "content": [{"type": "output_text", "text", "annotations": []}]} and {"type": "function_call", "id": "fc_...", "call_id", "name", "arguments", "status": "completed"}
usage object input_tokens (cached included), input_tokens_details.cached_tokens, output_tokens, output_tokens_details.reasoning_tokens (the vendor’s thinking count when it reports one, else 0), total_tokens
previous_response_id string or null what you sent
conversation object or null {"id": "conv_..."}; null only when store is false and no conversation was given
instructions string or null what you sent
metadata object what you sent, or {}
store boolean what applied
max_output_tokens integer or null what applied; null on GET /v1/responses/{id}
tools, tool_choice, parallel_tool_calls what applied; tool_choice defaults to auto. GET returns [], auto and true
temperature, top_p number present when you sent them
incomplete_details object or null {"reason": "max_output_tokens"} when status is incomplete
error null always null on this endpoint; errors are HTTP errors or a stream error event
zabio object the zabio response object, with escalated_from and compacted filled in when they apply

With stream: true the body is text/event-stream with event: and data: lines. Every payload carries type and a sequence_number starting at 0. The sequence for a text answer is response.created, response.in_progress, response.output_item.added, response.content_part.added, response.output_text.delta (repeated), response.output_text.done, response.content_part.done, response.output_item.done, then response.completed (or response.incomplete, response.failed) with the full response object under response. Tool calls use response.output_item.added, response.function_call_arguments.delta (repeated), response.function_call_arguments.done, response.output_item.done. A vendor failure after the stream started sends event: error with {"type": "error", "code", "message", "param": null}.

Delta events carry output_index, item_id, content_index (text only) and delta. Item ids inside a stream are msg_<response id>_<index> and fc_<response id>_<index>; the stored ids returned by response.completed and by GET are msg_ and fc_ followed by 32 hex characters.

The same keepalive as chat completions: after 90 seconds without an answer a non-streamed call commits its 200 and writes a space every 20 seconds until the object; a failure after that is the object with status: "failed" and error: {"code", "message"}. A stream sends : keepalive comments every 20 seconds while the vendor opens or is silent, before response.created if need be; a failure before any output is a response.failed event carrying that object.

Status code When
400 missing_field input, an item’s role, content or call_id is missing; param names it
400 invalid_value a field is out of range; includes previous_response_id and conversation that are not ids, zabio.retention_days, zabio.compaction, an input with no user message or tool output, and background with stream or with store: false
400 background_unavailable background responses are not available on this server
402 insufficient_credits on a background response headed for a batch: its estimated cost exceeds the available balance; nothing is stored
400 conversation_mismatch previous_response_id belongs to another conversation
400 conversation_too_large the thread reached 4000 entries or 2000000 estimated tokens
400 context_length_exceeded the history plus this turn does not fit and compaction is off, impossible (store: false, no conversation) or not enough; the message carries the numbers
403 insufficient_scope, conversations_closed the key lacks conversations; stored conversations are closed for the organization
404 previous_response_not_found, conversation_not_found unknown or deleted
503 conversations_unavailable stored conversations are not available on this server

Everything listed for chat completions applies as well: model_not_found, insufficient_credits, the 429 family and vendor errors.

Two turns, the second continuing from the first.

curl

Terminal window
curl https://api.zabio.ai/v1/responses \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "My name is Ana. Remember it.", "zabio": {"retention_days": 7}}'
# then, with the id from the first answer:
curl https://api.zabio.ai/v1/responses \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "What is my name?", "previous_response_id": "resp_..."}'

Python

first = client.responses.create(model="auto", input="My name is Ana. Remember it.",
extra_body={"zabio": {"retention_days": 7}})
second = client.responses.create(model="auto", input="What is my name?", previous_response_id=first.id)
print(second.output_text)
print(second.conversation.id, second.model_extra["zabio"]["model_served"])

Node

const first = await client.responses.create({ model: "auto", input: "My name is Ana. Remember it.", zabio: { retention_days: 7 } });
const second = await client.responses.create({ model: "auto", input: "What is my name?", previous_response_id: first.id });
console.log(second.output_text);
console.log(second.conversation.id, second.zabio.model_served);

Streaming a turn and reading the final object.

curl

Terminal window
curl -N https://api.zabio.ai/v1/responses \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "Count to three.", "stream": true}'

Python

with client.responses.stream(model="auto", input="Count to three.") as stream:
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
final = stream.get_final_response()
print("\n", final.status, final.model_extra["zabio"])

Node

const stream = client.responses.stream({ model: "auto", input: "Count to three." });
for await (const event of stream) {
if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
}
const final = await stream.finalResponse();
console.log("\n", final.status, final.zabio);

Returns a stored response with its output. model is the model that served it. Errors: 404 response_not_found when the id is malformed, unknown, deleted, or was created with store: false.

curl

Terminal window
curl https://api.zabio.ai/v1/responses/resp_... \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

r = client.responses.retrieve("resp_...")
print(r.status, r.output_text)

Node

const r = await client.responses.retrieve("resp_...");
console.log(r.status, r.output_text);

Cancels a background response. Queued, or still waiting for its batch to be sent: the response ends cancelled and nothing is billed. Running as a job: the vendor call is cut and the response ends cancelled. Already sent to the model vendor inside a batch: 409 not_cancellable, the answer arrives and is billed. Already ended: the object as it is. Returns the response object. Errors: 400 not_background when the response was not created with background; 404 response_not_found.

curl

Terminal window
curl -X POST https://api.zabio.ai/v1/responses/resp_.../cancel \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

r = client.responses.cancel("resp_...")
print(r.status) # cancelled

Node

const r = await client.responses.cancel("resp_...");
console.log(r.status); // cancelled

Deletes the response with its turn (the inputs it stored and its outputs) and everything stored after it on the same thread. The conversation continues from where that turn started. Returns {"id": "resp_...", "object": "response", "deleted": true}. Errors: 404 response_not_found.

curl

Terminal window
curl -X DELETE https://api.zabio.ai/v1/responses/resp_... \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

client.responses.delete("resp_...")

Node

await client.responses.delete("resp_...");

Creates an empty or seeded conversation. The body may be empty.

Field Type Required Notes
metadata object no up to 16 string keys
items array no input items in the same shapes as input on POST /v1/responses; stored without a model turn
zabio.retention_days integer no 0 to 365; default is the organization’s retention

Response, also returned by GET and by POST /v1/conversations/{id}:

Field Type Notes
id string conv_ followed by 32 hex characters
object string conversation
created_at integer Unix seconds
metadata object
zabio.entries integer stored items, all threads
zabio.token_estimate integer estimated tokens of the stored items
zabio.expires_at string RFC 3339; when the conversation is deleted unless a new turn resets the clock
zabio.last_turn_at string RFC 3339
zabio.retention_days integer present when the conversation has its own retention

Errors: 400 invalid_json, 400 invalid_value on metadata or zabio.retention_days, 400 missing_field or invalid_value on a bad item.

curl

Terminal window
curl https://api.zabio.ai/v1/conversations \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"metadata": {"customer": "acme"}, "items": [{"role": "user", "content": "Hello"}], "zabio": {"retention_days": 30}}'

Python

conv = client.conversations.create(metadata={"customer": "acme"},
items=[{"role": "user", "content": "Hello"}],
extra_body={"zabio": {"retention_days": 30}})
print(conv.id, conv.model_extra["zabio"]["expires_at"])

Node

const conv = await client.conversations.create({
metadata: { customer: "acme" },
items: [{ role: "user", content: "Hello" }],
zabio: { retention_days: 30 },
});
console.log(conv.id, conv.zabio.expires_at);

Returns the conversation object above. Errors: 404 conversation_not_found.

curl

Terminal window
curl https://api.zabio.ai/v1/conversations/conv_... \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

conv = client.conversations.retrieve("conv_...")
print(conv.model_extra["zabio"]["entries"])

Node

const conv = await client.conversations.retrieve("conv_...");
console.log(conv.zabio.entries);

Replaces the conversation’s metadata with {"metadata": {...}} (up to 16 keys) and returns the conversation object. Errors: 400 invalid_json, 400 invalid_value, 404 conversation_not_found.

curl

Terminal window
curl https://api.zabio.ai/v1/conversations/conv_... \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"metadata": {"customer": "acme", "ticket": "4711"}}'

Python

conv = client.conversations.update("conv_...", metadata={"customer": "acme", "ticket": "4711"})

Node

const conv = await client.conversations.update("conv_...", { metadata: { customer: "acme", ticket: "4711" } });

Deletes the conversation and everything in it, immediately and permanently. Returns {"id": "conv_...", "object": "conversation.deleted", "deleted": true}. Errors: 404 conversation_not_found.

curl

Terminal window
curl -X DELETE https://api.zabio.ai/v1/conversations/conv_... \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

client.conversations.delete("conv_...")

Node

await client.conversations.delete("conv_...");

Lists every stored item in creation order, all threads, including compaction summaries.

Query parameter Default Notes
limit 20 1 to 100
after an item id (msg_..., fc_..., fco_..., cmp_...) to continue after
order asc asc or desc

Response: {"object": "list", "data": [...], "has_more", "first_id", "last_id"}. Items:

type Shape
message {"id": "msg_...", "role", "status": "completed", "content": [...]}. User, system and developer content is input_text and input_image parts; assistant content is output_text parts
function_call {"id": "fc_...", "call_id", "name", "arguments", "status"}
function_call_output {"id": "fco_...", "call_id", "output", "status": "completed"}
compaction {"id": "cmp_...", "summary"}: the summary that replaced older history

Errors: 400 invalid_value on limit or after, 404 conversation_not_found.

curl

Terminal window
curl "https://api.zabio.ai/v1/conversations/conv_.../items?limit=50&order=asc" \
-H "Authorization: Bearer insert your zabio.ai key here"

Python

page = client.conversations.items.list("conv_...", limit=50)
for item in page.data:
print(item.type, getattr(item, "role", ""))

Node

const page = await client.conversations.items.list("conv_...", { limit: 50 });
for (const item of page.data) console.log(item.type, item.role ?? "");

Appends items without running a model turn, for example to seed history from another system. Body: {"items": [...]} with at least one item in the input shapes. Returns {"object": "list", "data": [the stored items], "has_more": false}. Errors: 400 missing_field (items), 400 conversation_too_large, 404 conversation_not_found.

curl

Terminal window
curl https://api.zabio.ai/v1/conversations/conv_.../items \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"items": [{"role": "user", "content": "Earlier question"}, {"role": "assistant", "content": "Earlier answer"}]}'

Python

client.conversations.items.create("conv_...", items=[
{"role": "user", "content": "Earlier question"},
{"role": "assistant", "content": "Earlier answer"},
])

Node

await client.conversations.items.create("conv_...", { items: [
{ role: "user", content: "Earlier question" },
{ role: "assistant", content: "Earlier answer" },
] });

Upload a file in OpenAI’s shape, multipart/form-data. Today the only purpose is batch: a JSONL file of chat completion requests for Batches. Files are encrypted per organization and kept for the organization’s retention period from the upload, or until deleted, or until the expiry you set. Needs the chat scope.

Part Required Notes
file yes the file; 100 MiB at most
purpose yes batch
expires_after[anchor] no created_at
expires_after[seconds] no 3600 to 2592000; shortens the retention

A batch input is checked line by line on upload: each line is a JSON object with a unique custom_id, method POST, a url and a body object; one line may be up to 10 MiB.

Field Type Notes
id string file_...
object string file
bytes integer the size
created_at, expires_at integer Unix seconds; expires_at is the retention or the expiry you set, whichever is sooner
filename, purpose string as uploaded
status string processed
Status code When
400 invalid_multipart, missing_field not multipart, no file part, no purpose
400 invalid_value a purpose other than batch, or a bad expires_after
400 invalid_file the JSONL is not valid; the message names the line
413 file_too_large over 100 MiB

Lists your files, newest first. purpose filters; limit (1 to 10000) and after (a file id) page. Returns {"object": "list", "data": [file...], "first_id", "last_id", "has_more"}.

Returns the file object, or 404 not_found.

Deletes the content at once and permanently; returns {"id", "object": "file", "deleted": true}. A file a running batch still needs answers 409 file_in_use.

Streams the file’s bytes, with Content-Disposition naming the file.

curl

Terminal window
curl https://api.zabio.ai/v1/files \
-H "Authorization: Bearer insert your zabio.ai key here" \
-F purpose=batch -F file=@requests.jsonl
curl https://api.zabio.ai/v1/files/file_.../content \
-H "Authorization: Bearer insert your zabio.ai key here" -o output.jsonl

Python

f = client.files.create(file=open("requests.jsonl", "rb"), purpose="batch")
print(client.files.content(f.id).text)

Creates a batch from an uploaded file: every line is routed at the vendors’ batch prices and answered within 24 hours. See Batches for the whole flow, the models and the money. Needs the chat scope.

Field Type Required Notes
input_file_id string yes a processed upload with purpose batch, at most 50,000 lines
endpoint string yes /v1/chat/completions
completion_window string no 24h, the only value
metadata object no up to 16 string pairs; keys at most 64 characters, values at most 512
zabio.allow_standard_price boolean no default false. true runs the lines that name a model without a batch tier as synchronous calls at the standard price instead of failing the batch; each such line says zabio.tier.applied: "standard" with fallback: "no_batch_tier". See Lines Zabio answers directly

The batch object:

Field Type Notes
id string batch_...
object string batch
endpoint, input_file_id, completion_window string as created
status string validating, failed, in_progress, finalizing, completed, expired, cancelling, cancelled
output_file_id, error_file_id string or null set when the batch ends; the error file only when some lines failed
errors object on a failed batch: {"object": "list", "data": [{"code", "message", "param", "line"}]}
created_at, expires_at, in_progress_at, finalizing_at, completed_at, failed_at, expired_at, cancelling_at, cancelled_at integer Unix seconds; expires_at is 24 hours after creation
request_counts object total, completed, failed (failed, expired and cancelled lines)
metadata object as created
zabio object tier (batch) and cost: vendor_micros, billed_micros, direct_micros, saved_micros, classifier_micros (always 0), the running totals of the lines billed so far. saved_micros adds each line’s own saved_micros, so an auto line on a key whose baseline is pending adds nothing
Status code When
400 missing_field input_file_id or endpoint absent
400 invalid_value endpoint not /v1/chat/completions, completion_window not 24h, metadata too big, the file not a processed batch upload
400 batch_too_large more than 50,000 lines
404 not_found no file with that id
429 too_many_open_batches 20 batches already open for the organization

A batch that was created can still fail before running, with status: failed and the reasons in errors: model_not_batchable (a line names a model without a batch tier and zabio.allow_standard_price is not set; the message lists the batch-capable models and the opt-in), model_not_found, no_batch_model, insufficient_credits (the estimate and the available balance are in the message), api_key_revoked, empty_file, input_file_missing, or a request code from a line (invalid_url, invalid_request, missing_messages, invalid_json). Nothing is billed on a failed batch.

Lists your batches, newest first; limit (1 to 100, default 20) and after (a batch id) page. Returns {"object": "list", "data": [batch...], "first_id", "last_id", "has_more"}.

Returns the batch object, or 404 not_found.

Cancels the lines not yet at a vendor and asks the vendors to stop the rest; what they finish is kept and billed. Returns the batch object with status cancelling. A batch that has ended answers 400 batch_not_cancellable.

curl

Terminal window
curl https://api.zabio.ai/v1/batches \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"input_file_id": "file_...", "endpoint": "/v1/chat/completions", "completion_window": "24h"}'
curl https://api.zabio.ai/v1/batches/batch_... -H "Authorization: Bearer insert your zabio.ai key here"

Python

b = client.batches.create(input_file_id=f.id, endpoint="/v1/chat/completions", completion_window="24h")
b = client.batches.retrieve(b.id)
print(b.status, b.request_counts, b.model_extra["zabio"]["cost"])

Node

const b = await client.batches.create({ input_file_id: f.id, endpoint: "/v1/chat/completions", completion_window: "24h" });
console.log((await client.batches.retrieve(b.id)).status);

Reads a chat completions request and says what it costs today, what it would cost through Zabio and how to cut it, without running it. This is the engine behind the analyzer on zabio.ai and the tips the Playground shows after each call. Public: no API key. Rate limited per address, on top of the general per-IP limit, and capped for everyone by a daily allowance.

The only vendor call is Zabio’s routing classifier, which reads the same excerpt it reads on any auto call (see Privacy); no model answers the request and Zabio pays the classification. The request is never stored: Zabio logs its size, the job the classifier named and a hash of the caller’s address, nothing else.

Field Type Required Notes
request object yes a chat completions request body, exactly as you would send it to POST /v1/chat/completions, with the same validation
calls_per_month integer no projects the month in monthly and is reported on the batch tip

Bodies over 1 MB answer 413 payload_too_large. Unknown fields are ignored.

Field Type Notes
job object the classifier’s view: task, difficulty (1 to 5), stakes (normal or high)
tokens object input (estimated, characters over four), output (what every price assumes) and output_basis: typical for the job, or max_tokens when the request’s cap is lower
today object or null the request on the model it names, at the vendor’s list price: model, vendor, usd. Null when the request names auto or a model Zabio does not know
lines[] array one row per virtual model: line, the model and vendor it would pick (by the same choice a live auto request makes, see Models and routing), provider_usd, fee_usd (the service fee), call_fee_usd (the fixed fee of an auto request, the price of automatic model choice), classifier_usd (always 0), total_usd and saving_usd against today, or against the plain auto pick when there is no today
normal_stakes object only when the classifier read the job as high stakes and that moved the lines. A high-stakes job moves every line one step up, so the rows above show what a live call does. This field says what auto/cheap would pick and cost per call had the stakes been normal: model, vendor, total_usd, on the same tokens and against the same baseline as the rows. Absent when the stakes did not change the picks
switches[] array the six feature levers, always in this order: split, cache, effort, jev, flex, batch. Each has id, applicable (whether the lever can change this request’s price), default (the product’s: split, cache, effort and jev on, flex and batch off) and status (active or coming_soon)
scenarios object all 64 combinations of the switches. The key is one 0 or 1 per switch in the switches’ order, so 111100 is the defaults and 000000 every lever off. Each value is {"total_usd", "saving_usd"}: the per-call bill through Zabio on the auto pick with those levers on, priced together so overlapping savings are never counted twice, and the saving against today, or against 000000 when there is no today. With flex and batch both on, batch wins. Batching is priced from the catalog’s batch tier, the price the vendor’s batch API charges. effort never changes a price today, because reasoning tokens are not priced yet, so its scenarios equal the ones without it
tips[] array each tip only when it applies: id, status (active or coming_soon), saving_usd (per call, or null when the saving cannot be priced) and params, numbers and names only, listed below
monthly object or null with calls_per_month: calls, today_usd (null without a today) and zabio_usd on the plain auto line

Every amount is USD per call at list prices, six decimals.

The site and the Playground word each tip from its parameters; the API sends no sentences. Each parameter’s meaning and unit:

id When Parameter Type Meaning
line a cheaper line whose pick the knowledge base does not rate probably worse than the best line’s pick, with a saving line string the line to send the request to: auto/cheap, auto/balanced or auto/best
model string the model that line would pick
vendor string its vendor
total_usd number the per-call bill on that line, USD
max_tokens the request’s max_tokens is at least four times the typical output for the job and at least 1,000; saving_usd is null, the bill follows the actual output max_tokens integer the cap the request sent, tokens
typical_output integer the output the job usually needs, tokens
cache the system prompt plus the history before the last message is at least the pick’s smallest cacheable prefix, on a vendor that caches; saving_usd is per repeated call model string the pick the prefix would be cached on
vendor string its vendor
prefix_tokens integer the stable prefix, tokens
min_prefix_tokens integer the smallest prefix that vendor caches, tokens
ttl string the cache lifetime priced (5m)
first_call_extra_usd number what the first call pays on top to write the cache, USD (0 where the vendor charges no write premium)
jev the json_schema maps onto Jev’s questions and Jev is cheaper than the pick model string jev-1.13.0
fields integer the schema’s typed fields
jev_usd number Jev’s price for the request, USD
pick_model string the chat model the line would use instead
pick_usd number that model’s price with the schema’s output, USD
split a big plain text source with a question, on a job that is not whole-document work, where Smart Splitting’s own gate and price check pass on the estimate model string the model that answers on the excerpt
extractor string the cheap model that copies the relevant lines
source_tokens integer the last user message, tokens
unsplit_usd number the request sent whole on the pick, USD
split_usd number the extract, the check and the answer on the excerpt, USD
flex the pick has a flex tier model string the pick
vendor string its vendor
deadline string the shortest zabio.deadline that asks for flex (15m)
standard_usd number the call at the standard tier, USD
flex_usd number the same call at the flex tier, USD
batch active. The pick has a batch tier; the saving is the request at its batch price against its standard price (see Batches) model string the pick
vendor string its vendor
discount number the batch discount as a share of the standard price (0.5 is 50 percent)
window string the vendor’s completion window (24h)
standard_usd number the call at the standard tier, USD
calls_per_month integer the volume the request carried, 0 when none
effort the named model reasons, the job is easy or medium with normal stakes, and the setting in force is above the model’s lowest; saving_usd is null because reasoning tokens are not priced yet model string the named model
current string the reasoning setting in force, as zabio.reasoning.applied reports it
suggested string the model’s lowest setting, in the same form
suggested_level string the reasoning_effort value that selects it, when the setting maps to the scale
requested string the reasoning_effort the request sent, empty when none
Status code When
400 missing_parameter, invalid_value and the chat completions request errors the body or the request inside it is not valid
413 payload_too_large the body is over 1 MB
429 ip_rate_limit_exceeded too many analyses from one address; wait for Retry-After
503 analyzer_daily_cap the day’s allowance for everyone is spent; sign up to run the call for real
503 analyzer_unavailable the analyzer cannot run right now

curl

Terminal window
curl https://api.zabio.ai/v1/public/analyze \
-H "Content-Type: application/json" \
-d '{
"request": {
"model": "claude-opus-5-5",
"messages": [{"role": "user", "content": "Hola, tengo una pregunta sobre un cargo duplicado en mi factura."}]
},
"calls_per_month": 2000
}'
{
"job": {"task": "conversation", "difficulty": 1, "stakes": "normal"},
"tokens": {"input": 20, "output": 150, "output_basis": "typical"},
"today": {"model": "claude-opus-5-5", "vendor": "anthropic", "usd": 0.00385},
"lines": [
{"line": "auto", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557},
{"line": "auto/best", "model": "gemini-3.8-flash", "vendor": "google", "provider_usd": 0.000375, "fee_usd": 0.000019, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000644, "saving_usd": 0.003206},
{"line": "auto/balanced", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557},
{"line": "auto/cheap", "model": "qwen3.5-9b", "vendor": "together", "provider_usd": 0.000041, "fee_usd": 0.000002, "call_fee_usd": 0.00025, "classifier_usd": 0, "total_usd": 0.000293, "saving_usd": 0.003557}
],
"switches": [
{"id": "split", "applicable": false, "default": true, "status": "active"},
{"id": "cache", "applicable": false, "default": true, "status": "active"},
{"id": "effort", "applicable": false, "default": true, "status": "active"},
{"id": "jev", "applicable": false, "default": true, "status": "active"},
{"id": "flex", "applicable": false, "default": false, "status": "active"},
{"id": "batch", "applicable": true, "default": false, "status": "active"}
],
"scenarios": {
"000000": {"total_usd": 0.000887, "saving_usd": 0.002963},
"111100": {"total_usd": 0.000887, "saving_usd": 0.002963},
"000001": {"total_usd": 0.000867, "saving_usd": 0.002983}
},
"tips": [
{"id": "line", "status": "active", "saving_usd": 0.002963, "params": {"line": "auto/cheap", "model": "qwen3.5-9b", "vendor": "together", "total_usd": 0.000887}},
{"id": "batch", "status": "active", "saving_usd": 0.00002, "params": {"model": "gpt-5.4-nano", "vendor": "openai", "discount": 0.5, "window": "24h", "standard_usd": 0.000041, "batch_usd": 0.000021, "calls_per_month": 2000}}
],
"monthly": {"calls": 2000, "today_usd": 7.7, "zabio_usd": 1.774}
}

The numbers above are illustrative and scenarios is cut to three of its 64 keys; the live check in build note 32 carries real responses.

The public service status, the same document the page at zabio.ai/status shows. No API key is needed; it is one of the two public routes under /v1, with POST /v1/public/analyze. The per-IP rate limit still applies.

Response: {"status", "updated_at", "source", "components": [Component, ...]}.

Field Type Notes
status string operational, degraded, major_outage, or unknown when source is unavailable. The worst component wins, except that a vendor counts as degraded at most because calls fail over to another vendor
updated_at string RFC 3339 time the numbers were computed. The document is recomputed every 30 seconds
source string live; stale when the metrics backend could not be reached and the last good document (up to 5 minutes old) is served; unavailable when there is nothing usable
components[].id string api, vendor:anthropic, vendor:openai, vendor:google, vendor:deepseek, vendor:xai, vendor:mistral, vendor:together, vendor:typesafe, vendor:alibaba-us, vendor:alibaba-intl, web
components[].name string Display name
components[].status string operational under 2 percent of failed requests in the last 15 minutes, degraded under 20 percent, major_outage above. No requests in the window is operational with a null rate
components[].success_rate_15m number or null Share of requests without a failure in the last 15 minutes (0.995 is 99.5 percent). Not present on the web row
components[].success_rate_24h number or null The same over the last 24 hours
components[].p95_latency_ms integer or null 95th percentile request duration over the last 15 minutes. Not present on the web row

The document contains nothing about your organization or your calls.

curl

Terminal window
curl https://api.zabio.ai/v1/status
{
"status": "operational",
"updated_at": "2026-09-21T12:00:00Z",
"source": "live",
"components": [
{"id": "api", "name": "API", "status": "operational", "success_rate_15m": 0.999, "success_rate_24h": 0.998, "p95_latency_ms": 420},
{"id": "vendor:anthropic", "name": "Anthropic", "status": "operational", "success_rate_15m": 0.998, "success_rate_24h": 0.997, "p95_latency_ms": 2100},
{"id": "vendor:openai", "name": "OpenAI", "status": "operational", "success_rate_15m": null, "success_rate_24h": 0.999, "p95_latency_ms": null},
{"id": "vendor:google", "name": "Google", "status": "operational", "success_rate_15m": 1, "success_rate_24h": 1, "p95_latency_ms": 1300},
{"id": "web", "name": "Web app", "status": "operational"}
]
}

Everything Zabio adds on top of OpenAI’s shapes, one worked example each. The Python SDK rejects unknown keyword arguments, so zabio travels in extra_body; the Node SDK passes unknown fields through.

auto names a line, how skilled the model must be: plain auto (or no model) and auto/cheap take the cheapest model skilled enough for the job’s kind of work, auto/balanced the cheapest whose skill for it is in the middle third of the candidates or above, and auto/best the cheapest in the top third; a hard or high-stakes request moves one step up. Every line classifies the job first (kind of work, difficulty, stakes) and applies its floor to the models enabled for the key (see Models and routing). These four are the only virtual ids. zabio.routing.trigger is explicit for a named model.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto/balanced", "messages": [{"role": "user", "content": "Refactor this function to remove the nested loops: ..."}]}'

Python

r = client.chat.completions.create(model="auto/balanced", messages=[{"role": "user", "content": "Refactor this function: ..."}])
print(r.model_extra["zabio"]["routing"]) # {'line': 'balanced', 'task': 'conversation', 'difficulty': 1, ...}

Node

const r = await client.chat.completions.create({ model: "auto/balanced", messages: [{ role: "user", content: "Refactor this function: ..." }] });
console.log(r.zabio.routing); // { line: 'balanced', floor: 'middle', task: 'conversation', difficulty: 1, ... }

Lines: auto/best, auto/balanced, auto/cheap

Section titled “Lines: auto/best, auto/balanced, auto/cheap”

Name the line in model. There is nothing else to set: the classifier names the job and the line applies its floor. A hard job, or a high-stakes one (a wrong answer can cause legal, medical, financial or security harm, or changes a production system), moves one step up: cheap to the middle third, balanced to the top third. zabio.routing.line shows the line you named and zabio.routing.floor the floor applied. The line lives in the request: there is no key setting for it.

Terminal window
curl https://api.zabio.ai/v1/chat/completions -H "Authorization: Bearer $ZABIO_API_KEY" -H "Content-Type: application/json" \
-d '{"model": "auto/best", "messages": [{"role": "user", "content": "Summarize this contract clause: ..."}]}'
r = client.chat.completions.create(model="auto/cheap", messages=[{"role": "user", "content": "Summarize this contract clause: ..."}])
print(r.model_extra["zabio"]["routing"]["line"], r.model_extra["zabio"]["model_served"])
const r = await client.chat.completions.create({ model: "auto/cheap", messages: [{ role: "user", content: "Summarize this contract clause: ..." }] });
console.log(r.zabio.routing.line, r.zabio.model_served);

Send the same session_id (up to 128 characters) on calls that belong together. What is reused is the job the classifier named for the session’s current user message: the tool-result turns of an agent loop report trigger: "cache_hit" and are not classified again; each request still pays the fee for automatic model choice once. A new user message is classified again (trigger: "new_human_message"), as is any turn after a signal (low_confidence_prior, long_idle, tool_failure_streak, customer_feedback). Without a session_id, Zabio keys the reuse on your system prompt and the last user message, so an agent loop still classifies once per user message. Session state is metadata only: line, model, confidence, a few counters and the last 200 characters of the last user message for the feedback signal. It lives one hour after the last call. Prompts are never stored for it. The response echoes the id in zabio.session_id.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "x-zabio-session-id: support-4711" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "messages": [{"role": "user", "content": "Second question in the same session"}]}'

Python

for question in ["First question", "Second question"]:
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": question}],
extra_body={"zabio": {"session_id": "support-4711"}})
print(r.model_extra["zabio"]["routing"]["trigger"]) # no_prior_decision, then cache_hit

Node

for (const question of ["First question", "Second question"]) {
const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: question }], zabio: { session_id: "support-4711" } });
console.log(r.zabio.routing.trigger); // no_prior_decision, then cache_hit
}

Prompt caching where the vendor offers it. cache.ttl is 5m, 1h or none; cache.breakpoints is auto or none.

Vendor What the values do
Anthropic auto places cache markers on the system prompt, the last tool, the last message and the previous user turn, so the conversation history is read from the cache on every turn and only the new part is written. ttl picks the 5 minute or the 1 hour cache; the 1 hour cache costs more to write (2 times the input price against 1.25 times). Anthropic does not cache prefixes shorter than the model’s minimum (from 512 to 4,096 tokens depending on the model), so short prompts report 0
OpenAI caching is automatic on prefixes of 1024 tokens or more; ttl is accepted and ignored
Google caching is automatic (implicit) on prompts of 4,096 tokens or more, with no write charge; cache reads are billed at Google’s cache price and reported in zabio.cache.read_tokens; ttl and breakpoints are accepted and ignored
Other vendors where the model has a published cache price (DeepSeek, Mistral, and the DeepSeek models on Together, for example), caching is automatic and cache reads are billed at that price; ttl is accepted and ignored

breakpoints: "none" disables caching for the call and forces ttl to none. Caching is automatic on every call, whether it names auto or a model; a call turns it off with ttl: "none", breakpoints: "none" or the header. The default ttl is 5m, the lifetime used when Zabio writes (keys no longer carry a default of their own). A long prompt (1,024 tokens or more) is written to the cache on its second appearance, not its first: the first time it is sent without a write, and when the same prompt or the same system prompt and tools come back within the lifetime it is written, and later calls read it. A prompt sent once never pays the write. Calls in a session_id, requests that carry earlier turns of the conversation, and requests that set cache.ttl themselves are written at once. On the Responses API a stored response (the default), a conversation and a previous_response_id are written at once. The rule matters on Anthropic, whose writes are billed; the other vendors cache by themselves. With that default, a call in a session_id whose previous turn was more than five minutes ago gets 1h, because a 5 minute cache would have expired and be rewritten on every turn, which costs more than no cache. Read the result in zabio.cache: read_tokens, write_tokens, ttl_applied. The header x-zabio-cache-ttl carries ttl; there is no header for breakpoints. A line inside a batch, and a background response served from the pool, gets none unless the line sets ttl itself: nothing reads a batch line’s cache entry back, and the write costs more than fresh input.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-5", "messages": [{"role": "system", "content": "<a long system prompt>"}, {"role": "user", "content": "Question"}], "zabio": {"cache": {"ttl": "1h", "breakpoints": "auto"}}}'

Python

r = client.chat.completions.create(model="claude-sonnet-5",
messages=[{"role": "system", "content": long_system_prompt}, {"role": "user", "content": "Question"}],
extra_body={"zabio": {"cache": {"ttl": "1h", "breakpoints": "auto"}}})
print(r.model_extra["zabio"]["cache"]) # {'read_tokens': 0, 'write_tokens': 5120, 'ttl_applied': '1h'} on the first call

Node

const r = await client.chat.completions.create({ model: "claude-sonnet-5",
messages: [{ role: "system", content: longSystemPrompt }, { role: "user", content: "Question" }],
zabio: { cache: { ttl: "1h", breakpoints: "auto" } } });
console.log(r.zabio.cache); // { read_tokens: 5120, write_tokens: 0, ttl_applied: '1h' } on the second call

vendors limits the candidates to the vendors you list (anthropic, openai, google, deepseek, xai, mistral, together). It applies to auto and to the fallback set of an explicit model.

fallback applies to explicitly named models when the model is unhealthy, unavailable or fails a capability check: same_vendor (default from the key’s fallback_policy) tries other models of the same vendor and class, cross_vendor any model of the same class, none fails with 503 no_provider_available. Whatever happened, zabio.model_served and zabio.vendor tell you what answered and zabio.attempts how many tries it took.

curl

Terminal window
curl https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-5.4", "messages": [{"role": "user", "content": "Hello"}], "zabio": {"vendors": ["openai", "anthropic"], "fallback": "cross_vendor"}}'

Python

r = client.chat.completions.create(model="gpt-5.4", messages=[{"role": "user", "content": "Hello"}],
extra_body={"zabio": {"vendors": ["openai", "anthropic"], "fallback": "cross_vendor"}})
z = r.model_extra["zabio"]
print(z["vendor"], z["model_served"], z.get("attempts"))

Node

const r = await client.chat.completions.create({ model: "gpt-5.4", messages: [{ role: "user", content: "Hello" }],
zabio: { vendors: ["openai", "anthropic"], fallback: "cross_vendor" } });
console.log(r.zabio.vendor, r.zabio.model_served, r.zabio.attempts);

cost_micros is what you were billed; cost breaks it down. direct_micros is what the same call would have cost on your baseline model with the cache it had: for a named model, that same model, so it equals the provider price; for auto, your key’s baseline model with the same tokens, cache reads and cache writes at that model’s own rates, priced uncached where its vendor has no prompt cache (Google, Together AI). A cache discount is never counted as a saving: we never take a cut of your cache or batch discounts. The baseline model is the model your key used at the 95th percentile of list price per token over the last 30 days, recomputed nightly: the most expensive 5 percent of your calls are set aside and the next one sets it, so a burst does not raise it. The rule needs 20 successful calls before it says anything, because under 20 the 5 percent set aside is zero calls and one expensive call would set the baseline alone. Until then a new key’s calls are their own baseline and save nothing: saved_micros is left out and cost.baseline shows the progress; the baseline is set usually within an hour of the 20th call. Those calls stay without a saving after the baseline is set: the dashboard, Usage, the CSV export, the CLI and batch totals count none for them, so every total adds up the calls as each one showed. classifier_micros is always 0: the classifier that reads an auto request is never passed on. Before version 1.2.6 it carried that call’s provider price. saved_micros is direct_micros minus cost_micros. service_fee_micros is the plan’s percentage of vendor_micros, and call_fee_micros is the fixed fee an auto request pays once, the price of automatic model choice (see Billing). savings_fee_micros is always 0 since version 1.2.7: Zabio takes no share of the saving. Divide by 1000000 for US dollars.

curl

Terminal window
curl -s https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "messages": [{"role": "user", "content": "Say hello."}]}' | jq .zabio.cost

Python

r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Say hello."}])
z = r.model_extra["zabio"]
print(f"billed ${z['cost_micros'] / 1e6:.6f}, direct ${z['cost']['direct_micros'] / 1e6:.6f}, saved ${z['cost'].get('saved_micros', 0) / 1e6:.6f}")

Node

const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Say hello." }] });
const { cost_micros, cost } = r.zabio;
console.log(`billed $${cost_micros / 1e6}, direct $${cost.direct_micros / 1e6}, saved $${(cost.saved_micros ?? 0) / 1e6}`);

Under auto, a request that carries a large source (a document, a log, a thread, a set of records) and a question that needs only part of it can be answered in two steps when that is cheaper: a cheap model copies the relevant lines verbatim, a check confirms the excerpt holds what the question names, and the routed model answers on the excerpt. The response then has zabio.split.applied: true, zabio.split.steps lists every call with its model and list price, and zabio.cost covers the whole request (vendor_micros is both calls, cost_micros the whole request). When any guard fails, the request goes whole to the routed model, zabio.split.fallback names the guard, and you pay exactly the unsplit price. A named model is never split. zabio.split: "off" (or the header x-zabio-split: off) sends a request whole; there is no key-level default. Details in Models and routing and Billing.

curl

Terminal window
curl -s https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d @request.json | jq '.zabio.split'

request.json holds "model": "auto" and a long user message with the source and the question; add "zabio": {"split": "off"} to it, or the header -H "x-zabio-split: off", to send the request whole.

Python

r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": contract + "\n\nQUESTION: what is the notice period for termination?"}])
s = r.model_extra["zabio"].get("split") or {}
print(s.get("applied"), s.get("reason") or s.get("fallback"), [(st["role"], st["model"], st["micros"]) for st in s.get("steps", [])])
# The same request sent whole:
r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": contract + "\n\nQUESTION: what is the notice period for termination?"}],
extra_body={"zabio": {"split": "off"}})
print(r.model_extra["zabio"]["split"]["reason"]) # opt_out

Node

const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: `${contract}\n\nQUESTION: what is the notice period for termination?` }] });
const s = r.zabio.split ?? {};
console.log(s.applied, s.reason ?? s.fallback, (s.steps ?? []).map((st) => [st.role, st.model, st.micros]));
// The same request sent whole:
const whole = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: `${contract}\n\nQUESTION: what is the notice period for termination?` }],
zabio: { split: "off" } });
console.log(whole.zabio.split.reason); // opt_out

When your request can wait, say so with zabio.deadline (or the header x-zabio-deadline), a duration of 15 minutes or more. Zabio then serves the call on the vendor’s flex tier where the model has one (OpenAI gpt-5.4-nano, gpt-5-mini, gpt-5.4-mini, gpt-5.4, gpt-5.5, gpt-6-astra; Google gemini-3.5-flash-lite, gemini-3.8-flash, gemini-3.5-flash, gemini-3.1-pro-preview): the same model at about half the price, slower, with best-effort capacity. Under auto the candidates are compared at their flex prices, so auto can pick a model because it is cheaper in flex. When the vendor refuses a flex request for capacity, Zabio retries the same model at the standard tier at once and bills the price actually used; a flex request never fails for that reason. zabio.tier reports requested, applied and, after a refusal, fallback: "capacity". A flex attempt may take up to 900 seconds, so set your client’s timeout accordingly. The saving is measured against the flex price of your key’s baseline model, or its standard price when that model has no flex tier: Zabio never takes a cut of the vendor’s discount. A deadline of 24 hours or more asks for the batch tier, which only a background response or a batch can wait for; a synchronous call with such a deadline is served at flex (or standard) and zabio.tier reports fallback: "synchronous". Details in Chat completions and Billing.

curl

Terminal window
curl -s https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-H "x-zabio-deadline: 30m" \
-d '{"model": "auto", "messages": [{"role": "user", "content": "Summarize the attached report."}]}' | jq '.zabio.tier'

Python

r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Summarize the attached report."}],
extra_body={"zabio": {"deadline": "30m"}}, timeout=900)
tier = r.model_extra["zabio"]["tier"]
print(tier["requested"], tier["applied"], tier.get("fallback")) # flex flex None, or flex standard capacity

Node

const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Summarize the attached report." }],
zabio: { deadline: "30m" } }, { timeout: 900_000 });
console.log(r.zabio.tier.requested, r.zabio.tier.applied, r.zabio.tier.fallback); // flex flex undefined, or flex standard capacity

When your organization has added its own vendor key for the vendor that served the call, the response says so: zabio.byok is true and the header x-zabio-byok: true is set. zabio.cost.vendor_micros is 0 because Zabio has no provider cost (the vendor bills you directly) and cost_micros is 0 on a named model and the fee for automatic model choice on an auto request; direct_micros is still filled in so the numbers stay comparable. Calls that your own keys can serve entirely are not refused for missing credits. If the vendor rejects your key, the call fails with 400 byok_key_invalid and Zabio never falls back to its own key. See Own keys.

curl

Terminal window
curl -si https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "Hello"}]}' | grep -i x-zabio-byok

Python

r = client.chat.completions.create(model="claude-sonnet-5", messages=[{"role": "user", "content": "Hello"}])
print(r.model_extra["zabio"].get("byok", False))

Node

const r = await client.chat.completions.create({ model: "claude-sonnet-5", messages: [{ role: "user", content: "Hello" }] });
console.log(r.zabio.byok ?? false);

On auto turns of POST /v1/responses with tool_choice auto or absent, Zabio gives the model a hidden tool. When the model calls it to say the task is beyond it, Zabio re-runs the same turn on a stronger model, with the full history, and returns that answer: the cheapest model one step up the skill floor for the kind of work, or, for a request routed by the earlier rule, the next model up the curve that its knowledge says is probably better. The response then carries zabio.escalated_from as line/model (for example cheap/gpt-5.4-nano), zabio.routing.trigger is escalation, and later turns of the conversation stay on the stronger model. When there is no stronger model to go to (the top floor, or nothing probably better under the earlier rule), the tool is not offered. The hidden call never appears in output. Escalation is not offered on /v1/chat/completions, on explicit models or when tool_choice is none, required or a named function.

curl

Terminal window
curl -s https://api.zabio.ai/v1/responses \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "Prove that there are infinitely many primes, then explain each step."}' | jq '.zabio | {model_served, escalated_from, routing}'

Python

r = client.responses.create(model="auto", input="Prove that there are infinitely many primes, then explain each step.")
z = r.model_extra["zabio"]
if z.get("escalated_from"):
print("escalated from", z["escalated_from"], "to", z["model_served"])

Node

const r = await client.responses.create({ model: "auto", input: "Prove that there are infinitely many primes, then explain each step." });
if (r.zabio.escalated_from) console.log("escalated from", r.zabio.escalated_from, "to", r.zabio.model_served);

When the stored history plus a new turn would not fit the context window of the model the turn is served by, Zabio summarizes the oldest part with a fast model, stores the summary as a compaction item and continues; the response has zabio.compacted: true and the summary call appears in the conversation’s billing. The summary runs on Zabio’s key, so a turn that needs compaction needs credit even when it names a model served on your own key; at zero balance it gets 402 insufficient_credits before any summary is made. zabio.compaction: "off" turns this off and the request fails with 400 context_length_exceeded instead. zabio.retention_days (0 to 365) sets the retention of a conversation created by the call; the clock restarts on every turn.

curl

Terminal window
curl https://api.zabio.ai/v1/responses \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "input": "Continue", "conversation": "conv_...", "zabio": {"compaction": "off"}}'

Python

r = client.responses.create(model="auto", input="Continue", conversation="conv_...",
extra_body={"zabio": {"compaction": "off"}})
print(r.model_extra["zabio"].get("compacted", False))

Node

const r = await client.responses.create({ model: "auto", input: "Continue", conversation: "conv_...", zabio: { compaction: "off" } });
console.log(r.zabio.compacted ?? false);

Some models do not take every sampling parameter: OpenAI’s reasoning models take no temperature or top_p, recent Claude models neither, Gemini models ignore both. Zabio never fails a request for that. It leaves the parameter out of the request to the model that answers, names it in zabio.dropped_parameters and in the x-zabio-dropped-params header, and the answer comes back as usual. Under auto this depends on the model chosen, so read the field rather than assuming. stop is never dropped: where the model’s provider does not take it, Zabio cuts the answer at the first sequence itself, streamed or not. The list per model is on the Models page.

Fields Zabio accepts so SDK defaults do not break, but does not act on, are listed in zabio.ignored_parameters. On chat completions: seed, logit_bias, presence_penalty and frequency_penalty, reported only when they carry a non default value. On the Responses API: include, truncation and user, reported whenever present. The list is omitted when empty.

curl

Terminal window
curl -s https://api.zabio.ai/v1/chat/completions \
-H "Authorization: Bearer insert your zabio.ai key here" \
-H "Content-Type: application/json" \
-d '{"model": "auto", "messages": [{"role": "user", "content": "Hello"}], "seed": 42, "presence_penalty": 0.5}' | jq .zabio.ignored_parameters

Python

r = client.chat.completions.create(model="auto", messages=[{"role": "user", "content": "Hello"}], seed=42, presence_penalty=0.5)
print(r.model_extra["zabio"].get("ignored_parameters")) # ['seed', 'presence_penalty']

Node

const r = await client.chat.completions.create({ model: "auto", messages: [{ role: "user", content: "Hello" }], seed: 42, presence_penalty: 0.5 });
console.log(r.zabio.ignored_parameters); // [ 'seed', 'presence_penalty' ]

Refused with 400 unsupported_parameter:

  • n greater than 1; Zabio serves exactly one choice
  • logprobs and top_logprobs
  • audio output and any modalities entry other than text
  • prediction and web_search_options
  • the legacy function message role (use tool)
  • content parts other than text and image_url on chat completions; input items other than message, function_call and function_call_output, and content parts other than input_text, text, output_text and input_image on the Responses API
  • tools other than type: "function" (400 invalid_value)

Accepted and ignored:

  • seed, logit_bias, presence_penalty, frequency_penalty on chat completions (reported in zabio.ignored_parameters)
  • stream_options.include_usage: the usage chunk is always sent
  • store on chat completions: nothing is stored either way
  • include, truncation, user on the Responses API (reported in zabio.ignored_parameters); background is honoured (see Background responses)
  • image_url.detail: images are always sent as bytes
  • cache.ttl on OpenAI models: caching is automatic there

Behaves differently:

  • model may be auto, auto/best, auto/balanced or auto/cheap; the response model is the model that answered, not the one you sent
  • every response carries a zabio object, and model objects carry zabio with class, capabilities and pricing
  • system and developer messages are joined into one system prompt
  • usage.prompt_tokens includes cached tokens; prompt_tokens_details.cached_tokens is the cached part
  • response_format: json_object on Anthropic models is best effort through prompting, see Chat completions
  • the Responses API stores by default and creates a conversation when none is given; store: false still reads the stored history but keeps nothing
  • max_output_tokens on the Responses API defaults to 4096; max_tokens on chat completions defaults to 16000
  • reasoning_effort and reasoning.effort take one scale, none to xhigh, mapped onto each model’s own control; zabio.reasoning reports what was applied, and on auto with no level set Zabio chooses the setting with the model
  • streamed Responses API item ids are msg_<response id>_<index> until response.completed reports the stored ids
  • images are downloaded by Zabio and sent to the vendor as bytes; private and internal addresses are refused
  • errors keep OpenAI’s envelope but use Zabio’s codes (model_not_found, insufficient_credits, quota_exceeded, upstream_error and the rest on Errors); the vendor’s own error body never reaches you
  • the request id is in the x-request-id header, never in the body, and one you send is not reused
  • there are no idempotency keys, no fine tuning, no assistants and no audio or image generation endpoints; /v1/files accepts the batch purpose only and /v1/batches the /v1/chat/completions endpoint only