From 811f1510ec3420cbc9a959e3ac72240fd20308c4 Mon Sep 17 00:00:00 2001 From: shrey kharbanda Date: Mon, 28 Sep 2026 13:40:56 -0700 Subject: [PATCH] feat(proxy): show model_list_return_wildcard_routes in the admin UI, add e2e Adding the field to _GENERAL_SETTINGS_CONFIG_LIST_FIELD_TYPES makes /config/list return it, so the General Settings page renders it as a toggle that saves through /config/field/update. The e2e test flips it on through that route, checks every replica lists the wildcard route in /v1/models and still leaves it out for return_wildcard_routes=false, then deletes it and checks the listing goes back. --- litellm/proxy/proxy_server.py | 1 + tests/e2e/coverage_registry/mgmt.yaml | 1 + .../test_model_list_wildcard_routes_e2e.py | 95 +++++++++++++++++++ tests/unit/proxy/test_proxy_server.py | 33 +++++++ 4 files changed, 130 insertions(+) create mode 100644 tests/e2e/management/test_model_list_wildcard_routes_e2e.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 9c45fd23b7a..9dd176cac15 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -18038,6 +18038,7 @@ _GENERAL_SETTINGS_CONFIG_LIST_FIELD_TYPES: Final[Mapping[str, str]] = MappingPro "apply_user_budget_to_team_keys": "Boolean", "user_api_key_cache_max_size": "Integer", "transcribe_media_buckets": "List", + "model_list_return_wildcard_routes": "Boolean", } ) diff --git a/tests/e2e/coverage_registry/mgmt.yaml b/tests/e2e/coverage_registry/mgmt.yaml index e1a840b1239..6fada6b992d 100644 --- a/tests/e2e/coverage_registry/mgmt.yaml +++ b/tests/e2e/coverage_registry/mgmt.yaml @@ -72,6 +72,7 @@ - {id: mgmt.callback.list.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "callback_management_endpoints.py", rationale: "Callback config (smoke)"} - {id: mgmt.cost_tracking.estimate.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "cost_tracking_settings.py", rationale: "Cost estimate (smoke)"} - {id: mgmt.router_settings.update.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "router_settings_endpoints.py", rationale: "Router config (smoke)"} +- {id: mgmt.general_settings.model_list_return_wildcard_routes.persists, module: mgmt, tier: P1, surface: api, assertions: [persists], source: "proxy_server.py:model_list", rationale: "The admin-set default lists wildcard routes in /v1/models on every replica, and return_wildcard_routes=false still leaves them out"} - {id: mgmt.jwt_key_mapping.new.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "jwt_key_mapping_endpoints.py", rationale: "JWT->key mapping (smoke)"} - {id: mgmt.compliance.gdpr.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "compliance_endpoints.py", rationale: "GDPR ops (smoke)"} - {id: mgmt.tool_management.list.happy_path, module: mgmt, tier: P2, surface: api, assertions: [happy_path], source: "tool_management_endpoints.py", rationale: "Tool inventory (smoke)"} diff --git a/tests/e2e/management/test_model_list_wildcard_routes_e2e.py b/tests/e2e/management/test_model_list_wildcard_routes_e2e.py new file mode 100644 index 00000000000..5aa81821d59 --- /dev/null +++ b/tests/e2e/management/test_model_list_wildcard_routes_e2e.py @@ -0,0 +1,95 @@ +"""Live e2e: `general_settings.model_list_return_wildcard_routes`, the proxy-wide +default for whether GET /v1/models lists a wildcard route such as `openai/*` next to +the models it expands to. + +The setting is written through /config/field/update, the route behind the admin UI's +General Settings toggle, and deleted on teardown so the shared proxy goes back to +leaving wildcard routes out. Every other listing the suites run passes +return_wildcard_routes explicitly, so the window with the setting on changes none of +them. The wildcard deployment gets a unique prefix instead of `openai/*`, which would +claim every `openai/...` request the other suites send. +""" + +from __future__ import annotations + +from typing import Final + +import pytest +from pydantic import BaseModel + +from e2e_config import unique_marker +from e2e_http import NoBody, Success, unwrap +from lifecycle import ResourceManager +from management_client import ManagementClient +from models import LiteLLMParamsBody, ModelsListParams, ModelsListResponse + +pytestmark = pytest.mark.e2e + +_SETTING: Final = "model_list_return_wildcard_routes" +_DUMMY_API_KEY: Final = "e2e-dummy-key" + + +class ConfigFieldUpdateBody(BaseModel): + field_name: str + field_value: bool + config_type: str = "general_settings" + + +class ConfigFieldDeleteBody(BaseModel): + field_name: str + config_type: str = "general_settings" + + +def _write_setting(client: ManagementClient, enabled: bool) -> None: + _ = unwrap( + client.proxy.transport.post( + "/config/field/update", + headers=client.proxy.transport.master, + json=ConfigFieldUpdateBody(field_name=_SETTING, field_value=enabled), + response_type=NoBody, + ) + ) + + +def _delete_setting(client: ManagementClient) -> None: + _ = unwrap( + client.proxy.transport.post( + "/config/field/delete", + headers=client.proxy.transport.master, + json=ConfigFieldDeleteBody(field_name=_SETTING), + response_type=NoBody, + ) + ) + + +def _await_listing(client: ManagementClient, pattern: str, query: BaseModel, *, listed: bool) -> None: + """Poll /v1/models under `query` on every replica until each one lists `pattern`, + or each one leaves it out, per `listed`.""" + _ = client.proxy.read_back_everywhere( + "/v1/models", + params=query, + response_type=ModelsListResponse, + converged=lambda result: ( + isinstance(result, Success) and any(entry.id == pattern for entry in result.data.data) is listed + ), + ) + + +class TestModelListWildcardRoutesSetting: + @pytest.mark.covers("mgmt.general_settings.model_list_return_wildcard_routes.persists") + def test_setting_lists_wildcard_route_unless_request_opts_out( + self, client: ManagementClient, resources: ResourceManager + ) -> None: + pattern = f"e2e-wildcard-{unique_marker()}/*" + model_id = client.proxy.create_model(pattern, LiteLLMParamsBody(model="openai/*", api_key=_DUMMY_API_KEY)) + resources.defer(lambda: client.proxy.delete_model(model_id)) + + _await_listing(client, pattern, NoBody(), listed=False) + + resources.defer(lambda: _delete_setting(client)) + _write_setting(client, True) + _await_listing(client, pattern, NoBody(), listed=True) + _await_listing(client, pattern, ModelsListParams(return_wildcard_routes=False), listed=False) + + _delete_setting(client) + _await_listing(client, pattern, NoBody(), listed=False) diff --git a/tests/unit/proxy/test_proxy_server.py b/tests/unit/proxy/test_proxy_server.py index 8c676e97c99..352b12f0f50 100644 --- a/tests/unit/proxy/test_proxy_server.py +++ b/tests/unit/proxy/test_proxy_server.py @@ -3169,3 +3169,36 @@ async def test_model_list_leaves_out_wildcard_routes_unless_enabled( assert "gpt-4o" in listed, listed assert "openai/*" not in listed, listed + + +@pytest.mark.asyncio +async def test_model_list_return_wildcard_routes_is_an_admin_ui_toggle(openai_wildcard_router, monkeypatch): + stored_general_settings: Final = {"model_list_return_wildcard_routes": True} + config_table: Final = MagicMock() + config_table.find_first = AsyncMock( + side_effect=lambda where: ( + MagicMock(param_value=stored_general_settings) if where["param_name"] == "general_settings" else None + ) + ) + ui_prisma_client: Final = MagicMock() + ui_prisma_client.db.litellm_config = config_table + monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", {}) + monkeypatch.setattr(litellm.proxy.proxy_server, "proxy_config", ProxyConfig()) + await litellm.proxy.proxy_server.proxy_config._update_general_settings(stored_general_settings) + + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", ui_prisma_client) + ui_fields: Final = { + field.field_name: field + for field in await litellm.proxy.proxy_server.get_config_list( + config_type="general_settings", + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.PROXY_ADMIN), + ) + } + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", None) + response: Final = await litellm.proxy.proxy_server.model_list( + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.INTERNAL_USER) + ) + + toggle: Final = ui_fields["model_list_return_wildcard_routes"] + assert (toggle.field_type, toggle.field_value, toggle.stored_in_db) == ("Boolean", True, True) + assert "openai/*" in [model["id"] for model in response["data"]]