fix: coerce dict-valued model/default config back to string across resolution paths

A dict-valued model.default (e.g. {provider:..., model:...}) in config.yaml
was leaking into agent.model and crashing the agent at init:

  AttributeError: 'dict' object has no attribute 'lower'
    agent/agent_runtime_helpers.py: anthropic_prompt_cache_policy

This manifested on the Telegram gateway as an infinite reset loop: every
turn built an agent with model=dict, crashed during init, the gateway
treated the failed turn as a session needing reset, and /reset rebuilt the
agent and crashed again.

Coerce dict -> string at every model-resolution entry point so the value
is normalized once and never reaches a .lower() call as a dict:
- agent/agent_runtime_helpers.py: anthropic_prompt_cache_policy (the crash site)
- agent/agent_init.py: configured default model resolution
- cli.py: CLI config model + _normalize_model_for_provider
- hermes_cli/main.py: _has_any_provider_configured
- hermes_cli/oneshot.py: _run_agent model resolution
- hermes_cli/runtime_provider.py: _get_model_config default handling
- model_tools.py: _resolve_active_context_length
This commit is contained in:
Ario Bagus Prakusa
2026-08-11 20:01:38 +07:00
committed by kshitij
parent aa5a960675
commit cb4daf23f3
7 changed files with 48 additions and 14 deletions
+4 -1
View File
@@ -2272,7 +2272,10 @@ def init_agent(
# overrides consistent with them and let provider metadata resolve the
# active model's window instead.
if _config_context_length is not None and isinstance(_model_cfg, dict):
_configured_default_model = str(_model_cfg.get("default") or "").strip()
_default = _model_cfg.get("default")
if isinstance(_default, dict):
_default = str(_default.get("model") or _default.get("default") or "")
_configured_default_model = str(_default or "").strip()
_configured_default_runtime_model = _configured_default_model
_active_runtime_model = agent.model
if _configured_default_model:
+3
View File
@@ -2235,6 +2235,9 @@ def anthropic_prompt_cache_policy(
logger.debug("MoA aggregator cache-policy resolution failed: %s", _moa_exc)
return False, False
if isinstance(eff_model, dict):
eff_model = eff_model.get('default') or eff_model.get('model') or ''
eff_model = eff_model if isinstance(eff_model, str) else str(eff_model or '')
model_lower = eff_model.lower()
provider_lower = eff_provider.lower()
is_claude = "claude" in model_lower
+16 -9
View File
@@ -4565,7 +4565,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# authoritative. This avoids conflicts in multi-agent setups where
# env vars would stomp each other.
_model_config = CLI_CONFIG.get("model", {})
_config_model = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "")
_raw_default = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "")
if isinstance(_raw_default, dict):
_config_model = str(_raw_default.get("model") or _raw_default.get("default") or _raw_default.get("provider") or "")
else:
_config_model = str(_raw_default or "")
_DEFAULT_CONFIG_MODEL = ""
# Track whether the user passed -m / --model so resume knows not to
# clobber an explicit override with the session's stored model.
@@ -6659,7 +6663,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
def _normalize_model_for_provider(self, resolved_provider: str) -> bool:
"""Normalize provider-specific model IDs and routing."""
current_model = (self.model or "").strip()
current_model = str(self.model or "").strip()
if isinstance(self.model, dict):
_m_dict = self.model
current_model = str(_m_dict.get("model") or _m_dict.get("default") or _m_dict.get("provider") or "").strip()
changed = False
try:
@@ -8973,11 +8980,11 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
CLI_CONFIG["agent"].get("service_tier", "")
)
_model_config = CLI_CONFIG.get("model", {})
_config_model = (
(_model_config.get("default") or _model_config.get("model") or "")
if isinstance(_model_config, dict)
else (_model_config or "")
)
_raw_default2 = (_model_config.get("default") or _model_config.get("model") or "") if isinstance(_model_config, dict) else (_model_config or "")
if isinstance(_raw_default2, dict):
_config_model = str(_raw_default2.get("model") or _raw_default2.get("default") or _raw_default2.get("provider") or "")
else:
_config_model = str(_raw_default2 or "")
if _config_model and _config_model != getattr(self, "model", None):
_config_provider = (
_model_config.get("provider", "")
@@ -14794,8 +14801,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
from hermes_cli.config import load_config
_img_mode = decide_image_input_mode(
(self.provider or "").strip(),
(self.model or "").strip(),
str(self.provider or "").strip() if not isinstance(self.provider, dict) else str(self.provider.get("provider") or self.provider.get("default") or "").strip(),
str(self.model or "").strip() if not isinstance(self.model, dict) else str(self.model.get("model") or self.model.get("default") or "").strip(),
load_config(),
requested_provider=(self.requested_provider or "").strip(),
)
+6 -1
View File
@@ -973,7 +973,12 @@ def _has_any_provider_configured() -> bool:
cfg = load_config()
model_cfg = cfg.get("model")
if isinstance(model_cfg, dict):
_model_name = (model_cfg.get("default") or "").strip()
_default = model_cfg.get("default")
if isinstance(_default, dict):
_model_name = (_default.get("model") or _default.get("provider") or "")
else:
_model_name = (_default or "")
_model_name = (str(_model_name) if not isinstance(_model_name, str) else _model_name).strip()
elif isinstance(model_cfg, str):
_model_name = model_cfg.strip()
else:
+5 -1
View File
@@ -343,7 +343,11 @@ def _run_agent(
if isinstance(model_cfg, str):
cfg_model = model_cfg
else:
cfg_model = model_cfg.get("default") or model_cfg.get("model") or ""
_raw = model_cfg.get("default") or model_cfg.get("model") or ""
if isinstance(_raw, dict):
cfg_model = str(_raw.get("model") or _raw.get("default") or _raw.get("provider") or "")
else:
cfg_model = str(_raw or "")
env_model = os.getenv("HERMES_INFERENCE_MODEL", "").strip()
effective_model = (model or "").strip() or env_model or cfg_model
+10 -1
View File
@@ -322,7 +322,16 @@ def _get_model_config() -> Dict[str, Any]:
# Accept "model" as alias for "default" (users intuitively write model.model)
if not cfg.get("default") and cfg.get("model"):
cfg["default"] = cfg["model"]
default = (cfg.get("default") or "").strip()
# Handle model.default being a dict {provider: ..., model: ...} rather than a string
_default = cfg.get("default")
if isinstance(_default, dict):
cfg_provider = str(_default.get("provider") or model_cfg.get("provider") or "")
cfg_model = str(_default.get("model") or _default.get("default") or "")
cfg["default"] = cfg_model
if cfg_provider and not cfg.get("provider"):
cfg["provider"] = cfg_provider
_default = cfg_model
default = (str(_default or "")).strip()
base_url = (cfg.get("base_url") or "").strip()
is_local = base_url_hostname(base_url) in ("localhost", "127.0.0.1")
is_fallback = not default
+4 -1
View File
@@ -664,7 +664,10 @@ def _resolve_active_context_length() -> int:
model_cfg = cfg.get("model") if isinstance(cfg.get("model"), dict) else {}
if not isinstance(model_cfg, dict):
model_cfg = {}
model_id = (model_cfg.get("model") or model_cfg.get("default") or "").strip()
_raw_model_id = model_cfg.get("model") or model_cfg.get("default") or ""
if isinstance(_raw_model_id, dict):
_raw_model_id = _raw_model_id.get("model") or _raw_model_id.get("provider") or ""
model_id = str(_raw_model_id).strip()
if not model_id:
return 0
from agent.model_metadata import get_model_context_length