mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +00:00
(UI) - Allow Internal Users to View their own logs (#8933)
* ui fix leftnav, allow internal users to view their own logs * pass user_id in uiSpendLogs call * ui filter logs for internal user * fix internal users page * ui show correct message when store prompts is disabled * fix internal user logs * test_ui_view_spend_logs_with_user_id * test spend management endpoint
This commit is contained in:
parent
de008bc67f
commit
7c8e37fc84
6 changed files with 479 additions and 15 deletions
|
@ -0,0 +1,402 @@
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
sys.path.insert(
|
||||||
|
0, os.path.abspath("../../../..")
|
||||||
|
) # Adds the parent directory to the system path
|
||||||
|
|
||||||
|
from litellm.proxy.proxy_server import app, prisma_client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client():
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_view_spend_logs_with_user_id(client, monkeypatch):
|
||||||
|
# Mock data for the test
|
||||||
|
mock_spend_logs = [
|
||||||
|
{
|
||||||
|
"id": "log1",
|
||||||
|
"request_id": "req1",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_1",
|
||||||
|
"team_id": "team1",
|
||||||
|
"spend": 0.05,
|
||||||
|
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log2",
|
||||||
|
"request_id": "req2",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_2",
|
||||||
|
"team_id": "team1",
|
||||||
|
"spend": 0.10,
|
||||||
|
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||||
|
"model": "gpt-4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create a mock prisma client
|
||||||
|
class MockDB:
|
||||||
|
async def find_many(self, *args, **kwargs):
|
||||||
|
# Filter based on user_id in the where conditions
|
||||||
|
print("kwargs to find_many", json.dumps(kwargs, indent=4))
|
||||||
|
if (
|
||||||
|
"where" in kwargs
|
||||||
|
and "user" in kwargs["where"]
|
||||||
|
and kwargs["where"]["user"] == "test_user_1"
|
||||||
|
):
|
||||||
|
return [mock_spend_logs[0]]
|
||||||
|
return mock_spend_logs
|
||||||
|
|
||||||
|
async def count(self, *args, **kwargs):
|
||||||
|
# Return count based on user_id filter
|
||||||
|
if (
|
||||||
|
"where" in kwargs
|
||||||
|
and "user" in kwargs["where"]
|
||||||
|
and kwargs["where"]["user"] == "test_user_1"
|
||||||
|
):
|
||||||
|
return 1
|
||||||
|
return len(mock_spend_logs)
|
||||||
|
|
||||||
|
class MockPrismaClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.db = MockDB()
|
||||||
|
self.db.litellm_spendlogs = self.db
|
||||||
|
|
||||||
|
# Apply the monkeypatch to replace the prisma_client
|
||||||
|
mock_prisma_client = MockPrismaClient()
|
||||||
|
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||||
|
|
||||||
|
# Set up test dates
|
||||||
|
start_date = (
|
||||||
|
datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7)
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Make the request with user_id filter
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
params={
|
||||||
|
"user_id": "test_user_1",
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
headers={"Authorization": "Bearer sk-test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Verify the response structure
|
||||||
|
assert "data" in data
|
||||||
|
assert "total" in data
|
||||||
|
assert "page" in data
|
||||||
|
assert "page_size" in data
|
||||||
|
assert "total_pages" in data
|
||||||
|
|
||||||
|
# Verify the filtered data
|
||||||
|
assert data["total"] == 1
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["user"] == "test_user_1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_view_spend_logs_with_team_id(client, monkeypatch):
|
||||||
|
# Mock data for the test
|
||||||
|
mock_spend_logs = [
|
||||||
|
{
|
||||||
|
"id": "log1",
|
||||||
|
"request_id": "req1",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_1",
|
||||||
|
"team_id": "team1",
|
||||||
|
"spend": 0.05,
|
||||||
|
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log2",
|
||||||
|
"request_id": "req2",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_2",
|
||||||
|
"team_id": "team2",
|
||||||
|
"spend": 0.10,
|
||||||
|
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||||
|
"model": "gpt-4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create a mock prisma client
|
||||||
|
class MockDB:
|
||||||
|
async def find_many(self, *args, **kwargs):
|
||||||
|
# Filter based on team_id in the where conditions
|
||||||
|
if (
|
||||||
|
"where" in kwargs
|
||||||
|
and "team_id" in kwargs["where"]
|
||||||
|
and kwargs["where"]["team_id"] == "team1"
|
||||||
|
):
|
||||||
|
return [mock_spend_logs[0]]
|
||||||
|
return mock_spend_logs
|
||||||
|
|
||||||
|
async def count(self, *args, **kwargs):
|
||||||
|
# Return count based on team_id filter
|
||||||
|
if (
|
||||||
|
"where" in kwargs
|
||||||
|
and "team_id" in kwargs["where"]
|
||||||
|
and kwargs["where"]["team_id"] == "team1"
|
||||||
|
):
|
||||||
|
return 1
|
||||||
|
return len(mock_spend_logs)
|
||||||
|
|
||||||
|
class MockPrismaClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.db = MockDB()
|
||||||
|
self.db.litellm_spendlogs = self.db
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
mock_prisma_client = MockPrismaClient()
|
||||||
|
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||||
|
|
||||||
|
# Set up test dates
|
||||||
|
start_date = (
|
||||||
|
datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7)
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Make the request with team_id filter
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
params={
|
||||||
|
"team_id": "team1",
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
headers={"Authorization": "Bearer sk-test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Verify the filtered data
|
||||||
|
assert data["total"] == 1
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["team_id"] == "team1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_view_spend_logs_pagination(client, monkeypatch):
|
||||||
|
# Create a larger set of mock data for pagination testing
|
||||||
|
mock_spend_logs = [
|
||||||
|
{
|
||||||
|
"id": f"log{i}",
|
||||||
|
"request_id": f"req{i}",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": f"test_user_{i % 3}",
|
||||||
|
"team_id": f"team{i % 2 + 1}",
|
||||||
|
"spend": 0.05 * i,
|
||||||
|
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
|
||||||
|
"model": "gpt-3.5-turbo" if i % 2 == 0 else "gpt-4",
|
||||||
|
}
|
||||||
|
for i in range(1, 26) # 25 records
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create a mock prisma client with pagination support
|
||||||
|
class MockDB:
|
||||||
|
async def find_many(self, *args, **kwargs):
|
||||||
|
# Handle pagination
|
||||||
|
skip = kwargs.get("skip", 0)
|
||||||
|
take = kwargs.get("take", 10)
|
||||||
|
return mock_spend_logs[skip : skip + take]
|
||||||
|
|
||||||
|
async def count(self, *args, **kwargs):
|
||||||
|
return len(mock_spend_logs)
|
||||||
|
|
||||||
|
class MockPrismaClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.db = MockDB()
|
||||||
|
self.db.litellm_spendlogs = self.db
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
mock_prisma_client = MockPrismaClient()
|
||||||
|
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||||
|
|
||||||
|
# Set up test dates
|
||||||
|
start_date = (
|
||||||
|
datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7)
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Test first page
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
params={
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 10,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
headers={"Authorization": "Bearer sk-test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 25
|
||||||
|
assert len(data["data"]) == 10
|
||||||
|
assert data["page"] == 1
|
||||||
|
assert data["page_size"] == 10
|
||||||
|
assert data["total_pages"] == 3
|
||||||
|
|
||||||
|
# Test second page
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
params={
|
||||||
|
"page": 2,
|
||||||
|
"page_size": 10,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
headers={"Authorization": "Bearer sk-test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 25
|
||||||
|
assert len(data["data"]) == 10
|
||||||
|
assert data["page"] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_view_spend_logs_date_range_filter(client, monkeypatch):
|
||||||
|
# Create mock data with different dates
|
||||||
|
today = datetime.datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
mock_spend_logs = [
|
||||||
|
{
|
||||||
|
"id": "log1",
|
||||||
|
"request_id": "req1",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_1",
|
||||||
|
"team_id": "team1",
|
||||||
|
"spend": 0.05,
|
||||||
|
"startTime": (today - datetime.timedelta(days=10)).isoformat(),
|
||||||
|
"model": "gpt-3.5-turbo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "log2",
|
||||||
|
"request_id": "req2",
|
||||||
|
"api_key": "sk-test-key",
|
||||||
|
"user": "test_user_2",
|
||||||
|
"team_id": "team1",
|
||||||
|
"spend": 0.10,
|
||||||
|
"startTime": (today - datetime.timedelta(days=2)).isoformat(),
|
||||||
|
"model": "gpt-4",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create a mock prisma client with date filtering
|
||||||
|
class MockDB:
|
||||||
|
async def find_many(self, *args, **kwargs):
|
||||||
|
# Check for date range filtering
|
||||||
|
if "where" in kwargs and "startTime" in kwargs["where"]:
|
||||||
|
date_filters = kwargs["where"]["startTime"]
|
||||||
|
filtered_logs = []
|
||||||
|
|
||||||
|
for log in mock_spend_logs:
|
||||||
|
log_date = datetime.datetime.fromisoformat(
|
||||||
|
log["startTime"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply gte filter if it exists
|
||||||
|
if "gte" in date_filters:
|
||||||
|
# Handle ISO format date strings
|
||||||
|
if "T" in date_filters["gte"]:
|
||||||
|
filter_date = datetime.datetime.fromisoformat(
|
||||||
|
date_filters["gte"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
filter_date = datetime.datetime.strptime(
|
||||||
|
date_filters["gte"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
if log_date < filter_date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Apply lte filter if it exists
|
||||||
|
if "lte" in date_filters:
|
||||||
|
# Handle ISO format date strings
|
||||||
|
if "T" in date_filters["lte"]:
|
||||||
|
filter_date = datetime.datetime.fromisoformat(
|
||||||
|
date_filters["lte"].replace("Z", "+00:00")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
filter_date = datetime.datetime.strptime(
|
||||||
|
date_filters["lte"], "%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
if log_date > filter_date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered_logs.append(log)
|
||||||
|
|
||||||
|
return filtered_logs
|
||||||
|
|
||||||
|
return mock_spend_logs
|
||||||
|
|
||||||
|
async def count(self, *args, **kwargs):
|
||||||
|
# For simplicity, we'll just call find_many and count the results
|
||||||
|
logs = await self.find_many(*args, **kwargs)
|
||||||
|
return len(logs)
|
||||||
|
|
||||||
|
class MockPrismaClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.db = MockDB()
|
||||||
|
self.db.litellm_spendlogs = self.db
|
||||||
|
|
||||||
|
# Apply the monkeypatch
|
||||||
|
mock_prisma_client = MockPrismaClient()
|
||||||
|
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
|
||||||
|
|
||||||
|
# Test with a date range that should only include the second log
|
||||||
|
start_date = (today - datetime.timedelta(days=5)).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
end_date = today.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
params={
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
headers={"Authorization": "Bearer sk-test"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["total"] == 1
|
||||||
|
assert len(data["data"]) == 1
|
||||||
|
assert data["data"][0]["id"] == "log2"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ui_view_spend_logs_unauthorized(client):
|
||||||
|
# Test without authorization header
|
||||||
|
response = client.get("/spend/logs/ui")
|
||||||
|
assert response.status_code == 401 or response.status_code == 403
|
||||||
|
|
||||||
|
# Test with invalid authorization
|
||||||
|
response = client.get(
|
||||||
|
"/spend/logs/ui",
|
||||||
|
headers={"Authorization": "Bearer invalid-token"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401 or response.status_code == 403
|
|
@ -20,6 +20,7 @@ import {
|
||||||
SafetyOutlined,
|
SafetyOutlined,
|
||||||
ExperimentOutlined
|
ExperimentOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { old_admin_roles, v2_admin_role_names, all_admin_roles, rolesAllowedToSeeUsage } from '../utils/roles';
|
||||||
|
|
||||||
const { Sider } = Layout;
|
const { Sider } = Layout;
|
||||||
|
|
||||||
|
@ -40,12 +41,6 @@ interface MenuItem {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const old_admin_roles = ["Admin", "Admin Viewer"];
|
|
||||||
const v2_admin_role_names = ["proxy_admin", "proxy_admin_viewer", "org_admin"];
|
|
||||||
const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names];
|
|
||||||
const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"];
|
|
||||||
|
|
||||||
|
|
||||||
// Note: If a menu item does not have a role, it is visible to all roles.
|
// Note: If a menu item does not have a role, it is visible to all roles.
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> },
|
{ key: "1", page: "api-keys", label: "Virtual Keys", icon: <KeyOutlined /> },
|
||||||
|
@ -57,7 +52,7 @@ const menuItems: MenuItem[] = [
|
||||||
{ key: "5", page: "users", label: "Internal Users", icon: <UserOutlined />, roles: all_admin_roles },
|
{ key: "5", page: "users", label: "Internal Users", icon: <UserOutlined />, roles: all_admin_roles },
|
||||||
{ key: "14", page: "api_ref", label: "API Reference", icon: <ApiOutlined /> },
|
{ key: "14", page: "api_ref", label: "API Reference", icon: <ApiOutlined /> },
|
||||||
{ key: "16", page: "model-hub", label: "Model Hub", icon: <AppstoreOutlined /> },
|
{ key: "16", page: "model-hub", label: "Model Hub", icon: <AppstoreOutlined /> },
|
||||||
{ key: "15", page: "logs", label: "Logs", icon: <LineChartOutlined />, roles: all_admin_roles },
|
{ key: "15", page: "logs", label: "Logs", icon: <LineChartOutlined />},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
|
@ -1806,8 +1806,7 @@ export const uiSpendLogsCall = async (
|
||||||
end_date?: string,
|
end_date?: string,
|
||||||
page?: number,
|
page?: number,
|
||||||
page_size?: number,
|
page_size?: number,
|
||||||
min_spend?: number,
|
user_id?: string,
|
||||||
max_spend?: number,
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// Construct base URL
|
// Construct base URL
|
||||||
|
@ -1817,13 +1816,12 @@ export const uiSpendLogsCall = async (
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (api_key) queryParams.append('api_key', api_key);
|
if (api_key) queryParams.append('api_key', api_key);
|
||||||
if (team_id) queryParams.append('team_id', team_id);
|
if (team_id) queryParams.append('team_id', team_id);
|
||||||
if (min_spend) queryParams.append('min_spend', min_spend.toString());
|
|
||||||
if (max_spend) queryParams.append('max_spend', max_spend.toString());
|
|
||||||
if (request_id) queryParams.append('request_id', request_id);
|
if (request_id) queryParams.append('request_id', request_id);
|
||||||
if (start_date) queryParams.append('start_date', start_date);
|
if (start_date) queryParams.append('start_date', start_date);
|
||||||
if (end_date) queryParams.append('end_date', end_date);
|
if (end_date) queryParams.append('end_date', end_date);
|
||||||
if (page) queryParams.append('page', page.toString());
|
if (page) queryParams.append('page', page.toString());
|
||||||
if (page_size) queryParams.append('page_size', page_size.toString());
|
if (page_size) queryParams.append('page_size', page_size.toString());
|
||||||
|
if (user_id) queryParams.append('user_id', user_id);
|
||||||
|
|
||||||
// Append query parameters to URL if any exist
|
// Append query parameters to URL if any exist
|
||||||
const queryString = queryParams.toString();
|
const queryString = queryParams.toString();
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ConfigInfoMessageProps {
|
||||||
|
show: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ConfigInfoMessage: React.FC<ConfigInfoMessageProps> = ({ show }) => {
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-start">
|
||||||
|
<div className="text-blue-500 mr-3 flex-shrink-0 mt-0.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-blue-800">Request/Response Data Not Available</h4>
|
||||||
|
<p className="text-sm text-blue-700 mt-1">
|
||||||
|
To view request and response details, enable prompt storage in your LiteLLM configuration by adding the following to your <code className="bg-blue-100 px-1 py-0.5 rounded">proxy_config.yaml</code> file:
|
||||||
|
</p>
|
||||||
|
<pre className="mt-2 bg-white p-3 rounded border border-blue-200 text-xs font-mono overflow-auto">
|
||||||
|
{`general_settings:
|
||||||
|
store_model_in_db: true
|
||||||
|
store_prompts_in_spend_logs: true`}
|
||||||
|
</pre>
|
||||||
|
<p className="text-xs text-blue-700 mt-2">
|
||||||
|
Note: This will only affect new requests after the configuration change.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -10,6 +10,8 @@ import { Row } from "@tanstack/react-table";
|
||||||
import { prefetchLogDetails } from "./prefetch";
|
import { prefetchLogDetails } from "./prefetch";
|
||||||
import { RequestResponsePanel } from "./columns";
|
import { RequestResponsePanel } from "./columns";
|
||||||
import { ErrorViewer } from './ErrorViewer';
|
import { ErrorViewer } from './ErrorViewer';
|
||||||
|
import { internalUserRoles } from "../../utils/roles";
|
||||||
|
import { ConfigInfoMessage } from './ConfigInfoMessage';
|
||||||
|
|
||||||
interface SpendLogsTableProps {
|
interface SpendLogsTableProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -62,6 +64,9 @@ export default function SpendLogsTable({
|
||||||
const [selectedTeamId, setSelectedTeamId] = useState("");
|
const [selectedTeamId, setSelectedTeamId] = useState("");
|
||||||
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
const [selectedKeyHash, setSelectedKeyHash] = useState("");
|
||||||
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
const [selectedFilter, setSelectedFilter] = useState("Team ID");
|
||||||
|
const [filterByCurrentUser, setFilterByCurrentUser] = useState(
|
||||||
|
userRole && internalUserRoles.includes(userRole)
|
||||||
|
);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
@ -93,6 +98,13 @@ export default function SpendLogsTable({
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userRole && internalUserRoles.includes(userRole)) {
|
||||||
|
setFilterByCurrentUser(true);
|
||||||
|
}
|
||||||
|
}, [userRole]);
|
||||||
|
|
||||||
const logs = useQuery<PaginatedResponse>({
|
const logs = useQuery<PaginatedResponse>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
"logs",
|
"logs",
|
||||||
|
@ -103,6 +115,7 @@ export default function SpendLogsTable({
|
||||||
endTime,
|
endTime,
|
||||||
selectedTeamId,
|
selectedTeamId,
|
||||||
selectedKeyHash,
|
selectedKeyHash,
|
||||||
|
filterByCurrentUser ? userID : null,
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!accessToken || !token || !userRole || !userID) {
|
if (!accessToken || !token || !userRole || !userID) {
|
||||||
|
@ -130,7 +143,8 @@ export default function SpendLogsTable({
|
||||||
formattedStartTime,
|
formattedStartTime,
|
||||||
formattedEndTime,
|
formattedEndTime,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize
|
pageSize,
|
||||||
|
filterByCurrentUser ? userID : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
// Trigger prefetch for all logs
|
// Trigger prefetch for all logs
|
||||||
|
@ -600,6 +614,12 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
const hasError = row.original.metadata?.status === "failure";
|
const hasError = row.original.metadata?.status === "failure";
|
||||||
const errorInfo = hasError ? row.original.metadata?.error_information : null;
|
const errorInfo = hasError ? row.original.metadata?.error_information : null;
|
||||||
|
|
||||||
|
// Check if request/response data is missing
|
||||||
|
const hasMessages = row.original.messages &&
|
||||||
|
(Array.isArray(row.original.messages) ? row.original.messages.length > 0 : Object.keys(row.original.messages).length > 0);
|
||||||
|
const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0;
|
||||||
|
const missingData = !hasMessages || !hasResponse;
|
||||||
|
|
||||||
// Format the response with error details if present
|
// Format the response with error details if present
|
||||||
const formattedResponse = () => {
|
const formattedResponse = () => {
|
||||||
if (hasError && errorInfo) {
|
if (hasError && errorInfo) {
|
||||||
|
@ -678,6 +698,9 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Configuration Info Message - Show when data is missing */}
|
||||||
|
<ConfigInfoMessage show={missingData} />
|
||||||
|
|
||||||
{/* Request/Response Panel */}
|
{/* Request/Response Panel */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{/* Request Side */}
|
{/* Request Side */}
|
||||||
|
@ -688,6 +711,7 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
onClick={() => navigator.clipboard.writeText(JSON.stringify(getRawRequest(), null, 2))}
|
onClick={() => navigator.clipboard.writeText(JSON.stringify(getRawRequest(), null, 2))}
|
||||||
className="p-1 hover:bg-gray-200 rounded"
|
className="p-1 hover:bg-gray-200 rounded"
|
||||||
title="Copy request"
|
title="Copy request"
|
||||||
|
disabled={!hasMessages}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
@ -715,6 +739,7 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
onClick={() => navigator.clipboard.writeText(JSON.stringify(formattedResponse(), null, 2))}
|
onClick={() => navigator.clipboard.writeText(JSON.stringify(formattedResponse(), null, 2))}
|
||||||
className="p-1 hover:bg-gray-200 rounded"
|
className="p-1 hover:bg-gray-200 rounded"
|
||||||
title="Copy response"
|
title="Copy response"
|
||||||
|
disabled={!hasResponse}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
@ -723,16 +748,18 @@ function RequestViewer({ row }: { row: Row<LogEntry> }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 overflow-auto max-h-96 bg-gray-50">
|
<div className="p-4 overflow-auto max-h-96 bg-gray-50">
|
||||||
<pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(formattedResponse(), null, 2)}</pre>
|
{hasResponse ? (
|
||||||
|
<pre className="text-xs font-mono whitespace-pre-wrap break-all">{JSON.stringify(formattedResponse(), null, 2)}</pre>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{/* Error Card - Only show for failures */}
|
{/* Error Card - Only show for failures */}
|
||||||
{hasError && errorInfo && <ErrorViewer errorInfo={errorInfo} />}
|
{hasError && errorInfo && <ErrorViewer errorInfo={errorInfo} />}
|
||||||
|
|
||||||
|
|
||||||
{/* Tags Card - Only show if there are tags */}
|
{/* Tags Card - Only show if there are tags */}
|
||||||
{row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (
|
{row.original.request_tags && Object.keys(row.original.request_tags).length > 0 && (
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
|
7
ui/litellm-dashboard/src/utils/roles.ts
Normal file
7
ui/litellm-dashboard/src/utils/roles.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
// Define admin roles and permissions
|
||||||
|
export const old_admin_roles = ["Admin", "Admin Viewer"];
|
||||||
|
export const v2_admin_role_names = ["proxy_admin", "proxy_admin_viewer", "org_admin"];
|
||||||
|
export const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names];
|
||||||
|
|
||||||
|
export const internalUserRoles = ["Internal User", "Internal Viewer"];
|
||||||
|
export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"];
|
Loading…
Add table
Add a link
Reference in a new issue