Documentation/API Reference

API Reference

Endpoints, schemas, supported models, streaming protocols, tools, and error handling — the contract for api.badlandslabs.com.

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

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.

FamilyExample models
QwenQwen3-Coder-480B-A35B-Instruct
Coding and tool use, with 480B total parameters.
DeepSeekDeepSeek-R1-Distill-Qwen-32B
DeepSeek-R1-Distill-Llama-70B
Distilled reasoning models in 32B and 70B sizes.
OpenAI gpt-ossgpt-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.

Discuss a model deployment

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:

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