mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 19:20:16 +00:00
125 lines
4.4 KiB
Python
125 lines
4.4 KiB
Python
import urllib.error
|
|
import urllib.request
|
|
from email.message import Message
|
|
|
|
from api import config
|
|
|
|
|
|
class _ConfigState:
|
|
def __enter__(self):
|
|
self.old_cfg = config.cfg
|
|
self.old_mtime = config._cfg_mtime
|
|
self.old_cache = config._available_models_cache
|
|
self.old_cache_ts = config._available_models_cache_ts
|
|
self.old_cache_fp = config._available_models_cache_source_fingerprint
|
|
config._cfg_mtime = 0.0
|
|
config._available_models_cache = None
|
|
config._available_models_cache_ts = 0.0
|
|
config._available_models_cache_source_fingerprint = None
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc, tb):
|
|
config.cfg = self.old_cfg
|
|
config._cfg_mtime = self.old_mtime
|
|
config._available_models_cache = self.old_cache
|
|
config._available_models_cache_ts = self.old_cache_ts
|
|
config._available_models_cache_source_fingerprint = self.old_cache_fp
|
|
return False
|
|
|
|
|
|
def _configure_named_custom_provider(tmp_path, monkeypatch, *, model=None):
|
|
monkeypatch.setattr(config, "_models_cache_path", tmp_path / "models_cache.json")
|
|
monkeypatch.setattr(config, "_get_auth_store_path", lambda: tmp_path / "auth.json")
|
|
entry = {
|
|
"name": "Broken Proxy",
|
|
"base_url": "https://broken.example/v1",
|
|
"api_key": "bad-key",
|
|
}
|
|
if model:
|
|
entry["model"] = model
|
|
config.cfg = {
|
|
"model": {"provider": "openai-codex", "default": "gpt-5.5"},
|
|
"providers": {},
|
|
"fallback_providers": [],
|
|
"custom_providers": [entry],
|
|
}
|
|
|
|
|
|
def _groups_by_provider(data):
|
|
return {group["provider_id"]: group for group in data["groups"]}
|
|
|
|
|
|
def test_named_custom_provider_models_endpoint_401_surfaces_error(monkeypatch, tmp_path):
|
|
def fake_urlopen(req, timeout=10):
|
|
raise urllib.error.HTTPError(
|
|
getattr(req, "full_url", "https://broken.example/v1/models"),
|
|
401,
|
|
"Unauthorized",
|
|
hdrs=Message(),
|
|
fp=None,
|
|
)
|
|
|
|
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
|
|
with _ConfigState():
|
|
_configure_named_custom_provider(tmp_path, monkeypatch, model="broken/manual")
|
|
data = config.get_available_models()
|
|
|
|
group = _groups_by_provider(data)["custom:broken-proxy"]
|
|
error = group["models_endpoint_error"]
|
|
assert error["kind"] == "auth"
|
|
assert error["code"] == 401
|
|
assert "check the API key" in error["message"]
|
|
assert "broken-proxy" in error["message"]
|
|
assert "@custom:broken-proxy:broken/manual" in {m["id"] for m in group["models"]}
|
|
|
|
|
|
def test_named_custom_provider_models_endpoint_network_error_surfaces_empty_group(monkeypatch, tmp_path):
|
|
def fake_urlopen(req, timeout=10):
|
|
raise urllib.error.URLError("connection refused")
|
|
|
|
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
|
|
with _ConfigState():
|
|
_configure_named_custom_provider(tmp_path, monkeypatch)
|
|
data = config.get_available_models()
|
|
|
|
group = _groups_by_provider(data)["custom:broken-proxy"]
|
|
assert group["models"] == []
|
|
assert group["models_endpoint_error"]["kind"] == "network"
|
|
assert group["models_endpoint_error"]["code"] is None
|
|
assert "verify base_url" in group["models_endpoint_error"]["message"]
|
|
|
|
|
|
def test_named_custom_provider_models_endpoint_5xx_preserves_status(monkeypatch, tmp_path):
|
|
def fake_urlopen(req, timeout=10):
|
|
raise urllib.error.HTTPError(
|
|
getattr(req, "full_url", "https://broken.example/v1/models"),
|
|
502,
|
|
"Bad Gateway",
|
|
hdrs=Message(),
|
|
fp=None,
|
|
)
|
|
|
|
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
|
|
|
|
with _ConfigState():
|
|
_configure_named_custom_provider(tmp_path, monkeypatch, model="broken/manual")
|
|
data = config.get_available_models()
|
|
|
|
error = _groups_by_provider(data)["custom:broken-proxy"]["models_endpoint_error"]
|
|
assert error["kind"] == "http"
|
|
assert error["code"] == 502
|
|
assert "returned 502" in error["message"]
|
|
|
|
|
|
def test_frontend_model_picker_renders_provider_endpoint_hint():
|
|
ui = open("static/ui.js", encoding="utf-8").read()
|
|
css = open("static/style.css", encoding="utf-8").read()
|
|
|
|
assert "models_endpoint_error" in ui
|
|
assert "dataset.modelsEndpointError" in ui
|
|
assert "model-provider-hint" in ui
|
|
assert "entry.modelsEndpointError.message" in ui
|
|
assert ".model-provider-hint" in css
|