@@ -3,7 +3,7 @@ import Chart from 'react-apexcharts';
33import type { ApexOptions } from 'apexcharts' ;
44import { DollarSign , TrendingUp , TrendingDown , Users , Bot , AlertTriangle , Download , Calendar } from 'lucide-react' ;
55import { Card , StatCard , Badge , Button , PageHeader , Table , Tabs } from '../components/ui' ;
6- import { useUsageSummary , useUsageByDepartment , useUsageByAgent , useUsageBudgets , useUsageTrend } from '../hooks/useApi' ;
6+ import { useUsageSummary , useUsageByDepartment , useUsageByAgent , useUsageBudgets , useUsageTrend , useUsageByModel } from '../hooks/useApi' ;
77
88const costTrendOpts : ApexOptions = {
99 chart : { type : 'area' , toolbar : { show : false } , background : 'transparent' } ,
@@ -22,6 +22,7 @@ export default function Usage() {
2222 const { data : summary } = useUsageSummary ( ) ;
2323 const { data : byDept = [ ] } = useUsageByDepartment ( ) ;
2424 const { data : byAgent = [ ] } = useUsageByAgent ( ) ;
25+ const { data : byModel = [ ] } = useUsageByModel ( ) ;
2526 const { data : budgets = [ ] } = useUsageBudgets ( ) ;
2627 const { data : trend = [ ] } = useUsageTrend ( ) ;
2728 const [ activeTab , setActiveTab ] = useState ( 'department' ) ;
@@ -170,48 +171,61 @@ export default function Usage() {
170171 < Chart
171172 options = { {
172173 chart : { type : 'donut' , background : 'transparent' } ,
173- colors : [ '#22c55e' , '#6366f1' , '#f59e0b' ] ,
174- labels : [ 'Nova 2 Lite' , 'Claude Sonnet 4.5' , 'Nova Pro' ] ,
174+ colors : [ '#22c55e' , '#6366f1' , '#f59e0b' , '#06b6d4' , '#ef4444' ] ,
175+ labels : byModel . map ( m => m . model . split ( '/' ) . pop ( ) ?. split ( ':' ) [ 0 ] || m . model ) ,
175176 legend : { position : 'bottom' , labels : { colors : '#94a3b8' } } ,
176- plotOptions : { pie : { donut : { size : '65%' , labels : { show : true , total : { show : true , label : 'Total Tokens' , color : '#94a3b8' , formatter : ( ) => `${ ( ( s . totalInputTokens + s . totalOutputTokens ) / 1000 ) . toFixed ( 0 ) } k` } } } } } ,
177+ plotOptions : { pie : { donut : { size : '65%' , labels : { show : true , total : { show : true , label : 'Total Tokens' , color : '#94a3b8' , formatter : ( ) => `${ ( byModel . reduce ( ( s , m ) => s + m . inputTokens + m . outputTokens , 0 ) / 1000 ) . toFixed ( 0 ) } k` } } } } } ,
177178 dataLabels : { enabled : false } ,
178179 tooltip : { theme : 'dark' } ,
179180 } }
180- series = { [
181- Math . round ( ( s . totalInputTokens + s . totalOutputTokens ) * 0.72 ) ,
182- Math . round ( ( s . totalInputTokens + s . totalOutputTokens ) * 0.18 ) ,
183- Math . round ( ( s . totalInputTokens + s . totalOutputTokens ) * 0.10 ) ,
184- ] }
181+ series = { byModel . map ( m => m . inputTokens + m . outputTokens ) }
185182 type = "donut" height = { 300 }
186183 />
187184 </ div >
188185 < div >
189186 < h3 className = "text-sm font-semibold text-text-primary mb-4" > Cost by Model</ h3 >
190187 < div className = "space-y-4" >
191- { [
192- { model : 'Nova 2 Lite' , id : 'global.amazon.nova-2-lite-v1:0' , requests : Math . round ( s . totalRequests * 0.72 ) , cost : s . totalCost * 0.45 , inputRate : 0.30 , outputRate : 2.50 , color : '#22c55e' , positions : 'Default (all positions)' } ,
193- { model : 'Claude Sonnet 4.5' , id : 'global.anthropic.claude-sonnet-4-5' , requests : Math . round ( s . totalRequests * 0.18 ) , cost : s . totalCost * 0.42 , inputRate : 3.00 , outputRate : 15.00 , color : '#6366f1' , positions : 'SA, SDE (override)' } ,
194- { model : 'Nova Pro' , id : 'us.amazon.nova-pro-v1:0' , requests : Math . round ( s . totalRequests * 0.10 ) , cost : s . totalCost * 0.13 , inputRate : 0.80 , outputRate : 3.20 , color : '#f59e0b' , positions : 'Finance, Legal (override)' } ,
195- ] . map ( m => (
196- < div key = { m . model } className = "rounded-lg bg-dark-bg p-4" >
197- < div className = "flex items-center justify-between mb-2" >
198- < div className = "flex items-center gap-2" >
199- < div className = "w-3 h-3 rounded-full" style = { { backgroundColor : m . color } } />
200- < span className = "text-sm font-medium" > { m . model } </ span >
188+ { ( ( ) => {
189+ const colors = [ '#22c55e' , '#6366f1' , '#f59e0b' , '#06b6d4' , '#ef4444' ] ;
190+ const modelRates : Record < string , { input : number ; output : number } > = {
191+ 'nova-2-lite' : { input : 0.30 , output : 2.50 } ,
192+ 'nova-pro' : { input : 0.80 , output : 3.20 } ,
193+ 'claude-sonnet' : { input : 3.00 , output : 15.00 } ,
194+ 'claude-haiku' : { input : 0.25 , output : 1.25 } ,
195+ } ;
196+ const totalModelCost = byModel . reduce ( ( s , m ) => s + m . cost , 0 ) ;
197+ return byModel . map ( ( m , i ) => {
198+ const shortName = m . model . split ( '/' ) . pop ( ) ?. split ( ':' ) [ 0 ] || m . model ;
199+ const rateKey = Object . keys ( modelRates ) . find ( k => shortName . toLowerCase ( ) . includes ( k ) ) || '' ;
200+ const rates = modelRates [ rateKey ] || { input : 0.30 , output : 2.50 } ;
201+ const pct = totalModelCost > 0 ? Math . round ( m . cost / totalModelCost * 100 ) : 0 ;
202+ return (
203+ < div key = { m . model } className = "rounded-lg bg-dark-bg p-4" >
204+ < div className = "flex items-center justify-between mb-2" >
205+ < div className = "flex items-center gap-2" >
206+ < div className = "w-3 h-3 rounded-full" style = { { backgroundColor : colors [ i % colors . length ] } } />
207+ < span className = "text-sm font-medium" > { shortName } </ span >
208+ </ div >
209+ < span className = "text-sm font-semibold" style = { { color : colors [ i % colors . length ] } } > ${ m . cost . toFixed ( 4 ) } </ span >
210+ </ div >
211+ < div className = "grid grid-cols-3 gap-3 text-xs text-text-muted" >
212+ < div > < span className = "block text-text-secondary" > { m . requests } </ span > requests</ div >
213+ < div > < span className = "block text-text-secondary" > ${ rates . input } /${ rates . output } </ span > per 1M tokens</ div >
214+ < div > < span className = "block text-text-secondary" > { pct } %</ span > of total cost</ div >
215+ </ div >
201216 </ div >
202- < span className = "text-sm font-semibold" style = { { color : m . color } } > ${ m . cost . toFixed ( 2 ) } </ span >
203- </ div >
204- < div className = "grid grid-cols-3 gap-3 text-xs text-text-muted" >
205- < div > < span className = "block text-text-secondary" > { m . requests } </ span > requests</ div >
206- < div > < span className = "block text-text-secondary" > ${ m . inputRate } /${ m . outputRate } </ span > per 1M tokens</ div >
207- < div > < span className = "block text-text-secondary" > { m . positions } </ span > assigned to</ div >
208- </ div >
209- </ div >
210- ) ) }
211- </ div >
212- < div className = "mt-4 rounded-lg bg-success/5 border border-success/20 p-3 text-xs text-success" >
213- 💡 Nova 2 Lite handles 72% of requests at 45% of cost. Claude Sonnet 4.5 handles 18% of requests but accounts for 42% of cost due to higher per-token pricing.
217+ ) ;
218+ } ) ;
219+ } ) ( ) }
214220 </ div >
221+ { byModel . length === 0 && (
222+ < p className = "text-sm text-text-muted text-center py-8" > No model usage data available</ p >
223+ ) }
224+ { byModel . length > 0 && (
225+ < div className = "mt-4 rounded-lg bg-success/5 border border-success/20 p-3 text-xs text-success" >
226+ 💡 Data from DynamoDB usage records. Real token counts from AgentCore invocations.
227+ </ div >
228+ ) }
215229 </ div >
216230 </ div >
217231 ) }
0 commit comments