Skip to content

[Bug]: OpenRouter: Anthropic models send wrong model ID to API (includes openrouter/ prefix) #92611

Description

@lijenhsin

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

🐛 OpenRouter: Anthropic models send wrong model ID to API (includes openrouter/ prefix)

Bug Description

When using OpenRouter provider with Anthropic models (e.g., openrouter/anthropic/claude-sonnet-4.6), the model ID sent to OpenRouter API incorrectly includes the openrouter/ prefix.

  • OpenClaw internal model ID: openrouter/anthropic/claude-sonnet-4.6
  • Sent to OpenRouter API: openrouter/anthropic/claude-sonnet-4.6
  • Expected by OpenRouter API: anthropic/claude-sonnet-4.6

This causes HTTP 400 errors for all Anthropic models via OpenRouter.

Root Cause

In packages/llm-runtime/src/openai-completions.tsbuildParams(), the model parameter is set directly from model.id:

const params = {
  model: model.id,  // ← No normalization for OpenRouter
  ...
};

However, OpenClaw already has the normalization function normalizeOpenRouterModelId() in packages/models/src/extensions/openrouter/models.ts that correctly strips the openrouter/ prefix, but it's not used in the request path.

Affected Code Locations

  1. Primary fix: extensions/openrouter/stream.tsnormalizeOpenRouterResolvedModel() hook should normalize model.id
  2. Alternative fix: packages/llm-runtime/src/openai-completions.tsbuildParams() should normalize for OpenRouter baseUrl

Reproduction

# Configure OpenRouter with anthropic model
openclaw config set 'agents.defaults.models."openrouter/anthropic/claude-sonnet-4.6"' --json '{"alias":"claude"}'
openclaw gateway restart

# Probe fails
openclaw models status --probe
# → openrouter/anthropic/claude-sonnet-4.6: failed

# Direct curl works (correct model ID)
curl -X POST https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -d '{"model":"anthropic/claude-sonnet-4.6","messages":[{"role":"user","content":"hi"}]}'
# → ✅ Success

Workaround

Use non-Anthropic models on OpenRouter (Google, Meta, Mistral, DeepSeek, etc.) which work correctly because their model IDs don't have the anthropic/ prefix confusion.

Suggested Fix

In normalizeOpenRouterResolvedModel() (extensions/openrouter/stream.ts):

function normalizeOpenRouterResolvedModel(model) {
  const normalizedBaseUrl = normalizeOpenRouterBaseUrl(model.baseUrl);
  const reasoning = isOpenRouterProxyReasoningUnsupportedModel(model.id) ? false : model.reasoning;
  const normalizedId = normalizeOpenRouterModelId(model.id);  // Add this
  if ((!normalizedBaseUrl || normalizedBaseUrl === model.baseUrl) && 
      reasoning === model.reasoning &&
      normalizedId === model.id) return;
  return {
    ...model,
    ...normalizedId ? { id: normalizedId } : {},  // Add this
    ...normalizedBaseUrl ? { baseUrl: normalizedBaseUrl } : {},
    reasoning
  };
}

Labels

  • bug
  • provider: openrouter
  • priority: high (affects all Anthropic models on OpenRouter)

Steps to reproduce

Steps to Reproduce

Prerequisites

  • OpenClaw gateway running
  • OpenRouter API key configured (OPENROUTER_API_KEY env var)

1. Configure Anthropic model via OpenRouter

# Add model to allowlist
openclaw config set 'agents.defaults.models."openrouter/anthropic/claude-sonnet-4.6"' --json '{"alias":"claude"}'

# Restart gateway to apply
openclaw gateway restart

2. Verify model appears in probe

openclaw models status --probe

Expected (bug):

openrouter/anthropic/claude-sonnet-4.6  ❌ failed · 400 Bad Request

3. Test direct API call (proves OpenRouter works)

curl -X POST https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-4.6",
    "messages": [{"role": "user", "content": "hi"}],
    "max_tokens": 10
  }'

Result: ✅ Success (returns completion)

4. Compare with working OpenRouter model

openclaw models status --probe | grep gemini

Result:openrouter/google/gemini-2.5-flash ok · 1.8s


Minimal Test Script

#!/bin/bash
# test-openrouter-anthropic.sh

echo "=== Testing OpenRouter Anthropic model ==="
openclaw models status --probe | grep claude

echo -e "\n=== Direct API test (correct model ID) ==="
curl -s -X POST https://openrouter.ai/api/v1/chat/completions \
  -H "Authorization: Bearer $OPENROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"anthropic/claude-sonnet-4.6","messages":[{"role":"user","content":"hi"}],"max_tokens":5}' \
  | jq -r '.choices[0].message.content // .error.message'

Run: bash test-openrouter-anthropic.sh

Expected behavior

Expected Behavior

Probe Output (After Fix)

$ openclaw models status --probe

┌────────────────────────────────────────────┬────────────────────┬─────────────────────────────────────┐
│ Model                                      │ Profile            │ Status                              │
├────────────────────────────────────────────┼────────────────────┼─────────────────────────────────────┤
│ openrouter/anthropic/claude-sonnet-4.6    │ openrouter:default │ ✅ ok · ~2-3s                       │
│ openrouter/google/gemini-2.5-flash        │ openrouter:default │ ✅ ok · 1.8s                        │
└────────────────────────────────────────────┴────────────────────┴─────────────────────────────────────┘

API Request Payload (After Fix)

POST https://openrouter.ai/api/v1/chat/completions
{
  "model": "anthropic/claude-sonnet-4.6",  ← 正確:無 openrouter/ 前綴
  "messages": [...],
  ...
}

User Experience

  1. Config unchanged: User still uses openrouter/anthropic/claude-sonnet-4.6 in config
  2. Routing unchanged: OpenClaw correctly routes to OpenRouter provider
  3. API call succeeds: OpenRouter receives correct model ID, returns completion
  4. All Anthropic models work: claude-3.5-sonnet, claude-3-opus, claude-3-haiku, etc.

Verification Checklist

  • openclaw models status --probe shows ✅ for all openrouter/anthropic/* models
  • Actual chat completion works (not just probe)
  • Streaming works
  • Tool calling works (if model supports)
  • Reasoning/thinking works (if model supports)
  • No regression on other OpenRouter models (Google, Meta, Mistral, DeepSeek, etc.)
  • Direct Anthropic provider (anthropic/claude-*) still works independently

Actual behavior

Actual Behavior

Probe Output

$ openclaw models status --probe

┌────────────────────────────────────────────┬────────────────────┬─────────────────────────────────────┐
│ Model                                      │ Profile            │ Status                              │
├────────────────────────────────────────────┼────────────────────┼─────────────────────────────────────┤
│ openrouter/anthropic/claude-sonnet-4.6    │ openrouter:default │ ❌ failed · 400 Bad Request         │
│ openrouter/google/gemini-2.5-flash        │ openrouter:default │ ✅ ok · 1.8s                        │
└────────────────────────────────────────────┴────────────────────┴─────────────────────────────────────┘

Error Details (from gateway logs)

[openrouter-stream] POST https://openrouter.ai/api/v1/chat/completions 400
{
  "error": {
    "code": 400,
    "message": "Invalid model: openrouter/anthropic/claude-sonnet-4.6. 
                Available models: anthropic/claude-sonnet-4.6, ...",
    "metadata": {
      "provider": "openrouter",
      "model": "openrouter/anthropic/claude-sonnet-4.6"
    }
  }
}

Key Evidence

Aspect Value
Model ID sent to API openrouter/anthropic/claude-sonnet-4.6
Model ID expected by OpenRouter anthropic/claude-sonnet-4.6
Direct curl with correct ID Works perfectly
Other OpenRouter models (Google, Meta, etc.) Work correctly

Why Other Models Work

  • openrouter/google/gemini-2.5-flash → API receives google/gemini-2.5-flash (matches OpenRouter catalog)
  • openrouter/anthropic/claude-sonnet-4.6 → API receives openrouter/anthropic/claude-sonnet-4.6 (does NOT match catalog)

The bug only affects Anthropic models because their OpenRouter catalog IDs start with anthropic/, which collides with OpenClaw's internal openrouter/ prefix stripping logic.

OpenClaw version

2026.6.6

Operating system

macos 26.3.1

Install method

npm global

Model

openrouter/anthropic/claude-sonnet-4.6

Provider / routing chain

openclaw->openrouter -> claude

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

Impact & Severity

Severity: HIGH 🔴

  • User-facing: All Anthropic models via OpenRouter completely broken
  • Scope: Affects claude-sonnet-4.6, claude-3.5-sonnet, claude-3-opus, etc. — all openrouter/anthropic/* models
  • Workaround exists: Yes (use Google/Meta/Mistral/DeepSeek on OpenRouter, or direct Anthropic API)
  • Data loss: No
  • Security: No

Affected Users

User Segment Impact
OpenClaw users with OpenRouter + Anthropic models Cannot use any Anthropic model via OpenRouter
Users relying on claude alias for coding/reasoning Forced to switch models or providers
New users trying OpenRouter Anthropic models Immediate 400 error, bad first impression

Business/Product Impact

  • Model diversity reduced: OpenRouter's main value prop (access to all providers) partially broken
  • Claude 4.6 Sonnet is a flagship model — unavailable via OpenRouter in OpenClaw
  • Workaround friction: Users must reconfigure to direct Anthropic API (needs separate API key, different rate limits, no OpenRouter routing features)

Technical Debt

  • Function exists but unused: normalizeOpenRouterModelId() written, tested, but not hooked into request path
  • Inconsistent behavior: Google/Meta/Mistral models work on OpenRouter, only Anthropic fails
  • Silent failure: Probe shows generic "failed · 400", no hint about model ID format issue

Severity Justification

Criterion Rating
User blocking 🔴 High (complete model category unavailable)
Workaround ease 🟡 Medium (config change, different API key)
Fix complexity 🟢 Low (~3 lines in correct hook)
Regression risk 🟢 Low (normalization is idempotent)

Overall: HIGH severity, LOW fix effort — ideal quick win.

Additional information

No response

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.bugSomething isn't workingclawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerAuth, provider routing, model choice, or SecretRef resolution may break.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.regressionBehavior that previously worked and now fails

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions