@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.
Files changed (193) hide show
  1. package/README.md +86 -406
  2. package/dist/http.js +23504 -0
  3. package/dist/index.js +21290 -0
  4. package/package.json +36 -92
  5. package/.zscripts/build.sh +0 -175
  6. package/.zscripts/database-runtime-build.sh +0 -33
  7. package/.zscripts/dev.pid +0 -1
  8. package/.zscripts/dev.sh +0 -154
  9. package/.zscripts/mini-services-build.sh +0 -78
  10. package/.zscripts/mini-services-install.sh +0 -65
  11. package/.zscripts/mini-services-start.sh +0 -123
  12. package/.zscripts/python-runtime-build.sh +0 -120
  13. package/.zscripts/start.sh +0 -145
  14. package/CAPACITY_STUDY.md +0 -283
  15. package/CODE_OF_CONDUCT.md +0 -55
  16. package/CONTRIBUTING.md +0 -177
  17. package/Caddyfile +0 -23
  18. package/LICENSE +0 -204
  19. package/bun.lock +0 -1965
  20. package/components.json +0 -21
  21. package/db/custom.db +0 -0
  22. package/download/README.md +0 -1
  23. package/download/tokcalc-dark-calculator.png +0 -0
  24. package/download/tokcalc-dark-default.png +0 -0
  25. package/download/tokcalc-demo.webm +0 -0
  26. package/download/tokcalc-github-link.png +0 -0
  27. package/download/tokcalc-hydration-fixed.png +0 -0
  28. package/download/tokcalc-issue-resolved.png +0 -0
  29. package/download/tokcalc-light-mode.png +0 -0
  30. package/download/tokcalc-light-reference.png +0 -0
  31. package/download/tokcalc-long-context-qwen.png +0 -0
  32. package/download/tokcalc-long-context.png +0 -0
  33. package/download/tokcalc-og-image-preview.png +0 -0
  34. package/download/tokcalc-phase2-3.png +0 -0
  35. package/download/tokcalc-plain-english.png +0 -0
  36. package/download/tokcalc-preview.png +0 -0
  37. package/download/tokcalc-share-bvb.png +0 -0
  38. package/download/tokcalc-share-feature.png +0 -0
  39. package/download/tokcalc-tab-build-vs-buy.png +0 -0
  40. package/download/tokcalc-tab-calculator.png +0 -0
  41. package/download/tokcalc-tab-reference.png +0 -0
  42. package/eslint.config.mjs +0 -50
  43. package/examples/websocket/frontend.tsx +0 -196
  44. package/examples/websocket/server.ts +0 -138
  45. package/mini-services/.gitkeep +0 -0
  46. package/mini-services/mcp-server/README.md +0 -86
  47. package/mini-services/mcp-server/bun.lock +0 -202
  48. package/mini-services/mcp-server/index.ts +0 -504
  49. package/mini-services/mcp-server/package.json +0 -40
  50. package/next.config.ts +0 -12
  51. package/postcss.config.mjs +0 -5
  52. package/prisma/schema.prisma +0 -32
  53. package/public/google6f58ca6be85fa903.html +0 -1
  54. package/public/logo.svg +0 -29
  55. package/public/manifest.json +0 -51
  56. package/public/og-icon-256.png +0 -0
  57. package/public/og.png +0 -0
  58. package/public/robots.txt +0 -25
  59. package/public/sitemap.xml +0 -23
  60. package/public/tokcalc-demo.gif +0 -0
  61. package/scripts/og-template.html +0 -120
  62. package/scripts/render-og.mjs +0 -43
  63. package/server.json +0 -21
  64. package/src/app/api/pricing/aws/route.ts +0 -186
  65. package/src/app/api/pricing/azure/route.ts +0 -168
  66. package/src/app/api/pricing/gcp/route.ts +0 -230
  67. package/src/app/api/pricing/vast-ai/route.ts +0 -164
  68. package/src/app/api/route.ts +0 -5
  69. package/src/app/compare/h100-vs-h200/layout.tsx +0 -30
  70. package/src/app/compare/h100-vs-h200/page.tsx +0 -328
  71. package/src/app/globals.css +0 -122
  72. package/src/app/layout.tsx +0 -276
  73. package/src/app/page.tsx +0 -2670
  74. package/src/components/azure-live-pricing.tsx +0 -185
  75. package/src/components/benchmark-import.tsx +0 -340
  76. package/src/components/confidence-badge.tsx +0 -116
  77. package/src/components/live-pricing-comparison.tsx +0 -241
  78. package/src/components/theme-provider.tsx +0 -11
  79. package/src/components/theme-toggle.tsx +0 -55
  80. package/src/components/ui/accordion.tsx +0 -66
  81. package/src/components/ui/alert-dialog.tsx +0 -157
  82. package/src/components/ui/alert.tsx +0 -66
  83. package/src/components/ui/aspect-ratio.tsx +0 -11
  84. package/src/components/ui/avatar.tsx +0 -53
  85. package/src/components/ui/badge.tsx +0 -46
  86. package/src/components/ui/breadcrumb.tsx +0 -109
  87. package/src/components/ui/button.tsx +0 -59
  88. package/src/components/ui/calendar.tsx +0 -213
  89. package/src/components/ui/card.tsx +0 -92
  90. package/src/components/ui/carousel.tsx +0 -241
  91. package/src/components/ui/chart.tsx +0 -353
  92. package/src/components/ui/checkbox.tsx +0 -32
  93. package/src/components/ui/collapsible.tsx +0 -33
  94. package/src/components/ui/command.tsx +0 -184
  95. package/src/components/ui/context-menu.tsx +0 -252
  96. package/src/components/ui/dialog.tsx +0 -143
  97. package/src/components/ui/drawer.tsx +0 -135
  98. package/src/components/ui/dropdown-menu.tsx +0 -257
  99. package/src/components/ui/form.tsx +0 -167
  100. package/src/components/ui/hover-card.tsx +0 -44
  101. package/src/components/ui/input-otp.tsx +0 -77
  102. package/src/components/ui/input.tsx +0 -21
  103. package/src/components/ui/label.tsx +0 -24
  104. package/src/components/ui/menubar.tsx +0 -276
  105. package/src/components/ui/navigation-menu.tsx +0 -168
  106. package/src/components/ui/pagination.tsx +0 -127
  107. package/src/components/ui/popover.tsx +0 -48
  108. package/src/components/ui/progress.tsx +0 -31
  109. package/src/components/ui/radio-group.tsx +0 -45
  110. package/src/components/ui/resizable.tsx +0 -56
  111. package/src/components/ui/scroll-area.tsx +0 -58
  112. package/src/components/ui/select.tsx +0 -185
  113. package/src/components/ui/separator.tsx +0 -28
  114. package/src/components/ui/sheet.tsx +0 -139
  115. package/src/components/ui/sidebar.tsx +0 -726
  116. package/src/components/ui/skeleton.tsx +0 -13
  117. package/src/components/ui/slider.tsx +0 -63
  118. package/src/components/ui/sonner.tsx +0 -25
  119. package/src/components/ui/switch.tsx +0 -31
  120. package/src/components/ui/table.tsx +0 -116
  121. package/src/components/ui/tabs.tsx +0 -66
  122. package/src/components/ui/textarea.tsx +0 -18
  123. package/src/components/ui/toast.tsx +0 -129
  124. package/src/components/ui/toaster.tsx +0 -35
  125. package/src/components/ui/toggle-group.tsx +0 -73
  126. package/src/components/ui/toggle.tsx +0 -47
  127. package/src/components/ui/tooltip.tsx +0 -61
  128. package/src/components/vast-ai-live-pricing.tsx +0 -176
  129. package/src/hooks/use-mobile.ts +0 -19
  130. package/src/hooks/use-toast.ts +0 -194
  131. package/src/lib/benchmark-parser-sglang.ts +0 -150
  132. package/src/lib/benchmark-parser-tokcalc.ts +0 -247
  133. package/src/lib/benchmark-parser-trtllm.ts +0 -152
  134. package/src/lib/benchmark-parser-vllm.ts +0 -198
  135. package/src/lib/benchmark-schema.ts +0 -263
  136. package/src/lib/db.ts +0 -13
  137. package/src/lib/engine-presets.ts +0 -183
  138. package/src/lib/price-schema.ts +0 -141
  139. package/src/lib/token-calc.ts +0 -808
  140. package/src/lib/track.ts +0 -31
  141. package/src/lib/url-state.ts +0 -256
  142. package/src/lib/utils.ts +0 -6
  143. package/tailwind.config.ts +0 -64
  144. package/tests/database-runtime-build.sh +0 -75
  145. package/tests/python-runtime-build.sh +0 -64
  146. package/tests/python-runtime-container.sh +0 -31
  147. package/tool-results/bash_1789888171144_2c5381860539.txt +0 -161
  148. package/tool-results/bash_1789888175925_49c53ba3c61b.txt +0 -191
  149. package/tool-results/bash_1789888181202_49c53ba3c61b.txt +0 -191
  150. package/tool-results/bash_1789888195219_4a86a5c91411.txt +0 -200
  151. package/tool-results/bash_1789888203128_6cca13c71b47.txt +0 -199
  152. package/tool-results/bash_1789929256963_2a52aff0d0a8.txt +0 -160
  153. package/tool-results/read_1789888151021_69f58eec6a5b.txt +0 -653
  154. package/tool-results/read_1789888153837_1d3a8bfc2a94.txt +0 -653
  155. package/tool-results/read_1789888163087_ccc406d47505.txt +0 -122
  156. package/tool-results/read_1789888167347_67d1d7c9830a.txt +0 -122
  157. package/tool-results/read_1789929252529_d90e8f383a25.txt +0 -285
  158. package/tsconfig.json +0 -42
  159. package/upload/Pasted Content_1789887800864.txt +0 -652
  160. package/upload/Pasted Content_1789887909561.txt +0 -652
  161. package/upload/Pasted Content_1789887918428.txt +0 -652
  162. package/upload/Pasted Content_1789887959420.txt +0 -652
  163. package/upload/Pasted Content_1789888020485.txt +0 -652
  164. package/upload/Pasted Content_1789888058079.txt +0 -652
  165. package/upload/Pasted Content_1789888885033.txt +0 -686
  166. package/upload/Pasted Content_1789928912741.txt +0 -285
  167. package/upload/Pasted Content_1789928938402.txt +0 -285
  168. package/upload/Pasted Content_1789929160389.txt +0 -285
  169. package/upload/Pasted Content_1789929176660.txt +0 -285
  170. package/upload/issue_vision.json +0 -28
  171. package/upload/pasted_image_1789883175209.png +0 -0
  172. package/upload/pasted_image_1789899056690.png +0 -0
  173. package/upload/pasted_image_1789900371483.png +0 -0
  174. package/upload/pasted_image_1789900472823.png +0 -0
  175. package/upload/pasted_image_1789900490374.png +0 -0
  176. package/upload/pasted_image_1789900585552.png +0 -0
  177. package/upload/pasted_image_1789900606519.png +0 -0
  178. package/upload/pasted_image_1789901598705.png +0 -0
  179. package/upload/pasted_image_1789901613545.png +0 -0
  180. package/upload/pasted_image_1789978382674.png +0 -0
  181. package/upload/pasted_image_1789978392749.png +0 -0
  182. package/upload/pasted_image_1789978474879.png +0 -0
  183. package/upload/pasted_image_1789978523652.png +0 -0
  184. package/upload/pasted_image_1789984219089.png +0 -0
  185. package/upload/pasted_image_1789984491896.png +0 -0
  186. package/upload/pasted_image_1789985017950.png +0 -0
  187. package/upload/pasted_image_1789985036765.png +0 -0
  188. package/upload/pasted_image_1789985049848.png +0 -0
  189. package/upload/pasted_image_1790002427833.png +0 -0
  190. package/upload/pasted_image_1790002659944.png +0 -0
  191. package/upload/pasted_image_1790037038476.png +0 -0
  192. package/upload/screenshot_analysis.json +0 -28
  193. 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
@@ -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>
Binary file
@@ -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>
@@ -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
- }