feat(model-switch): excluded_providers config to hide providers from /model picker

This commit is contained in:
Craig French 2026-07-19 19:34:00 -04:00 • committed by Teknium
parent 1b56d0d1a2
commit b239ee2123
7 changed files with 245 additions and 1 deletions

View file

@ -52,6 +52,7 @@ class ConfigContext:
current_base_url: str
user_providers: dict
custom_providers: list
excluded_providers: list = None
def with_overrides(
self,
@ -96,12 +97,14 @@ def load_picker_context() -> ConfigContext:
current_provider = ""
current_base_url = ""
raw = cfg.get("providers")
excluded = cfg.get("model_catalog", {}).get("excluded_providers") or []
return ConfigContext(
current_provider=current_provider,
current_model=current_model,
current_base_url=current_base_url,
user_providers=raw if isinstance(raw, dict) else {},
custom_providers=get_compatible_custom_providers(cfg),
excluded_providers=excluded if isinstance(excluded, list) else [],
)
@ -179,6 +182,7 @@ def build_models_payload(
refresh=refresh,
probe_custom_providers=probe_custom_providers,
probe_current_custom_provider=probe_current_custom_provider,
excluded_providers=ctx.excluded_providers or [],
)
moa_row = _moa_provider_row(ctx.current_provider)

View file

@ -3167,6 +3167,7 @@ def select_provider_and_model(args=None):
from hermes_cli.models import (
CANONICAL_PROVIDERS,
_PROVIDER_LABELS,
_PROVIDER_ALIASES,
group_providers,
provider_group_for_slug,
)
@ -3190,7 +3191,30 @@ def select_provider_and_model(args=None):
# resolves back to a concrete slug, so the dispatch chain below is
# unchanged. Custom providers and the trailing actions stay flat.
canonical_descs = {p.slug: p.tui_desc for p in CANONICAL_PROVIDERS}
grouped_rows = group_providers([p.slug for p in CANONICAL_PROVIDERS])
# Honor ``model_catalog.excluded_providers`` so the CLI ``hermes model``
# picker hides the same providers the gateway/TUI pickers do. A canonical
# provider is hidden if its slug OR any of its aliases appears in the
# exclusion list (case-insensitive), matching list_authenticated_providers'
# matching against hermes_id / alias / canonical slug.
_cli_excluded = {
str(p).strip().lower()
for p in (config.get("model_catalog", {}) or {}).get("excluded_providers") or []
if p
}
if _cli_excluded:
_alias_to_canon = _PROVIDER_ALIASES
_names_for: dict[str, set[str]] = {}
for _p in CANONICAL_PROVIDERS:
_names_for[_p.slug] = {_p.slug.lower()}
for _alias, _canon in _alias_to_canon.items():
_names_for.setdefault(_canon, {_canon.lower()}).add(_alias.lower())
_visible_slugs = [
p.slug for p in CANONICAL_PROVIDERS
if not _names_for.get(p.slug, {p.slug.lower()}) & _cli_excluded
]
else:
_visible_slugs = [p.slug for p in CANONICAL_PROVIDERS]
grouped_rows = group_providers(_visible_slugs)
# The group/slug that should be pre-selected: the active provider's group
# if it's grouped, otherwise the active slug itself.

View file

@ -1628,6 +1628,7 @@ def prewarm_picker_cache_async() -> Optional["_threading.Thread"]:
current_model=ctx.current_model,
user_providers=ctx.user_providers,
custom_providers=ctx.custom_providers,
excluded_providers=ctx.excluded_providers or [],
)
except Exception:
# Best-effort warmup — never surface errors into the session.
@ -1651,6 +1652,7 @@ def list_authenticated_providers(
probe_custom_providers: bool = True,
probe_current_custom_provider: bool = False,
for_picker: bool = False,
excluded_providers: list | None = None,
) -> List[dict]:
"""Detect which providers have credentials and list their curated models.
@ -1722,6 +1724,11 @@ def list_authenticated_providers(
def _can_probe_custom_provider(*, row_is_current: bool) -> bool:
return bool(probe_custom_providers or (probe_current_custom_provider and row_is_current))
# Normalize the excluded-providers list once for fast membership checks.
# Compared against hermes_id / mdev_id (section 1), pid / hermes_slug
# (section 2) and canonical slug (section 2b) so a single entry like
# ``copilot`` hides the provider regardless of which key it surfaces under.
_excluded: set = {str(p).strip().lower() for p in (excluded_providers or []) if p}
# 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
@ -1861,6 +1868,8 @@ def list_authenticated_providers(
# The first one with valid credentials wins (#10526).
if mdev_id in seen_mdev_ids:
continue
if hermes_id.lower() in _excluded or mdev_id.lower() in _excluded:
continue
pdata = data.get(mdev_id)
if not isinstance(pdata, dict):
continue
@ -1962,6 +1971,8 @@ def list_authenticated_providers(
hermes_slug = _mdev_to_hermes.get(pid, pid)
if hermes_slug.lower() in seen_slugs:
continue
if pid.lower() in _excluded or hermes_slug.lower() in _excluded:
continue
# Check if credentials exist
has_creds = False
@ -2131,6 +2142,8 @@ def list_authenticated_providers(
for _cp in _canon_provs:
if _cp.slug.lower() in seen_slugs:
continue
if _cp.slug.lower() in _excluded:
continue
# Check credentials via PROVIDER_REGISTRY (auth.py)
_cp_config = _auth_registry.get(_cp.slug)
@ -2731,6 +2744,7 @@ def list_picker_providers(
max_models: int | None = None,
current_model: str = "",
include_moa: bool = False,
excluded_providers: list | None = None,
) -> List[dict]:
"""Interactive-picker variant of :func:`list_authenticated_providers`.
@ -2761,6 +2775,7 @@ def list_picker_providers(
max_models=max_models,
current_model=current_model,
for_picker=True,
excluded_providers=excluded_providers,
)
if include_moa:
providers = _prepend_moa_picker_provider(providers, current_provider=current_provider)