#API Reference
Base URL: https://api.badlandslabs.com
One API, two dialects, and a caller-compatible model catalog. The gateway accepts requests in both the Anthropic Messages format and the OpenAI Chat Completions format. Public model ids are caller-facing labels; the current fusion and hard-fallback route use MiniMax as the only configured upstream model source. Callers authenticate with their own OpenRouter key; the gateway stores nothing.
your client (either dialect, a valid model card, your OpenRouter key)
│
▼
https://api.badlandslabs.com Cloudflare edge, TLS, global
│ • auth normalization (BYOK)
▼ • model → managed preset
OpenRouter @preset routing • response identity filtered before return
│
▼
MiniMax (current configured upstream source)
#Authentication
Send your OpenRouter API key (sk-or-v1-...) with every /v1/* request, in either
header style:
Authorization: Bearer sk-or-v1-... # OpenAI style
x-api-key: sk-or-v1-... # Anthropic style
- The key is forwarded to OpenRouter per-request over TLS. It is never stored or logged.
- If a request carries both headers (common when an SDK picks up an ambient
ANTHROPIC_AUTH_TOKEN), the gateway uses whichever value has thesk-or-prefix. - Missing key →
401in your dialect's error shape (see Errors).
#Models
Send a valid model id from the catalog below. Requests are routed through the managed preset, which controls which model actually serves each request.
GET /v1/models advertises the following valid ids:
| Family | Model ids |
|---|---|
| Anthropic | claude-fable-5, claude-opus-4-8, claude-sonnet-5, claude-haiku-4-5, claude-sonnet-4-6, claude-opus-4-5, claude-3-7-sonnet-20250219, claude-3-5-haiku-20241022 |
| OpenAI | gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex, gpt-5.2, gpt-5.1, gpt-5, gpt-5-mini, gpt-4.1, gpt-4.1-mini, gpt-4o, gpt-4o-mini, o3, o4-mini |
| MiniMax | minimax-m3, MiniMax-M2.7, minimax-m2.5, minimax-m2, minimax-m1 |
| GLM (Zhipu) | glm-5.2, glm-5.1, glm-5, glm-4.7 |
| Direct | @preset/auto (the pool itself) |
The model table contains compatibility labels only; it does not select an upstream
provider. The response model field is the caller's requested model label, not the resolved
upstream model. The gateway removes known provider/model family mentions from response
text (including nested strings and split streaming chunks), strips reasoning markup, and
filters known upstream identity headers.
#Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/messages |
Chat — Anthropic Messages dialect |
POST |
/v1/messages/count_tokens |
Token estimate |
POST |
/v1/chat/completions |
Chat — OpenAI dialect |
POST |
/v1/responses |
Responses — OpenAI dialect |
GET |
/v1/models |
Model catalog (dialect-aware shape) |
GET |
/v1/models/{id} |
Single model card |
GET |
/health |
Gateway status (no auth) |
Any other /v1/* path is proxied to OpenRouter with the same auth handling and response
identity filtering.
#POST /v1/messages
Anthropic Messages dialect. Supports streaming, system prompts, multi-turn conversation history, tool use, and image content blocks (subject to the serving model's capabilities).
Headers
| Header | Required | Notes |
|---|---|---|
x-api-key or Authorization: Bearer |
✅ | your OpenRouter key |
content-type: application/json |
✅ | |
anthropic-version: 2023-06-01 |
recommended | forwarded upstream |
anthropic-beta |
optional | forwarded upstream |
Body — standard Anthropic schema. Frequently used fields:
| Field | Type | Notes |
|---|---|---|
model |
string | required by the schema; any value accepted (see Models) |
max_tokens |
int | required in this dialect |
messages |
array | `{role: "user" |
system |
string | block[] | system prompt |
stream |
bool | SSE streaming (below) |
tools, tool_choice |
Anthropic tool schema — fully supported | |
temperature, top_p, stop_sequences, metadata |
forwarded |
Example
curl https://api.badlandslabs.com/v1/messages \
-H "x-api-key: $OPENROUTER_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 1024,
"system": "You are a concise assistant.",
"messages": [{"role": "user", "content": "Hello!"}]
}'
Response (non-streaming) — standard Anthropic message object:
{
"id": "gen-...",
"type": "message",
"role": "assistant",
"content": [
{ "type": "text", "text": "Hello! How can I help you today?" }
],
"model": "claude-sonnet-5", // caller-facing label; upstream model is hidden
"stop_reason": "end_turn",
"usage": { "input_tokens": 814, "output_tokens": 35, "cache_read_input_tokens": 128 }
}
Streaming — set "stream": true. Standard Anthropic SSE event protocol, verified
end-to-end:
event: message_start → message envelope
event: content_block_start → per answer/tool block
event: content_block_delta → text_delta / tool input_json_delta chunks
event: content_block_stop
event: message_delta → stop_reason, usage
event: message_stop → done
Tool use — send Anthropic tools + optional tool_choice; responses contain
tool_use blocks with parsed input:
// response content includes:
{ "type": "tool_use", "id": "call_019f...", "name": "get_weather", "input": { "city": "Calgary" } }
Continue the loop by appending a tool_result content block in a user message,
exactly per the Anthropic spec.
#POST /v1/messages/count_tokens
Returns an estimate (~4 characters/token over the serialized request), because the upstream does not implement exact counting for the pool.
// request: same shape as /v1/messages (without max_tokens)
// response:
{ "input_tokens": 23 }
Budget with margin; tokenize client-side if you need exact numbers.
#POST /v1/chat/completions
OpenAI Chat Completions dialect. Supports streaming, tool calling, and multi-turn history.
Headers: Authorization: Bearer <key> (or x-api-key), content-type: application/json.
Body — standard OpenAI schema. Frequently used fields:
| Field | Type | Notes |
|---|---|---|
model |
string | any value accepted (see Models) |
messages |
array | `{role: "system" |
stream |
bool | SSE chunks (below) |
tools, tool_choice |
OpenAI function-calling schema — fully supported | |
max_tokens, temperature, top_p, stop, response_format |
forwarded | |
reasoning |
object | OpenRouter extension — {"exclude": true} strips reasoning from output |
Example
curl https://api.badlandslabs.com/v1/chat/completions \
-H "authorization: Bearer $OPENROUTER_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "gpt-4o",
"reasoning": {"exclude": true},
"messages": [
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "Hello!"}
]
}'
Response (non-streaming):
{
"id": "gen-...",
"object": "chat.completion",
"model": "gpt-4o", // caller-facing label; upstream model is hidden
"choices": [{
"index": 0,
"message": { "role": "assistant", "content": "Hello! How can I help?" },
"finish_reason": "stop"
}],
"usage": { "prompt_tokens": 25, "completion_tokens": 9, "total_tokens": 34 }
}
Streaming — set "stream": true. Standard data: {chunk} SSE lines terminated by
data: [DONE]. You may see : OPENROUTER PROCESSING comment lines — spec-compliant
keepalives; SSE libraries ignore them automatically.
Tool calling — standard shape, verified end-to-end:
// choices[0].message:
{
"role": "assistant",
"tool_calls": [{
"id": "call_019f...",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Calgary\"}" }
}]
}
#POST /v1/responses
OpenAI Responses dialect. The gateway preserves the current Responses request and
streaming event schemas—including input, instructions, function tools,
tool_choice, reasoning, structured output configuration, and store—while
rewriting only the model routing field. Responses use the preset tier followed by the
same hard-fallback tier as other chat-generation routes; the OpenRouter fusion plugin
is intentionally limited to Chat Completions.
Headers: Authorization: Bearer <key> (or x-api-key), content-type: application/json.
Example
curl https://api.badlandslabs.com/v1/responses \
-H "authorization: Bearer $OPENROUTER_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"instructions": "Answer concisely.",
"input": "What is the weather in Calgary?",
"tools": [{
"type": "function",
"name": "get_weather",
"description": "Get current weather.",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
"strict": true
}]
}'
Streaming — set "stream": true. The gateway buffers and filters the standard
Responses SSE stream, including response.created, response.output_text.delta,
response.output_text.done, and response.completed events.
#GET /v1/models
Returns the model catalog. The shape follows your dialect: send an
anthropic-version header for the Anthropic list shape, otherwise you get the OpenAI
shape.
// OpenAI shape // Anthropic shape (with anthropic-version)
{ "object": "list", { "data": [{ "type": "model",
"data": [{ "id": "claude-fable-5", "id": "claude-fable-5",
"object": "model", "display_name": "claude-fable-5",
"created": 1767225600, "created_at": "2026-01-01T00:00:00Z" }],
"owned_by": "badlands-gateway" }]} "has_more": false, "first_id": "...", "last_id": "..." }
GET /v1/models/{id} returns a single card in the same dialect-aware way.
#GET /health
No auth. Returns gateway status:
{
"ok": true,
"service": "badlands-api-gateway",
"auth": "bring your own OpenRouter key (Authorization: Bearer or x-api-key)",
"upstream": "https://openrouter.ai/api",
"endpoints": { "openai": "POST /v1/chat/completions", "openaiResponses": "POST /v1/responses", "anthropic": "POST /v1/messages", "models": "GET /v1/models" }
}
#Reasoning output
Part of the pool is reasoning models. Upstream responses may include:
- Anthropic dialect: native
thinking/redacted_thinkingblocks and occasionally<think>text inside normal content blocks. The gateway removes these reasoning blocks and returns answer/tool blocks only. - OpenAI dialect:
<think>...</think>preambles insidecontent; these are removed by the gateway before the response is returned. Responses API reasoning output items and splitresponse.output_text.deltareasoning mirrors are removed as well.
"reasoning": {"exclude": true} can still ask the upstream model for a clean answer.
The gateway also forces exclude: true on outbound OpenAI-dialect requests so a provider
cannot replace the final answer with an unclosed reasoning mirror. The gateway filters
both dialects after buffering the
response, removing native Anthropic reasoning blocks, <think>/<analysis> mirrors,
known provider/model identity text, and identity headers before returning the answer.
For Anthropic Messages callers, the gateway routes through @preset/auto without its
fusion advisor; the advisor can serialize deliberation as ordinary visible text that
Claude Code would otherwise render as leaked reasoning.
OpenAI usage metadata remains schema-compatible with strict clients: when
completion_tokens_details is present, reasoning_tokens is returned as a numeric count
(defaulting to 0 when the upstream omits or corrupts it), without returning reasoning text.
OpenAI responses also use a conservative Chat Completions envelope: usage is placed at the
response root, tool calls remain under the choice message/delta, and OpenRouter-only provider,
billing, and native-finish metadata is omitted.
#Errors
Errors use your dialect's native envelope:
// Anthropic dialect // OpenAI dialect
{ "type": "error", { "error": {
"error": { "type": "...", "message": "...",
"message": "..." } } "type": "...", "code": null } }
| Status | Type | Meaning / action |
|---|---|---|
| 401 | authentication_error |
No key sent — add Authorization: Bearer or x-api-key |
| 401 | upstream User not found. |
OpenRouter rejected the key — verify/rotate it |
| 402 | upstream | OpenRouter credits exhausted |
| 404 | not_found_error |
Unknown path (outside /v1/*, /health) |
| 429 | upstream | OpenRouter rate limit — back off and retry |
| 502 | api_error |
Gateway could not reach upstream — retry |
| 5xx | upstream | Provider-side failure — passed through with response filtering |
#Rate limits & billing
The gateway enforces fairness limits; sustained abuse is blocked earlier at the edge.
| Scope | Limit | On exceed |
|---|---|---|
| Per client IP (burst) | 10 requests / 10 seconds on /v1/* |
edge-level 429, blocked ~10 s |
| Per API key | 60 requests / minute | 429 rate_limit_error, Retry-After: 60 |
| Per client IP | 90 requests / minute | 429 rate_limit_error, Retry-After: 60 |
Limits count request starts — long-lived streaming responses don't consume extra budget. They're sized for interactive/single-instance usage: one agent session with a couple of subagents sits comfortably under them, and SDKs retry transparently on 429. If you legitimately need more, contact the operator.
Usage is billed and additionally rate-limited by OpenRouter against the API key you send — manage per-key limits and spend at openrouter.ai/settings/keys. The gateway itself adds no cost.
#Client quick-reference
| Client | Configuration |
|---|---|
| Anthropic SDK (py/ts) | base_url="https://api.badlandslabs.com", api_key=<OpenRouter key> |
| OpenAI SDK (py/ts) | base_url="https://api.badlandslabs.com/v1", api_key=<OpenRouter key> |
| Claude Code / Agent SDK | ANTHROPIC_BASE_URL=https://api.badlandslabs.com, ANTHROPIC_AUTH_TOKEN=<key>, ANTHROPIC_API_KEY="" |
| opencode | provider via @ai-sdk/anthropic, baseURL: https://api.badlandslabs.com/v1 — see Coding Tools |
| cline | cline auth -p openai -b https://api.badlandslabs.com/v1 -k <key> -m gpt-4o |
Note the dialect asymmetry: Anthropic-style clients take the bare base URL;
OpenAI-style clients take base URL + /v1.
Verified against production 2026-07-16: both dialects, streaming SSE protocols, tool use, model listing, and real agent conversations (Claude Code, opencode, cline).