src/App.tsx CHANGED
@@ -1,29 +1,22 @@
1
- import React, { useState, useEffect } from "react";
2
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3
- import { Checkbox } from "@/components/ui/checkbox";
4
  import { Input } from "@/components/ui/input";
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from "@/components/ui/table";
13
- import { MultiSelect } from "@/components/ui/multi-select";
14
- import {
15
- Collapsible,
16
- CollapsibleContent,
17
- CollapsibleTrigger,
18
- } from "@/components/ui/collapsible";
19
- import { Button } from "@/components/ui/button";
20
- import { ChevronDown, ChevronRight } from "lucide-react";
21
- import { mockData } from "./lib/data"; // Assuming you have this file for mock data
22
- import { Switch } from "@/components/ui/switch";
23
-
24
- interface FlattenedModel extends Model {
25
  provider: string;
26
  uri: string;
 
 
 
27
  }
28
 
29
  export interface Model {
@@ -47,171 +40,194 @@ const App: React.FC = () => {
47
  const [selectedModels, setSelectedModels] = useState<string[]>([]);
48
  const [expandedProviders, setExpandedProviders] = useState<string[]>([]);
49
  const [tokenCalculation, setTokenCalculation] = useState<string>("million");
50
- const [linkProviderModel, setLinkProviderModel] = useState<boolean>(false);
 
 
51
 
52
  const [sortConfig, setSortConfig] = useState<{
53
  key: keyof FlattenedModel;
54
  direction: string;
55
  } | null>(null);
56
 
 
 
 
 
 
 
57
  useEffect(() => {
58
  setData(mockData);
59
  }, []);
60
 
61
- const calculatePrice = (price: number, tokens: number): number => {
62
- let multiplier = 1;
63
- if (tokenCalculation === "thousand") {
64
- multiplier = 1e-3;
65
- } else if (tokenCalculation === "unit") {
66
- multiplier = 1e-6;
67
- } else if (tokenCalculation === "billion") {
68
- multiplier = 1e3;
69
- }
70
- return price * tokens * multiplier;
71
- };
72
-
73
- const calculateComparison = (
74
- modelPrice: number,
75
- comparisonPrice: number
76
- ): string => {
77
- return (((modelPrice - comparisonPrice) / comparisonPrice) * 100).toFixed(
78
- 2
79
- );
80
- };
81
 
82
- const flattenData = (data: Provider[]) => {
83
- return data.flatMap((provider) =>
84
  provider.models.map((model) => ({
85
  provider: provider.provider,
86
  uri: provider.uri,
87
  ...model,
 
88
  }))
89
  );
90
- };
91
 
92
- const filteredData =
93
- selectedProviders.length === 0 &&
94
- selectedModels.length === 0 &&
95
- !linkProviderModel
96
- ? data.map((provider) => ({
97
- ...provider,
98
- models: provider.models,
99
- }))
100
- : data
101
- .filter(
102
- (provider) =>
103
- selectedProviders.length === 0 ||
104
- selectedProviders.includes(provider.provider)
105
- )
106
- .map((provider) => ({
107
- ...provider,
108
- models: provider.models.filter((model) => {
109
- // If linking is enabled and no models are selected, filter by provider
110
- if (linkProviderModel && selectedModels.length === 0)
111
- return selectedProviders.includes(provider.provider);
112
-
113
- // If no models are selected and linking is off, show all models from selected providers (or all if no providers selected)
114
- if (!linkProviderModel && selectedModels.length === 0)
115
- return (
116
- selectedProviders.length === 0 ||
117
- selectedProviders.includes(provider.provider)
118
- );
119
-
120
- // Otherwise, only show selected models
121
- return selectedModels.includes(model.name);
122
- }),
123
- }))
124
- .filter((provider) => provider.models.length > 0);
125
-
126
- const sortedFlattenedData = React.useMemo(() => {
127
- let sortableData: FlattenedModel[] = flattenData(filteredData);
128
- if (sortConfig !== null) {
129
- sortableData.sort((a, b) => {
130
- const aValue = a[sortConfig.key];
131
- const bValue = b[sortConfig.key];
132
-
133
- if (typeof aValue === "string" && typeof bValue === "string") {
134
- return sortConfig.direction === "ascending"
135
- ? aValue.localeCompare(bValue)
136
- : bValue.localeCompare(aValue);
137
- } else if (typeof aValue === "number" && typeof bValue === "number") {
138
- return sortConfig.direction === "ascending"
139
- ? aValue - bValue
140
- : bValue - aValue;
141
- } else {
142
- return 0;
143
- }
144
- });
145
- }
146
- return sortableData;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }, [filteredData, sortConfig]);
148
 
149
  const requestSort = (key: keyof FlattenedModel) => {
150
- let direction = "ascending";
151
- if (
152
- sortConfig &&
153
- sortConfig.key === key &&
154
- sortConfig.direction === "ascending"
155
- ) {
156
- direction = "descending";
157
- }
158
  setSortConfig({ key, direction });
159
  };
160
 
161
  const toggleProviderExpansion = (provider: string) => {
162
  setExpandedProviders((prev) =>
163
- prev.includes(provider)
164
- ? prev.filter((p) => p !== provider)
165
- : [...prev, provider]
166
  );
167
  };
168
 
169
- const getModelsForSelectedProviders = () => {
170
- if (!linkProviderModel) {
171
- return data
172
- .flatMap((provider) =>
173
- provider.models.map((model) => ({
174
- label: model.name,
175
- value: model.name,
176
- provider: provider.provider,
177
- }))
178
- )
179
- .reduce(
180
- (
181
- acc: { label: string; value: string; provider: string }[],
182
- curr: { label: string; value: string; provider: string }
183
- ) => {
184
- if (!acc.find((m) => m.value === curr.value)) {
185
- acc.push(curr);
186
- }
187
- return acc;
188
- },
189
- []
190
- );
191
- }
192
 
193
- return data
194
- .filter((provider) => selectedProviders.includes(provider.provider))
195
- .flatMap((provider) =>
196
- provider.models.map((model) => ({
197
- label: model.name,
198
- value: model.name,
199
- provider: provider.provider,
200
- }))
201
- )
202
- .reduce(
203
- (
204
- acc: { label: string; value: string; provider: string }[],
205
- curr: { label: string; value: string; provider: string }
206
- ) => {
207
- if (!acc.find((m) => m.value === curr.value)) {
208
- acc.push(curr);
209
- }
210
- return acc;
211
- },
212
- []
213
- );
214
- };
215
 
216
  return (
217
  <Card className="w-full max-w-6xl mx-auto">
@@ -219,121 +235,46 @@ const App: React.FC = () => {
219
  <CardTitle>LLM Pricing Calculator</CardTitle>
220
  </CardHeader>
221
  <CardContent>
222
- <div className="mb-4">
223
- <p className="italic text-sm text-muted-foreground mb-4">
224
- <a
225
- href="https://huggingface.co/spaces/philschmid/llm-pricing"
226
- className="underline"
227
- >
228
- This is a fork of philschmid tool: philschmid/llm-pricing
229
- </a>
230
- </p>
231
- <h3 className="text-lg font-semibold mb-2">
232
- Select Comparison Models
233
- </h3>
234
- <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
235
- {data.map((provider) => (
236
- <Collapsible
237
- key={provider.provider}
238
- open={expandedProviders.includes(provider.provider)}
239
- onOpenChange={() => toggleProviderExpansion(provider.provider)}
240
- >
241
- <CollapsibleTrigger asChild>
242
- <Button variant="outline" className="w-full justify-between">
243
- {provider.provider}
244
- {expandedProviders.includes(provider.provider) ? (
245
- <ChevronDown className="h-4 w-4" />
246
- ) : (
247
- <ChevronRight className="h-4 w-4" />
248
- )}
249
- </Button>
250
- </CollapsibleTrigger>
251
- <CollapsibleContent className="mt-2">
252
- {provider.models.map((model) => (
253
- <div
254
- key={`${provider.provider}:${model.name}`}
255
- className="flex items-center space-x-2 mb-1"
256
- >
257
- <Checkbox
258
- id={`${provider.provider}:${model.name}`}
259
- checked={comparisonModels.includes(
260
- `${provider.provider}:${model.name}`
261
- )}
262
- onCheckedChange={(checked) => {
263
- if (checked) {
264
- setComparisonModels((prev) => [
265
- ...prev,
266
- `${provider.provider}:${model.name}`,
267
- ]);
268
- } else {
269
- setComparisonModels((prev) =>
270
- prev.filter(
271
- (m) =>
272
- m !== `${provider.provider}:${model.name}`
273
- )
274
- );
275
- }
276
- }}
277
- />
278
- <label
279
- htmlFor={`${provider.provider}:${model.name}`}
280
- className="text-sm font-medium text-gray-700"
281
- >
282
- {model.name}
283
- </label>
284
- </div>
285
- ))}
286
- </CollapsibleContent>
287
- </Collapsible>
288
- ))}
289
- </div>
290
- </div>
291
 
292
- <div className="flex gap-4 mb-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  <div className="flex-1">
294
- <label
295
- htmlFor="inputTokens"
296
- className="block text-sm font-medium text-gray-700"
297
- >
298
- Input Tokens ({tokenCalculation})
299
- </label>
300
- <Input
301
- id="inputTokens"
302
- type="number"
303
- value={inputTokens}
304
- min={1}
305
- onChange={(e) => setInputTokens(Number(e.target.value))}
306
- className="mt-1"
307
- />
308
  </div>
309
  <div className="flex-1">
310
- <label
311
- htmlFor="outputTokens"
312
- className="block text-sm font-medium text-gray-700"
313
- >
314
- Output Tokens ({tokenCalculation})
315
- </label>
316
- <Input
317
- id="outputTokens"
318
- type="number"
319
- value={outputTokens}
320
- min={1}
321
- onChange={(e) => setOutputTokens(Number(e.target.value))}
322
- className="mt-1"
323
- />
324
  </div>
325
  <div className="flex-1">
326
- <label
327
- htmlFor="tokenCalculation"
328
- className="block text-sm font-medium text-gray-700"
329
- >
330
- Token Calculation
331
- </label>
332
  <select
333
- id="tokenCalculation"
334
  value={tokenCalculation}
335
  onChange={(e) => setTokenCalculation(e.target.value)}
336
- className="mt-1 block w-full pl-3 pr-10 py-2 text-base bg-white border focus:outline-none focus:ring-indigo-500 sm:text-sm rounded-md"
337
  >
338
  <option value="billion">Billion Tokens</option>
339
  <option value="million">Million Tokens</option>
@@ -343,196 +284,58 @@ const App: React.FC = () => {
343
  </div>
344
  </div>
345
 
346
- <p className="italic text-sm text-muted-foreground mb-4">
347
- Note: If you use Amazon Bedrock or Azure prices for Anthropic, Cohere
348
- or OpenAI should be the same.
349
- </p>
350
- <div className="flex items-center space-x-2 mb-4">
351
- <Switch
352
- id="linkProviderModel"
353
- checked={linkProviderModel}
354
- onCheckedChange={setLinkProviderModel}
355
- />
356
- <label htmlFor="linkProviderModel" className="text-sm">
357
- Link Provider and Model
358
- </label>
359
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
 
361
- <Table>
362
- <TableHeader>
363
- <TableRow>
364
- <TableHead>
365
- <button type="button" onClick={() => requestSort("provider")}>
366
- Provider{" "}
367
- {sortConfig?.key === "provider"
368
- ? sortConfig.direction === "ascending"
369
- ? "▲"
370
- : "▼"
371
- : null}
372
- </button>
373
- </TableHead>
374
- <TableHead>
375
- <button type="button" onClick={() => requestSort("name")}>
376
- Model{" "}
377
- {sortConfig?.key === "name"
378
- ? sortConfig.direction === "ascending"
379
- ? "▲"
380
- : "▼"
381
- : null}
382
- </button>
383
- </TableHead>
384
-
385
- <TableHead>
386
- <button type="button" onClick={() => requestSort("inputPrice")}>
387
- Input Price (million tokens)
388
- {sortConfig?.key === "inputPrice"
389
- ? sortConfig.direction === "ascending"
390
- ? "▲"
391
- : "▼"
392
- : null}
393
- </button>
394
- </TableHead>
395
- <TableHead>
396
- <button
397
- type="button"
398
- onClick={() => requestSort("outputPrice")}
399
- >
400
- Output Price (million tokens)
401
- {sortConfig?.key === "outputPrice"
402
- ? sortConfig.direction === "ascending"
403
- ? "▲"
404
- : "▼"
405
- : null}
406
- </button>
407
- </TableHead>
408
-
409
- <TableHead>
410
- Total Price (per {tokenCalculation} tokens){" "}
411
- </TableHead>
412
- {comparisonModels.map((model) => (
413
- <TableHead key={model} colSpan={2}>
414
- Compared to {model}
415
- </TableHead>
416
- ))}
417
- </TableRow>
418
- <TableRow>
419
- <TableHead>
420
- <MultiSelect
421
- options={
422
- data.map((provider) => ({
423
- label: provider.provider,
424
- value: provider.provider,
425
- })) || []
426
- }
427
- onValueChange={setSelectedProviders}
428
- defaultValue={selectedProviders}
429
- />
430
- </TableHead>
431
- <TableHead>
432
- <MultiSelect
433
- options={getModelsForSelectedProviders()}
434
- defaultValue={selectedModels}
435
- onValueChange={setSelectedModels}
436
- />
437
- </TableHead>
438
- <TableHead />
439
- <TableHead />
440
- <TableHead />
441
- {comparisonModels.flatMap((model) => [
442
- <TableHead key={`${model}-input`}>Input</TableHead>,
443
- <TableHead key={`${model}-output`}>Output</TableHead>,
444
- ])}
445
- </TableRow>
446
- </TableHeader>
447
- <TableBody>
448
- {sortedFlattenedData.map((item) => (
449
- <TableRow key={`${item.provider}-${item.name}`}>
450
- <TableCell>
451
- {" "}
452
- <a href={item.uri} className="underline">
453
- {item.provider}
454
- </a>
455
- </TableCell>
456
- <TableCell>{item.name}</TableCell>
457
-
458
- <TableCell>{item.inputPrice.toFixed(2)}</TableCell>
459
- <TableCell>{item.outputPrice.toFixed(2)}</TableCell>
460
-
461
- <TableCell className="font-bold">
462
- $
463
- {(
464
- calculatePrice(item.inputPrice, inputTokens) +
465
- calculatePrice(item.outputPrice, outputTokens)
466
- ).toFixed(2)}
467
- </TableCell>
468
-
469
- {comparisonModels.flatMap((comparisonModel) => {
470
- const [comparisonProvider, comparisonModelName] =
471
- comparisonModel.split(":");
472
- const comparisonModelData = data
473
- .find((p) => p.provider === comparisonProvider)
474
- ?.models.find((m) => m.name === comparisonModelName)!;
475
- return [
476
- <TableCell
477
- key={`${comparisonModel}-input`}
478
- className={`${
479
- parseFloat(
480
- calculateComparison(
481
- item.inputPrice,
482
- comparisonModelData.inputPrice
483
- )
484
- ) < 0
485
- ? "bg-green-100"
486
- : parseFloat(
487
- calculateComparison(
488
- item.inputPrice,
489
- comparisonModelData.inputPrice
490
- )
491
- ) > 0
492
- ? "bg-red-100"
493
- : ""
494
- }`}
495
- >
496
- {`${item.provider}:${item.name}` === comparisonModel
497
- ? "0.00%"
498
- : `${calculateComparison(
499
- item.inputPrice,
500
- comparisonModelData.inputPrice
501
- )}%`}
502
- </TableCell>,
503
- <TableCell
504
- key={`${comparisonModel}-output`}
505
- className={`${
506
- parseFloat(
507
- calculateComparison(
508
- item.outputPrice,
509
- comparisonModelData.outputPrice
510
- )
511
- ) < 0
512
- ? "bg-green-100"
513
- : parseFloat(
514
- calculateComparison(
515
- item.outputPrice,
516
- comparisonModelData.outputPrice
517
- )
518
- ) > 0
519
- ? "bg-red-100"
520
- : ""
521
- }`}
522
- >
523
- {`${item.provider}:${item.name}` === comparisonModel
524
- ? "0.00%"
525
- : `${calculateComparison(
526
- item.outputPrice,
527
- comparisonModelData.outputPrice
528
- )}%`}
529
- </TableCell>,
530
- ];
531
- })}
532
- </TableRow>
533
- ))}
534
- </TableBody>
535
- </Table>
536
  </CardContent>
537
  </Card>
538
  );
 
1
+ import React, { useState, useEffect, useMemo } from "react";
2
  import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 
3
  import { Input } from "@/components/ui/input";
4
+ import { mockData } from "@/lib/data";
5
+
6
+ import { ComparisonSelector } from "@/components/ComparisonSelector";
7
+ import { PricingTable } from "@/components/PricingTable";
8
+
9
+ import { BenchmarkTable } from "./components/BenchmarkTable";
10
+ import { benchmarkData } from "./lib/benchmarks/ index";
11
+ import { BenchmarkComparisonSelector } from "./components/BenchmarkComparisonSelector";
12
+ import { benchmarkMetricOrder } from "./lib/benchmarks/types";
13
+
14
+ export interface FlattenedModel extends Model {
 
 
 
 
 
 
 
 
 
15
  provider: string;
16
  uri: string;
17
+ benchmark?: {
18
+ [key: string]: number;
19
+ };
20
  }
21
 
22
  export interface Model {
 
40
  const [selectedModels, setSelectedModels] = useState<string[]>([]);
41
  const [expandedProviders, setExpandedProviders] = useState<string[]>([]);
42
  const [tokenCalculation, setTokenCalculation] = useState<string>("million");
43
+ const [benchmarkComparisonMetrics, setBenchmarkComparisonMetrics] = useState<string[]>([]);
44
+ const [selectedBenchmarkProviders, setSelectedBenchmarkProviders] = useState<string[]>([]);
45
+ const [selectedBenchmarkModels, setSelectedBenchmarkModels] = useState<string[]>([]);
46
 
47
  const [sortConfig, setSortConfig] = useState<{
48
  key: keyof FlattenedModel;
49
  direction: string;
50
  } | null>(null);
51
 
52
+ const [benchmarkSortConfig, setBenchmarkSortConfig] = useState<{
53
+ key: string;
54
+ direction: "ascending" | "descending";
55
+ } | null>(null);
56
+
57
+
58
  useEffect(() => {
59
  setData(mockData);
60
  }, []);
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ const flattenDataFromPricing = (data: Provider[]): FlattenedModel[] =>
64
+ data.flatMap((provider) =>
65
  provider.models.map((model) => ({
66
  provider: provider.provider,
67
  uri: provider.uri,
68
  ...model,
69
+ benchmark: {},
70
  }))
71
  );
 
72
 
73
+ const flattenDataFromBenchmarks = (): FlattenedModel[] =>
74
+ benchmarkData.map((b) => ({
75
+ provider: b.provider ?? "Unknown",
76
+ uri: b.source, // placeholder or use an optional `b.link` if available
77
+ name: b.model,
78
+ inputPrice: b.inputPrice,
79
+ outputPrice: b.outputPrice,
80
+ benchmark: b.benchmark ?? {},
81
+ }));
82
+
83
+
84
+
85
+ const filteredData = useMemo(() => {
86
+ if (!selectedProviders.length && !selectedModels.length) return data;
87
+
88
+ return data
89
+ .filter((p) => !selectedProviders.length || selectedProviders.includes(p.provider))
90
+ .map((p) => ({
91
+ ...p,
92
+ models: p.models.filter((m) => {
93
+ if (!selectedModels.length) return selectedProviders.includes(p.provider);
94
+ if (!selectedModels.length) return !selectedProviders.length || selectedProviders.includes(p.provider);
95
+ return selectedModels.includes(m.name);
96
+ }),
97
+ }))
98
+ .filter((p) => p.models.length > 0);
99
+ }, [data, selectedProviders, selectedModels]);
100
+
101
+ const benchmarkedModels = useMemo(() => {
102
+ return flattenDataFromBenchmarks();
103
+ }, []);
104
+
105
+
106
+
107
+ const filteredBenchmarkedModels = useMemo(() => {
108
+ return benchmarkedModels.filter((model) => {
109
+ const providerMatch =
110
+ selectedBenchmarkProviders.length === 0 || selectedBenchmarkProviders.includes(model.provider);
111
+ const modelMatch =
112
+ selectedBenchmarkModels.length === 0 || selectedBenchmarkModels.includes(model.name);
113
+
114
+ // When not linking, allow filtering by either
115
+ if (selectedBenchmarkProviders.length > 0 && selectedBenchmarkModels.length > 0) {
116
+ return providerMatch || modelMatch;
117
+ }
118
+
119
+ return providerMatch && modelMatch; // this handles the case where one or both are empty
120
+ });
121
+ }, [
122
+ benchmarkedModels,
123
+ selectedBenchmarkProviders,
124
+ selectedBenchmarkModels,
125
+
126
+ ]);
127
+
128
+ const sortedBenchmarkedModels = useMemo(() => {
129
+ if (!benchmarkSortConfig) return filteredBenchmarkedModels;
130
+
131
+ return [...filteredBenchmarkedModels].sort((a, b) => {
132
+ const key = benchmarkSortConfig.key;
133
+
134
+ const isTopLevelKey = ["provider", "name", "inputPrice", "outputPrice"].includes(key);
135
+
136
+ const aVal = isTopLevelKey
137
+ ? (a as any)[key]
138
+ : a.benchmark?.[key] ?? -Infinity;
139
+ const bVal = isTopLevelKey
140
+ ? (b as any)[key]
141
+ : b.benchmark?.[key] ?? -Infinity;
142
+
143
+ if (typeof aVal === "string" && typeof bVal === "string") {
144
+ return benchmarkSortConfig.direction === "ascending"
145
+ ? aVal.localeCompare(bVal)
146
+ : bVal.localeCompare(aVal);
147
+ }
148
+
149
+ return benchmarkSortConfig.direction === "ascending"
150
+ ? aVal - bVal
151
+ : bVal - aVal;
152
+ });
153
+ }, [filteredBenchmarkedModels, benchmarkSortConfig]);
154
+
155
+
156
+ const pricingProviders = useMemo(() => {
157
+ const grouped: Record<string, FlattenedModel[]> = {};
158
+
159
+ flattenDataFromPricing(data).forEach((model) => {
160
+ const key = model.provider;
161
+ if (!grouped[key]) grouped[key] = [];
162
+ grouped[key].push(model);
163
+ });
164
+
165
+ return Object.entries(grouped).map(([provider, models]) => ({
166
+ provider,
167
+ uri: models[0]?.uri ?? "#",
168
+ models: models.map(({ name, inputPrice, outputPrice }) => ({
169
+ name,
170
+ inputPrice,
171
+ outputPrice,
172
+ })),
173
+ }));
174
+ }, [data]);
175
+
176
+
177
+ const benchmarkProviders = useMemo(() => {
178
+ const grouped: Record<string, FlattenedModel[]> = {};
179
+
180
+ benchmarkedModels.forEach((model) => {
181
+ const key = model.provider;
182
+ if (!grouped[key]) grouped[key] = [];
183
+ grouped[key].push(model);
184
+ });
185
+
186
+ return Object.entries(grouped).map(([provider, models]) => ({
187
+ provider,
188
+ uri: models[0]?.uri ?? "#",
189
+ models: models.map(({ name, inputPrice, outputPrice }) => ({
190
+ name,
191
+ inputPrice,
192
+ outputPrice,
193
+ })),
194
+ }));
195
+ }, [benchmarkedModels]);
196
+
197
+
198
+
199
+
200
+
201
+
202
+ const sortedFlattenedData = useMemo(() => {
203
+ const flattened = flattenDataFromPricing(filteredData);
204
+ if (!sortConfig) return flattened;
205
+
206
+ return [...flattened].sort((a, b) => {
207
+ const aVal = a[sortConfig.key];
208
+ const bVal = b[sortConfig.key];
209
+
210
+ if (typeof aVal === "string" && typeof bVal === "string") {
211
+ return sortConfig.direction === "ascending" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
212
+ } else if (typeof aVal === "number" && typeof bVal === "number") {
213
+ return sortConfig.direction === "ascending" ? aVal - bVal : bVal - aVal;
214
+ }
215
+ return 0;
216
+ });
217
  }, [filteredData, sortConfig]);
218
 
219
  const requestSort = (key: keyof FlattenedModel) => {
220
+ const direction = sortConfig?.key === key && sortConfig.direction === "ascending" ? "descending" : "ascending";
 
 
 
 
 
 
 
221
  setSortConfig({ key, direction });
222
  };
223
 
224
  const toggleProviderExpansion = (provider: string) => {
225
  setExpandedProviders((prev) =>
226
+ prev.includes(provider) ? prev.filter((p) => p !== provider) : [...prev, provider]
 
 
227
  );
228
  };
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
  return (
233
  <Card className="w-full max-w-6xl mx-auto">
 
235
  <CardTitle>LLM Pricing Calculator</CardTitle>
236
  </CardHeader>
237
  <CardContent>
238
+ {/* Source Link */}
239
+ <p className="italic text-sm text-muted-foreground mb-4">
240
+ <a
241
+ href="https://huggingface.co/spaces/philschmid/llm-pricing"
242
+ className="underline"
243
+ >
244
+ This is a fork of philschmid tool: philschmid/llm-pricing
245
+ </a>
246
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
+ {/* Comparison Model Selector */}
249
+ <h3 className="text-lg font-semibold mb-2">Select Comparison Models</h3>
250
+ <ComparisonSelector
251
+ data={data}
252
+ expanded={expandedProviders}
253
+ comparisonModels={comparisonModels}
254
+ onToggleExpand={toggleProviderExpansion}
255
+ onChangeModel={(modelId, checked) =>
256
+ setComparisonModels((prev) =>
257
+ checked ? [...prev, modelId] : prev.filter((m) => m !== modelId)
258
+ )
259
+ }
260
+ />
261
+
262
+ {/* Token Inputs */}
263
+ <div className="flex gap-4 mt-6 mb-4">
264
  <div className="flex-1">
265
+ <label className="block text-sm font-medium">Input Tokens ({tokenCalculation})</label>
266
+ <Input type="number" value={inputTokens} min={1} onChange={(e) => setInputTokens(Number(e.target.value))} />
 
 
 
 
 
 
 
 
 
 
 
 
267
  </div>
268
  <div className="flex-1">
269
+ <label className="block text-sm font-medium">Output Tokens ({tokenCalculation})</label>
270
+ <Input type="number" value={outputTokens} min={1} onChange={(e) => setOutputTokens(Number(e.target.value))} />
 
 
 
 
 
 
 
 
 
 
 
 
271
  </div>
272
  <div className="flex-1">
273
+ <label className="block text-sm font-medium">Token Calculation</label>
 
 
 
 
 
274
  <select
 
275
  value={tokenCalculation}
276
  onChange={(e) => setTokenCalculation(e.target.value)}
277
+ className="mt-1 block w-full pl-3 pr-10 py-2 text-base border rounded-md"
278
  >
279
  <option value="billion">Billion Tokens</option>
280
  <option value="million">Million Tokens</option>
 
284
  </div>
285
  </div>
286
 
287
+ {/* Pricing Table */}
288
+ <h2 className="text-lg font-semibold mb-2">Pricing Table</h2>
289
+ <PricingTable
290
+ data={sortedFlattenedData}
291
+ providers={pricingProviders}
292
+ selectedProviders={selectedProviders}
293
+ selectedModels={selectedModels}
294
+ onProviderChange={setSelectedProviders}
295
+ onModelChange={setSelectedModels}
296
+ comparisonModels={comparisonModels}
297
+ inputTokens={inputTokens}
298
+ outputTokens={outputTokens}
299
+ tokenCalculation={tokenCalculation}
300
+ requestSort={requestSort}
301
+ sortConfig={sortConfig}
302
+ />
303
+
304
+
305
+ {/* Benchmark Table */}
306
+ <h3 className="text-lg font-semibold mt-12 mb-2">Select Benchmark Metrics to Compare</h3>
307
+ <BenchmarkComparisonSelector
308
+ allMetrics={benchmarkMetricOrder.filter(
309
+ (metric) => benchmarkedModels.some((m) => m.benchmark?.[metric] !== undefined)
310
+ )}
311
+ selected={benchmarkComparisonMetrics}
312
+ onChange={(metric, checked) =>
313
+ setBenchmarkComparisonMetrics((prev) =>
314
+ checked ? [...prev, metric] : prev.filter((m) => m !== metric)
315
+ )
316
+ }
317
+ />
318
+
319
+ <h2 className="text-lg font-semibold mt-12 mb-2">Benchmark Table</h2>
320
+ <BenchmarkTable
321
+ data={sortedBenchmarkedModels}
322
+ comparisonMetrics={benchmarkComparisonMetrics}
323
+ requestSort={(key) => {
324
+ setBenchmarkSortConfig((prev) =>
325
+ prev?.key === key
326
+ ? { key, direction: prev.direction === "ascending" ? "descending" : "ascending" }
327
+ : { key, direction: "descending" }
328
+ );
329
+ }}
330
+ sortConfig={benchmarkSortConfig}
331
+ allProviders={benchmarkProviders}
332
+ selectedProviders={selectedBenchmarkProviders}
333
+ selectedModels={selectedBenchmarkModels}
334
+ onProviderChange={setSelectedBenchmarkProviders}
335
+ onModelChange={setSelectedBenchmarkModels}
336
+ />
337
+
338
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  </CardContent>
340
  </Card>
341
  );
src/components/BenchmarkComparisonSelector.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // components/BenchmarkComparisonSelector.tsx
2
+ import React from "react";
3
+ import { Checkbox } from "@/components/ui/checkbox";
4
+
5
+ interface Props {
6
+ allMetrics: string[];
7
+ selected: string[];
8
+ onChange: (metric: string, checked: boolean) => void;
9
+ }
10
+
11
+ export const BenchmarkComparisonSelector: React.FC<Props> = ({
12
+ allMetrics,
13
+ selected,
14
+ onChange,
15
+ }) => {
16
+ return (
17
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 mb-4">
18
+ {allMetrics.map((metric) => (
19
+ <div key={metric} className="flex items-center space-x-2">
20
+ <Checkbox
21
+ id={metric}
22
+ checked={selected.includes(metric)}
23
+ onCheckedChange={(checked) => onChange(metric, !!checked)}
24
+ />
25
+ <label htmlFor={metric} className="text-sm">
26
+ {metric.replace(/_/g, " ").toUpperCase()}
27
+ </label>
28
+ </div>
29
+ ))}
30
+ </div>
31
+ );
32
+ };
src/components/BenchmarkTable.tsx ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import {
3
+ Table,
4
+ TableHeader,
5
+ TableBody,
6
+ TableRow,
7
+ TableHead,
8
+ TableCell,
9
+ } from "@/components/ui/table";
10
+ import { MultiSelect } from "@/components/ui/multi-select";
11
+ import { Provider, FlattenedModel } from "@/App";
12
+
13
+ interface BenchmarkTableProps {
14
+ data: FlattenedModel[];
15
+ comparisonMetrics?: string[];
16
+ requestSort: (key: string) => void;
17
+ sortConfig: {
18
+ key: string;
19
+ direction: "ascending" | "descending";
20
+ } | null;
21
+ allProviders: Provider[];
22
+ selectedProviders: string[];
23
+ selectedModels: string[];
24
+ onProviderChange: (values: string[]) => void;
25
+ onModelChange: (values: string[]) => void;
26
+ }
27
+
28
+
29
+ export const BenchmarkTable: React.FC<BenchmarkTableProps> = ({
30
+ data,
31
+ comparisonMetrics,
32
+ requestSort,
33
+ sortConfig,
34
+ allProviders,
35
+ selectedProviders,
36
+ selectedModels,
37
+ onProviderChange,
38
+ onModelChange,
39
+
40
+ }) => {
41
+ const benchmarkMetrics = React.useMemo(() => {
42
+ if (!comparisonMetrics || comparisonMetrics.length === 0) return [];
43
+ return comparisonMetrics;
44
+ }, [comparisonMetrics]);
45
+
46
+ const getCellStyle = (value?: number) => {
47
+ if (value === undefined) return "";
48
+ if (value >= 85) return "bg-green-100";
49
+ if (value >= 60) return "bg-yellow-100";
50
+ return "bg-red-100";
51
+ };
52
+
53
+ const renderSortIndicator = (key: string) => {
54
+ if (sortConfig?.key !== key) return null;
55
+ return sortConfig.direction === "ascending" ? " ▲" : " ▼";
56
+ };
57
+
58
+ const modelOptions = React.useMemo(() => {
59
+ const flat = allProviders.flatMap((provider) =>
60
+ provider.models.map((model) => ({
61
+ label: model.name,
62
+ value: model.name,
63
+ provider: provider.provider,
64
+ }))
65
+ );
66
+
67
+ const filtered = flat.filter((m) =>
68
+ !selectedProviders.length || selectedProviders.includes(m.provider)
69
+ );
70
+
71
+
72
+ return Array.from(new Map(filtered.map((m) => [m.value, m])).values());
73
+ }, [allProviders, selectedProviders]);
74
+
75
+
76
+ return (
77
+ <>
78
+ <div className="flex items-center space-x-4 mb-4">
79
+ <div className="flex-1">
80
+ <label className="text-sm font-medium block mb-1">Providers</label>
81
+ <MultiSelect
82
+ options={allProviders.map((p) => ({ label: p.provider, value: p.provider }))}
83
+ defaultValue={selectedProviders}
84
+ onValueChange={onProviderChange}
85
+ />
86
+ </div>
87
+ <div className="flex-1">
88
+ <label className="text-sm font-medium block mb-1">Models</label>
89
+ <MultiSelect
90
+ options={modelOptions.map(({ label, value }) => ({ label, value }))}
91
+ defaultValue={selectedModels}
92
+ onValueChange={onModelChange}
93
+ />
94
+ </div>
95
+
96
+ </div>
97
+
98
+ <Table>
99
+ <TableHeader>
100
+ <TableRow>
101
+ <TableHead>
102
+ <button
103
+ onClick={() => requestSort("provider")}
104
+ className="font-medium underline underline-offset-2"
105
+ >
106
+ Provider{renderSortIndicator("provider")}
107
+ </button>
108
+ </TableHead>
109
+ <TableHead>
110
+ <button
111
+ onClick={() => requestSort("name")}
112
+ className="font-medium underline underline-offset-2"
113
+ >
114
+ Model{renderSortIndicator("name")}
115
+ </button>
116
+ </TableHead>
117
+ <TableHead>
118
+ <button
119
+ onClick={() => requestSort("inputPrice")}
120
+ className="font-medium underline underline-offset-2"
121
+ >
122
+ Input Price (USD){renderSortIndicator("inputPrice")}
123
+ </button>
124
+ </TableHead>
125
+ <TableHead>
126
+ <button
127
+ onClick={() => requestSort("outputPrice")}
128
+ className="font-medium underline underline-offset-2"
129
+ >
130
+ Output Price (USD){renderSortIndicator("outputPrice")}
131
+ </button>
132
+ </TableHead>
133
+
134
+ {benchmarkMetrics.map((metric) => (
135
+ <TableHead key={metric}>
136
+ <button
137
+ onClick={() => requestSort(metric)}
138
+ className="font-medium underline underline-offset-2"
139
+ >
140
+ {metric.replace(/_/g, " ").toUpperCase()}
141
+ {renderSortIndicator(metric)}
142
+ </button>
143
+ </TableHead>
144
+ ))}
145
+ </TableRow>
146
+ </TableHeader>
147
+ <TableBody>
148
+ {data.map((model) => (
149
+ <TableRow key={`${model.provider}-${model.name}`}>
150
+ <TableCell>
151
+ <a href={model.uri} className="underline">
152
+ {model.provider}
153
+ </a>
154
+ </TableCell>
155
+ <TableCell>{model.name}</TableCell>
156
+ <TableCell>${model.inputPrice.toFixed(2)}</TableCell>
157
+ <TableCell>${model.outputPrice.toFixed(2)}</TableCell>
158
+
159
+ {benchmarkMetrics.map((metric) => {
160
+ const score = model.benchmark?.[metric];
161
+ return (
162
+ <TableCell key={metric} className={getCellStyle(score)}>
163
+ {score !== undefined ? `${score.toFixed(1)}%` : "-"}
164
+ </TableCell>
165
+ );
166
+ })}
167
+ </TableRow>
168
+ ))}
169
+ </TableBody>
170
+ </Table>
171
+ </>
172
+ );
173
+ };
src/components/ComparisonSelector.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Collapsible,
4
+ CollapsibleContent,
5
+ CollapsibleTrigger,
6
+ } from "@/components/ui/collapsible";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Checkbox } from "@/components/ui/checkbox";
9
+ import { ChevronDown, ChevronRight } from "lucide-react";
10
+ import { Provider } from "@/App";
11
+
12
+ interface Props {
13
+ data: Provider[];
14
+ expanded: string[];
15
+ comparisonModels: string[];
16
+ onToggleExpand: (provider: string) => void;
17
+ onChangeModel: (value: string, checked: boolean) => void;
18
+ }
19
+
20
+ export const ComparisonSelector: React.FC<Props> = ({
21
+ data,
22
+ expanded,
23
+ comparisonModels,
24
+ onToggleExpand,
25
+ onChangeModel,
26
+ }) => (
27
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
28
+ {data.map((provider) => (
29
+ <Collapsible
30
+ key={provider.provider}
31
+ open={expanded.includes(provider.provider)}
32
+ onOpenChange={() => onToggleExpand(provider.provider)}
33
+ >
34
+ <CollapsibleTrigger asChild>
35
+ <Button variant="outline" className="w-full justify-between">
36
+ {provider.provider}
37
+ {expanded.includes(provider.provider) ? (
38
+ <ChevronDown className="h-4 w-4" />
39
+ ) : (
40
+ <ChevronRight className="h-4 w-4" />
41
+ )}
42
+ </Button>
43
+ </CollapsibleTrigger>
44
+ <CollapsibleContent className="mt-2">
45
+ {provider.models.map((model) => {
46
+ const id = `${provider.provider}:${model.name}`;
47
+ return (
48
+ <div key={id} className="flex items-center space-x-2 mb-1">
49
+ <Checkbox
50
+ id={id}
51
+ checked={comparisonModels.includes(id)}
52
+ onCheckedChange={(checked) => onChangeModel(id, !!checked)}
53
+ />
54
+ <label htmlFor={id} className="text-sm font-medium text-gray-700">
55
+ {model.name}
56
+ </label>
57
+ </div>
58
+ );
59
+ })}
60
+ </CollapsibleContent>
61
+ </Collapsible>
62
+ ))}
63
+ </div>
64
+ );
src/components/PricingTable.tsx ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import {
3
+ Table,
4
+ TableHeader,
5
+ TableBody,
6
+ TableRow,
7
+ TableHead,
8
+ TableCell,
9
+ } from "@/components/ui/table";
10
+ import { MultiSelect } from "@/components/ui/multi-select";
11
+ import { FlattenedModel, Provider } from "@/App";
12
+
13
+ interface PricingTableProps {
14
+ data: FlattenedModel[];
15
+ providers: Provider[];
16
+ selectedProviders: string[];
17
+ selectedModels: string[];
18
+ onProviderChange: (values: string[]) => void;
19
+ onModelChange: (values: string[]) => void;
20
+ comparisonModels: string[];
21
+ inputTokens: number;
22
+ outputTokens: number;
23
+ tokenCalculation: string;
24
+ requestSort: (key: keyof FlattenedModel) => void;
25
+ sortConfig: {
26
+ key: keyof FlattenedModel;
27
+ direction: string;
28
+ } | null;
29
+
30
+ }
31
+
32
+ export const PricingTable: React.FC<PricingTableProps> = ({
33
+ data,
34
+ providers,
35
+ selectedProviders,
36
+ selectedModels,
37
+ onProviderChange,
38
+ onModelChange,
39
+ comparisonModels,
40
+ inputTokens,
41
+ outputTokens,
42
+ tokenCalculation,
43
+ requestSort,
44
+ sortConfig,
45
+
46
+ }) => {
47
+ const calculatePrice = (price: number, tokens: number): number => {
48
+ let multiplier = 1;
49
+ if (tokenCalculation === "thousand") multiplier = 1e-3;
50
+ else if (tokenCalculation === "unit") multiplier = 1e-6;
51
+ else if (tokenCalculation === "billion") multiplier = 1e3;
52
+ return price * tokens * multiplier;
53
+ };
54
+
55
+ const calculateComparison = (modelPrice: number, comparisonPrice: number): string => {
56
+ if (comparisonPrice === 0) return "∞";
57
+ return (((modelPrice - comparisonPrice) / comparisonPrice) * 100).toFixed(2);
58
+ };
59
+
60
+ const getModelsForSelectedProviders = () => {
61
+ const flatModels = providers.flatMap((provider) =>
62
+ provider.models.map((model) => ({
63
+ label: model.name,
64
+ value: model.name,
65
+ provider: provider.provider,
66
+ }))
67
+ );
68
+
69
+ const filtered = flatModels.filter((m) =>
70
+ !selectedProviders.length || selectedProviders.includes(m.provider)
71
+ );
72
+
73
+
74
+ // Remove duplicates
75
+ return Array.from(new Map(filtered.map((m) => [m.value, m])).values());
76
+ };
77
+
78
+ return (
79
+ <Table>
80
+ <TableHeader>
81
+ <TableRow>
82
+ <TableHead>
83
+ <button onClick={() => requestSort("provider")}>
84
+ Provider {sortConfig?.key === "provider" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
85
+ </button>
86
+ </TableHead>
87
+ <TableHead>
88
+ <button onClick={() => requestSort("name")}>
89
+ Model {sortConfig?.key === "name" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
90
+ </button>
91
+ </TableHead>
92
+ <TableHead>
93
+ <button onClick={() => requestSort("inputPrice")}>
94
+ Input Price (million tokens) {sortConfig?.key === "inputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
95
+ </button>
96
+ </TableHead>
97
+ <TableHead>
98
+ <button onClick={() => requestSort("outputPrice")}>
99
+ Output Price (million tokens) {sortConfig?.key === "outputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
100
+ </button>
101
+ </TableHead>
102
+ <TableHead>Total Price (per {tokenCalculation} tokens)</TableHead>
103
+ {comparisonModels.map((model) => (
104
+ <TableHead key={model} colSpan={2}>
105
+ Compared to {model}
106
+ </TableHead>
107
+ ))}
108
+ </TableRow>
109
+ <TableRow>
110
+ <TableHead>
111
+ <MultiSelect
112
+ options={providers.map((p) => ({ label: p.provider, value: p.provider }))}
113
+ defaultValue={selectedProviders}
114
+ onValueChange={onProviderChange}
115
+ />
116
+ </TableHead>
117
+ <TableHead>
118
+ <MultiSelect
119
+ options={getModelsForSelectedProviders()}
120
+ defaultValue={selectedModels}
121
+ onValueChange={onModelChange}
122
+ />
123
+ </TableHead>
124
+ <TableHead />
125
+ <TableHead />
126
+ <TableHead />
127
+ {comparisonModels.flatMap((model) => [
128
+ <TableHead key={`${model}-input`}>Input</TableHead>,
129
+ <TableHead key={`${model}-output`}>Output</TableHead>,
130
+ ])}
131
+ </TableRow>
132
+ </TableHeader>
133
+ <TableBody>
134
+ {data.map((item) => (
135
+ <TableRow key={`${item.provider}-${item.name}`}>
136
+ <TableCell>
137
+ <a href={item.uri} className="underline">
138
+ {item.provider}
139
+ </a>
140
+ </TableCell>
141
+ <TableCell>{item.name}</TableCell>
142
+ <TableCell>{item.inputPrice.toFixed(2)}</TableCell>
143
+ <TableCell>{item.outputPrice.toFixed(2)}</TableCell>
144
+ <TableCell className="font-bold">
145
+ ${(
146
+ calculatePrice(item.inputPrice, inputTokens) +
147
+ calculatePrice(item.outputPrice, outputTokens)
148
+ ).toFixed(2)}
149
+ </TableCell>
150
+ {comparisonModels.flatMap((model) => {
151
+ const [comparisonProvider, comparisonModelName] = model.split(":");
152
+ const comparisonModel = providers
153
+ .find((p) => p.provider === comparisonProvider)
154
+ ?.models.find((m) => m.name === comparisonModelName);
155
+
156
+ if (!comparisonModel) return [<TableCell key="missing">–</TableCell>, <TableCell key="missing2">–</TableCell>];
157
+
158
+ const inputDelta = calculateComparison(item.inputPrice, comparisonModel.inputPrice);
159
+ const outputDelta = calculateComparison(item.outputPrice, comparisonModel.outputPrice);
160
+
161
+ return [
162
+ <TableCell key={`${model}-input`} className={parseFloat(inputDelta) < 0 ? "bg-green-100" : parseFloat(inputDelta) > 0 ? "bg-red-100" : ""}>
163
+ {`${item.provider}:${item.name}` === model ? "0.00%" : `${inputDelta}%`}
164
+ </TableCell>,
165
+ <TableCell key={`${model}-output`} className={parseFloat(outputDelta) < 0 ? "bg-green-100" : parseFloat(outputDelta) > 0 ? "bg-red-100" : ""}>
166
+ {`${item.provider}:${item.name}` === model ? "0.00%" : `${outputDelta}%`}
167
+ </TableCell>,
168
+ ];
169
+ })}
170
+ </TableRow>
171
+ ))}
172
+ </TableBody>
173
+ </Table>
174
+ );
175
+ };
src/lib/benchmarks/ index.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Benchmark } from "./types";
2
+ import { xaiBenchmarks } from "./xai";
3
+ import { googleBenchmarks } from "./google";
4
+ // import other sources here as you add them
5
+ // import { openaiBenchmarks } from "./openai";
6
+
7
+ export const benchmarkData: Benchmark[] = [
8
+ ...xaiBenchmarks,
9
+ ...googleBenchmarks,
10
+ // ...openaiBenchmarks,
11
+ ];
src/lib/benchmarks/google.ts ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Benchmark } from "./types";
2
+
3
+
4
+ export const googleBenchmarks: Benchmark[] = [
5
+ {
6
+ model: "Gemini Diffusion",
7
+ provider: "Google",
8
+ inputPrice: 0,
9
+ outputPrice: 0,
10
+ benchmark: {
11
+ livecodebench_v6: 30.9,
12
+ bigcodebench: 45.4,
13
+ lbpp_v2: 56.8,
14
+ swe_bench_verified: 22.9,
15
+ humaneval: 89.6,
16
+ mbpp: 76.0,
17
+ gpqa_diamond: 40.4,
18
+ aime_2025: 23.3,
19
+ bigbench_extra_hard: 15.0,
20
+ global_mmlu_lite: 69.1,
21
+ },
22
+ source: "https://deepmind.google/models/gemini-diffusion/",
23
+ },
24
+ {
25
+ model: "Gemini 2.0 Flash-Lite",
26
+ provider: "Google",
27
+ inputPrice: 0.10,
28
+ outputPrice: 0.40,
29
+ benchmark: {
30
+ livecodebench_v6: 28.5,
31
+ bigcodebench: 45.8,
32
+ lbpp_v2: 56.0,
33
+ swe_bench_verified: 28.5,
34
+ humaneval: 90.2,
35
+ mbpp: 75.8,
36
+ gpqa_diamond: 56.5,
37
+ aime_2025: 20.0,
38
+ bigbench_extra_hard: 21.0,
39
+ global_mmlu_lite: 79.0,
40
+ },
41
+ source: "https://deepmind.google/models/gemini-diffusion/",
42
+ },
43
+
44
+ {
45
+ model: "Gemini 2.5 Flash Preview (05-20)",
46
+ provider: "Google",
47
+ inputPrice: 0.15,
48
+ outputPrice: 3.5,
49
+ source: "https://ai.google.dev/gemini-api/docs/thinking",
50
+ benchmark: {
51
+ aime_2025: 72.0,
52
+ gpqa_diamond: 82.8,
53
+ simpleqa: 26.9,
54
+ global_mmlu_lite: 88.4,
55
+ swe_bench_verified: 60.4,
56
+ livecodebench_v6: 63.9,
57
+ mmmu: 79.7,
58
+ lbpp_v2: 61.9,
59
+ bigcodebench: 56.7,
60
+ facts_grounding: 85.3,
61
+ humanitys_last_exam: 11.0,
62
+ mrcr_v2_avg_128k: 74.0,
63
+ mrcr_v2_pointwise_1m: 32.0,
64
+
65
+ },
66
+ },
67
+ {
68
+ model: "Gemini 2.5 Flash Preview (04-17) Thinking",
69
+ provider: "Google",
70
+ inputPrice: 0.15,
71
+ outputPrice: 3.5,
72
+ source: "https://ai.google.dev/gemini-api/docs/thinking",
73
+ benchmark: {
74
+ aime_2025: 78.0,
75
+ gpqa_diamond: 78.3,
76
+ simpleqa: 29.7,
77
+ global_mmlu_lite: 88.4,
78
+ livecodebench_v6: 63.5,
79
+ lbpp_v2: 51.1,
80
+ bigcodebench: 44.2,
81
+ mmmu: 76.7,
82
+ humanitys_last_exam: 12.1
83
+
84
+ },
85
+ },
86
+ {
87
+ model: "Gemini 2.0 Flash",
88
+ provider: "Google",
89
+ inputPrice: 0.1,
90
+ outputPrice: 0.4,
91
+ source: "https://ai.google.dev/gemini-api/docs/thinking",
92
+ benchmark: {
93
+ aime_2025: 27.5,
94
+ gpqa_diamond: 60.1,
95
+ simpleqa: 29.9,
96
+ global_mmlu_lite: 83.4,
97
+ livecodebench_v6: 34.5,
98
+ lbpp_v2: 22.2,
99
+ mmmu: 71.7,
100
+ facts_grounding: 84.6,
101
+ humanitys_last_exam: 5.1,
102
+ mrcr_v2_avg_128k: 36.0,
103
+ mrcr_v2_pointwise_1m: 6.0,
104
+
105
+ },
106
+ },
107
+ {
108
+ model: "Gemini 2.5 Pro Preview (05-06)",
109
+ provider: "Google",
110
+ inputPrice: 2.5,
111
+ outputPrice: 15.0,
112
+ source: "https://blog.google/products/gemini/gemini-2-5-pro-updates/",
113
+ benchmark: {
114
+ humanitys_last_exam: 17.8,
115
+ gpqa_diamond: 83.0,
116
+ aime_2025: 83.0,
117
+ livecodebench_v6: 75.6,
118
+ lbpp_v2: 76.5,
119
+ bigcodebench: 72.7,
120
+ swe_bench_verified: 63.2,
121
+ simpleqa: 50.8,
122
+ mmmu: 79.6,
123
+
124
+ video_mme: 84.8,
125
+ mrcr_v2_avg_128k: 93.0,
126
+ mrcr_v2_pointwise_1m: 82.9,
127
+ global_mmlu_lite: 88.6,
128
+ },
129
+ },
130
+ {
131
+ model: "Gemini 2.5 Pro Experimental (03-25)",
132
+ provider: "Google",
133
+ inputPrice: 2.5,
134
+ outputPrice: 15.0,
135
+ source: "https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/",
136
+ benchmark: {
137
+ humanitys_last_exam: 18.8,
138
+ gpqa_diamond: 84.0,
139
+ aime_2025: 86.7,
140
+ livecodebench_v6: 70.4,
141
+ lbpp_v2: 74.0,
142
+ bigcodebench: 68.6,
143
+ swe_bench_verified: 63.8,
144
+ simpleqa: 52.9,
145
+ mmmu: 81.7,
146
+ mrcr_v2_avg_128k: 94.5,
147
+ mrcr_v2_pointwise_1m: 83.1,
148
+ global_mmlu_lite: 89.8,
149
+ },
150
+ },
151
+
152
+
153
+ ];
src/lib/benchmarks/types.ts ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type BenchmarkMetric =
2
+ // Most common and high-priority
3
+ | "simpleqa"
4
+ | "mmlu_pro"
5
+ | "gpqa"
6
+ | "egoschema"
7
+ | "loft"
8
+ | "mmmu"
9
+ | "lcb"
10
+ | "aime_24"
11
+ | "aime_2025"
12
+ | "gpqa_diamond"
13
+
14
+ // Code benchmarks (frequent)
15
+ | "humaneval"
16
+ | "mbpp"
17
+ | "bigcodebench"
18
+ | "livecodebench_v6"
19
+ | "swe_bench_verified"
20
+ | "lbpp_v2"
21
+
22
+ // General reasoning & robustness
23
+ | "bigbench_extra_hard"
24
+ | "global_mmlu_lite"
25
+
26
+ // Optional: less frequent but still potentially useful
27
+ | "facts_grounding"
28
+ | "humanitys_last_exam"
29
+ | "mrcr_v2_avg_128k"
30
+ | "mrcr_v2_pointwise_1m"
31
+ | "video_mme";
32
+
33
+ // Note: "vibe_eval_reka" is intentionally excluded for now.
34
+
35
+ export interface Benchmark {
36
+ model: string;
37
+ provider: string;
38
+ benchmark: Partial<Record<BenchmarkMetric, number>>;
39
+ inputPrice: number;
40
+ outputPrice: number;
41
+ source: string;
42
+ version?: string;
43
+ }
44
+
45
+ export const benchmarkMetricOrder: BenchmarkMetric[] = [
46
+ // Most common and high-priority
47
+ "simpleqa",
48
+ "mmlu_pro",
49
+ "gpqa",
50
+ "egoschema",
51
+ "loft",
52
+ "mmmu",
53
+ "lcb",
54
+ "aime_24",
55
+ "aime_2025",
56
+ "gpqa_diamond",
57
+
58
+ // // Code benchmarks (frequent)
59
+ // "humaneval",
60
+ // "mbpp",
61
+ // "bigcodebench",
62
+ // "livecodebench_v6",
63
+ // "swe_bench_verified",
64
+ // "lbpp_v2",
65
+
66
+ // // General reasoning & robustness
67
+ // "bigbench_extra_hard",
68
+ // "global_mmlu_lite",
69
+
70
+ // // Optional: less frequent but still potentially useful
71
+ // "facts_grounding",
72
+ // "humanitys_last_exam",
73
+ // "mrcr_v2_avg_128k",
74
+ // "mrcr_v2_pointwise_1m",
75
+ // "video_mme",
76
+ ];
src/lib/benchmarks/xai.ts ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Benchmark } from "./types";
2
+
3
+
4
+ export const xaiBenchmarks: Benchmark[] = [
5
+ {
6
+ model: "Grok 3 Beta",
7
+ provider: "Xai",
8
+ inputPrice: 3.00,
9
+ outputPrice: 15.00,
10
+ benchmark: {
11
+ aime_24: 52.2,
12
+ gpqa: 75.4,
13
+ lcb: 57.0,
14
+ mmlu_pro: 79.9,
15
+ loft: 83.3,
16
+ simpleqa: 43.6,
17
+ mmmu: 73.2,
18
+ egoschema: 74.5,
19
+ },
20
+ source: "https://x.ai/news/grok-3",
21
+ },
22
+ {
23
+ model: "Grok 3 mini Beta",
24
+ provider: "Xai",
25
+ inputPrice: 0.30,
26
+ outputPrice: 0.50,
27
+ benchmark: {
28
+ aime_24: 39.7,
29
+ gpqa: 66.2,
30
+ lcb: 41.5,
31
+ mmlu_pro: 78.9,
32
+ loft: 83.1,
33
+ simpleqa: 21.7,
34
+ mmmu: 69.4,
35
+ egoschema: 74.3,
36
+ },
37
+ source: "https://x.ai/news/grok-3",
38
+ },
39
+ ];