diff --git a/src/config.py b/src/config.py index 1703b5cd..88c04673 100644 --- a/src/config.py +++ b/src/config.py @@ -926,6 +926,10 @@ class DialecticSettings(HonchoSettings): del base_mc[k] level_override[mc_key] = {**base_mc, **override_mc} levels_raw[level_name] = {**base, **level_override} + # Backfill any reasoning levels the operator didn't explicitly set with the default values. + for default_level_name, default_level in defaults.items(): + if default_level_name not in levels_raw: + levels_raw[default_level_name] = default_level.model_dump(by_alias=True) return data # pyright: ignore[reportUnknownVariableType] @model_validator(mode="after") diff --git a/tests/llm/test_model_config.py b/tests/llm/test_model_config.py index 0f0664ff..4ed1ca06 100644 --- a/tests/llm/test_model_config.py +++ b/tests/llm/test_model_config.py @@ -515,3 +515,50 @@ def test_dialectic_level_transport_override_drops_default_thinking_params( assert minimal_mc["model"] == "gpt-4.1-mini" assert "thinking_budget_tokens" not in minimal_mc assert "thinking_effort" not in minimal_mc + + +def test_dialectic_settings_backfills_missing_levels( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Operators only need to override the levels they care about. + + Env-var overrides replace the LEVELS dict wholesale (bypassing the + default_factory), so without a backfill the unmentioned levels would be + dropped and _validate_all_levels_present would fail. + """ + from src.config import ( + DialecticSettings, + _default_dialectic_levels, # pyright: ignore[reportPrivateUsage] + ) + + for key in list(os.environ): + if key.startswith("DIALECTIC_LEVELS"): + monkeypatch.delenv(key) + + settings = DialecticSettings( + LEVELS={ # pyright: ignore[reportArgumentType] + "low": { + "MODEL_CONFIG": { + "transport": "anthropic", + "model": "claude-haiku-4-5-20251001", + "thinking_budget_tokens": 1024, + }, + "MAX_OUTPUT_TOKENS": 2500, + } + } + ) + + assert set(settings.LEVELS.keys()) == {"minimal", "low", "medium", "high", "max"} + assert settings.LEVELS["low"].MODEL_CONFIG.transport == "anthropic" + assert settings.LEVELS["low"].MODEL_CONFIG.model == "claude-haiku-4-5-20251001" + assert settings.LEVELS["low"].MAX_OUTPUT_TOKENS == 2500 + # Backfilled levels come from _default_dialectic_levels() + defaults = _default_dialectic_levels() + assert ( + settings.LEVELS["minimal"].MAX_TOOL_ITERATIONS + == defaults["minimal"].MAX_TOOL_ITERATIONS + ) + assert ( + settings.LEVELS["max"].MAX_TOOL_ITERATIONS + == defaults["max"].MAX_TOOL_ITERATIONS + )