Skip to content
Open
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
4 changes: 4 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions nerve/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ class AgentConfig:
cron_backend: str = ""
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
Expand Down Expand Up @@ -202,6 +205,7 @@ def from_dict(cls, d: dict) -> AgentConfig:
cron_backend=str(d.get("cron_backend") or "").strip().lower(),
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),
Expand Down
9 changes: 8 additions & 1 deletion nerve/gateway/routes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,15 @@ async def list_models(user: dict = Depends(require_auth)):
"diagnostics": {"codex": codex_preflight},
}

# 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", "backend": "claude"},
{"id": m, "provider": "anthropic", "backend": "claude"} for m in anthropic_ids
]
if codex_preflight.get("available"):
models.extend({
Expand Down
10 changes: 10 additions & 0 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ def test_claude_system_prompt_excludes_codex_runbook_policy(tmp_path):
assert "Codex-native skills" not in str(options.system_prompt)


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 == []


# ---------------------------------------------------------------------------
# ClaudeClient.receive_turn — per-message idle timeout (hung-CLI detection)
# ---------------------------------------------------------------------------
Expand Down
Loading