diff --git a/CHANGELOG.md b/CHANGELOG.md index 66b5cd5f..fc6f7b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- **PR #2415** by @Michaelyklam (fixes #2399) — `providers.only_configured` and other scalar flags under the top-level `providers:` config mapping no longer appear as fake provider groups in the model picker. Provider detection now only seeds picker groups from known provider ids/aliases or dict-shaped provider configs, so filtering flags cannot render as `Only-Configured`. + ## [v0.51.79] — 2026-05-16 — Release BC (stage-372 — 5-PR batch — text-mode image history fix + Activity-group compression boundary + named custom provider routing + quota chip Settings toggle + RFC docs) ### Added diff --git a/api/config.py b/api/config.py index ec1064de..d0508f2c 100644 --- a/api/config.py +++ b/api/config.py @@ -2957,6 +2957,13 @@ def get_available_models() -> dict: # A user may configure a provider key via config.yaml providers..api_key # without setting the corresponding env var. (#604) # + # Gating: only seed picker groups for keys whose canonical id is known + # to ``_PROVIDER_MODELS`` / ``_PROVIDER_DISPLAY``, or whose value is a + # dict-shaped provider config (custom/local). Scalar siblings under + # ``providers:`` (e.g. ``providers.only_configured: true``) are config + # flags, not providers, and must not render as phantom picker groups + # like ``Only-Configured`` (#2399). + # # Canonicalise the id slug here so a user with ``providers.opencode_go`` # (underscore variant) doesn't see TWO provider groups in the picker — # one for the canonical ``opencode-go`` from active_provider detection @@ -2969,13 +2976,24 @@ def get_available_models() -> dict: # provider_cfg values (#2245). _canonical_to_raw_provider_key: dict[str, str] = {} if isinstance(_cfg_providers, dict): - for _pid_key in _cfg_providers: + for _pid_key, _provider_cfg in _cfg_providers.items(): _canonical = _canonicalise_provider_id(_pid_key) if not _canonical: continue + + # See the gating comment on the block above. ``_PROVIDER_MODELS`` + # / ``_PROVIDER_DISPLAY`` membership accepts known providers and + # aliases; ``isinstance(_provider_cfg, dict)`` accepts custom + # entries that supply their own models/api_key/base_url. (#2399) + _is_known_provider = ( + _canonical in _PROVIDER_MODELS or _canonical in _PROVIDER_DISPLAY + ) + _is_provider_config = isinstance(_provider_cfg, dict) + if not (_is_known_provider or _is_provider_config): + continue + _canonical_to_raw_provider_key.setdefault(_canonical, _pid_key) - if _canonical in _PROVIDER_MODELS or _canonical in _cfg_providers or _pid_key in _cfg_providers: - detected_providers.add(_canonical) + detected_providers.add(_canonical) def _configured_provider_for_base_url(base_url: object) -> str: target = _normalize_base_url_for_match(base_url) diff --git a/tests/test_issue2399_provider_config_flags.py b/tests/test_issue2399_provider_config_flags.py new file mode 100644 index 00000000..d001096f --- /dev/null +++ b/tests/test_issue2399_provider_config_flags.py @@ -0,0 +1,79 @@ +"""Regression coverage for provider-level config flags in the model picker.""" + +import pathlib + +import api.config as config + + +def _reset_models_cache(): + config._available_models_cache = None + config._available_models_cache_ts = 0.0 + + +def _provider_ids(payload: dict) -> set[str]: + return {str(group.get("provider_id") or "") for group in payload.get("groups", [])} + + +def test_providers_only_configured_flag_does_not_create_picker_group(monkeypatch): + """providers.only_configured is a filter flag, not a provider id (#2399).""" + _reset_models_cache() + monkeypatch.setattr( + config, + "cfg", + { + "model": {"provider": "openai", "default": "gpt-4o-mini"}, + "providers": { + "only_configured": True, + "openai": {"models": ["gpt-4o-mini"]}, + }, + }, + raising=False, + ) + monkeypatch.setattr(config, "_cfg_has_in_memory_overrides", lambda: True) + monkeypatch.setattr( + config, + "_get_auth_store_path", + lambda: pathlib.Path("/tmp/hermes-webui-missing-auth-store-issue2399.json"), + ) + + try: + payload = config.get_available_models() + finally: + _reset_models_cache() + + provider_ids = _provider_ids(payload) + assert "openai" in provider_ids + assert "only-configured" not in provider_ids + assert all("Only-Configured" not in str(group.get("provider")) for group in payload["groups"]) + + +def test_unknown_scalar_provider_config_flags_are_ignored(monkeypatch): + """Unknown scalar siblings under providers must not seed phantom groups.""" + _reset_models_cache() + monkeypatch.setattr( + config, + "cfg", + { + "model": {"provider": "openai", "default": "gpt-4o-mini"}, + "providers": { + "future_toggle": "enabled", + "openai": {"models": ["gpt-4o-mini"]}, + }, + }, + raising=False, + ) + monkeypatch.setattr(config, "_cfg_has_in_memory_overrides", lambda: True) + monkeypatch.setattr( + config, + "_get_auth_store_path", + lambda: pathlib.Path("/tmp/hermes-webui-missing-auth-store-issue2399.json"), + ) + + try: + payload = config.get_available_models() + finally: + _reset_models_cache() + + provider_ids = _provider_ids(payload) + assert "openai" in provider_ids + assert "future-toggle" not in provider_ids