mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(tui): probe active custom model provider
This commit is contained in:
parent
4131ec380b
commit
4b4f058860
7 changed files with 137 additions and 11 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue