From 253e5bbbf55820f9179ee0a4be9fab09ca54e274 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:13:33 +0000 Subject: [PATCH] fix(openapi): correct budget limits documentation --- .../internal_user_endpoints.py | 4 +- .../key_management_endpoints.py | 4 +- .../management_endpoints/team_endpoints.py | 4 +- .../proxy/test_openapi_schema_validation.py | 98 ++++++++++--------- ui/litellm-dashboard/src/lib/http/schema.d.ts | 12 +-- 5 files changed, 65 insertions(+), 57 deletions(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index ccd15a68437..930a50ec0de 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -388,7 +388,7 @@ async def new_user( - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts. - organizations: List[str] - List of organization id's the user is a member of - - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. Returns: - key: (str) The generated api key for the user - expires: (datetime) Datetime object for when key expires. @@ -1389,7 +1389,7 @@ async def user_update( - key_alias: Optional[str] - [NOT IMPLEMENTED]. - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts. - - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. """ try: diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index bf64f537c7f..a69ebc5009d 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1519,7 +1519,7 @@ async def generate_key_fn( - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings. - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"]. - - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. Examples: @@ -2543,7 +2543,7 @@ async def update_key_fn( - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings. - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"]. - - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. Example: ```bash diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 8ec7ec707a2..08189263a1d 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -955,7 +955,7 @@ async def new_team( - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"]. - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}. - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}. - - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models. Returns: @@ -1627,7 +1627,7 @@ async def update_team( - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"]. - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}. - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}. - - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models. ``` diff --git a/tests/test_litellm/proxy/test_openapi_schema_validation.py b/tests/test_litellm/proxy/test_openapi_schema_validation.py index b44edc8a3bc..10af5462382 100644 --- a/tests/test_litellm/proxy/test_openapi_schema_validation.py +++ b/tests/test_litellm/proxy/test_openapi_schema_validation.py @@ -11,6 +11,43 @@ Related issue: https://github.com/BerriAI/litellm/issues/21305 import pytest +class TestBudgetLimitsOpenAPISchema: + def test_descriptions_use_budget_limit_entry_field_names(self): + from fastapi.openapi.utils import get_openapi + + from litellm.proxy.management_endpoints.internal_user_endpoints import ( + router as user_router, + ) + from litellm.proxy.management_endpoints.key_management_endpoints import ( + router as key_router, + ) + from litellm.proxy.management_endpoints.team_endpoints import ( + router as team_router, + ) + + schema = get_openapi( + title="LiteLLM", + version="test", + routes=[*key_router.routes, *user_router.routes, *team_router.routes], + ) + + for path in ( + "/key/generate", + "/key/update", + "/user/new", + "/user/update", + "/team/new", + "/team/update", + ): + description = schema["paths"][path]["post"]["description"] + budget_limits_line = next(line for line in description.splitlines() if "budget_limits:" in line) + + assert "max_budget and budget_duration" in budget_limits_line + assert '{"budget_duration": "24h", "max_budget": 10.0}' in budget_limits_line + assert '"budget_limit"' not in budget_limits_line + assert '"time_period"' not in budget_limits_line + + class TestSpendCalculateOpenAPISchema: """Test /spend/calculate response schema is valid OpenAPI 3.x.""" @@ -22,9 +59,7 @@ class TestSpendCalculateOpenAPISchema: if hasattr(route, "path") and route.path == "/spend/calculate": responses = route.responses or {} response_200 = responses.get(200, {}) - assert ( - "description" in response_200 - ), "/spend/calculate 200 response must have a 'description' field" + assert "description" in response_200, "/spend/calculate 200 response must have a 'description' field" break else: pytest.fail("/spend/calculate route not found in router") @@ -43,9 +78,7 @@ class TestSpendCalculateOpenAPISchema: "top-level property - use 'content' wrapper instead" ) # Must have 'content' wrapper - assert ( - "content" in response_200 - ), "/spend/calculate 200 response must have a 'content' field" + assert "content" in response_200, "/spend/calculate 200 response must have a 'content' field" content = response_200["content"] assert "application/json" in content assert "schema" in content["application/json"] @@ -81,8 +114,7 @@ class TestCredentialEndpointsOpenAPISchema: by_name_endpoint = by_name_routes[0].endpoint by_model_endpoint = by_model_routes[0].endpoint assert by_name_endpoint is not by_model_endpoint, ( - "by_name and by_model must be separate handler functions " - "to avoid path parameter conflicts in OpenAPI spec" + "by_name and by_model must be separate handler functions to avoid path parameter conflicts in OpenAPI spec" ) def test_by_model_route_does_not_require_credential_name(self): @@ -97,9 +129,7 @@ class TestCredentialEndpointsOpenAPISchema: sig = inspect.signature(get_credential_by_model) param_names = list(sig.parameters.keys()) - assert ( - "credential_name" not in param_names - ), "get_credential_by_model must not have a credential_name parameter" + assert "credential_name" not in param_names, "get_credential_by_model must not have a credential_name parameter" def test_by_name_route_does_not_require_model_id(self): """ @@ -113,9 +143,7 @@ class TestCredentialEndpointsOpenAPISchema: sig = inspect.signature(get_credential_by_name) param_names = list(sig.parameters.keys()) - assert ( - "model_id" not in param_names - ), "get_credential_by_name must not have a model_id parameter" + assert "model_id" not in param_names, "get_credential_by_name must not have a model_id parameter" def test_by_model_has_model_id_path_param(self): """The by_model handler must accept model_id as a path parameter.""" @@ -125,9 +153,7 @@ class TestCredentialEndpointsOpenAPISchema: ) sig = inspect.signature(get_credential_by_model) - assert ( - "model_id" in sig.parameters - ), "get_credential_by_model must have a model_id parameter" + assert "model_id" in sig.parameters, "get_credential_by_model must have a model_id parameter" def test_by_name_has_credential_name_path_param(self): """The by_name handler must accept credential_name as a path parameter.""" @@ -137,9 +163,7 @@ class TestCredentialEndpointsOpenAPISchema: ) sig = inspect.signature(get_credential_by_name) - assert ( - "credential_name" in sig.parameters - ), "get_credential_by_name must have a credential_name parameter" + assert "credential_name" in sig.parameters, "get_credential_by_name must have a credential_name parameter" class TestWebSocketStubInjection: @@ -166,26 +190,16 @@ class TestWebSocketStubInjection: _inject_websocket_stubs_into_openapi_schema, ) - schema = { - "paths": { - "/v1/responses": { - "post": {"summary": "responses_api", "operationId": "responses_api"} - } - } - } + schema = {"paths": {"/v1/responses": {"post": {"summary": "responses_api", "operationId": "responses_api"}}}} ws_routes = [self._make_fake_ws_route("/v1/responses", name="responses_ws")] result = _inject_websocket_stubs_into_openapi_schema(schema, ws_routes) - assert ( - "post" in result["paths"]["/v1/responses"] - ), "POST operation must be preserved when a WebSocket route shares the path" - assert ( - result["paths"]["/v1/responses"]["post"]["operationId"] == "responses_api" + assert "post" in result["paths"]["/v1/responses"], ( + "POST operation must be preserved when a WebSocket route shares the path" ) - assert ( - "get" in result["paths"]["/v1/responses"] - ), "WebSocket stub should also be added under 'get'" + assert result["paths"]["/v1/responses"]["post"]["operationId"] == "responses_api" + assert "get" in result["paths"]["/v1/responses"], "WebSocket stub should also be added under 'get'" assert result["paths"]["/v1/responses"]["get"]["tags"] == ["WebSocket"] def test_websocket_stub_added_when_path_is_new(self): @@ -217,20 +231,14 @@ class TestWebSocketStubInjection: _inject_websocket_stubs_into_openapi_schema, ) - schema = { - "paths": { - "/health": { - "get": {"summary": "health_check", "operationId": "real_get"} - } - } - } + schema = {"paths": {"/health": {"get": {"summary": "health_check", "operationId": "real_get"}}}} ws_routes = [self._make_fake_ws_route("/health", name="health_ws")] result = _inject_websocket_stubs_into_openapi_schema(schema, ws_routes) - assert ( - result["paths"]["/health"]["get"]["operationId"] == "real_get" - ), "Real GET must take precedence over WebSocket stub" + assert result["paths"]["/health"]["get"]["operationId"] == "real_get", ( + "Real GET must take precedence over WebSocket stub" + ) def test_responses_post_routes_registered_on_router(self): """ diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 4ca2f85be2b..164fa0381d3 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -6531,7 +6531,7 @@ export interface paths { * - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. * - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings. * - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"]. - * - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. * * Examples: * @@ -6924,7 +6924,7 @@ export interface paths { * - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. * - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings. * - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"]. - * - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. * * Example: * ```bash @@ -13576,7 +13576,7 @@ export interface paths { * - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"]. * - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}. * - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}. - * - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. * - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models. * * Returns: @@ -13756,7 +13756,7 @@ export interface paths { * - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"]. * - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}. * - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}. - * - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. * - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models. * * ``` @@ -14636,7 +14636,7 @@ export interface paths { * - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. * - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts. * - organizations: List[str] - List of organization id's the user is a member of - * - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. * Returns: * - key: (str) The generated api key for the user * - expires: (datetime) Datetime object for when key expires. @@ -14716,7 +14716,7 @@ export interface paths { * - key_alias: Optional[str] - [NOT IMPLEMENTED]. * - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission. * - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts. - * - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}]. + * - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}]. */ post: operations["user_update_user_update_post"]; delete?: never;