mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Merge 47b80b6a3d into 431ecd8920
This commit is contained in:
commit
5b0f29c70f
11 changed files with 352 additions and 11 deletions
|
|
@ -25553,7 +25553,7 @@
|
|||
},
|
||||
"/cursor/models": {
|
||||
"get": {
|
||||
"description": "OpenAI-compatible model listing for the Cursor BYOK base URL.\n\nClients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and\nverify models via `GET {base}/models` (the OpenAI SDK contract). Without this\nroute those requests fall through to the Cursor Cloud Agents passthrough, which\ndemands a Cursor API key and 401s, so key verification silently fails before any\nchat request is ever sent. Delegates to the standard `/v1/models` handler.",
|
||||
"description": "OpenAI-compatible model listing for the Cursor BYOK base URL.\n\nClients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and\nverify models via `GET {base}/models` (the OpenAI SDK contract). Without this\nroute those requests fall through to the Cursor Cloud Agents passthrough, which\ndemands a Cursor API key and 401s, so key verification silently fails before any\nchat request is ever sent. Delegates to the standard `/v1/models` handler with\nwildcard routes left out, since Cursor offers every listed id as a callable model.",
|
||||
"operationId": "cursor_model_list_cursor_models_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -25578,7 +25578,7 @@
|
|||
},
|
||||
"/cursor/v1/models": {
|
||||
"get": {
|
||||
"description": "OpenAI-compatible model listing for the Cursor BYOK base URL.\n\nClients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and\nverify models via `GET {base}/models` (the OpenAI SDK contract). Without this\nroute those requests fall through to the Cursor Cloud Agents passthrough, which\ndemands a Cursor API key and 401s, so key verification silently fails before any\nchat request is ever sent. Delegates to the standard `/v1/models` handler.",
|
||||
"description": "OpenAI-compatible model listing for the Cursor BYOK base URL.\n\nClients pointed at `<proxy>/cursor` as an OpenAI-compatible base URL resolve and\nverify models via `GET {base}/models` (the OpenAI SDK contract). Without this\nroute those requests fall through to the Cursor Cloud Agents passthrough, which\ndemands a Cursor API key and 401s, so key verification silently fails before any\nchat request is ever sent. Delegates to the standard `/v1/models` handler with\nwildcard routes left out, since Cursor offers every listed id as a callable model.",
|
||||
"operationId": "cursor_model_list_cursor_v1_models_get",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
|
|||
|
|
@ -2871,6 +2871,15 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase):
|
|||
"hidden model can still be called."
|
||||
),
|
||||
)
|
||||
model_list_return_wildcard_routes: bool | None = Field(
|
||||
None,
|
||||
description=(
|
||||
"When true, `/v1/models` lists wildcard routes such as `openai/*` next to the models they "
|
||||
"expand to, without the caller passing `return_wildcard_routes=true`. A request that passes "
|
||||
"`return_wildcard_routes=false` still leaves them out, as do the dashboard's model pickers "
|
||||
"and `/cursor/v1/models`."
|
||||
),
|
||||
)
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -11498,6 +11498,16 @@ async def _deployment_hidden_by_listing_callbacks(deployment: Deployment, user_a
|
|||
return listed_name in await _names_hidden_by_listing_callbacks(user_api_key_dict, (listed_name,))
|
||||
|
||||
|
||||
def _includes_wildcard_routes(return_wildcard_routes: bool | None, settings: Mapping[str, object]) -> bool:
|
||||
"""Whether a listing or lookup includes wildcard routes such as `openai/*`. A request's
|
||||
own `return_wildcard_routes` wins; without one, `model_list_return_wildcard_routes`
|
||||
counts as on exactly when the admin UI's switch shows it on (`true` or `"true"`)."""
|
||||
if return_wildcard_routes is not None:
|
||||
return return_wildcard_routes
|
||||
configured: Final = settings.get("model_list_return_wildcard_routes")
|
||||
return configured is True or configured == "true"
|
||||
|
||||
|
||||
@router.get("/v1/models", dependencies=[Depends(user_api_key_auth)], tags=["model management"])
|
||||
@router.get(
|
||||
"/models", dependencies=[Depends(user_api_key_auth)], tags=["model management"]
|
||||
|
|
@ -11505,7 +11515,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,
|
||||
|
|
@ -11520,6 +11530,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
|
||||
|
|
@ -11608,6 +11622,8 @@ async def model_list(
|
|||
|
||||
hidden_names: Final = blocked_names | unhealthy_names
|
||||
|
||||
include_wildcard_routes: Final = _includes_wildcard_routes(return_wildcard_routes, settings)
|
||||
|
||||
# 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
|
||||
|
|
@ -11631,7 +11647,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,
|
||||
|
|
@ -11691,7 +11707,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,
|
||||
)
|
||||
|
||||
|
|
@ -11755,6 +11771,7 @@ async def model_info(
|
|||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
team_id: str | None = None,
|
||||
healthy_only: bool | None = False,
|
||||
return_wildcard_routes: bool | None = None,
|
||||
):
|
||||
"""
|
||||
Retrieve information about a specific model accessible to your API key.
|
||||
|
|
@ -11789,7 +11806,7 @@ async def model_info(
|
|||
team_id=team_id,
|
||||
include_model_access_groups=False,
|
||||
only_model_access_groups=False,
|
||||
return_wildcard_routes=False,
|
||||
return_wildcard_routes=_includes_wildcard_routes(return_wildcard_routes, settings),
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
|
||||
|
|
@ -18176,6 +18193,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",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -478,11 +478,12 @@ async def cursor_model_list(
|
|||
verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
|
||||
route those requests fall through to the Cursor Cloud Agents passthrough, which
|
||||
demands a Cursor API key and 401s, so key verification silently fails before any
|
||||
chat request is ever sent. Delegates to the standard `/v1/models` handler.
|
||||
chat request is ever sent. Delegates to the standard `/v1/models` handler with
|
||||
wildcard routes left out, since Cursor offers every listed id as a callable model.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import model_list
|
||||
|
||||
return await model_list(user_api_key_dict=user_api_key_dict)
|
||||
return await model_list(user_api_key_dict=user_api_key_dict, return_wildcard_routes=False)
|
||||
|
||||
|
||||
@router.post(
|
||||
|
|
|
|||
|
|
@ -73,6 +73,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)"}
|
||||
|
|
|
|||
127
tests/e2e/management/test_model_list_wildcard_routes_e2e.py
Normal file
127
tests/e2e/management/test_model_list_wildcard_routes_e2e.py
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"""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 teardown puts back whatever the database held before, so
|
||||
the shared proxy ends the test the way it started. The other listings the suites run either pass
|
||||
return_wildcard_routes explicitly or only check that a named model is present, 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, Literal
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, ConfigDict, JsonValue, RootModel
|
||||
|
||||
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 ConfigListParams, 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: JsonValue
|
||||
config_type: str = "general_settings"
|
||||
|
||||
|
||||
class ConfigFieldDeleteBody(BaseModel):
|
||||
field_name: str
|
||||
config_type: str = "general_settings"
|
||||
|
||||
|
||||
class StoredField(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore")
|
||||
field_name: str
|
||||
field_value: JsonValue = None
|
||||
source: Literal["config", "db", "env", "default", "unset"] = "unset"
|
||||
|
||||
|
||||
class StoredFieldList(RootModel[tuple[StoredField, ...]]):
|
||||
pass
|
||||
|
||||
|
||||
def _write_setting(client: ManagementClient, value: JsonValue) -> None:
|
||||
_ = unwrap(
|
||||
client.proxy.transport.post(
|
||||
"/config/field/update",
|
||||
headers=client.proxy.transport.master,
|
||||
json=ConfigFieldUpdateBody(field_name=_SETTING, field_value=value),
|
||||
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 _stored_setting(client: ManagementClient) -> StoredField | None:
|
||||
fields: Final = unwrap(
|
||||
client.proxy.transport.get(
|
||||
"/config/list",
|
||||
headers=client.proxy.transport.master,
|
||||
params=ConfigListParams(config_type="general_settings"),
|
||||
response_type=StoredFieldList,
|
||||
)
|
||||
).root
|
||||
return next((field for field in fields if field.field_name == _SETTING and field.source == "db"), None)
|
||||
|
||||
|
||||
def _restore_setting(client: ManagementClient, stored: StoredField | None) -> None:
|
||||
if stored is None:
|
||||
_delete_setting(client)
|
||||
return
|
||||
_write_setting(client, stored.field_value)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
stored: Final = _stored_setting(client)
|
||||
resources.defer(lambda: _restore_setting(client, stored))
|
||||
_write_setting(client, False)
|
||||
_await_listing(client, pattern, NoBody(), listed=False)
|
||||
|
||||
_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)
|
||||
|
|
@ -1351,6 +1351,9 @@ def test_cursor_models_route_delegates_to_model_list():
|
|||
assert response.status_code == 200, f"{path}: {response.text}"
|
||||
assert response.json() == model_payload
|
||||
assert mock_model_list.call_count == 2
|
||||
assert all(call.kwargs["return_wildcard_routes"] is False for call in mock_model_list.call_args_list), (
|
||||
"Cursor offers every listed id, so model_list_return_wildcard_routes must not reach it"
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.pop(user_api_key_auth, None)
|
||||
|
||||
|
|
|
|||
|
|
@ -3108,3 +3108,139 @@ 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("setting", [True, "true"])
|
||||
@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, setting
|
||||
):
|
||||
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": setting}, 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"}, {}),
|
||||
({"model_list_return_wildcard_routes": 1}, {}),
|
||||
({"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
|
||||
|
||||
|
||||
async def _model_info_resolves(model_id: str, **query: bool) -> bool:
|
||||
from fastapi import HTTPException
|
||||
|
||||
try:
|
||||
response = await litellm.proxy.proxy_server.model_info(
|
||||
model_id=model_id,
|
||||
user_api_key_dict=UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.INTERNAL_USER),
|
||||
**query,
|
||||
)
|
||||
except HTTPException as e:
|
||||
if e.status_code != 404:
|
||||
raise
|
||||
return False
|
||||
return response["id"] == model_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"general_settings, query, wildcard_listed",
|
||||
[
|
||||
({}, {}, False),
|
||||
({"model_list_return_wildcard_routes": True}, {}, True),
|
||||
({"model_list_return_wildcard_routes": "true"}, {}, True),
|
||||
({"model_list_return_wildcard_routes": True}, {"return_wildcard_routes": False}, False),
|
||||
({}, {"return_wildcard_routes": True}, True),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_info_resolves_exactly_the_ids_model_list_lists(
|
||||
openai_wildcard_router, monkeypatch, general_settings, query, wildcard_listed
|
||||
):
|
||||
listed = await _listed_model_ids(monkeypatch, general_settings, LitellmUserRoles.INTERNAL_USER, **query)
|
||||
resolvable = [model_id for model_id in ("gpt-4o", "openai/*") if await _model_info_resolves(model_id, **query)]
|
||||
|
||||
assert ("openai/*" in listed) is wildcard_listed, listed
|
||||
assert resolvable == [model_id for model_id in ("gpt-4o", "openai/*") if model_id in listed], (listed, resolvable)
|
||||
|
||||
|
||||
@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"]]
|
||||
|
|
|
|||
|
|
@ -157,6 +157,35 @@ describe("modelInfoCall", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("modelAvailableCall", () => {
|
||||
let currentFetch: typeof global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
currentFetch = global.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = currentFetch;
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, "False"],
|
||||
[false, "False"],
|
||||
[true, "True"],
|
||||
])("sends return_wildcard_routes=%s as %s", async (returnWildcardRoutes, sent) => {
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ ok: true, text: vi.fn().mockResolvedValue(JSON.stringify({ data: [] })) } as any);
|
||||
global.fetch = mockFetch as any;
|
||||
|
||||
await Networking.modelAvailableCall("token", "user", "Admin", returnWildcardRoutes);
|
||||
|
||||
const parsed = new URL(mockFetch.mock.calls[0][0] as string, "http://example.com");
|
||||
expect(parsed.pathname).toBe("/models");
|
||||
expect(parsed.searchParams.get("return_wildcard_routes")).toBe(sent);
|
||||
});
|
||||
});
|
||||
|
||||
describe("daily activity helpers", () => {
|
||||
const startTime = new Date("2025-02-12T00:00:00.000Z");
|
||||
const endTime = new Date("2025-02-19T00:00:00.000Z");
|
||||
|
|
|
|||
|
|
@ -1916,7 +1916,7 @@ export const modelAvailableCall = async (
|
|||
accessToken,
|
||||
query: {
|
||||
include_model_access_groups: "True",
|
||||
return_wildcard_routes: return_wildcard_routes === true ? "True" : undefined,
|
||||
return_wildcard_routes: return_wildcard_routes === true ? "True" : "False",
|
||||
only_model_access_groups: only_model_access_groups === true ? "True" : undefined,
|
||||
team_id: teamID || undefined,
|
||||
scope: scope || undefined,
|
||||
|
|
|
|||
21
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
21
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -3761,7 +3761,8 @@ export interface paths {
|
|||
* verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
|
||||
* route those requests fall through to the Cursor Cloud Agents passthrough, which
|
||||
* demands a Cursor API key and 401s, so key verification silently fails before any
|
||||
* chat request is ever sent. Delegates to the standard `/v1/models` handler.
|
||||
* chat request is ever sent. Delegates to the standard `/v1/models` handler with
|
||||
* wildcard routes left out, since Cursor offers every listed id as a callable model.
|
||||
*/
|
||||
get: operations["cursor_model_list_cursor_models_get"];
|
||||
put?: never;
|
||||
|
|
@ -3787,7 +3788,8 @@ export interface paths {
|
|||
* verify models via `GET {base}/models` (the OpenAI SDK contract). Without this
|
||||
* route those requests fall through to the Cursor Cloud Agents passthrough, which
|
||||
* demands a Cursor API key and 401s, so key verification silently fails before any
|
||||
* chat request is ever sent. Delegates to the standard `/v1/models` handler.
|
||||
* chat request is ever sent. Delegates to the standard `/v1/models` handler with
|
||||
* wildcard routes left out, since Cursor offers every listed id as a callable model.
|
||||
*/
|
||||
get: operations["cursor_model_list_cursor_v1_models_get"];
|
||||
put?: never;
|
||||
|
|
@ -10086,6 +10088,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
|
||||
|
|
@ -20502,6 +20508,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
|
||||
|
|
@ -28973,6 +28983,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, `/v1/models` lists wildcard routes such as `openai/*` next to the models they expand to, without the caller passing `return_wildcard_routes=true`. A request that passes `return_wildcard_routes=false` still leaves them out, as do the dashboard's model pickers and `/cursor/v1/models`.
|
||||
*/
|
||||
model_list_return_wildcard_routes?: boolean | null;
|
||||
/**
|
||||
* Otel
|
||||
* @description [BETA] OpenTelemetry support - this might change, use with caution.
|
||||
|
|
@ -62172,6 +62187,7 @@ export interface operations {
|
|||
query?: {
|
||||
team_id?: string | null;
|
||||
healthy_only?: boolean | null;
|
||||
return_wildcard_routes?: boolean | null;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
|
|
@ -75764,6 +75780,7 @@ export interface operations {
|
|||
query?: {
|
||||
team_id?: string | null;
|
||||
healthy_only?: boolean | null;
|
||||
return_wildcard_routes?: boolean | null;
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue