docs(models): pin empty-allowlist semantics with regression coverage
Greptile flagged that `models: []` / `models: "[]"` falls through the non-empty allowlist gate and leaves the full live catalog selectable. The symptom is real, but implementing the ask (gate on declared-ness instead) measurably makes things worse: it emits an EMPTY picker and leaves the provider unselectable. Measured both branches against the real handler before deciding: models key absent declared=False -> [chat-a, chat-b, image-gen-1, audio-tts-1] models: [] declared=True -> [] EMPTY PICKER models: "[]" declared=True -> [] EMPTY PICKER models: [chat-a] declared=True -> [chat-a] An unselectable provider is a harder failure than a few extra visible models, and it is unrecoverable from the UI: `custom_providers` has no WebUI write path, so it is hand-edited in config.yaml. There is also no deny-all use case — a provider the user wants hidden is removed from `custom_providers` outright, not left behind with an empty allowlist. No production behavior change. Records the trade-off in the filter block and adds `test_empty_allowlist_is_treated_as_not_configured` (native empty list, serialized "[]", empty mapping, blank scalar) so a future "fix" of this edge case fails loudly instead of silently emptying the picker. Verified non-vacuous: applying the declared-ness gate fails all 4 new cases with `assert [] == [...]`.
This commit is contained in:
1 parent
4a1fb47ce5
commit
88236dbc26
2 files changed
+50
-1
No files matched your search
+13
-1
@@ -22016,12 +22016,24 @@ def _handle_live_models(handler, parsed):
|
||||
# providers (esp. New-API-style gateways) expose their ENTIRE
|
||||
# catalog via /v1/models — including image/audio models that
|
||||
# are not chat models. The picker must not surface models the
|
||||
# user never declared in config.yaml. Only an explicit
|
||||
# user never declared in config.yaml. Only a NON-EMPTY explicit
|
||||
# ``models`` allowlist gates the filter — a provider configured
|
||||
# with just a singular ``model`` (no ``models`` list) keeps the
|
||||
# unfiltered live catalog. When no allowlist is configured,
|
||||
# return the live list as-is (preserves the pre-filter
|
||||
# discovery behaviour).
|
||||
#
|
||||
# An empty allowlist (``models: []``, ``models: "[]"``, or a
|
||||
# value that decodes to no usable ids) is deliberately treated
|
||||
# as "not configured", NOT as "allow nothing". Gating on
|
||||
# declared-ness instead would emit an EMPTY picker and make the
|
||||
# provider unselectable — a harder failure than surfacing a few
|
||||
# extra models, and unrecoverable from the UI because
|
||||
# ``custom_providers`` is hand-edited in config.yaml (the WebUI
|
||||
# has no write path for it). ``[]`` in practice means a
|
||||
# leftover/placeholder key, not an intentional deny-all, and no
|
||||
# deny-all use case exists: a provider the user wants hidden is
|
||||
# removed from ``custom_providers`` outright.
|
||||
if ids:
|
||||
if _allowlist_ids:
|
||||
_allowlist_set = set(_allowlist_ids)
|
||||
|
||||
@@ -192,3 +192,40 @@ def test_probe_failure_falls_back_to_config_ids(monkeypatch):
|
||||
)
|
||||
|
||||
assert _ids(payload) == ["chat-default", "chat-a"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"models_value",
|
||||
[
|
||||
pytest.param([], id="native-empty-list"),
|
||||
pytest.param("[]", id="serialized-empty-list"),
|
||||
pytest.param({}, id="empty-mapping"),
|
||||
pytest.param(" ", id="blank-scalar"),
|
||||
],
|
||||
)
|
||||
def test_empty_allowlist_is_treated_as_not_configured(monkeypatch, models_value):
|
||||
"""An empty ``models`` value must NOT be read as "allow nothing".
|
||||
|
||||
Pins a deliberate semantic decision. Gating the filter on *declared-ness*
|
||||
rather than on a non-empty allowlist would make ``models: []`` emit an
|
||||
EMPTY picker, leaving the provider unselectable — a harder failure than
|
||||
surfacing a few extra models, and unrecoverable from the UI because
|
||||
``custom_providers`` has no WebUI write path (hand-edited config.yaml only).
|
||||
There is also no deny-all use case: a provider the user wants hidden is
|
||||
removed from ``custom_providers`` outright.
|
||||
|
||||
If someone later "fixes" the empty case into a deny-all gate, this test
|
||||
fails and forces the trade-off to be re-argued rather than silently shipped.
|
||||
"""
|
||||
import api.routes as routes
|
||||
|
||||
provider = dict(_BASE_PROVIDER, models=models_value)
|
||||
payload, _ = _run_live_models(
|
||||
monkeypatch, routes, provider, catalog=_LIVE_CATALOG
|
||||
)
|
||||
|
||||
assert _ids(payload) == _LIVE_CATALOG, (
|
||||
"empty allowlist must fall through to full live discovery, "
|
||||
"never collapse the picker to zero models"
|
||||
)
|
||||
assert _ids(payload), "picker must never be emptied by an empty allowlist"
|
||||
Reference in new issue
Block a user