fix(config): clear stale model.base_url when model.provider changes via config set
When a user runs `hermes config set model.provider <new>` to switch providers, the old provider's `model.base_url` is left behind in config.yaml. This causes API calls to go to the wrong endpoint. The wizard flow (`_update_config_for_provider`) already handles this correctly by clearing stale base_url on provider switch, but the `hermes config set` path did not. Now `set_config_value` detects when `model.provider` is being changed and removes the stale `model.base_url`, allowing runtime auto-detection to resolve the correct endpoint for the new provider. Fixes #40862
This commit is contained in:
@@ -3705,6 +3705,19 @@ def set_config_value(key: str, value: str, force: bool = False):
|
||||
_set_nested(user_config, key, value)
|
||||
except ValueError as e:
|
||||
_exit_invalid(f"✗ {e}")
|
||||
|
||||
# When switching model.provider, clear stale model.base_url so the
|
||||
# runtime auto-detection resolves the correct endpoint for the new
|
||||
# provider. Without this, ``hermes config set model.provider deepseek``
|
||||
# after a wizard run that set base_url to xAI's endpoint leaves config
|
||||
# in an inconsistent state (provider=deepseek, base_url=https://api.x.ai/v1).
|
||||
# See: https://github.com/NousResearch/hermes-agent/issues/40862
|
||||
if key == "model.provider":
|
||||
model_section = user_config.get("model")
|
||||
if isinstance(model_section, dict) and "base_url" in model_section:
|
||||
removed_url = model_section.pop("base_url")
|
||||
print(f" Cleared stale model.base_url ({removed_url}) — "
|
||||
"the new provider's endpoint will be resolved automatically.")
|
||||
# api_base -> base_url alias at set-time too (mirrors _normalize_root_model_keys).
|
||||
if key.strip().lower() in ("model.api_base", "api_base"):
|
||||
# Normalize the api_base → base_url alias at set-time too (issue #8919), so a fresh `hermes config
|
||||
|
||||
@@ -1100,3 +1100,68 @@ class TestContainerTypeRefusal:
|
||||
import yaml as _yaml
|
||||
saved = _yaml.safe_load(_read_config(_isolated_hermes_home))
|
||||
assert saved["agent"]["disabled_toolsets"] == ["web"]
|
||||
|
||||
|
||||
class TestProviderSwitchClearsBaseUrl:
|
||||
"""When model.provider changes via config set, stale model.base_url
|
||||
must be cleared so runtime auto-detection resolves the correct endpoint.
|
||||
See: https://github.com/NousResearch/hermes-agent/issues/40862
|
||||
"""
|
||||
|
||||
def _write_config(self, tmp_path, body):
|
||||
(tmp_path / "config.yaml").write_text(body)
|
||||
|
||||
def test_switching_provider_clears_base_url(self, _isolated_hermes_home, capsys):
|
||||
"""Switching from xai-oauth to deepseek should remove base_url."""
|
||||
self._write_config(_isolated_hermes_home, (
|
||||
"model:\n"
|
||||
" default: grok-4.3\n"
|
||||
" provider: xai-oauth\n"
|
||||
" base_url: https://api.x.ai/v1\n"
|
||||
))
|
||||
|
||||
set_config_value("model.provider", "deepseek")
|
||||
|
||||
import yaml
|
||||
reloaded = yaml.safe_load(_read_config(_isolated_hermes_home))
|
||||
assert reloaded["model"]["provider"] == "deepseek"
|
||||
assert "base_url" not in reloaded["model"]
|
||||
# User gets a message about the cleared URL
|
||||
captured = capsys.readouterr()
|
||||
assert "Cleared stale model.base_url" in captured.out
|
||||
assert "api.x.ai" in captured.out
|
||||
|
||||
def test_switching_provider_no_base_url_is_noop(self, _isolated_hermes_home, capsys):
|
||||
"""If there's no base_url to clear, the switch still works silently."""
|
||||
self._write_config(_isolated_hermes_home, (
|
||||
"model:\n"
|
||||
" default: gpt-4o\n"
|
||||
" provider: openai\n"
|
||||
))
|
||||
|
||||
set_config_value("model.provider", "anthropic")
|
||||
|
||||
import yaml
|
||||
reloaded = yaml.safe_load(_read_config(_isolated_hermes_home))
|
||||
assert reloaded["model"]["provider"] == "anthropic"
|
||||
assert "base_url" not in reloaded["model"]
|
||||
captured = capsys.readouterr()
|
||||
assert "Cleared stale" not in captured.out
|
||||
|
||||
def test_non_provider_key_preserves_base_url(self, _isolated_hermes_home):
|
||||
"""Changing model.default should NOT touch base_url."""
|
||||
self._write_config(_isolated_hermes_home, (
|
||||
"model:\n"
|
||||
" default: grok-4.3\n"
|
||||
" provider: xai-oauth\n"
|
||||
" base_url: https://api.x.ai/v1\n"
|
||||
))
|
||||
|
||||
set_config_value("model.default", "deepseek-v4-pro")
|
||||
|
||||
import yaml
|
||||
reloaded = yaml.safe_load(_read_config(_isolated_hermes_home))
|
||||
assert reloaded["model"]["default"] == "deepseek-v4-pro"
|
||||
# base_url must be preserved — only model.default changed
|
||||
assert reloaded["model"]["base_url"] == "https://api.x.ai/v1"
|
||||
assert reloaded["model"]["provider"] == "xai-oauth"
|
||||
|
||||
Reference in New Issue
Block a user