Presidentlin commited on
Commit
2c1eb53
·
1 Parent(s): de892c6
Files changed (4) hide show
  1. package-lock.json +0 -0
  2. package.json +47 -46
  3. src/App.tsx +541 -541
  4. src/components/ui/switch.tsx +27 -27
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -1,46 +1,47 @@
1
- {
2
- "name": "llm-pricing",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc -b && vite build",
9
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
- "preview": "vite preview"
11
- },
12
- "dependencies": {
13
- "@radix-ui/react-checkbox": "^1.1.1",
14
- "@radix-ui/react-collapsible": "^1.1.0",
15
- "@radix-ui/react-dialog": "^1.1.1",
16
- "@radix-ui/react-popover": "^1.1.1",
17
- "@radix-ui/react-select": "^2.1.1",
18
- "@radix-ui/react-separator": "^1.1.0",
19
- "@radix-ui/react-slot": "^1.1.0",
20
- "class-variance-authority": "^0.7.0",
21
- "clsx": "^2.1.1",
22
- "cmdk": "^1.0.0",
23
- "lucide-react": "^0.399.0",
24
- "react": "^18.3.1",
25
- "react-dom": "^18.3.1",
26
- "recharts": "^2.12.7",
27
- "tailwind-merge": "^2.3.0",
28
- "tailwindcss-animate": "^1.0.7"
29
- },
30
- "devDependencies": {
31
- "@types/node": "^20.14.9",
32
- "@types/react": "^18.3.3",
33
- "@types/react-dom": "^18.3.0",
34
- "@typescript-eslint/eslint-plugin": "^7.13.1",
35
- "@typescript-eslint/parser": "^7.13.1",
36
- "@vitejs/plugin-react": "^4.3.1",
37
- "autoprefixer": "^10.4.19",
38
- "eslint": "^8.57.0",
39
- "eslint-plugin-react-hooks": "^4.6.2",
40
- "eslint-plugin-react-refresh": "^0.4.7",
41
- "postcss": "^8.4.39",
42
- "tailwindcss": "^3.4.4",
43
- "typescript": "^5.2.2",
44
- "vite": "^5.3.1"
45
- }
46
- }
 
 
1
+ {
2
+ "name": "llm-pricing",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@radix-ui/react-checkbox": "^1.1.1",
14
+ "@radix-ui/react-collapsible": "^1.1.0",
15
+ "@radix-ui/react-dialog": "^1.1.1",
16
+ "@radix-ui/react-popover": "^1.1.1",
17
+ "@radix-ui/react-select": "^2.1.1",
18
+ "@radix-ui/react-separator": "^1.1.0",
19
+ "@radix-ui/react-slot": "^1.1.0",
20
+ "@radix-ui/react-switch": "^1.1.1",
21
+ "class-variance-authority": "^0.7.0",
22
+ "clsx": "^2.1.1",
23
+ "cmdk": "^1.0.0",
24
+ "lucide-react": "^0.399.0",
25
+ "react": "^18.3.1",
26
+ "react-dom": "^18.3.1",
27
+ "recharts": "^2.12.7",
28
+ "tailwind-merge": "^2.3.0",
29
+ "tailwindcss-animate": "^1.0.7"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.14.9",
33
+ "@types/react": "^18.3.3",
34
+ "@types/react-dom": "^18.3.0",
35
+ "@typescript-eslint/eslint-plugin": "^7.13.1",
36
+ "@typescript-eslint/parser": "^7.13.1",
37
+ "@vitejs/plugin-react": "^4.3.1",
38
+ "autoprefixer": "^10.4.19",
39
+ "eslint": "^8.57.0",
40
+ "eslint-plugin-react-hooks": "^4.6.2",
41
+ "eslint-plugin-react-refresh": "^0.4.7",
42
+ "postcss": "^8.4.39",
43
+ "tailwindcss": "^3.4.4",
44
+ "typescript": "^5.2.2",
45
+ "vite": "^5.3.1"
46
+ }
47
+ }
src/App.tsx CHANGED
@@ -1,541 +1,541 @@
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 {
30
- name: string;
31
- inputPrice: number;
32
- outputPrice: number;
33
- }
34
-
35
- export interface Provider {
36
- provider: string;
37
- uri: string;
38
- models: Model[];
39
- }
40
-
41
- const App: React.FC = () => {
42
- const [data, setData] = useState<Provider[]>([]);
43
- const [comparisonModels, setComparisonModels] = useState<string[]>([]);
44
- const [inputTokens, setInputTokens] = useState<number>(1);
45
- const [outputTokens, setOutputTokens] = useState<number>(1);
46
- const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
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">
218
- <CardHeader>
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>
340
- <option value="thousand">Thousand Tokens</option>
341
- <option value="unit">Unit Tokens</option>
342
- </select>
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
- );
539
- };
540
-
541
- export default App;
 
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 {
30
+ name: string;
31
+ inputPrice: number;
32
+ outputPrice: number;
33
+ }
34
+
35
+ export interface Provider {
36
+ provider: string;
37
+ uri: string;
38
+ models: Model[];
39
+ }
40
+
41
+ const App: React.FC = () => {
42
+ const [data, setData] = useState<Provider[]>([]);
43
+ const [comparisonModels, setComparisonModels] = useState<string[]>([]);
44
+ const [inputTokens, setInputTokens] = useState<number>(1);
45
+ const [outputTokens, setOutputTokens] = useState<number>(1);
46
+ const [selectedProviders, setSelectedProviders] = useState<string[]>([]);
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">
218
+ <CardHeader>
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>
340
+ <option value="thousand">Thousand Tokens</option>
341
+ <option value="unit">Unit Tokens</option>
342
+ </select>
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
+ );
539
+ };
540
+
541
+ export default App;
src/components/ui/switch.tsx CHANGED
@@ -1,27 +1,27 @@
1
- import { forwardRef } from "react";
2
- import * as SwitchPrimitives from "@radix-ui/react-switch";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const Switch = forwardRef<
7
- React.ElementRef<typeof SwitchPrimitives.Root>,
8
- React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
9
- >(({ className, ...props }, ref) => (
10
- <SwitchPrimitives.Root
11
- className={cn(
12
- "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
13
- className
14
- )}
15
- {...props}
16
- ref={ref}
17
- >
18
- <SwitchPrimitives.Thumb
19
- className={cn(
20
- "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
21
- )}
22
- />
23
- </SwitchPrimitives.Root>
24
- ));
25
- Switch.displayName = SwitchPrimitives.Root.displayName;
26
-
27
- export { Switch };
 
1
+ import { forwardRef } from "react";
2
+ import * as SwitchPrimitives from "@radix-ui/react-switch";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const Switch = forwardRef<
7
+ React.ElementRef<typeof SwitchPrimitives.Root>,
8
+ React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
9
+ >(({ className, ...props }, ref) => (
10
+ <SwitchPrimitives.Root
11
+ className={cn(
12
+ "peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
13
+ className
14
+ )}
15
+ {...props}
16
+ ref={ref}
17
+ >
18
+ <SwitchPrimitives.Thumb
19
+ className={cn(
20
+ "pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
21
+ )}
22
+ />
23
+ </SwitchPrimitives.Root>
24
+ ));
25
+ Switch.displayName = SwitchPrimitives.Root.displayName;
26
+
27
+ export { Switch };