mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-21 10:22:18 +00:00
fix(model): clear stale endpoint credentials across switches
This commit is contained in:
parent
95a3affc2e
commit
c253b07380
9 changed files with 187 additions and 17 deletions
|
|
@ -378,6 +378,99 @@ def test_model_flow_nous_does_not_restore_stale_custom_api_key(tmp_path, monkeyp
|
|||
assert "api_mode" not in model
|
||||
|
||||
|
||||
def _seed_stale_custom_model(tmp_path, monkeypatch):
|
||||
import yaml
|
||||
|
||||
config_home = tmp_path / "hermes"
|
||||
config_home.mkdir()
|
||||
monkeypatch.setenv("HERMES_HOME", str(config_home))
|
||||
config_path = config_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"model": {
|
||||
"provider": "custom",
|
||||
"default": "glm-5.2",
|
||||
"base_url": "https://api.neuralwatt.com/v1",
|
||||
"api_key": "${NEURALWATT_API_KEY}",
|
||||
"api": "legacy-stale-key",
|
||||
"api_mode": "anthropic_messages",
|
||||
}
|
||||
},
|
||||
sort_keys=False,
|
||||
)
|
||||
)
|
||||
(config_home / ".env").write_text("")
|
||||
return config_path
|
||||
|
||||
|
||||
def test_model_flow_openrouter_clears_stale_custom_key(tmp_path, monkeypatch):
|
||||
import yaml
|
||||
|
||||
config_path = _seed_stale_custom_model(tmp_path, monkeypatch)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.main._prompt_api_key",
|
||||
lambda *args, **kwargs: ("sk-openrouter", False),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.models.model_ids",
|
||||
lambda **kwargs: ["anthropic/claude-sonnet-4.6"],
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.models.get_pricing_for_provider", lambda *a, **k: {})
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._prompt_model_selection",
|
||||
lambda *args, **kwargs: "anthropic/claude-sonnet-4.6",
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.deactivate_provider", lambda: None)
|
||||
|
||||
hermes_main._model_flow_openrouter({}, current_model="glm-5.2")
|
||||
|
||||
config = yaml.safe_load(config_path.read_text()) or {}
|
||||
model = config["model"]
|
||||
assert model["provider"] == "openrouter"
|
||||
assert model["default"] == "anthropic/claude-sonnet-4.6"
|
||||
assert model["api_mode"] == "chat_completions"
|
||||
assert "api_key" not in model
|
||||
assert "api" not in model
|
||||
|
||||
|
||||
def test_model_flow_anthropic_clears_stale_custom_key_and_mode(tmp_path, monkeypatch):
|
||||
import yaml
|
||||
|
||||
config_path = _seed_stale_custom_model(tmp_path, monkeypatch)
|
||||
|
||||
monkeypatch.setattr("hermes_cli.auth.get_anthropic_key", lambda: "sk-ant-api03-test")
|
||||
monkeypatch.setattr(
|
||||
"agent.anthropic_adapter.read_claude_code_credentials",
|
||||
lambda: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"agent.anthropic_adapter.is_claude_code_token_valid",
|
||||
lambda creds: False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.model_setup_flows._prompt_auth_credentials_choice",
|
||||
lambda title: "use",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._prompt_model_selection",
|
||||
lambda *args, **kwargs: "claude-sonnet-4-6",
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth.deactivate_provider", lambda: None)
|
||||
|
||||
hermes_main._model_flow_anthropic({}, current_model="glm-5.2")
|
||||
|
||||
config = yaml.safe_load(config_path.read_text()) or {}
|
||||
model = config["model"]
|
||||
assert model["provider"] == "anthropic"
|
||||
assert model["default"] == "claude-sonnet-4-6"
|
||||
assert "base_url" not in model
|
||||
assert "api_key" not in model
|
||||
assert "api" not in model
|
||||
assert "api_mode" not in model
|
||||
|
||||
|
||||
def test_model_flow_nous_offers_tool_gateway_prompt_when_unconfigured(monkeypatch, capsys):
|
||||
from hermes_cli.nous_account import NousPortalAccountInfo
|
||||
|
||||
|
|
|
|||
|
|
@ -140,7 +140,13 @@ async def _drive_picker(runner, event):
|
|||
"seed_model",
|
||||
[
|
||||
# Already-nested dict (common case).
|
||||
{"default": "old-model", "provider": "openai-codex"},
|
||||
{
|
||||
"default": "old-model",
|
||||
"provider": "custom",
|
||||
"base_url": "https://api.custom.example/v1",
|
||||
"api_key": "sk-stale",
|
||||
"api_mode": "anthropic_messages",
|
||||
},
|
||||
# Flat-string model: must be coerced to a nested dict on a tap (same
|
||||
# scalar-``model:`` guard the text path has) instead of raising
|
||||
# ``TypeError`` on assignment.
|
||||
|
|
@ -166,6 +172,8 @@ async def test_picker_tap_persists_by_default(tmp_path, monkeypatch, seed_model)
|
|||
assert written["model"]["default"] == "gpt-5.5"
|
||||
assert written["model"]["provider"] == "openrouter"
|
||||
assert written["model"]["base_url"] == "https://openrouter.ai/api/v1"
|
||||
assert "api_key" not in written["model"]
|
||||
assert "api_mode" not in written["model"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from __future__ import annotations
|
|||
import yaml
|
||||
|
||||
from hermes_cli.auth import _update_config_for_provider
|
||||
from hermes_cli.config import get_config_path
|
||||
from hermes_cli.config import clear_model_endpoint_credentials, get_config_path
|
||||
|
||||
|
||||
def _read_model_cfg() -> dict:
|
||||
|
|
@ -49,6 +49,23 @@ def _seed_custom_provider_config(api_mode: str = "anthropic_messages") -> None:
|
|||
|
||||
|
||||
class TestUpdateConfigForProviderClearsStaleCustomFields:
|
||||
def test_clear_model_endpoint_credentials_removes_key_alias_and_mode(self):
|
||||
model_cfg = {
|
||||
"provider": "openrouter",
|
||||
"default": "anthropic/claude-sonnet-4.6",
|
||||
"api_key": "sk-stale",
|
||||
"api": "sk-legacy-stale",
|
||||
"api_mode": "anthropic_messages",
|
||||
}
|
||||
|
||||
returned = clear_model_endpoint_credentials(model_cfg)
|
||||
|
||||
assert returned is model_cfg
|
||||
assert "api_key" not in model_cfg
|
||||
assert "api" not in model_cfg
|
||||
assert "api_mode" not in model_cfg
|
||||
assert model_cfg["provider"] == "openrouter"
|
||||
|
||||
def test_switching_to_openrouter_clears_api_key_and_api_mode(self):
|
||||
_seed_custom_provider_config()
|
||||
|
||||
|
|
|
|||
|
|
@ -2327,9 +2327,10 @@ class TestWebServerEndpoints:
|
|||
# api_key follows the same lifecycle as base_url:
|
||||
# supplied → persisted.
|
||||
out = _apply_main_model_assignment(
|
||||
{}, "custom", "m", "http://x/v1", "sk-secret"
|
||||
{"api": "sk-legacy-old"}, "custom", "m", "http://x/v1", "sk-secret"
|
||||
)
|
||||
assert out["api_key"] == "sk-secret"
|
||||
assert "api" not in out
|
||||
|
||||
# same provider, no new key → existing key preserved (re-picking a model
|
||||
# on the same custom endpoint must not wipe the saved key).
|
||||
|
|
@ -2342,9 +2343,12 @@ class TestWebServerEndpoints:
|
|||
|
||||
# switching providers without a new key → stale key cleared.
|
||||
out = _apply_main_model_assignment(
|
||||
{"provider": "custom", "api_key": "sk-old"}, "openrouter", "m"
|
||||
{"provider": "custom", "api_key": "sk-old", "api_mode": "anthropic_messages"},
|
||||
"openrouter",
|
||||
"m",
|
||||
)
|
||||
assert out["api_key"] == ""
|
||||
assert "api_key" not in out
|
||||
assert "api_mode" not in out
|
||||
|
||||
def test_parse_model_ids_handles_openai_and_bare_shapes(self):
|
||||
"""Model discovery must tolerate the common /v1/models shapes and
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue