fix(tui): probe active custom model provider

This commit is contained in:
helix4u 2026-07-06 13:06:37 -06:00 committed by Teknium
parent 4131ec380b
commit 4b4f058860
7 changed files with 137 additions and 11 deletions

View file

@ -119,6 +119,7 @@ def build_models_payload(
force_fresh_nous_tier: bool = False,
refresh: bool = False,
probe_custom_providers: bool = True,
probe_current_custom_provider: bool = False,
max_models: int | None = None,
) -> dict:
"""Build the ``{providers, model, provider}`` shape every consumer
@ -155,6 +156,10 @@ def build_models_payload(
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.
- ``probe_current_custom_provider``: when ``probe_custom_providers`` is
false, still live-probe the current custom endpoint. This keeps normal
GUI/TUI picker opens fast while making the active custom provider's model
list match the classic CLI picker.
"""
from hermes_cli.model_switch import list_authenticated_providers
@ -168,6 +173,7 @@ def build_models_payload(
max_models=max_models,
refresh=refresh,
probe_custom_providers=probe_custom_providers,
probe_current_custom_provider=probe_current_custom_provider,
)
moa_row = _moa_provider_row(ctx.current_provider)

View file

@ -1455,6 +1455,7 @@ def list_authenticated_providers(
current_model: str = "",
refresh: bool = False,
probe_custom_providers: bool = True,
probe_current_custom_provider: bool = False,
) -> List[dict]:
"""Detect which providers have credentials and list their curated models.
@ -1486,6 +1487,11 @@ def list_authenticated_providers(
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.
``probe_current_custom_provider`` is the middle ground for GUI picker
opens: probe only the currently-selected custom endpoint so its model list
matches the active provider without blocking on every saved/offline custom
endpoint.
"""
import os
from agent.models_dev import (
@ -1515,6 +1521,12 @@ def list_authenticated_providers(
results: List[dict] = []
seen_slugs: set = set() # lowercase-normalized to catch case variants (#9545)
seen_mdev_ids: set = set() # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)
_current_provider_norm = str(current_provider or "").strip().lower()
_current_base_url_norm = str(current_base_url or "").strip().rstrip("/").lower()
def _can_probe_custom_provider(*, row_is_current: bool) -> bool:
return bool(probe_custom_providers or (probe_current_custom_provider and row_is_current))
# Effective base URLs of every built-in row we emit (normalized lower+rstrip).
# Section 4 uses this to hide ``custom_providers`` entries that point at the
# same endpoint as a built-in (e.g. a user-defined "my-dashscope" on
@ -2021,7 +2033,19 @@ def list_authenticated_providers(
if isinstance(discover, str):
discover = discover.lower() not in {"false", "no", "0"}
has_explicit_models = bool(models_list)
should_probe = probe_custom_providers and bool(api_url) and discover and (
_ep_url_norm = str(api_url).strip().rstrip("/").lower()
_ep_slug_norm = str(ep_name).strip().lower()
_ep_custom_slug_norm = custom_provider_slug(display_name).lower()
_ep_is_current = (
_ep_slug_norm == _current_provider_norm
or _ep_custom_slug_norm == _current_provider_norm
or (
_current_provider_norm == "custom"
and bool(_current_base_url_norm)
and _ep_url_norm == _current_base_url_norm
)
)
should_probe = _can_probe_custom_provider(row_is_current=_ep_is_current) and bool(api_url) and discover and (
bool(api_key) or not has_explicit_models
)
if should_probe:
@ -2040,7 +2064,7 @@ def list_authenticated_providers(
results.append({
"slug": ep_name,
"name": display_name,
"is_current": ep_name == current_provider,
"is_current": _ep_is_current,
"is_user_defined": True,
"models": models_list,
"total_models": len(models_list) if models_list else 0,
@ -2064,7 +2088,6 @@ def list_authenticated_providers(
# picker to render, but the gateway only passes this current model slice to
# list_authenticated_providers(). Surface the active endpoint explicitly so
# /model does not look like it ignored config.yaml.
_current_provider_norm = str(current_provider or "").strip().lower()
if (
_current_provider_norm == "custom"
and current_base_url
@ -2081,6 +2104,15 @@ def list_authenticated_providers(
)
):
_models = [current_model] if current_model else []
if _can_probe_custom_provider(row_is_current=True):
try:
from hermes_cli.models import fetch_api_models
_live_models = fetch_api_models("", str(current_base_url).strip().rstrip("/"))
if _live_models:
_models = _live_models
except Exception:
pass
results.append({
"slug": "custom",
"name": "Custom endpoint",
@ -2203,7 +2235,6 @@ def list_authenticated_providers(
groups[group_key]["models"].append(model_id)
_section4_emitted_slugs: set = set()
_current_base_url_norm = str(current_base_url or "").strip().rstrip("/").lower()
_current_base_url_group_count = sum(
1
for _grp in groups.values()
@ -2275,8 +2306,14 @@ def list_authenticated_providers(
# api_key is present. This supports endpoints that expose a
# full aggregator catalog via /models but only serve a subset
# (parity with section 3's user ``providers:`` behaviour).
_grp_is_current = slug.lower() == _current_provider_norm or (
_current_provider_norm == "custom"
and bool(_current_base_url_norm)
and _grp_url_norm == _current_base_url_norm
and _current_base_url_group_count == 1
)
should_probe = (
probe_custom_providers
_can_probe_custom_provider(row_is_current=_grp_is_current)
and bool(api_url)
and (bool(api_key) or not grp["models"])
and grp.get("discover_models", True)
@ -2298,12 +2335,7 @@ def list_authenticated_providers(
results.append({
"slug": slug,
"name": grp["name"],
"is_current": slug == current_provider or (
current_provider == "custom"
and bool(_current_base_url_norm)
and _grp_url_norm == _current_base_url_norm
and _current_base_url_group_count == 1
),
"is_current": _grp_is_current,
"is_user_defined": True,
"models": grp["models"],
"total_models": len(grp["models"]),

View file

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

View file

@ -258,6 +258,24 @@ def test_build_models_payload_can_skip_custom_provider_probes():
assert mock_list.call_args.kwargs["probe_custom_providers"] is False
def test_build_models_payload_can_probe_only_current_custom_provider():
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,
probe_current_custom_provider=True,
)
mock_list.assert_called_once()
assert mock_list.call_args.kwargs["probe_custom_providers"] is False
assert mock_list.call_args.kwargs["probe_current_custom_provider"] is True
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

@ -69,6 +69,49 @@ def test_list_authenticated_providers_can_skip_custom_provider_live_probe(monkey
assert row["total_models"] == 1
def test_list_authenticated_providers_can_probe_only_current_custom_provider(monkeypatch):
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
calls = []
def fetch(api_key, api_url, **kwargs):
calls.append(api_url)
if api_url == "http://active.local/v1":
return ["active-a", "active-b"]
raise AssertionError(f"unexpected probe for {api_url}")
monkeypatch.setattr("hermes_cli.models.fetch_api_models", fetch)
providers = list_authenticated_providers(
current_provider="custom:active-proxy",
user_providers={},
custom_providers=[
{
"name": "Active Proxy",
"base_url": "http://active.local/v1",
"api_key": "sk-active",
"model": "configured-active",
},
{
"name": "Offline Proxy",
"base_url": "http://offline.local/v1",
"api_key": "sk-offline",
"model": "configured-offline",
},
],
probe_custom_providers=False,
probe_current_custom_provider=True,
)
active = next(p for p in providers if p["slug"] == "custom:active-proxy")
offline = next(p for p in providers if p["slug"] == "custom:offline-proxy")
assert calls == ["http://active.local/v1"]
assert active["is_current"] is True
assert active["models"] == ["active-a", "active-b"]
assert offline["models"] == ["configured-offline"]
def test_resolve_provider_full_finds_named_custom_provider():
"""Explicit /model --provider should resolve saved custom_providers entries."""
resolved = resolve_provider_full(
@ -119,6 +162,29 @@ def test_list_authenticated_providers_includes_active_bare_custom_endpoint(monke
assert bare_custom["api_url"] == "https://www.ccsub.net/v1"
def test_list_authenticated_providers_can_probe_active_bare_custom_endpoint(monkeypatch):
monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {})
monkeypatch.setattr(
"hermes_cli.models.fetch_api_models",
lambda api_key, api_url, **kwargs: ["gpt-4o", "gpt-4o-mini"],
)
providers = list_authenticated_providers(
current_provider="custom",
current_base_url="https://www.ccsub.net/v1",
current_model="gpt-4o",
user_providers={},
custom_providers=[],
probe_custom_providers=False,
probe_current_custom_provider=True,
)
bare_custom = next(p for p in providers if p["slug"] == "custom")
assert bare_custom["is_current"] is True
assert bare_custom["models"] == ["gpt-4o", "gpt-4o-mini"]
def test_switch_model_accepts_explicit_bare_custom_current_endpoint(monkeypatch):
"""Picker selections for bare custom endpoints should route to current base_url."""
monkeypatch.setattr("hermes_cli.models.validate_requested_model", lambda *a, **k: _MOCK_VALIDATION)

View file

@ -5963,6 +5963,7 @@ def test_model_options_does_not_overwrite_curated_models(monkeypatch):
# list_authenticated_providers is the single source.
assert listing.call_count == 1
assert listing.call_args.kwargs["probe_custom_providers"] is False
assert listing.call_args.kwargs["probe_current_custom_provider"] is True
def test_model_options_propagates_list_exception(monkeypatch):
@ -6002,6 +6003,7 @@ def test_model_options_refresh_allows_custom_provider_probes(monkeypatch):
assert "result" in resp, resp
assert listing.call_args.kwargs["probe_custom_providers"] is True
assert listing.call_args.kwargs["probe_current_custom_provider"] is False
class _ImmediateThread:

View file

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