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 preset and hard fallbacks use MiniMax as the only configured upstream model source (M3, then M2.7). 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 for on-premises deployment
We set up open-weight models on your infrastructure, from compact models to deployments of up to 500 billion parameters. We select the model and hardware around your workload, data requirements, and budget.
| Family | Example models |
|---|---|
| Qwen | Qwen3-Coder-480B-A35B-Instruct Coding and tool use, with 480B total parameters. |
| DeepSeek | DeepSeek-R1-Distill-Qwen-32B DeepSeek-R1-Distill-Llama-70B Distilled reasoning models in 32B and 70B sizes. |
| OpenAI gpt-oss | gpt-oss-120b and gpt-oss-20b Open-weight models for private reasoning and tool-based workflows. |
The deployment range depends on available memory, quantization, context length, and concurrent usage. Larger models may require multiple systems. We validate capacity and performance against your workload before handover.
For existing API customers: these are on-premises deployment options. Query GET /v1/models on your API endpoint for its valid model IDs. The hosted gateway uses compatibility labels; a requested label does not select or identify the upstream model.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/messages |
Chat — Anthropic Messages dialect |
POST |
/v1/messages/count_tokens |
Token count or compatibility 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 returns a dialect-native 404 and is never proxied. Known
endpoints called with the wrong method return 405 with an Allow header.
Generation requests must use a JSON content type, contain a JSON object, and include a
non-empty string model. Request and filtered response bodies are limited to 8 MiB.
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 the upstream numeric token count when supported. If the upstream reports this
route unsupported with 404 or 405, the gateway returns an estimate (~4
characters/token over the serialized request) in the same Anthropic shape.
// 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 terminate
with exactly one data: [DONE]. You may see : keep-alive comments while the gateway
filters the stream; SSE libraries ignore comments 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.
Gateway-owned routing fields (preset, plugins, models, provider, route, and
transforms) are removed, so callers cannot enable provider-specific routing or
plugins through the compatibility API. Unknown additive contract fields are preserved.
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 API key (Authorization: Bearer or x-api-key)",
"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.
The gateway does not inject OpenRouter plugins, preventing advisor output from being
serialized as ordinary visible text that clients can 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 |
|---|---|---|
| 400 | invalid_request_error |
Malformed JSON, missing model, or invalid request |
| 401 | authentication_error |
Missing or rejected key — verify/rotate it |
| 404 | not_found_error |
Unsupported path |
| 405 | invalid_request_error |
Supported path called with the wrong method |
| 413 | request_too_large / invalid_request_error |
Request exceeds the 8 MiB gateway limit |
| 415 | invalid_request_error |
Request is not JSON |
| 429 | rate_limit_error |
Rate limit — back off and retry |
| 502 | api_error |
Gateway could not reach upstream — retry |
| 5xx | api_error |
Serving failure, normalized without upstream implementation detail |
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).