Presidentlin commited on
Commit
f54e377
·
1 Parent(s): aa63d7e
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 {
@@ -48,6 +41,7 @@ const App: React.FC = () => {
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;
@@ -58,159 +52,99 @@ const App: React.FC = () => {
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 (
@@ -219,121 +153,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 +202,19 @@ 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 { Switch } from "@/components/ui/switch";
5
 
6
+ import { mockData } from "@/lib/data";
7
+
8
+ import { ComparisonSelector } from "@/components/ComparisonSelector";
9
+ import { PricingTable } from "@/components/PricingTable";
10
+ import { benchmarkData } from "./lib/benchmarks/ index";
11
+ import { BenchmarkTable } from "./components/BenchmarkTable";
12
+
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 {
 
41
  const [expandedProviders, setExpandedProviders] = useState<string[]>([]);
42
  const [tokenCalculation, setTokenCalculation] = useState<string>("million");
43
  const [linkProviderModel, setLinkProviderModel] = useState<boolean>(false);
44
+ const [benchmarkSortKey, setBenchmarkSortKey] = useState<string | null>(null);
45
 
46
  const [sortConfig, setSortConfig] = useState<{
47
  key: keyof FlattenedModel;
 
52
  setData(mockData);
53
  }, []);
54
 
55
+ const normalize = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, "");
 
 
 
 
 
 
 
 
 
 
56
 
57
+ const flattenData = (data: Provider[]): FlattenedModel[] =>
58
+ data.flatMap((provider) =>
59
+ provider.models.map((model) => {
60
+ const matchedBenchmark = benchmarkData.find(
61
+ (b) => normalize(b.model) === normalize(model.name)
62
+ );
63
+ return {
64
+ provider: provider.provider,
65
+ uri: provider.uri,
66
+ ...model,
67
+ benchmark: matchedBenchmark?.benchmark ?? {},
68
+ };
69
+ })
70
  );
 
71
 
72
+ const filteredData = useMemo(() => {
73
+ if (!selectedProviders.length && !selectedModels.length && !linkProviderModel) return data;
74
+
75
+ return data
76
+ .filter((p) => !selectedProviders.length || selectedProviders.includes(p.provider))
77
+ .map((p) => ({
78
+ ...p,
79
+ models: p.models.filter((m) => {
80
+ if (linkProviderModel && !selectedModels.length) return selectedProviders.includes(p.provider);
81
+ if (!linkProviderModel && !selectedModels.length) return !selectedProviders.length || selectedProviders.includes(p.provider);
82
+ return selectedModels.includes(m.name);
83
+ }),
84
  }))
85
+ .filter((p) => p.models.length > 0);
86
+ }, [data, selectedProviders, selectedModels, linkProviderModel]);
87
 
88
+ const benchmarkedModels = useMemo(() => {
89
+ const flattened = flattenData(data); // all models, with benchmark lookup
90
+ return flattened.filter(
91
+ (model) => model.benchmark && Object.keys(model.benchmark).length > 0
92
+ );
93
+ }, [data]);
94
+
95
+ const sortedBenchmarkedModels = useMemo(() => {
96
+ if (!benchmarkSortKey) return benchmarkedModels;
97
+
98
+ return [...benchmarkedModels].sort((a, b) => {
99
+ const aVal = a.benchmark?.[benchmarkSortKey] ?? -Infinity;
100
+ const bVal = b.benchmark?.[benchmarkSortKey] ?? -Infinity;
101
+ return bVal - aVal;
102
+ });
103
+ }, [benchmarkedModels, benchmarkSortKey]);
104
+
105
+
106
+ const sortedFlattenedData = useMemo(() => {
107
+ const flattened = flattenData(filteredData);
108
+ if (!sortConfig) return flattened;
109
+
110
+ return [...flattened].sort((a, b) => {
111
+ const aVal = a[sortConfig.key];
112
+ const bVal = b[sortConfig.key];
113
+
114
+ if (typeof aVal === "string" && typeof bVal === "string") {
115
+ return sortConfig.direction === "ascending" ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
116
+ } else if (typeof aVal === "number" && typeof bVal === "number") {
117
+ return sortConfig.direction === "ascending" ? aVal - bVal : bVal - aVal;
118
+ }
119
+ return 0;
120
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }, [filteredData, sortConfig]);
122
 
123
  const requestSort = (key: keyof FlattenedModel) => {
124
+ const direction = sortConfig?.key === key && sortConfig.direction === "ascending" ? "descending" : "ascending";
 
 
 
 
 
 
 
125
  setSortConfig({ key, direction });
126
  };
127
 
128
  const toggleProviderExpansion = (provider: string) => {
129
  setExpandedProviders((prev) =>
130
+ prev.includes(provider) ? prev.filter((p) => p !== provider) : [...prev, provider]
 
 
131
  );
132
  };
133
 
134
  const getModelsForSelectedProviders = () => {
135
+ const allModels = data.flatMap((provider) =>
136
+ provider.models.map((model) => ({
137
+ label: model.name,
138
+ value: model.name,
139
+ provider: provider.provider,
140
+ }))
141
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ const filtered = linkProviderModel
144
+ ? allModels.filter((m) => selectedProviders.includes(m.provider))
145
+ : allModels;
146
+
147
+ return Array.from(new Map(filtered.map((m) => [m.value, m])).values());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  };
149
 
150
  return (
 
153
  <CardTitle>LLM Pricing Calculator</CardTitle>
154
  </CardHeader>
155
  <CardContent>
156
+ {/* Source Link */}
157
+ <p className="italic text-sm text-muted-foreground mb-4">
158
+ <a
159
+ href="https://huggingface.co/spaces/philschmid/llm-pricing"
160
+ className="underline"
161
+ >
162
+ This is a fork of philschmid tool: philschmid/llm-pricing
163
+ </a>
164
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
 
166
+ {/* Comparison Model Selector */}
167
+ <h3 className="text-lg font-semibold mb-2">Select Comparison Models</h3>
168
+ <ComparisonSelector
169
+ data={data}
170
+ expanded={expandedProviders}
171
+ comparisonModels={comparisonModels}
172
+ onToggleExpand={toggleProviderExpansion}
173
+ onChangeModel={(modelId, checked) =>
174
+ setComparisonModels((prev) =>
175
+ checked ? [...prev, modelId] : prev.filter((m) => m !== modelId)
176
+ )
177
+ }
178
+ />
179
+
180
+ {/* Token Inputs */}
181
+ <div className="flex gap-4 mt-6 mb-4">
182
  <div className="flex-1">
183
+ <label className="block text-sm font-medium">Input Tokens ({tokenCalculation})</label>
184
+ <Input type="number" value={inputTokens} min={1} onChange={(e) => setInputTokens(Number(e.target.value))} />
 
 
 
 
 
 
 
 
 
 
 
 
185
  </div>
186
  <div className="flex-1">
187
+ <label className="block text-sm font-medium">Output Tokens ({tokenCalculation})</label>
188
+ <Input type="number" value={outputTokens} min={1} onChange={(e) => setOutputTokens(Number(e.target.value))} />
 
 
 
 
 
 
 
 
 
 
 
 
189
  </div>
190
  <div className="flex-1">
191
+ <label className="block text-sm font-medium">Token Calculation</label>
 
 
 
 
 
192
  <select
 
193
  value={tokenCalculation}
194
  onChange={(e) => setTokenCalculation(e.target.value)}
195
+ className="mt-1 block w-full pl-3 pr-10 py-2 text-base border rounded-md"
196
  >
197
  <option value="billion">Billion Tokens</option>
198
  <option value="million">Million Tokens</option>
 
202
  </div>
203
  </div>
204
 
205
+ {/* Provider–Model Toggle */}
206
+ <div className="flex items-center space-x-2 mb-6">
207
+ <Switch id="linkProviderModel" checked={linkProviderModel} onCheckedChange={setLinkProviderModel} />
208
+ <label htmlFor="linkProviderModel" className="text-sm">Link Provider and Model</label>
 
 
 
 
 
 
 
 
 
209
  </div>
210
 
211
+ {/* Pricing Table */}
212
+ <h2 className="text-lg font-semibold mb-2">Pricing Table</h2>
213
+
214
+
215
+ {/* Benchmark Table */}
216
+ <h2 className="text-lg font-semibold mt-12 mb-2">Benchmark Table</h2>
217
+ <BenchmarkTable data={sortedBenchmarkedModels} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </CardContent>
219
  </Card>
220
  );
src/components/BenchmarkTable.tsx ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 } from "@/App";
12
+
13
+ interface BenchmarkTableProps {
14
+ data: FlattenedModel[];
15
+ }
16
+
17
+ export const BenchmarkTable: React.FC<BenchmarkTableProps> = ({ data }) => {
18
+ // Extract all unique benchmark keys
19
+ const benchmarkMetrics = React.useMemo(() => {
20
+ const allKeys = data.flatMap((model) =>
21
+ model.benchmark ? Object.keys(model.benchmark) : []
22
+ );
23
+ return Array.from(new Set(allKeys)).sort();
24
+ }, [data]);
25
+
26
+ // State: visible metrics
27
+ const [visibleMetrics, setVisibleMetrics] = React.useState<string[]>([]);
28
+
29
+ // Set default to all if empty on first render
30
+ React.useEffect(() => {
31
+ if (benchmarkMetrics.length && visibleMetrics.length === 0) {
32
+ setVisibleMetrics(benchmarkMetrics);
33
+ }
34
+ }, [benchmarkMetrics]);
35
+
36
+ // Style cells by score
37
+ const getCellStyle = (value?: number) => {
38
+ if (value === undefined) return "";
39
+ if (value >= 85) return "bg-green-100";
40
+ if (value >= 60) return "bg-yellow-100";
41
+ return "bg-red-100";
42
+ };
43
+
44
+ return (
45
+ <>
46
+ <div className="mb-4 max-w-md">
47
+ <MultiSelect
48
+ options={benchmarkMetrics.map((metric) => ({
49
+ label: metric.replace(/_/g, " ").toUpperCase(),
50
+ value: metric,
51
+ }))}
52
+ defaultValue={benchmarkMetrics}
53
+ onValueChange={setVisibleMetrics}
54
+ placeholder="Select benchmarks to show"
55
+ />
56
+ </div>
57
+
58
+ <Table>
59
+ <TableHeader>
60
+ <TableRow>
61
+ <TableHead>Provider</TableHead>
62
+ <TableHead>Model</TableHead>
63
+ {visibleMetrics.map((metric) => (
64
+ <TableHead key={metric}>
65
+ {metric.replace(/_/g, " ").toUpperCase()}
66
+ </TableHead>
67
+ ))}
68
+ </TableRow>
69
+ </TableHeader>
70
+ <TableBody>
71
+ {data.map((model) => (
72
+ <TableRow key={`${model.provider}-${model.name}`}>
73
+ <TableCell>
74
+ <a href={model.uri} className="underline">
75
+ {model.provider}
76
+ </a>
77
+ </TableCell>
78
+ <TableCell>{model.name}</TableCell>
79
+ {visibleMetrics.map((metric) => {
80
+ const score = model.benchmark?.[metric];
81
+ return (
82
+ <TableCell key={metric} className={getCellStyle(score)}>
83
+ {score !== undefined ? `${score.toFixed(1)}%` : "—"}
84
+ </TableCell>
85
+ );
86
+ })}
87
+ </TableRow>
88
+ ))}
89
+ </TableBody>
90
+ </Table>
91
+ </>
92
+ );
93
+ };
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,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ linkProviderModel: boolean;
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
+ linkProviderModel,
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 = linkProviderModel
70
+ ? flatModels.filter((m) => selectedProviders.includes(m.provider))
71
+ : flatModels;
72
+
73
+ // Remove duplicates
74
+ return Array.from(new Map(filtered.map((m) => [m.value, m])).values());
75
+ };
76
+
77
+ return (
78
+ <Table>
79
+ <TableHeader>
80
+ <TableRow>
81
+ <TableHead>
82
+ <button onClick={() => requestSort("provider")}>
83
+ Provider {sortConfig?.key === "provider" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
84
+ </button>
85
+ </TableHead>
86
+ <TableHead>
87
+ <button onClick={() => requestSort("name")}>
88
+ Model {sortConfig?.key === "name" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
89
+ </button>
90
+ </TableHead>
91
+ <TableHead>
92
+ <button onClick={() => requestSort("inputPrice")}>
93
+ Input Price (million tokens) {sortConfig?.key === "inputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
94
+ </button>
95
+ </TableHead>
96
+ <TableHead>
97
+ <button onClick={() => requestSort("outputPrice")}>
98
+ Output Price (million tokens) {sortConfig?.key === "outputPrice" ? (sortConfig.direction === "ascending" ? "▲" : "▼") : null}
99
+ </button>
100
+ </TableHead>
101
+ <TableHead>Total Price (per {tokenCalculation} tokens)</TableHead>
102
+ {comparisonModels.map((model) => (
103
+ <TableHead key={model} colSpan={2}>
104
+ Compared to {model}
105
+ </TableHead>
106
+ ))}
107
+ </TableRow>
108
+ <TableRow>
109
+ <TableHead>
110
+ <MultiSelect
111
+ options={providers.map((p) => ({ label: p.provider, value: p.provider }))}
112
+ defaultValue={selectedProviders}
113
+ onValueChange={onProviderChange}
114
+ />
115
+ </TableHead>
116
+ <TableHead>
117
+ <MultiSelect
118
+ options={getModelsForSelectedProviders()}
119
+ defaultValue={selectedModels}
120
+ onValueChange={onModelChange}
121
+ />
122
+ </TableHead>
123
+ <TableHead />
124
+ <TableHead />
125
+ <TableHead />
126
+ {comparisonModels.flatMap((model) => [
127
+ <TableHead key={`${model}-input`}>Input</TableHead>,
128
+ <TableHead key={`${model}-output`}>Output</TableHead>,
129
+ ])}
130
+ </TableRow>
131
+ </TableHeader>
132
+ <TableBody>
133
+ {data.map((item) => (
134
+ <TableRow key={`${item.provider}-${item.name}`}>
135
+ <TableCell>
136
+ <a href={item.uri} className="underline">
137
+ {item.provider}
138
+ </a>
139
+ </TableCell>
140
+ <TableCell>{item.name}</TableCell>
141
+ <TableCell>{item.inputPrice.toFixed(2)}</TableCell>
142
+ <TableCell>{item.outputPrice.toFixed(2)}</TableCell>
143
+ <TableCell className="font-bold">
144
+ ${(
145
+ calculatePrice(item.inputPrice, inputTokens) +
146
+ calculatePrice(item.outputPrice, outputTokens)
147
+ ).toFixed(2)}
148
+ </TableCell>
149
+ {comparisonModels.flatMap((model) => {
150
+ const [comparisonProvider, comparisonModelName] = model.split(":");
151
+ const comparisonModel = providers
152
+ .find((p) => p.provider === comparisonProvider)
153
+ ?.models.find((m) => m.name === comparisonModelName);
154
+
155
+ if (!comparisonModel) return [<TableCell key="missing">–</TableCell>, <TableCell key="missing2">–</TableCell>];
156
+
157
+ const inputDelta = calculateComparison(item.inputPrice, comparisonModel.inputPrice);
158
+ const outputDelta = calculateComparison(item.outputPrice, comparisonModel.outputPrice);
159
+
160
+ return [
161
+ <TableCell key={`${model}-input`} className={parseFloat(inputDelta) < 0 ? "bg-green-100" : parseFloat(inputDelta) > 0 ? "bg-red-100" : ""}>
162
+ {`${item.provider}:${item.name}` === model ? "0.00%" : `${inputDelta}%`}
163
+ </TableCell>,
164
+ <TableCell key={`${model}-output`} className={parseFloat(outputDelta) < 0 ? "bg-green-100" : parseFloat(outputDelta) > 0 ? "bg-red-100" : ""}>
165
+ {`${item.provider}:${item.name}` === model ? "0.00%" : `${outputDelta}%`}
166
+ </TableCell>,
167
+ ];
168
+ })}
169
+ </TableRow>
170
+ ))}
171
+ </TableBody>
172
+ </Table>
173
+ );
174
+ };
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,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Benchmark } from "./types";
2
+
3
+
4
+ export const googleBenchmarks: Benchmark[] = [
5
+ {
6
+ model: "Gemini Diffusion",
7
+ benchmark: {
8
+ livecodebench_v6: 30.9,
9
+ bigcodebench: 45.4,
10
+ lbpp_v2: 56.8,
11
+ swe_bench_verified: 22.9,
12
+ humaneval: 89.6,
13
+ mbpp: 76.0,
14
+ gpqa_diamond: 40.4,
15
+ aime_2025: 23.3,
16
+ bigbench_extra_hard: 15.0,
17
+ global_mmlu_lite: 69.1,
18
+ },
19
+ source: "Google Gemini site",
20
+ },
21
+ {
22
+ model: "Gemini 2.0 Flash-Lite",
23
+ benchmark: {
24
+ livecodebench_v6: 28.5,
25
+ bigcodebench: 45.8,
26
+ lbpp_v2: 56.0,
27
+ swe_bench_verified: 28.5,
28
+ humaneval: 90.2,
29
+ mbpp: 75.8,
30
+ gpqa_diamond: 56.5,
31
+ aime_2025: 20.0,
32
+ bigbench_extra_hard: 21.0,
33
+ global_mmlu_lite: 79.0,
34
+ },
35
+ source: "Google Gemini site",
36
+ },
37
+ ];
src/lib/{types.ts → benchmarks/types.ts} RENAMED
File without changes
src/lib/benchmarks/xai.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Benchmark } from "./types";
2
+
3
+
4
+ export const xaiBenchmarks: Benchmark[] = [
5
+ {
6
+ model: "Grok 3 Beta",
7
+ benchmark: {
8
+ aime_24: 52.2,
9
+ gpqa: 75.4,
10
+ lcb: 57.0,
11
+ mmlu_pro: 79.9,
12
+ loft: 83.3,
13
+ simpleqa: 43.6,
14
+ mmmu: 73.2,
15
+ egoschema: 74.5,
16
+ },
17
+ source: "x.ai blog",
18
+ },
19
+ {
20
+ model: "Grok 3 mini Beta",
21
+ benchmark: {
22
+ aime_24: 39.7,
23
+ gpqa: 66.2,
24
+ lcb: 41.5,
25
+ mmlu_pro: 78.9,
26
+ loft: 83.1,
27
+ simpleqa: 21.7,
28
+ mmmu: 69.4,
29
+ egoschema: 74.3,
30
+ },
31
+ source: "x.ai blog",
32
+ },
33
+ ];