Files
ALwrity/frontend/src/components/billing/CostOptimizationRecommendations.tsx

444 lines
17 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Grid,
Chip,
Tooltip,
Button,
Alert,
CircularProgress,
Accordion,
AccordionSummary,
AccordionDetails,
} from '@mui/material';
import { ExpandMore } from '@mui/icons-material';
import { motion } from 'framer-motion';
import {
Lightbulb,
TrendingDown,
DollarSign,
ArrowRight,
Info,
Sparkles
} from 'lucide-react';
// Types
import { UsageLog } from '../../types/billing';
// Services
import { billingService, formatCurrency } from '../../services/billingService';
interface CostOptimizationRecommendationsProps {
userId?: string;
terminalTheme?: boolean;
}
interface OptimizationRecommendation {
id: string;
title: string;
description: string;
potentialSavings: number;
savingsPercentage: number;
category: 'model_switch' | 'provider_switch' | 'usage_pattern' | 'efficiency';
priority: 'high' | 'medium' | 'low';
actionItems: string[];
currentCost: number;
recommendedCost: number;
}
const CostOptimizationRecommendations: React.FC<CostOptimizationRecommendationsProps> = ({
userId,
terminalTheme = false
}) => {
const [usageLogs, setUsageLogs] = useState<UsageLog[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchUsageLogs = async () => {
try {
setLoading(true);
setError(null);
const currentDate = new Date();
const billingPeriod = `${currentDate.getFullYear()}-${String(currentDate.getMonth() + 1).padStart(2, '0')}`;
const response = await billingService.getUsageLogs(1000, 0, undefined, undefined, billingPeriod);
setUsageLogs(response.logs || []);
} catch (err) {
console.error('[CostOptimizationRecommendations] Error fetching usage logs:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch usage logs');
} finally {
setLoading(false);
}
};
fetchUsageLogs();
}, [userId]);
// Analyze usage patterns and generate recommendations
const recommendations = useMemo(() => {
if (usageLogs.length === 0) return [];
const recs: OptimizationRecommendation[] = [];
// 1. Model Switch Recommendations
// Analyze if user is using expensive models when cheaper alternatives exist
const modelUsage = usageLogs.reduce((acc, log) => {
if (!log.model_used || log.cost_total === 0) return acc;
const key = `${log.provider}:${log.model_used}`;
if (!acc[key]) {
acc[key] = { cost: 0, calls: 0, tokens: 0 };
}
acc[key].cost += log.cost_total;
acc[key].calls += 1;
acc[key].tokens += log.tokens_total;
return acc;
}, {} as Record<string, { cost: number; calls: number; tokens: number }>);
// Check for Gemini Pro usage when Flash could work
const geminiProUsage = Object.entries(modelUsage).find(([key]) =>
key.includes('gemini') && key.includes('pro') && !key.includes('flash')
);
if (geminiProUsage) {
const [_, data] = geminiProUsage;
// Estimate Flash cost (typically 10-20x cheaper)
const estimatedFlashCost = data.cost * 0.1; // Conservative estimate
const savings = data.cost - estimatedFlashCost;
if (savings > 0.01) { // Only show if savings > $0.01
recs.push({
id: 'gemini-pro-to-flash',
title: 'Switch Gemini Pro to Flash for Simple Tasks',
description: `You're using Gemini Pro for ${data.calls} calls. Consider using Gemini Flash for simpler tasks - it's 10x cheaper with similar quality for most use cases.`,
potentialSavings: savings,
savingsPercentage: (savings / data.cost) * 100,
category: 'model_switch',
priority: 'high',
actionItems: [
'Use Gemini Flash for content generation',
'Use Gemini Flash for simple Q&A',
'Reserve Gemini Pro for complex reasoning tasks only'
],
currentCost: data.cost,
recommendedCost: estimatedFlashCost
});
}
}
// 2. Provider Cost Analysis
const providerCosts = usageLogs.reduce((acc, log) => {
if (log.cost_total === 0) return acc;
if (!acc[log.provider]) {
acc[log.provider] = { cost: 0, calls: 0, avgCostPerCall: 0 };
}
acc[log.provider].cost += log.cost_total;
acc[log.provider].calls += 1;
acc[log.provider].avgCostPerCall = acc[log.provider].cost / acc[log.provider].calls;
return acc;
}, {} as Record<string, { cost: number; calls: number; avgCostPerCall: number }>);
// Find expensive providers
const sortedProviders = Object.entries(providerCosts)
.sort(([, a], [, b]) => b.avgCostPerCall - a.avgCostPerCall);
if (sortedProviders.length > 1) {
const [mostExpensive, secondMost] = sortedProviders;
const [expensiveProvider, expensiveData] = mostExpensive;
const [alternativeProvider, alternativeData] = secondMost;
// If expensive provider has significantly higher cost per call
if (expensiveData.avgCostPerCall > alternativeData.avgCostPerCall * 1.5 && expensiveData.calls > 10) {
const estimatedAlternativeCost = expensiveData.calls * alternativeData.avgCostPerCall;
const savings = expensiveData.cost - estimatedAlternativeCost;
if (savings > 0.01) {
recs.push({
id: `provider-switch-${expensiveProvider}`,
title: `Consider ${alternativeProvider} for Some Operations`,
description: `${expensiveProvider} costs $${expensiveData.avgCostPerCall.toFixed(4)} per call on average, while ${alternativeProvider} costs $${alternativeData.avgCostPerCall.toFixed(4)}. Consider switching for non-critical operations.`,
potentialSavings: savings,
savingsPercentage: (savings / expensiveData.cost) * 100,
category: 'provider_switch',
priority: 'medium',
actionItems: [
`Use ${alternativeProvider} for batch operations`,
`Reserve ${expensiveProvider} for high-priority tasks only`,
'Review operation requirements to identify switchable tasks'
],
currentCost: expensiveData.cost,
recommendedCost: estimatedAlternativeCost
});
}
}
}
// 3. Usage Pattern Analysis - High Token Usage
const highTokenUsage = usageLogs.filter(log =>
log.tokens_total > 10000 && log.cost_total > 0.01
);
if (highTokenUsage.length > 5) {
const totalHighTokenCost = highTokenUsage.reduce((sum, log) => sum + log.cost_total, 0);
const avgTokensPerCall = highTokenUsage.reduce((sum, log) => sum + log.tokens_total, 0) / highTokenUsage.length;
recs.push({
id: 'optimize-high-token-usage',
title: 'Optimize High Token Usage Operations',
description: `You have ${highTokenUsage.length} operations using >10K tokens each. Consider breaking down large requests or using more efficient prompts.`,
potentialSavings: totalHighTokenCost * 0.15, // Estimate 15% savings
savingsPercentage: 15,
category: 'usage_pattern',
priority: 'medium',
actionItems: [
'Break down large requests into smaller chunks',
'Use more concise prompts',
'Implement result caching for repeated queries',
`Average tokens per call: ${Math.round(avgTokensPerCall).toLocaleString()}`
],
currentCost: totalHighTokenCost,
recommendedCost: totalHighTokenCost * 0.85
});
}
// 4. Efficiency - Failed Requests
const failedRequests = usageLogs.filter(log => log.status === 'failed' && log.cost_total > 0);
if (failedRequests.length > 0) {
const failedCost = failedRequests.reduce((sum, log) => sum + log.cost_total, 0);
const failureRate = (failedRequests.length / usageLogs.length) * 100;
if (failureRate > 5) { // More than 5% failure rate
recs.push({
id: 'reduce-failed-requests',
title: 'Reduce Failed API Requests',
description: `${failedRequests.length} requests failed (${failureRate.toFixed(1)}% failure rate), costing $${failedCost.toFixed(4)}. Improve error handling and retry logic.`,
potentialSavings: failedCost * 0.8, // Can save 80% by preventing failures
savingsPercentage: 80,
category: 'efficiency',
priority: 'high',
actionItems: [
'Review and fix error-prone operations',
'Implement better retry logic',
'Add input validation before API calls',
'Monitor error patterns and address root causes'
],
currentCost: failedCost,
recommendedCost: failedCost * 0.2
});
}
}
// Sort by priority and potential savings
return recs.sort((a, b) => {
const priorityOrder = { high: 3, medium: 2, low: 1 };
if (priorityOrder[a.priority] !== priorityOrder[b.priority]) {
return priorityOrder[b.priority] - priorityOrder[a.priority];
}
return b.potentialSavings - a.potentialSavings;
});
}, [usageLogs]);
const totalPotentialSavings = recommendations.reduce((sum, rec) => sum + rec.potentialSavings, 0);
const totalCurrentCost = usageLogs.reduce((sum, log) => sum + log.cost_total, 0);
if (loading) {
return (
<Card sx={{ p: 3, textAlign: 'center' }}>
<CircularProgress />
<Typography sx={{ mt: 2, color: 'rgba(255,255,255,0.7)' }}>
Analyzing usage patterns...
</Typography>
</Card>
);
}
if (error) {
return (
<Card sx={{ p: 3 }}>
<Alert severity="error">{error}</Alert>
</Card>
);
}
if (recommendations.length === 0) {
return (
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
}}
>
<CardContent sx={{ textAlign: 'center', py: 4 }}>
<Sparkles size={48} color="#22c55e" style={{ marginBottom: 16 }} />
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#ffffff', mb: 1 }}>
Great Job!
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Your usage patterns are already optimized. No recommendations at this time.
</Typography>
</CardContent>
</Card>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card
sx={{
height: '100%',
background: 'linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 3,
}}
>
<CardContent sx={{ pb: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 'bold', color: '#ffffff' }}>
<Lightbulb size={20} />
Cost Optimization Recommendations
</Typography>
<Tooltip title="AI-powered suggestions to reduce your API costs">
<Info size={16} color="rgba(255,255,255,0.7)" />
</Tooltip>
</Box>
{/* Summary */}
<Box
sx={{
p: 2.5,
mb: 3,
background: 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.1) 100%)',
borderRadius: 2,
border: '1px solid rgba(34, 197, 94, 0.3)',
textAlign: 'center'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, mb: 1 }}>
<TrendingDown size={24} color="#22c55e" />
<Typography variant="h5" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(totalPotentialSavings)}
</Typography>
</Box>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.9)', mb: 0.5 }}>
Potential Monthly Savings
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
{recommendations.length} recommendation{recommendations.length !== 1 ? 's' : ''}
{totalCurrentCost > 0 ? ` ${((totalPotentialSavings / totalCurrentCost) * 100).toFixed(1)}% of current spending` : ''}
</Typography>
</Box>
</CardContent>
<CardContent sx={{ pt: 0 }}>
{/* Recommendations List */}
{recommendations.map((rec, index) => (
<Accordion
key={rec.id}
sx={{
mb: 2,
backgroundColor: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 2,
'&:before': { display: 'none' },
boxShadow: 'none'
}}
>
<AccordionSummary
expandIcon={<ExpandMore sx={{ color: 'rgba(255,255,255,0.7)' }} />}
sx={{ px: 2, py: 1.5 }}
>
<Box sx={{ width: '100%', display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ flex: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', color: '#ffffff' }}>
{rec.title}
</Typography>
<Chip
label={rec.priority}
size="small"
sx={{
backgroundColor: rec.priority === 'high' ? 'rgba(239, 68, 68, 0.2)' :
rec.priority === 'medium' ? 'rgba(245, 158, 11, 0.2)' :
'rgba(100, 116, 139, 0.2)',
color: rec.priority === 'high' ? '#ef4444' :
rec.priority === 'medium' ? '#f59e0b' :
'#64748b',
fontWeight: 'bold',
fontSize: '0.7rem',
height: 20
}}
/>
</Box>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)', display: 'block' }}>
{rec.description}
</Typography>
</Box>
<Box sx={{ textAlign: 'right', minWidth: 120 }}>
<Typography variant="h6" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(rec.potentialSavings)}
</Typography>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.6)' }}>
{rec.savingsPercentage.toFixed(1)}% savings
</Typography>
</Box>
</Box>
</AccordionSummary>
<AccordionDetails sx={{ px: 2, pb: 2 }}>
<Box sx={{ mb: 2 }}>
<Grid container spacing={2}>
<Grid item xs={6}>
<Box sx={{ p: 1.5, backgroundColor: 'rgba(239, 68, 68, 0.1)', borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Current Cost
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#ef4444' }}>
{formatCurrency(rec.currentCost)}
</Typography>
</Box>
</Grid>
<Grid item xs={6}>
<Box sx={{ p: 1.5, backgroundColor: 'rgba(34, 197, 94, 0.1)', borderRadius: 1 }}>
<Typography variant="caption" sx={{ color: 'rgba(255,255,255,0.7)' }}>
Recommended Cost
</Typography>
<Typography variant="body2" sx={{ fontWeight: 'bold', color: '#22c55e' }}>
{formatCurrency(rec.recommendedCost)}
</Typography>
</Box>
</Grid>
</Grid>
</Box>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1, color: '#ffffff' }}>
Action Items:
</Typography>
<Box component="ul" sx={{ m: 0, pl: 2 }}>
{rec.actionItems.map((item, idx) => (
<li key={idx}>
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.8)', mb: 0.5 }}>
{item}
</Typography>
</li>
))}
</Box>
</AccordionDetails>
</Accordion>
))}
</CardContent>
</Card>
</motion.div>
);
};
export default CostOptimizationRecommendations;