From fe5054bccfc5128167adafe8235e84c5f807b136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B3=AF=E5=B2=B8=E3=80=80=E4=BA=AE?= <1920071390@campus.ouj.ac.jp> Date: Sun, 21 Jun 2026 01:38:28 +0900 Subject: [PATCH] fix(desktop): avoid probing custom providers on model picker open --- hermes_cli/inventory.py | 7 ++++++ hermes_cli/model_switch.py | 11 +++++++-- hermes_cli/web_server.py | 1 + tests/hermes_cli/test_inventory.py | 13 ++++++++++ .../test_model_switch_custom_providers.py | 24 +++++++++++++++++++ tests/test_tui_gateway_server.py | 17 +++++++++++++ tui_gateway/server.py | 1 + 7 files changed, 72 insertions(+), 2 deletions(-) diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 0914cfc0377..51569c73b2c 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -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) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index b9a50f6df63..c92d50a7319 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -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) ) diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 401dfa6506a..26f35fce384 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -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 diff --git a/tests/hermes_cli/test_inventory.py b/tests/hermes_cli/test_inventory.py index d33c7ff651f..6bd582d438f 100644 --- a/tests/hermes_cli/test_inventory.py +++ b/tests/hermes_cli/test_inventory.py @@ -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. diff --git a/tests/hermes_cli/test_model_switch_custom_providers.py b/tests/hermes_cli/test_model_switch_custom_providers.py index bfffa0474de..04dbc3f36ed 100644 --- a/tests/hermes_cli/test_model_switch_custom_providers.py +++ b/tests/hermes_cli/test_model_switch_custom_providers.py @@ -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( diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 6d39a252cfe..7c9667e61c6 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -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.""" diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 878b28d02c2..8b2ab195b02 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -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: