fix(model-switch): treat models dict as metadata, not allowlist

hermes model saves custom_providers models: {default: {context_length}} for
local Ollama. That dict shape was treated as an explicit catalog, so no-key
endpoints skipped live /v1/models probing and Desktop/Telegram only showed
the saved default — Refresh could not help. Keep list/string shapes as
allowlists; pin dict catalogs with discover_models: false.
This commit is contained in:
HexLab98 2026-07-27 07:21:27 +07:00 committed by Teknium
parent f5be9236e0
commit f66319097e
1 changed files with 60 additions and 33 deletions

View File

@ -102,6 +102,30 @@ def _declared_model_ids(value: Any) -> list[str]:
return ids
def _models_config_is_allowlist(value: Any) -> bool:
"""Return True when ``models:`` is an intentional ID allowlist.
A mapping like ``{model_id: {context_length: N}}`` is per-model *metadata*
written by ``_save_custom_provider`` / the ``hermes model`` wizard not a
catalog narrow. Treating that shape as an allowlist made Desktop/Telegram
pickers show only the saved default for local Ollama (no ``api_key``),
while ``hermes model`` still live-probed the full ``/v1/models`` list.
Refresh could not help because the same gate skipped probing.
List/string shapes remain allowlists for no-key endpoints. To pin a
dict-shaped catalog, set ``discover_models: false``.
"""
if value is None:
return False
if isinstance(value, str):
return bool(value.strip())
if isinstance(value, dict):
return False
if isinstance(value, (list, tuple)):
return bool(_declared_model_ids(value))
return False
def _save_discovered_models_to_config(
api_url: str, model_ids: list[str]
) -> None:
@ -2631,12 +2655,13 @@ def list_authenticated_providers(
for _m in entry_models:
if _m and _m not in ep_groups[group_key]["models"]:
ep_groups[group_key]["models"].append(_m)
# Track explicit ``models:`` declarations separately from the
# merged list: a singular ``default_model``/``model`` is only the
# active selection and must not be mistaken for the user narrowing
# the endpoint to a curated subset (mirrors section 4's
# declaration-tracking; see #40542 / PR #61928).
if entry_declared_models:
# Track allowlist-shaped ``models:`` separately from the merged
# list: a singular ``default_model``/``model`` is only the active
# selection and must not suppress discovery (see #40542 / PR
# #61928). Dict-shaped ``models:`` is context_length metadata from
# ``hermes model``, not an allowlist — see
# ``_models_config_is_allowlist``.
if _models_config_is_allowlist(ep_cfg.get("models")):
ep_groups[group_key]["has_explicit_models"] = True
ep_groups[group_key]["raw_names"].append(display_name)
ep_groups[group_key]["aliases"].update(
@ -2663,14 +2688,16 @@ def list_authenticated_providers(
# unless the provider explicitly opts out via discover_models: false.
# Policy mirrors Section 4's should_probe logic:
# - With an api_key: always probe (user opted into the endpoint).
# - Without an api_key but with an explicit ``models:`` list:
# skip — the user is narrowing a public endpoint to a specific
# subset. A singular ``default_model``/``model`` does NOT count
# as narrowing (it's just the active selection) and must not
# suppress discovery — mirrors section 4 / #40542.
# - Without an api_key AND no explicit models: probe anyway so
# bare-endpoint providers (local llama.cpp / Ollama servers)
# still show their full model catalog.
# - Without an api_key but with an allowlist-shaped ``models:``
# (list/string): skip — the user narrowed a public endpoint.
# A singular ``default_model``/``model`` does NOT count as
# narrowing (mirrors section 4 / #40542).
# - A dict-shaped ``models:`` is per-model metadata
# (context_length), not an allowlist — still probe so local
# Ollama/llama.cpp match ``hermes model``. Pin with
# ``discover_models: false`` instead.
# - Without an api_key AND no allowlist: probe anyway so bare
# local endpoints still show their full model catalog.
api_key = str(ep_cfg.get("api_key", "") or "").strip()
if not api_key:
key_env = str(ep_cfg.get("key_env", "") or "").strip()
@ -2911,8 +2938,12 @@ def list_authenticated_providers(
if default_model and default_model not in groups[group_key]["models"]:
groups[group_key]["models"].append(default_model)
declared_models = _declared_model_ids(entry.get("models", {}))
if declared_models:
models_field = entry.get("models", {})
declared_models = _declared_model_ids(models_field)
# Dict-shaped models: is context_length metadata from
# ``_save_custom_provider``, not an allowlist — see
# ``_models_config_is_allowlist``.
if _models_config_is_allowlist(models_field):
groups[group_key]["has_explicit_models"] = True
for model_id in declared_models:
if model_id not in groups[group_key]["models"]:
@ -2974,24 +3005,20 @@ def list_authenticated_providers(
# Live-discovery policy:
# - With an api_key, the user has explicitly opted into the
# endpoint and live /models is the source of truth — replace
# the (possibly partial) ``models:`` subset configured for
# context-length overrides with the full live catalog.
# This is the Bifrost / aggregator-gateway case.
# - Without an api_key but with an explicit ``models:`` list,
# the user is narrowing a public endpoint to a specific subset
# (e.g. ollama.com /v1/models returns 35 models but the user
# only wants 4). Preserve the explicit list and skip live
# discovery. The singular ``model:`` field is only the current
# active selection and must not suppress discovery on local
# no-key endpoints.
# - Without an api_key AND no explicit models, fall through to
# live discovery so bare-endpoint custom providers (local
# llama.cpp / Ollama servers) still appear populated.
# the (possibly partial) ``models:`` subset with the full
# live catalog (Bifrost / aggregator-gateway case).
# - Without an api_key but with an allowlist-shaped ``models:``
# (list/string), the user narrowed a public endpoint (e.g.
# ollama.com). Preserve that list and skip live discovery.
# - A dict-shaped ``models:`` is per-model metadata written by
# ``_save_custom_provider`` for context_length — not an
# allowlist. Still probe so Desktop/Telegram match
# ``hermes model``. Pin a dict catalog with
# ``discover_models: false``.
# - The singular ``model:`` field is only the current active
# selection and must not suppress discovery.
# - When discover_models: false is set, skip live discovery and
# keep the explicit ``models:`` list regardless of whether an
# 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).
# keep the configured ``models:`` list regardless of api_key.
_grp_is_current = (
slug.lower() == _current_provider_norm
or _current_provider_norm in {