Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions gateway/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5631,13 +5631,15 @@ def _route_backend(model: str | None, explicit: str | None) -> str:
"opus-4.6": "us.anthropic.claude-opus-4-6", "opus-4.5": "us.anthropic.claude-opus-4-5",
"sonnet-4.6": "us.anthropic.claude-sonnet-4-6", "sonnet-4.5": "us.anthropic.claude-sonnet-4-5",
"opus-5": "us.anthropic.claude-opus-5", "sonnet-5": "us.anthropic.claude-sonnet-5",
"opus-5.5": "us.anthropic.claude-opus-5-5",
"haiku-4.5": "us.anthropic.claude-haiku-4-5-20251001-v1:0", "fable-5": "us.anthropic.claude-fable-5",
"fable-5.1": "us.anthropic.claude-fable-5-1",
}
_ANTHROPIC_CLAUDE = {
"opus-4.8": "claude-opus-4-8", "opus-4.7": "claude-opus-4-7", "opus-4.6": "claude-opus-4-6",
"opus-4.5": "claude-opus-4-5", "sonnet-4.6": "claude-sonnet-4-6", "sonnet-4.5": "claude-sonnet-4-5",
"opus-5": "claude-opus-5", "sonnet-5": "claude-sonnet-5",
"opus-5.5": "claude-opus-5-5",
"haiku-4.5": "claude-haiku-4-5-20251001", "fable-5": "claude-fable-5",
"fable-5.1": "claude-fable-5-1",
}
Expand All @@ -5658,6 +5660,7 @@ def _route_backend(model: str | None, explicit: str | None) -> str:
# vendor/slug forms. TokenRouter mirrors OpenRouter's slugs.
_VENDOR_MODELS: dict[str, dict[str, str]] = {
"anthropic": {
"claude-opus-5.5": "claude-opus-5-5",
"claude-opus-5": "claude-opus-5",
"claude-fable-5": "claude-fable-5",
"claude-fable-5-1": "claude-fable-5-1",
Expand All @@ -5668,6 +5671,7 @@ def _route_backend(model: str | None, explicit: str | None) -> str:
"claude-haiku-4.5": "claude-haiku-4-5-20251001",
},
"bedrock": {
"claude-opus-5.5": "us.anthropic.claude-opus-5-5",
"claude-opus-5": "us.anthropic.claude-opus-5",
"claude-fable-5": "us.anthropic.claude-fable-5",
"claude-fable-5-1": "us.anthropic.claude-fable-5-1",
Expand Down Expand Up @@ -5709,6 +5713,7 @@ def _route_backend(model: str | None, explicit: str | None) -> str:
"gpt-5.4-mini": "openai/gpt-5.4-mini",
"gpt-5.2": "openai/gpt-5.2",
"gpt-5.3-codex": "openai/gpt-5.3-codex",
"claude-opus-5.5": "anthropic/claude-opus-5.5",
"claude-opus-5": "anthropic/claude-opus-5",
"claude-fable-5": "anthropic/claude-fable-5",
"claude-fable-5-1": "anthropic/claude-fable-5.1",
Expand Down Expand Up @@ -5892,6 +5897,7 @@ def _route_backend(model: str | None, explicit: str | None) -> str:
_TOKENROUTER_NO_CHANNEL = {
"minimax-m3", "nemotron-3-ultra", "hunyuan-3", "ling-3.0-flash", "qwen3.7-flash",
"qwen3.8-27b", # not on TokenRouter's /v1/models (2026-09-12)
"claude-opus-5.5", # not on TokenRouter's /v1/models (2026-09-22): the vendor and the other aggregators serve it
# No Meta id is on TokenRouter's list (2026-09-13).
"muse-spark-1.3", "muse-spark-1.2", "muse-spark-1.1", "muse-glimmer-30b",
"llama-4-maverick", "llama-3.3-70b",
Expand Down Expand Up @@ -6051,7 +6057,7 @@ async def _hosted_models_refresh(api_key: str = "", force: bool = False, models_
"gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4",
"gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2",
# Anthropic
"claude-fable-5-1", "claude-fable-5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4.8",
"claude-fable-5-1", "claude-fable-5", "claude-opus-5.5", "claude-opus-5", "claude-sonnet-5", "claude-opus-4.8",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
# Google
"gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash",
Expand Down Expand Up @@ -6191,7 +6197,7 @@ def _valid_default() -> str | None:
# Anthropic's models overview and AWS's own model card), so it routes directly instead of
# falling through to the unmapped default it used at launch.
"claude": {"default": "claude-sonnet-4.6",
"models": ["claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"models": ["claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5"]},
# hermes (NousResearch hermes-agent) is multi-family — it runs any frontier model through
# the matching provider connection (family-aware chain selection in _resp_execute). Friendly
Expand All @@ -6202,7 +6208,7 @@ def _valid_default() -> str | None:
"models": ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
"gpt-5.4", "gpt-5.4-mini", "gpt-5.2",
"gpt-5.3-codex",
"claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
# frontier US+China set, served via the TokenRouter/OpenRouter
# integrations (2026-07-22: each probe-verified through the hermes
Expand Down Expand Up @@ -6235,7 +6241,7 @@ def _valid_default() -> str | None:
"models": ["deepseek-v4-pro", "deepseek-v4-flash",
"gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
"gpt-5.4", "gpt-5.4-mini", "gpt-5.2", "gpt-5.3-codex",
"claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
"gemini-3.6-flash", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "kimi-k3", "glm-5.3", "glm-5.3-flash", "kimi-k2.7-code",
"qwen3.7-max", "qwen3.8-max",
Expand All @@ -6251,7 +6257,7 @@ def _valid_default() -> str | None:
"opencode": {"default": "gpt-5.4",
"models": ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
"gpt-5.4", "gpt-5.4-mini", "gpt-5.2", "gpt-5.3-codex",
"claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
"gemini-3.6-flash", "deepseek-v4-pro", "deepseek-v4-flash", "kimi-k3", "glm-5.3", "glm-5.3-flash",
"kimi-k2.7-code", "qwen3.7-max", "qwen3.8-max",
Expand Down Expand Up @@ -6281,7 +6287,7 @@ def _valid_default() -> str | None:
"aider": {"default": "gpt-5.4",
"models": [
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5.5", "claude-opus-5",
"claude-sonnet-5", "claude-opus-4.8", "claude-opus-4.7", "claude-sonnet-4.6",
"claude-haiku-4.5", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash",
"gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview",
Expand All @@ -6307,7 +6313,7 @@ def _valid_default() -> str | None:
"kimi": {"default": "kimi-k3",
"models": [
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5.5", "claude-opus-5",
"claude-sonnet-5", "claude-opus-4.8", "claude-opus-4.7", "claude-sonnet-4.6",
"claude-haiku-4.5", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash",
"gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview",
Expand All @@ -6328,7 +6334,7 @@ def _valid_default() -> str | None:
"openhands": {"default": "gpt-5.4",
"models": [
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5",
"gpt-5.4-mini", "gpt-5.2", "claude-fable-5-1", "claude-fable-5", "claude-opus-5.5", "claude-opus-5",
"claude-sonnet-5", "claude-opus-4.8", "claude-opus-4.7", "claude-sonnet-4.6",
"claude-haiku-4.5", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash",
"gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview",
Expand All @@ -6343,7 +6349,7 @@ def _valid_default() -> str | None:
"models": ["qwen3.7-max", "qwen3.8-max",
"gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
"gpt-5.4", "gpt-5.4-mini", "gpt-5.2",
"claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
"gemini-3.6-flash", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "deepseek-v4-pro", "deepseek-v4-flash", "kimi-k3",
"kimi-k2.7-code", "mistral-medium-3.5", "step-3.7-flash", "glm-5.3", "glm-5.3-flash",
Expand All @@ -6364,7 +6370,7 @@ def _valid_default() -> str | None:
# the CHANNEL decides, not the model.
"cline": {"default": "gpt-5.4",
"models": ["gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
"gpt-5.5", "gpt-5.4-mini", "gpt-5.2", "claude-opus-5",
"gpt-5.5", "gpt-5.4-mini", "gpt-5.2", "claude-opus-5.5", "claude-opus-5",
"claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5", "claude-opus-4.7",
"claude-sonnet-4.6", "claude-haiku-4.5", "deepseek-v4-pro", "deepseek-v4-flash",
"kimi-k3", "kimi-k2.7-code", "qwen3.7-max", "qwen3.8-max",
Expand All @@ -6375,7 +6381,7 @@ def _valid_default() -> str | None:
"pi": {"default": "gpt-5.4",
"models": ["gpt-6-astra", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
"gpt-5.4", "gpt-5.4-mini", "gpt-5.2", "gpt-5.3-codex",
"claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-5.5", "claude-opus-5", "claude-fable-5", "claude-fable-5-1", "claude-opus-4.8", "claude-sonnet-5",
"claude-opus-4.7", "claude-sonnet-4.6", "claude-haiku-4.5",
"gemini-3.6-flash", "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-flash-lite", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "deepseek-v4-pro", "deepseek-v4-flash", "kimi-k3",
"kimi-k2.7-code", "qwen3.7-max", "qwen3.8-max",
Expand Down Expand Up @@ -6523,7 +6529,7 @@ def _valid_default() -> str | None:
# model), which is today's behaviour and the honest answer: we cannot route to a model that no
# integration here can reach.
_VISION_CAPABLE = ("claude-haiku-4.5", "gpt-5.4-mini", "claude-sonnet-5", "gpt-5.6-luna",
"gpt-5.4", "claude-sonnet-4.6", "claude-opus-5", "gpt-5.5")
"gpt-5.4", "claude-sonnet-4.6", "claude-opus-5.5", "claude-opus-5", "gpt-5.5")


async def _vision_auth(sid: str, backend: str) -> dict | None:
Expand Down
34 changes: 34 additions & 0 deletions gateway/tests/test_opus_5_5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Claude Opus 5.5 (Anthropic's models overview and pricing page, 2026-09-22; OpenRouter's and
Vercel's /v1/models the same day): every provider that serves it names it under its own id, the
one that does not list it yet is left out, and every backend that offers Opus 5 offers it beside."""
from __future__ import annotations

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import app as gw # noqa: E402


def test_every_provider_that_serves_opus_5_5_names_its_own_id():
assert gw._VENDOR_MODELS["anthropic"]["claude-opus-5.5"] == "claude-opus-5-5"
assert gw._VENDOR_MODELS["bedrock"]["claude-opus-5.5"] == "us.anthropic.claude-opus-5-5"
assert gw._VENDOR_MODELS["openrouter"]["claude-opus-5.5"] == "anthropic/claude-opus-5.5"
assert gw._VENDOR_MODELS["vercel"]["claude-opus-5.5"] == "anthropic/claude-opus-5.5"
assert gw._ANTHROPIC_CLAUDE["opus-5.5"] == "claude-opus-5-5" and gw._BEDROCK_CLAUDE["opus-5.5"] == "us.anthropic.claude-opus-5-5"


def test_aggregators_that_do_not_list_it_do_not_promise_it():
assert "claude-opus-5.5" not in gw._VENDOR_MODELS["tokenrouter"] # TokenRouter's list, 2026-09-22
assert "claude-opus-5.5" not in gw._VENDOR_MODELS["llmtr"] # llmtr's list, 2026-09-22


def test_it_sits_beside_opus_5_in_every_catalog_that_offers_opus_5():
for base, cat in gw._MODEL_CATALOG.items():
models = cat["models"]
if "claude-opus-5" in models:
assert models.index("claude-opus-5.5") == models.index("claude-opus-5") - 1, base
else:
assert "claude-opus-5.5" not in models, base
assert gw._MODEL_ORDER.index("claude-opus-5.5") == gw._MODEL_ORDER.index("claude-opus-5") - 1
assert "claude-opus-5.5" in gw._VISION_CAPABLE
Loading
Loading