mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
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.
This commit is contained in:
parent
b841d9a6c2
commit
811f1510ec
4 changed files with 130 additions and 0 deletions
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)"}
|
||||
|
|
|
|||
95
tests/e2e/management/test_model_list_wildcard_routes_e2e.py
Normal file
95
tests/e2e/management/test_model_list_wildcard_routes_e2e.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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"]]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue