Slow connectionWeak connection. We are continuing to load the ordinary chat; keep this page open if a message is being sent.

Start here

Connect your coding agent

Pick the client you already use. Claude Code takes the host root; Codex and OpenAI-compatible tools take /v1. Each guide is a complete setup: key, config file, model, verification and fixes.

Create key
One keyThree native protocolsMetered per request

Both addresses belong to the same API and the same key. The difference is only what the client appends.

ClientBase URL · Endpoint it calls

Claude Code

Anthropic Messages

Native Anthropic Messages. One settings.json and you are in.

https://api.sanero.ai

POST /v1/messages

Codex CLI

OpenAI Responses

Native Responses API with a Codex model catalogue.

https://api.sanero.ai/v1

POST /v1/responses

OpenAI SDK & editors

Chat Completions

Cursor, Cline, Continue, Kilo, Aider and raw Chat Completions.

https://api.sanero.ai/v1

POST /v1/chat/completions

The Markdown contains both base URLs, complete settings.json and config.toml files, live model ids and error codes. Paste it into Claude Code, Codex or another agent.

01 · Anthropic Messages

Claude Code

5 steps: install → key → config → model → verify

Claude Code speaks the Anthropic Messages API. Sanero accepts it natively at /v1/messages, including streaming text, tool calls, tool results, images and Claude Code's thinking controls. Claude Code appends /v1/messages itself, so ANTHROPIC_BASE_URL is the host root — no /v1.

Use the host root, not the OpenAI /v1 address

If you put /v1 in ANTHROPIC_BASE_URL, Claude Code calls /v1/v1/messages and fails with 404. The root and the /v1 address use the same key and the same account.

ANTHROPIC_BASE_URL=https://api.sanero.ai
  1. 1Install Claude Code
    npm
    shell
    npm install -g @anthropic-ai/claude-code
    claude --version

    A current Claude Code release works. The npm package is identical on macOS, Linux and Windows; Anthropic's native installer is also supported.

  2. 2Create a Sanero key
    1. 01Open the dashboard → API keys → Create key.
    2. 02Enable the chat and models scopes. Claude Code uses models for /model discovery.
    3. 03Copy the key once. It is shown only at creation.
    Open API keys
  3. 3Write the complete settings file

    Claude Code reads ~/.claude/settings.json. It does not read a file called config.json. The file below is complete — five lines are enough. Advanced switches live in the collapsed section.

    ~/.claude/settings.json
    json
    {
      "env": {
        "ANTHROPIC_BASE_URL": "https://api.sanero.ai",
        "ANTHROPIC_AUTH_TOKEN": "sk-sanero-…",
        "ANTHROPIC_MODEL": "<model-id>",
        "ANTHROPIC_DEFAULT_HAIKU_MODEL": "<model-id>",
        "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT": "1"
      }
    }

    Settings file: ~/.claude/settings.json · Windows: %USERPROFILE%\.claude\settings.json

    Advanced settings

    Discovery, model aliases and experimental beta-field suppression. Needed only for MCP, non-standard ids, or clients that send extra beta blocks.

    settings.json · advanced
    json
    {
      "env": {
        "ANTHROPIC_BASE_URL": "https://api.sanero.ai",
        "ANTHROPIC_AUTH_TOKEN": "sk-sanero-…",
        "ANTHROPIC_MODEL": "<model-id>",
        "ANTHROPIC_DEFAULT_OPUS_MODEL": "<model-id>",
        "ANTHROPIC_DEFAULT_SONNET_MODEL": "<model-id>",
        "ANTHROPIC_DEFAULT_HAIKU_MODEL": "<model-id>",
        "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1",
        "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT": "1",
        "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1",
        "ENABLE_TOOL_SEARCH": "false"
      }
    }
  4. 4Choose the models

    The main model does the work; the small/fast model handles titles, summaries and background helpers. Both ids come from your live catalogue. The template also pins the sonnet and opus aliases to the main model, so /model sonnet and /model opus cannot fall through to an unknown id.

    Reasoning level

    Catalogue is loading…

    Levels are read from the live catalogue

    Model discovery only lists ids that contain claude or anthropic. A gpt-, gemini- or kimi- id will not appear in /model; pin it with ANTHROPIC_MODEL instead.

    CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1 makes Claude Code send the effort parameter even for model ids it does not recognise. Without it, a custom gateway id falls back to a fixed thinking budget and Claude Code may not send the effort level at all. Sanero maps either form to the nearest supported level.

    CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 and ENABLE_TOOL_SEARCH=false prevent Claude Code from sending beta fields and MCP tool-search blocks that an OpenAI-compatible upstream cannot consume. The template already includes both.

    ANTHROPIC_SMALL_FAST_MODEL is deprecated. Use ANTHROPIC_DEFAULT_HAIKU_MODEL, as in the template.

  5. 5Prove the connection

    Start with a non-interactive request. It checks auth, model routing and streaming in one shot; then open the interactive client and inspect /status and /model.

    verify
    shell
    claude -p "Reply with exactly: sanero-ok"

    Expected: sanero-ok

    Raw protocol check

    curl
    shell
    curl -sS https://api.sanero.ai/v1/messages \
      -H "Authorization: Bearer sk-sanero-…" \
      -H "anthropic-version: 2023-06-01" \
      -H "content-type: application/json" \
      -d '{
        "model": "<model-id>",
        "max_tokens": 32,
        "messages": [{"role": "user", "content": "Reply with exactly: sanero-ok"}]
      }'
    inside Claude Code
    shell
    /status
    /model

How thinking maps to reasoning levels

Claude Code sends thinking.budget_tokens for fixed budgets, or adaptive thinking with output_config.effort on newer profiles. Sanero maps the request to the selected model's reasoning ladder and picks the nearest supported level; it does not reject the turn just because the exact level is missing. The model's visible reasoning is streamed back as Anthropic thinking blocks with signature deltas, so Claude Code can render it instead of hiding it. Lower MAX_THINKING_TOKENS if you want cheaper turns.

BudgetLevel
≤ 1,024minimal / low
≤ 4,096medium
≤ 8,192high
≤ 16,000xhigh
31,999 (Claude Code default)max / highest supported

Protocol notes

FieldBehaviour
EndpointPOST /v1/messages; POST /v1/messages/count_tokens
AuthAuthorization: Bearer … or x-api-key: …
Contenttext, image, tool_use, tool_result; input thinking blocks are ignored; output thinking is returned; document blocks are not supported
Visible reasoningstreaming thinking blocks with thinking_delta + signature_delta; non-streaming thinking blocks with a signature
Streamingmessage_start → content_block_* → message_delta → message_stop; ping events keep the connection alive
ErrorsAnthropic envelope: authentication_error, not_found_error, rate_limit_error, overloaded_error, api_error
Beta / cache headersaccepted for client compatibility; Anthropic server-side prompt caching and beta server tools are not implemented

If it does not work

401 / authentication_error

The shell or settings file did not load the key. Use ANTHROPIC_AUTH_TOKEN for Bearer or ANTHROPIC_API_KEY for x-api-key; do not set both to different values.

404 / not_found_error

The model id is not in your catalogue. Pick a live id above, or update ANTHROPIC_MODEL and ANTHROPIC_DEFAULT_HAIKU_MODEL.

404 on /v1/v1/messages

ANTHROPIC_BASE_URL includes /v1. Remove it: Claude Code appends /v1/messages itself.

400 / insufficient balance

Top up the wallet. Agent turns reserve up to 32,768 output tokens, so keep headroom.

413 / request_too_large

The context is larger than the model or the 4 MB request limit. Run /compact.

400 unsupported content block

Remove document or server-tool blocks; local text, image, tool_use and tool_result are supported.

400 naming tool_reference or defer_loading

Set CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 and ENABLE_TOOL_SEARCH=false, then restart Claude Code. MCP tool search is not bridged by this gateway.

The model is missing from /model

Discovery keeps only ids containing claude or anthropic. For any other id, set ANTHROPIC_MODEL explicitly.

Effort / reasoning is ignored

Set CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1. Claude Code does not send effort for unrecognised gateway ids by default.

Tools never execute

Use a tool-capable coding model from the catalogue; the protocol layer streams tool_use correctly.

Small helper model fails

Set ANTHROPIC_DEFAULT_HAIKU_MODEL to a cheap model in your account. ANTHROPIC_SMALL_FAST_MODEL is deprecated.

Warning about /api/hello

Claude Code probes this path to warm the connection; the probe is best-effort. Sanero does not implement it, and the first real /v1/messages request still works.

unrecognized_model warning

Cosmetic when the selected Sanero id is not a Claude-named id. Use a claude-* model if you want the warning gone.

What leaves your machine

Claude Code sends prompts, selected file contents, tool results and images to the gateway so the model can answer. They are processed under the same API retention policy. Keep the key in an environment variable or a private settings file, never in a project that is committed.

Cost

Every turn is a normal metered API request. A turn is capped at 32,768 output tokens (or the model's lower limit); the hold is released after the response and the exact charge appears in the dashboard. Agent clients may display their own estimate.

02 · OpenAI Responses

Codex CLI

5 steps: install → key → config.toml → model → verify

Codex uses the OpenAI Responses API. Sanero serves /v1/responses and a Codex-native model catalogue at /v1/models?catalog=codex. The catalogue supplies context windows, reasoning levels and the freeform apply_patch tool, so Codex does not fall back to generic metadata.

The Codex base URL includes /v1

Codex appends /responses to base_url. Use https://api.sanero.ai/v1, not the host root. The same base URL is used for the model catalogue.

base_url=https://api.sanero.ai/v1
  1. 1Install Codex CLI
    npm
    shell
    npm install -g @openai/codex
    codex --version

    A current Node.js is required. Homebrew and the native installer are also supported; npm is identical on macOS, Linux and Windows.

  2. 2Create a Sanero key
    1. 01Open the dashboard → API keys → Create key.
    2. 02Enable the chat and models scopes. Codex fetches /v1/models for its catalogue.
    3. 03Export the key as SANERO_API_KEY in the same shell that starts Codex.
    Open API keys
  3. 3Write ~/.codex/config.toml

    This file is TOML, not JSON: Codex does not read config.json. Keep model, model_provider, model_reasoning_effort and web_search before the first table header, otherwise Codex treats them as provider settings. The file below is complete; compatibility flags are collapsed.

    ~/.codex/config.toml
    toml
    model = "<model-id>"
    model_provider = "sanero"
    model_reasoning_effort = "high"
    web_search = "disabled"
    
    [model_providers.sanero]
    name = "Sanero"
    base_url = "https://api.sanero.ai/v1"
    env_key = "SANERO_API_KEY"
    wire_api = "responses"
    model_catalog_url = "https://api.sanero.ai/v1/models?catalog=codex"

    macOS/Linux: ~/.codex/config.toml · Windows: %USERPROFILE%\.codex\config.toml

    Model catalogue & compatibility

    The ?catalog=codex query makes the catalogue deterministic. Without it, /v1/models can return the generic OpenAI list instead of Codex metadata.

    If your Codex build reports “model metadata not found”, remove model_catalog_url and keep an explicit model. Upgrading Codex restores catalogue metadata.

    config.toml · advanced
    toml
    model = "<model-id>"
    model_provider = "sanero"
    model_reasoning_effort = "high"
    web_search = "disabled"
    
    [model_providers.sanero]
    name = "Sanero"
    base_url = "https://api.sanero.ai/v1"
    env_key = "SANERO_API_KEY"
    wire_api = "responses"
    model_catalog_url = "https://api.sanero.ai/v1/models?catalog=codex"
    
    [model_providers.sanero.capabilities]
    remote_compaction = "unsupported"
    external_web_access = false
  4. 4Choose the model and reasoning level

    Codex reads context windows and supported reasoning levels from the Sanero catalogue. Use a level this model actually advertises — Codex validates the value before sending.

    Reasoning level

    Catalogue is loading…

    Levels are read from the live catalogue

  5. 5Prove the connection

    Run a non-interactive prompt first. A response without “model metadata not found” means the catalogue and the model route were loaded.

    shell
    export SANERO_API_KEY="sk-sanero-…"
    codex exec --model <model-id> "Reply with exactly: sanero-ok"

    Expected: sanero-ok

    inside Codex
    shell
    /status

Tools Codex will use

Sanero advertises a direct tool profile, so Codex sends exec_command, write_stdin, view_image, apply_patch (freeform custom tool), request_user_input and multi-agent namespace tools. apply_patch is translated through a JSON wrapper and back to a custom_tool_call, so Codex can edit files normally. Deferred tool search is not implemented; MCP tools that Codex sends as direct function definitions work normally.

Protocol notes

FieldBehaviour
EndpointPOST /v1/responses; GET /v1/models?catalog=codex
Toolsfunction, custom (apply_patch), namespace; built-in web_search is skipped
Inputmessage, function_call, function_call_output, custom_tool_call, custom_tool_call_output, reasoning (ignored), additional_tools
Outputmessage items and reasoning items; visible reasoning is returned as summary_text
Streamingresponse.created → reasoning summary / output items / text deltas → response.completed; response.failed on errors
Stateful featuresstore is accepted but stateless; previous_response_id and background are rejected
Structured outputstext.format is not supported; Codex coding does not need it

If it does not work

Model metadata not found

Check that model_catalog_url ends in /models?catalog=codex and that the key has the models scope. If the Codex build is older, remove model_catalog_url and pin model explicitly.

404 /responses

base_url is missing /v1 or has a trailing path. Use the /v1 address shown above.

401

SANERO_API_KEY is not set in the same shell, or env_key does not match the variable name.

402 / insufficient_quota

Top up the wallet. Agent turns reserve up to 32,768 output tokens.

413 / request_too_large

Run /compact. The public API accepts up to 4 MB of JSON.

stream closed before response.completed

A proxy or network dropped the stream. Sanero sends keep-alives every 15 seconds; disable buffering in your proxy.

apply_patch unavailable

The model catalogue was not loaded. Fix model_catalog_url and restart Codex.

Remote compaction errors

Set model_providers.sanero.capabilities.remote_compaction = "unsupported" so Codex uses local compaction.

web_search fails

The gateway does not proxy OpenAI-hosted web search. Keep web_search = "disabled" in config.toml.

Reasoning level rejected

Pick a level advertised by the selected model. Codex validates model_reasoning_effort locally.

What leaves your machine

Codex sends prompts, tool calls, tool results and selected file contents to the gateway. They are processed under the same API retention policy. Keep SANERO_API_KEY in the environment; never put it in a committed config file.

Cost

Every Codex turn is a normal metered API request. A turn is capped at 32,768 output tokens (or the model's lower limit); the hold is released after the response and the exact charge appears in the dashboard.

Cursor, Cline, Continue and other editors

Editors and extensions that support an “OpenAI Compatible” provider use the Chat Completions endpoint. Create one provider, then add the model id you want to use. The base URL always ends in /v1 for these clients.

Provider typeOpenAI Compatible
Base URLhttps://api.sanero.ai/v1
API keysk-sanero-…
Model<model-id>

Where to configure

ToolProtocolWhere to configure
CursorChat CompletionsSettings → Models → OpenAI API Key + Override OpenAI Base URL, then add the model id
ClineChat CompletionsSettings → API Configuration → API Provider: OpenAI Compatible
Roo CodeChat CompletionsSettings → Providers → API Provider: OpenAI Compatible
Kilo CodeChat CompletionsSettings → Providers → OpenAI Compatible
ContinueChat Completions~/.continue/config.yaml → provider: openai + apiBase
AiderChat CompletionsOPENAI_API_BASE + OPENAI_API_KEY, model openai/<id>
  1. 1Create an API key in your Sanero dashboard and copy it.
  2. 2Open the editor or extension settings and choose the OpenAI-compatible provider type.
  3. 3Paste the base URL above, the API key, and a model id from the catalogue.
  4. 4If the client asks for a protocol, choose Chat Completions. For Claude Code or Codex use the dedicated sections above.
  5. 5Open the chat panel: answers stream back live and tool calls execute in the editor.

Ready-made configs

Continue reads ~/.continue/config.yaml; older releases read ~/.continue/config.json. Cline, Roo Code and Kilo use the OpenAI Compatible provider with the same base URL.

json
{
  "models": [
    {
      "title": "Sanero · <model-id>",
      "provider": "openai",
      "model": "<model-id>",
      "apiBase": "https://api.sanero.ai/v1",
      "apiKey": "sk-sanero-…"
    }
  ]
}

Agent clients may show their own estimated cost or no cost at all. Sanero bills the exact amount shown in your dashboard; non-stream responses also carry the X-Sanero-Cost-Usd header.

Works with OpenAI-compatible providers such as Continue, Cline, Roo Code, Kilo Code and Aider, and with the official OpenAI Python and Node SDKs.

Code examples

Copy, paste, run. The same key works across Chat Completions, Responses and Anthropic Messages; use the base URL form each SDK expects. The model id comes from the live catalogue.

curl https://api.sanero.ai/v1/chat/completions \
  -H "Authorization: Bearer sk-sanero-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id>",
    "messages": [
      {"role": "system", "content": "You are a concise assistant."},
      {"role": "user", "content": "Hello, Sanero!"}
    ]
  }'
Python: streaming
from openai import OpenAI

client = OpenAI(base_url="https://api.sanero.ai/v1", api_key="sk-sanero-…")

stream = client.chat.completions.create(
    model="<model-id>",
    messages=[{"role": "user", "content": "Hello, Sanero!"}],
    stream=True,
    stream_options={"include_usage": True},
)

for chunk in stream:
    # The final usage chunk carries no choices; guard before indexing.
    if chunk.choices:
        print(chunk.choices[0].delta.content or "", end="", flush=True)
Python: Anthropic Messages (Claude Code protocol)
from anthropic import Anthropic

client = Anthropic(base_url="https://api.sanero.ai", api_key="sk-sanero-…")

message = client.messages.create(
    model="<model-id>",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Hello, Sanero!"}],
)
print(message.content[0].text)
Python: OpenAI Responses (Codex protocol)
from openai import OpenAI

client = OpenAI(base_url="https://api.sanero.ai/v1", api_key="sk-sanero-…")

response = client.responses.create(
    model="<model-id>",
    input="Hello, Sanero!",
)
print(response.output_text)

Overview

Sanero speaks three protocols on one authenticated, metered surface: OpenAI Chat Completions, OpenAI Responses (Codex), and Anthropic Messages (Claude Code). Every request is billed pay-as-you-go from your wallet balance. There is no SDK to install: point the client at the correct base URL, paste your key, and choose a model id from the catalogue.

Use the address your client expects: Claude Code and the Anthropic SDK use the host root; Codex and OpenAI clients use /v1. Endpoints below are relative to the OpenAI base.

Claude Code / Anthropic SDKhttps://api.sanero.ai
OpenAI SDK / editorshttps://api.sanero.ai/v1

Endpoints

POST/chat/completions
OpenAI Chat Completions. Streaming and non-streaming.
POST/messages
Anthropic Messages for Claude Code and the Anthropic SDK.
POST/messages/count_tokens
Anthropic input-token counting. Free, rate-limited.
POST/responses
OpenAI Responses for Codex and Responses-native clients.
GET/models
The models your key can call. Also serves the Anthropic list and the Codex metadata catalogue.
GET/models/{id}
One model in full detail: prices, reasoning levels, output limit.
GET/key
What your key is and what it may do. Free, and never billed.
GET/usage
Your own spend and error breakdown.
GET/errors
Every error code, with an explanation and a fix.
GET/rates
Platform billing facts, such as the minimum charge.

What is and is not supported

Supported: Chat Completions, Responses and Anthropic Messages; streaming; tools and function calling; custom and namespaced Codex tools; image inputs; reasoning effort; system, developer, user, assistant and tool roles; max output tokens; and usage reporting.

Not supported: n > 1 (returns 400), embeddings, the legacy /completions endpoint, Anthropic batch/file/server-tool APIs, PDF document blocks, and stateful Responses features such as previous_response_id and background mode. Unsupported shapes fail with a clear protocol-native error rather than being silently ignored.

Authentication

Create a key in your dashboard, then send it on every request in the Authorization header as a Bearer token. Claude Code may also send it as x-api-key; both are accepted. A key spends real money from your balance, so treat it exactly like a password. If a key leaks, revoke it in the dashboard and it stops working immediately.

Authorization header
Authorization: Bearer sk-sanero-…

Never embed a key in front-end code, a public repository or a shared notebook. Anyone holding it can spend your balance.

Checking a key without spending anything

GET /key tells you whether a key works, what it is allowed to do and what your balance is. It costs nothing, so use it in setup wizards and health checks instead of sending a throwaway completion.

GET /key
curl https://api.sanero.ai/v1/key \
  -H "Authorization: Bearer sk-sanero-…"

Scoped keys

A key can be restricted to specific capabilities (chat, models, usage) and to a list of models. A restricted key sees only its allowed models in GET /models, and a model outside the list returns 404 rather than 403, because we do not confirm that a model exists but is blocked. A key with no restrictions can do everything.

Chat completions

The core endpoint. Send a list of messages, get a completion back. Set stream: true for token-by-token streaming.

POST/chat/completions
curl https://api.sanero.ai/v1/chat/completions \
  -H "Authorization: Bearer sk-sanero-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id>",
    "messages": [
      {"role": "system", "content": "You are a concise assistant."},
      {"role": "user", "content": "Hello, Sanero!"}
    ]
  }'
Response
{
  "id": "chatcmpl-9f2b1c7e4a8d",
  "object": "chat.completion",
  "created": 1700000000,
  "model": "<model-id>",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "Hello! How can I help you today?" },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 18,
    "completion_tokens": 9,
    "total_tokens": 27
  },
  "sanero": {
    "cost": {
      "charged_usd": "0.00001512",
      "price_usd": "0.00001512",
      "input_usd": "0.00000432",
      "output_usd": "0.00001080",
      "cache_read_usd": "0",
      "cache_write_usd": "0",
      "request_usd": "0",
      "balance_usd": "12.48213600",
      "underfunded": false
    }
  }
}

The response is a standard OpenAI chat.completion object, plus a namespaced `sanero` block carrying the cost of the request. SDKs ignore unknown top-level fields, so the extra block breaks nothing.

Request parameters

ParameterTypeDescription
modelstringRequired. A model id from GET /models.
messagesarrayRequired. A list of {role, content} messages. Roles: system, user, assistant. Up to 2000 messages and 2,000,000 characters in total.
streambooleanStream the response as server-sent events. Default false.
temperaturenumber0–2. Forwarded only when you set it; we never send a default, because some reasoning models reject the parameter outright.
top_pnumber0–1. Forwarded only when you set it.
max_tokensintegerUpper bound on generated tokens. Also caps what the request can cost.
max_completion_tokensintegerAlias of max_tokens, for newer OpenAI clients.
stopstring | arrayUp to 4 stop sequences, each up to 64 characters.
presence_penaltynumber−2 to 2. Forwarded only when you set it.
frequency_penaltynumber−2 to 2. Forwarded only when you set it.
seedintegerForwarded to the model when supported. Not a determinism guarantee.
reasoning_effortstringFor reasoning-capable models. The valid values are per-model; see Reasoning effort below.
stream_optionsobject{ "include_usage": true } adds a final usage chunk when streaming.
nintegerOnly n = 1 is supported. Any other value returns 400.

Omitted means omitted

A sampling parameter you do not send is not sent upstream either. We do not substitute OpenAI's documented default, because doing so would override a model whose own default differs. Several reasoning models reject temperature and top_p entirely, so a defaulted value would break those requests.

Streaming

With stream: true the API returns server-sent events. Each event is a chat.completion.chunk whose choices[0].delta.content carries the next piece of text. The stream ends with a data: [DONE] line. This is exactly what the OpenAI SDK expects, so streaming works out of the box.

Terminal events by protocol

All three protocols stream over SSE. The parser in each client expects a different terminal event; Sanero emits the correct one for the endpoint you called.

ProtocolTerminal eventErrors
Chat Completionsdata: [DONE]An error object in a data frame
Anthropic Messagesevent: message_stopevent: error, then close
Responsesevent: response.completedevent: response.failed
Request
curl -N https://api.sanero.ai/v1/chat/completions \
  -H "Authorization: Bearer sk-sanero-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id>",
    "messages": [{"role": "user", "content": "Hello, Sanero!"}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'
text/event-stream
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[],"usage":{"prompt_tokens":12,"completion_tokens":4,"total_tokens":16},"sanero":{"cost":{"charged_usd":"0.00000768","price_usd":"0.00000768","balance_usd":"12.48212832","underfunded":false}}}

data: [DONE]

Add stream_options: { include_usage: true } to receive a final chunk carrying token usage and the cost of the request.

Errors inside a stream

Once the first byte is sent the HTTP status is already 200, so a later failure cannot change it. Such a failure arrives as a frame containing an error object, followed by data: [DONE]. Handle that frame in your reader: a stream that ends early otherwise looks like a short answer.

text/event-stream
data: {"id":"chatcmpl-…","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"The answer is"},"finish_reason":null}]}

data: {"error":{"message":"The stream ended before the response was complete.","type":"api_error","param":null,"code":"stream_interrupted"}}

data: [DONE]

Tokens already delivered are billed even if the stream breaks or you disconnect: they cannot be undelivered. A request like that appears in your logs as partial, with a charge and an explanation.

Model catalogue & prices

Every model the API can serve right now, with list prices per 1,000,000 tokens. This table is generated from the same configuration the API bills from, so it cannot fall out of date.

Loading…

Models

List the models your key can call. The default response is the exact OpenAI models shape, so an SDK that enumerates models works unchanged. Model ids are Sanero-branded; pass one as the `model` field of a chat request.

GET/models
Request
curl https://api.sanero.ai/v1/models \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "object": "list",
  "data": [
    { "id": "<model-id>", "object": "model", "created": 1771113600, "owned_by": "sanero" }
  ]
}

One endpoint, three catalogues

The same GET /models adapts to the client. The shape is selected by the signal the client already sends, so neither Claude Code nor Codex needs a second URL:

Client signalShapeUsed by
Authorization: Bearer onlyOpenAI listChat Completions, editors, SDKs
anthropic-version headerAnthropic model listClaude Code model discovery
client_version queryCodex ModelsResponse catalogueCodex picker, context windows, reasoning

Extended catalogue

Add ?extended=true (or fetch a single model) to get everything we can tell you about it: display name, the reasoning levels THAT model supports, its maximum output length, and your effective prices. Prices are per 1,000,000 tokens and are returned as decimal strings, never floats, because 0.048 has no exact binary representation, and a client summing thousands of them would drift.

GET/models/{id}
Request
curl "https://api.sanero.ai/v1/models/<model-id>" \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "id": "<model-id>",
  "object": "model",
  "created": null,
  "owned_by": "sanero",
  "display_name": "Model name",
  "reasoning_levels": [],
  "max_output_tokens": null,
  "pricing": {
    "input_per_1m": null,
    "output_per_1m": null,
    "cache_read_per_1m": null,
    "cache_write_per_1m": null
  }
}

Absent fields, not nulls

A field we have no value for is left out of the response instead of being sent as null. In pricing that is the whole contract: a component that is present bills, and a component that is absent does not. There is no separate type field to consult, and a price is never 0 to mean “free”. Every amount is USD, and the unit is in the field name (input_per_1m is per 1,000,000 tokens), so nothing has to be paired with a currency field to be read correctly.

created is the model's release date as a Unix timestamp, so you can tell a current model from an older one. For a model whose release date we do not publish it falls back to a fixed placeholder; treat it as “unknown”, not as a real date.

The prices in an authenticated response are what YOUR wallet will be charged, including any active promotion. The catalogue on this page shows list prices, because we do not know who is reading it.

The catalogue is read live on every request. When we enable, retire or reprice a model, GET /models reflects it immediately: there is nothing to redeploy and no cached list to invalidate.

Reasoning effort

Reasoning-capable models accept a reasoning_effort. The available levels are a property of each model, not a fixed set: depending on the model you may have minimal, low, medium, high, xhigh, max, or a simple on/off. Read the exact list from the model's reasoning_levels; the catalogue above shows it for every model.

Visible reasoning is returned, not hidden: Chat Completions streams reasoning_content, Anthropic Messages streams thinking blocks, and Responses returns reasoning items with summary_text. The reasoning channel is not billed twice; the upstream completion usage already includes those tokens. Treat reasoning text as sensitive as the final answer: it can quote prompt or tool data.

The same ladder across protocols

Every protocol is mapped onto the model's real reasoning ladder. Anthropic budgets are approximate: Sanero picks the nearest supported level, and a model with only off/on still works. If a model has no reasoning, the field is ignored rather than rejected.

ProtocolSend
Chat Completions{ "reasoning_effort": "high" }
Anthropic Messages{ "thinking": { "type": "enabled", "budget_tokens": 16000 } }
Responses{ "reasoning": { "effort": "high" } }
POST /chat/completions
curl https://api.sanero.ai/v1/chat/completions \
  -H "Authorization: Bearer sk-sanero-…" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id>",
    "messages": [{"role": "user", "content": "Prove that sqrt(2) is irrational."}],
    "reasoning_effort": "high"
  }'

Any model accepts "off" (and "none"), which means "do not ask for reasoning". Sending a level a model does not support returns 400 and names the levels it does support, so you never have to guess.

400 invalid_value
{
  "error": {
    "message": "`galactic` is not a valid value for `reasoning_effort` for model `claude-opus-5`. Supported values: `off`, `low`, `medium`, `high`, `xhigh`, `max`.",
    "type": "invalid_request_error",
    "param": "reasoning_effort",
    "code": "invalid_value"
  }
}

Higher effort means more generated tokens, and generated tokens are what you pay for. A high-effort answer can cost several times a low-effort one for the same prompt.

What a request costs

Every response tells you what it cost. There is no need to estimate from token counts and a price list: the exact charge, its component breakdown and your remaining balance come back with the answer.

For a streamed request the same block arrives in the final usage chunk (ask for it with stream_options.include_usage).

Cost fields

charged_usdWhat your balance was actually debited. This is what you paid.
price_usdThe price of the request. Differs from charged_usd only if your balance ran out mid-stream, in which case we collected what was left.
input_usd / output_usdThe prompt and completion components.
cache_read_usd / cache_write_usdPrompt-cache components, when the model uses one.
request_usdA per-request component, for models priced that way.
balance_usdYour remaining balance after this charge.
underfundedTrue when your balance could not cover the full price.

Every money value is a decimal string, never a JSON number. Parse it into a decimal type: 0.0031 cannot be represented exactly in binary floating point, and a client summing thousands of such values accumulates real error.

Response headers

The same figures are also response headers, for clients that see headers but cannot read the body: a proxy, a logging layer, or an editor extension that wants to show a running total.

X-Sanero-Cost-UsdWhat was actually debited from your balance.
X-Sanero-Price-UsdThe list price of the request.
X-Sanero-Balance-UsdYour balance after this charge.
Python: reading the cost of a request
from decimal import Decimal

import httpx

# The SDK drops unknown fields, so read the raw response to get the cost block.
# Every money value is a decimal STRING: parse it with Decimal, never float.
response = httpx.post(
    "https://api.sanero.ai/v1/chat/completions",
    headers={"Authorization": "Bearer sk-sanero-…"},
    json={
        "model": "<model-id>",
        "messages": [{"role": "user", "content": "Hello, Sanero!"}],
    },
    timeout=120,
)
response.raise_for_status()
body = response.json()

cost = Decimal(body["sanero"]["cost"]["charged_usd"])
balance = Decimal(body["sanero"]["cost"]["balance_usd"])
print(f"charged {cost} USD, {balance} USD left")

# Also available as headers, for clients that never parse the body.
print(response.headers["X-Sanero-Cost-Usd"])

Billing

The API is pay-as-you-go from your wallet balance, at the per-model prices on our pricing page. There is no subscription and no monthly commitment for API use, and API usage never touches a subscription plan's allowance.

Nothing runs that you cannot pay for

Before a request reaches a model we price it, using your prompt and your max_tokens, and check that your balance covers that estimate. If it does not, you get 402 (insufficient_quota) and nothing is sent and nothing is charged.

While a request runs, its estimated cost is held against your balance, and the hold is released the moment the real charge is known. That is why a burst of parallel requests can hit 402 while your balance still looks sufficient: the requests already running have claimed part of it. Agent turns use the same rule: the hold is based on their output ceiling, capped at 32,768 tokens, and the unused part is returned.

Per-key spending limits

A key can carry a monthly spending ceiling. When it is reached, that key returns 402 with the code spend_limit_exceeded, which is distinct from an empty balance, because the remedy is different: the money is there, the key is not allowed to spend it. Raise or clear the limit in your dashboard.

Rate & concurrency limits

Each key has a requests-per-minute allowance and each account has a cap on simultaneously running requests. Exceeding either returns 429 with a Retry-After header. GET /key reports both for the key you are holding. Claude Code and Codex can issue parallel turns and count_tokens calls; each one consumes the same per-key allowance.

Failed requests count too

The per-minute allowance is consumed when a request is admitted, not when it finishes, so a burst of parallel calls and a loop of failing calls are both throttled. A cheap failing request is not a free request.

Why there is a concurrency cap

Requests still in flight are counted against your balance while they run, so a large swarm of your own requests could otherwise lock up your own funds. The cap bounds that, and bounds the damage a runaway script can do.

Account endpoints

Read-only endpoints for your own account. They cost nothing and return only your own data.

GET /key: what this key is

Name, last four characters, creation and last-used timestamps, rate and concurrency limits, monthly spending limit and how much of it is used, scopes, allowed models, and your balance. Requires no scope: a key must always be able to describe itself, or a misconfigured key would give you a 403 and no way to find out why.

Response
{
  "object": "api_key",
  "id": "6f1d0b2c-9a44-4e51-8c07-1b2d3e4f5a6b",
  "name": "production",
  "last4": "9f3a",
  "created_at": "2026-08-01T09:14:22+00:00",
  "last_used_at": "2026-08-21T11:03:48+00:00",
  "limits": {
    "requests_per_minute": 120,
    "max_concurrent_requests": 24,
    "monthly_spend_limit_usd": "50.00000000",
    "monthly_spend_used_usd": "3.41220000",
    "monthly_spend_remaining_usd": "46.58780000"
  },
  "balance_usd": "12.48213600"
}

GET /usage: your spend

Totals and a per-model breakdown for the last N days (up to 90), plus a count of failed requests grouped by error code. Failed requests carry no charge, so they appear in the failure counts without inflating your spend.

Request
curl "https://api.sanero.ai/v1/usage?days=7" \
  -H "Authorization: Bearer sk-sanero-…"
Response
{
  "object": "usage",
  "period": { "start": "2026-08-14T…", "end": "2026-08-21T…", "days": 7 },
  "total": {
    "requests": 1842,
    "failed_requests": 11,
    "prompt_tokens": 2841002,
    "completion_tokens": 512338,
    "spend_usd": "4.21870000"
  },
  "models": [
    {
      "model": "<model-id>",
      "requests": 1610,
      "failed_requests": 4,
      "prompt_tokens": 2510440,
      "completion_tokens": 470112,
      "spend_usd": "3.88110000"
    }
  ],
  "errors": [
    { "code": "rate_limit_exceeded", "count": 7 },
    { "code": "upstream_error", "count": 4 }
  ]
}

GET /errors: the error reference

The whole error table as machine-readable JSON, in English or Russian. The same source this page renders from, so your retry logic can be built against data rather than prose.

GET /rates: billing facts

Platform-wide values you need to interpret a charge, chiefly the minimum per-request charge and the token block prices are quoted against.

Errors

Chat Completions and Responses use the OpenAI envelope, so their SDKs surface typed exceptions. Anthropic Messages uses the Anthropic envelope. The status tells you the category; the code/type tells you exactly what happened.

application/json
{
  "error": {
    "message": "Insufficient balance to make this request. Top up your account at https://dash.sanero.ai to continue.",
    "type": "insufficient_quota",
    "param": null,
    "code": "insufficient_quota"
  }
}

The same failure, in each dialect

The gateway maps its catalogued error onto the protocol the client is speaking. A balance failure is a 402 insufficient_quota for OpenAI clients and a 400 invalid_request_error for Anthropic clients. A full context is context_too_large for Chat, but context_length_exceeded for Responses so Codex compacts instead of retrying blindly.

ConditionChat codeResponses codeAnthropic type
Bad request400 invalid_request400 invalid_request400 invalid_request_error
Bad or missing key401 missing_api_key / invalid_api_key401 missing_api_key / invalid_api_key401 authentication_error
No such model404 model_not_found404 model_not_found404 not_found_error
Insufficient balance402 insufficient_quota402 insufficient_quota400 invalid_request_error
Rate limit429 rate_limit_exceeded429 rate_limit_exceeded429 rate_limit_error
Context too large400 context_too_large400 context_length_exceeded400 invalid_request_error
Service unavailable503 service_unavailable503 server_is_overloaded503 overloaded_error

Loading…

Retry? says whether repeating the identical request could succeed. Retrying a 400 forever is a bug; giving up on a 502 loses a request that would have worked. When we can be specific about timing, a Retry-After header says how long to wait; honour it.

Every failed request also appears in your dashboard logs with its code, an explanation and a suggested fix, so you can debug an integration without adding logging on your side.

One exception: a request rejected because the key itself is invalid is not recorded against any account. There is no way to tell whose it was meant to be, and attributing it to the claimed owner would let anyone write into a stranger's log.