OpenRouter developer on retainer: Anthropic Claude models return IndexError: list index out of range — OpenRouter returns HTTP 200 with empty choices array when HTTP-Referer header is missing — openai 1.40.0 — 100% of Claude model calls fail for 4 weeks — OpenRouter developer on monthly retainer
openai 1.40.0 • Python 3.11 • OpenRouter API • anthropic/claude-3-5-sonnet • HTTP-Referer header missing • HTTP 200 with choices=[] • IndexError on choices[0] • 100% Claude calls fail 4 weeks • fix: extra_headers={'HTTP-Referer': '...', 'X-Title': '...'} • 1.5h retainer engagement
Westfield Multi-Model Ltd. built a document analysis pipeline in August 2026 using OpenRouter to route LLM requests across multiple providers. They pointed the OpenAI Python SDK at openrouter.ai/api/v1 and mixed calls to OpenAI GPT-4o for speed-sensitive tasks and Anthropic Claude 3.5 Sonnet for quality-critical analysis. The GPT-4o calls worked perfectly from day one. Every single call to anthropic/claude-3-5-sonnet raised IndexError: list index out of range. 100% failure rate. They engaged an OpenRouter retainer developer to investigate after four weeks of failed debugging attempts.
The pipeline at the time of the engagement:
# document_analyzer.py
# Westfield Multi-Model Ltd.
# openai 1.40.0, Python 3.11, OpenRouter API
import openai
OPENROUTER_API_KEY = "sk-or-v1-..." # OpenRouter API key (distinct from OpenAI key)
# OpenRouter is OpenAI-compatible: point base_url at OpenRouter
client = openai.OpenAI(
api_key=OPENROUTER_API_KEY,
base_url="https://openrouter.ai/api/v1",
# ← BUG: no default_headers set.
# OpenRouter requires HTTP-Referer for certain providers, including Anthropic.
# Without HTTP-Referer, Claude calls return HTTP 200 with an error body
# and empty choices array. The OpenAI SDK does not raise an exception —
# it returns a ChatCompletion object with choices=[].
)
def analyze_document(text: str, use_claude: bool = False) -> str:
"""Analyze a document using GPT-4o or Claude 3.5 Sonnet via OpenRouter."""
model = "anthropic/claude-3-5-sonnet" if use_claude else "openai/gpt-4o"
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "You are a document analysis expert."},
{"role": "user", "content": f"Analyze this document:\n\n{text}"}
],
max_tokens=1024,
# ← No extra_headers. OpenRouter requires HTTP-Referer for Anthropic models.
)
return response.choices[0].message.content
# ↑ IndexError: list index out of range when use_claude=True
# response.choices is an empty list [].
# response itself has no error attribute that the SDK exposes.
# The IndexError stack trace points to this line — not to the HTTP response.
# GPT-4o calls (use_claude=False): succeed 100% of the time.
# Claude calls (use_claude=True): raise IndexError 100% of the time.
# Debugging: API key valid? Yes (GPT-4o works). Model name correct? Yes (matches OpenRouter catalog).
# Account balance? Sufficient. Network? HTTP 200 is logged. What is going on?
How OpenRouter returns errors without HTTP error codes
OpenRouter’s error handling for routing failures uses HTTP 200 with a response body that contains both an error field and an empty choices array. This design decision allows OpenRouter to return structured error information in the same envelope as a successful response, but it means the OpenAI Python SDK — which uses HTTP status codes to determine whether to raise an exception — never sees an error. It parses the HTTP 200 response as a valid ChatCompletion object with choices=[].
# Diagnosing the missing HTTP-Referer header issue
# openai 1.40.0, Python 3.11
import openai
import httpx
# Method 1: Log the raw HTTP response BEFORE the OpenAI SDK processes it
# Use httpx as the transport to inspect raw responses
class LoggingTransport(httpx.HTTPTransport):
def handle_request(self, request):
response = super().handle_request(request)
body = response.read()
print(f"Status: {response.status_code}")
print(f"Raw body (first 500 chars): {body[:500].decode()}")
return response
client_debug = openai.OpenAI(
api_key="sk-or-v1-...",
base_url="https://openrouter.ai/api/v1",
http_client=httpx.Client(transport=LoggingTransport()),
)
try:
response = client_debug.chat.completions.create(
model="anthropic/claude-3-5-sonnet",
messages=[{"role": "user", "content": "Hello"}],
)
print(f"choices count: {len(response.choices)}")
except Exception as e:
print(f"Exception: {e}")
# Output (when HTTP-Referer is missing):
# Status: 200
# Raw body (first 500 chars): {"id":"gen-1730285142-abc123","object":"chat.completion",
# "created":1730285142,"model":"anthropic/claude-3-5-sonnet",
# "error":{"code":400,"message":"Anthropic requires an HTTP-Referer header
# identifying your application. Add it via extra_headers when calling the OpenAI SDK.",
# "metadata":{"raw":"..."}},
# "choices":[],"usage":null}
# choices count: 0
# ← HTTP 200. No exception from SDK. choices is empty list.
# If you access choices[0] next, you get IndexError.
# Method 2: Check response.choices length before accessing
try:
response = client_debug.chat.completions.create(
model="anthropic/claude-3-5-sonnet",
messages=[{"role": "user", "content": "Hello"}],
)
if not response.choices:
# Log raw response to find the error message
print("ERROR: empty choices — likely missing HTTP-Referer or routing error")
print(f"Response dict: {response.model_dump()}")
# OpenRouter error appears in response.model_dump()['error']
error_info = response.model_dump().get("error")
if error_info:
print(f"OpenRouter error: {error_info['message']}")
else:
content = response.choices[0].message.content
print(f"Success: {content[:100]}")
except Exception as e:
print(f"SDK exception: {e}")
# Output:
# ERROR: empty choices — likely missing HTTP-Referer or routing error
# OpenRouter error: Anthropic requires an HTTP-Referer header identifying your application.
# ← Root cause identified: HTTP-Referer header missing.
The fix: add HTTP-Referer and X-Title headers
# document_analyzer.py — FIXED
# Westfield Multi-Model Ltd.
# openai 1.40.0, Python 3.11, OpenRouter API
import openai
OPENROUTER_API_KEY = "sk-or-v1-..."
# Option A: set default headers on the client (applies to all requests)
client = openai.OpenAI(
api_key=OPENROUTER_API_KEY,
base_url="https://openrouter.ai/api/v1",
default_headers={
"HTTP-Referer": "https://westfield.app", # ← FIX: identifies your application
"X-Title": "Westfield Multi-Model", # ← recommended: display name on OpenRouter
}
# Setting these as default_headers means every request inherits them
# without needing to pass extra_headers each time.
)
# Option B: per-request extra_headers (if header values vary per call)
def analyze_document(text: str, use_claude: bool = False) -> str:
model = "anthropic/claude-3-5-sonnet" if use_claude else "openai/gpt-4o"
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": "You are a document analysis expert."},
{"role": "user", "content": f"Analyze this document:\n\n{text}"}
],
max_tokens=1024,
# extra_headers here would override the client-level default_headers
# extra_headers={"HTTP-Referer": "https://westfield.app/doc-analyzer"}
)
# Defensive guard: always check choices before accessing [0]
if not response.choices:
error_info = response.model_dump().get("error")
msg = error_info["message"] if error_info else "unknown routing error"
raise RuntimeError(f"OpenRouter returned empty choices: {msg}")
return response.choices[0].message.content
# Additional: check X-LLM-Provider header to audit which provider served the request
# This is useful when OpenRouter routes requests between multiple providers for the same model
def analyze_with_provider_audit(text: str, use_claude: bool = False) -> tuple[str, str]:
"""Returns (content, provider_name) for auditing routing decisions."""
model = "anthropic/claude-3-5-sonnet" if use_claude else "openai/gpt-4o"
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": text}],
max_tokens=1024,
)
if not response.choices:
raise RuntimeError("Empty choices from OpenRouter")
# OpenRouter adds X-LLM-Provider to the response headers
# via the openai SDK, access via response._raw_response.headers (openai 1.40.0)
provider = "unknown"
if hasattr(response, "_raw_response") and response._raw_response:
provider = response._raw_response.headers.get("X-LLM-Provider", "unknown")
return response.choices[0].message.content, provider
# After fix:
# Claude calls (use_claude=True): succeed 100% of the time.
# GPT-4o calls (use_claude=False): unchanged, still 100% success.
# 500 daily document analysis jobs: all complete successfully.
# 0 IndexError exceptions post-fix.
Which OpenRouter model routes require HTTP-Referer
# OpenRouter header requirements by provider (as of October 2026)
# Best practice: always send HTTP-Referer and X-Title for ALL OpenRouter requests
# regardless of provider, since requirements may change as OpenRouter onboards providers.
# Confirmed to require HTTP-Referer:
# anthropic/claude-* (all Claude models) — Anthropic usage policy requires application ID
# anthropic/claude-3-5-sonnet — affected in this engagement
# anthropic/claude-3-opus — same requirement
# anthropic/claude-3-haiku — same requirement
# Work without HTTP-Referer:
# openai/gpt-4o — OpenAI-routed models do not require it
# openai/gpt-4o-mini — same
# meta-llama/llama-3.1-8b-instruct — open source models do not require it
# mistralai/mistral-7b-instruct — same
# google/gemini-flash-1.5 — Google models do not require it via OpenRouter
# Best practice: always include both headers for all requests
REQUIRED_HEADERS = {
"HTTP-Referer": "https://yourapp.com", # replace with your actual app URL
"X-Title": "Your App Name", # replace with your actual app name
}
# With the OpenAI SDK, set these once on the client object:
client = openai.OpenAI(
api_key=OPENROUTER_API_KEY,
base_url="https://openrouter.ai/api/v1",
default_headers=REQUIRED_HEADERS,
)
# All calls through this client will include the required headers.
# No per-call extra_headers needed.
# Validation: confirm headers are present on actual request
# Use httpx event hooks to log outgoing request headers before the call is made:
import httpx
def log_request(request: httpx.Request):
print(f"HTTP-Referer: {request.headers.get('http-referer', 'MISSING')}")
print(f"X-Title: {request.headers.get('x-title', 'MISSING')}")
client_audited = openai.OpenAI(
api_key=OPENROUTER_API_KEY,
base_url="https://openrouter.ai/api/v1",
default_headers=REQUIRED_HEADERS,
http_client=httpx.Client(event_hooks={"request": [log_request]}),
)
# Output on next call:
# HTTP-Referer: https://yourapp.com ← confirmed present
# X-Title: Your App Name ← confirmed present
Validation
# Pipeline validation (October 30 2026)
# 200 test documents with known content and expected output topics
# Before fix (HTTP-Referer missing):
# Claude calls: 0% success (200/200 IndexError on empty choices[])
# GPT-4o calls: 100% success (200/200 correct analysis)
# All 500 daily Claude analysis jobs failing for 4 weeks.
# After fix (HTTP-Referer added to default_headers):
# Claude calls: 100% success (200/200 correct analysis)
# GPT-4o calls: 100% success (200/200 correct analysis, unchanged)
# Quality comparison (domain expert evaluation, 50 documents):
# Claude 3.5 Sonnet: 94% accuracy on complex multi-section analysis
# GPT-4o: 87% accuracy on same benchmark
# (Claude chosen for quality-critical analysis tasks; GPT-4o for speed-sensitive tasks)
# Error handling validation:
# Temporarily removed HTTP-Referer header, ran 10 test calls with new guard:
# Confirmed RuntimeError raised with message "OpenRouter returned empty choices: Anthropic requires..."
# rather than unhandled IndexError — error surfaced correctly at the call site
# rather than propagating as obscure IndexError downstream.
# Production monitoring (first 24 hours post-fix):
# Total document analysis jobs: 498
# Claude calls: 249 — success: 249 (100%)
# GPT-4o calls: 249 — success: 249 (100%)
# IndexError count: 0
# Average Claude latency: 3.8s
# Average GPT-4o latency: 1.2s
Related retainer posts
- Ollama developer on retainer —
response.json()raisesJSONDecodeErroron streaming NDJSON response —stream=truedefault —requests 2.31.0— 100% failure 3 weeks - LiteLLM developer on retainer — provider routing misconfiguration causes silent fallback to wrong model tier
- Groq developer on retainer — streaming chat pauses 3–4 seconds mid-response when prompt exceeds 2,048 tokens —
groq 0.9.0TPM budget exhausted mid-stream — 12% of production requests
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →