@tokcalc/mcp-server 0.1.1 → 0.1.3

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/.zscripts/build.sh +175 -0
  2. package/.zscripts/database-runtime-build.sh +33 -0
  3. package/.zscripts/dev.pid +1 -0
  4. package/.zscripts/dev.sh +154 -0
  5. package/.zscripts/mini-services-build.sh +78 -0
  6. package/.zscripts/mini-services-install.sh +65 -0
  7. package/.zscripts/mini-services-start.sh +123 -0
  8. package/.zscripts/python-runtime-build.sh +120 -0
  9. package/.zscripts/start.sh +145 -0
  10. package/CAPACITY_STUDY.md +283 -0
  11. package/CODE_OF_CONDUCT.md +55 -0
  12. package/CONTRIBUTING.md +177 -0
  13. package/Caddyfile +23 -0
  14. package/LICENSE +204 -0
  15. package/README.md +406 -86
  16. package/bun.lock +1965 -0
  17. package/components.json +21 -0
  18. package/db/custom.db +0 -0
  19. package/download/README.md +1 -0
  20. package/download/tokcalc-dark-calculator.png +0 -0
  21. package/download/tokcalc-dark-default.png +0 -0
  22. package/download/tokcalc-demo.webm +0 -0
  23. package/download/tokcalc-github-link.png +0 -0
  24. package/download/tokcalc-hydration-fixed.png +0 -0
  25. package/download/tokcalc-issue-resolved.png +0 -0
  26. package/download/tokcalc-light-mode.png +0 -0
  27. package/download/tokcalc-light-reference.png +0 -0
  28. package/download/tokcalc-long-context-qwen.png +0 -0
  29. package/download/tokcalc-long-context.png +0 -0
  30. package/download/tokcalc-og-image-preview.png +0 -0
  31. package/download/tokcalc-phase2-3.png +0 -0
  32. package/download/tokcalc-plain-english.png +0 -0
  33. package/download/tokcalc-preview.png +0 -0
  34. package/download/tokcalc-share-bvb.png +0 -0
  35. package/download/tokcalc-share-feature.png +0 -0
  36. package/download/tokcalc-tab-build-vs-buy.png +0 -0
  37. package/download/tokcalc-tab-calculator.png +0 -0
  38. package/download/tokcalc-tab-reference.png +0 -0
  39. package/eslint.config.mjs +50 -0
  40. package/examples/websocket/frontend.tsx +196 -0
  41. package/examples/websocket/server.ts +138 -0
  42. package/mini-services/.gitkeep +0 -0
  43. package/mini-services/mcp-server/README.md +86 -0
  44. package/mini-services/mcp-server/bun.lock +202 -0
  45. package/mini-services/mcp-server/index.ts +504 -0
  46. package/mini-services/mcp-server/package.json +40 -0
  47. package/next.config.ts +12 -0
  48. package/package.json +92 -35
  49. package/postcss.config.mjs +5 -0
  50. package/prisma/schema.prisma +32 -0
  51. package/public/google6f58ca6be85fa903.html +1 -0
  52. package/public/logo.svg +29 -0
  53. package/public/manifest.json +51 -0
  54. package/public/og-icon-256.png +0 -0
  55. package/public/og.png +0 -0
  56. package/public/robots.txt +25 -0
  57. package/public/sitemap.xml +23 -0
  58. package/public/tokcalc-demo.gif +0 -0
  59. package/scripts/og-template.html +120 -0
  60. package/scripts/render-og.mjs +43 -0
  61. package/server.json +21 -0
  62. package/src/app/api/pricing/aws/route.ts +186 -0
  63. package/src/app/api/pricing/azure/route.ts +168 -0
  64. package/src/app/api/pricing/gcp/route.ts +230 -0
  65. package/src/app/api/pricing/vast-ai/route.ts +164 -0
  66. package/src/app/api/route.ts +5 -0
  67. package/src/app/compare/h100-vs-h200/layout.tsx +30 -0
  68. package/src/app/compare/h100-vs-h200/page.tsx +328 -0
  69. package/src/app/globals.css +122 -0
  70. package/src/app/layout.tsx +276 -0
  71. package/src/app/page.tsx +2670 -0
  72. package/src/components/azure-live-pricing.tsx +185 -0
  73. package/src/components/benchmark-import.tsx +340 -0
  74. package/src/components/confidence-badge.tsx +116 -0
  75. package/src/components/live-pricing-comparison.tsx +241 -0
  76. package/src/components/theme-provider.tsx +11 -0
  77. package/src/components/theme-toggle.tsx +55 -0
  78. package/src/components/ui/accordion.tsx +66 -0
  79. package/src/components/ui/alert-dialog.tsx +157 -0
  80. package/src/components/ui/alert.tsx +66 -0
  81. package/src/components/ui/aspect-ratio.tsx +11 -0
  82. package/src/components/ui/avatar.tsx +53 -0
  83. package/src/components/ui/badge.tsx +46 -0
  84. package/src/components/ui/breadcrumb.tsx +109 -0
  85. package/src/components/ui/button.tsx +59 -0
  86. package/src/components/ui/calendar.tsx +213 -0
  87. package/src/components/ui/card.tsx +92 -0
  88. package/src/components/ui/carousel.tsx +241 -0
  89. package/src/components/ui/chart.tsx +353 -0
  90. package/src/components/ui/checkbox.tsx +32 -0
  91. package/src/components/ui/collapsible.tsx +33 -0
  92. package/src/components/ui/command.tsx +184 -0
  93. package/src/components/ui/context-menu.tsx +252 -0
  94. package/src/components/ui/dialog.tsx +143 -0
  95. package/src/components/ui/drawer.tsx +135 -0
  96. package/src/components/ui/dropdown-menu.tsx +257 -0
  97. package/src/components/ui/form.tsx +167 -0
  98. package/src/components/ui/hover-card.tsx +44 -0
  99. package/src/components/ui/input-otp.tsx +77 -0
  100. package/src/components/ui/input.tsx +21 -0
  101. package/src/components/ui/label.tsx +24 -0
  102. package/src/components/ui/menubar.tsx +276 -0
  103. package/src/components/ui/navigation-menu.tsx +168 -0
  104. package/src/components/ui/pagination.tsx +127 -0
  105. package/src/components/ui/popover.tsx +48 -0
  106. package/src/components/ui/progress.tsx +31 -0
  107. package/src/components/ui/radio-group.tsx +45 -0
  108. package/src/components/ui/resizable.tsx +56 -0
  109. package/src/components/ui/scroll-area.tsx +58 -0
  110. package/src/components/ui/select.tsx +185 -0
  111. package/src/components/ui/separator.tsx +28 -0
  112. package/src/components/ui/sheet.tsx +139 -0
  113. package/src/components/ui/sidebar.tsx +726 -0
  114. package/src/components/ui/skeleton.tsx +13 -0
  115. package/src/components/ui/slider.tsx +63 -0
  116. package/src/components/ui/sonner.tsx +25 -0
  117. package/src/components/ui/switch.tsx +31 -0
  118. package/src/components/ui/table.tsx +116 -0
  119. package/src/components/ui/tabs.tsx +66 -0
  120. package/src/components/ui/textarea.tsx +18 -0
  121. package/src/components/ui/toast.tsx +129 -0
  122. package/src/components/ui/toaster.tsx +35 -0
  123. package/src/components/ui/toggle-group.tsx +73 -0
  124. package/src/components/ui/toggle.tsx +47 -0
  125. package/src/components/ui/tooltip.tsx +61 -0
  126. package/src/components/vast-ai-live-pricing.tsx +176 -0
  127. package/src/hooks/use-mobile.ts +19 -0
  128. package/src/hooks/use-toast.ts +194 -0
  129. package/src/lib/benchmark-parser-sglang.ts +150 -0
  130. package/src/lib/benchmark-parser-tokcalc.ts +247 -0
  131. package/src/lib/benchmark-parser-trtllm.ts +152 -0
  132. package/src/lib/benchmark-parser-vllm.ts +198 -0
  133. package/src/lib/benchmark-schema.ts +263 -0
  134. package/src/lib/db.ts +13 -0
  135. package/src/lib/engine-presets.ts +183 -0
  136. package/src/lib/price-schema.ts +141 -0
  137. package/src/lib/token-calc.ts +808 -0
  138. package/src/lib/track.ts +31 -0
  139. package/src/lib/url-state.ts +256 -0
  140. package/src/lib/utils.ts +6 -0
  141. package/tailwind.config.ts +64 -0
  142. package/tests/database-runtime-build.sh +75 -0
  143. package/tests/python-runtime-build.sh +64 -0
  144. package/tests/python-runtime-container.sh +31 -0
  145. package/tool-results/bash_1789888171144_2c5381860539.txt +161 -0
  146. package/tool-results/bash_1789888175925_49c53ba3c61b.txt +191 -0
  147. package/tool-results/bash_1789888181202_49c53ba3c61b.txt +191 -0
  148. package/tool-results/bash_1789888195219_4a86a5c91411.txt +200 -0
  149. package/tool-results/bash_1789888203128_6cca13c71b47.txt +199 -0
  150. package/tool-results/bash_1789929256963_2a52aff0d0a8.txt +160 -0
  151. package/tool-results/read_1789888151021_69f58eec6a5b.txt +653 -0
  152. package/tool-results/read_1789888153837_1d3a8bfc2a94.txt +653 -0
  153. package/tool-results/read_1789888163087_ccc406d47505.txt +122 -0
  154. package/tool-results/read_1789888167347_67d1d7c9830a.txt +122 -0
  155. package/tool-results/read_1789929252529_d90e8f383a25.txt +285 -0
  156. package/tsconfig.json +42 -0
  157. package/upload/Pasted Content_1789887800864.txt +652 -0
  158. package/upload/Pasted Content_1789887909561.txt +652 -0
  159. package/upload/Pasted Content_1789887918428.txt +652 -0
  160. package/upload/Pasted Content_1789887959420.txt +652 -0
  161. package/upload/Pasted Content_1789888020485.txt +652 -0
  162. package/upload/Pasted Content_1789888058079.txt +652 -0
  163. package/upload/Pasted Content_1789888885033.txt +686 -0
  164. package/upload/Pasted Content_1789928912741.txt +285 -0
  165. package/upload/Pasted Content_1789928938402.txt +285 -0
  166. package/upload/Pasted Content_1789929160389.txt +285 -0
  167. package/upload/Pasted Content_1789929176660.txt +285 -0
  168. package/upload/issue_vision.json +28 -0
  169. package/upload/pasted_image_1789883175209.png +0 -0
  170. package/upload/pasted_image_1789899056690.png +0 -0
  171. package/upload/pasted_image_1789900371483.png +0 -0
  172. package/upload/pasted_image_1789900472823.png +0 -0
  173. package/upload/pasted_image_1789900490374.png +0 -0
  174. package/upload/pasted_image_1789900585552.png +0 -0
  175. package/upload/pasted_image_1789900606519.png +0 -0
  176. package/upload/pasted_image_1789901598705.png +0 -0
  177. package/upload/pasted_image_1789901613545.png +0 -0
  178. package/upload/pasted_image_1789978382674.png +0 -0
  179. package/upload/pasted_image_1789978392749.png +0 -0
  180. package/upload/pasted_image_1789978474879.png +0 -0
  181. package/upload/pasted_image_1789978523652.png +0 -0
  182. package/upload/pasted_image_1789984219089.png +0 -0
  183. package/upload/pasted_image_1789984491896.png +0 -0
  184. package/upload/pasted_image_1789985017950.png +0 -0
  185. package/upload/pasted_image_1789985036765.png +0 -0
  186. package/upload/pasted_image_1789985049848.png +0 -0
  187. package/upload/pasted_image_1790002427833.png +0 -0
  188. package/upload/pasted_image_1790002659944.png +0 -0
  189. package/upload/pasted_image_1790037038476.png +0 -0
  190. package/upload/screenshot_analysis.json +28 -0
  191. package/upload/vision_output.json +28 -0
  192. package/dist/index.js +0 -21014
@@ -0,0 +1,168 @@
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
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Google Cloud GPU pricing proxy.
3
+ *
4
+ * Uses the Google Cloud Billing Catalog API:
5
+ * GET https://cloudbilling.googleapis.com/v1/services/{SERVICE_ID}/skus?key=API_KEY
6
+ *
7
+ * Requires GCP_API_KEY environment variable.
8
+ * Set on Vercel: Settings → Environment Variables → GCP_API_KEY
9
+ *
10
+ * This serverless function:
11
+ * 1. Fetches Compute Engine SKUs from the Cloud Billing Catalog API
12
+ * 2. Filters for GPU instance types (a3, a4 — H100, H200, B200)
13
+ * 3. Extracts on-demand pricing
14
+ * 4. Caches in memory for 24 hours
15
+ *
16
+ * The API key is stored as an env var — NOT hardcoded in source code.
17
+ * This is important because tokcalc is open source on GitHub.
18
+ */
19
+
20
+ import { NextResponse } from "next/server";
21
+
22
+ // GCP GPU instance types to look for in SKU descriptions
23
+ const GPU_PATTERNS = [
24
+ { pattern: "H100", gpu: "H100" },
25
+ { pattern: "H200", gpu: "H200" },
26
+ { pattern: "B200", gpu: "B200" },
27
+ { pattern: "A100", gpu: "A100" },
28
+ { pattern: "L4", gpu: "L4" },
29
+ { pattern: "T4", gpu: "T4" },
30
+ { pattern: "L40", gpu: "L40S" },
31
+ { pattern: "V100", gpu: "V100" },
32
+ ];
33
+
34
+ // GCP instance type → GPU count mapping
35
+ const GCP_GPU_COUNTS: Record<string, number> = {
36
+ "a3-highgpu-8g": 8, "a3-highgpu-2g": 2,
37
+ "a3-edgegpu-8g": 8, "a3-edgegpu-2g": 2,
38
+ "a4-highgpu-8g": 8,
39
+ "a2-highgpu-1g": 1, "a2-highgpu-2g": 2, "a2-highgpu-4g": 4, "a2-highgpu-8g": 8,
40
+ "n1-standard-4": 1, // T4 usually 1 per instance
41
+ "g2-standard-4": 1, "g2-standard-8": 1, "g2-standard-12": 1, "g2-standard-16": 1, "g2-standard-24": 4, "g2-standard-32": 1, "g2-standard-48": 8,
42
+ };
43
+
44
+ let cache: { data: CleanGcpPrice[]; timestamp: number } | null = null;
45
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
46
+
47
+ interface CleanGcpPrice {
48
+ gpu: string;
49
+ instanceType: string;
50
+ pricePerGpuHour: number;
51
+ instancePriceHour: number;
52
+ gpuCount: number;
53
+ region: string;
54
+ retrievedAt: string;
55
+ }
56
+
57
+ export async function GET() {
58
+ const apiKey = process.env.GCP_API_KEY;
59
+
60
+ if (!apiKey) {
61
+ return NextResponse.json(
62
+ {
63
+ source: "gcp",
64
+ error: "GCP_API_KEY environment variable is not set. Add it on Vercel: Settings → Environment Variables → GCP_API_KEY",
65
+ fallback: "Use Azure or Vast.ai live pricing instead.",
66
+ },
67
+ { status: 503 },
68
+ );
69
+ }
70
+
71
+ if (cache && Date.now() - cache.timestamp < CACHE_TTL_MS) {
72
+ return NextResponse.json({
73
+ source: "gcp",
74
+ cached: true,
75
+ retrievedAt: new Date(cache.timestamp).toISOString(),
76
+ count: cache.data.length,
77
+ prices: cache.data,
78
+ });
79
+ }
80
+
81
+ try {
82
+ // Step 1: Get the list of services to find Compute Engine's service ID
83
+ const servicesUrl = `https://cloudbilling.googleapis.com/v1/services?key=${apiKey}&pageSize=100`;
84
+ const servicesResponse = await fetch(servicesUrl, {
85
+ signal: AbortSignal.timeout(10000),
86
+ });
87
+
88
+ if (!servicesResponse.ok) {
89
+ throw new Error(`GCP Services API returned ${servicesResponse.status}`);
90
+ }
91
+
92
+ const servicesData = await servicesResponse.json() as {
93
+ services?: Array<{ serviceId: string; displayName: string }>;
94
+ };
95
+
96
+ // Find Compute Engine service
97
+ const computeService = servicesData.services?.find(
98
+ (s) => s.displayName?.toLowerCase().includes("compute engine"),
99
+ );
100
+
101
+ if (!computeService) {
102
+ throw new Error("Compute Engine service not found in GCP billing catalog");
103
+ }
104
+
105
+ // Step 2: Fetch SKUs for Compute Engine
106
+ const skusUrl = `https://cloudbilling.googleapis.com/v1/services/${computeService.serviceId}/skus?key=${apiKey}&pageSize=5000`;
107
+ const skusResponse = await fetch(skusUrl, {
108
+ signal: AbortSignal.timeout(15000),
109
+ });
110
+
111
+ if (!skusResponse.ok) {
112
+ throw new Error(`GCP SKUs API returned ${skusResponse.status}`);
113
+ }
114
+
115
+ const skusData = await skusResponse.json() as {
116
+ skus?: Array<{
117
+ skuId: string;
118
+ description: string;
119
+ pricingInfo?: Array<{
120
+ pricingExpression?: {
121
+ tieredRates?: Array<{
122
+ unitPrice?: { units?: string; nanos?: number };
123
+ }>;
124
+ };
125
+ }>;
126
+ serviceRegions?: string[];
127
+ geoTaxonomy?: { regions?: string[] };
128
+ }>;
129
+ nextPagePageToken?: string;
130
+ };
131
+
132
+ const skus = skusData.skus || [];
133
+
134
+ const prices: CleanGcpPrice[] = [];
135
+
136
+ for (const sku of skus) {
137
+ const description = sku.description || "";
138
+
139
+ // Check if this SKU matches any GPU pattern
140
+ const gpuMatch = GPU_PATTERNS.find((p) => description.includes(p.pattern));
141
+ if (!gpuMatch) continue;
142
+
143
+ // Skip preemptible/spot/reserved
144
+ if (description.toLowerCase().includes("preemptible")) continue;
145
+ if (description.toLowerCase().includes("spot")) continue;
146
+ if (description.toLowerCase().includes("reserved")) continue;
147
+ if (description.toLowerCase().includes("commitment")) continue;
148
+
149
+ // Skip non-GPU SKUs that happen to mention GPU in text
150
+ if (!description.toLowerCase().includes("gpu") && !description.toLowerCase().includes("premium")) {
151
+ // Only include if the instance type is in our known GPU instances
152
+ const instanceMatch = Object.keys(GCP_GPU_COUNTS).find(
153
+ (it) => description.toLowerCase().includes(it.toLowerCase()),
154
+ );
155
+ if (!instanceMatch) continue;
156
+ }
157
+
158
+ // Extract price
159
+ const pricingInfo = sku.pricingInfo?.[0];
160
+ const tieredRates = pricingInfo?.pricingExpression?.tieredRates;
161
+ const firstRate = tieredRates?.[0];
162
+ const unitPrice = firstRate?.unitPrice;
163
+
164
+ if (!unitPrice) continue;
165
+
166
+ // GCP prices are in USD with nanos precision
167
+ const dollars = parseFloat(unitPrice.units || "0");
168
+ const nanos = (unitPrice.nanos || 0) / 1e9;
169
+ const pricePerHour = dollars + nanos;
170
+
171
+ if (isNaN(pricePerHour) || pricePerHour <= 0) continue;
172
+
173
+ // Try to determine instance type and GPU count
174
+ let instanceType = "unknown";
175
+ let gpuCount = 1;
176
+
177
+ for (const [it, count] of Object.entries(GCP_GPU_COUNTS)) {
178
+ if (description.toLowerCase().includes(it.toLowerCase())) {
179
+ instanceType = it;
180
+ gpuCount = count;
181
+ break;
182
+ }
183
+ }
184
+
185
+ const pricePerGpuHour = Math.round((pricePerHour / gpuCount) * 100) / 100;
186
+
187
+ prices.push({
188
+ gpu: gpuMatch.gpu,
189
+ instanceType,
190
+ pricePerGpuHour,
191
+ instancePriceHour: Math.round(pricePerHour * 100) / 100,
192
+ gpuCount,
193
+ region: sku.serviceRegions?.[0] || sku.geoTaxonomy?.regions?.[0] || "global",
194
+ retrievedAt: new Date().toISOString(),
195
+ });
196
+ }
197
+
198
+ // Deduplicate: keep cheapest per-GPU price per GPU model
199
+ const seen = new Map<string, CleanGcpPrice>();
200
+ for (const p of prices) {
201
+ const key = p.gpu;
202
+ if (!seen.has(key) || seen.get(key)!.pricePerGpuHour > p.pricePerGpuHour) {
203
+ seen.set(key, p);
204
+ }
205
+ }
206
+
207
+ const deduped = Array.from(seen.values()).sort((a, b) => a.pricePerGpuHour - b.pricePerGpuHour);
208
+
209
+ cache = { data: deduped, timestamp: Date.now() };
210
+
211
+ return NextResponse.json({
212
+ source: "gcp",
213
+ cached: false,
214
+ retrievedAt: new Date().toISOString(),
215
+ count: deduped.length,
216
+ prices: deduped,
217
+ note: "On-demand pricing from Google Cloud Billing Catalog API. Region varies by SKU. Requires GCP_API_KEY env var.",
218
+ });
219
+ } catch (error) {
220
+ console.error("GCP pricing fetch error:", error);
221
+ return NextResponse.json(
222
+ {
223
+ source: "gcp",
224
+ error: "Failed to fetch GCP pricing. Check that GCP_API_KEY is set and valid.",
225
+ fallback: "Use Azure or Vast.ai live pricing instead.",
226
+ },
227
+ { status: 502 },
228
+ );
229
+ }
230
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Vast.ai marketplace pricing proxy.
3
+ *
4
+ * Fetches live spot-market GPU offers from Vast.ai's public API:
5
+ * GET https://cloud.vast.ai/api/v0/bundles/
6
+ *
7
+ * This serverless function:
8
+ * 1. Fetches server-side (avoids CORS issues)
9
+ * 2. Filters for GPU-only offers
10
+ * 3. Deduplicates by GPU model (keeps cheapest available)
11
+ * 4. Caches in memory for 5 minutes (spot prices change rapidly)
12
+ *
13
+ * Per Perplexity research (Prompt #3):
14
+ * "Vast.ai is the best fit for dynamic spot/marketplace pricing"
15
+ * "Official offer-search REST API / OpenAPI"
16
+ * "Treat every returned offer as temporary market data"
17
+ */
18
+
19
+ import { NextRequest, NextResponse } from "next/server";
20
+
21
+ interface VastAiBundle {
22
+ id: number;
23
+ gpu_name?: string;
24
+ num_gpus?: number;
25
+ gpu_ram?: number;
26
+ dph_total?: number; // total $/hour
27
+ reliability?: number;
28
+ geolocation?: string;
29
+ inetname?: string;
30
+ dlperf?: number; // download performance
31
+ cuda_max_good?: number; // CUDA version
32
+ total_flops?: number;
33
+ cpu_ram?: number;
34
+ cpu_cores?: number;
35
+ }
36
+
37
+ interface CleanOffer {
38
+ gpu: string;
39
+ gpuCount: number;
40
+ vramGb: number;
41
+ pricePerGpuHour: number;
42
+ totalPriceHour: number;
43
+ reliability: number;
44
+ region: string;
45
+ bundleId: number;
46
+ }
47
+
48
+ // In-memory cache (5 minute TTL — spot prices change rapidly)
49
+ let cache: { data: CleanOffer[]; timestamp: number } | null = null;
50
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
51
+
52
+ // GPUs we care about — use short names for flexible matching
53
+ // (e.g., "H100" matches both "H100 SXM" and "H100 NVL")
54
+ const TARGET_GPUS = [
55
+ "H100", "H200", "B200",
56
+ "A100", "A6000",
57
+ "RTX 4090", "RTX 3090", "RTX 4080", "RTX 3080",
58
+ "RTX 5090", "RTX 5080", "RTX 5070",
59
+ "L40", "L4",
60
+ "V100",
61
+ "T4",
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: "vast_ai",
69
+ cached: true,
70
+ retrievedAt: new Date(cache.timestamp).toISOString(),
71
+ count: cache.data.length,
72
+ offers: cache.data,
73
+ note: "Spot-market prices. Updated every 5 minutes. Individual offers may be gone by the time you try to rent.",
74
+ });
75
+ }
76
+
77
+ try {
78
+ // Vast.ai API: search for GPU offers, sorted by cheapest first
79
+ // The q parameter filters by GPU name, order sorts by price ascending
80
+ const url = "https://cloud.vast.ai/api/v0/bundles/";
81
+ const response = await fetch(url, {
82
+ headers: { "Accept": "application/json" },
83
+ // Timeout after 10 seconds
84
+ signal: AbortSignal.timeout(10000),
85
+ });
86
+
87
+ if (!response.ok) {
88
+ throw new Error(`Vast.ai API returned ${response.status}`);
89
+ }
90
+
91
+ const data = await response.json() as { offers?: VastAiBundle[] };
92
+ const bundles = data.offers || [];
93
+
94
+ // Filter and clean offers
95
+ const offers: CleanOffer[] = [];
96
+
97
+ for (const bundle of bundles) {
98
+ if (!bundle.gpu_name || !bundle.dph_total || bundle.dph_total <= 0) continue;
99
+ if (!bundle.num_gpus || bundle.num_gpus <= 0) continue;
100
+
101
+ const gpuName = bundle.gpu_name.trim();
102
+ const gpuCount = bundle.num_gpus;
103
+ const totalPriceHour = bundle.dph_total;
104
+ const pricePerGpuHour = Math.round((totalPriceHour / gpuCount) * 100) / 100;
105
+
106
+ // Check if this GPU is in our target list (bidirectional matching)
107
+ const isTarget = TARGET_GPUS.some(
108
+ (target) =>
109
+ gpuName.toLowerCase().includes(target.toLowerCase()) ||
110
+ target.toLowerCase().includes(gpuName.toLowerCase()),
111
+ );
112
+ if (!isTarget) continue;
113
+
114
+ offers.push({
115
+ gpu: gpuName,
116
+ gpuCount,
117
+ vramGb: bundle.gpu_ram || 0,
118
+ pricePerGpuHour,
119
+ totalPriceHour: Math.round(totalPriceHour * 100) / 100,
120
+ reliability: bundle.reliability || 0,
121
+ region: bundle.geolocation || "unknown",
122
+ bundleId: bundle.id,
123
+ });
124
+ }
125
+
126
+ // Deduplicate: keep cheapest per-GPU price for each GPU model
127
+ const cheapestPerGpu = new Map<string, CleanOffer>();
128
+ for (const offer of offers) {
129
+ const key = offer.gpu;
130
+ if (!cheapestPerGpu.has(key) || cheapestPerGpu.get(key)!.pricePerGpuHour > offer.pricePerGpuHour) {
131
+ cheapestPerGpu.set(key, offer);
132
+ }
133
+ }
134
+
135
+ const deduped = Array.from(cheapestPerGpu.values()).sort(
136
+ (a, b) => a.pricePerGpuHour - b.pricePerGpuHour,
137
+ );
138
+
139
+ // Update cache
140
+ cache = {
141
+ data: deduped,
142
+ timestamp: Date.now(),
143
+ };
144
+
145
+ return NextResponse.json({
146
+ source: "vast_ai",
147
+ cached: false,
148
+ retrievedAt: new Date().toISOString(),
149
+ count: deduped.length,
150
+ offers: deduped,
151
+ note: "Spot-market prices from Vast.ai marketplace. Individual offers may be gone by the time you try to rent. Reliability score indicates host trustworthiness (0-1).",
152
+ });
153
+ } catch (error) {
154
+ console.error("Vast.ai pricing fetch error:", error);
155
+ return NextResponse.json(
156
+ {
157
+ source: "vast_ai",
158
+ error: "Failed to fetch Vast.ai marketplace pricing. The API may be temporarily unavailable or rate-limited.",
159
+ fallback: "Use static pricing from tokcalc's GPU catalog.",
160
+ },
161
+ { status: 502 },
162
+ );
163
+ }
164
+ }
@@ -0,0 +1,5 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ export async function GET() {
4
+ return NextResponse.json({ message: "Hello, world!" });
5
+ }
@@ -0,0 +1,30 @@
1
+ import type { Metadata } from "next";
2
+
3
+ export const metadata: Metadata = {
4
+ title: "H100 vs H200 for LLM Inference: Throughput, Latency, VRAM & Cost",
5
+ description:
6
+ "Side-by-side comparison of NVIDIA H100 SXM5 80GB vs H200 SXM5 141GB for Llama 3.3 70B FP8 serving. Interactive throughput, latency, VRAM, and cost calculator with transparent formulas.",
7
+ keywords: [
8
+ "H100 vs H200",
9
+ "H100 H200 comparison",
10
+ "LLM inference GPU comparison",
11
+ "H100 LLM throughput",
12
+ "H200 LLM throughput",
13
+ "H100 vs H200 VRAM",
14
+ "Llama 70B GPU comparison",
15
+ "NVIDIA H100 H200",
16
+ ],
17
+ openGraph: {
18
+ title: "H100 vs H200 for LLM Inference — Throughput, VRAM & Cost",
19
+ description:
20
+ "Interactive comparison of H100 80GB vs H200 141GB for Llama 3.3 70B serving. See how context length affects capacity.",
21
+ type: "article",
22
+ },
23
+ alternates: {
24
+ canonical: "https://tokcalc.vercel.app/compare/h100-vs-h200",
25
+ },
26
+ };
27
+
28
+ export default function Page({ children }: { children: React.ReactNode }) {
29
+ return children;
30
+ }