diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 3d8d701d15b..41a9341c6cf 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2856,6 +2856,14 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): "hidden model can still be called." ), ) + model_list_return_wildcard_routes: bool | None = Field( + None, + description=( + "When true, `/models` lists wildcard routes such as `openai/*` next to the models they " + "expand to, for every caller, without needing `return_wildcard_routes=true` per request. " + "A request can still pass `return_wildcard_routes=false` to leave them out." + ), + ) alerting: list | None = Field( None, description="List of alerting integrations - e.g. `alerting: ['slack', 'webhook', 'email']`. 'slack' posts Slack-format messages to any Slack-compatible webhook (Slack, Rocket.Chat, Mattermost); 'webhook' posts structured JSON budget alerts to WEBHOOK_URL", diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index ae559f30857..9c45fd23b7a 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -11349,7 +11349,7 @@ async def _deployment_hidden_by_listing_callbacks(deployment: Deployment, user_a async def model_list( request: Request = None, # pyright: ignore[reportArgumentType] # FastAPI always injects the Request; the None default only serves direct in-process callers user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), - return_wildcard_routes: bool | None = False, + return_wildcard_routes: bool | None = None, team_id: str | None = None, include_model_access_groups: bool | None = False, only_model_access_groups: bool | None = False, @@ -11364,6 +11364,10 @@ async def model_list( This is just for compatibility with openai projects like aider. Query Parameters: + - return_wildcard_routes: When true, also list wildcard routes (e.g. `openai/*`) + next to the models they expand to. Defaults to + `general_settings.model_list_return_wildcard_routes`, which is + false unless set; pass `false` to leave them out regardless. - include_metadata: Include additional metadata in the response with fallback information - fallback_type: Type of fallbacks to include ("general", "context_window", "content_policy") Defaults to "general" when include_metadata=true @@ -11452,6 +11456,12 @@ async def model_list( hidden_names: Final = blocked_names | unhealthy_names + include_wildcard_routes: Final = ( + settings.get("model_list_return_wildcard_routes") is True + if return_wildcard_routes is None + else return_wildcard_routes + ) + # If scope=expand and user has admin privileges, return all proxy models if should_expand_scope: # Get all proxy models as if user is a proxy admin @@ -11475,7 +11485,7 @@ async def model_list( proxy_model_list=proxy_model_list, user_model=None, infer_model_from_keys=False, - return_wildcard_routes=return_wildcard_routes or False, + return_wildcard_routes=include_wildcard_routes, llm_router=llm_router, model_access_groups=model_access_groups, include_model_access_groups=include_model_access_groups or False, @@ -11535,7 +11545,7 @@ async def model_list( team_id=team_id, include_model_access_groups=include_model_access_groups or False, only_model_access_groups=only_model_access_groups or False, - return_wildcard_routes=return_wildcard_routes or False, + return_wildcard_routes=include_wildcard_routes, user_api_key_cache=user_api_key_cache, ) diff --git a/tests/unit/proxy/test_proxy_server.py b/tests/unit/proxy/test_proxy_server.py index 8947da4d9fc..8c676e97c99 100644 --- a/tests/unit/proxy/test_proxy_server.py +++ b/tests/unit/proxy/test_proxy_server.py @@ -3108,3 +3108,64 @@ def test_get_litellm_model_info(data): ): get_litellm_model_info(model=model) get_info_mock.assert_called_once_with(data["expected"]) + + +@pytest.fixture +def openai_wildcard_router(monkeypatch: pytest.MonkeyPatch) -> litellm.Router: + router = litellm.Router( + model_list=[ + {"model_name": "gpt-4o", "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-fake"}}, + {"model_name": "openai/*", "litellm_params": {"model": "openai/*", "api_key": "sk-fake"}}, + ] + ) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_router", router) + monkeypatch.setattr(litellm.proxy.proxy_server, "llm_model_list", router.model_list) + monkeypatch.setattr(litellm.proxy.proxy_server, "prisma_client", None) + monkeypatch.setattr(litellm.proxy.proxy_server, "user_model", None) + return router + + +async def _listed_model_ids( + monkeypatch: pytest.MonkeyPatch, + general_settings: dict[str, object], + user_role: LitellmUserRoles, + **query: str | bool, +) -> list[str]: + monkeypatch.setattr(litellm.proxy.proxy_server, "general_settings", general_settings) + response = await litellm.proxy.proxy_server.model_list( + user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_role=user_role), **query + ) + return [model["id"] for model in response["data"]] + + +@pytest.mark.parametrize( + "user_role, query", + [(LitellmUserRoles.INTERNAL_USER, {}), (LitellmUserRoles.PROXY_ADMIN, {"scope": "expand"})], +) +@pytest.mark.asyncio +async def test_model_list_return_wildcard_routes_setting_matches_query_param( + openai_wildcard_router, monkeypatch, user_role, query +): + requested = await _listed_model_ids(monkeypatch, {}, user_role, return_wildcard_routes=True, **query) + from_setting = await _listed_model_ids(monkeypatch, {"model_list_return_wildcard_routes": True}, user_role, **query) + + assert "openai/*" in from_setting, from_setting + assert from_setting == requested + + +@pytest.mark.parametrize( + "general_settings, query", + [ + ({}, {}), + ({"model_list_return_wildcard_routes": "false"}, {}), + ({"model_list_return_wildcard_routes": True}, {"return_wildcard_routes": False}), + ], +) +@pytest.mark.asyncio +async def test_model_list_leaves_out_wildcard_routes_unless_enabled( + openai_wildcard_router, monkeypatch, general_settings, query +): + listed = await _listed_model_ids(monkeypatch, general_settings, LitellmUserRoles.INTERNAL_USER, **query) + + assert "gpt-4o" in listed, listed + assert "openai/*" not in listed, listed diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 16325970ed1..d0becba1ba1 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -9779,6 +9779,10 @@ export interface paths { * This is just for compatibility with openai projects like aider. * * Query Parameters: + * - return_wildcard_routes: When true, also list wildcard routes (e.g. `openai/*`) + * next to the models they expand to. Defaults to + * `general_settings.model_list_return_wildcard_routes`, which is + * false unless set; pass `false` to leave them out regardless. * - include_metadata: Include additional metadata in the response with fallback information * - fallback_type: Type of fallbacks to include ("general", "context_window", "content_policy") * Defaults to "general" when include_metadata=true @@ -20059,6 +20063,10 @@ export interface paths { * This is just for compatibility with openai projects like aider. * * Query Parameters: + * - return_wildcard_routes: When true, also list wildcard routes (e.g. `openai/*`) + * next to the models they expand to. Defaults to + * `general_settings.model_list_return_wildcard_routes`, which is + * false unless set; pass `false` to leave them out regardless. * - include_metadata: Include additional metadata in the response with fallback information * - fallback_type: Type of fallbacks to include ("general", "context_window", "content_policy") * Defaults to "general" when include_metadata=true @@ -28379,6 +28387,11 @@ export interface components { * @description When true, `/models`, `/v1/models/{id}` and `/model/info` hide models whose backing deployments are all unhealthy, for every caller, without needing `healthy_only=true` per request. Requires `background_health_checks: true`, and keeps deployment health state cached without turning on `enable_health_check_routing`, so routing is unaffected. With no health state nothing is hidden. Hiding is presentation-only, a hidden model can still be called. */ model_list_healthy_only?: boolean | null; + /** + * Model List Return Wildcard Routes + * @description When true, `/models` lists wildcard routes such as `openai/*` next to the models they expand to, for every caller, without needing `return_wildcard_routes=true` per request. A request can still pass `return_wildcard_routes=false` to leave them out. + */ + model_list_return_wildcard_routes?: boolean | null; /** * Otel * @description [BETA] OpenTelemetry support - this might change, use with caution.