From 8166eb6214ea46b06ac9cb26337a329bfcb34a22 Mon Sep 17 00:00:00 2001 From: Benjamin McCormick Date: Thu, 5 Feb 2026 11:29:23 -0500 Subject: [PATCH] fix: allow partial dialectic reasoning level configs, merge in defaults --- src/config.py | 107 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/src/config.py b/src/config.py index 369b7ddc..ed83ca6c 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Annotated, Any, ClassVar, Literal, Protocol +from typing import Annotated, Any, ClassVar, Literal, Protocol, cast import tomllib from dotenv import load_dotenv @@ -356,43 +356,49 @@ class DialecticSettings(HonchoSettings): env_prefix="DIALECTIC_", env_nested_delimiter="__", extra="ignore" ) + # Default level configurations - defined as class var so we can merge partial overrides + _DEFAULT_LEVELS: ClassVar[dict[ReasoningLevel, dict[str, Any]]] = { + "minimal": { + "PROVIDER": "google", + "MODEL": "gemini-2.5-flash-lite", + "THINKING_BUDGET_TOKENS": 0, + "MAX_TOOL_ITERATIONS": 1, + "MAX_OUTPUT_TOKENS": 250, + "TOOL_CHOICE": "any", + }, + "low": { + "PROVIDER": "google", + "MODEL": "gemini-2.5-flash-lite", + "THINKING_BUDGET_TOKENS": 0, + "MAX_TOOL_ITERATIONS": 5, + "TOOL_CHOICE": "any", + }, + "medium": { + "PROVIDER": "anthropic", + "MODEL": "claude-haiku-4-5", + "THINKING_BUDGET_TOKENS": 1024, + "MAX_TOOL_ITERATIONS": 2, + }, + "high": { + "PROVIDER": "anthropic", + "MODEL": "claude-haiku-4-5", + "THINKING_BUDGET_TOKENS": 1024, + "MAX_TOOL_ITERATIONS": 4, + }, + "max": { + "PROVIDER": "anthropic", + "MODEL": "claude-haiku-4-5", + "THINKING_BUDGET_TOKENS": 2048, + "MAX_TOOL_ITERATIONS": 10, + }, + } + # Per-level settings for provider, model, thinking budget, and tool iterations - # TODO: Fill in appropriate values for each reasoning level + # Defaults defined in _DEFAULT_LEVELS class var above LEVELS: dict[ReasoningLevel, DialecticLevelSettings] = Field( default_factory=lambda: { - "minimal": DialecticLevelSettings( - PROVIDER="google", - MODEL="gemini-2.5-flash-lite", - THINKING_BUDGET_TOKENS=0, - MAX_TOOL_ITERATIONS=1, - MAX_OUTPUT_TOKENS=250, - TOOL_CHOICE="any", - ), - "low": DialecticLevelSettings( - PROVIDER="google", - MODEL="gemini-2.5-flash-lite", - THINKING_BUDGET_TOKENS=0, - MAX_TOOL_ITERATIONS=5, - TOOL_CHOICE="any", - ), - "medium": DialecticLevelSettings( - PROVIDER="anthropic", - MODEL="claude-haiku-4-5", - THINKING_BUDGET_TOKENS=1024, - MAX_TOOL_ITERATIONS=2, - ), - "high": DialecticLevelSettings( - PROVIDER="anthropic", - MODEL="claude-haiku-4-5", - THINKING_BUDGET_TOKENS=1024, - MAX_TOOL_ITERATIONS=4, - ), - "max": DialecticLevelSettings( - PROVIDER="anthropic", - MODEL="claude-haiku-4-5", - THINKING_BUDGET_TOKENS=2048, - MAX_TOOL_ITERATIONS=10, - ), + level: DialecticLevelSettings(**config) + for level, config in DialecticSettings._DEFAULT_LEVELS.items() } ) @@ -408,6 +414,37 @@ class DialecticSettings(HonchoSettings): int, Field(default=4_096, ge=0, le=16_384) ] = 4_096 + @model_validator(mode="before") + @classmethod + def _merge_levels_with_defaults(cls, data: Any) -> Any: + """Merge partial LEVELS config with defaults so users can override just one level.""" + if not isinstance(data, dict): + return data + + data_dict = cast(dict[str, Any], data) + levels: dict[str, Any] | None = data_dict.get("LEVELS") + if levels is None: + return data_dict + + # Build merged levels: start with defaults, overlay user config + merged: dict[str, Any] = {} + for level_name, default_config in cls._DEFAULT_LEVELS.items(): + user_config: dict[str, Any] | DialecticLevelSettings | None = levels.get( + level_name + ) + if user_config is None: + # Use default entirely + merged[level_name] = default_config.copy() + elif isinstance(user_config, dict): + # Merge user config over defaults + merged[level_name] = {**default_config, **user_config} + else: + # Already a DialecticLevelSettings - use as-is + merged[level_name] = user_config + + data_dict["LEVELS"] = merged + return data_dict + @model_validator(mode="after") def _validate_token_budgets(self) -> "DialecticSettings": """Ensure the output token limit exceeds all thinking budgets."""