mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-29 23:41:35 +08:00
Compare commits
1 Commits
fix/plugin
...
salvage/he
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c20577b6c0 |
@@ -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"):
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user