@tokcalc/mcp-server 0.1.3 → 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.
- package/README.md +86 -406
- package/dist/index.js +21285 -0
- package/package.json +32 -90
- package/.zscripts/build.sh +0 -175
- package/.zscripts/database-runtime-build.sh +0 -33
- package/.zscripts/dev.pid +0 -1
- package/.zscripts/dev.sh +0 -154
- package/.zscripts/mini-services-build.sh +0 -78
- package/.zscripts/mini-services-install.sh +0 -65
- package/.zscripts/mini-services-start.sh +0 -123
- package/.zscripts/python-runtime-build.sh +0 -120
- package/.zscripts/start.sh +0 -145
- package/CAPACITY_STUDY.md +0 -283
- package/CODE_OF_CONDUCT.md +0 -55
- package/CONTRIBUTING.md +0 -177
- package/Caddyfile +0 -23
- package/LICENSE +0 -204
- package/bun.lock +0 -1965
- package/components.json +0 -21
- package/db/custom.db +0 -0
- package/download/README.md +0 -1
- package/download/tokcalc-dark-calculator.png +0 -0
- package/download/tokcalc-dark-default.png +0 -0
- package/download/tokcalc-demo.webm +0 -0
- package/download/tokcalc-github-link.png +0 -0
- package/download/tokcalc-hydration-fixed.png +0 -0
- package/download/tokcalc-issue-resolved.png +0 -0
- package/download/tokcalc-light-mode.png +0 -0
- package/download/tokcalc-light-reference.png +0 -0
- package/download/tokcalc-long-context-qwen.png +0 -0
- package/download/tokcalc-long-context.png +0 -0
- package/download/tokcalc-og-image-preview.png +0 -0
- package/download/tokcalc-phase2-3.png +0 -0
- package/download/tokcalc-plain-english.png +0 -0
- package/download/tokcalc-preview.png +0 -0
- package/download/tokcalc-share-bvb.png +0 -0
- package/download/tokcalc-share-feature.png +0 -0
- package/download/tokcalc-tab-build-vs-buy.png +0 -0
- package/download/tokcalc-tab-calculator.png +0 -0
- package/download/tokcalc-tab-reference.png +0 -0
- package/eslint.config.mjs +0 -50
- package/examples/websocket/frontend.tsx +0 -196
- package/examples/websocket/server.ts +0 -138
- package/mini-services/.gitkeep +0 -0
- package/mini-services/mcp-server/README.md +0 -86
- package/mini-services/mcp-server/bun.lock +0 -202
- package/mini-services/mcp-server/index.ts +0 -504
- package/mini-services/mcp-server/package.json +0 -40
- package/next.config.ts +0 -12
- package/postcss.config.mjs +0 -5
- package/prisma/schema.prisma +0 -32
- package/public/google6f58ca6be85fa903.html +0 -1
- package/public/logo.svg +0 -29
- package/public/manifest.json +0 -51
- package/public/og-icon-256.png +0 -0
- package/public/og.png +0 -0
- package/public/robots.txt +0 -25
- package/public/sitemap.xml +0 -23
- package/public/tokcalc-demo.gif +0 -0
- package/scripts/og-template.html +0 -120
- package/scripts/render-og.mjs +0 -43
- package/server.json +0 -21
- package/src/app/api/pricing/aws/route.ts +0 -186
- package/src/app/api/pricing/azure/route.ts +0 -168
- package/src/app/api/pricing/gcp/route.ts +0 -230
- package/src/app/api/pricing/vast-ai/route.ts +0 -164
- package/src/app/api/route.ts +0 -5
- package/src/app/compare/h100-vs-h200/layout.tsx +0 -30
- package/src/app/compare/h100-vs-h200/page.tsx +0 -328
- package/src/app/globals.css +0 -122
- package/src/app/layout.tsx +0 -276
- package/src/app/page.tsx +0 -2670
- package/src/components/azure-live-pricing.tsx +0 -185
- package/src/components/benchmark-import.tsx +0 -340
- package/src/components/confidence-badge.tsx +0 -116
- package/src/components/live-pricing-comparison.tsx +0 -241
- package/src/components/theme-provider.tsx +0 -11
- package/src/components/theme-toggle.tsx +0 -55
- package/src/components/ui/accordion.tsx +0 -66
- package/src/components/ui/alert-dialog.tsx +0 -157
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/aspect-ratio.tsx +0 -11
- package/src/components/ui/avatar.tsx +0 -53
- package/src/components/ui/badge.tsx +0 -46
- package/src/components/ui/breadcrumb.tsx +0 -109
- package/src/components/ui/button.tsx +0 -59
- package/src/components/ui/calendar.tsx +0 -213
- package/src/components/ui/card.tsx +0 -92
- package/src/components/ui/carousel.tsx +0 -241
- package/src/components/ui/chart.tsx +0 -353
- package/src/components/ui/checkbox.tsx +0 -32
- package/src/components/ui/collapsible.tsx +0 -33
- package/src/components/ui/command.tsx +0 -184
- package/src/components/ui/context-menu.tsx +0 -252
- package/src/components/ui/dialog.tsx +0 -143
- package/src/components/ui/drawer.tsx +0 -135
- package/src/components/ui/dropdown-menu.tsx +0 -257
- package/src/components/ui/form.tsx +0 -167
- package/src/components/ui/hover-card.tsx +0 -44
- package/src/components/ui/input-otp.tsx +0 -77
- package/src/components/ui/input.tsx +0 -21
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/menubar.tsx +0 -276
- package/src/components/ui/navigation-menu.tsx +0 -168
- package/src/components/ui/pagination.tsx +0 -127
- package/src/components/ui/popover.tsx +0 -48
- package/src/components/ui/progress.tsx +0 -31
- package/src/components/ui/radio-group.tsx +0 -45
- package/src/components/ui/resizable.tsx +0 -56
- package/src/components/ui/scroll-area.tsx +0 -58
- package/src/components/ui/select.tsx +0 -185
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/sheet.tsx +0 -139
- package/src/components/ui/sidebar.tsx +0 -726
- package/src/components/ui/skeleton.tsx +0 -13
- package/src/components/ui/slider.tsx +0 -63
- package/src/components/ui/sonner.tsx +0 -25
- package/src/components/ui/switch.tsx +0 -31
- package/src/components/ui/table.tsx +0 -116
- package/src/components/ui/tabs.tsx +0 -66
- package/src/components/ui/textarea.tsx +0 -18
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/toggle-group.tsx +0 -73
- package/src/components/ui/toggle.tsx +0 -47
- package/src/components/ui/tooltip.tsx +0 -61
- package/src/components/vast-ai-live-pricing.tsx +0 -176
- package/src/hooks/use-mobile.ts +0 -19
- package/src/hooks/use-toast.ts +0 -194
- package/src/lib/benchmark-parser-sglang.ts +0 -150
- package/src/lib/benchmark-parser-tokcalc.ts +0 -247
- package/src/lib/benchmark-parser-trtllm.ts +0 -152
- package/src/lib/benchmark-parser-vllm.ts +0 -198
- package/src/lib/benchmark-schema.ts +0 -263
- package/src/lib/db.ts +0 -13
- package/src/lib/engine-presets.ts +0 -183
- package/src/lib/price-schema.ts +0 -141
- package/src/lib/token-calc.ts +0 -808
- package/src/lib/track.ts +0 -31
- package/src/lib/url-state.ts +0 -256
- package/src/lib/utils.ts +0 -6
- package/tailwind.config.ts +0 -64
- package/tests/database-runtime-build.sh +0 -75
- package/tests/python-runtime-build.sh +0 -64
- package/tests/python-runtime-container.sh +0 -31
- package/tool-results/bash_1789888171144_2c5381860539.txt +0 -161
- package/tool-results/bash_1789888175925_49c53ba3c61b.txt +0 -191
- package/tool-results/bash_1789888181202_49c53ba3c61b.txt +0 -191
- package/tool-results/bash_1789888195219_4a86a5c91411.txt +0 -200
- package/tool-results/bash_1789888203128_6cca13c71b47.txt +0 -199
- package/tool-results/bash_1789929256963_2a52aff0d0a8.txt +0 -160
- package/tool-results/read_1789888151021_69f58eec6a5b.txt +0 -653
- package/tool-results/read_1789888153837_1d3a8bfc2a94.txt +0 -653
- package/tool-results/read_1789888163087_ccc406d47505.txt +0 -122
- package/tool-results/read_1789888167347_67d1d7c9830a.txt +0 -122
- package/tool-results/read_1789929252529_d90e8f383a25.txt +0 -285
- package/tsconfig.json +0 -42
- package/upload/Pasted Content_1789887800864.txt +0 -652
- package/upload/Pasted Content_1789887909561.txt +0 -652
- package/upload/Pasted Content_1789887918428.txt +0 -652
- package/upload/Pasted Content_1789887959420.txt +0 -652
- package/upload/Pasted Content_1789888020485.txt +0 -652
- package/upload/Pasted Content_1789888058079.txt +0 -652
- package/upload/Pasted Content_1789888885033.txt +0 -686
- package/upload/Pasted Content_1789928912741.txt +0 -285
- package/upload/Pasted Content_1789928938402.txt +0 -285
- package/upload/Pasted Content_1789929160389.txt +0 -285
- package/upload/Pasted Content_1789929176660.txt +0 -285
- package/upload/issue_vision.json +0 -28
- package/upload/pasted_image_1789883175209.png +0 -0
- package/upload/pasted_image_1789899056690.png +0 -0
- package/upload/pasted_image_1789900371483.png +0 -0
- package/upload/pasted_image_1789900472823.png +0 -0
- package/upload/pasted_image_1789900490374.png +0 -0
- package/upload/pasted_image_1789900585552.png +0 -0
- package/upload/pasted_image_1789900606519.png +0 -0
- package/upload/pasted_image_1789901598705.png +0 -0
- package/upload/pasted_image_1789901613545.png +0 -0
- package/upload/pasted_image_1789978382674.png +0 -0
- package/upload/pasted_image_1789978392749.png +0 -0
- package/upload/pasted_image_1789978474879.png +0 -0
- package/upload/pasted_image_1789978523652.png +0 -0
- package/upload/pasted_image_1789984219089.png +0 -0
- package/upload/pasted_image_1789984491896.png +0 -0
- package/upload/pasted_image_1789985017950.png +0 -0
- package/upload/pasted_image_1789985036765.png +0 -0
- package/upload/pasted_image_1789985049848.png +0 -0
- package/upload/pasted_image_1790002427833.png +0 -0
- package/upload/pasted_image_1790002659944.png +0 -0
- package/upload/pasted_image_1790037038476.png +0 -0
- package/upload/screenshot_analysis.json +0 -28
- package/upload/vision_output.json +0 -28
|
@@ -1,230 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
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
|
-
}
|
package/src/app/api/route.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
}
|