fix(usage): refactor usage logs to utilize React Query for model pricing and enhance error handling

This commit is contained in:
sharath 2025-06-25 21:47:21 +00:00
parent f2dc615f06
commit af869db43f
No known key found for this signature in database
1 changed files with 143 additions and 38 deletions

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState, useMemo } from 'react';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { import {
Card, Card,
@ -28,6 +28,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ExternalLink, Loader2 } from 'lucide-react'; import { ExternalLink, Loader2 } from 'lucide-react';
import { isLocalMode } from '@/lib/config'; import { isLocalMode } from '@/lib/config';
import { useAvailableModels } from '@/hooks/react-query/subscriptions/use-model';
interface UsageLogEntry { interface UsageLogEntry {
message_id: string; message_id: string;
@ -41,7 +42,7 @@ interface UsageLogEntry {
model: string; model: string;
}; };
total_tokens: number; total_tokens: number;
estimated_cost: number; estimated_cost: number | string;
project_id: string; project_id: string;
} }
@ -65,45 +66,137 @@ export default function UsageLogs({ accountId }: Props) {
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [modelPricing, setModelPricing] = useState<
Record<string, { input: number; output: number }> // Use React Query hook instead of manual fetching
>({}); const {
data: modelsData,
isLoading: isLoadingModels,
error: modelsError,
} = useAvailableModels();
const ITEMS_PER_PAGE = 1000; const ITEMS_PER_PAGE = 1000;
const fetchModelPricing = async () => { // Helper function to normalize model names for better matching
try { const normalizeModelName = (name: string): string => {
const response = await fetch('/api/billing/available-models'); return name
if (!response.ok) throw new Error('Failed to fetch model pricing'); .toLowerCase()
.replace(/[-_.]/g, '') // Remove hyphens, underscores, dots
.replace(/\s+/g, '') // Remove spaces
.replace(/latest$/, '') // Remove 'latest' suffix
.replace(/preview$/, '') // Remove 'preview' suffix
.replace(/\d{8}$/, ''); // Remove date suffixes like 20250514
};
const data = await response.json(); // Helper function to find matching pricing for a model
const pricing: Record<string, { input: number; output: number }> = {}; const findModelPricing = (
modelName: string,
pricingData: Record<string, { input: number; output: number }>,
) => {
// Direct match first
if (pricingData[modelName]) {
return pricingData[modelName];
}
data.models.forEach((model: any) => { // Try normalized matching
if ( const normalizedTarget = normalizeModelName(modelName);
model.input_cost_per_million_tokens &&
model.output_cost_per_million_tokens for (const [pricingKey, pricingValue] of Object.entries(pricingData)) {
) { const normalizedKey = normalizeModelName(pricingKey);
pricing[model.id] = {
// Exact normalized match
if (normalizedKey === normalizedTarget) {
return pricingValue;
}
// Partial matches - check if one contains the other
if (
normalizedKey.includes(normalizedTarget) ||
normalizedTarget.includes(normalizedKey)
) {
return pricingValue;
}
// Try matching without provider prefix from pricing key
const keyWithoutProvider = pricingKey.replace(/^[^\/]+\//, '');
const normalizedKeyWithoutProvider =
normalizeModelName(keyWithoutProvider);
if (
normalizedKeyWithoutProvider === normalizedTarget ||
normalizedKeyWithoutProvider.includes(normalizedTarget) ||
normalizedTarget.includes(normalizedKeyWithoutProvider)
) {
return pricingValue;
}
// Try matching the end part of the pricing key with the model name
const pricingKeyParts = pricingKey.split('/');
const lastPart = pricingKeyParts[pricingKeyParts.length - 1];
const normalizedLastPart = normalizeModelName(lastPart);
if (
normalizedLastPart === normalizedTarget ||
normalizedLastPart.includes(normalizedTarget) ||
normalizedTarget.includes(normalizedLastPart)
) {
return pricingValue;
}
}
console.log(`No pricing match found for: "${modelName}"`);
return null;
};
// Create pricing lookup from models data
const modelPricing = useMemo(() => {
if (!modelsData?.models) {
return {};
}
const pricing: Record<string, { input: number; output: number }> = {};
modelsData.models.forEach((model) => {
if (
model.input_cost_per_million_tokens &&
model.output_cost_per_million_tokens
) {
// Use the model.id as the key, which should match the model names in usage logs
pricing[model.id] = {
input: model.input_cost_per_million_tokens,
output: model.output_cost_per_million_tokens,
};
// Also try to match by display_name and short_name if they exist
if (model.display_name && model.display_name !== model.id) {
pricing[model.display_name] = {
input: model.input_cost_per_million_tokens, input: model.input_cost_per_million_tokens,
output: model.output_cost_per_million_tokens, output: model.output_cost_per_million_tokens,
}; };
} }
});
setModelPricing(pricing); if (model.short_name && model.short_name !== model.id) {
} catch (error) { pricing[model.short_name] = {
console.error('Error fetching model pricing:', error); input: model.input_cost_per_million_tokens,
} output: model.output_cost_per_million_tokens,
}; };
}
}
});
console.log(
'Pricing lookup ready with',
Object.keys(pricing).length,
'entries',
);
return pricing;
}, [modelsData, isLoadingModels, modelsError]);
const calculateTokenCost = ( const calculateTokenCost = (
promptTokens: number, promptTokens: number,
completionTokens: number, completionTokens: number,
model: string, model: string,
): number => { ): number | string => {
// Use fetched pricing data from available-models API // Use the more lenient matching function
const costs = modelPricing[model]; const costs = findModelPricing(model, modelPricing);
if (costs) { if (costs) {
// Convert from per-million to per-token costs // Convert from per-million to per-token costs
@ -113,9 +206,8 @@ export default function UsageLogs({ accountId }: Props) {
); );
} }
// Fallback to a reasonable average if no pricing data available // Return "unknown" instead of fallback cost
const fallbackCost = 0.002; // per 1K tokens return 'unknown';
return ((promptTokens + completionTokens) / 1000) * fallbackCost;
}; };
const fetchUsageLogs = async ( const fetchUsageLogs = async (
@ -211,9 +303,11 @@ export default function UsageLogs({ accountId }: Props) {
}; };
useEffect(() => { useEffect(() => {
fetchModelPricing(); // Only fetch usage logs after models data is loaded
fetchUsageLogs(0, false); if (!isLoadingModels && modelsData) {
}, [accountId]); fetchUsageLogs(0, false);
}
}, [accountId, isLoadingModels, modelsData]);
const loadMore = () => { const loadMore = () => {
const nextPage = page + 1; const nextPage = page + 1;
@ -225,7 +319,10 @@ export default function UsageLogs({ accountId }: Props) {
return new Date(dateString).toLocaleString(); return new Date(dateString).toLocaleString();
}; };
const formatCost = (cost: number) => { const formatCost = (cost: number | string) => {
if (typeof cost === 'string') {
return cost;
}
return `$${cost.toFixed(4)}`; return `$${cost.toFixed(4)}`;
}; };
@ -263,7 +360,8 @@ export default function UsageLogs({ accountId }: Props) {
acc[date].logs.push(log); acc[date].logs.push(log);
acc[date].totalTokens += log.total_tokens; acc[date].totalTokens += log.total_tokens;
acc[date].totalCost += log.estimated_cost; acc[date].totalCost +=
typeof log.estimated_cost === 'number' ? log.estimated_cost : 0;
acc[date].requestCount += 1; acc[date].requestCount += 1;
if (!acc[date].models.includes(log.content.model)) { if (!acc[date].models.includes(log.content.model)) {
@ -297,7 +395,7 @@ export default function UsageLogs({ accountId }: Props) {
); );
} }
if (loading) { if (loading || isLoadingModels) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@ -315,7 +413,7 @@ export default function UsageLogs({ accountId }: Props) {
); );
} }
if (error) { if (error || modelsError) {
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@ -323,7 +421,13 @@ export default function UsageLogs({ accountId }: Props) {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg"> <div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg">
<p className="text-sm text-destructive">Error: {error}</p> <p className="text-sm text-destructive">
Error:{' '}
{error ||
(modelsError instanceof Error
? modelsError.message
: 'Failed to load data')}
</p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -332,7 +436,8 @@ export default function UsageLogs({ accountId }: Props) {
const dailyUsage = groupLogsByDate(usageLogs); const dailyUsage = groupLogsByDate(usageLogs);
const totalUsage = usageLogs.reduce( const totalUsage = usageLogs.reduce(
(sum, log) => sum + log.estimated_cost, (sum, log) =>
sum + (typeof log.estimated_cost === 'number' ? log.estimated_cost : 0),
0, 0,
); );