mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
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
This commit is contained in:
parent
5ce28fb8e2
commit
4089198b06
14 changed files with 287 additions and 284 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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"},
|
||||
)
|
||||
|
|
@ -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:
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
137
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
137
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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<string, never>;
|
||||
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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue