From 6cdd89b72f5b7fd087a0148c6f1144407f135b30 Mon Sep 17 00:00:00 2001 From: agentforce314 Date: Mon, 13 Jul 2026 15:17:52 -0700 Subject: [PATCH] feat(anthropic): register claude-opus-4-8 and claude-fable-5 model configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the two latest Anthropic models across every model-config surface: - models/configs.py: ModelConfig rows (1M context; 32K first-attempt wire max_tokens with the 128K true cap noted; Opus 4.8 at $5/$25, Fable 5 at $10/$50 per MTok with matching cache rates). Placed after the legacy opus row so prefix fallback for opus-4-1..4-7 is unchanged. - services/pricing.py: new 10/50 tier for Fable 5; exact PRICING and _FAMILY_PREFIXES entries for both models (dated snapshots included). - providers: both added to the anthropic available_models lists; `fable` added to the large-max-tokens pattern so Fable 5 isn't capped at 4096. - query.py: opus-4-8 and fable-5 added to the adaptive-thinking and effort allowlists, fable to the thinking-eligible pattern — without this, opus-4-8 would take the budget_tokens path, a hard 400 on 4.8+. Co-Authored-By: Claude Fable 5 --- src/models/configs.py | 38 +++++++++++++++++++++++++++++ src/providers/__init__.py | 3 +++ src/providers/anthropic_provider.py | 9 +++++-- src/query/query.py | 38 ++++++++++++++++++++--------- src/services/pricing.py | 11 +++++++++ 5 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/models/configs.py b/src/models/configs.py index 3a9f1edb..dc024eea 100644 --- a/src/models/configs.py +++ b/src/models/configs.py @@ -51,6 +51,44 @@ class ModelConfig: cost_cache_read_per_mtok=1.50, ), + # Claude Opus 4.8 / Fable 5 (current frontier — 1M context window, + # 128K true output cap). Placed AFTER the legacy claude-opus-4-20250514 + # row on purpose: ``get_model_config``'s prefix fallback iterates in + # insertion order and both opus keys share the ``claude-opus-4`` base, + # so bare 4.x ids without an exact entry (opus-4-1/4-5/4-6/4-7 and + # dated snapshots) keep resolving to the legacy 200K row — under- + # estimating the window compacts early, the safe direction (see the + # GPT-5 note below). Register future dated 4-8 snapshots explicitly. + # + # max_output_tokens is the FIRST-ATTEMPT wire ``max_tokens`` for + # Anthropic providers (``resolve_max_output_tokens`` step 3), not the + # model's capability ceiling: 32_000 keeps the query loop's 64K + # truncation-escalation (``ESCALATED_MAX_TOKENS``) meaningful. + "claude-opus-4-8": ModelConfig( + model_id="claude-opus-4-8", + display_name="Claude Opus 4.8", + context_window=1_000_000, + max_output_tokens=32_000, + supports_thinking=True, + supports_computer_use=True, + cost_input_per_mtok=5.0, + cost_output_per_mtok=25.0, + cost_cache_create_per_mtok=6.25, + cost_cache_read_per_mtok=0.50, + ), + "claude-fable-5": ModelConfig( + model_id="claude-fable-5", + display_name="Claude Fable 5", + context_window=1_000_000, + max_output_tokens=32_000, + supports_thinking=True, + supports_computer_use=True, + cost_input_per_mtok=10.0, + cost_output_per_mtok=50.0, + cost_cache_create_per_mtok=12.50, + cost_cache_read_per_mtok=1.00, + ), + # Claude 3.7 series "claude-3-7-sonnet-20250219": ModelConfig( model_id="claude-3-7-sonnet-20250219", diff --git a/src/providers/__init__.py b/src/providers/__init__.py index 3e9811d4..e6dee752 100644 --- a/src/providers/__init__.py +++ b/src/providers/__init__.py @@ -21,12 +21,15 @@ class ProviderInfo(TypedDict): "default_base_url": "https://api.anthropic.com", "default_model": "claude-sonnet-4-6", "available_models": [ + # Frontier (above Opus tier) + "claude-fable-5", # Claude 4 series (latest) "claude-sonnet-4-6", "claude-sonnet-4-5", "claude-sonnet-4-5-20250929", "claude-sonnet-4-0", "claude-sonnet-4-20250514", + "claude-opus-4-8", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-5-20251101", diff --git a/src/providers/anthropic_provider.py b/src/providers/anthropic_provider.py index afe52ed4..13ddad5f 100644 --- a/src/providers/anthropic_provider.py +++ b/src/providers/anthropic_provider.py @@ -133,9 +133,11 @@ def _extract_usage_dict(usage: Any) -> dict[str, Any]: # Older Claude 3.x snapshots cap at 4K-8K depending on tier; pulling the # default up to 32K would make the API reject those requests with a 400. # Detection is by model-name pattern so newer 4.x point releases -# (e.g. ``claude-opus-4-7-20260201``) opt in automatically. +# (e.g. ``claude-opus-4-7-20260201``) opt in automatically. ``fable`` +# covers the Claude 5 frontier family (claude-fable-5), which has a +# 128K output ceiling — well above the 32K default used here. _LARGE_MAX_TOKENS_MODEL_PATTERN = re.compile( - r"claude-(?:sonnet|opus|haiku)-(?:4-\d+|[5-9]\b|\d{2,})", + r"claude-(?:sonnet|opus|haiku|fable)-(?:4-\d+|[5-9]\b|\d{2,})", re.IGNORECASE, ) @@ -774,12 +776,15 @@ def get_available_models(self) -> list[str]: List of model names """ return [ + # Frontier (above Opus tier) + "claude-fable-5", # Claude 4 series (latest) "claude-sonnet-4-6", "claude-sonnet-4-5", "claude-sonnet-4-5-20250929", "claude-sonnet-4-0", "claude-sonnet-4-20250514", + "claude-opus-4-8", "claude-opus-4-6", "claude-opus-4-5", "claude-opus-4-5-20251101", diff --git a/src/query/query.py b/src/query/query.py index 48adb68b..e96a5fa5 100644 --- a/src/query/query.py +++ b/src/query/query.py @@ -417,7 +417,7 @@ def _is_hook_stopped_continuation(msg: Message | None) -> bool: _THINKING_ELIGIBLE_MODEL_PATTERN = re.compile( - r"claude-(?:sonnet|opus|haiku)-(?:4-\d+|[5-9]\b|\d{2,})", + r"claude-(?:sonnet|opus|haiku|fable)-(?:4-\d+|[5-9]\b|\d{2,})", re.IGNORECASE, ) @@ -448,29 +448,45 @@ def _model_supports_adaptive_thinking(model: str | None) -> bool: Only a subset of Claude 4 models accept ``thinking={"type": "adaptive"}``; the rest support thinking only with an explicit ``budget_tokens``. Sending adaptive to a non-adaptive model is rejected with HTTP 400 "adaptive - thinking is not supported on this model". Allowlist ported verbatim from - TS ``modelSupportsAdaptiveThinking`` (thinking.ts:152-169): Opus 4.6/4.7 - and Sonnet 4.6. Substring match mirrors the reference's ``.includes()`` - so dated snapshots (``claude-sonnet-4-6-20250929``) match. + thinking is not supported on this model". Allowlist ported from TS + ``modelSupportsAdaptiveThinking`` (thinking.ts:152-169): Opus 4.6/4.7 + and Sonnet 4.6; extended with Opus 4.8 and Fable 5, where adaptive is + the ONLY accepted thinking config — the budget fallback below would be + a hard 400 on them (``budget_tokens`` is removed on 4.7+; Fable 5 also + rejects ``{"type": "disabled"}``, and accepts adaptive or an omitted + param). Substring match mirrors the reference's ``.includes()`` so + dated snapshots (``claude-sonnet-4-6-20250929``) match. """ if not model: return False m = model.lower() - return "opus-4-7" in m or "opus-4-6" in m or "sonnet-4-6" in m + return ( + "fable-5" in m + or "opus-4-8" in m + or "opus-4-7" in m + or "opus-4-6" in m + or "sonnet-4-6" in m + ) def _model_supports_effort(model: str | None) -> bool: """True iff the model accepts ``output_config={"effort": ...}``. - Narrower than thinking support — Opus 4.6 and Sonnet 4.6 only (TS - ``modelSupportsEffort``, effort.ts:32-51). Sending effort to a model that - doesn't support it is rejected, so the caller gates on this independently - of the thinking type. + Narrower than thinking support — Opus 4.6/4.8, Sonnet 4.6, and Fable 5 + (TS ``modelSupportsEffort``, effort.ts:32-51, plus the 4.8/Fable + additions where effort is GA). Sending effort to a model that doesn't + support it is rejected, so the caller gates on this independently of + the thinking type. """ if not model: return False m = model.lower() - return "opus-4-6" in m or "sonnet-4-6" in m + return ( + "fable-5" in m + or "opus-4-8" in m + or "opus-4-6" in m + or "sonnet-4-6" in m + ) def _is_overloaded_error(e: Exception) -> bool: diff --git a/src/services/pricing.py b/src/services/pricing.py index 607c187d..1c773ec4 100644 --- a/src/services/pricing.py +++ b/src/services/pricing.py @@ -41,6 +41,12 @@ "cache_creation": 6.25 / 1_000_000, "cache_read": 0.50 / 1_000_000, } +_TIER_10_50 = { + "input": 10.0 / 1_000_000, + "output": 50.0 / 1_000_000, + "cache_creation": 12.50 / 1_000_000, + "cache_read": 1.00 / 1_000_000, +} _TIER_HAIKU_45 = { "input": 1.0 / 1_000_000, "output": 5.0 / 1_000_000, @@ -127,7 +133,10 @@ "claude-3-7-sonnet-20250219": _TIER_3_15, "claude-3-5-sonnet-20241022": _TIER_3_15, "claude-3-5-sonnet-20240620": _TIER_3_15, + # Fable family — frontier tier above Opus (10/50) + "claude-fable-5": _TIER_10_50, # Opus family — 4.5+ on the 5/25 tier, 4.0/4.1 on 15/75 + "claude-opus-4-8": _TIER_5_25, "claude-opus-4-7": _TIER_5_25, "claude-opus-4-6": _TIER_5_25, "claude-opus-4-5": _TIER_5_25, @@ -160,9 +169,11 @@ # tier. Unknown opus-4.x now falls through to None instead of being # tagged with the wrong price. _FAMILY_PREFIXES: list[tuple[str, dict[str, float]]] = [ + ("claude-fable-5", _TIER_10_50), ("claude-haiku-4", _TIER_HAIKU_45), ("claude-3-5-haiku", _TIER_HAIKU_45), ("claude-3-haiku", _TIER_HAIKU_3), + ("claude-opus-4-8", _TIER_5_25), ("claude-opus-4-7", _TIER_5_25), ("claude-opus-4-6", _TIER_5_25), ("claude-opus-4-5", _TIER_5_25),