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
38 changes: 38 additions & 0 deletions src/models/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 7 additions & 2 deletions src/providers/anthropic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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",
Expand Down
38 changes: 27 additions & 11 deletions src/query/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions src/services/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
Loading