API reference

One OpenAI-compatible API in front of 20 LLM providers. Cailos routes each request to the endpoint that fits it, falls back automatically when one fails, and records what happened.

Authentication

Every request requires an API key in the Authorization header:

Authorization: Bearer cai_your_key_here

Create keys in the dashboard (per-team). Keys are Argon2-hashed at rest; the full key is shown only once, when it's created.

Teams can also add their own provider keys in the dashboard and choose whether they're used first, as a fallback, or exclusively. cailos.key_source on each response says which key served it.

Base URL


    

Set this as base_url in any OpenAI SDK. All paths below are relative to this.

Quickstart

Pythonfrom openai import OpenAI

client = OpenAI(
    base_url=,
    api_key="cai_your_key_here",
)

response = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Hello"}],
)
print(response.choices[0].message.content)
cURLcurl /chat/completions \
  -H "Authorization: Bearer cai_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "auto",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
Works with the OpenAI SDKs. Python, Node.js, Go, Rust or plain HTTP: set base_url and api_key.

List models

GET /v1/models

Returns the model codenames your team can use (after its trust level and excluded providers and models), plus your team's custom models. Each entry carries a capabilities object describing what the model can serve for your team. Models whose endpoints are all temporarily out of rotation are left out.

Response{
  "object": "list",
  "data": [
    { "id": "auto", "object": "model", "capabilities": { "vision": true } },
    { "id": "claude-sonnet-5-5", "object": "model", "capabilities": { "vision": true } },
    { "id": "deepseek-4-1-flash", "object": "model", "capabilities": { "vision": false } }
  ]
}
CapabilityMeaning
visionA request with images in messages can be served. auto is vision-capable whenever any vision model is available to your team; a custom model is vision-capable if any of its targets is.
Capabilities reflect your team's trust level and excluded providers and models, so two teams can see different answers. Temporary outages and budget caps don't change them. capabilities is a Cailos extension; OpenAI SDKs ignore it.

Retrieve a model

GET /v1/models/{model}

Returns one model and its capabilities. Accepts any model string a chat request would, including strategy and mode suffixes (auto:speed, gpt-5-6-luna:cost) and custom model names, so you can check exactly what you're about to send. A retired model's codename returns the capabilities of the model now serving it. Returns 404 if the model is unknown to your team.

Response{
  "id": "auto:speed",
  "object": "model",
  "created": 0,
  "owned_by": "cailos",
  "capabilities": { "vision": true }
}

Chat completions

POST /v1/chat/completions

Send a conversation, get a completion. Requests and responses use the OpenAI Chat Completions format. Parameters not listed below (for example reasoning_effort, stream_options, parallel_tool_calls) are ignored.

Request body

ParameterTypeDescription
model string required A model codename (gpt-5-6-luna, claude-sonnet-5-5), a custom model's name, or auto to let Cailos choose. Add a strategy or an execution mode with a suffix: auto:speed, gpt-5-6-luna:cost, auto:quality:judge. See Routing.
messages array required The conversation. Each message has role ("system", "user", "assistant" or "tool") and content (string or multipart array). A tool message needs tool_call_id. The developer role isn't accepted; send those instructions as system.
stream boolean optional Stream response as Server-Sent Events. See Streaming.
tools array optional Tool/function definitions. See Tool calls.
tool_choice string | object optional "auto", "none", "required", or {"type": "function", "function": {"name": "..."}}.
response_format object optional Force JSON output, optionally to a schema. See Structured output.
max_tokens integer optional Max tokens to generate. Endpoints with a lower output limit are skipped, and the value sent is capped at 20,000. max_completion_tokens is an alias (takes precedence if both are set).
temperature float optional 0–2. Higher is more random. Dropped for OpenAI reasoning models, Claude 4.7 and later, and Claude while thinking; set to 1 for reasoning models on OpenAI-compatible providers. 0 turns Claude's thinking off.
top_p float optional 0–1. Nucleus sampling. Not sent where temperature isn't.
stop string | array optional Up to 4 stop sequences. OpenAI models have no equivalent and ignore it.
presence_penalty float optional -2–2. OpenAI-compatible providers only; ignored elsewhere.
frequency_penalty float optional -2–2. OpenAI-compatible providers only; ignored elsewhere.
seed integer optional Deterministic sampling (best effort). OpenAI-compatible providers only.
n integer optional Number of completions. Only 1 is reliably supported: OpenAI models refuse more, and other providers return one.
logit_bias object optional Token ID to bias value mapping. OpenAI-compatible providers only.
user string optional End-user identifier for abuse detection.
cailos object optional Routing hints and per-request settings. See Routing hints.

Response format

Standard OpenAI chat completion format, plus a cailos metadata object:

Response{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "object": "chat.completion",
  "model": "gpt-5.4-mini-2026-03-17",
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "Hello! How can I help you?",
      "reasoning_content": null,     // present only for reasoning models
      "tool_calls": null              // present only when tools are invoked
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21,
    "prompt_tokens_details": { "cached_tokens": 0 }
  },
  "cailos": {
    "optimise": "quality",
    "provider": "openai",
    "endpoint": "gpt-5.4-mini-2026-03-17",
    "trust_level": 2,
    "detected_languages": ["en"],
    "tier": "paid",
    "key_source": "cailos",
    "redirected_to": null,
    "compression": { "original_input_tokens": 11, "input_tokens_post_substitute": 9, "total_messages": 1, "compressed_messages": 1 }
  }
}
FieldDescription
idThe request's ID in Cailos (a UUID). Find the request in your dashboard with it, or pass it to feedback.
message.contentThe model's text response. null when the model responds with tool calls only.
message.reasoning_contentThe model's reasoning, for reasoning models. Absent otherwise. See Reasoning.
message.tool_callsFunction calls made by the model. See Tool calls.
finish_reason"stop", "length", "tool_calls" or "content_filter".
usage.reasoning_tokensTokens used for thinking, when the provider reports them. Included in completion_tokens.
usage.prompt_tokens_details.cached_tokensInput tokens read from the provider's prompt cache (OpenAI and Anthropic). See Prompt caching.
cailos.optimiseStrategy used: "speed", "cost", "quality", or "balanced".
cailos.providerProvider that served the request (e.g. "openai", "anthropic").
cailos.endpointProvider's native model ID.
cailos.trust_levelTrust level of the endpoint (0–3). Details
cailos.detected_languagesAuto-detected input languages (e.g. ["en", "he"]), or null.
cailos.tierYour team's plan ("free" or "paid").
cailos.redirected_toWhen the model you asked for has been retired, the model now serving its requests. Switch to it to stop being redirected. Otherwise null.
cailos.key_source"cailos", or "byok" when your team's own provider key served the request.
cailos.compressionWhat prompt compression did: original_input_tokens, input_tokens_post_substitute, total_messages, compressed_messages. Estimates only; billing uses usage.
The cailos object is a Cailos extension, not part of the OpenAI format. SDKs ignore unknown keys, so it's safe to leave in your response handling. Fields not documented here are internal and may change.

Examples

Streaming

Set stream: true to receive Server-Sent Events. Each event is a ChatCompletionChunk with incremental deltas, ending with data: [DONE]. The first chunk carries the cailos object, and the last chunk before [DONE] has an empty choices array and the usage totals, so check choices before indexing it.

Pythonstream = client.chat.completions.create(
    model="auto:speed",
    messages=[{"role": "user", "content": "Explain quantum computing"}],
    stream=True,
)

for chunk in stream:
    if not chunk.choices:   # the final chunk carries usage only
        continue
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="")
Responsedata: {"choices": [{"delta": {"content": "Hello"}}]}

data: {"choices": [{"delta": {"content": "!"}}]}

data: {"choices": [{"delta": {}, "finish_reason": "stop"}]}

data: {"choices": [], "usage": {"prompt_tokens": 9, "completion_tokens": 2, "total_tokens": 11}}

data: [DONE]

Tool calls

Define tools in the request. When the model invokes one, finish_reason is "tool_calls" and content is null:

Pythonresponse = client.chat.completions.create(
    model="auto:quality",
    messages=[{"role": "user", "content": "What's the weather in Amsterdam?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"]
            }
        }
    }],
)
Response{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call_abc123",
        "type": "function",
        "function": {
          "name": "get_weather",
          "arguments": "{\"city\": \"Amsterdam\"}"
        }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

To continue, append the assistant message (with tool_calls) and a tool message (with the result) back into messages.

Structured output

Force JSON with response_format: {"type": "json_object"}, or {"type": "json_schema", "json_schema": {...}} to follow a schema. Only endpoints that support structured output are considered.

Pythonresponse = client.chat.completions.create(
    model="auto:quality",
    messages=[{"role": "user", "content": "List 3 planets as JSON"}],
    response_format={"type": "json_object"},
)

data = json.loads(response.choices[0].message.content)
# {"planets": ["Mercury", "Venus", "Earth"]}
Repair. If the model wraps JSON in code fences, leaves trailing commas or truncates it, Cailos repairs it before returning. If it still isn't valid JSON, the next endpoint is tried. Streamed responses are passed through unrepaired.

Vision

Send images as multipart content in the messages array:

Pythonresponse = client.chat.completions.create(
    model="auto:quality",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "What's in this image?"},
            {"type": "image_url", "image_url": {
                "url": "https://example.com/photo.jpg"
            }}
        ]
    }],
)

Both URLs and base64 data URIs (data:image/png;base64,...) are supported, and a message can carry several image_url parts. Cailos auto-detects vision requirements and routes to a capable model.

Pinned models. auto and custom models route around models that can't see images. A specific codename that can't returns 404. Check capabilities.vision before attaching images, and remember every image in the history counts on every later turn.

Reasoning

When a reasoning model answers (DeepSeek R1, Qwen3, Claude with extended thinking, Gemini thinking models, OpenAI GPT-5 and o-series), its reasoning is returned in reasoning_content, never as raw tags in content. For OpenAI models this is OpenAI's reasoning summary.

Response{
  "choices": [{
    "message": {
      "role": "assistant",
      "content": "The answer is 42.",
      "reasoning_content": "Let me think through this step by step..."
    }
  }],
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 120,
    "reasoning_tokens": 98
  }
}
Pythonmsg = response.choices[0].message
print(msg.content)             # The actual answer

if hasattr(msg, "reasoning_content") and msg.reasoning_content:
    print(msg.reasoning_content)   # Chain-of-thought
  • reasoning_content is absent for non-reasoning models.
  • reasoning_tokens, when the provider reports them, are included in completion_tokens.
  • Thinking tags (<think>, <thinking> and similar) are extracted automatically, so they never appear in content.

Routing

The model field takes a codename (gpt-5-6-luna, claude-sonnet-5-5). The same codename can be served by several providers, and Cailos ranks their endpoints by the strategy. When model is "auto", every model is a candidate.

Append a strategy suffix to any model value with : to control how providers are ranked:

SyntaxBehaviour
autoEvery model is a candidate. A classifier picks the strategy from the latest user message (balanced when uncertain).
gpt-5-6-lunaThat model on its best endpoint, with the strategy predicted from the latest user message.
model:speedFastest response: ranks mostly (70%) on how soon the first token arrives and how fast the rest streams; quality and cost decide between close candidates.
model:costCheapest: ranks mostly (70%) on $/M tokens; quality and speed decide between close candidates.
model:qualityMost capable: ranks mostly (70%) on eval scores; cost and speed decide between close candidates.
model:balancedComposite blend of quality (40%), speed (35%), and cost (25%).

Where model is auto, a codename or a custom model. Examples: auto:speed, gpt-5-6-luna:cost, claude-sonnet-5-5:quality.

Candidates are compared with each other, not against a fixed scale, and by how far apart they are: in a pool of a $3 and a $6 model, the $3 one is the cheap end. Differences too small to matter count as near-ties, so a model 3% dearer but much more capable can win a :cost request.

Choosing a strategy

Use caseRecommended
Real-time chat, autocomplete, typing indicators:speed: favours the shortest measured response times and highest throughput.
High-volume batch processing, background jobs, bulk classification:cost: routes to the cheap end of the candidates.
Complex reasoning, code generation, high-stakes analysis:quality: picks the most capable candidate, unless one far dearer is only marginally better.
General-purpose, unsure, or mixed workloads:balanced, or omit the suffix: blends all three factors.
Tip. When you name a model like gpt-5-6-luna without a suffix, Cailos predicts the strategy from the latest user message. Add a suffix only when you want explicit control.

On the free plan every request is routed for cost, whatever the suffix says. Strategy suffixes and the optimise hint take effect on paid plans.

Fallback

Each request gets a short ranked list of endpoints, usually three and from at least two providers when possible. If one fails or times out, the next is tried. If a stream fails after it started answering, the next endpoint able to continue a partial answer (Cailos tests each one) carries on from the text you already have, so the response stays one answer. Each recent failure, rate limits included, moves an endpoint down the ranking; the effect fades within the hour. After three failures in a row, or when half of its calls in the last two minutes failed, an endpoint is taken out of rotation until probes show it has recovered.

When a model is retired, its codename keeps working: requests go to the closest replacement and cailos.redirected_to names it. Switch to that codename to stop being redirected.

Execution modes

Routing sends each request to one endpoint. Add an execution mode to the model string to put more than one model on the question. A mode combines with a strategy, in any order (auto:cost:race, auto:race:cost), and a request can name one mode.

SyntaxBehaviour
model:raceThe two best-ranked endpoints start together, and a failed one is replaced by the next. The first to answer (when streaming, the first to send a token) serves the reply and the other is stopped. While it streams, the answer moves to another endpoint when the stream goes quiet for 100 ms (cailos.baton_pass_ms) and another endpoint delivers a token sooner, or when it streams slower than the typical speed of the endpoints in the race and a faster one is waiting. The new endpoint continues from the text so far, without a break in the response. There's no handover once tool calls have started streaming.
model:judgeTwo different models answer in full, then a third different model picks one. You receive the chosen answer verbatim. Nothing streams until the verdict is in.
model:wizardTwo different models draft, then a third different model writes the reply from both drafts.
Pythonresponse = client.chat.completions.create(
    model="auto:quality:judge",
    messages=[{"role": "user", "content": "Review this contract clause for indemnity risk"}],
)

print(response.cailos.get("winner"))   # the endpoint whose answer you received; absent if the mode couldn't run

The models taking part come from the same ranked list a plain request would use, so the strategy and your trust level and exclusions still apply. The cailos object says what happened: mode names the mode and contenders lists the endpoints that took part. For race and judge, winner is the endpoint whose answer you received; for wizard, the model that wrote the reply is wizard.endpoint. When there aren't enough models for the mode to run, winner and contenders are absent and the mode's own object carries fallback. If the judge or writer can't finish, the top-ranked candidate's reply is served and the mode's object names the reason in fallback. A race with only one eligible endpoint runs as a normal request.

Not enough models. Judge and wizard need three different models. When fewer are eligible (a pinned codename, a strict trust level, a small custom model), the request is routed normally and the response says so: "judge": {"verdict": null, "fallback": "not_enough_models", "models_available": 2, "models_needed": 3}. Execution modes are on paid plans; on the free plan a request that names one returns 403.

Custom models

Teams can create custom virtual models in their dashboard that operate in one of two routing modes:

  • Dynamic pool: Defines an allowed candidate subset of models (for example gpt-5-6-luna, claude-sonnet-5-5, gemini-3-8-flash). For each request, Cailos ranks every endpoint of those models and routes to the best one for the active strategy: fastest for speed, cheapest for cost, most capable for quality.
  • Routing instructions (dynamic pools): Describe in your own words which models should take which requests, for example “code to Claude, everything else to GPT; the harder the question, the more quality matters”. Cailos translates this into a routing plan. For each request, Jev, TypeSafe's classification model, reads the end of the latest user message (its last 800 characters, with PII cloaked; earlier turns are not sent) and the plan either narrows the pool to the right models or slides its ranking between two strategies (for example from cost towards quality). A strategy suffix or optimise hint, or the free plan's cost routing, overrides the slide; narrowing still applies. If Jev can't answer, the pool routes as usual.
  • Fallback chain: Evaluates models in a strict sequential precedence tier (for example gpt-5-6-luna → claude-sonnet-5-5 → auto:speed). Cailos tries the first model's endpoints first and moves down the chain when they fail (errors, timeouts, rate limits, invalid JSON). Models that can't serve the request, for example without vision, are skipped.
Pythonresponse = client.chat.completions.create(
    model="my-custom-model",
    messages=[{"role": "user", "content": "Summarize this document"}],
)

# You can also override the optimisation strategy dynamically:
fast_response = client.chat.completions.create(
    model="my-custom-model:speed",
    messages=[{"role": "user", "content": "Ping"}],
)

Custom models work with the rest of Cailos:

  • Strategy overrides: Configure a default strategy on the custom model (e.g. quality), or override per request via suffix (e.g. my-custom-model:cost).
  • Model discovery: Active custom models for your team appear in GET /v1/models when authenticated with your team API key.
  • Playground dry-run: Custom models in both dynamic pool and fallback modes get a real-time routing preview in the Playground, showing which underlying model and provider would be picked. The preview leaves routing instructions out; the custom model editor's “Try it” shows what they decide.

Routing hints

Fine-tune routing with the cailos object. Most fields are detected from the request, so set them only to override.

FieldTypeDescription
optimise string "speed", "cost", "quality" or "balanced". The same as a strategy suffix; a suffix on model wins if both are set.
trust_level integer 0–3. Only routes to endpoints at or above this trust level. Cannot go below your team's floor. Details
require_tools boolean Only route to models with function calling support. Auto-detected from tools.
require_vision boolean Only route to models with image input support. Auto-detected from image_url in messages.
require_structured_output boolean Only route to models with JSON output support. Auto-detected from response_format.
require_web_access boolean Only route to models with web access.
timeout integer Seconds to wait for each provider call, 1–600. Default 300.
compression boolean Turn prompt compression on or off for this request, overriding your team's setting.
modifiers array An execution mode as a hint: ["race"], ["judge"] or ["wizard"]. The same as the model suffix.
baton_pass_ms integer Race mode: how long a stream may go quiet before the next endpoint is started to take over. Default 100.
language string Language code (en, pt-BR, any BCP 47 tag). Only routes to models tagged with this language; 404 if none can, 400 for an unknown language. Without it, Cailos detects the languages of user messages and prefers models that support them when routing auto or a custom pool.
JSON{
  "model": "auto",
  "messages": [...],
  "cailos": {
    "trust_level": 2,
    "language": "en"
  }
}
Auto-detection. Fields marked auto-detected are inferred from your request payload. You don't need to set them unless you want to override.

Privacy and compression

PII protection

Before a request leaves Cailos, email addresses, phone numbers, credit card numbers and IP addresses in messages are replaced with placeholders by llmshield. The placeholders are swapped back in the reply, so you see the original values and the provider never does. Names, places and organisations are left as they are, so code and documentation aren't mangled.

What the provider seesRefund card <CREDIT_CARD_2>, then email <EMAIL_0> or call <PHONE_1>.

Cailos doesn't store the text of your prompts. It keeps the routing decision, latency, token counts and cost for each request.

Prompt compression

Cailos shortens prompts before sending them, with fixed word-level substitutions: filler phrases are removed and wordy ones shortened (“in order to” becomes “to”; articles such as “the” are dropped). Code blocks, inline code, XML and HTML elements and tool messages are left untouched. The response's cailos.compression says how much changed.

Compression is on unless your team turns it off in the dashboard. Set cailos.compression to false or true to override it for one request.

Prompt caching

Multi-turn conversations tend to stay on the endpoint that served the previous turn when its provider caches prompts, which makes later turns cheaper and faster. Anthropic prompt caching is turned on automatically. Cached input shows in usage.prompt_tokens_details.cached_tokens.

Feedback

Submit human feedback on any routed request. Satisfaction ratings per endpoint feed into model selection over time.

POST /accounts/requests/{id}/feedback/

The {id} in the URL is the id field returned in every chat completion response; pass it through unchanged. No authentication is needed: the id is unguessable, and feedback is accepted for 1 week after the request. After that the endpoint returns 410 Gone. An unknown id returns 404.

Request body

ParameterTypeDescription
positive string required "1" for thumbs up; any other value is thumbs down. Missing returns 400.
comment string optional Short note on what went wrong or right. Cut to 100 characters.
cURLcurl -X POST /accounts/requests/550e8400-e29b-41d4-a716-446655440000/feedback/ \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d 'positive=0&comment=wrong language in response'

The endpoint returns an HTML partial (designed for htmx). The feedback is also visible on the request detail page in the dashboard, and aggregated satisfaction ratings appear per endpoint on the model browse page.

Why give feedback? Even sparse feedback (a few percent of requests) gives Cailos a quality signal per endpoint and task type that static eval scores can't capture. Over time this directly improves which provider gets picked for your workload.

Errors

All errors follow the OpenAI error envelope. For validation errors, param names the field.

{
  "error": {
    "message": "Model 'nonexistent' not found or not active.",
    "type": "not_found_error",
    "param": null,
    "code": null
  }
}
StatusMeaning
200Success.
400Malformed JSON or invalid parameters (the field is in param), an unsupported language, or two execution modes in one request.
401Missing or invalid API key, or the team is paused.
403The model is excluded for your team, or the plan doesn't include the execution mode the request names.
404Unknown model, or no endpoint can serve this request. The message names the constraint (for example supports_vision, a context window, trust level, language or an exhausted budget). Also returned while every endpoint of the model is temporarily out of rotation.
429Rate limit, daily request limit, input token limit, or a team or owner monthly budget reached. See Rate limits. Only the per-minute limit sends Retry-After.
502Every endpoint tried returned an error.
503No endpoint could be tried right now (all recovering from failures).
504Upstream provider timed out.

Once a stream has started, errors arrive as an event instead of a status code: data: {"error": {"message": "...", "type": "provider_error"}} followed by data: [DONE]. Types are provider_error, provider_timeout, model_unavailable and server_error. A stream cut off mid-answer that no other endpoint could continue ends with finish_reason: "error".

Rate limits

Limits are per team, across all its keys, and set by plan. Each returns 429 when reached.

LimitFreePaid
Requests per minute10No limit
Requests per day300No limit
Input tokens per request10,000No limit
Active API keys210
StrategyAlways costAny
Execution modesNonerace, judge, wizard

The per-minute window starts with the first request and lasts 60 seconds. Monthly budgets set on a team, or on its owner across all their teams, also return 429 once spent, and reset on the 1st.

Providers

You never talk to providers directly; send a codename and Cailos does the rest. For transparency, this is how each one is called:

ProviderFormatNotes
OpenAIconvertedResponses API. Reasoning summaries come back as reasoning_content. stop, seed, penalties and logit_bias have no equivalent and are dropped; n above 1 is refused. Nothing is stored at OpenAI.
AnthropicconvertedSystem prompt, tools, vision, thinking (adaptive on Claude 4.6 and later) and structured output (json_schema as output_config; json_object as an instruction) are translated. Prompt caching is on automatically. temperature and top_p are left out for Claude 4.7 and later, and while thinking, as the API requires.
GoogleconvertedGemini API. Thinking parts extracted.
Cohereconvertedv2 Chat API.
Groq, Together, DeepInfra, Cerebras, Scaleway, SambaNova, xAI, OpenRouter, Novita, NCompass, NScale, Inception, Tinfoil, BytePlus, Cloudflare, DarkbloomnativeOpenAI-compatible.