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
14 changes: 10 additions & 4 deletions src/agent/agent_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class AgentDefinition:
tools: list[str] | None = None # None or ['*'] means all tools
source: AgentSource = "built-in"
base_dir: str = "built-in"
model: str | None = None # None → inherit parent, 'inherit' → force inherit
# None → the provider's default subagent model (PROVIDER_INFO
# ``subagent_model``; inherits the parent when the provider declares
# none). 'inherit' → always the parent's model.
model: str | None = None
permission_mode: PermissionMode | None = None
max_turns: int | None = None
background: bool = False
Expand Down Expand Up @@ -96,7 +99,8 @@ def _general_purpose_system_prompt(**_kwargs: Any) -> str:
tools=["*"],
source="built-in",
base_dir="built-in",
# model intentionally omitted — uses default subagent model
# model intentionally omitted — uses the provider's default subagent
# model (the cheap fan-out tier; see PROVIDER_INFO subagent_model)
get_system_prompt=_general_purpose_system_prompt,
)

Expand Down Expand Up @@ -160,8 +164,10 @@ def _explore_system_prompt(**_kwargs: Any) -> str:
omit_clawcodex_md=True,
# ch08 round-4 (critic M1) — Explore is the fast/cheap read-only agent;
# TS exploreAgent.ts:77 runs it on Haiku. get_agent_model resolves this
# against the session provider and inherits on providers that don't
# serve haiku (e.g. DeepSeek), so it is cross-provider safe.
# per provider via the ``subagent_tier_models`` tables (anthropic →
# claude-haiku-4-5, deepseek → deepseek-v4-flash) and inherits on
# providers without a haiku-class mapping, so it is cross-provider
# safe.
model="haiku",
get_system_prompt=_explore_system_prompt,
)
Expand Down
340 changes: 305 additions & 35 deletions src/agent/agent_model.py

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions src/agent/run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,13 @@ async def run_agent(params: RunAgentParams) -> AsyncGenerator[Message, None]:

# ch08 round-4 WI-1 — per-subagent model resolution (TS getAgentModel,
# runAgent.ts:340). Resolve the model from the tool param / agent-def /
# env, then apply it to a per-subagent provider CLONE. NEVER mutate the
# shared session provider: ch07 made Agent concurrency-safe, so N
# parallel subagents share params.provider — mutating provider.model
# would race across them. copy.copy shares the HTTP client (thread-safe,
# per-request model) and gives this subagent its own .model.
# env / the provider's default subagent model (PROVIDER_INFO
# ``subagent_model``), then apply it to a per-subagent provider CLONE.
# NEVER mutate the shared session provider: ch07 made Agent
# concurrency-safe, so N parallel subagents share params.provider —
# mutating provider.model would race across them. copy.copy shares the
# HTTP client (thread-safe, per-request model) and gives this subagent
# its own .model.
turn_provider = params.provider
try:
from .agent_model import get_agent_model
Expand All @@ -378,6 +380,16 @@ async def run_agent(params: RunAgentParams) -> AsyncGenerator[Message, None]:
):
turn_provider = copy.copy(params.provider)
turn_provider.model = resolved_model
# Debug trace of the routing — nothing in the app configures a
# level that shows INFO, so don't pretend otherwise. The
# USER-facing surfaces are the Agent tool's result/registry
# "model" field and the agent-progress emits (tools/agent.py
# resolves the same inputs for reporting).
logger.debug(
"subagent %s runs on model %s (session model %s)",
agent_def.agent_type, resolved_model,
getattr(params.provider, "model", None),
)
except Exception: # noqa: BLE001 — model resolution never blocks a spawn
logger.debug("subagent model resolution failed; using session model",
exc_info=True)
Expand Down
2 changes: 1 addition & 1 deletion src/command_system/rename_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ async def _generate_session_name(messages: Any) -> str | None:
default_headers=get_anthropic_custom_headers() or None
)
result = client.messages.create(
model="claude-3-5-haiku-20241022",
model="claude-haiku-4-5",
max_tokens=100,
system=_NAME_PROMPT,
messages=[{"role": "user", "content": text[:4000]}],
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/exec_agent_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ async def execute_agent_hook(
messages = [{"role": "user", "content": user_prompt}]
system_prompt = AGENT_HOOK_SYSTEM_PROMPT

effective_model = model or "claude-sonnet-4-20250514"
effective_model = model or "claude-sonnet-5"

if hasattr(provider, "chat_async"):
response = await provider.chat_async(
Expand Down
2 changes: 1 addition & 1 deletion src/memdir/find_relevant_memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _resolve_recall_model(provider: Any) -> str | None:
TS pins the selector to a small default (``getDefaultSonnetModel``) so a
turn on an expensive session model doesn't pay full price for the recall
call. The multi-provider port wires the ``small_fast_model`` setting —
BUT its shipped default is an Anthropic id (``claude-3-5-haiku-``,
BUT its shipped default is an Anthropic id (``claude-haiku-4-5``,
settings/constants.py), which is only valid on the first-party Anthropic
endpoint. Passing it to a DeepSeek/OpenAI/Minimax session would 400 and
(since recall swallows errors) silently kill recall every turn (critic
Expand Down
8 changes: 5 additions & 3 deletions src/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@
get_context_window_for_model,
get_model_max_output_tokens,
)
from .agent_routing import get_model_for_agent, AgentModelConfig
# NOTE: the old ``agent_routing`` module (get_model_for_agent /
# AgentModelConfig) was deleted 2026-08: it was a dead parallel path with
# inherit-parent semantics that contradicted the shipped resolver
# (src/agent/agent_model.get_agent_model), and nothing wrote the
# ``agent_models`` config key it read.

__all__ = [
"BEDROCK_MODEL_MAP",
"MODEL_ALIASES",
"MODEL_CONFIGS",
"AgentModelConfig",
"ModelConfig",
"canonical_model_name",
"deprecation_warning",
Expand All @@ -42,7 +45,6 @@
"get_context_window_for_model",
"get_model_capabilities",
"get_model_config",
"get_model_for_agent",
"get_model_max_output_tokens",
"is_model_allowed",
"resolve_alias",
Expand Down
37 changes: 0 additions & 37 deletions src/models/agent_routing.py

This file was deleted.

66 changes: 49 additions & 17 deletions src/models/aliases.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,63 @@
"""Model alias table matching TypeScript model/aliases.ts."""
"""Model alias table matching TypeScript model/aliases.ts.

The bare family aliases (``sonnet`` / ``opus`` / ``haiku`` / ``fable``) track
the CURRENT first-party model of each family — the TS reference resolves
them through ``getDefaultSonnetModel()``-style functions that are updated at
every model launch (the ``@[MODEL LAUNCH]`` markers in model.ts). Targets
below were verified against the live ``GET /v1/models`` catalog 2026-08-12:
the API no longer serves ``claude-sonnet-4-20250514`` or
``claude-3-5-haiku-20241022`` (404 ``not_found_error``), so pointing an
alias at them turns every ``--model sonnet`` / Explore-agent spawn into a
hard API error.

Note on ``haiku``: the live catalog LISTS only the dated
``claude-haiku-4-5-20251001``, but the bare ``claude-haiku-4-5`` resolves
server-side to it (probed live 2026-08-12 — the API's max_tokens 400 names
the dated id), so the alias uses the bare form like every other current
target.

Per-provider SUBAGENT tier resolution (e.g. ``haiku`` on a DeepSeek
session) does not use this table — see ``subagent_model`` /
``subagent_tier_models`` in ``src/providers/__init__.py`` and
``src/agent/agent_model.py``.
"""

from __future__ import annotations

MODEL_ALIASES: dict[str, str] = {
# Short names → canonical
"sonnet": "claude-sonnet-4-20250514",
"opus": "claude-opus-4-20250514",
"haiku": "claude-3-5-haiku-20241022",

# Version aliases
"claude-4-sonnet": "claude-sonnet-4-20250514",
"claude-4-opus": "claude-opus-4-20250514",
"claude-sonnet": "claude-sonnet-4-20250514",
"claude-opus": "claude-opus-4-20250514",
"claude-haiku": "claude-3-5-haiku-20241022",

# Legacy aliases
# Short names → canonical (current family heads)
"sonnet": "claude-sonnet-5",
"opus": "claude-opus-5",
"haiku": "claude-haiku-4-5",
"fable": "claude-fable-5",

# Version aliases. The explicitly-versioned ``claude-4-*`` spellings
# point at the newest LIVE 4.x snapshot of their family (the dated
# 2025-05-14 ids they used to target were retired from the API).
"claude-4-sonnet": "claude-sonnet-4-6",
"claude-4-opus": "claude-opus-4-8",
"claude-sonnet": "claude-sonnet-5",
"claude-opus": "claude-opus-5",
"claude-haiku": "claude-haiku-4-5",
"claude-fable": "claude-fable-5",

# Legacy aliases — explicit historical pins, kept verbatim. These name a
# specific retired generation on purpose; the API answers 404 for them,
# which is more honest than silently substituting a different model.
"claude-3.5-sonnet": "claude-3-5-sonnet-20241022",
"claude-3.5-haiku": "claude-3-5-haiku-20241022",
"claude-3-sonnet": "claude-3-sonnet-20240229",
"claude-3-opus": "claude-3-opus-20240229",
"claude-3-haiku": "claude-3-haiku-20240307",
"claude-3.7-sonnet": "claude-3-7-sonnet-20250219",

# Common typos / shortcuts
"s4": "claude-sonnet-4-20250514",
"o4": "claude-opus-4-20250514",
# Common typos / shortcuts. ``s4``/``o4`` mean "sonnet 4"/"opus 4" —
# same intent as claude-4-sonnet/claude-4-opus above, so they track the
# same newest LIVE 4.x snapshots (their old dated targets were retired
# and 404). ``h35`` names a specific retired generation like the legacy
# block, and keeps its historical pin.
"s4": "claude-sonnet-4-6",
"o4": "claude-opus-4-8",
"h35": "claude-3-5-haiku-20241022",
}

Expand Down
58 changes: 54 additions & 4 deletions src/models/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ class ModelConfig:
# Every 4.x id still matches the earlier ``claude-opus-4`` base first,
# so their 200K/legacy resolution is unchanged, but two other strings
# now land here: an unregistered future ``claude-opus-<n>``, AND the
# literal ``claude-opus`` — a live MODEL_ALIASES key (aliases.py:15)
# that resolves to claude-opus-4-20250514, so ``display_name()`` on the
# UNRESOLVED alias now reads "Claude Opus 5". Callers that canonicalize
# first (the normal path) are unaffected; test_model_system pins both.
# literal ``claude-opus`` — a live MODEL_ALIASES key that (since the
# 2026-08 alias refresh) also RESOLVES to claude-opus-5, so the
# resolved and unresolved spellings agree on "Claude Opus 5".
# This inverts the "under-estimate is the safe direction" note above
# for unknown opus ids, and over-estimating is the worse failure —
# auto-compact never fires and the request eventually exceeds the real
Expand All @@ -122,6 +121,57 @@ class ModelConfig:
cost_cache_create_per_mtok=6.25,
cost_cache_read_per_mtok=0.50,
),
# Claude Sonnet 5 — the current Sonnet tier (the ``sonnet`` alias and
# subagent tier target; see ``subagent_tier_models`` in
# src/providers/__init__.py). 128K true output cap probed live
# 2026-08-12 (``max_tokens: 2000000 > 128000`` from the API's own
# 400); the 1M context window is NOT probed — the window probe kept
# rate-limiting — and comes from the launch docs plus the Claude-5
# family convention (opus-5 / fable-5 / opus-4-8 all ship 1M). $2/$10
# per MTok — Sonnet 5's launch pricing was made permanent, so it does
# NOT inherit the 4.x sonnet 3/15 tier. Same 32_000 first-attempt wire
# ``max_tokens`` convention as the other 1M rows above.
#
# Placement is load-bearing the same way ``claude-opus-5``'s is: this
# row's prefix base is the family-wide ``claude-sonnet``, so it must
# sit AFTER ``claude-sonnet-4-20250514`` (the table's first row) —
# unknown 4.x sonnet ids (claude-sonnet-4-5-*, -4-6 …) keep resolving
# to that conservative 200K row first, and only unregistered future
# ``claude-sonnet-<n>`` ids land here.
"claude-sonnet-5": ModelConfig(
model_id="claude-sonnet-5",
display_name="Claude Sonnet 5",
context_window=1_000_000,
max_output_tokens=32_000,
supports_thinking=True,
cost_input_per_mtok=2.0,
cost_output_per_mtok=10.0,
cost_cache_create_per_mtok=2.50,
cost_cache_read_per_mtok=0.20,
),
# Claude Haiku 4.5 — the anthropic DEFAULT SUBAGENT model and its
# ``haiku`` tier target (Explore runs on it, mirroring TS
# exploreAgent.ts; cheapest current-gen tier at 1/5 per MTok). The
# live catalog LISTS only this dated id, but the bare
# ``claude-haiku-4-5`` — the spelling the alias table and subagent
# tables use — resolves server-side to it (probed live 2026-08-12).
# Window and cap probed the same day: 200K window (``prompt is too
# long: … > 200000``) and a 64_000 true output cap (``max_tokens: …
# > 64000``), so the 32_000 first-attempt convention leaves the 64K
# truncation-escalation exactly at the model's real ceiling. Prefix
# base ``claude-haiku-4-5`` also catches the bare spelling and future
# dated snapshots.
"claude-haiku-4-5-20251001": ModelConfig(
model_id="claude-haiku-4-5-20251001",
display_name="Claude Haiku 4.5",
context_window=200_000,
max_output_tokens=32_000,
supports_thinking=True,
cost_input_per_mtok=1.0,
cost_output_per_mtok=5.0,
cost_cache_create_per_mtok=1.25,
cost_cache_read_per_mtok=0.10,
),

# Claude 3.7 series
"claude-3-7-sonnet-20250219": ModelConfig(
Expand Down
2 changes: 1 addition & 1 deletion src/models/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def resolve_model(name: str) -> str:
"""Resolve a model name/alias to its canonical form.

Steps:
1. Resolve alias (e.g. "sonnet" → "claude-sonnet-4-20250514")
1. Resolve alias (e.g. "sonnet" → "claude-sonnet-5")
2. Return canonical name
"""
canonical = resolve_alias(name)
Expand Down
Loading
Loading