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:
liuhao1024
2026-06-07 07:42:09 +08:00
committed by Teknium
parent 309b764a15
commit 39d46d944e
2 changed files with 78 additions and 0 deletions

View File

@@ -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

View File

@@ -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"