From 96cb953a2798f68a9bdcfbc1f8f23392aaab627b Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sun, 6 Sep 2026 20:58:03 -0700 Subject: [PATCH] [eric] models: Fable 5 back on both lanes (1-token probe 200, drill turn completed), Fable 5.1 API-key only (the pinned router's CLI version is refused on the sub lane), gpt-5.4 off the ChatGPT lane (OpenAI refuses it), Sonnet 5 at its $2/$10 rate Co-Authored-By: Claude Fable 5.1 --- backend/apps/agents/providers/pricing.py | 4 ++++ backend/apps/agents/providers/registry.py | 22 ++++++++++++++++++---- backend/tests/test_v2_invariants.py | 13 ++++++++----- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/backend/apps/agents/providers/pricing.py b/backend/apps/agents/providers/pricing.py index 12a0fe16..32a8d07d 100644 --- a/backend/apps/agents/providers/pricing.py +++ b/backend/apps/agents/providers/pricing.py @@ -20,6 +20,10 @@ MODEL_TIERS: dict[str, tuple[int, int, int]] = { "anthropic/claude-opus-4": (5, 2, 5), "claude-sonnet-5": (5, 4, 3), "anthropic/claude-sonnet-5": (5, 4, 3), + "claude-fable-5-1": (5, 2, 5), + "anthropic/claude-fable-5-1": (5, 2, 5), + "claude-fable-5": (5, 2, 5), + "anthropic/claude-fable-5": (5, 2, 5), "claude-sonnet-4-6": (4, 4, 3), "claude-sonnet-4.6": (4, 4, 3), "anthropic/claude-sonnet-4.6": (4, 4, 3), diff --git a/backend/apps/agents/providers/registry.py b/backend/apps/agents/providers/registry.py index 3394514f..05078bbe 100644 --- a/backend/apps/agents/providers/registry.py +++ b/backend/apps/agents/providers/registry.py @@ -39,6 +39,12 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = { # Opus 5 (added 2026-07-26): drop-in Opus 4.8 successor, same $5/$25, 1M ctx, 128k out. Thinking is ON by default; explicit thinking:disabled is only valid at effort<=high, which is all this app ever sends (run_options_helpers caps at "high"), so the off toggle stays safe. {"value": "opus-5", "label": "Claude Opus 5", "context_window": 1_000_000, "model_id": "claude-opus-5", "router_model_id": "cc/claude-opus-5", "api": "anthropic", "reasoning": True}, + # Fable 5.1 (current, $10/$50, 1M ctx, 128k out, adaptive thinking always on) is API-KEY ONLY here: the pinned + # 9router 0.3.60 presents itself to Anthropic as "claude-cli/2.1.63" and the subscription endpoint answers + # "Claude Code 2.1.63 does not support this model; version 2.1.251 or newer is required" (probed 2026-09-06). + # Fable 5 (legacy, same price, retire not before 2027-06-09) serves on the subscription lane: 1-token probe 200. + {"value": "fable-5", "label": "Claude Fable 5", "context_window": 1_000_000, + "model_id": "claude-fable-5", "router_model_id": "cc/claude-fable-5", "api": "anthropic", "reasoning": True}, # Opus 4.8 (released 2026-05-28): previous Opus flagship. Adaptive thinking (not extended), effort param defaults to high. 1M ctx, 128k max output, $5/$25. Verified live on the cc sub route (this app runs on it) and the API. {"value": "opus-4-8", "label": "Claude Opus 4.8", "context_window": 1_000_000, "model_id": "claude-opus-4-8", "router_model_id": "cc/claude-opus-4-8", "api": "anthropic", "reasoning": True}, @@ -52,11 +58,14 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = { "model_id": "claude-sonnet-4-6", "router_model_id": "cc/claude-sonnet-4-6", "api": "anthropic", "reasoning": True}, {"value": "opus", "label": "Claude Opus 4.6", "context_window": 1_000_000, "model_id": "claude-opus-4-6", "router_model_id": "cc/claude-opus-4-6", "api": "anthropic", "reasoning": True}, + # Retirement "not sooner than October 15, 2026", the soonest in this catalog (model page, read 2026-09-06); no model here is retired today. {"value": "haiku", "label": "Claude Haiku 4.5", "context_window": 200_000, "model_id": "claude-haiku-4-5", "router_model_id": "cc/claude-haiku-4-5-20251001", "api": "anthropic", "reasoning": True}, # cc/ pins the user's Claude sub regardless of connection_mode. {"value": "opus-5-cc", "label": "Claude Opus 5", "context_window": 1_000_000, "model_id": "claude-opus-5", "router_model_id": "cc/claude-opus-5", "api": "anthropic", "reasoning": True, "route": "cc"}, + {"value": "fable-5-cc", "label": "Claude Fable 5", "context_window": 1_000_000, + "model_id": "claude-fable-5", "router_model_id": "cc/claude-fable-5", "api": "anthropic", "reasoning": True, "route": "cc"}, {"value": "opus-4-8-cc", "label": "Claude Opus 4.8", "context_window": 1_000_000, "model_id": "claude-opus-4-8", "router_model_id": "cc/claude-opus-4-8", "api": "anthropic", "reasoning": True, "route": "cc"}, {"value": "opus-4-7-cc", "label": "Claude Opus 4.7", "context_window": 1_000_000, @@ -73,6 +82,10 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = { # Fable 5 re-added 2026-07-02 after the ban lifted (Eric confirmed access is back); pull both rows again if it errors live. {"value": "opus-5-api", "label": "Claude Opus 5 (API key)", "context_window": 1_000_000, "model_id": "claude-opus-5", "router_model_id": "claude-opus-5", "api": "anthropic", "reasoning": True, "route": "api"}, + {"value": "fable-5-1-api", "label": "Claude Fable 5.1 (API key)", "context_window": 1_000_000, + "model_id": "claude-fable-5-1", "router_model_id": "claude-fable-5-1", "api": "anthropic", "reasoning": True, "route": "api"}, + {"value": "fable-5-api", "label": "Claude Fable 5 (API key)", "context_window": 1_000_000, + "model_id": "claude-fable-5", "router_model_id": "claude-fable-5", "api": "anthropic", "reasoning": True, "route": "api"}, {"value": "opus-4-8-api", "label": "Claude Opus 4.8 (API key)", "context_window": 1_000_000, "model_id": "claude-opus-4-8", "router_model_id": "claude-opus-4-8", "api": "anthropic", "reasoning": True, "route": "api"}, {"value": "opus-4-7-api", "label": "Claude Opus 4.7 (API key)", "context_window": 1_000_000, @@ -111,9 +124,7 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = { {"value": "gpt-5.5", "label": "GPT-5.5", "context_window": 1_000_000, "router_model_id": "cx/gpt-5.5", "api": "codex", "subscription_only": True, "reasoning": True}, - {"value": "gpt-5.4", "label": "GPT-5.4", - "context_window": 1_000_000, "router_model_id": "cx/gpt-5.4", - "api": "codex", "subscription_only": True, "reasoning": True}, + # gpt-5.4 has no ChatGPT-lane row: OpenAI refuses it there ("not supported when using Codex with a ChatGPT account", 400, drilled 2026-09-06); the API-key row below stays, unverified on this box. {"value": "gpt-5.4-mini", "label": "GPT-5.4 Mini", "context_window": 400_000, "router_model_id": "cx/gpt-5.4-mini", "api": "codex", "subscription_only": True, "reasoning": True}, @@ -409,7 +420,10 @@ def get_context_window(provider: str, model: str, settings: AppSettings | None = COST_PER_1M_TOKENS: dict[tuple[str, str], tuple[float, float]] = { # (provider, model): (input_cost_per_1M, output_cost_per_1M) NOTE: real cost numbers come from 9Router's usage stats. These entries are kept so the table matches BUILTIN_MODELS and can be used by any future native-loop path. Subscription-routed models are zero-cost to the user, but API rates are recorded here for reference where they exist. Anthropic (direct API rates). ("Anthropic", "sonnet"): (3.0, 15.0), - ("Anthropic", "sonnet-5"): (3.0, 15.0), + ("Anthropic", "sonnet-5"): (2.0, 10.0), + # Fable 5 (legacy, retire not before 2027-06-09) and Fable 5.1, both $10 / $50 (platform.claude.com model pages, read 2026-09-06). + ("Anthropic", "fable-5"): (10.0, 50.0), + ("Anthropic", "fable-5-1"): (10.0, 50.0), ("Anthropic", "opus"): (5.0, 25.0), ("Anthropic", "opus-4-7"): (5.0, 25.0), ("Anthropic", "opus-4-8"): (5.0, 25.0), diff --git a/backend/tests/test_v2_invariants.py b/backend/tests/test_v2_invariants.py index 94053a70..62f541fc 100644 --- a/backend/tests/test_v2_invariants.py +++ b/backend/tests/test_v2_invariants.py @@ -672,16 +672,18 @@ def test_banned_models_not_offered(): selectable and simply did not work, and Opus covers the same ground.)""" from backend.apps.agents.providers.registry import BUILTIN_MODELS all_values = {m["value"] for models in BUILTIN_MODELS.values() for m in models} - for dead in ("gemini-3.1-pro", "gemini-3.1-pro-api", "gemini-3-flash", "gemini-3-flash-api", - "fable-5-cc", "fable-5-api"): + # Fable 5 left this list 2026-09-06: a 1-token probe on cc/claude-fable-5 through the pinned router answered 200 + # with a real completion and a drill turn on the sub lane completed. gpt-5.4 (cx) joined it the same day: OpenAI + # answers "The 'gpt-5.4' model is not supported when using Codex with a ChatGPT account" (400, deterministic). + for dead in ("gemini-3.1-pro", "gemini-3.1-pro-api", "gemini-3-flash", "gemini-3-flash-api", "gpt-5.4"): assert dead not in all_values, f"{dead} is back in the picker" assert "gpt-5.5-api" in all_values assert "gpt-5.5" in all_values # cx lane restored 2026-07-26 (live-probed) # No '3.1 pro' label survives in any provider group either. all_labels = " | ".join(m["label"].lower() for models in BUILTIN_MODELS.values() for m in models) assert "3.1 pro" not in all_labels - assert "fable" not in all_labels assert "gemini 3 flash" not in all_labels + assert "gpt-5.4" not in {m["value"] for m in BUILTIN_MODELS["OpenAI"] if m.get("api") == "codex"} # =========================================================================== Group E, 9Router-streamed 401 detection =========================================================================== 9Router sometimes returns upstream auth failures AS the assistant's reply text, not as an exception. We detect the pattern in the stream handler to substitute a friendly bubble. @@ -1189,9 +1191,10 @@ async def test_aux_returns_sonnet_when_preferred_tier_set(): def test_get_api_type_openai(): from backend.apps.agents.providers.registry import get_api_type - # gpt-5.4 maps to codex (the OpenAI-via-Codex-subscription api family) - api = get_api_type("gpt-5.4") + # gpt-5.5 maps to codex (the OpenAI-via-Codex-subscription api family); gpt-5.4 left that lane 2026-09-06. + api = get_api_type("gpt-5.5") assert api in ("openai", "codex"), f"unexpected: {api}" + assert get_api_type("gpt-5.4-api") == "openai" def test_find_builtin_model_returns_none_for_unknown():