Offload blocking I/O calls off async event loops
Cluster 44560 · apps/desktop. Data as of
2 open items. Fix merged in #58183 on 4 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #58183. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #58183. That is separate from the fix event. - Still open
- 0 issues and 2 PRs.
These issues and PRs address synchronous HTTP calls, model discovery, and security checks running on async event loops, causing freezes and timeouts in Discord, Telegram, WebSocket, and desktop contexts. Fixes involve threading, caching, TTLs, and deferring work to executors.
Open issues0
Open PRs2
Fixes on record
-
#58183 fix(desktop): avoid probing custom providers on model picker open (salvage #49713)
files:
hermes_cli/inventory.py,hermes_cli/model_switch.py,hermes_cli/web_server.py,tests/hermes_cli/test_inventory.py,tests/hermes_cli/test_model_switch_custom_providers.py,tests/test_tui_gateway_server.py,tui_gateway/server.pyShow the diff
-
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) -
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 _has_aws_sdk_creds_for_listing(slug: str) -> bool: 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 _has_aws_sdk_creds_for_listing(slug: str) -> bool: # 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) ) -
hermes_cli/web_server.py@@ -4225,6 +4225,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 -
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. -
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": "[redacted:secret-assignment]", + "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( -
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.""" -
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:
-
2 PRs closed without merging
From the close comments: superseded by another PR 2
Closed issues · 1
- Closed#44560model.options handler blocks on synchronous per-provider HTTP calls, causing WebSocket timeout
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- apps/desktop
- Merged PRs
- #58183
- Closed issues
- 1
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 4 Jul 2026