Spaces:
Paused
Paused
| import React, { useState, useEffect } from "react"; | |
| import { | |
| BarChart, Card, Title, Text, | |
| Grid, Col, DateRangePicker, DateRangePickerValue, | |
| Table, TableHead, TableRow, TableHeaderCell, TableBody, TableCell, | |
| DonutChart, | |
| TabPanel, TabGroup, TabList, Tab, TabPanels | |
| } from "@tremor/react"; | |
| import { Select } from 'antd'; | |
| import { ActivityMetrics, processActivityData } from './activity_metrics'; | |
| import { DailyData, KeyMetricWithMetadata, EntityMetricWithMetadata } from './usage/types'; | |
| import { tagDailyActivityCall, teamDailyActivityCall } from './networking'; | |
| import TopKeyView from "./top_key_view"; | |
| interface EntityMetrics { | |
| metrics: { | |
| spend: number; | |
| prompt_tokens: number; | |
| completion_tokens: number; | |
| cache_read_input_tokens: number; | |
| cache_creation_input_tokens: number; | |
| total_tokens: number; | |
| successful_requests: number; | |
| failed_requests: number; | |
| api_requests: number; | |
| }; | |
| metadata: Record<string, any>; | |
| } | |
| interface BreakdownMetrics { | |
| models: Record<string, any>; | |
| providers: Record<string, any>; | |
| api_keys: Record<string, any>; | |
| entities: Record<string, EntityMetrics>; | |
| } | |
| interface ExtendedDailyData extends DailyData { | |
| breakdown: BreakdownMetrics; | |
| } | |
| interface EntitySpendData { | |
| results: ExtendedDailyData[]; | |
| metadata: { | |
| total_spend: number; | |
| total_api_requests: number; | |
| total_successful_requests: number; | |
| total_failed_requests: number; | |
| total_tokens: number; | |
| }; | |
| } | |
| export interface EntityList { | |
| label: string; | |
| value: string; | |
| } | |
| interface EntityUsageProps { | |
| accessToken: string | null; | |
| entityType: 'tag' | 'team'; | |
| entityId?: string | null; | |
| userID: string | null; | |
| userRole: string | null; | |
| entityList: EntityList[] | null; | |
| } | |
| const EntityUsage: React.FC<EntityUsageProps> = ({ | |
| accessToken, | |
| entityType, | |
| entityId, | |
| userID, | |
| userRole, | |
| entityList | |
| }) => { | |
| const [spendData, setSpendData] = useState<EntitySpendData>({ | |
| results: [], | |
| metadata: { | |
| total_spend: 0, | |
| total_api_requests: 0, | |
| total_successful_requests: 0, | |
| total_failed_requests: 0, | |
| total_tokens: 0 | |
| } | |
| }); | |
| const modelMetrics = processActivityData(spendData, "models"); | |
| const keyMetrics = processActivityData(spendData, "api_keys"); | |
| const [selectedTags, setSelectedTags] = useState<string[]>([]); | |
| const [dateValue, setDateValue] = useState<DateRangePickerValue>({ | |
| from: new Date(Date.now() - 28 * 24 * 60 * 60 * 1000), | |
| to: new Date(), | |
| }); | |
| const fetchSpendData = async () => { | |
| if (!accessToken || !dateValue.from || !dateValue.to) return; | |
| const startTime = dateValue.from; | |
| const endTime = dateValue.to; | |
| if (entityType === 'tag') { | |
| const data = await tagDailyActivityCall( | |
| accessToken, | |
| startTime, | |
| endTime, | |
| 1, | |
| selectedTags.length > 0 ? selectedTags : null | |
| ); | |
| setSpendData(data); | |
| } else if (entityType === 'team') { | |
| const data = await teamDailyActivityCall( | |
| accessToken, | |
| startTime, | |
| endTime, | |
| 1, | |
| selectedTags.length > 0 ? selectedTags : null | |
| ); | |
| setSpendData(data); | |
| } else { | |
| throw new Error("Invalid entity type"); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchSpendData(); | |
| }, [accessToken, dateValue, entityId, selectedTags]); | |
| const getTopModels = () => { | |
| const modelSpend: { [key: string]: any } = {}; | |
| spendData.results.forEach(day => { | |
| Object.entries(day.breakdown.models || {}).forEach(([model, metrics]) => { | |
| if (!modelSpend[model]) { | |
| modelSpend[model] = { | |
| spend: 0, | |
| requests: 0, | |
| successful_requests: 0, | |
| failed_requests: 0, | |
| tokens: 0 | |
| }; | |
| } | |
| try { | |
| modelSpend[model].spend += metrics.metrics.spend; | |
| } catch (e) { | |
| console.log(`Error adding spend for ${model}: ${e}, got metrics: ${JSON.stringify(metrics)}`); | |
| } | |
| modelSpend[model].requests += metrics.metrics.api_requests; | |
| modelSpend[model].successful_requests += metrics.metrics.successful_requests; | |
| modelSpend[model].failed_requests += metrics.metrics.failed_requests; | |
| modelSpend[model].tokens += metrics.metrics.total_tokens; | |
| }); | |
| }); | |
| return Object.entries(modelSpend) | |
| .map(([model, metrics]) => ({ | |
| key: model, | |
| ...metrics | |
| })) | |
| .sort((a, b) => b.spend - a.spend) | |
| .slice(0, 5); | |
| }; | |
| const getTopAPIKeys = () => { | |
| const keySpend: { [key: string]: KeyMetricWithMetadata } = {}; | |
| spendData.results.forEach(day => { | |
| Object.entries(day.breakdown.api_keys || {}).forEach(([key, metrics]) => { | |
| if (!keySpend[key]) { | |
| keySpend[key] = { | |
| metrics: { | |
| spend: 0, | |
| prompt_tokens: 0, | |
| completion_tokens: 0, | |
| total_tokens: 0, | |
| api_requests: 0, | |
| successful_requests: 0, | |
| failed_requests: 0, | |
| cache_read_input_tokens: 0, | |
| cache_creation_input_tokens: 0 | |
| }, | |
| metadata: { | |
| key_alias: metrics.metadata.key_alias | |
| } | |
| }; | |
| } | |
| keySpend[key].metrics.spend += metrics.metrics.spend; | |
| keySpend[key].metrics.prompt_tokens += metrics.metrics.prompt_tokens; | |
| keySpend[key].metrics.completion_tokens += metrics.metrics.completion_tokens; | |
| keySpend[key].metrics.total_tokens += metrics.metrics.total_tokens; | |
| keySpend[key].metrics.api_requests += metrics.metrics.api_requests; | |
| keySpend[key].metrics.successful_requests += metrics.metrics.successful_requests; | |
| keySpend[key].metrics.failed_requests += metrics.metrics.failed_requests; | |
| keySpend[key].metrics.cache_read_input_tokens += metrics.metrics.cache_read_input_tokens || 0; | |
| keySpend[key].metrics.cache_creation_input_tokens += metrics.metrics.cache_creation_input_tokens || 0; | |
| }); | |
| }); | |
| return Object.entries(keySpend) | |
| .map(([api_key, metrics]) => ({ | |
| api_key, | |
| key_alias: metrics.metadata.key_alias || "-", // Using truncated key as alias | |
| spend: metrics.metrics.spend, | |
| })) | |
| .sort((a, b) => b.spend - a.spend) | |
| .slice(0, 5); | |
| }; | |
| const getProviderSpend = () => { | |
| const providerSpend: { [key: string]: any } = {}; | |
| spendData.results.forEach(day => { | |
| Object.entries(day.breakdown.providers || {}).forEach(([provider, metrics]) => { | |
| if (!providerSpend[provider]) { | |
| providerSpend[provider] = { | |
| provider, | |
| spend: 0, | |
| requests: 0, | |
| successful_requests: 0, | |
| failed_requests: 0, | |
| tokens: 0 | |
| }; | |
| } | |
| try { | |
| providerSpend[provider].spend += metrics.metrics.spend; | |
| providerSpend[provider].requests += metrics.metrics.api_requests; | |
| providerSpend[provider].successful_requests += metrics.metrics.successful_requests; | |
| providerSpend[provider].failed_requests += metrics.metrics.failed_requests; | |
| providerSpend[provider].tokens += metrics.metrics.total_tokens; | |
| } catch (e) { | |
| console.log(`Error processing provider ${provider}: ${e}`); | |
| } | |
| }); | |
| }); | |
| return Object.values(providerSpend) | |
| .filter(provider => provider.spend > 0) | |
| .sort((a, b) => b.spend - a.spend); | |
| }; | |
| const getAllTags = () => { | |
| if (entityList) { | |
| return entityList; | |
| } | |
| }; | |
| const filterDataByTags = (data: EntityMetricWithMetadata[]) => { | |
| if (selectedTags.length === 0) return data; | |
| return data.filter(item => selectedTags.includes(item.metadata.id)); | |
| }; | |
| const getEntityBreakdown = () => { | |
| const entitySpend: { [key: string]: EntityMetricWithMetadata } = {}; | |
| spendData.results.forEach(day => { | |
| Object.entries(day.breakdown.entities || {}).forEach(([entity, data]) => { | |
| if (!entitySpend[entity]) { | |
| entitySpend[entity] = { | |
| metrics: { | |
| spend: 0, | |
| prompt_tokens: 0, | |
| completion_tokens: 0, | |
| total_tokens: 0, | |
| api_requests: 0, | |
| successful_requests: 0, | |
| failed_requests: 0, | |
| cache_read_input_tokens: 0, | |
| cache_creation_input_tokens: 0 | |
| }, | |
| metadata: { | |
| alias: data.metadata.team_alias || entity, | |
| id: entity | |
| } | |
| }; | |
| } | |
| entitySpend[entity].metrics.spend += data.metrics.spend; | |
| entitySpend[entity].metrics.api_requests += data.metrics.api_requests; | |
| entitySpend[entity].metrics.successful_requests += data.metrics.successful_requests; | |
| entitySpend[entity].metrics.failed_requests += data.metrics.failed_requests; | |
| entitySpend[entity].metrics.total_tokens += data.metrics.total_tokens; | |
| }); | |
| }); | |
| const result = Object.values(entitySpend) | |
| .sort((a, b) => b.metrics.spend - a.metrics.spend); | |
| return filterDataByTags(result); | |
| }; | |
| return ( | |
| <div style={{ width: "100%" }}> | |
| <Grid numItems={2} className="gap-2 w-full mb-4"> | |
| <Col> | |
| <Text>Select Time Range</Text> | |
| <DateRangePicker | |
| enableSelect={true} | |
| value={dateValue} | |
| onValueChange={setDateValue} | |
| /> | |
| </Col> | |
| {entityList && entityList.length > 0 && ( | |
| <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>Model Activity</Tab> | |
| <Tab>Key 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> | |
| {/* 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} | |
| customTooltip={({ payload, active }) => { | |
| if (!active || !payload?.[0]) return null; | |
| const data = payload[0].payload; | |
| return ( | |
| <div className="bg-white p-4 shadow-lg rounded-lg border"> | |
| <p className="font-bold">{data.date}</p> | |
| <p className="text-cyan-500">Total Spend: ${data.metrics.spend.toFixed(2)}</p> | |
| <p className="text-gray-600">Total Requests: {data.metrics.api_requests}</p> | |
| <p className="text-gray-600">Successful: {data.metrics.successful_requests}</p> | |
| <p className="text-gray-600">Failed: {data.metrics.failed_requests}</p> | |
| <p className="text-gray-600">Total Tokens: {data.metrics.total_tokens}</p> | |
| <div className="mt-2 border-t pt-2"> | |
| <p className="font-semibold">Spend by {entityType === 'tag' ? 'Tag' : 'Team'}:</p> | |
| {Object.entries(data.breakdown.entities || {}).map(([entity, entityData]) => { | |
| const metrics = entityData as EntityMetrics; | |
| return ( | |
| <p key={entity} className="text-sm text-gray-600"> | |
| {metrics.metadata.team_alias || entity}: ${metrics.metrics.spend.toFixed(2)} | |
| </p> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| }} | |
| /> | |
| </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="metadata.alias" | |
| categories={["metrics.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.metrics.spend > 0) | |
| .map((entity) => ( | |
| <TableRow key={entity.metadata.id}> | |
| <TableCell>{entity.metadata.alias}</TableCell> | |
| <TableCell>${entity.metrics.spend.toFixed(4)}</TableCell> | |
| <TableCell className="text-green-600"> | |
| {entity.metrics.successful_requests.toLocaleString()} | |
| </TableCell> | |
| <TableCell className="text-red-600"> | |
| {entity.metrics.failed_requests.toLocaleString()} | |
| </TableCell> | |
| <TableCell>{entity.metrics.total_tokens.toLocaleString()}</TableCell> | |
| </TableRow> | |
| ))} | |
| </TableBody> | |
| </Table> | |
| </Col> | |
| </Grid> | |
| </div> | |
| </Card> | |
| </Col> | |
| {/* Top API Keys */} | |
| <Col numColSpan={1}> | |
| <Card> | |
| <Title>Top API Keys</Title> | |
| <TopKeyView | |
| topKeys={getTopAPIKeys()} | |
| accessToken={accessToken} | |
| userID={userID} | |
| userRole={userRole} | |
| teams={null} | |
| /> | |
| </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)}`} | |
| 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> | |
| <TabPanel> | |
| <ActivityMetrics modelMetrics={keyMetrics} /> | |
| </TabPanel> | |
| </TabPanels> | |
| </TabGroup> | |
| </div> | |
| ); | |
| }; | |
| export default EntityUsage; |