This OpenRouter Python tutorial takes you from a first HTTP request to a client with timeouts, bounded retries, and useful usage logs. Keep credentials server-side and validate results before using them.
Build a Python client that can survive production
OpenRouter accepts ordinary HTTP requests: send a Bearer token to /api/v1/chat/completions, name a model, and pass a messages array. This tutorial starts with raw HTTP so timeouts, retries, errors, and usage stay visible.
Checked October 15, 2026: model names, prices, and routing change. Confirm your model in the live catalog before deployment.
1. Create a clean project
mkdir openrouter-demo && cd openrouter-demo
python -m venv .venv
. .venv/bin/activate
python -m pip install --upgrade pip requests
export OPENROUTER_API_KEY='your-key'
export OPENROUTER_MODEL='openai/gpt-5.2'
On Windows PowerShell activate with .\.venv\Scripts\Activate.ps1. Store the key in a secret manager for real deployments.
2. Run a boring smoke test
import os, requests
r = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
"Content-Type": "application/json"},
json={"model": os.environ["OPENROUTER_MODEL"],
"messages":[{"role":"user","content":"Reply: smoke test passed"}],
"max_tokens":32},
timeout=(5,30),
)
print(r.status_code, r.text)
r.raise_for_status()
Test response shape and finish status rather than exact wording. Model output is not deterministic.
3. Add bounded retries
import os, random, time
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
import requests
RETRYABLE = {408, 429, 500, 502, 503, 504}
def retry_delay(response, attempt):
value = response.headers.get("Retry-After") if response is not None else None
if value:
try:
delay = float(value)
except ValueError:
try:
delay = (parsedate_to_datetime(value) - datetime.now(timezone.utc)).total_seconds()
except (TypeError, ValueError, OverflowError):
delay = None
if delay is not None:
return max(0.0, delay)
return min(2 ** attempt, 30) + random.uniform(0, 0.25)
def complete(payload, max_attempts=4, deadline_seconds=120):
deadline = time.monotonic() + deadline_seconds
for attempt in range(max_attempts):
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("request deadline exhausted")
response = None
try:
response = requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json=payload,
timeout=(min(5, remaining), min(30, remaining)),
)
except (requests.Timeout, requests.ConnectionError):
if attempt == max_attempts - 1:
raise
else:
if response.status_code not in RETRYABLE:
response.raise_for_status()
data = response.json()
if data.get("error"):
raise RuntimeError("OpenRouter returned an error envelope")
return data
if attempt == max_attempts - 1:
response.raise_for_status()
delay = retry_delay(response, attempt)
if delay >= deadline - time.monotonic():
raise TimeoutError("Retry-After exceeds the retry budget; queue for later")
time.sleep(delay)
raise RuntimeError("retry budget exhausted")
Do not retry 400 validation errors, 401 credentials, 402 credit limits, 403 policies, 404 model errors, or 413 payloads without changing the request. Let one layer own retries; stacked SDK and application retries multiply cost and latency.
4. Log useful facts, not prompts
data=complete({
"model":os.environ["OPENROUTER_MODEL"],
"messages":[{"role":"user","content":"Give one focus tip."}],
"max_tokens":128,
})
usage=data.get("usage",{})
print({"id":data.get("id"),"model":data.get("model"),
"tokens":usage.get("total_tokens"),"cost":usage.get("cost")})
print(data["choices"][0]["message"]["content"])
Keep request ID, returned model, latency, token counts, cost, and final outcome. Avoid logging API keys, full prompts, or sensitive output.
Platform versus provider failures
A 429 may come from an OpenRouter limit or an upstream provider. Record safe error metadata and rate-limit headers. With streaming, a failure may arrive inside an SSE event after HTTP 200; mark partial output as incomplete.
Production checklist
- Bound connection and read timeouts.
- Cap retries, output tokens, concurrency, and total job duration. The example checks a retry budget between attempts; a requests read timeout measures inactivity, so enforce a strict wall-clock deadline in your worker if required.
- Validate the response before triggering external actions.
- Use separate keys and budgets per environment.
- Monitor the returned model and usage.
- Test one reviewed fallback model.
Troubleshooting
- 401: test
GET /api/v1/keyfrom the same process. - 402/403: inspect credits, key cap, workspace budget, and guardrails.
- 404: check the live model slug.
- 429: honor
Retry-After, reduce concurrency, and review routing. - 200 but no answer: inspect
error, finish reason, and token budget.
OpenRouter Python FAQ
Should I stream immediately?
No. Validate a non-streaming request first, then add SSE parsing and a clear partial-failure state when time-to-first-token matters.
Why does the returned model differ?
Aliases and fallback routing may resolve differently. Log the response model and pin a reviewed slug when reproducibility matters.
What next?
See the OpenAI SDK guide, 429 guide, and pillar guide.
Official references
Documentation reviewed 15 October 2026. Examples illustrate the request pattern; confirm model availability and account settings before use. No uptime or performance benchmark is claimed.
