From b28a673d9444bd2aa4b0d1a6764500502d6381cf Mon Sep 17 00:00:00 2001 From: Pervakov Grigorii Date: Mon, 6 Jul 2026 22:30:30 +0200 Subject: [PATCH] Offer multiple Anthropic models in the composer model picker The picker (added in #147) only exposed the single configured Anthropic model plus local Ollama models, so there was no way to switch, e.g., Opus 4.8 to Fable 5 for a message. Add agent.selectable_models: extra Anthropic model ids that GET /api/models emits alongside the default (de-duplicated, order preserved). The existing composer picker already renders 2+ models and the engine already switches model per turn, so no session state, migration, or new UI is needed. Enabled with [claude-fable-5] in config.example.yaml. Co-Authored-By: Claude Opus 4.7 --- config.example.yaml | 4 ++++ nerve/config.py | 4 ++++ nerve/gateway/routes/models.py | 8 +++++++- tests/test_engine.py | 10 ++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/config.example.yaml b/config.example.yaml index 5fd7e09..d81d3c6 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -36,6 +36,10 @@ agent: # Use claude-fable-5 for Anthropic's most capable (Mythos-class) model. # Note: ~2x the cost of Opus 4.8 ($10/$50 per MTok in/out). cron_model: claude-sonnet-4-6 # Cheaper model for cron jobs + # Extra Anthropic models offered in the web composer's model picker (alongside + # `model`). Lets you switch a message to a stronger model like Fable 5 on demand. + selectable_models: + - claude-fable-5 title_model: claude-haiku-4-5-20251001 # Session title generation max_turns: 50 # Max agentic turns per request max_concurrent: 4 # Max concurrent agent sessions diff --git a/nerve/config.py b/nerve/config.py index 22e6464..142502d 100644 --- a/nerve/config.py +++ b/nerve/config.py @@ -133,6 +133,9 @@ def from_dict(cls, d: dict) -> PromptRewriteConfig: class AgentConfig: model: str = "claude-opus-4-8" cron_model: str = "claude-sonnet-4-6" + # Extra Anthropic chat models to offer in the composer's model picker + # alongside `model`. Empty = only the default model is selectable. + selectable_models: list[str] = field(default_factory=list) title_model: str = "claude-haiku-4-5-20251001" # Session title generation max_turns: int = 100 max_concurrent: int = 4 @@ -175,6 +178,7 @@ def from_dict(cls, d: dict) -> AgentConfig: return cls( model=d.get("model", "claude-opus-4-8"), cron_model=d.get("cron_model", "claude-sonnet-4-6"), + selectable_models=list(d.get("selectable_models") or []), title_model=d.get("title_model", "claude-haiku-4-5-20251001"), max_turns=d.get("max_turns", 100), max_concurrent=d.get("max_concurrent", 4), diff --git a/nerve/gateway/routes/models.py b/nerve/gateway/routes/models.py index dcc9b92..459e52e 100644 --- a/nerve/gateway/routes/models.py +++ b/nerve/gateway/routes/models.py @@ -43,8 +43,14 @@ async def list_models(user: dict = Depends(require_auth)): config = get_config() default_model = config.agent.model + # Default model first, then any extra selectable Anthropic models + # (config.agent.selectable_models), de-duplicated, order preserved. + anthropic_ids: list[str] = [default_model] + for m in config.agent.selectable_models: + if m and m not in anthropic_ids: + anthropic_ids.append(m) models: list[dict[str, str]] = [ - {"id": default_model, "provider": "anthropic"}, + {"id": m, "provider": "anthropic"} for m in anthropic_ids ] ollama_available = False diff --git a/tests/test_engine.py b/tests/test_engine.py index f90f082..01de37d 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -102,6 +102,16 @@ def test_agent_config_cron_effort_default_and_override(): assert cfg.effort == "max" +def test_agent_config_selectable_models(): + # Defaults to an empty list (only the default model is selectable). + assert AgentConfig.from_dict({}).selectable_models == [] + # Explicit list preserved; None coerces to []. + assert AgentConfig.from_dict( + {"selectable_models": ["claude-fable-5"]} + ).selectable_models == ["claude-fable-5"] + assert AgentConfig.from_dict({"selectable_models": None}).selectable_models == [] + + # --------------------------------------------------------------------------- # _iter_response_with_timeout — hung-CLI detection # ---------------------------------------------------------------------------