Compare commits

...

1 Commits

Author SHA1 Message Date
helix4u
c20577b6c0 fix(zai): autodetect endpoint during setup flow 2026-04-19 20:13:59 +05:30
5 changed files with 450 additions and 102 deletions

View File

@@ -2516,28 +2516,18 @@ def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]:
if not pconfig or pconfig.auth_type != "api_key":
return {"configured": False}
api_key = ""
key_source = ""
api_key, key_source = _resolve_api_key_provider_secret(provider_id, pconfig)
env_url = ""
if pconfig.base_url_env_var:
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
if provider_id in ("kimi-coding", "kimi-coding-cn"):
base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url)
elif env_url:
base_url = env_url
else:
base_url = pconfig.inference_base_url
try:
creds = resolve_api_key_provider_credentials(provider_id)
except AuthError:
creds = {"api_key": "", "source": "", "base_url": pconfig.inference_base_url}
return {
"configured": bool(api_key),
"configured": bool(creds.get("api_key")),
"provider": provider_id,
"name": pconfig.name,
"key_source": key_source,
"base_url": base_url,
"logged_in": bool(api_key), # compat with OAuth status shape
"key_source": creds.get("source", ""),
"base_url": creds.get("base_url", pconfig.inference_base_url),
"logged_in": bool(creds.get("api_key")), # compat with OAuth status shape
}
@@ -2598,10 +2588,12 @@ def get_auth_status(provider_id: Optional[str] = None) -> Dict[str, Any]:
return {"logged_in": False}
def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
def resolve_api_key_provider_credentials(provider_id: str, ignore_env_base_url: bool = False) -> Dict[str, Any]:
"""Resolve API key and base URL for an API-key provider.
Returns dict with: provider, api_key, base_url, source.
When ``ignore_env_base_url`` is True, skip the provider's explicit base URL
env override and compute the auto/default endpoint only.
"""
pconfig = PROVIDER_REGISTRY.get(provider_id)
if not pconfig or pconfig.auth_type != "api_key":
@@ -2616,7 +2608,7 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]:
api_key, key_source = _resolve_api_key_provider_secret(provider_id, pconfig)
env_url = ""
if pconfig.base_url_env_var:
if pconfig.base_url_env_var and not ignore_env_base_url:
env_url = os.getenv(pconfig.base_url_env_var, "").strip()
if provider_id in ("kimi-coding", "kimi-coding-cn"):

View File

@@ -1525,11 +1525,12 @@ def select_provider_and_model(args=None):
_model_flow_kimi(config, current_model)
elif selected_provider == "bedrock":
_model_flow_bedrock(config, current_model)
elif selected_provider == "zai":
_model_flow_zai(config, current_model)
elif selected_provider in (
"gemini",
"deepseek",
"xai",
"zai",
"kimi-coding-cn",
"minimax",
"minimax-cn",
@@ -3314,15 +3315,16 @@ def _model_flow_kimi(config, current_model=""):
# Step 2: Auto-detect endpoint from key prefix
is_coding_plan = existing_key.startswith("sk-kimi-")
auto_base = KIMI_CODE_BASE_URL if is_coding_plan else pconfig.inference_base_url
if is_coding_plan:
effective_base = KIMI_CODE_BASE_URL
print(f" Detected Kimi Coding Plan key → {effective_base}")
print(f" Detected Kimi Coding Plan key → {auto_base}")
else:
effective_base = pconfig.inference_base_url
print(f" Using Moonshot endpoint → {effective_base}")
# Clear any manual base URL override so auto-detection works at runtime
if base_url_env and get_env_value(base_url_env):
save_env_value(base_url_env, "")
print(f" Using Moonshot endpoint → {auto_base}")
effective_base = _configure_base_url_override(
provider_name=pconfig.name,
base_url_env=base_url_env,
auto_base=auto_base,
)
print()
# Step 3: Model selection — show appropriate models for the endpoint
@@ -3638,6 +3640,66 @@ def _model_flow_bedrock(config, current_model=""):
print(" No change.")
def _configure_base_url_override(provider_name: str, base_url_env: str, auto_base: str) -> str:
"""Allow users to keep, clear, or replace a manual base URL override.
Shared setup UX for providers with OpenAI-compatible base URLs. ``auto_base``
is the endpoint Hermes would use with no explicit override.
"""
from hermes_cli.config import get_env_value, save_env_value
if not base_url_env:
return auto_base.rstrip("/")
current_override = (get_env_value(base_url_env) or os.getenv(base_url_env, "")).strip().rstrip("/")
auto_base = (auto_base or "").strip().rstrip("/")
if current_override:
print(f" Using explicit {base_url_env} override → {current_override}")
if auto_base:
print(f" Auto/default endpoint: {auto_base}")
try:
choice = input(f" {provider_name} base URL override: [K]eep/[C]lear/[E]dit (default: keep): ").strip().lower()
except (KeyboardInterrupt, EOFError):
print()
choice = ""
if choice in ("c", "clear"):
save_env_value(base_url_env, "")
return auto_base
if choice in ("e", "edit"):
try:
override = input(f" New base URL [{current_override}]: ").strip().rstrip("/")
except (KeyboardInterrupt, EOFError):
print()
return current_override
if not override:
return current_override
if not override.startswith(("http://", "https://")):
print(" Invalid URL — must start with http:// or https://. Keeping current override.")
return current_override
save_env_value(base_url_env, override)
return override
return current_override
try:
override = input(f"{provider_name} base URL override [{auto_base}] (press Enter to keep auto/default): ").strip().rstrip("/")
except (KeyboardInterrupt, EOFError):
print()
override = ""
if override:
if not override.startswith(("http://", "https://")):
print(" Invalid URL — must start with http:// or https://. Keeping auto/default endpoint.")
return auto_base
save_env_value(base_url_env, override)
return override
return auto_base
def _model_flow_api_key_provider(config, provider_id, current_model=""):
"""Generic flow for API-key providers (z.ai, MiniMax, OpenCode, etc.)."""
from hermes_cli.auth import (
@@ -3689,25 +3751,12 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
print(f" {pconfig.name} API key: {existing_key[:8]}... ✓")
print()
# Optional base URL override
current_base = ""
if base_url_env:
current_base = get_env_value(base_url_env) or os.getenv(base_url_env, "")
effective_base = current_base or pconfig.inference_base_url
try:
override = input(f"Base URL [{effective_base}]: ").strip()
except (KeyboardInterrupt, EOFError):
print()
override = ""
if override and base_url_env:
if not override.startswith(("http://", "https://")):
print(
" Invalid URL — must start with http:// or https://. Keeping current value."
)
else:
save_env_value(base_url_env, override)
effective_base = override
# Optional base URL override / recovery for stale manual overrides
effective_base = _configure_base_url_override(
provider_name=pconfig.name,
base_url_env=base_url_env,
auto_base=pconfig.inference_base_url,
)
# Model selection — resolution order:
# 1. models.dev registry (cached, filtered for agentic/tool-capable models)
@@ -3802,6 +3851,117 @@ def _model_flow_api_key_provider(config, provider_id, current_model=""):
print("No change.")
def _model_flow_zai(config, current_model=""):
"""Z.AI / GLM model selection with auth-path endpoint autodetection.
Unlike generic API-key providers, Z.AI has multiple billing endpoints.
Reuse resolve_api_key_provider_credentials("zai") here so the interactive
setup flow stays aligned with the auth/runtime resolution path instead of
asking users to guess which base URL they need.
"""
from hermes_cli.auth import (
PROVIDER_REGISTRY, _prompt_model_selection, _save_model_choice,
deactivate_provider, resolve_api_key_provider_credentials,
)
from hermes_cli.config import get_env_value, save_env_value, load_config, save_config
from hermes_cli.models import fetch_api_models
del config
provider_id = "zai"
pconfig = PROVIDER_REGISTRY[provider_id]
key_env = pconfig.api_key_env_vars[0] if pconfig.api_key_env_vars else ""
base_url_env = pconfig.base_url_env_var or ""
existing_key = ""
for ev in pconfig.api_key_env_vars:
existing_key = get_env_value(ev) or os.getenv(ev, "")
if existing_key:
break
if not existing_key:
print(f"No {pconfig.name} API key configured.")
if key_env:
try:
import getpass
new_key = getpass.getpass(f"{key_env} (or Enter to cancel): ").strip()
except (KeyboardInterrupt, EOFError):
print()
return
if not new_key:
print("Cancelled.")
return
save_env_value(key_env, new_key)
existing_key = new_key
print("API key saved.")
print()
else:
print(f" {pconfig.name} API key: {existing_key[:8]}... ✓")
print()
creds = resolve_api_key_provider_credentials(provider_id, ignore_env_base_url=False)
auto_creds = resolve_api_key_provider_credentials(provider_id, ignore_env_base_url=True)
auto_base = auto_creds.get("base_url") or pconfig.inference_base_url
effective_base = _configure_base_url_override(
provider_name=pconfig.name,
base_url_env=base_url_env,
auto_base=auto_base,
)
if effective_base == auto_base:
print(f" Auto-detected Z.AI endpoint → {effective_base}")
print()
curated = _PROVIDER_MODELS.get(provider_id, [])
model_list = []
try:
from agent.models_dev import list_agentic_models
model_list = list_agentic_models(provider_id)
except Exception:
model_list = []
if model_list:
print(f" Found {len(model_list)} model(s) from models.dev registry")
elif curated and len(curated) >= 8:
model_list = curated
print(f" Showing {len(model_list)} curated models — use \"Enter custom model name\" for others.")
else:
live_models = fetch_api_models(existing_key, effective_base)
if live_models and len(live_models) >= len(curated):
model_list = live_models
print(f" Found {len(model_list)} model(s) from {pconfig.name} API")
else:
model_list = curated
if model_list:
print(f" Showing {len(model_list)} curated models — use \"Enter custom model name\" for others.")
if model_list:
selected = _prompt_model_selection(model_list, current_model=current_model)
else:
try:
selected = input("Model name: ").strip()
except (KeyboardInterrupt, EOFError):
selected = None
if selected:
_save_model_choice(selected)
cfg = load_config()
model = cfg.get("model")
if not isinstance(model, dict):
model = {"default": model} if model else {}
cfg["model"] = model
model["provider"] = provider_id
model["base_url"] = effective_base
model.pop("api_mode", None)
save_config(cfg)
deactivate_provider()
print(f"Default model set to: {selected} (via {pconfig.name})")
else:
print("No change.")
def _run_anthropic_oauth_flow(save_env_value):
"""Run the Claude OAuth setup-token flow. Returns True if credentials were saved."""
from agent.anthropic_adapter import (

View File

@@ -618,20 +618,26 @@ def _resolve_explicit_runtime(
if pconfig.base_url_env_var:
env_url = os.getenv(pconfig.base_url_env_var, "").strip().rstrip("/")
cfg_base_url = ""
if isinstance(model_cfg, dict):
cfg_provider = str(model_cfg.get("provider") or "").strip().lower()
if cfg_provider == provider:
cfg_base_url = str(model_cfg.get("base_url") or "").strip().rstrip("/")
creds = resolve_api_key_provider_credentials(provider)
base_url = explicit_base_url
if not base_url:
if provider in ("kimi-coding", "kimi-coding-cn"):
creds = resolve_api_key_provider_credentials(provider)
base_url = creds.get("base_url", "").rstrip("/")
if provider in ("kimi-coding", "kimi-coding-cn", "zai"):
base_url = cfg_base_url or creds.get("base_url", "").rstrip("/")
else:
base_url = env_url or pconfig.inference_base_url
base_url = cfg_base_url or env_url or pconfig.inference_base_url
api_key = explicit_api_key
if not api_key:
creds = resolve_api_key_provider_credentials(provider)
api_key = creds.get("api_key", "")
if not base_url:
base_url = creds.get("base_url", "").rstrip("/")
base_url = cfg_base_url or creds.get("base_url", "").rstrip("/")
api_mode = "chat_completions"
if provider == "copilot":

View File

@@ -1,6 +1,7 @@
"""Tests for API-key provider support (z.ai/GLM, Kimi, MiniMax, AI Gateway)."""
import os
from unittest.mock import patch
import pytest
@@ -295,6 +296,22 @@ class TestApiKeyProviderStatus:
assert status["key_source"] == "GLM_API_KEY"
assert "z.ai" in status["base_url"].lower() or "api.z.ai" in status["base_url"]
def test_zai_status_uses_resolved_endpoint(self, monkeypatch):
monkeypatch.setenv("GLM_API_KEY", "test-key")
monkeypatch.delenv("ZAI_API_KEY", raising=False)
monkeypatch.delenv("GLM_BASE_URL", raising=False)
with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
return_value={
"provider": "zai",
"api_key": "test-key",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"source": "GLM_API_KEY",
},
):
status = get_api_key_provider_status("zai")
assert status["base_url"] == "https://api.z.ai/api/coding/paas/v4"
def test_fallback_env_var(self, monkeypatch):
"""ZAI_API_KEY should work when GLM_API_KEY is not set."""
monkeypatch.setenv("ZAI_API_KEY", "zai-fallback-key")
@@ -507,6 +524,29 @@ class TestRuntimeProviderResolution:
assert result["api_key"] == "glm-key"
assert "z.ai" in result["base_url"] or "api.z.ai" in result["base_url"]
def test_runtime_zai_uses_config_base_url(self, monkeypatch):
monkeypatch.setenv("GLM_API_KEY", "glm-key")
monkeypatch.delenv("GLM_BASE_URL", raising=False)
from hermes_cli.runtime_provider import resolve_runtime_provider
with patch(
"hermes_cli.runtime_provider._get_model_config",
return_value={
"provider": "zai",
"default": "glm-5.1",
"base_url": "https://api.z.ai/api/coding/paas/v4",
},
), patch(
"hermes_cli.runtime_provider.resolve_api_key_provider_credentials",
return_value={
"provider": "zai",
"api_key": "glm-key",
"base_url": "https://api.z.ai/api/paas/v4",
"source": "GLM_API_KEY",
},
):
result = resolve_runtime_provider(requested="zai")
assert result["base_url"] == "https://api.z.ai/api/coding/paas/v4"
def test_runtime_kimi(self, monkeypatch):
monkeypatch.setenv("KIMI_API_KEY", "kimi-key")
from hermes_cli.runtime_provider import resolve_runtime_provider

View File

@@ -259,74 +259,224 @@ class TestProviderPersistsAfterModelSave:
assert model.get("api_mode") == "anthropic_messages"
class TestBaseUrlValidation:
"""Reject non-URL values in the base URL prompt (e.g. shell commands)."""
class TestBaseUrlOverrideManagement:
def test_configure_base_url_override_accepts_manual_override(self, config_home, monkeypatch):
from hermes_cli.main import _configure_base_url_override
from hermes_cli.config import get_env_value
def test_invalid_base_url_rejected(self, config_home, monkeypatch, capsys):
"""Typing a non-URL string should not be saved as the base URL."""
from hermes_cli.auth import PROVIDER_REGISTRY
monkeypatch.setattr("builtins.input", lambda prompt='': "https://custom.example/v4")
pconfig = PROVIDER_REGISTRY.get("zai")
if not pconfig:
pytest.skip("zai not in PROVIDER_REGISTRY")
effective = _configure_base_url_override(
provider_name="Example Provider",
base_url_env="EXAMPLE_BASE_URL",
auto_base="https://auto.example/v4",
)
assert effective == "https://custom.example/v4"
assert get_env_value("EXAMPLE_BASE_URL") == "https://custom.example/v4"
def test_configure_base_url_override_can_clear_existing_override(self, config_home, monkeypatch):
from hermes_cli.main import _configure_base_url_override
from hermes_cli.config import get_env_value, save_env_value
save_env_value("EXAMPLE_BASE_URL", "https://stale.example/v4")
monkeypatch.setattr("builtins.input", lambda prompt='': "c")
effective = _configure_base_url_override(
provider_name="Example Provider",
base_url_env="EXAMPLE_BASE_URL",
auto_base="https://auto.example/v4",
)
assert effective == "https://auto.example/v4"
assert get_env_value("EXAMPLE_BASE_URL") in ("", None)
class TestZaiSetupAutodetect:
def test_zai_setup_uses_resolved_endpoint_without_manual_override(self, config_home, monkeypatch):
monkeypatch.setenv("GLM_API_KEY", "test-key")
from hermes_cli.main import _model_flow_api_key_provider
from hermes_cli.main import _model_flow_zai
from hermes_cli.config import load_config, get_env_value
# User types a shell command instead of a URL at the base URL prompt
with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \
patch("hermes_cli.auth.deactivate_provider"), \
patch("builtins.input", return_value="nano ~/.hermes/.env"):
_model_flow_api_key_provider(load_config(), "zai", "old-model")
with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
return_value={
"provider": "zai",
"api_key": "***",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"source": "GLM_API_KEY",
},
), patch(
"hermes_cli.models.fetch_api_models",
return_value=["glm-5.1", "glm-5"],
), patch(
"hermes_cli.auth._prompt_model_selection",
return_value="glm-5.1",
), patch(
"hermes_cli.auth.deactivate_provider",
), patch(
"builtins.input",
return_value="",
):
_model_flow_zai(load_config(), "old-model")
# The garbage value should NOT have been saved
saved = get_env_value("GLM_BASE_URL") or ""
assert not saved or saved.startswith(("http://", "https://")), \
f"Non-URL value was saved as GLM_BASE_URL: {saved}"
captured = capsys.readouterr()
assert "Invalid URL" in captured.out
import yaml
def test_valid_base_url_accepted(self, config_home, monkeypatch):
"""A proper URL should be saved normally."""
from hermes_cli.auth import PROVIDER_REGISTRY
pconfig = PROVIDER_REGISTRY.get("zai")
if not pconfig:
pytest.skip("zai not in PROVIDER_REGISTRY")
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
model = config.get("model")
assert isinstance(model, dict)
assert model.get("provider") == "zai"
assert model.get("default") == "glm-5.1"
assert model.get("base_url") == "https://api.z.ai/api/coding/paas/v4"
assert get_env_value("GLM_BASE_URL") in ("", None)
def test_zai_setup_can_set_manual_base_url_override(self, config_home, monkeypatch):
monkeypatch.setenv("GLM_API_KEY", "test-key")
from hermes_cli.main import _model_flow_api_key_provider
from hermes_cli.main import _model_flow_zai
from hermes_cli.config import load_config, get_env_value
with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \
patch("hermes_cli.auth.deactivate_provider"), \
patch("builtins.input", return_value="https://custom.z.ai/api/paas/v4"):
_model_flow_api_key_provider(load_config(), "zai", "old-model")
with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
return_value={
"provider": "zai",
"api_key": "***",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"source": "GLM_API_KEY",
},
), patch(
"hermes_cli.models.fetch_api_models",
return_value=["glm-5", "glm-4.7"],
), patch(
"hermes_cli.auth._prompt_model_selection",
return_value="glm-5",
), patch(
"hermes_cli.auth.deactivate_provider",
), patch(
"builtins.input",
return_value="https://manual.example/v4",
):
_model_flow_zai(load_config(), "old-model")
saved = get_env_value("GLM_BASE_URL") or ""
assert saved == "https://custom.z.ai/api/paas/v4"
import yaml
def test_empty_base_url_keeps_default(self, config_home, monkeypatch):
"""Pressing Enter (empty) should not change the base URL."""
from hermes_cli.auth import PROVIDER_REGISTRY
pconfig = PROVIDER_REGISTRY.get("zai")
if not pconfig:
pytest.skip("zai not in PROVIDER_REGISTRY")
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
model = config.get("model")
assert isinstance(model, dict)
assert model.get("provider") == "zai"
assert model.get("default") == "glm-5"
assert model.get("base_url") == "https://manual.example/v4"
assert get_env_value("GLM_BASE_URL") == "https://manual.example/v4"
def test_zai_setup_can_clear_explicit_base_url_override(self, config_home, monkeypatch):
monkeypatch.setenv("GLM_API_KEY", "test-key")
monkeypatch.delenv("GLM_BASE_URL", raising=False)
monkeypatch.setenv("GLM_BASE_URL", "https://stale.example/v4")
from hermes_cli.main import _model_flow_api_key_provider
from hermes_cli.main import _model_flow_zai
from hermes_cli.config import load_config, get_env_value
with patch("hermes_cli.auth._prompt_model_selection", return_value="glm-5"), \
patch("hermes_cli.auth.deactivate_provider"), \
patch("builtins.input", return_value=""):
_model_flow_api_key_provider(load_config(), "zai", "old-model")
def _resolve(provider_id, ignore_env_base_url=False):
assert provider_id == "zai"
if ignore_env_base_url:
return {
"provider": "zai",
"api_key": "***",
"base_url": "https://api.z.ai/api/coding/paas/v4",
"source": "GLM_API_KEY",
}
return {
"provider": "zai",
"api_key": "***",
"base_url": "https://stale.example/v4",
"source": "GLM_API_KEY",
}
saved = get_env_value("GLM_BASE_URL") or ""
assert saved == "", "Empty input should not save a base URL"
with patch(
"hermes_cli.auth.resolve_api_key_provider_credentials",
side_effect=_resolve,
), patch(
"hermes_cli.models.fetch_api_models",
return_value=["glm-5", "glm-4.7"],
), patch(
"hermes_cli.auth._prompt_model_selection",
return_value="glm-5",
), patch(
"hermes_cli.auth.deactivate_provider",
), patch(
"builtins.input",
return_value="c",
):
_model_flow_zai(load_config(), "old-model")
import yaml
config = yaml.safe_load((config_home / "config.yaml").read_text()) or {}
model = config.get("model")
assert isinstance(model, dict)
assert model.get("provider") == "zai"
assert model.get("default") == "glm-5"
assert model.get("base_url") == "https://api.z.ai/api/coding/paas/v4"
assert get_env_value("GLM_BASE_URL") in ("", None)
class TestProviderDispatch:
def test_select_provider_and_model_routes_zai_to_dedicated_flow(self, config_home, monkeypatch):
from types import SimpleNamespace
from hermes_cli.main import select_provider_and_model
monkeypatch.setattr("hermes_cli.auth.resolve_provider", lambda value: None)
monkeypatch.setattr(
"hermes_cli.models.CANONICAL_PROVIDERS",
[SimpleNamespace(slug="zai", tui_desc="Z.AI")],
)
monkeypatch.setattr(
"hermes_cli.models._PROVIDER_LABELS",
{"zai": "Z.AI"},
)
monkeypatch.setattr("hermes_cli.main._prompt_provider_choice", lambda labels, default=0: 0)
seen = {}
monkeypatch.setattr(
"hermes_cli.main._model_flow_zai",
lambda config, current_model="": seen.setdefault("called", ("zai", current_model)),
)
monkeypatch.setattr(
"hermes_cli.main._model_flow_api_key_provider",
lambda config, provider_id, current_model="": seen.setdefault("called", ("generic", provider_id, current_model)),
)
monkeypatch.setattr("hermes_cli.main._clear_stale_openai_base_url", lambda: None)
select_provider_and_model()
assert seen.get("called") == ("zai", "some-old-model")
def test_select_provider_and_model_keeps_neighboring_api_key_providers_generic(self, config_home, monkeypatch):
from types import SimpleNamespace
from hermes_cli.main import select_provider_and_model
monkeypatch.setattr("hermes_cli.auth.resolve_provider", lambda value: None)
monkeypatch.setattr(
"hermes_cli.models.CANONICAL_PROVIDERS",
[SimpleNamespace(slug="nvidia", tui_desc="NVIDIA")],
)
monkeypatch.setattr(
"hermes_cli.models._PROVIDER_LABELS",
{"nvidia": "NVIDIA"},
)
monkeypatch.setattr("hermes_cli.main._prompt_provider_choice", lambda labels, default=0: 0)
seen = {}
monkeypatch.setattr(
"hermes_cli.main._model_flow_zai",
lambda config, current_model="": seen.setdefault("called", ("zai", current_model)),
)
monkeypatch.setattr(
"hermes_cli.main._model_flow_api_key_provider",
lambda config, provider_id, current_model="": seen.setdefault("called", ("generic", provider_id, current_model)),
)
monkeypatch.setattr("hermes_cli.main._clear_stale_openai_base_url", lambda: None)
select_provider_and_model()
assert seen.get("called") == ("generic", "nvidia", "some-old-model")