diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index da4aab108d..cff75aff13 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -1167,7 +1167,8 @@ def _resolve_named_custom_runtime( if pool_result: # Propagate the model name even when using pooled credentials — # the pool doesn't know about the custom_providers model field. - model_name = custom_provider.get("model") + # An explicit ``target_model`` wins (same rule as the non-pool path). + model_name = target_model or custom_provider.get("model") if model_name: pool_result["model"] = model_name if isinstance(custom_provider.get("max_output_tokens"), int): @@ -1228,7 +1229,13 @@ def _resolve_named_custom_runtime( } # Propagate the model name so callers can override self.model when the # provider name differs from the actual model string the API expects. - if custom_provider.get("model"): + # An explicit ``target_model`` wins over the provider's configured + # default (regression: auxiliary slots / background-review resolve a + # concrete model for a custom provider and must not silently fall back + # to ``default_model``). + if target_model: + result["model"] = target_model + elif custom_provider.get("model"): result["model"] = custom_provider["model"] if isinstance(custom_provider.get("max_output_tokens"), int): result["max_output_tokens"] = custom_provider["max_output_tokens"] diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index fc42da1c01..acb97724e2 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -1677,3 +1677,73 @@ def test_resolve_runtime_provider_opencode_free_missing_env_still_resolves(monke assert resolved["provider"] == "opencode-free" assert resolved["api_key"] == "opencode-zen-free-keyless" assert resolved["base_url"] == "https://opencode.ai/zen/v1" + + +def test_custom_provider_explicit_target_model_wins(monkeypatch): + """An explicit target_model must not be silently replaced by the custom + provider's configured default model (regression: auxiliary slots such as + background-review resolve a concrete model and got default_model instead).""" + monkeypatch.setattr( + rp, + "_get_named_custom_provider", + lambda p: { + "name": "myproxy", + "base_url": "http://127.0.0.1:10100/v1", + "api_key": "no-key-required", + "model": "default-model", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="myproxy", target_model="myproxy/gemini-flash") + + assert resolved is not None + assert resolved["provider"] == "custom" + assert resolved["model"] == "myproxy/gemini-flash" + assert resolved["base_url"] == "http://127.0.0.1:10100/v1" + + +def test_custom_provider_without_target_model_keeps_default(monkeypatch): + """No target_model -> the provider's configured model is preserved.""" + monkeypatch.setattr( + rp, + "_get_named_custom_provider", + lambda p: { + "name": "myproxy", + "base_url": "http://127.0.0.1:10100/v1", + "api_key": "no-key-required", + "model": "default-model", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="myproxy") + + assert resolved is not None + assert resolved["model"] == "default-model" + + +def test_custom_provider_pool_target_model_wins(monkeypatch): + """Pooled-credentials path also honors target_model over the default.""" + monkeypatch.setattr( + rp, + "_try_resolve_from_custom_pool", + lambda *a, **k: { + "provider": "custom", + "api_key": "pooled-key", + "base_url": "http://127.0.0.1:10100/v1", + }, + ) + monkeypatch.setattr( + rp, + "_get_named_custom_provider", + lambda p: { + "name": "myproxy", + "base_url": "http://127.0.0.1:10100/v1", + "api_key": "no-key-required", + "model": "default-model", + }, + ) + + resolved = rp.resolve_runtime_provider(requested="myproxy", target_model="myproxy/gemini-flash") + + assert resolved is not None + assert resolved["model"] == "myproxy/gemini-flash"