11import { clsx , type ClassValue } from "clsx"
22import { twMerge } from "tailwind-merge"
3+ import { defaultServerMainFields } from "vite" ;
34
45export function cn ( ...inputs : ClassValue [ ] ) {
56 return twMerge ( clsx ( inputs ) )
@@ -196,8 +197,10 @@ export function getModelUsageSummary(data: CopilotUsageData[]): ModelUsageSummar
196197
197198 data . forEach ( item => {
198199 if ( ! summary [ item . model ] ) {
199- const multiplier = MODEL_MULTIPLIERS [ item . model ] || 1 ;
200- const displayName = DEFAULT_MODELS . includes ( item . model ) ? 'Default' : item . model ;
200+ const multiplier = getModelMultiplier ( item . model ) ;
201+ const displayName = isDefaultModel ( item . model ) ? 'Default' : item . model ;
202+
203+ console . log ( `Processing model: ${ item . model } ; Multiplier: ${ multiplier } ; Display Name: ${ displayName } ` ) ;
201204
202205 summary [ item . model ] = {
203206 model : item . model ,
@@ -333,26 +336,58 @@ export const PLAN_MONTHLY_LIMITS = {
333336} as const ;
334337
335338// Model multipliers based on GitHub documentation (for paid plans)
339+ // Uses display names as they appear in GitHub Copilot exports.
336340export const MODEL_MULTIPLIERS : Record < string , number > = {
337- // Default models (0x multiplier for paid plans)
341+ // Current default models (0x multiplier for paid plans)
342+ 'GPT-4.1' : 0 ,
343+ 'GPT-4o' : 0 ,
344+ 'GPT-5 mini' : 0 ,
345+ 'Raptor mini' : 0 ,
346+
347+ // OpenAI models
348+ 'GPT-5.1' : 1 ,
349+ 'GPT-5.1-Codex' : 1 ,
350+ 'GPT-5.1-Codex-Mini' : 0.33 ,
351+ 'GPT-5.1-Codex-Max' : 1 ,
352+ 'GPT-5.2' : 1 ,
353+ 'GPT-5.2-Codex' : 1 ,
354+ 'GPT-5.3-Codex' : 1 ,
355+ 'GPT-5.4' : 1 ,
356+ 'GPT-5.4 mini' : 0.33 ,
357+
358+ // Anthropic models
359+ 'Claude Haiku 4.5' : 0.33 ,
360+ 'Claude Sonnet 4' : 1 ,
361+ 'Claude Sonnet 4.5' : 1 ,
362+ 'Claude Sonnet 4.6' : 1 ,
363+ 'Claude Opus 4.5' : 3 ,
364+ 'Claude Opus 4.6' : 3 ,
365+ 'Claude Opus 4.6 (fast mode) (preview)' : 30 ,
366+
367+ // Google models
368+ 'Gemini 2.5 Pro' : 1 ,
369+ 'Gemini 3 Flash' : 0.33 ,
370+ 'Gemini 3 Pro' : 1 ,
371+ 'Gemini 3.1 Pro' : 1 ,
372+
373+ // xAI and other models
374+ 'Grok Code Fast 1' : 0.25 ,
375+ 'Goldeneye' : 1 ,
376+
377+ // Backward compatibility for older exports
338378 'gpt-4o-2024-11-20' : 0 ,
339379 'gpt-4.1-2025-04-14' : 0 ,
340380 'gpt-4o' : 0 ,
341381 'gpt-4.1' : 0 ,
342- // GPT-4.5 models
343382 'gpt-4.5' : 50 ,
344- // Vision models
345383 'gpt-4.1-vision' : 0 ,
346- // Claude models
347384 'claude-sonnet-3.5' : 1 ,
348385 'claude-sonnet-3.7' : 1 ,
349386 'claude-sonnet-3.7-thinking' : 1.25 ,
350387 'claude-sonnet-4' : 1 ,
351388 'claude-opus-4' : 10 ,
352- // Gemini models
353389 'gemini-2.0-flash' : 0.25 ,
354390 'gemini-2.5-pro' : 1 ,
355- // O-series models
356391 'o1' : 10 ,
357392 'o3' : 1 ,
358393 'o3-mini' : 0.33 ,
@@ -363,7 +398,19 @@ export const MODEL_MULTIPLIERS: Record<string, number> = {
363398} ;
364399
365400// Default models that should be grouped
366- export const DEFAULT_MODELS = [ 'gpt-4o-2024-11-20' , 'gpt-4.1-2025-04-14' ] ;
401+ export const DEFAULT_MODELS = [ 'GPT-4o' , 'GPT-4.1' , 'gpt-4o-2024-11-20' , 'gpt-4.1-2025-04-14' ] ;
402+
403+ function normalizeModelName ( model : string ) : string {
404+ return model . replace ( / ^ A u t o : \s * / , '' ) . trim ( ) ;
405+ }
406+
407+ function getModelMultiplier ( model : string ) : number {
408+ return MODEL_MULTIPLIERS [ normalizeModelName ( model ) ] ?? 1 ;
409+ }
410+
411+ function isDefaultModel ( model : string ) : boolean {
412+ return DEFAULT_MODELS . includes ( normalizeModelName ( model ) ) ;
413+ }
367414
368415// Cost per excess request (in USD) for premium requests
369416export const EXCESS_REQUEST_COST = 0.04 ; // $0.04 per excess request
@@ -788,6 +835,91 @@ export function getProjectedUsersExceedingQuotaDetails(data: CopilotUsageData[],
788835 return projectedUsers . sort ( ( a , b ) => b . projectedMonthlyTotal - a . projectedMonthlyTotal ) ;
789836}
790837
838+ /**
839+ * Calculate the total expected excess cost if there were no monthly quota limit.
840+ * For each user who has reached the plan limit:
841+ * 1. Find the day their cumulative requests hit the limit (budget exhaustion day).
842+ * 2. Compute daily average requests per model, excluding the last usage day (to avoid partial-day skew).
843+ * 3. Project those requests over the remaining days after the exhaustion day.
844+ * 4. Apply each model's cost multiplier and sum the cost at $0.04/PRU.
845+ */
846+ export function getExpectedExcessCost ( data : CopilotUsageData [ ] , plan : string = COPILOT_PLANS . BUSINESS ) : number {
847+ if ( ! data . length ) return 0 ;
848+
849+ const planLimit = ( PLAN_MONTHLY_LIMITS as Record < string , number > ) [ plan ] ?? PLAN_MONTHLY_LIMITS [ COPILOT_PLANS . BUSINESS ] ;
850+
851+ const lastDate = getLastDateFromData ( data ) ;
852+ if ( ! lastDate ) return 0 ;
853+
854+ const lastDateObj = new Date ( lastDate ) ;
855+ const totalDaysInMonth = new Date ( lastDateObj . getFullYear ( ) , lastDateObj . getMonth ( ) + 1 , 0 ) . getDate ( ) ;
856+
857+ // Group all items by user
858+ const userDataMap : Record < string , CopilotUsageData [ ] > = { } ;
859+ data . forEach ( item => {
860+ if ( ! userDataMap [ item . user ] ) userDataMap [ item . user ] = [ ] ;
861+ userDataMap [ item . user ] . push ( item ) ;
862+ } ) ;
863+
864+ let totalExpectedCost = 0 ;
865+
866+ Object . values ( userDataMap ) . forEach ( userItems => {
867+ // Only process users who have reached the plan limit
868+ const totalRequests = userItems . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) ;
869+ if ( totalRequests < planLimit ) return ;
870+
871+ // Group by date (YYYY-MM-DD), sorted ascending
872+ const byDate : Record < string , CopilotUsageData [ ] > = { } ;
873+ userItems . forEach ( item => {
874+ const date = item . timestamp . toISOString ( ) . split ( 'T' ) [ 0 ] ;
875+ if ( ! byDate [ date ] ) byDate [ date ] = [ ] ;
876+ byDate [ date ] . push ( item ) ;
877+ } ) ;
878+ const sortedDates = Object . keys ( byDate ) . sort ( ) ;
879+
880+ // Find the day cumulative requests first reach the plan limit
881+ let cumulative = 0 ;
882+ let budgetExhaustionDayOfMonth : number | null = null ;
883+ for ( const date of sortedDates ) {
884+ const dayTotal = byDate [ date ] . reduce ( ( sum , item ) => sum + item . requestsUsed , 0 ) ;
885+ cumulative += dayTotal ;
886+ if ( cumulative >= planLimit ) {
887+ budgetExhaustionDayOfMonth = new Date ( date ) . getDate ( ) ;
888+ break ;
889+ }
890+ }
891+ if ( budgetExhaustionDayOfMonth === null ) return ;
892+
893+ const remainingDays = totalDaysInMonth - budgetExhaustionDayOfMonth ;
894+ if ( remainingDays <= 0 ) return ;
895+
896+ // Compute daily average per model, excluding the last usage day (which may be partial)
897+ const lastUsageDate = sortedDates [ sortedDates . length - 1 ] ;
898+ const datesForAverage = sortedDates . filter ( d => d !== lastUsageDate ) ;
899+ if ( datesForAverage . length === 0 ) return ;
900+
901+ const modelTotals : Record < string , number > = { } ;
902+ datesForAverage . forEach ( date => {
903+ byDate [ date ] . forEach ( item => {
904+ modelTotals [ item . model ] = ( modelTotals [ item . model ] || 0 ) + item . requestsUsed ;
905+ } ) ;
906+ } ) ;
907+
908+ const numDays = datesForAverage . length ;
909+
910+ // Project extra requests per model over remaining days and apply cost multiplier
911+ Object . entries ( modelTotals ) . forEach ( ( [ model , total ] ) => {
912+ const multiplier = getModelMultiplier ( model ) ;
913+ if ( multiplier === 0 ) return ; // Free models have no cost
914+ const dailyAvg = total / numDays ;
915+ const projectedExtra = dailyAvg * remainingDays ;
916+ totalExpectedCost += projectedExtra * multiplier * EXCESS_REQUEST_COST ;
917+ } ) ;
918+ } ) ;
919+
920+ return totalExpectedCost ;
921+ }
922+
791923// Re-export month utilities for backward compatibility
792924export { getAvailableMonths , filterDataByMonth , getMonthCoverage } from './month-utils' ;
793925export type { MonthOption } from '../types/month' ;
0 commit comments