#Developer Guide

This guide takes you from a working API key to a production AI application: multi-turn conversations, streaming, tool calling, and error handling.

#Setup

Everything below assumes your OpenRouter API key is in the environment:

export OPENROUTER_API_KEY=sk-or-v1-...

Pick whichever SDK your app already uses — the gateway speaks both dialects:

# Anthropic SDK — bare base URL
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["OPENROUTER_API_KEY"],
                   base_url="https://api.badlandslabs.com")

# OpenAI SDK — base URL + /v1
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENROUTER_API_KEY"],
                base_url="https://api.badlandslabs.com/v1")
// Node — same rule: Anthropic takes the bare URL, OpenAI takes /v1
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic({
  apiKey: process.env.OPENROUTER_API_KEY,
  baseURL: "https://api.badlandslabs.com",
});

import OpenAI from "openai";
const openai = new OpenAI({
  apiKey: process.env.OPENROUTER_API_KEY,
  baseURL: "https://api.badlandslabs.com/v1",
});

#Multi-turn conversations

The API is stateless — your app owns the conversation history. Append each assistant reply to the message list and send the whole list every turn:

messages = []

def chat(user_text: str) -> str:
    messages.append({"role": "user", "content": user_text})
    res = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=1024,
        system="You are a concise support assistant for Acme Corp.",
        messages=messages,
    )
    reply = res.content[0].text
    messages.append({"role": "assistant", "content": reply})
    return reply

print(chat("What plans do you offer?"))
print(chat("Which one did you just recommend?"))  # remembers turn 1

Budgeting context: POST /v1/messages/count_tokens returns an estimate (~4 chars/token). For exact budgets, tokenize client-side before sending.

#Streaming

Stream for anything user-facing — first tokens arrive in well under a second.

# Anthropic SDK
with client.messages.stream(
    model="claude-sonnet-5", max_tokens=1024,
    messages=[{"role": "user", "content": "Write a haiku about prairies."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
// OpenAI SDK
const stream = await openai.chat.completions.create({
  model: "gpt-4o",
  stream: true,
  messages: [{ role: "user", content: "Write a haiku about prairies." }],
});
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}

Both dialects emit their standard wire protocol (Anthropic SSE events / OpenAI data: chunks ending in data: [DONE]), so any SSE client works. You may see : OPENROUTER PROCESSING keepalive comments mid-stream — spec-compliant, SDKs ignore them automatically.

#Tool calling

Define tools, let the model decide when to call them, run the tool, and send the result back. A complete round trip on the Anthropic dialect:

tools = [{
    "name": "get_weather",
    "description": "Current weather for a city",
    "input_schema": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
    },
}]

messages = [{"role": "user", "content": "Should I bike to work in Calgary today?"}]
res = client.messages.create(model="claude-sonnet-5", max_tokens=1024,
                             tools=tools, messages=messages)

while res.stop_reason == "tool_use":
    tool_use = next(b for b in res.content if b.type == "tool_use")
    result = get_weather(**tool_use.input)          # your function
    messages.append({"role": "assistant", "content": res.content})
    messages.append({"role": "user", "content": [{
        "type": "tool_result",
        "tool_use_id": tool_use.id,
        "content": str(result),
    }]})
    res = client.messages.create(model="claude-sonnet-5", max_tokens=1024,
                                 tools=tools, messages=messages)

print(res.content[0].text)

On the OpenAI dialect the same loop reads res.choices[0].message.tool_calls, runs each call, and appends a {"role": "tool", "tool_call_id": ..., "content": ...} message. Parallel tool calls, forced tools (tool_choice), and streaming tool deltas all work in both dialects.

#Handling reasoning output

The serving model may be a reasoning model. The gateway sends this OpenAI-dialect option upstream automatically to keep the final-answer channel usable:

{ "reasoning": { "exclude": true } }

The gateway remains the final privacy boundary on both dialects: it removes native Anthropic thinking/redacted_thinking blocks, <think>/<analysis> text, known provider/model identity mentions (including those split across SSE chunks), and the resolved upstream model identity. The response model field remains caller-facing.

#Errors, rate limits, retries

Errors arrive in your dialect's native shape, so SDK exception types work as usual. The ones to handle in app code:

Status Meaning App behavior
401 Missing or rejected key Fix/rotate the key — don't retry
402 Key out of credits Surface to the user/billing
429 Rate limited (Retry-After: 60) Back off and retry
5xx Upstream hiccup Retry with backoff

Official SDKs already retry 429/5xx with exponential backoff — tune rather than reimplement:

client = Anthropic(api_key=key, base_url="https://api.badlandslabs.com",
                   max_retries=4, timeout=120.0)

Respect Retry-After if you roll your own HTTP: sleep the given seconds, then resend the identical request.

#Production checklist