From 4089198b064ae552ecbf5bcb6f803db9012e5fed Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sun, 12 Jul 2026 21:23:33 -0700 Subject: [PATCH] refactor(proxy): rename usage_endpoints to dashboard_ai and the Ask AI route to /dashboard/ai/chat The Ask AI feature lived under usage_endpoints and served /usage/ai/chat, which reads like the home for all usage/spend endpoints when it is really one AI chat feature that happens to sit on the Usage dashboard, and it collided in basename with the unrelated guardrails/usage_endpoints module. This renames the package to dashboard_ai and the route to /dashboard/ai/chat so it is positioned as the dashboard's AI endpoint rather than a usage-only one, ahead of extending it to more of the dashboard The chat-feature symbols move with it (usage_ai_chat to dashboard_ai_chat, UsageAIChatRequest to DashboardAIChatRequest, stream_usage_ai_chat to stream_dashboard_ai_chat). The scoped data-access layer keeps its usage-oriented names (ScopedUsageDataProvider, AdminScope, UserScope) because it still reads usage and spend data. The lazy-feature registration, the dashboard client URL, and the regenerated OpenAPI snapshot and dashboard types are updated to match The moved modules drop Optional[...] for the modern X | None form so they stay within the ruff strict budget, and ruff-strict-budget.json is ratcheted down by the violations this replacement cleared --- litellm/proxy/_lazy_features.py | 6 +- litellm/proxy/_lazy_openapi_snapshot.json | 300 +++++++++--------- .../dashboard_ai/__init__.py | 9 + .../agent.py | 28 +- .../endpoints.py | 24 +- .../scoped_data.py | 12 +- .../usage_endpoints/__init__.py | 9 - ruff-strict-budget.json | 6 +- .../__init__.py | 0 .../test_agent.py | 18 +- .../test_endpoints.py | 18 +- .../test_scoped_data.py | 2 +- .../src/components/networking.tsx | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 137 ++++---- 14 files changed, 287 insertions(+), 284 deletions(-) create mode 100644 litellm/proxy/management_endpoints/dashboard_ai/__init__.py rename litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/agent.py (95%) rename litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/endpoints.py (74%) rename litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/scoped_data.py (95%) delete mode 100644 litellm/proxy/management_endpoints/usage_endpoints/__init__.py rename tests/test_litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/__init__.py (100%) rename tests/test_litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/test_agent.py (95%) rename tests/test_litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/test_endpoints.py (74%) rename tests/test_litellm/proxy/management_endpoints/{usage_endpoints => dashboard_ai}/test_scoped_data.py (98%) diff --git a/litellm/proxy/_lazy_features.py b/litellm/proxy/_lazy_features.py index 1dda1f29fb9..afb79a8cc8b 100644 --- a/litellm/proxy/_lazy_features.py +++ b/litellm/proxy/_lazy_features.py @@ -229,9 +229,9 @@ LAZY_FEATURES: Tuple[LazyFeature, ...] = ( path_prefixes=("/vantage",), ), LazyFeature( - name="usage_ai", - module_path="litellm.proxy.management_endpoints.usage_endpoints", - path_prefixes=("/usage/ai",), + name="dashboard_ai", + module_path="litellm.proxy.management_endpoints.dashboard_ai", + path_prefixes=("/dashboard/ai",), ), LazyFeature( name="prompts", diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 0957f394698..e763da97d8f 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -6132,6 +6132,156 @@ } } }, + "dashboard_ai": { + "components": { + "schemas": { + "ChatMessage": { + "properties": { + "content": { + "title": "Content", + "type": "string" + }, + "role": { + "enum": [ + "user", + "assistant" + ], + "title": "Role", + "type": "string" + } + }, + "required": [ + "role", + "content" + ], + "title": "ChatMessage", + "type": "object" + }, + "DashboardAIChatRequest": { + "properties": { + "messages": { + "description": "Chat messages (user/assistant history)", + "items": { + "$ref": "#/components/schemas/ChatMessage" + }, + "title": "Messages", + "type": "array" + }, + "model": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Model group to use for AI chat", + "title": "Model" + } + }, + "required": [ + "messages" + ], + "title": "DashboardAIChatRequest", + "type": "object" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "title": "Detail", + "type": "array" + } + }, + "title": "HTTPValidationError", + "type": "object" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "title": "Location", + "type": "array" + }, + "msg": { + "title": "Message", + "type": "string" + }, + "type": { + "title": "Error Type", + "type": "string" + } + }, + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError", + "type": "object" + } + } + }, + "paths": { + "/dashboard/ai/chat": { + "post": { + "description": "AI chat about usage data. Streams SSE events with the AI response.\n\nThe agent queries aggregated daily activity data through a provider scoped\nto the caller: admins get a global view, non-admins are restricted to their\nown ``user_id``.", + "operationId": "dashboard_ai_chat_dashboard_ai_chat_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DashboardAIChatRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Dashboard Ai Chat", + "tags": [ + "usage_ai" + ] + } + } + } + }, "evals": { "components": { "schemas": { @@ -27576,156 +27726,6 @@ } } }, - "usage_ai": { - "components": { - "schemas": { - "ChatMessage": { - "properties": { - "content": { - "title": "Content", - "type": "string" - }, - "role": { - "enum": [ - "user", - "assistant" - ], - "title": "Role", - "type": "string" - } - }, - "required": [ - "role", - "content" - ], - "title": "ChatMessage", - "type": "object" - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "title": "Detail", - "type": "array" - } - }, - "title": "HTTPValidationError", - "type": "object" - }, - "UsageAIChatRequest": { - "properties": { - "messages": { - "description": "Chat messages (user/assistant history)", - "items": { - "$ref": "#/components/schemas/ChatMessage" - }, - "title": "Messages", - "type": "array" - }, - "model": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], - "description": "Model group to use for AI chat", - "title": "Model" - } - }, - "required": [ - "messages" - ], - "title": "UsageAIChatRequest", - "type": "object" - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "title": "Location", - "type": "array" - }, - "msg": { - "title": "Message", - "type": "string" - }, - "type": { - "title": "Error Type", - "type": "string" - } - }, - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError", - "type": "object" - } - } - }, - "paths": { - "/usage/ai/chat": { - "post": { - "description": "AI chat about usage data. Streams SSE events with the AI response.\n\nThe agent queries aggregated daily activity data through a provider scoped\nto the caller: admins get a global view, non-admins are restricted to their\nown ``user_id``.", - "operationId": "usage_ai_chat_usage_ai_chat_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UsageAIChatRequest" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": {} - } - }, - "description": "Successful Response" - }, - "422": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - }, - "description": "Validation Error" - } - }, - "security": [ - { - "APIKeyHeader": [] - } - ], - "summary": "Usage Ai Chat", - "tags": [ - "usage_ai" - ] - } - } - } - }, "vantage": { "components": { "schemas": { diff --git a/litellm/proxy/management_endpoints/dashboard_ai/__init__.py b/litellm/proxy/management_endpoints/dashboard_ai/__init__.py new file mode 100644 index 00000000000..01344ebee39 --- /dev/null +++ b/litellm/proxy/management_endpoints/dashboard_ai/__init__.py @@ -0,0 +1,9 @@ +""" +Dashboard AI endpoints package. + +Re-exports the router from endpoints module. +""" + +from litellm.proxy.management_endpoints.dashboard_ai.endpoints import ( # noqa: F401 + router, +) diff --git a/litellm/proxy/management_endpoints/usage_endpoints/agent.py b/litellm/proxy/management_endpoints/dashboard_ai/agent.py similarity index 95% rename from litellm/proxy/management_endpoints/usage_endpoints/agent.py rename to litellm/proxy/management_endpoints/dashboard_ai/agent.py index 50a91430674..4eb2f72a1ce 100644 --- a/litellm/proxy/management_endpoints/usage_endpoints/agent.py +++ b/litellm/proxy/management_endpoints/dashboard_ai/agent.py @@ -10,13 +10,18 @@ rate-limited, and guardrailed like any other proxy request. import json from dataclasses import dataclass from datetime import date -from typing import Any, AsyncIterator, Dict, List, Literal, Optional, Set, Union, cast +from typing import Any, AsyncIterator, Dict, List, Literal, Set, Union, cast from pydantic import BaseModel, TypeAdapter from typing_extensions import TypedDict, assert_never import litellm from litellm._logging import verbose_proxy_logger +from litellm.proxy.management_endpoints.dashboard_ai.scoped_data import ( + ScopedUsageDataProvider, + summarise_entity_data, + summarise_usage_data, +) from litellm.router import Router from litellm.types.llms.openai import AllMessageValues from litellm.types.utils import ( @@ -25,11 +30,6 @@ from litellm.types.utils import ( Message, ModelResponse, ) -from litellm.proxy.management_endpoints.usage_endpoints.scoped_data import ( - ScopedUsageDataProvider, - summarise_entity_data, - summarise_usage_data, -) USAGE_AI_TEMPERATURE = 0.2 MAX_CHAT_MESSAGES = 20 @@ -127,7 +127,7 @@ def _require_router() -> Router: return llm_router -def _assembled_message(chunks: List[object]) -> Optional[Message]: +def _assembled_message(chunks: List[object]) -> Message | None: """Reassemble streamed chunks into a single message (content + tool_calls).""" built = litellm.stream_chunk_builder(chunks) if not isinstance(built, ModelResponse) or not built.choices: @@ -136,7 +136,7 @@ def _assembled_message(chunks: List[object]) -> Optional[Message]: return choice.message if isinstance(choice, Choices) else None # pyright: ignore[reportUnnecessaryIsInstance] # choices[0] can be StreamingChoices at runtime -def resolve_model(requested: Optional[str]) -> Union[str, ModelNotConfigured]: +def resolve_model(requested: str | None) -> Union[str, ModelNotConfigured]: """Resolve the model group to use: explicit request wins, then the configured ``usage_ai_model`` setting, else an actionable error value.""" explicit = (requested or "").strip() @@ -145,7 +145,7 @@ def resolve_model(requested: Optional[str]) -> Union[str, ModelNotConfigured]: from litellm.proxy.proxy_server import general_settings - configured = TypeAdapter(Optional[str]).validate_python(general_settings.get(USAGE_AI_MODEL_SETTING)) + configured = TypeAdapter(str | None).validate_python(general_settings.get(USAGE_AI_MODEL_SETTING)) stripped = (configured or "").strip() return stripped or ModelNotConfigured() @@ -235,19 +235,19 @@ _TOOL_LABELS = { class _UsageArgs(BaseModel): start_date: str end_date: str - user_id: Optional[str] = None + user_id: str | None = None class _TeamArgs(BaseModel): start_date: str end_date: str - team_ids: Optional[str] = None + team_ids: str | None = None class _TagArgs(BaseModel): start_date: str end_date: str - tags: Optional[str] = None + tags: str | None = None async def _dispatch_tool(name: str, raw_args: Dict[str, Any], provider: ScopedUsageDataProvider) -> str: @@ -341,10 +341,10 @@ async def _run_tool_call( convo.append({"role": "tool", "tool_call_id": tc.id, "content": result}) -async def stream_usage_ai_chat( +async def stream_dashboard_ai_chat( provider: ScopedUsageDataProvider, messages: List[Dict[str, str]], - model: Optional[str] = None, + model: str | None = None, ) -> AsyncIterator[str]: """Stream SSE events: status -> tool_call -> chunk -> done (or a single error).""" resolved = resolve_model(model) diff --git a/litellm/proxy/management_endpoints/usage_endpoints/endpoints.py b/litellm/proxy/management_endpoints/dashboard_ai/endpoints.py similarity index 74% rename from litellm/proxy/management_endpoints/usage_endpoints/endpoints.py rename to litellm/proxy/management_endpoints/dashboard_ai/endpoints.py index 443936b5cc9..6f340a41dba 100644 --- a/litellm/proxy/management_endpoints/usage_endpoints/endpoints.py +++ b/litellm/proxy/management_endpoints/dashboard_ai/endpoints.py @@ -1,10 +1,10 @@ """ -USAGE AI CHAT ENDPOINT +DASHBOARD AI CHAT ENDPOINT -/usage/ai/chat - Stream AI chat responses about usage data +/dashboard/ai/chat - Stream AI chat responses about usage data """ -from typing import List, Literal, Optional +from typing import List, Literal from fastapi import APIRouter, Depends, Request from fastapi.responses import StreamingResponse @@ -21,18 +21,18 @@ class ChatMessage(BaseModel): content: str -class UsageAIChatRequest(BaseModel): +class DashboardAIChatRequest(BaseModel): messages: List[ChatMessage] = Field(..., description="Chat messages (user/assistant history)") - model: Optional[str] = Field(default=None, description="Model group to use for AI chat") + model: str | None = Field(default=None, description="Model group to use for AI chat") @router.post( - "/usage/ai/chat", + "/dashboard/ai/chat", tags=["Budget & Spend Tracking"], dependencies=[Depends(user_api_key_auth)], ) -async def usage_ai_chat( - data: UsageAIChatRequest, +async def dashboard_ai_chat( + data: DashboardAIChatRequest, request: Request, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): @@ -47,10 +47,10 @@ async def usage_ai_chat( from litellm.proxy.management_endpoints.common_utils import ( require_caller_user_id_for_non_admin, ) - from litellm.proxy.management_endpoints.usage_endpoints.agent import ( - stream_usage_ai_chat, + from litellm.proxy.management_endpoints.dashboard_ai.agent import ( + stream_dashboard_ai_chat, ) - from litellm.proxy.management_endpoints.usage_endpoints.scoped_data import ( + from litellm.proxy.management_endpoints.dashboard_ai.scoped_data import ( AdminScope, ScopedUsageDataProvider, UserScope, @@ -66,7 +66,7 @@ async def usage_ai_chat( messages = [{"role": m.role, "content": m.content} for m in data.messages] return StreamingResponse( - stream_usage_ai_chat(provider=provider, messages=messages, model=data.model), + stream_dashboard_ai_chat(provider=provider, messages=messages, model=data.model), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, ) diff --git a/litellm/proxy/management_endpoints/usage_endpoints/scoped_data.py b/litellm/proxy/management_endpoints/dashboard_ai/scoped_data.py similarity index 95% rename from litellm/proxy/management_endpoints/usage_endpoints/scoped_data.py rename to litellm/proxy/management_endpoints/dashboard_ai/scoped_data.py index 11e8611bac6..992d354775a 100644 --- a/litellm/proxy/management_endpoints/usage_endpoints/scoped_data.py +++ b/litellm/proxy/management_endpoints/dashboard_ai/scoped_data.py @@ -42,7 +42,7 @@ class AdminScope: """Global view. ``caller_user_id`` is the admin's own id (may be None) and is not used to filter; admins may optionally pass an explicit user filter.""" - caller_user_id: Optional[str] + caller_user_id: str | None @dataclass(frozen=True, slots=True) @@ -55,7 +55,7 @@ class UserScope: AiChatScope = Union[AdminScope, UserScope] -def _parse_csv(raw: Optional[str]) -> Optional[List[str]]: +def _parse_csv(raw: str | None) -> List[str] | None: if not raw: return None return [t.strip() for t in raw.split(",") if t.strip()] @@ -73,7 +73,7 @@ class ScopedUsageDataProvider: return isinstance(self._scope, AdminScope) async def usage( - self, start_date: str, end_date: str, user_id_filter: Optional[str] + self, start_date: str, end_date: str, user_id_filter: str | None ) -> SpendAnalyticsPaginatedResponse: scope = self._scope effective_user_id = scope.user_id if isinstance(scope, UserScope) else user_id_filter @@ -93,13 +93,13 @@ class ScopedUsageDataProvider: api_key=None, ) - async def team(self, start_date: str, end_date: str, team_ids: Optional[str]) -> SpendAnalyticsPaginatedResponse: + async def team(self, start_date: str, end_date: str, team_ids: str | None) -> SpendAnalyticsPaginatedResponse: self._require_admin("team usage") return await self._paginated( TABLE_DAILY_TEAM_SPEND, ENTITY_FIELD_TEAM, _parse_csv(team_ids), start_date, end_date ) - async def tag(self, start_date: str, end_date: str, tags: Optional[str]) -> SpendAnalyticsPaginatedResponse: + async def tag(self, start_date: str, end_date: str, tags: str | None) -> SpendAnalyticsPaginatedResponse: self._require_admin("tag usage") return await self._paginated(TABLE_DAILY_TAG_SPEND, ENTITY_FIELD_TAG, _parse_csv(tags), start_date, end_date) @@ -111,7 +111,7 @@ class ScopedUsageDataProvider: self, table_name: str, entity_id_field: str, - entity_id: Optional[List[str]], + entity_id: List[str] | None, start_date: str, end_date: str, ) -> SpendAnalyticsPaginatedResponse: diff --git a/litellm/proxy/management_endpoints/usage_endpoints/__init__.py b/litellm/proxy/management_endpoints/usage_endpoints/__init__.py deleted file mode 100644 index 6e68dcd2a2e..00000000000 --- a/litellm/proxy/management_endpoints/usage_endpoints/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Usage endpoints package. - -Re-exports the router from endpoints module. -""" - -from litellm.proxy.management_endpoints.usage_endpoints.endpoints import ( # noqa: F401 - router, -) diff --git a/ruff-strict-budget.json b/ruff-strict-budget.json index dcde6fd1641..11b1d70f65b 100644 --- a/ruff-strict-budget.json +++ b/ruff-strict-budget.json @@ -24,7 +24,7 @@ "limit": 130 }, "ANN401": { - "limit": 2075 + "limit": 2073 }, "ASYNC230": { "limit": 14 @@ -324,7 +324,7 @@ "limit": 883 }, "UP006": { - "limit": 12792 + "limit": 12780 }, "UP007": { "limit": 2570 @@ -363,6 +363,6 @@ "limit": 105 }, "UP045": { - "limit": 18462 + "limit": 18450 } } diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/__init__.py b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/__init__.py similarity index 100% rename from tests/test_litellm/proxy/management_endpoints/usage_endpoints/__init__.py rename to tests/test_litellm/proxy/management_endpoints/dashboard_ai/__init__.py diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_agent.py b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_agent.py similarity index 95% rename from tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_agent.py rename to tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_agent.py index 7c8442e6d66..6badedca02f 100644 --- a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_agent.py +++ b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_agent.py @@ -20,17 +20,17 @@ from litellm.types.utils import ( ModelResponseStream, StreamingChoices, ) -from litellm.proxy.management_endpoints.usage_endpoints import agent as agent_mod -from litellm.proxy.management_endpoints.usage_endpoints.agent import ( +from litellm.proxy.management_endpoints.dashboard_ai import agent as agent_mod +from litellm.proxy.management_endpoints.dashboard_ai.agent import ( LLMCallError, ModelNotConfigured, RouterUnavailable, _error_event, resolve_model, - stream_usage_ai_chat, + stream_dashboard_ai_chat, tools_for_role, ) -from litellm.proxy.management_endpoints.usage_endpoints.scoped_data import ( +from litellm.proxy.management_endpoints.dashboard_ai.scoped_data import ( AdminScope, ScopedUsageDataProvider, UserScope, @@ -98,7 +98,7 @@ async def _collect(provider, messages, model, router): ): mock_agg.return_value = _usage_response_mock() events = [] - async for raw in stream_usage_ai_chat(provider=provider, messages=messages, model=model): + async for raw in stream_dashboard_ai_chat(provider=provider, messages=messages, model=model): events.append(json.loads(raw.replace("data: ", "").strip())) return events, mock_agg @@ -184,7 +184,7 @@ class TestMultiRoundLoop: mock_paginated.return_value = _usage_response_mock() events = [ json.loads(raw.replace("data: ", "").strip()) - async for raw in stream_usage_ai_chat( + async for raw in stream_dashboard_ai_chat( provider=provider, messages=[{"role": "user", "content": "q"}], model="m" ) ] @@ -224,7 +224,7 @@ class TestErrorPaths: ): events = [ json.loads(raw.replace("data: ", "").strip()) - async for raw in stream_usage_ai_chat( + async for raw in stream_dashboard_ai_chat( provider=provider, messages=[{"role": "user", "content": "q"}], model=None ) ] @@ -240,7 +240,7 @@ class TestErrorPaths: with patch.object(agent_mod, "_require_router", side_effect=agent_mod._RouterUnavailableError()): events = [ json.loads(raw.replace("data: ", "").strip()) - async for raw in stream_usage_ai_chat( + async for raw in stream_dashboard_ai_chat( provider=provider, messages=[{"role": "user", "content": "q"}], model="m" ) ] @@ -257,7 +257,7 @@ class TestErrorPaths: with patch.object(agent_mod, "_require_router", return_value=broken_router): events = [ json.loads(raw.replace("data: ", "").strip()) - async for raw in stream_usage_ai_chat( + async for raw in stream_dashboard_ai_chat( provider=provider, messages=[{"role": "user", "content": "q"}], model="m" ) ] diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_endpoints.py b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_endpoints.py similarity index 74% rename from tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_endpoints.py rename to tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_endpoints.py index d0fd761dda5..dd77f430d6e 100644 --- a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_endpoints.py @@ -1,4 +1,4 @@ -"""Endpoint-boundary tests for /usage/ai/chat. +"""Endpoint-boundary tests for /dashboard/ai/chat. Security regression: a non-admin caller with user_id=None (a service-account key) must be rejected at the endpoint before any scope/provider is built, so it @@ -11,10 +11,10 @@ import pytest from fastapi import HTTPException from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth -from litellm.proxy.management_endpoints.usage_endpoints.endpoints import ( +from litellm.proxy.management_endpoints.dashboard_ai.endpoints import ( ChatMessage, - UsageAIChatRequest, - usage_ai_chat, + DashboardAIChatRequest, + dashboard_ai_chat, ) @@ -22,10 +22,10 @@ class TestServiceAccountGuard: @pytest.mark.asyncio async def test_non_admin_with_user_id_none_is_rejected(self): service_account_key = UserAPIKeyAuth(user_id=None, user_role=LitellmUserRoles.INTERNAL_USER) - body = UsageAIChatRequest(messages=[ChatMessage(role="user", content="hi")], model="m") + body = DashboardAIChatRequest(messages=[ChatMessage(role="user", content="hi")], model="m") with pytest.raises(HTTPException) as exc_info: - await usage_ai_chat(data=body, request=MagicMock(), user_api_key_dict=service_account_key) + await dashboard_ai_chat(data=body, request=MagicMock(), user_api_key_dict=service_account_key) assert exc_info.value.status_code == 403 assert "Service-account keys" in str(exc_info.value.detail) @@ -35,7 +35,7 @@ class TestScopeSelection: @pytest.mark.asyncio async def test_admin_caller_builds_admin_scope(self): admin_key = UserAPIKeyAuth(user_id="admin-1", user_role=LitellmUserRoles.PROXY_ADMIN) - body = UsageAIChatRequest(messages=[ChatMessage(role="user", content="hi")], model="m") + body = DashboardAIChatRequest(messages=[ChatMessage(role="user", content="hi")], model="m") captured = {} @@ -53,10 +53,10 @@ class TestScopeSelection: try: with pytest.MonkeyPatch.context() as mp: mp.setattr( - "litellm.proxy.management_endpoints.usage_endpoints.agent.stream_usage_ai_chat", + "litellm.proxy.management_endpoints.dashboard_ai.agent.stream_dashboard_ai_chat", _fake_stream, ) - response = await usage_ai_chat(data=body, request=MagicMock(), user_api_key_dict=admin_key) + response = await dashboard_ai_chat(data=body, request=MagicMock(), user_api_key_dict=admin_key) # Drain the streaming body so the generator runs. async for _ in response.body_iterator: pass diff --git a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_scoped_data.py b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_scoped_data.py similarity index 98% rename from tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_scoped_data.py rename to tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_scoped_data.py index 9576e0ade6a..1c57c31822b 100644 --- a/tests/test_litellm/proxy/management_endpoints/usage_endpoints/test_scoped_data.py +++ b/tests/test_litellm/proxy/management_endpoints/dashboard_ai/test_scoped_data.py @@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from litellm.proxy.management_endpoints.usage_endpoints.scoped_data import ( +from litellm.proxy.management_endpoints.dashboard_ai.scoped_data import ( AdminScope, ScopedUsageDataProvider, UserScope, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 10d7f12604d..9085d8aa0bc 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -4187,7 +4187,7 @@ export const usageAiChatStream = async ( onToolCall?: (event: UsageAiToolCallEvent) => void, signal?: AbortSignal, ) => { - const url = proxyBaseUrl ? `${proxyBaseUrl}/usage/ai/chat` : `/usage/ai/chat`; + const url = proxyBaseUrl ? `${proxyBaseUrl}/dashboard/ai/chat` : `/dashboard/ai/chat`; const response = await fetch(url, { method: "POST", diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 46b9fe68ec1..797d30822c1 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -3078,6 +3078,30 @@ export interface paths { patch?: never; trace?: never; }; + "/dashboard/ai/chat": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Dashboard Ai Chat + * @description AI chat about usage data. Streams SSE events with the AI response. + * + * The agent queries aggregated daily activity data through a provider scoped + * to the caller: admins get a global view, non-admins are restricted to their + * own ``user_id``. + */ + post: operations["dashboard_ai_chat_dashboard_ai_chat_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/debug/asyncio-tasks": { parameters: { query?: never; @@ -14355,27 +14379,6 @@ export interface paths { patch?: never; trace?: never; }; - "/usage/ai/chat": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** - * Usage Ai Chat - * @description AI chat about usage data. Streams SSE events with the AI response. - * The AI agent has access to tools that query aggregated daily activity data. - */ - post: operations["usage_ai_chat_usage_ai_chat_post"]; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/user/available_roles": { parameters: { query?: never; @@ -23254,6 +23257,19 @@ export interface components { * DefaultInternalUserParams * @description Default parameters to apply when a new user signs in via SSO or is created on the /user/new API endpoint */ + /** DashboardAIChatRequest */ + DashboardAIChatRequest: { + /** + * Messages + * @description Chat messages (user/assistant history) + */ + messages: components["schemas"]["ChatMessage"][]; + /** + * Model + * @description Model group to use for AI chat + */ + model?: string | null; + }; DefaultInternalUserParams: { /** * Budget Duration @@ -32601,19 +32617,6 @@ export interface components { /** User Role */ user_role?: ("proxy_admin" | "proxy_admin_viewer" | "internal_user" | "internal_user_viewer") | null; }; - /** UsageAIChatRequest */ - UsageAIChatRequest: { - /** - * Messages - * @description Chat messages (user/assistant history) - */ - messages: components["schemas"]["ChatMessage"][]; - /** - * Model - * @description Model group to use for AI chat - */ - model?: string | null; - }; /** UsageDetailResponse */ UsageDetailResponse: { /** Avglatency */ @@ -33682,6 +33685,39 @@ export interface components { } export type $defs = Record; export interface operations { + dashboard_ai_chat_dashboard_ai_chat_post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DashboardAIChatRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": unknown; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; home__get: { parameters: { query?: never; @@ -51294,39 +51330,6 @@ export interface operations { }; }; }; - usage_ai_chat_usage_ai_chat_post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - "application/json": components["schemas"]["UsageAIChatRequest"]; - }; - }; - responses: { - /** @description Successful Response */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": unknown; - }; - }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; - }; - }; ui_get_available_role_user_available_roles_get: { parameters: { query?: never;