@tokcalc/mcp-server 0.1.3 → 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -406
- package/dist/http.js +23504 -0
- package/dist/index.js +21290 -0
- package/package.json +36 -92
- package/.zscripts/build.sh +0 -175
- package/.zscripts/database-runtime-build.sh +0 -33
- package/.zscripts/dev.pid +0 -1
- package/.zscripts/dev.sh +0 -154
- package/.zscripts/mini-services-build.sh +0 -78
- package/.zscripts/mini-services-install.sh +0 -65
- package/.zscripts/mini-services-start.sh +0 -123
- package/.zscripts/python-runtime-build.sh +0 -120
- package/.zscripts/start.sh +0 -145
- package/CAPACITY_STUDY.md +0 -283
- package/CODE_OF_CONDUCT.md +0 -55
- package/CONTRIBUTING.md +0 -177
- package/Caddyfile +0 -23
- package/LICENSE +0 -204
- package/bun.lock +0 -1965
- package/components.json +0 -21
- package/db/custom.db +0 -0
- package/download/README.md +0 -1
- package/download/tokcalc-dark-calculator.png +0 -0
- package/download/tokcalc-dark-default.png +0 -0
- package/download/tokcalc-demo.webm +0 -0
- package/download/tokcalc-github-link.png +0 -0
- package/download/tokcalc-hydration-fixed.png +0 -0
- package/download/tokcalc-issue-resolved.png +0 -0
- package/download/tokcalc-light-mode.png +0 -0
- package/download/tokcalc-light-reference.png +0 -0
- package/download/tokcalc-long-context-qwen.png +0 -0
- package/download/tokcalc-long-context.png +0 -0
- package/download/tokcalc-og-image-preview.png +0 -0
- package/download/tokcalc-phase2-3.png +0 -0
- package/download/tokcalc-plain-english.png +0 -0
- package/download/tokcalc-preview.png +0 -0
- package/download/tokcalc-share-bvb.png +0 -0
- package/download/tokcalc-share-feature.png +0 -0
- package/download/tokcalc-tab-build-vs-buy.png +0 -0
- package/download/tokcalc-tab-calculator.png +0 -0
- package/download/tokcalc-tab-reference.png +0 -0
- package/eslint.config.mjs +0 -50
- package/examples/websocket/frontend.tsx +0 -196
- package/examples/websocket/server.ts +0 -138
- package/mini-services/.gitkeep +0 -0
- package/mini-services/mcp-server/README.md +0 -86
- package/mini-services/mcp-server/bun.lock +0 -202
- package/mini-services/mcp-server/index.ts +0 -504
- package/mini-services/mcp-server/package.json +0 -40
- package/next.config.ts +0 -12
- package/postcss.config.mjs +0 -5
- package/prisma/schema.prisma +0 -32
- package/public/google6f58ca6be85fa903.html +0 -1
- package/public/logo.svg +0 -29
- package/public/manifest.json +0 -51
- package/public/og-icon-256.png +0 -0
- package/public/og.png +0 -0
- package/public/robots.txt +0 -25
- package/public/sitemap.xml +0 -23
- package/public/tokcalc-demo.gif +0 -0
- package/scripts/og-template.html +0 -120
- package/scripts/render-og.mjs +0 -43
- package/server.json +0 -21
- package/src/app/api/pricing/aws/route.ts +0 -186
- package/src/app/api/pricing/azure/route.ts +0 -168
- package/src/app/api/pricing/gcp/route.ts +0 -230
- package/src/app/api/pricing/vast-ai/route.ts +0 -164
- package/src/app/api/route.ts +0 -5
- package/src/app/compare/h100-vs-h200/layout.tsx +0 -30
- package/src/app/compare/h100-vs-h200/page.tsx +0 -328
- package/src/app/globals.css +0 -122
- package/src/app/layout.tsx +0 -276
- package/src/app/page.tsx +0 -2670
- package/src/components/azure-live-pricing.tsx +0 -185
- package/src/components/benchmark-import.tsx +0 -340
- package/src/components/confidence-badge.tsx +0 -116
- package/src/components/live-pricing-comparison.tsx +0 -241
- package/src/components/theme-provider.tsx +0 -11
- package/src/components/theme-toggle.tsx +0 -55
- package/src/components/ui/accordion.tsx +0 -66
- package/src/components/ui/alert-dialog.tsx +0 -157
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/aspect-ratio.tsx +0 -11
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/badge.tsx +0 -46
- package/src/components/ui/breadcrumb.tsx +0 -109
- package/src/components/ui/button.tsx +0 -59
- package/src/components/ui/calendar.tsx +0 -213
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/carousel.tsx +0 -241
- package/src/components/ui/chart.tsx +0 -353
- package/src/components/ui/checkbox.tsx +0 -32
- package/src/components/ui/collapsible.tsx +0 -33
- package/src/components/ui/command.tsx +0 -184
- package/src/components/ui/context-menu.tsx +0 -252
- package/src/components/ui/dialog.tsx +0 -143
- package/src/components/ui/drawer.tsx +0 -135
- package/src/components/ui/dropdown-menu.tsx +0 -257
- package/src/components/ui/form.tsx +0 -167
- package/src/components/ui/hover-card.tsx +0 -44
- package/src/components/ui/input-otp.tsx +0 -77
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/menubar.tsx +0 -276
- package/src/components/ui/navigation-menu.tsx +0 -168
- package/src/components/ui/pagination.tsx +0 -127
- package/src/components/ui/popover.tsx +0 -48
- package/src/components/ui/progress.tsx +0 -31
- package/src/components/ui/radio-group.tsx +0 -45
- package/src/components/ui/resizable.tsx +0 -56
- package/src/components/ui/scroll-area.tsx +0 -58
- package/src/components/ui/select.tsx +0 -185
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/sidebar.tsx +0 -726
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/slider.tsx +0 -63
- package/src/components/ui/sonner.tsx +0 -25
- package/src/components/ui/switch.tsx +0 -31
- package/src/components/ui/table.tsx +0 -116
- package/src/components/ui/tabs.tsx +0 -66
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/toggle-group.tsx +0 -73
- package/src/components/ui/toggle.tsx +0 -47
- package/src/components/ui/tooltip.tsx +0 -61
- package/src/components/vast-ai-live-pricing.tsx +0 -176
- package/src/hooks/use-mobile.ts +0 -19
- package/src/hooks/use-toast.ts +0 -194
- package/src/lib/benchmark-parser-sglang.ts +0 -150
- package/src/lib/benchmark-parser-tokcalc.ts +0 -247
- package/src/lib/benchmark-parser-trtllm.ts +0 -152
- package/src/lib/benchmark-parser-vllm.ts +0 -198
- package/src/lib/benchmark-schema.ts +0 -263
- package/src/lib/db.ts +0 -13
- package/src/lib/engine-presets.ts +0 -183
- package/src/lib/price-schema.ts +0 -141
- package/src/lib/token-calc.ts +0 -808
- package/src/lib/track.ts +0 -31
- package/src/lib/url-state.ts +0 -256
- package/src/lib/utils.ts +0 -6
- package/tailwind.config.ts +0 -64
- package/tests/database-runtime-build.sh +0 -75
- package/tests/python-runtime-build.sh +0 -64
- package/tests/python-runtime-container.sh +0 -31
- package/tool-results/bash_1789888171144_2c5381860539.txt +0 -161
- package/tool-results/bash_1789888175925_49c53ba3c61b.txt +0 -191
- package/tool-results/bash_1789888181202_49c53ba3c61b.txt +0 -191
- package/tool-results/bash_1789888195219_4a86a5c91411.txt +0 -200
- package/tool-results/bash_1789888203128_6cca13c71b47.txt +0 -199
- package/tool-results/bash_1789929256963_2a52aff0d0a8.txt +0 -160
- package/tool-results/read_1789888151021_69f58eec6a5b.txt +0 -653
- package/tool-results/read_1789888153837_1d3a8bfc2a94.txt +0 -653
- package/tool-results/read_1789888163087_ccc406d47505.txt +0 -122
- package/tool-results/read_1789888167347_67d1d7c9830a.txt +0 -122
- package/tool-results/read_1789929252529_d90e8f383a25.txt +0 -285
- package/tsconfig.json +0 -42
- package/upload/Pasted Content_1789887800864.txt +0 -652
- package/upload/Pasted Content_1789887909561.txt +0 -652
- package/upload/Pasted Content_1789887918428.txt +0 -652
- package/upload/Pasted Content_1789887959420.txt +0 -652
- package/upload/Pasted Content_1789888020485.txt +0 -652
- package/upload/Pasted Content_1789888058079.txt +0 -652
- package/upload/Pasted Content_1789888885033.txt +0 -686
- package/upload/Pasted Content_1789928912741.txt +0 -285
- package/upload/Pasted Content_1789928938402.txt +0 -285
- package/upload/Pasted Content_1789929160389.txt +0 -285
- package/upload/Pasted Content_1789929176660.txt +0 -285
- package/upload/issue_vision.json +0 -28
- package/upload/pasted_image_1789883175209.png +0 -0
- package/upload/pasted_image_1789899056690.png +0 -0
- package/upload/pasted_image_1789900371483.png +0 -0
- package/upload/pasted_image_1789900472823.png +0 -0
- package/upload/pasted_image_1789900490374.png +0 -0
- package/upload/pasted_image_1789900585552.png +0 -0
- package/upload/pasted_image_1789900606519.png +0 -0
- package/upload/pasted_image_1789901598705.png +0 -0
- package/upload/pasted_image_1789901613545.png +0 -0
- package/upload/pasted_image_1789978382674.png +0 -0
- package/upload/pasted_image_1789978392749.png +0 -0
- package/upload/pasted_image_1789978474879.png +0 -0
- package/upload/pasted_image_1789978523652.png +0 -0
- package/upload/pasted_image_1789984219089.png +0 -0
- package/upload/pasted_image_1789984491896.png +0 -0
- package/upload/pasted_image_1789985017950.png +0 -0
- package/upload/pasted_image_1789985036765.png +0 -0
- package/upload/pasted_image_1789985049848.png +0 -0
- package/upload/pasted_image_1790002427833.png +0 -0
- package/upload/pasted_image_1790002659944.png +0 -0
- package/upload/pasted_image_1790037038476.png +0 -0
- package/upload/screenshot_analysis.json +0 -28
- package/upload/vision_output.json +0 -28
package/public/robots.txt
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# Crawl everything
|
|
2
|
-
User-agent: *
|
|
3
|
-
Allow: /
|
|
4
|
-
|
|
5
|
-
# Block the dev API route from indexing
|
|
6
|
-
Disallow: /api/
|
|
7
|
-
|
|
8
|
-
# Explicitly allow social preview crawlers (redundant, but explicit)
|
|
9
|
-
User-agent: Twitterbot
|
|
10
|
-
Allow: /
|
|
11
|
-
|
|
12
|
-
User-agent: facebookexternalhit
|
|
13
|
-
Allow: /
|
|
14
|
-
|
|
15
|
-
User-agent: LinkedInBot
|
|
16
|
-
Allow: /
|
|
17
|
-
|
|
18
|
-
User-agent: Discordbot
|
|
19
|
-
Allow: /
|
|
20
|
-
|
|
21
|
-
User-agent: Slackbot
|
|
22
|
-
Allow: /
|
|
23
|
-
|
|
24
|
-
# Sitemap
|
|
25
|
-
Sitemap: https://tokcalc.vercel.app/sitemap.xml
|
package/public/sitemap.xml
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
3
|
-
<url>
|
|
4
|
-
<loc>https://tokcalc.vercel.app/</loc>
|
|
5
|
-
<changefreq>weekly</changefreq>
|
|
6
|
-
<priority>1.0</priority>
|
|
7
|
-
</url>
|
|
8
|
-
<url>
|
|
9
|
-
<loc>https://tokcalc.vercel.app/compare/h100-vs-h200</loc>
|
|
10
|
-
<changefreq>monthly</changefreq>
|
|
11
|
-
<priority>0.9</priority>
|
|
12
|
-
</url>
|
|
13
|
-
<url>
|
|
14
|
-
<loc>https://tokcalc.vercel.app/compare/gguf-q4-k-m-vs-q5-k-m</loc>
|
|
15
|
-
<changefreq>monthly</changefreq>
|
|
16
|
-
<priority>0.8</priority>
|
|
17
|
-
</url>
|
|
18
|
-
<url>
|
|
19
|
-
<loc>https://tokcalc.vercel.app/self-host-vs-openai-api</loc>
|
|
20
|
-
<changefreq>monthly</changefreq>
|
|
21
|
-
<priority>0.8</priority>
|
|
22
|
-
</url>
|
|
23
|
-
</urlset>
|
package/public/tokcalc-demo.gif
DELETED
|
Binary file
|
package/scripts/og-template.html
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<style>
|
|
6
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@600;700;800;900&family=JetBrains+Mono:wght@500;700&display=swap');
|
|
7
|
-
|
|
8
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
html, body { width: 1200px; height: 630px; overflow: hidden; }
|
|
10
|
-
body {
|
|
11
|
-
font-family: 'Inter', -apple-system, sans-serif;
|
|
12
|
-
/* Solid dark background — no gradients (LinkedIn compression destroys subtle gradients).
|
|
13
|
-
Slight green tint stays on-brand without losing contrast. */
|
|
14
|
-
background: #0a0e0a;
|
|
15
|
-
color: #ffffff;
|
|
16
|
-
display: flex;
|
|
17
|
-
flex-direction: column;
|
|
18
|
-
padding: 80px 88px;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/* Top row: BIG logo + brand name. Logo is 96px (was 56px) — visible at 520px card */
|
|
22
|
-
.header { display: flex; align-items: center; gap: 24px; }
|
|
23
|
-
.logo-mark {
|
|
24
|
-
width: 96px; height: 96px; border-radius: 22px;
|
|
25
|
-
background: linear-gradient(135deg, #34d399 0%, #059669 100%);
|
|
26
|
-
display: flex; align-items: center; justify-content: center;
|
|
27
|
-
/* Subtle shadow only — no glow effect (gets destroyed by compression) */
|
|
28
|
-
box-shadow: 0 12px 40px rgba(16, 185, 129, 0.5);
|
|
29
|
-
}
|
|
30
|
-
.logo-mark svg { width: 52px; height: 52px; }
|
|
31
|
-
.brand {
|
|
32
|
-
font-size: 64px; font-weight: 900; letter-spacing: -0.05em;
|
|
33
|
-
color: #ffffff; line-height: 1;
|
|
34
|
-
}
|
|
35
|
-
.brand-sub {
|
|
36
|
-
font-size: 22px; color: #34d399; font-weight: 600;
|
|
37
|
-
margin-left: 20px; padding-left: 20px;
|
|
38
|
-
border-left: 2px solid rgba(52, 211, 153, 0.4);
|
|
39
|
-
line-height: 1.2;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* Hero headline — MASSIVE. 88px so even at 520px card width it's ~38px = readable.
|
|
43
|
-
Pure white on near-black = max contrast, survives LinkedIn compression. */
|
|
44
|
-
.hero {
|
|
45
|
-
margin-top: 40px;
|
|
46
|
-
font-size: 88px;
|
|
47
|
-
font-weight: 900;
|
|
48
|
-
line-height: 1.05;
|
|
49
|
-
letter-spacing: -0.045em;
|
|
50
|
-
color: #ffffff;
|
|
51
|
-
}
|
|
52
|
-
.hero .accent {
|
|
53
|
-
color: #34d399; /* Saturated emerald, not subtle — survives compression */
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* Tagline — 28px (was 18px). Single line, no metric clutter. */
|
|
57
|
-
.tagline {
|
|
58
|
-
margin-top: 24px;
|
|
59
|
-
font-size: 28px;
|
|
60
|
-
color: #d4d4d8; /* Light gray, not dark gray — stays readable */
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
line-height: 1.35;
|
|
63
|
-
max-width: 1000px;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/* Footer: bold URL. No chips, no metric pills — those get destroyed by compression.
|
|
67
|
-
Just one big readable brand line. */
|
|
68
|
-
.footer {
|
|
69
|
-
margin-top: auto;
|
|
70
|
-
display: flex;
|
|
71
|
-
align-items: center;
|
|
72
|
-
justify-content: space-between;
|
|
73
|
-
padding-top: 32px;
|
|
74
|
-
border-top: 2px solid rgba(52, 211, 153, 0.25);
|
|
75
|
-
}
|
|
76
|
-
.url {
|
|
77
|
-
font-family: 'JetBrains Mono', monospace;
|
|
78
|
-
font-size: 32px;
|
|
79
|
-
color: #34d399;
|
|
80
|
-
font-weight: 700;
|
|
81
|
-
letter-spacing: -0.02em;
|
|
82
|
-
}
|
|
83
|
-
.tagline-pill {
|
|
84
|
-
font-size: 24px;
|
|
85
|
-
color: #ffffff;
|
|
86
|
-
font-weight: 700;
|
|
87
|
-
background: rgba(52, 211, 153, 0.15);
|
|
88
|
-
border: 2px solid rgba(52, 211, 153, 0.4);
|
|
89
|
-
padding: 10px 20px;
|
|
90
|
-
border-radius: 8px;
|
|
91
|
-
}
|
|
92
|
-
</style>
|
|
93
|
-
</head>
|
|
94
|
-
<body>
|
|
95
|
-
<div class="header">
|
|
96
|
-
<div class="logo-mark">
|
|
97
|
-
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
98
|
-
<path d="M12 2L12 6M12 18L12 22M4.93 4.93L7.76 7.76M16.24 16.24L19.07 19.07M2 12L6 12M18 12L22 12M4.93 19.07L7.76 16.24M16.24 7.76L19.07 4.93"
|
|
99
|
-
stroke="#042f1c" stroke-width="3" stroke-linecap="round"/>
|
|
100
|
-
<circle cx="12" cy="12" r="3" fill="#042f1c"/>
|
|
101
|
-
</svg>
|
|
102
|
-
</div>
|
|
103
|
-
<div class="brand">tokcalc</div>
|
|
104
|
-
<div class="brand-sub">LLM serving<br>capacity planner</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div class="hero">
|
|
108
|
-
Plan your LLM deployment<br>
|
|
109
|
-
<span class="accent">before you rent the GPUs.</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div class="tagline">
|
|
112
|
-
Open-source capacity planner: model fit, KV-cache, throughput, latency, multi-GPU scaling, and cost.
|
|
113
|
-
</div>
|
|
114
|
-
|
|
115
|
-
<div class="footer">
|
|
116
|
-
<div class="url">tokcalc.dev</div>
|
|
117
|
-
<div class="tagline-pill">Open source · 35 models · 30 GPUs</div>
|
|
118
|
-
</div>
|
|
119
|
-
</body>
|
|
120
|
-
</html>
|
package/scripts/render-og.mjs
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render tokcalc OG image via Playwright snapshot of an HTML template.
|
|
3
|
-
* Outputs:
|
|
4
|
-
* - /home/z/my-project/public/og.png (2400x1260 @ 2x scale, for all social platforms)
|
|
5
|
-
*
|
|
6
|
-
* The og-icon-256.png is generated separately if needed (see git history for the
|
|
7
|
-
* original render script with the icon step).
|
|
8
|
-
*
|
|
9
|
-
* Usage: node scripts/render-og.mjs
|
|
10
|
-
*/
|
|
11
|
-
import { chromium } from 'playwright';
|
|
12
|
-
import { fileURLToPath } from 'url';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
|
|
15
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
-
const TEMPLATE = path.join(__dirname, 'og-template.html');
|
|
17
|
-
const OUT_OG = path.join(__dirname, '..', 'public', 'og.png');
|
|
18
|
-
|
|
19
|
-
async function main() {
|
|
20
|
-
const browser = await chromium.launch();
|
|
21
|
-
const context = await browser.newContext({
|
|
22
|
-
viewport: { width: 1200, height: 630 },
|
|
23
|
-
deviceScaleFactor: 2, // 2x for crisp retina display
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const page = await context.newPage();
|
|
27
|
-
await page.goto('file://' + TEMPLATE, { waitUntil: 'networkidle' });
|
|
28
|
-
// Give web fonts a moment to settle (Inter / JetBrains Mono from Google Fonts CDN)
|
|
29
|
-
await page.waitForTimeout(1000);
|
|
30
|
-
await page.screenshot({
|
|
31
|
-
path: OUT_OG,
|
|
32
|
-
type: 'png',
|
|
33
|
-
clip: { x: 0, y: 0, width: 1200, height: 630 },
|
|
34
|
-
});
|
|
35
|
-
console.log('✓ OG image:', OUT_OG);
|
|
36
|
-
|
|
37
|
-
await browser.close();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
main().catch(err => {
|
|
41
|
-
console.error(err);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
});
|
package/server.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
-
"name": "io.github.stevecrates489-commits/tokcalc",
|
|
4
|
-
"title": "TokCalc MCP Server",
|
|
5
|
-
"description": "LLM capacity planning tools to estimate VRAM, compute, latency, and GPU topology for agents.",
|
|
6
|
-
"version": "0.1.3",
|
|
7
|
-
"repository": {
|
|
8
|
-
"url": "https://github.com/stevecrates489-commits/tokcalc.git",
|
|
9
|
-
"source": "github"
|
|
10
|
-
},
|
|
11
|
-
"packages": [
|
|
12
|
-
{
|
|
13
|
-
"registryType": "npm",
|
|
14
|
-
"identifier": "@tokcalc/mcp-server",
|
|
15
|
-
"version": "0.1.3",
|
|
16
|
-
"transport": {
|
|
17
|
-
"type": "stdio"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
}
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AWS EC2 pricing proxy — uses public bulk pricing files (NO IAM credentials required).
|
|
3
|
-
*
|
|
4
|
-
* Per the user's research: AWS publishes public pricing JSON files at:
|
|
5
|
-
* https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/us-east-1/index.json
|
|
6
|
-
*
|
|
7
|
-
* This file is large (~50-100MB) but contains all EC2 pricing for us-east-1.
|
|
8
|
-
* This serverless function:
|
|
9
|
-
* 1. Fetches the bulk file (with 8s timeout — might fail on slow connections)
|
|
10
|
-
* 2. Streams-parses to extract only GPU instance types (p5, p4d, g5, g6, etc.)
|
|
11
|
-
* 3. Caches in memory for 24 hours
|
|
12
|
-
* 4. Returns clean JSON with per-GPU pricing
|
|
13
|
-
*
|
|
14
|
-
* No IAM credentials, no API key, no payment setup — just public data.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { NextResponse } from "next/server";
|
|
18
|
-
|
|
19
|
-
// GPU instance type prefixes — filter the massive EC2 catalog
|
|
20
|
-
const GPU_INSTANCE_PREFIXES = ["p5", "p5e", "p6", "p4d", "p4de", "p3", "g5", "g6", "g4dn"];
|
|
21
|
-
|
|
22
|
-
// Known GPU counts per instance type
|
|
23
|
-
const GPU_COUNTS: Record<string, number> = {
|
|
24
|
-
"p5.48xlarge": 8, "p5e.48xlarge": 8, "p6.48xlarge": 8,
|
|
25
|
-
"p4d.24xlarge": 8, "p4de.24xlarge": 8,
|
|
26
|
-
"p3.2xlarge": 1, "p3.8xlarge": 4, "p3.16xlarge": 8,
|
|
27
|
-
"g5.xlarge": 1, "g5.2xlarge": 1, "g5.4xlarge": 1, "g5.8xlarge": 1, "g5.12xlarge": 4, "g5.16xlarge": 1, "g5.48xlarge": 8,
|
|
28
|
-
"g6.xlarge": 1, "g6.2xlarge": 1, "g6.4xlarge": 1, "g6.8xlarge": 1, "g6.12xlarge": 4, "g6.16xlarge": 1, "g6.48xlarge": 8,
|
|
29
|
-
"g4dn.xlarge": 1, "g4dn.2xlarge": 1, "g4dn.4xlarge": 1, "g4dn.8xlarge": 1, "g4dn.12xlarge": 4, "g4dn.16xlarge": 1,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Known GPU model per instance type
|
|
33
|
-
const GPU_MODEL: Record<string, string> = {
|
|
34
|
-
p5: "H100", p5e: "H200", p6: "B200",
|
|
35
|
-
p4d: "A100 40GB", p4de: "A100 80GB",
|
|
36
|
-
p3: "V100",
|
|
37
|
-
g5: "A10G", g6: "L4", g4dn: "T4",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
let cache: { data: CleanGpuPrice[]; timestamp: number } | null = null;
|
|
41
|
-
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
42
|
-
|
|
43
|
-
interface CleanGpuPrice {
|
|
44
|
-
gpu: string;
|
|
45
|
-
instanceType: string;
|
|
46
|
-
pricePerGpuHour: number;
|
|
47
|
-
instancePriceHour: number;
|
|
48
|
-
gpuCount: number;
|
|
49
|
-
region: string;
|
|
50
|
-
retrievedAt: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function getGpuModel(instanceType: string): string | null {
|
|
54
|
-
const prefix = Object.keys(GPU_MODEL).find((p) => instanceType.startsWith(p));
|
|
55
|
-
return prefix ? GPU_MODEL[prefix] : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function getGpuCount(instanceType: string): number {
|
|
59
|
-
return GPU_COUNTS[instanceType] || 1;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function GET() {
|
|
63
|
-
if (cache && Date.now() - cache.timestamp < CACHE_TTL_MS) {
|
|
64
|
-
return NextResponse.json({
|
|
65
|
-
source: "aws",
|
|
66
|
-
cached: true,
|
|
67
|
-
retrievedAt: new Date(cache.timestamp).toISOString(),
|
|
68
|
-
count: cache.data.length,
|
|
69
|
-
prices: cache.data,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
// Fetch the AWS EC2 bulk pricing file for us-east-1
|
|
75
|
-
// This is a large file (~50-100MB) but we only need GPU instances
|
|
76
|
-
const url = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/us-east-1/index.json";
|
|
77
|
-
|
|
78
|
-
const response = await fetch(url, {
|
|
79
|
-
signal: AbortSignal.timeout(8000), // 8 second timeout
|
|
80
|
-
headers: { "Accept": "application/json" },
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
if (!response.ok) {
|
|
84
|
-
throw new Error(`AWS pricing API returned ${response.status}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Parse the large JSON — this is memory-intensive but works on Vercel's 1GB limit
|
|
88
|
-
const data = await response.json() as {
|
|
89
|
-
products?: Record<string, {
|
|
90
|
-
productFamily?: string;
|
|
91
|
-
attributes?: {
|
|
92
|
-
instanceType?: string;
|
|
93
|
-
location?: string;
|
|
94
|
-
operatingSystem?: string;
|
|
95
|
-
tenancy?: string;
|
|
96
|
-
capacitystatus?: string;
|
|
97
|
-
};
|
|
98
|
-
}>;
|
|
99
|
-
terms?: {
|
|
100
|
-
OnDemand?: Record<string, {
|
|
101
|
-
priceDimensions?: Record<string, {
|
|
102
|
-
pricePerUnit?: { USD?: string };
|
|
103
|
-
}>;
|
|
104
|
-
}>;
|
|
105
|
-
};
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const products = data.products || {};
|
|
109
|
-
const onDemandTerms = data.terms?.OnDemand || {};
|
|
110
|
-
|
|
111
|
-
const prices: CleanGpuPrice[] = [];
|
|
112
|
-
|
|
113
|
-
// Iterate products and find GPU instance types
|
|
114
|
-
for (const [sku, product] of Object.entries(products)) {
|
|
115
|
-
const attrs = product.attributes || {};
|
|
116
|
-
const instanceType = attrs.instanceType || "";
|
|
117
|
-
|
|
118
|
-
// Skip non-GPU instances
|
|
119
|
-
if (!GPU_INSTANCE_PREFIXES.some((p) => instanceType.startsWith(p))) continue;
|
|
120
|
-
|
|
121
|
-
// Filter: Linux, Shared tenancy, Used capacity (on-demand)
|
|
122
|
-
if (attrs.operatingSystem !== "Linux") continue;
|
|
123
|
-
if (attrs.tenancy !== "Shared") continue;
|
|
124
|
-
if (attrs.capacitystatus !== "Used") continue;
|
|
125
|
-
|
|
126
|
-
// Find the OnDemand price for this SKU
|
|
127
|
-
const term = onDemandTerms[sku];
|
|
128
|
-
if (!term?.priceDimensions) continue;
|
|
129
|
-
|
|
130
|
-
const priceDims = Object.values(term.priceDimensions);
|
|
131
|
-
const usdPrice = priceDims[0]?.pricePerUnit?.USD;
|
|
132
|
-
if (!usdPrice) continue;
|
|
133
|
-
|
|
134
|
-
const instancePriceHour = parseFloat(usdPrice);
|
|
135
|
-
if (isNaN(instancePriceHour) || instancePriceHour <= 0) continue;
|
|
136
|
-
|
|
137
|
-
const gpuModel = getGpuModel(instanceType);
|
|
138
|
-
const gpuCount = getGpuCount(instanceType);
|
|
139
|
-
if (!gpuModel) continue;
|
|
140
|
-
|
|
141
|
-
const pricePerGpuHour = Math.round((instancePriceHour / gpuCount) * 100) / 100;
|
|
142
|
-
|
|
143
|
-
prices.push({
|
|
144
|
-
gpu: gpuModel,
|
|
145
|
-
instanceType,
|
|
146
|
-
pricePerGpuHour,
|
|
147
|
-
instancePriceHour: Math.round(instancePriceHour * 100) / 100,
|
|
148
|
-
gpuCount,
|
|
149
|
-
region: "us-east-1",
|
|
150
|
-
retrievedAt: new Date().toISOString(),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Deduplicate: keep cheapest per-GPU price
|
|
155
|
-
const seen = new Map<string, CleanGpuPrice>();
|
|
156
|
-
for (const p of prices) {
|
|
157
|
-
const key = p.gpu;
|
|
158
|
-
if (!seen.has(key) || seen.get(key)!.pricePerGpuHour > p.pricePerGpuHour) {
|
|
159
|
-
seen.set(key, p);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const deduped = Array.from(seen.values()).sort((a, b) => a.pricePerGpuHour - b.pricePerGpuHour);
|
|
164
|
-
|
|
165
|
-
cache = { data: deduped, timestamp: Date.now() };
|
|
166
|
-
|
|
167
|
-
return NextResponse.json({
|
|
168
|
-
source: "aws",
|
|
169
|
-
cached: false,
|
|
170
|
-
retrievedAt: new Date().toISOString(),
|
|
171
|
-
count: deduped.length,
|
|
172
|
-
prices: deduped,
|
|
173
|
-
note: "On-demand Linux pricing from AWS public bulk files. No IAM credentials required. Region: us-east-1.",
|
|
174
|
-
});
|
|
175
|
-
} catch (error) {
|
|
176
|
-
console.error("AWS pricing fetch error:", error);
|
|
177
|
-
return NextResponse.json(
|
|
178
|
-
{
|
|
179
|
-
source: "aws",
|
|
180
|
-
error: "Failed to fetch AWS bulk pricing. The file is large (~50MB) and may have timed out. Cached results from a previous fetch may still be available.",
|
|
181
|
-
fallback: "Use static pricing from tokcalc's GPU catalog or Azure/Vast.ai live pricing.",
|
|
182
|
-
},
|
|
183
|
-
{ status: 502 },
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Azure Retail Prices API proxy.
|
|
3
|
-
*
|
|
4
|
-
* Fetches live GPU pricing from Azure's public, unauthenticated API:
|
|
5
|
-
* GET https://prices.azure.com/api/retail/prices
|
|
6
|
-
*
|
|
7
|
-
* This serverless function:
|
|
8
|
-
* 1. Fetches server-side (avoids CORS issues)
|
|
9
|
-
* 2. Caches in memory for 24 hours (reduces API load)
|
|
10
|
-
* 3. Returns a clean JSON response with per-GPU pricing
|
|
11
|
-
*
|
|
12
|
-
* Usage from client:
|
|
13
|
-
* fetch('/api/pricing/azure?XTransformPort=3000')
|
|
14
|
-
* or (since this is the same app):
|
|
15
|
-
* fetch('/api/pricing/azure')
|
|
16
|
-
*
|
|
17
|
-
* Per the fullstack skill rules, if requesting to a different port,
|
|
18
|
-
* use XTransformPort query param. But this API is on the same port
|
|
19
|
-
* so no special handling needed.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
23
|
-
|
|
24
|
-
interface AzurePriceItem {
|
|
25
|
-
currencyCode: string;
|
|
26
|
-
tierMinimumUnits: number;
|
|
27
|
-
reservationPrice?: number;
|
|
28
|
-
retailPrice: number;
|
|
29
|
-
unitPrice: number;
|
|
30
|
-
armRegionName: string;
|
|
31
|
-
location: string;
|
|
32
|
-
armSkuName: string;
|
|
33
|
-
productName: string;
|
|
34
|
-
skuName: string;
|
|
35
|
-
serviceName: string;
|
|
36
|
-
serviceFamily: string;
|
|
37
|
-
unit: string;
|
|
38
|
-
meters: Array<{
|
|
39
|
-
meterId: string;
|
|
40
|
-
meterName: string;
|
|
41
|
-
unit: string;
|
|
42
|
-
unitPrice: number;
|
|
43
|
-
}>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// In-memory cache (lives for the lifetime of the serverless function instance)
|
|
47
|
-
let cache: {
|
|
48
|
-
data: Array<{ gpu: string; sku: string; price: number; region: string; retrievedAt: string }>;
|
|
49
|
-
timestamp: number;
|
|
50
|
-
} | null = null;
|
|
51
|
-
|
|
52
|
-
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
53
|
-
|
|
54
|
-
// GPU instance types to query (H100, H200, A100, L40S, T4, L4, V100)
|
|
55
|
-
const AZURE_GPU_SKUS = [
|
|
56
|
-
"Standard_ND96isr_H100_v5", // 8× H100 80GB
|
|
57
|
-
"Standard_NDm_A100_v4", // 8× A100 80GB (NDm)
|
|
58
|
-
"Standard_ND96asr_v4", // 8× A100 80GB (NDasr)
|
|
59
|
-
"Standard_NC24ads_A100_v4", // 1× A100 80GB
|
|
60
|
-
"Standard_NC4as_T4_v3", // 1× T4
|
|
61
|
-
"Standard_ND96amsr_A100_v4", // 4× A100 80GB
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export async function GET(_request: NextRequest) {
|
|
65
|
-
// Return cached data if fresh
|
|
66
|
-
if (cache && Date.now() - cache.timestamp < CACHE_TTL_MS) {
|
|
67
|
-
return NextResponse.json({
|
|
68
|
-
source: "azure",
|
|
69
|
-
cached: true,
|
|
70
|
-
retrievedAt: new Date(cache.timestamp).toISOString(),
|
|
71
|
-
prices: cache.data,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
try {
|
|
76
|
-
// Fetch from Azure's public, unauthenticated API
|
|
77
|
-
const prices: Array<{ gpu: string; sku: string; price: number; region: string; retrievedAt: string }> = [];
|
|
78
|
-
|
|
79
|
-
for (const sku of AZURE_GPU_SKUS) {
|
|
80
|
-
const url = `https://prices.azure.com/api/retail/prices?$filter=armSkuName eq '${sku}' and priceType eq 'Consumption'`;
|
|
81
|
-
const response = await fetch(url, {
|
|
82
|
-
headers: { "Accept": "application/json" },
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (!response.ok) continue;
|
|
86
|
-
|
|
87
|
-
const data = await response.json() as { Items: AzurePriceItem[]; NextPageLink?: string };
|
|
88
|
-
const items = data.Items || [];
|
|
89
|
-
|
|
90
|
-
for (const item of items) {
|
|
91
|
-
// Extract GPU name from product name
|
|
92
|
-
const productName = item.productName || "";
|
|
93
|
-
let gpuName = "unknown";
|
|
94
|
-
let gpuCount = 1;
|
|
95
|
-
|
|
96
|
-
if (productName.includes("H100")) { gpuName = "H100"; gpuCount = 8; }
|
|
97
|
-
else if (productName.includes("H200")) { gpuName = "H200"; gpuCount = 8; }
|
|
98
|
-
else if (productName.includes("A100") && productName.includes("80")) { gpuName = "A100 80GB"; gpuCount = sku.includes("96") ? 8 : (sku.includes("24") ? 1 : 4); }
|
|
99
|
-
else if (productName.includes("A100")) { gpuName = "A100 40GB"; gpuCount = sku.includes("96") ? 8 : 1; }
|
|
100
|
-
else if (productName.includes("T4")) { gpuName = "T4"; gpuCount = 1; }
|
|
101
|
-
else if (productName.includes("L40")) { gpuName = "L40S"; gpuCount = 8; }
|
|
102
|
-
|
|
103
|
-
if (gpuName === "unknown") continue;
|
|
104
|
-
|
|
105
|
-
// Skip spot/reserved — only on-demand for now
|
|
106
|
-
if (item.skuName?.toLowerCase().includes("spot")) continue;
|
|
107
|
-
if (item.skuName?.toLowerCase().includes("low priority")) continue;
|
|
108
|
-
|
|
109
|
-
const instancePricePerHour = item.retailPrice;
|
|
110
|
-
const derivedGpuPrice = instancePricePerHour / gpuCount;
|
|
111
|
-
|
|
112
|
-
prices.push({
|
|
113
|
-
gpu: gpuName,
|
|
114
|
-
sku: item.armSkuName,
|
|
115
|
-
price: Math.round(derivedGpuPrice * 100) / 100, // per-GPU $/hr, 2 decimals
|
|
116
|
-
region: item.armRegionName,
|
|
117
|
-
retrievedAt: new Date().toISOString(),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Follow pagination if needed (Azure API returns max 100 items per page)
|
|
122
|
-
let nextLink = data.NextPageLink;
|
|
123
|
-
while (nextLink) {
|
|
124
|
-
const nextResponse = await fetch(nextLink);
|
|
125
|
-
if (!nextResponse.ok) break;
|
|
126
|
-
const nextData = await nextResponse.json() as { Items: AzurePriceItem[]; NextPageLink?: string };
|
|
127
|
-
// Process same as above (simplified — just get first page for now)
|
|
128
|
-
nextLink = nextData.NextPageLink;
|
|
129
|
-
break; // Limit to 2 pages to avoid timeout
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Deduplicate: keep cheapest per GPU per region
|
|
134
|
-
const seen = new Map<string, { gpu: string; sku: string; price: number; region: string; retrievedAt: string }>();
|
|
135
|
-
for (const p of prices) {
|
|
136
|
-
const key = `${p.gpu}-${p.region}`;
|
|
137
|
-
if (!seen.has(key) || seen.get(key)!.price > p.price) {
|
|
138
|
-
seen.set(key, p);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const deduped = Array.from(seen.values());
|
|
143
|
-
|
|
144
|
-
// Update cache
|
|
145
|
-
cache = {
|
|
146
|
-
data: deduped,
|
|
147
|
-
timestamp: Date.now(),
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
return NextResponse.json({
|
|
151
|
-
source: "azure",
|
|
152
|
-
cached: false,
|
|
153
|
-
retrievedAt: new Date().toISOString(),
|
|
154
|
-
count: deduped.length,
|
|
155
|
-
prices: deduped,
|
|
156
|
-
});
|
|
157
|
-
} catch (error) {
|
|
158
|
-
console.error("Azure pricing fetch error:", error);
|
|
159
|
-
return NextResponse.json(
|
|
160
|
-
{
|
|
161
|
-
source: "azure",
|
|
162
|
-
error: "Failed to fetch Azure pricing. The Azure Retail Prices API may be temporarily unavailable.",
|
|
163
|
-
fallback: "Use static pricing from tokcalc's GPU catalog.",
|
|
164
|
-
},
|
|
165
|
-
{ status: 502 },
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
}
|