fix: keep nested model.default provider paired through HermesCLI

The prior dict coercion converted a dict-valued model.default to a plain
model string but dropped the nested provider. On the interactive CLI path
requested_provider then fell back to the outer merged model.provider
(typically "auto", authoritative at runtime resolution), so the model
could be routed through the wrong active provider.

Canonicalize both halves at the shared boundary: _split_model_config_default
flattens a dict-valued default into (model, provider) and HermesCLI.__init__
feeds the nested provider into the requested_provider chain (still below an
explicit --provider argument). new_session reuses the same helper.

Adds regression coverage asserting the nested provider stays paired with the
model, that flat string defaults keep the outer provider behavior, and that
an explicit provider argument still wins.
This commit is contained in:
mariobgsp
2026-08-12 00:30:25 +07:00
committed by kshitij
parent cb4daf23f3
commit 86054eff62
2 changed files with 86 additions and 8 deletions
+28 -8
View File
@@ -4381,6 +4381,26 @@ def _normalize_moa_model(model: Optional[str]) -> tuple[Optional[str], Optional[
return "moa", preset
return None, model
def _split_model_config_default(raw_default: Any) -> tuple[str, str]:
"""Canonicalize a config ``model.default``/``model.model`` value.
A dict-valued default (``model.default: {provider: ..., model: ...}``)
pairs the model string with the provider it must be routed through. The
dict is flattened here at the shared boundary so both halves stay
together through ``HermesCLI`` construction: the model becomes a plain
string and the provider is returned explicitly instead of being lost to
the outer merged ``model.provider`` default (often ``"auto"``, which
runtime resolution treats as authoritative and would otherwise route the
model through the wrong active provider).
Returns ``(model, provider)``; both are ``""`` when nothing is usable.
"""
if isinstance(raw_default, dict):
provider = str(raw_default.get("provider") or "").strip()
model = raw_default.get("model") or raw_default.get("default")
return (str(model or "").strip(), provider)
return (str(raw_default or "").strip(), "")
class _VoiceInputMessage:
"""Sentinel wrapper for voice-transcribed messages in ``_pending_input``.
@@ -4566,10 +4586,12 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# env vars would stomp each other.
_model_config = CLI_CONFIG.get("model", {})
_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 "")
# A dict-valued default (``model.default: {provider: ..., model: ...}``)
# carries its own provider; flatten it here so the nested provider is
# available when ``requested_provider`` is constructed below instead of
# being discarded and replaced by the outer merged ``model.provider``
# (typically ``"auto"``, which is authoritative at runtime resolution).
_config_model, _nested_provider = _split_model_config_default(_raw_default)
_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.
@@ -4618,6 +4640,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self.requested_provider = (
_moa_provider_override
or provider
or _nested_provider
or CLI_CONFIG["model"].get("provider")
or os.getenv("HERMES_INFERENCE_PROVIDER")
or "auto"
@@ -8981,10 +9004,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
)
_model_config = CLI_CONFIG.get("model", {})
_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 "")
_config_model, _ = _split_model_config_default(_raw_default2)
if _config_model and _config_model != getattr(self, "model", None):
_config_provider = (
_model_config.get("provider", "")
+58
View File
@@ -372,6 +372,64 @@ class TestHistoryDisplay:
)
class TestNestedDictModelDefaultPairing:
"""A dict-valued ``model.default`` must keep its nested provider paired.
``model.default: {provider: ..., model: ...}`` canonicalizes to the string
model AND the nested provider, so ``HermesCLI`` routes the model through
that provider instead of discarding it and falling back to the outer
merged ``model.provider`` (``"auto"`` — authoritative at runtime
resolution, which would route the model through the wrong active
provider).
"""
def test_nested_dict_default_keeps_provider_paired(self):
cli = _make_cli(config_overrides={
"model": {
"default": {"provider": "nous", "model": "nested-default-model"},
"provider": "auto",
},
})
assert cli.model == "nested-default-model"
assert cli.requested_provider == "nous"
assert cli.provider == "nous"
def test_nested_dict_model_alias_keeps_provider_paired(self):
cli = _make_cli(config_overrides={
"model": {
"model": {"provider": "openai", "model": "nested-alias-model"},
"provider": "auto",
},
})
assert cli.model == "nested-alias-model"
assert cli.requested_provider == "openai"
assert cli.provider == "openai"
def test_flat_string_default_still_uses_outer_provider(self):
cli = _make_cli(config_overrides={
"model": {
"default": "flat-default-model",
"provider": "auto",
},
})
assert cli.model == "flat-default-model"
assert cli.requested_provider == "auto"
assert cli.provider == "auto"
def test_nested_provider_does_not_override_explicit_provider_arg(self):
cli = _make_cli(
config_overrides={
"model": {
"default": {"provider": "nous", "model": "nested-default-model"},
"provider": "auto",
},
},
provider="anthropic",
)
assert cli.model == "nested-default-model"
assert cli.requested_provider == "anthropic"
assert cli.provider == "anthropic"
class TestRootLevelProviderOverride:
"""Root-level provider/base_url in config.yaml must NOT override model.provider."""