mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 11:14:04 +00:00
Add team based usage dashboard at 1m+ spend logs (+ new /team/daily/activity
API) (#10081)
* feat(ui/): add team based usage to dashboard allows admin to see spend across teams + within teams at 1m+ spend logs * fix(entity_usage.tsx): add activity page to entity usage * style(entity_usage.tsx): place filter above tab switcher
This commit is contained in:
parent
8a17398ea1
commit
f200bb2c70
5 changed files with 372 additions and 240 deletions
|
@ -364,6 +364,7 @@ async def delete_tag(
|
||||||
"/tag/daily/activity",
|
"/tag/daily/activity",
|
||||||
response_model=SpendAnalyticsPaginatedResponse,
|
response_model=SpendAnalyticsPaginatedResponse,
|
||||||
tags=["tag management"],
|
tags=["tag management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
)
|
)
|
||||||
async def get_tag_daily_activity(
|
async def get_tag_daily_activity(
|
||||||
tags: Optional[str] = None,
|
tags: Optional[str] = None,
|
||||||
|
|
|
@ -62,6 +62,9 @@ from litellm.proxy.management_endpoints.common_utils import (
|
||||||
_is_user_team_admin,
|
_is_user_team_admin,
|
||||||
_set_object_metadata_field,
|
_set_object_metadata_field,
|
||||||
)
|
)
|
||||||
|
from litellm.proxy.management_endpoints.tag_management_endpoints import (
|
||||||
|
get_daily_activity,
|
||||||
|
)
|
||||||
from litellm.proxy.management_helpers.team_member_permission_checks import (
|
from litellm.proxy.management_helpers.team_member_permission_checks import (
|
||||||
TeamMemberPermissionChecks,
|
TeamMemberPermissionChecks,
|
||||||
)
|
)
|
||||||
|
@ -75,6 +78,9 @@ from litellm.proxy.utils import (
|
||||||
handle_exception_on_proxy,
|
handle_exception_on_proxy,
|
||||||
)
|
)
|
||||||
from litellm.router import Router
|
from litellm.router import Router
|
||||||
|
from litellm.types.proxy.management_endpoints.common_daily_activity import (
|
||||||
|
SpendAnalyticsPaginatedResponse,
|
||||||
|
)
|
||||||
from litellm.types.proxy.management_endpoints.team_endpoints import (
|
from litellm.types.proxy.management_endpoints.team_endpoints import (
|
||||||
GetTeamMemberPermissionsResponse,
|
GetTeamMemberPermissionsResponse,
|
||||||
UpdateTeamMemberPermissionsRequest,
|
UpdateTeamMemberPermissionsRequest,
|
||||||
|
@ -515,12 +521,12 @@ async def update_team(
|
||||||
updated_kv["model_id"] = _model_id
|
updated_kv["model_id"] = _model_id
|
||||||
|
|
||||||
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
|
updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv)
|
||||||
team_row: Optional[LiteLLM_TeamTable] = (
|
team_row: Optional[
|
||||||
await prisma_client.db.litellm_teamtable.update(
|
LiteLLM_TeamTable
|
||||||
where={"team_id": data.team_id},
|
] = await prisma_client.db.litellm_teamtable.update(
|
||||||
data=updated_kv,
|
where={"team_id": data.team_id},
|
||||||
include={"litellm_model_table": True}, # type: ignore
|
data=updated_kv,
|
||||||
)
|
include={"litellm_model_table": True}, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
if team_row is None or team_row.team_id is None:
|
if team_row is None or team_row.team_id is None:
|
||||||
|
@ -1146,10 +1152,10 @@ async def delete_team(
|
||||||
team_rows: List[LiteLLM_TeamTable] = []
|
team_rows: List[LiteLLM_TeamTable] = []
|
||||||
for team_id in data.team_ids:
|
for team_id in data.team_ids:
|
||||||
try:
|
try:
|
||||||
team_row_base: Optional[BaseModel] = (
|
team_row_base: Optional[
|
||||||
await prisma_client.db.litellm_teamtable.find_unique(
|
BaseModel
|
||||||
where={"team_id": team_id}
|
] = await prisma_client.db.litellm_teamtable.find_unique(
|
||||||
)
|
where={"team_id": team_id}
|
||||||
)
|
)
|
||||||
if team_row_base is None:
|
if team_row_base is None:
|
||||||
raise Exception
|
raise Exception
|
||||||
|
@ -1307,10 +1313,10 @@ async def team_info(
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
team_info: Optional[BaseModel] = (
|
team_info: Optional[
|
||||||
await prisma_client.db.litellm_teamtable.find_unique(
|
BaseModel
|
||||||
where={"team_id": team_id}
|
] = await prisma_client.db.litellm_teamtable.find_unique(
|
||||||
)
|
where={"team_id": team_id}
|
||||||
)
|
)
|
||||||
if team_info is None:
|
if team_info is None:
|
||||||
raise Exception
|
raise Exception
|
||||||
|
@ -2079,3 +2085,52 @@ async def update_team_member_permissions(
|
||||||
)
|
)
|
||||||
|
|
||||||
return updated_team
|
return updated_team
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/team/daily/activity",
|
||||||
|
response_model=SpendAnalyticsPaginatedResponse,
|
||||||
|
tags=["team management"],
|
||||||
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
|
)
|
||||||
|
async def get_team_daily_activity(
|
||||||
|
team_ids: Optional[str] = None,
|
||||||
|
start_date: Optional[str] = None,
|
||||||
|
end_date: Optional[str] = None,
|
||||||
|
model: Optional[str] = None,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 10,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get daily activity for specific teams or all teams.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
team_ids (Optional[str]): Comma-separated list of team IDs to filter by. If not provided, returns data for all teams.
|
||||||
|
start_date (Optional[str]): Start date for the activity period (YYYY-MM-DD).
|
||||||
|
end_date (Optional[str]): End date for the activity period (YYYY-MM-DD).
|
||||||
|
model (Optional[str]): Filter by model name.
|
||||||
|
api_key (Optional[str]): Filter by API key.
|
||||||
|
page (int): Page number for pagination.
|
||||||
|
page_size (int): Number of items per page.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SpendAnalyticsPaginatedResponse: Paginated response containing daily activity data.
|
||||||
|
"""
|
||||||
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
|
# Convert comma-separated tags string to list if provided
|
||||||
|
team_ids_list = team_ids.split(",") if team_ids else None
|
||||||
|
|
||||||
|
return await get_daily_activity(
|
||||||
|
prisma_client=prisma_client,
|
||||||
|
table_name="litellm_dailyteamspend",
|
||||||
|
entity_id_field="team_id",
|
||||||
|
entity_id=team_ids_list,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
model=model,
|
||||||
|
api_key=api_key,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
|
|
@ -3,12 +3,13 @@ import {
|
||||||
BarChart, Card, Title, Text,
|
BarChart, Card, Title, Text,
|
||||||
Grid, Col, DateRangePicker, DateRangePickerValue,
|
Grid, Col, DateRangePicker, DateRangePickerValue,
|
||||||
Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell,
|
Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell,
|
||||||
DonutChart
|
DonutChart,
|
||||||
|
TabPanel, TabGroup, TabList, Tab, TabPanels
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import { ActivityMetrics, processActivityData } from './activity_metrics';
|
import { ActivityMetrics, processActivityData } from './activity_metrics';
|
||||||
import { SpendMetrics, DailyData } from './usage/types';
|
import { SpendMetrics, DailyData } from './usage/types';
|
||||||
import { tagDailyActivityCall } from './networking';
|
import { tagDailyActivityCall, teamDailyActivityCall } from './networking';
|
||||||
|
|
||||||
interface EntityMetrics {
|
interface EntityMetrics {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
@ -69,6 +70,8 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const modelMetrics = processActivityData(spendData);
|
||||||
|
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||||
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
|
const [dateValue, setDateValue] = useState<DateRangePickerValue>({
|
||||||
from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000),
|
from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000),
|
||||||
|
@ -89,6 +92,15 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||||
selectedTags.length > 0 ? selectedTags : null
|
selectedTags.length > 0 ? selectedTags : null
|
||||||
);
|
);
|
||||||
setSpendData(data);
|
setSpendData(data);
|
||||||
|
} else if (entityType === 'team') {
|
||||||
|
const data = await teamDailyActivityCall(
|
||||||
|
accessToken,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
1,
|
||||||
|
selectedTags.length > 0 ? selectedTags : null
|
||||||
|
);
|
||||||
|
setSpendData(data);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Invalid entity type");
|
throw new Error("Invalid entity type");
|
||||||
}
|
}
|
||||||
|
@ -236,240 +248,254 @@ const EntityUsage: React.FC<EntityUsageProps> = ({
|
||||||
return filterDataByTags(result);
|
return filterDataByTags(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<Grid numItems={2} className="gap-2 w-full mb-4">
|
<Grid numItems={2} className="gap-2 w-full mb-4">
|
||||||
<Col>
|
<Col>
|
||||||
<Text>Select Time Range</Text>
|
<Text>Select Time Range</Text>
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
enableSelect={true}
|
enableSelect={true}
|
||||||
value={dateValue}
|
value={dateValue}
|
||||||
onValueChange={setDateValue}
|
onValueChange={setDateValue}
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col>
|
|
||||||
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
|
|
||||||
<Select
|
|
||||||
mode="multiple"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`}
|
|
||||||
value={selectedTags}
|
|
||||||
onChange={setSelectedTags}
|
|
||||||
options={getAllTags()}
|
|
||||||
className="mt-2"
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid numItems={2} className="gap-2 w-full">
|
|
||||||
{/* Total Spend Card */}
|
|
||||||
<Col numColSpan={2}>
|
|
||||||
<Card>
|
|
||||||
<Title>{entityType === 'tag' ? 'Tag' : 'Team'} Spend Overview</Title>
|
|
||||||
<Grid numItems={5} className="gap-4 mt-4">
|
|
||||||
<Card>
|
|
||||||
<Title>Total Spend</Title>
|
|
||||||
<Text className="text-2xl font-bold mt-2">
|
|
||||||
${spendData.metadata.total_spend.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Title>Total Requests</Title>
|
|
||||||
<Text className="text-2xl font-bold mt-2">
|
|
||||||
{spendData.metadata.total_api_requests.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Title>Successful Requests</Title>
|
|
||||||
<Text className="text-2xl font-bold mt-2 text-green-600">
|
|
||||||
{spendData.metadata.total_successful_requests.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Title>Failed Requests</Title>
|
|
||||||
<Text className="text-2xl font-bold mt-2 text-red-600">
|
|
||||||
{spendData.metadata.total_failed_requests.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<Title>Total Tokens</Title>
|
|
||||||
<Text className="text-2xl font-bold mt-2">
|
|
||||||
{spendData.metadata.total_tokens.toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
</Card>
|
|
||||||
</Grid>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Daily Spend Chart */}
|
|
||||||
<Col numColSpan={2}>
|
|
||||||
<Card>
|
|
||||||
<Title>Daily Spend</Title>
|
|
||||||
<BarChart
|
|
||||||
data={[...spendData.results].sort((a, b) =>
|
|
||||||
new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
||||||
)}
|
|
||||||
index="date"
|
|
||||||
categories={["metrics.spend"]}
|
|
||||||
colors={["cyan"]}
|
|
||||||
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
|
||||||
yAxisWidth={100}
|
|
||||||
showLegend={false}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Col>
|
||||||
</Col>
|
<Col>
|
||||||
|
<Text>Filter by {entityType === 'tag' ? 'Tags' : 'Teams'}</Text>
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={`Select ${entityType === 'tag' ? 'tags' : 'teams'} to filter...`}
|
||||||
|
value={selectedTags}
|
||||||
|
onChange={setSelectedTags}
|
||||||
|
options={getAllTags()}
|
||||||
|
className="mt-2"
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
<TabGroup>
|
||||||
|
<TabList variant="solid" className="mt-1">
|
||||||
|
<Tab>Cost</Tab>
|
||||||
|
<Tab>Activity</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanels>
|
||||||
|
<TabPanel>
|
||||||
|
<Grid numItems={2} className="gap-2 w-full">
|
||||||
|
{/* Total Spend Card */}
|
||||||
|
<Col numColSpan={2}>
|
||||||
|
<Card>
|
||||||
|
<Title>{entityType === 'tag' ? 'Tag' : 'Team'} Spend Overview</Title>
|
||||||
|
<Grid numItems={5} className="gap-4 mt-4">
|
||||||
|
<Card>
|
||||||
|
<Title>Total Spend</Title>
|
||||||
|
<Text className="text-2xl font-bold mt-2">
|
||||||
|
${spendData.metadata.total_spend.toFixed(2)}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Title>Total Requests</Title>
|
||||||
|
<Text className="text-2xl font-bold mt-2">
|
||||||
|
{spendData.metadata.total_api_requests.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Title>Successful Requests</Title>
|
||||||
|
<Text className="text-2xl font-bold mt-2 text-green-600">
|
||||||
|
{spendData.metadata.total_successful_requests.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Title>Failed Requests</Title>
|
||||||
|
<Text className="text-2xl font-bold mt-2 text-red-600">
|
||||||
|
{spendData.metadata.total_failed_requests.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Title>Total Tokens</Title>
|
||||||
|
<Text className="text-2xl font-bold mt-2">
|
||||||
|
{spendData.metadata.total_tokens.toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
{/* Entity Breakdown Section */}
|
{/* Daily Spend Chart */}
|
||||||
<Col numColSpan={2}>
|
<Col numColSpan={2}>
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex flex-col space-y-4">
|
<Title>Daily Spend</Title>
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
<Title>Spend Per {entityType === 'tag' ? 'Tag' : 'Team'}</Title>
|
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
|
||||||
<span>Get Started Tracking cost per {entityType} </span>
|
|
||||||
<a href="https://docs.litellm.ai/docs/proxy/tags" className="text-blue-500 hover:text-blue-700 ml-1">
|
|
||||||
here
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Grid numItems={2}>
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<BarChart
|
<BarChart
|
||||||
className="mt-4 h-52"
|
data={[...spendData.results].sort((a, b) =>
|
||||||
data={getEntityBreakdown()}
|
new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||||
index="entity"
|
)}
|
||||||
|
index="date"
|
||||||
|
categories={["metrics.spend"]}
|
||||||
|
colors={["cyan"]}
|
||||||
|
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
||||||
|
yAxisWidth={100}
|
||||||
|
showLegend={false}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Entity Breakdown Section */}
|
||||||
|
<Col numColSpan={2}>
|
||||||
|
<Card>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-col space-y-2">
|
||||||
|
<Title>Spend Per {entityType === 'tag' ? 'Tag' : 'Team'}</Title>
|
||||||
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
|
<span>Get Started Tracking cost per {entityType} </span>
|
||||||
|
<a href="https://docs.litellm.ai/docs/proxy/tags" className="text-blue-500 hover:text-blue-700 ml-1">
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Grid numItems={2}>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<BarChart
|
||||||
|
className="mt-4 h-52"
|
||||||
|
data={getEntityBreakdown()}
|
||||||
|
index="entity"
|
||||||
|
categories={["spend"]}
|
||||||
|
colors={["cyan"]}
|
||||||
|
valueFormatter={(value) => `$${value.toFixed(4)}`}
|
||||||
|
layout="vertical"
|
||||||
|
showLegend={false}
|
||||||
|
yAxisWidth={100}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>{entityType === 'tag' ? 'Tag' : 'Team'}</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend</TableHeaderCell>
|
||||||
|
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||||
|
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{getEntityBreakdown()
|
||||||
|
.filter(entity => entity.spend > 0)
|
||||||
|
.map((entity) => (
|
||||||
|
<TableRow key={entity.entity}>
|
||||||
|
<TableCell>{entity.entity}</TableCell>
|
||||||
|
<TableCell>${entity.spend.toFixed(4)}</TableCell>
|
||||||
|
<TableCell className="text-green-600">
|
||||||
|
{entity.successful_requests.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-red-600">
|
||||||
|
{entity.failed_requests.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{entity.tokens.toLocaleString()}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Top API Keys */}
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Card>
|
||||||
|
<Title>Top API Keys</Title>
|
||||||
|
<BarChart
|
||||||
|
className="mt-4 h-40"
|
||||||
|
data={getTopApiKeys()}
|
||||||
|
index="key"
|
||||||
categories={["spend"]}
|
categories={["spend"]}
|
||||||
colors={["cyan"]}
|
colors={["cyan"]}
|
||||||
valueFormatter={(value) => `$${value.toFixed(4)}`}
|
|
||||||
layout="vertical"
|
|
||||||
showLegend={false}
|
|
||||||
yAxisWidth={100}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<Table>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableHeaderCell>{entityType === 'tag' ? 'Tag' : 'Team'}</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Spend</TableHeaderCell>
|
|
||||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
|
||||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
|
||||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{getEntityBreakdown()
|
|
||||||
.filter(entity => entity.spend > 0)
|
|
||||||
.map((entity) => (
|
|
||||||
<TableRow key={entity.entity}>
|
|
||||||
<TableCell>{entity.entity}</TableCell>
|
|
||||||
<TableCell>${entity.spend.toFixed(4)}</TableCell>
|
|
||||||
<TableCell className="text-green-600">
|
|
||||||
{entity.successful_requests.toLocaleString()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-red-600">
|
|
||||||
{entity.failed_requests.toLocaleString()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{entity.tokens.toLocaleString()}</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Col>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Top API Keys */}
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<Card>
|
|
||||||
<Title>Top API Keys</Title>
|
|
||||||
<BarChart
|
|
||||||
className="mt-4 h-40"
|
|
||||||
data={getTopApiKeys()}
|
|
||||||
index="key"
|
|
||||||
categories={["spend"]}
|
|
||||||
colors={["cyan"]}
|
|
||||||
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
|
||||||
layout="vertical"
|
|
||||||
yAxisWidth={200}
|
|
||||||
showLegend={false}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* Top Models */}
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<Card>
|
|
||||||
<Title>Top Models</Title>
|
|
||||||
<BarChart
|
|
||||||
className="mt-4 h-40"
|
|
||||||
data={getTopModels()}
|
|
||||||
index="key"
|
|
||||||
categories={["spend"]}
|
|
||||||
colors={["cyan"]}
|
|
||||||
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
|
||||||
layout="vertical"
|
|
||||||
yAxisWidth={200}
|
|
||||||
showLegend={false}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Spend by Provider */}
|
|
||||||
<Col numColSpan={2}>
|
|
||||||
<Card>
|
|
||||||
<div className="flex flex-col space-y-4">
|
|
||||||
<Title>Provider Usage</Title>
|
|
||||||
<Grid numItems={2}>
|
|
||||||
<Col numColSpan={1}>
|
|
||||||
<DonutChart
|
|
||||||
className="mt-4 h-40"
|
|
||||||
data={getProviderSpend()}
|
|
||||||
index="provider"
|
|
||||||
category="spend"
|
|
||||||
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
||||||
colors={["cyan", "blue", "indigo", "violet", "purple"]}
|
layout="vertical"
|
||||||
|
yAxisWidth={200}
|
||||||
|
showLegend={false}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Card>
|
||||||
<Col numColSpan={1}>
|
</Col>
|
||||||
<Table>
|
|
||||||
<TableHead>
|
{/* Top Models */}
|
||||||
<TableRow>
|
<Col numColSpan={1}>
|
||||||
<TableHeaderCell>Provider</TableHeaderCell>
|
<Card>
|
||||||
<TableHeaderCell>Spend</TableHeaderCell>
|
<Title>Top Models</Title>
|
||||||
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
<BarChart
|
||||||
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
className="mt-4 h-40"
|
||||||
<TableHeaderCell>Tokens</TableHeaderCell>
|
data={getTopModels()}
|
||||||
</TableRow>
|
index="key"
|
||||||
</TableHead>
|
categories={["spend"]}
|
||||||
<TableBody>
|
colors={["cyan"]}
|
||||||
{getProviderSpend().map((provider) => (
|
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
||||||
<TableRow key={provider.provider}>
|
layout="vertical"
|
||||||
<TableCell>{provider.provider}</TableCell>
|
yAxisWidth={200}
|
||||||
<TableCell>${provider.spend.toFixed(2)}</TableCell>
|
showLegend={false}
|
||||||
<TableCell className="text-green-600">
|
/>
|
||||||
{provider.successful_requests.toLocaleString()}
|
</Card>
|
||||||
</TableCell>
|
</Col>
|
||||||
<TableCell className="text-red-600">
|
|
||||||
{provider.failed_requests.toLocaleString()}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
{/* Spend by Provider */}
|
||||||
</TableRow>
|
<Col numColSpan={2}>
|
||||||
))}
|
<Card>
|
||||||
</TableBody>
|
<div className="flex flex-col space-y-4">
|
||||||
</Table>
|
<Title>Provider Usage</Title>
|
||||||
</Col>
|
<Grid numItems={2}>
|
||||||
</Grid>
|
<Col numColSpan={1}>
|
||||||
</div>
|
<DonutChart
|
||||||
</Card>
|
className="mt-4 h-40"
|
||||||
</Col>
|
data={getProviderSpend()}
|
||||||
</Grid>
|
index="provider"
|
||||||
|
category="spend"
|
||||||
|
valueFormatter={(value) => `$${value.toFixed(2)}`}
|
||||||
|
colors={["cyan", "blue", "indigo", "violet", "purple"]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col numColSpan={1}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableHeaderCell>Provider</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Spend</TableHeaderCell>
|
||||||
|
<TableHeaderCell className="text-green-600">Successful</TableHeaderCell>
|
||||||
|
<TableHeaderCell className="text-red-600">Failed</TableHeaderCell>
|
||||||
|
<TableHeaderCell>Tokens</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{getProviderSpend().map((provider) => (
|
||||||
|
<TableRow key={provider.provider}>
|
||||||
|
<TableCell>{provider.provider}</TableCell>
|
||||||
|
<TableCell>${provider.spend.toFixed(2)}</TableCell>
|
||||||
|
<TableCell className="text-green-600">
|
||||||
|
{provider.successful_requests.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-red-600">
|
||||||
|
{provider.failed_requests.toLocaleString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{provider.tokens.toLocaleString()}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Grid>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<ActivityMetrics modelMetrics={modelMetrics} />
|
||||||
|
</TabPanel>
|
||||||
|
</TabPanels>
|
||||||
|
</TabGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1186,6 +1186,47 @@ export const tagDailyActivityCall = async (accessToken: String, startTime: Date,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const teamDailyActivityCall = async (accessToken: String, startTime: Date, endTime: Date, page: number = 1, teamIds: string[] | null = null) => {
|
||||||
|
/**
|
||||||
|
* Get daily user activity on proxy
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/team/daily/activity` : `/team/daily/activity`;
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
queryParams.append('start_date', startTime.toISOString());
|
||||||
|
queryParams.append('end_date', endTime.toISOString());
|
||||||
|
queryParams.append('page_size', '1000');
|
||||||
|
queryParams.append('page', page.toString());
|
||||||
|
if (teamIds) {
|
||||||
|
queryParams.append('team_ids', teamIds.join(','));
|
||||||
|
}
|
||||||
|
const queryString = queryParams.toString();
|
||||||
|
if (queryString) {
|
||||||
|
url += `?${queryString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getTotalSpendCall = async (accessToken: String) => {
|
export const getTotalSpendCall = async (accessToken: String) => {
|
||||||
/**
|
/**
|
||||||
* Get all models on proxy
|
* Get all models on proxy
|
||||||
|
|
|
@ -236,6 +236,7 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
<TabList variant="solid" className="mt-1">
|
<TabList variant="solid" className="mt-1">
|
||||||
<Tab>Your Usage</Tab>
|
<Tab>Your Usage</Tab>
|
||||||
<Tab>Tag Usage</Tab>
|
<Tab>Tag Usage</Tab>
|
||||||
|
<Tab>Team Usage</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{/* Your Usage Panel */}
|
{/* Your Usage Panel */}
|
||||||
|
@ -469,6 +470,14 @@ const NewUsagePage: React.FC<NewUsagePageProps> = ({
|
||||||
entityType="tag"
|
entityType="tag"
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
{/* Team Usage Panel */}
|
||||||
|
<TabPanel>
|
||||||
|
<EntityUsage
|
||||||
|
accessToken={accessToken}
|
||||||
|
entityType="team"
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue