#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

#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:

"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).