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.
Both addresses belong to the same API and the same key. The difference is only what the client appends.
Claude Code
Anthropic MessagesNative Anthropic Messages. One settings.json and you are in.
https://api.sanero.aiPOST /v1/messages
Codex CLI
OpenAI ResponsesNative Responses API with a Codex model catalogue.
https://api.sanero.ai/v1POST /v1/responses
OpenAI SDK & editors
Chat CompletionsCursor, Cline, Continue, Kilo, Aider and raw Chat Completions.
https://api.sanero.ai/v1POST /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
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.ai1Install Claude Code
npmshellnpm install -g @anthropic-ai/claude-code claude --versionA current Claude Code release works. The npm package is identical on macOS, Linux and Windows; Anthropic's native installer is also supported.
2Create a Sanero key
- 01Open the dashboard → API keys → Create key.
- 02Enable the chat and models scopes. Claude Code uses models for /model discovery.
- 03Copy the key once. It is shown only at creation.
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.jsonjson{ "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.jsonAdvanced 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 · advancedjson{ "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" } }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.
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.
verifyshellclaude -p "Reply with exactly: sanero-ok"Expected: sanero-ok
Raw protocol check
curlshellcurl -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 Codeshell/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.
| Budget | Level |
|---|---|
| ≤ 1,024 | minimal / low |
| ≤ 4,096 | medium |
| ≤ 8,192 | high |
| ≤ 16,000 | xhigh |
| 31,999 (Claude Code default) | max / highest supported |
Protocol notes
| Field | Behaviour |
|---|---|
| Endpoint | POST /v1/messages; POST /v1/messages/count_tokens |
| Auth | Authorization: Bearer … or x-api-key: … |
| Content | text, image, tool_use, tool_result; input thinking blocks are ignored; output thinking is returned; document blocks are not supported |
| Visible reasoning | streaming thinking blocks with thinking_delta + signature_delta; non-streaming thinking blocks with a signature |
| Streaming | message_start → content_block_* → message_delta → message_stop; ping events keep the connection alive |
| Errors | Anthropic envelope: authentication_error, not_found_error, rate_limit_error, overloaded_error, api_error |
| Beta / cache headers | accepted 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
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/v11Install Codex CLI
npmshellnpm install -g @openai/codex codex --versionA current Node.js is required. Homebrew and the native installer are also supported; npm is identical on macOS, Linux and Windows.
2Create a Sanero key
- 01Open the dashboard → API keys → Create key.
- 02Enable the chat and models scopes. Codex fetches /v1/models for its catalogue.
- 03Export the key as SANERO_API_KEY in the same shell that starts Codex.
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.tomltomlmodel = "<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.tomlModel 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 · advancedtomlmodel = "<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 = false4Choose 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
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.
shellexport SANERO_API_KEY="sk-sanero-…" codex exec --model <model-id> "Reply with exactly: sanero-ok"Expected: sanero-ok
inside Codexshell/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
| Field | Behaviour |
|---|---|
| Endpoint | POST /v1/responses; GET /v1/models?catalog=codex |
| Tools | function, custom (apply_patch), namespace; built-in web_search is skipped |
| Input | message, function_call, function_call_output, custom_tool_call, custom_tool_call_output, reasoning (ignored), additional_tools |
| Output | message items and reasoning items; visible reasoning is returned as summary_text |
| Streaming | response.created → reasoning summary / output items / text deltas → response.completed; response.failed on errors |
| Stateful features | store is accepted but stateless; previous_response_id and background are rejected |
| Structured outputs | text.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 type | OpenAI Compatible |
| Base URL | https://api.sanero.ai/v1 |
| API key | sk-sanero-… |
| Model | <model-id> |
Where to configure
| Tool | Protocol | Where to configure |
|---|---|---|
| Cursor | Chat Completions | Settings → Models → OpenAI API Key + Override OpenAI Base URL, then add the model id |
| Cline | Chat Completions | Settings → API Configuration → API Provider: OpenAI Compatible |
| Roo Code | Chat Completions | Settings → Providers → API Provider: OpenAI Compatible |
| Kilo Code | Chat Completions | Settings → Providers → OpenAI Compatible |
| Continue | Chat Completions | ~/.continue/config.yaml → provider: openai + apiBase |
| Aider | Chat Completions | OPENAI_API_BASE + OPENAI_API_KEY, model openai/<id> |
- 1Create an API key in your Sanero dashboard and copy it.
- 2Open the editor or extension settings and choose the OpenAI-compatible provider type.
- 3Paste the base URL above, the API key, and a model id from the catalogue.
- 4If the client asks for a protocol, choose Chat Completions. For Claude Code or Codex use the dedicated sections above.
- 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.
{
"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!"}
]
}'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)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)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.
https://api.sanero.aihttps://api.sanero.ai/v1Endpoints
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: 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.
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.
/chat/completionscurl 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!"}
]
}'{
"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
| Parameter | Type | Description |
|---|---|---|
model | string | Required. A model id from GET /models. |
messages | array | Required. A list of {role, content} messages. Roles: system, user, assistant. Up to 2000 messages and 2,000,000 characters in total. |
stream | boolean | Stream the response as server-sent events. Default false. |
temperature | number | 0–2. Forwarded only when you set it; we never send a default, because some reasoning models reject the parameter outright. |
top_p | number | 0–1. Forwarded only when you set it. |
max_tokens | integer | Upper bound on generated tokens. Also caps what the request can cost. |
max_completion_tokens | integer | Alias of max_tokens, for newer OpenAI clients. |
stop | string | array | Up to 4 stop sequences, each up to 64 characters. |
presence_penalty | number | −2 to 2. Forwarded only when you set it. |
frequency_penalty | number | −2 to 2. Forwarded only when you set it. |
seed | integer | Forwarded to the model when supported. Not a determinism guarantee. |
reasoning_effort | string | For reasoning-capable models. The valid values are per-model; see Reasoning effort below. |
stream_options | object | { "include_usage": true } adds a final usage chunk when streaming. |
n | integer | Only 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.
| Protocol | Terminal event | Errors |
|---|---|---|
| Chat Completions | data: [DONE] | An error object in a data frame |
| Anthropic Messages | event: message_stop | event: error, then close |
| Responses | event: response.completed | event: response.failed |
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}
}'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.
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.
/modelscurl https://api.sanero.ai/v1/models \
-H "Authorization: Bearer sk-sanero-…"{
"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 signal | Shape | Used by |
|---|---|---|
| Authorization: Bearer only | OpenAI list | Chat Completions, editors, SDKs |
| anthropic-version header | Anthropic model list | Claude Code model discovery |
| client_version query | Codex ModelsResponse catalogue | Codex 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.
/models/{id}curl "https://api.sanero.ai/v1/models/<model-id>" \
-H "Authorization: Bearer sk-sanero-…"{
"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.
| Protocol | Send |
|---|---|
| Chat Completions | { "reasoning_effort": "high" } |
| Anthropic Messages | { "thinking": { "type": "enabled", "budget_tokens": 16000 } } |
| Responses | { "reasoning": { "effort": "high" } } |
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.
{
"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_usd | What your balance was actually debited. This is what you paid. |
price_usd | The 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_usd | The prompt and completion components. |
cache_read_usd / cache_write_usd | Prompt-cache components, when the model uses one. |
request_usd | A per-request component, for models priced that way. |
balance_usd | Your remaining balance after this charge. |
underfunded | True 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-Usd | What was actually debited from your balance. |
X-Sanero-Price-Usd | The list price of the request. |
X-Sanero-Balance-Usd | Your balance after this charge. |
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.
{
"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.
curl "https://api.sanero.ai/v1/usage?days=7" \
-H "Authorization: Bearer sk-sanero-…"{
"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.
{
"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.
| Condition | Chat code | Responses code | Anthropic type |
|---|---|---|---|
| Bad request | 400 invalid_request | 400 invalid_request | 400 invalid_request_error |
| Bad or missing key | 401 missing_api_key / invalid_api_key | 401 missing_api_key / invalid_api_key | 401 authentication_error |
| No such model | 404 model_not_found | 404 model_not_found | 404 not_found_error |
| Insufficient balance | 402 insufficient_quota | 402 insufficient_quota | 400 invalid_request_error |
| Rate limit | 429 rate_limit_exceeded | 429 rate_limit_exceeded | 429 rate_limit_error |
| Context too large | 400 context_too_large | 400 context_length_exceeded | 400 invalid_request_error |
| Service unavailable | 503 service_unavailable | 503 server_is_overloaded | 503 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.