Skip to content

Commit 7629db3

Browse files
Copilotrajbos
andcommitted
Implement model info and limits feature with grouping and cost calculation
Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com>
1 parent 67414f8 commit 7629db3

3 files changed

Lines changed: 256 additions & 4 deletions

File tree

src/App.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,7 @@ function App() {
492492
</div>
493493

494494
{/* Model Usage Table */}
495-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
495+
<div className="mb-6">
496496
<Card className="p-5">
497497
<h3 className="text-md font-medium mb-3">Requests per Model</h3>
498498
<div className="overflow-auto max-h-60">
@@ -503,6 +503,10 @@ function App() {
503503
<TableHead className="text-right">Total Requests</TableHead>
504504
<TableHead className="text-right">Compliant</TableHead>
505505
<TableHead className="text-right">Exceeding</TableHead>
506+
<TableHead className="text-right">Multiplier</TableHead>
507+
<TableHead className="text-right">Individual Limit</TableHead>
508+
<TableHead className="text-right">Business/Enterprise Limit</TableHead>
509+
<TableHead className="text-right">Excess Cost</TableHead>
506510
</TableRow>
507511
</TableHeader>
508512
<TableBody>
@@ -512,13 +516,20 @@ function App() {
512516
<TableCell className="text-right">{item.totalRequests.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</TableCell>
513517
<TableCell className="text-right">{item.compliantRequests.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</TableCell>
514518
<TableCell className="text-right">{item.exceedingRequests.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 0})}</TableCell>
519+
<TableCell className="text-right">{item.multiplier}x</TableCell>
520+
<TableCell className="text-right">{item.individualPlanLimit.toLocaleString()}</TableCell>
521+
<TableCell className="text-right">{item.businessPlanLimit.toLocaleString()}</TableCell>
522+
<TableCell className="text-right">${item.excessCost.toFixed(2)}</TableCell>
515523
</TableRow>
516524
))}
517525
</TableBody>
518526
</Table>
519527
</div>
520528
</Card>
521-
529+
</div>
530+
531+
{/* Models List Card */}
532+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
522533
<Card className="p-5">
523534
<h3 className="text-md font-medium mb-3">Models List</h3>
524535
<div className="overflow-auto max-h-60">

src/lib/utils.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,15 @@ export function parseCSV(csv: string): CopilotUsageData[] {
122122

123123
export interface ModelUsageSummary {
124124
model: string;
125+
displayName: string; // For grouping default models
125126
totalRequests: number;
126127
compliantRequests: number;
127128
exceedingRequests: number;
129+
multiplier: number;
130+
individualPlanLimit: number;
131+
businessPlanLimit: number;
132+
enterprisePlanLimit: number;
133+
excessCost: number; // Cost for exceeding requests
128134
}
129135

130136
export interface DailyModelData {
@@ -168,11 +174,20 @@ export function getModelUsageSummary(data: CopilotUsageData[]): ModelUsageSummar
168174

169175
data.forEach(item => {
170176
if (!summary[item.model]) {
177+
const multiplier = MODEL_MULTIPLIERS[item.model] || 1;
178+
const displayName = DEFAULT_MODELS.includes(item.model) ? 'Default' : item.model;
179+
171180
summary[item.model] = {
172181
model: item.model,
182+
displayName,
173183
totalRequests: 0,
174184
compliantRequests: 0,
175-
exceedingRequests: 0
185+
exceedingRequests: 0,
186+
multiplier,
187+
individualPlanLimit: Math.floor(PLAN_MONTHLY_LIMITS[COPILOT_PLANS.INDIVIDUAL] / multiplier),
188+
businessPlanLimit: Math.floor(PLAN_MONTHLY_LIMITS[COPILOT_PLANS.BUSINESS] / multiplier),
189+
enterprisePlanLimit: Math.floor(PLAN_MONTHLY_LIMITS[COPILOT_PLANS.ENTERPRISE] / multiplier),
190+
excessCost: 0
176191
};
177192
}
178193

@@ -185,8 +200,31 @@ export function getModelUsageSummary(data: CopilotUsageData[]): ModelUsageSummar
185200
}
186201
});
187202

203+
// Calculate excess costs and group default models
204+
const summaryArray = Object.values(summary);
205+
const groupedSummary: Record<string, ModelUsageSummary> = {};
206+
207+
summaryArray.forEach(item => {
208+
const key = item.displayName;
209+
210+
if (!groupedSummary[key]) {
211+
groupedSummary[key] = {
212+
...item,
213+
model: key === 'Default' ? 'Default (GPT-4o, GPT-4.1)' : item.model
214+
};
215+
} else {
216+
// Merge default models
217+
groupedSummary[key].totalRequests += item.totalRequests;
218+
groupedSummary[key].compliantRequests += item.compliantRequests;
219+
groupedSummary[key].exceedingRequests += item.exceedingRequests;
220+
}
221+
222+
// Calculate excess cost
223+
groupedSummary[key].excessCost = groupedSummary[key].exceedingRequests * groupedSummary[key].multiplier * EXCESS_REQUEST_COST;
224+
});
225+
188226
// Convert to array and sort by total requests (descending)
189-
return Object.values(summary).sort((a, b) => b.totalRequests - a.totalRequests);
227+
return Object.values(groupedSummary).sort((a, b) => b.totalRequests - a.totalRequests);
190228
}
191229

192230
export function getDailyModelData(data: CopilotUsageData[]): DailyModelData[] {
@@ -235,6 +273,38 @@ export interface PowerUserSummary {
235273
// Define power user threshold - users with more than 10 requests
236274
export const POWER_USER_THRESHOLD = 10;
237275

276+
// Copilot plan constants
277+
export const COPILOT_PLANS = {
278+
INDIVIDUAL: 'Individual',
279+
BUSINESS: 'Business',
280+
ENTERPRISE: 'Enterprise'
281+
} as const;
282+
283+
export const PLAN_MONTHLY_LIMITS = {
284+
[COPILOT_PLANS.INDIVIDUAL]: 150,
285+
[COPILOT_PLANS.BUSINESS]: 500,
286+
[COPILOT_PLANS.ENTERPRISE]: 500
287+
} as const;
288+
289+
// Model multipliers based on GitHub documentation
290+
export const MODEL_MULTIPLIERS: Record<string, number> = {
291+
// Default models (1x multiplier)
292+
'gpt-4o-2024-11-20': 1,
293+
'gpt-4.1-2025-04-14': 1,
294+
// Vision models
295+
'gpt-4.1-vision': 1,
296+
// O-series models (higher multipliers)
297+
'o3-mini-2025-01-31': 1,
298+
'o4-mini-2025-04-16': 1,
299+
// Add other models as needed - fallback to 1x for unknown models
300+
};
301+
302+
// Default models that should be grouped
303+
export const DEFAULT_MODELS = ['gpt-4o-2024-11-20', 'gpt-4.1-2025-04-14'];
304+
305+
// Cost per excess request (in USD) for premium requests
306+
export const EXCESS_REQUEST_COST = 0.15; // $0.15 per excess request
307+
238308
export function getPowerUsers(data: CopilotUsageData[]): PowerUserSummary {
239309
// First, aggregate total requests per user
240310
const userTotals: Record<string, number> = {};

src/test/model-info-limits.test.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
getModelUsageSummary,
4+
CopilotUsageData,
5+
COPILOT_PLANS,
6+
PLAN_MONTHLY_LIMITS,
7+
MODEL_MULTIPLIERS,
8+
DEFAULT_MODELS,
9+
EXCESS_REQUEST_COST
10+
} from '../lib/utils';
11+
12+
describe('Model Info and Limits Feature', () => {
13+
const mockData: CopilotUsageData[] = [
14+
{
15+
timestamp: new Date('2025-01-01T10:00:00Z'),
16+
user: 'user1',
17+
model: 'gpt-4o-2024-11-20',
18+
requestsUsed: 100,
19+
exceedsQuota: false,
20+
totalMonthlyQuota: '500'
21+
},
22+
{
23+
timestamp: new Date('2025-01-01T11:00:00Z'),
24+
user: 'user2',
25+
model: 'gpt-4.1-2025-04-14',
26+
requestsUsed: 50,
27+
exceedsQuota: true,
28+
totalMonthlyQuota: '500'
29+
},
30+
{
31+
timestamp: new Date('2025-01-01T12:00:00Z'),
32+
user: 'user3',
33+
model: 'gpt-4.1-vision',
34+
requestsUsed: 25,
35+
exceedsQuota: false,
36+
totalMonthlyQuota: '500'
37+
},
38+
{
39+
timestamp: new Date('2025-01-01T13:00:00Z'),
40+
user: 'user4',
41+
model: 'o3-mini-2025-01-31',
42+
requestsUsed: 10,
43+
exceedsQuota: true,
44+
totalMonthlyQuota: '500'
45+
}
46+
];
47+
48+
it('should calculate plan limits based on model multipliers', () => {
49+
const result = getModelUsageSummary(mockData);
50+
51+
const defaultGroup = result.find(item => item.model === 'Default (GPT-4o, GPT-4.1)');
52+
expect(defaultGroup).toBeDefined();
53+
54+
if (defaultGroup) {
55+
expect(defaultGroup.multiplier).toBe(1);
56+
expect(defaultGroup.individualPlanLimit).toBe(150); // 150 / 1
57+
expect(defaultGroup.businessPlanLimit).toBe(500); // 500 / 1
58+
expect(defaultGroup.enterprisePlanLimit).toBe(500); // 500 / 1
59+
}
60+
});
61+
62+
it('should group default models (GPT-4o and GPT-4.1) together', () => {
63+
const result = getModelUsageSummary(mockData);
64+
65+
const defaultGroup = result.find(item => item.model === 'Default (GPT-4o, GPT-4.1)');
66+
expect(defaultGroup).toBeDefined();
67+
68+
if (defaultGroup) {
69+
// Should combine requests from both gpt-4o-2024-11-20 (100) and gpt-4.1-2025-04-14 (50)
70+
expect(defaultGroup.totalRequests).toBe(150);
71+
expect(defaultGroup.compliantRequests).toBe(100);
72+
expect(defaultGroup.exceedingRequests).toBe(50);
73+
}
74+
75+
// Should not have individual entries for default models
76+
const gpt4oEntry = result.find(item => item.model === 'gpt-4o-2024-11-20');
77+
const gpt41Entry = result.find(item => item.model === 'gpt-4.1-2025-04-14');
78+
expect(gpt4oEntry).toBeUndefined();
79+
expect(gpt41Entry).toBeUndefined();
80+
});
81+
82+
it('should keep non-default models separate', () => {
83+
const result = getModelUsageSummary(mockData);
84+
85+
const visionModel = result.find(item => item.model === 'gpt-4.1-vision');
86+
expect(visionModel).toBeDefined();
87+
expect(visionModel?.totalRequests).toBe(25);
88+
89+
const o3Model = result.find(item => item.model === 'o3-mini-2025-01-31');
90+
expect(o3Model).toBeDefined();
91+
expect(o3Model?.totalRequests).toBe(10);
92+
});
93+
94+
it('should calculate excess costs correctly', () => {
95+
const result = getModelUsageSummary(mockData);
96+
97+
const defaultGroup = result.find(item => item.model === 'Default (GPT-4o, GPT-4.1)');
98+
expect(defaultGroup).toBeDefined();
99+
100+
if (defaultGroup) {
101+
// 50 exceeding requests * 1x multiplier * $0.15 = $7.50
102+
expect(defaultGroup.excessCost).toBe(50 * 1 * EXCESS_REQUEST_COST);
103+
}
104+
105+
const o3Model = result.find(item => item.model === 'o3-mini-2025-01-31');
106+
if (o3Model) {
107+
// 10 exceeding requests * 1x multiplier * $0.15 = $1.50
108+
expect(o3Model.excessCost).toBe(10 * 1 * EXCESS_REQUEST_COST);
109+
}
110+
});
111+
112+
it('should include all required fields in ModelUsageSummary', () => {
113+
const result = getModelUsageSummary(mockData);
114+
115+
expect(result.length).toBeGreaterThan(0);
116+
117+
result.forEach(item => {
118+
expect(item).toHaveProperty('model');
119+
expect(item).toHaveProperty('displayName');
120+
expect(item).toHaveProperty('totalRequests');
121+
expect(item).toHaveProperty('compliantRequests');
122+
expect(item).toHaveProperty('exceedingRequests');
123+
expect(item).toHaveProperty('multiplier');
124+
expect(item).toHaveProperty('individualPlanLimit');
125+
expect(item).toHaveProperty('businessPlanLimit');
126+
expect(item).toHaveProperty('enterprisePlanLimit');
127+
expect(item).toHaveProperty('excessCost');
128+
129+
// Validate types
130+
expect(typeof item.model).toBe('string');
131+
expect(typeof item.displayName).toBe('string');
132+
expect(typeof item.totalRequests).toBe('number');
133+
expect(typeof item.compliantRequests).toBe('number');
134+
expect(typeof item.exceedingRequests).toBe('number');
135+
expect(typeof item.multiplier).toBe('number');
136+
expect(typeof item.individualPlanLimit).toBe('number');
137+
expect(typeof item.businessPlanLimit).toBe('number');
138+
expect(typeof item.enterprisePlanLimit).toBe('number');
139+
expect(typeof item.excessCost).toBe('number');
140+
});
141+
});
142+
143+
it('should handle unknown models with default multiplier', () => {
144+
const unknownModelData: CopilotUsageData[] = [
145+
{
146+
timestamp: new Date('2025-01-01T10:00:00Z'),
147+
user: 'user1',
148+
model: 'unknown-model-2025',
149+
requestsUsed: 20,
150+
exceedsQuota: false,
151+
totalMonthlyQuota: '500'
152+
}
153+
];
154+
155+
const result = getModelUsageSummary(unknownModelData);
156+
157+
expect(result).toHaveLength(1);
158+
expect(result[0].model).toBe('unknown-model-2025');
159+
expect(result[0].multiplier).toBe(1); // Default multiplier
160+
expect(result[0].individualPlanLimit).toBe(150); // 150 / 1
161+
expect(result[0].businessPlanLimit).toBe(500); // 500 / 1
162+
});
163+
164+
it('should sort results by total requests descending', () => {
165+
const result = getModelUsageSummary(mockData);
166+
167+
for (let i = 0; i < result.length - 1; i++) {
168+
expect(result[i].totalRequests).toBeGreaterThanOrEqual(result[i + 1].totalRequests);
169+
}
170+
});
171+
});

0 commit comments

Comments
 (0)