fix(desktop): avoid probing custom providers on model picker open

This commit is contained in:
峯岸 亮 2026-06-21 01:38:28 +09:00 committed by Teknium
parent bd480b4c8c
commit fe5054bccf
7 changed files with 72 additions and 2 deletions

View file

@ -118,6 +118,7 @@ def build_models_payload(
capabilities: bool = False,
force_fresh_nous_tier: bool = False,
refresh: bool = False,
probe_custom_providers: bool = True,
max_models: int | None = None,
) -> dict:
"""Build the ``{providers, model, provider}`` shape every consumer
@ -149,6 +150,11 @@ def build_models_payload(
re-fetches its live catalog. Set only for an explicit user-triggered
"refresh models" action; normal picker opens leave it false to stay
snappy on the 1h cache.
- ``probe_custom_providers``: allow saved custom/provider endpoints to
run live ``/models`` discovery while building the payload. GUI picker
opens should leave this false unless the user explicitly refreshes; the
row can still render its configured model immediately, and slow/offline
local endpoints no longer block the dialog.
"""
from hermes_cli.model_switch import list_authenticated_providers
@ -161,6 +167,7 @@ def build_models_payload(
force_fresh_nous_tier=force_fresh_nous_tier,
max_models=max_models,
refresh=refresh,
probe_custom_providers=probe_custom_providers,
)
moa_row = _moa_provider_row(ctx.current_provider)

View file

@ -1426,6 +1426,7 @@ def list_authenticated_providers(
max_models: int | None = None,
current_model: str = "",
refresh: bool = False,
probe_custom_providers: bool = True,
) -> List[dict]:
"""Detect which providers have credentials and list their curated models.
@ -1452,6 +1453,11 @@ def list_authenticated_providers(
live catalog. Use for an explicit user-triggered "refresh models" action
(e.g. the desktop picker's refresh control); leave false for normal picker
opens so they stay snappy on the 1h cache.
``probe_custom_providers`` controls live ``/models`` discovery for saved
custom OpenAI-compatible endpoints. Keep the default true for CLI parity;
GUI picker opens can pass false to show configured models immediately
without waiting on offline local endpoints.
"""
import os
from agent.models_dev import (
@ -1994,7 +2000,7 @@ def list_authenticated_providers(
if isinstance(discover, str):
discover = discover.lower() not in {"false", "no", "0"}
has_explicit_models = bool(models_list)
should_probe = bool(api_url) and discover and (
should_probe = probe_custom_providers and bool(api_url) and discover and (
bool(api_key) or not has_explicit_models
)
if should_probe:
@ -2255,7 +2261,8 @@ def list_authenticated_providers(
# full aggregator catalog via /models but only serve a subset
# (parity with section 3's user ``providers:`` behaviour).
should_probe = (
bool(api_url)
probe_custom_providers
and bool(api_url)
and (bool(api_key) or not grp["models"])
and grp.get("discover_models", True)
)

View file

@ -4296,6 +4296,7 @@ def get_model_options(profile: Optional[str] = None, refresh: bool = False):
pricing=True,
capabilities=True,
refresh=bool(refresh),
probe_custom_providers=bool(refresh),
)
except HTTPException:
raise

View file

@ -219,6 +219,19 @@ def test_build_models_payload_can_force_fresh_nous_tier():
assert mock_list.call_args.kwargs["force_fresh_nous_tier"] is True
def test_build_models_payload_can_skip_custom_provider_probes():
ctx = _empty_ctx()
rows = []
with patch(
"hermes_cli.model_switch.list_authenticated_providers",
return_value=rows,
) as mock_list:
build_models_payload(ctx, probe_custom_providers=False)
mock_list.assert_called_once()
assert mock_list.call_args.kwargs["probe_custom_providers"] is False
def test_list_authenticated_providers_force_fresh_is_keyword_only():
"""``force_fresh_nous_tier`` must be keyword-only on the public listing API.

View file

@ -45,6 +45,30 @@ def test_list_authenticated_providers_includes_custom_providers(monkeypatch):
)
def test_list_authenticated_providers_can_skip_custom_provider_live_probe(monkeypatch):
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
fetch = lambda *a, **k: (_ for _ in ()).throw(AssertionError("unexpected probe"))
monkeypatch.setattr("hermes_cli.models.fetch_api_models", fetch)
providers = list_authenticated_providers(
user_providers={},
custom_providers=[
{
"name": "Slow Local",
"base_url": "http://127.0.0.1:8080/v1",
"api_key": "sk-local",
"model": "local-model",
}
],
probe_custom_providers=False,
)
row = next(p for p in providers if p["slug"] == "custom:slow-local")
assert row["models"] == ["local-model"]
assert row["total_models"] == 1
def test_resolve_provider_full_finds_named_custom_provider():
"""Explicit /model --provider should resolve saved custom_providers entries."""
resolved = resolve_provider_full(

View file

@ -5845,6 +5845,7 @@ def test_model_options_does_not_overwrite_curated_models(monkeypatch):
live_fetch.assert_not_called()
# list_authenticated_providers is the single source.
assert listing.call_count == 1
assert listing.call_args.kwargs["probe_custom_providers"] is False
def test_model_options_propagates_list_exception(monkeypatch):
@ -5870,6 +5871,22 @@ def test_model_options_propagates_list_exception(monkeypatch):
# ---------------------------------------------------------------------------
def test_model_options_refresh_allows_custom_provider_probes(monkeypatch):
monkeypatch.setattr(
server,
"_load_cfg",
lambda: {"providers": {}, "custom_providers": []},
)
with patch(
"hermes_cli.model_switch.list_authenticated_providers",
return_value=[],
) as listing:
resp = server._methods["model.options"](78, {"session_id": "", "refresh": True})
assert "result" in resp, resp
assert listing.call_args.kwargs["probe_custom_providers"] is True
class _ImmediateThread:
"""Runs the target callable synchronously so assertions can follow."""

View file

@ -12325,6 +12325,7 @@ def _(rid, params: dict) -> dict:
pricing=True,
capabilities=True,
refresh=bool(params.get("refresh")),
probe_custom_providers=bool(params.get("refresh")),
)
return _ok(rid, payload)
except Exception as e: