diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index adb797bd3..e7753df51 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -437,8 +437,7 @@ class AgentLoop: context_window_tokens = extra.pop("context_window_tokens", None) or resolved.context_window_tokens model_preset_snapshot_builder = extra.pop("model_preset_snapshot_builder", None) model_presets = dict(config.model_presets) - if "default" not in model_presets: - model_presets["default"] = resolved + model_presets["default"] = config.resolve_default_preset() return cls( bus=bus, provider=provider, diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 43936597b..c2fceff22 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -282,17 +282,12 @@ class Config(BaseSettings): @model_validator(mode="after") def _validate_model_preset(self) -> "Config": name = self.agents.defaults.model_preset - if name and name not in self.model_presets: + if name and name != "default" and name not in self.model_presets: raise ValueError(f"model_preset {name!r} not found in model_presets") return self - def resolve_preset(self, name: str | None = None) -> ModelPresetConfig: - """Return effective model params: from active preset, or individual defaults.""" - name = self.agents.defaults.model_preset if name is None else name - if name: - if name not in self.model_presets: - raise KeyError(f"model_preset {name!r} not found in model_presets") - return self.model_presets[name] + def resolve_default_preset(self) -> ModelPresetConfig: + """Return the implicit `default` preset from agents.defaults fields.""" d = self.agents.defaults return ModelPresetConfig( model=d.model, provider=d.provider, max_tokens=d.max_tokens, @@ -300,6 +295,15 @@ class Config(BaseSettings): temperature=d.temperature, reasoning_effort=d.reasoning_effort, ) + def resolve_preset(self, name: str | None = None) -> ModelPresetConfig: + """Return effective model params from a named preset or the implicit default.""" + name = self.agents.defaults.model_preset if name is None else name + if not name or name == "default": + return self.resolve_default_preset() + if name not in self.model_presets: + raise KeyError(f"model_preset {name!r} not found in model_presets") + return self.model_presets[name] + @property def workspace_path(self) -> Path: """Get expanded workspace path.""" diff --git a/tests/agent/test_self_model_preset.py b/tests/agent/test_self_model_preset.py index cbde23672..a996d75f2 100644 --- a/tests/agent/test_self_model_preset.py +++ b/tests/agent/test_self_model_preset.py @@ -284,7 +284,7 @@ def test_from_config_injects_default_preset(tmp_path) -> None: assert loop.model_presets["default"].model == "openai/gpt-4.1" -def test_from_config_preserves_existing_default_preset(tmp_path) -> None: +def test_from_config_reserves_default_for_agent_defaults(tmp_path) -> None: from unittest.mock import patch from nanobot.config.schema import Config @@ -297,4 +297,4 @@ def test_from_config_preserves_existing_default_preset(tmp_path) -> None: fake_provider = _provider("openai/gpt-4.1") with patch("nanobot.providers.factory.make_provider", return_value=fake_provider): loop = AgentLoop.from_config(config) - assert loop.model_presets["default"].model == "custom-model" + assert loop.model_presets["default"].model == "openai/gpt-4.1" diff --git a/tests/config/test_model_presets.py b/tests/config/test_model_presets.py index b243d6e27..171f9834e 100644 --- a/tests/config/test_model_presets.py +++ b/tests/config/test_model_presets.py @@ -39,6 +39,24 @@ def test_resolve_preset_returns_active_preset() -> None: assert resolved.reasoning_effort == "low" +def test_default_preset_is_agents_defaults_even_when_named_preset_is_active() -> None: + config = Config.model_validate({ + "agents": { + "defaults": { + "model": "openai/gpt-4.1", + "provider": "openai", + "modelPreset": "fast", + } + }, + "modelPresets": { + "fast": {"model": "openai/gpt-4.1-mini", "provider": "openai"}, + }, + }) + + assert config.resolve_preset().model == "openai/gpt-4.1-mini" + assert config.resolve_preset("default").model == "openai/gpt-4.1" + + def test_model_presets_accepts_camel_case_root_key() -> None: config = Config.model_validate({ "modelPresets": { @@ -79,6 +97,19 @@ def test_validator_rejects_unknown_preset() -> None: }) +def test_model_preset_accepts_explicit_default_name() -> None: + config = Config.model_validate({ + "agents": { + "defaults": { + "model": "openai/gpt-4.1", + "modelPreset": "default", + } + } + }) + + assert config.resolve_preset().model == "openai/gpt-4.1" + + def test_resolve_preset_rejects_unknown_named_preset() -> None: import pytest with pytest.raises(KeyError, match="model_preset 'missing' not found"):