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:
@@ -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", "")
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user