@tokcalc/mcp-server 0.1.2 → 0.1.4

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