From 960050c6233e9d467832bbb11f879d26204a704c Mon Sep 17 00:00:00 2001 From: Nathan Scott Date: Fri, 4 Sep 2026 18:46:34 +1000 Subject: [PATCH] refactor: remove legacy livePricing fallback from AdvancedEstimate and RoutingEconomics Now that aicostings.dev is integrated, remove: - livePricing state and fetch effects from both components - getCloudRate/getOwnedRate functions and gpu-rates.ts module - livePricing type definition from GpuSpec - livePricing response field from API responses Both AdvancedEstimate and RoutingEconomics now use aicostings cloud rates via resolveCloudRate(), with hardware amortization as final fallback. Co-Authored-By: Claude Haiku 4.5 Signed-off-by: Nathan Scott --- app/api/gpus/route.ts | 63 +----------- app/performance/PerformanceEstimate.tsx | 40 +------- app/recommend/AdvancedEstimate.tsx | 30 +----- app/routing/RoutingEconomics.tsx | 33 +------ lib/api/cloudflare.ts | 125 ------------------------ lib/api/responses.ts | 2 - lib/gpu-math/gpus.ts | 19 ---- lib/pricing/gpu-rates.ts | 20 ---- 8 files changed, 10 insertions(+), 322 deletions(-) delete mode 100644 lib/api/cloudflare.ts delete mode 100644 lib/pricing/gpu-rates.ts diff --git a/app/api/gpus/route.ts b/app/api/gpus/route.ts index d61c2fa..ea6a389 100644 --- a/app/api/gpus/route.ts +++ b/app/api/gpus/route.ts @@ -7,7 +7,6 @@ import { GpuCatalogQuerySchema } from '@/lib/api/schemas' import { ApiErrors } from '@/lib/api/errors' import { formatGpuCatalogResponse } from '@/lib/api/responses' import type { GpuSpec } from '@/lib/gpu-math/gpus' -import { fetchGPUPricing, aggregateGPUPricing } from '@/lib/api/cloudflare' // AIConfigurator /systems response schema interface AicSystem { @@ -36,7 +35,6 @@ export async function GET(req: NextRequest) { const maxPrice = searchParams.get('max_price') const vendor = searchParams.get('vendor') const sort = searchParams.get('sort') - const includeLivePricing = searchParams.get('live_pricing') === 'true' if (minMem) query.min_memory = minMem if (maxPrice) query.max_price = maxPrice @@ -96,66 +94,15 @@ export async function GET(req: NextRequest) { } as GpuSpec }) - // Optionally enrich with live pricing from Cloudflare Worker - if (includeLivePricing) { - try { - console.log('[GPUs API] Fetching live pricing from Cloudflare Worker...') - const cloudflareData = await fetchGPUPricing() - console.log(`[GPUs API] Received ${cloudflareData.prices.length} prices from Cloudflare`) - - // Enrich each GPU with live pricing - filteredGpus = filteredGpus.map(gpu => { - // Extract GPU model from name (e.g., "NVIDIA H100 SXM" โ†’ "H100") - const gpuModel = gpu.name.replace('NVIDIA ', '').split(' ')[0] - - // Find matching prices from Cloudflare - const matchingPrices = cloudflareData.prices.filter(p => - p.gpu === gpuModel && - p.vram_gb === gpu.vramGb - ) - - console.log(`[GPUs API] ${gpu.name}: Found ${matchingPrices.length} matching prices`) - - if (matchingPrices.length === 0) { - return gpu - } - - // Aggregate on-demand and spot pricing - const onDemandPricing = aggregateGPUPricing(matchingPrices, 'on_demand') - const spotPricing = aggregateGPUPricing(matchingPrices, 'spot') - - console.log(`[GPUs API] ${gpu.name}: on-demand count=${onDemandPricing.count}, spot count=${spotPricing.count}`) - - // Update pricePerHour with live pricing if available - const livePrice = onDemandPricing.median ?? spotPricing.median ?? gpu.pricePerHour - - return { - ...gpu, - pricePerHour: livePrice, - livePricing: { - onDemand: onDemandPricing.count > 0 ? onDemandPricing : undefined, - spot: spotPricing.count > 0 ? spotPricing : undefined, - lastUpdated: cloudflareData.timestamp - } - } - }) - } catch (cloudflareError) { - // Log error but continue with default pricing - console.error('[GPUs API] Failed to fetch live pricing from Cloudflare:', cloudflareError) - } - } // Filter by min_memory if (validatedQuery.min_memory) { filteredGpus = filteredGpus.filter(gpu => gpu.vramGb >= validatedQuery.min_memory!) } - // Filter by max_price (use live pricing median if available, fallback to static) + // Filter by max_price if (validatedQuery.max_price) { - filteredGpus = filteredGpus.filter(gpu => { - const effectivePrice = gpu.livePricing?.onDemand?.median ?? gpu.pricePerHour - return effectivePrice <= validatedQuery.max_price! - }) + filteredGpus = filteredGpus.filter(gpu => gpu.pricePerHour <= validatedQuery.max_price!) } // Filter by vendor @@ -172,11 +119,7 @@ export async function GET(req: NextRequest) { filteredGpus.sort((a, b) => b.vramGb - a.vramGb) break case 'price': - filteredGpus.sort((a, b) => { - const priceA = a.livePricing?.onDemand?.median ?? a.pricePerHour - const priceB = b.livePricing?.onDemand?.median ?? b.pricePerHour - return priceA - priceB - }) + filteredGpus.sort((a, b) => a.pricePerHour - b.pricePerHour) break case 'performance': filteredGpus.sort((a, b) => b.tflops - a.tflops) diff --git a/app/performance/PerformanceEstimate.tsx b/app/performance/PerformanceEstimate.tsx index 3a2329a..b83f303 100644 --- a/app/performance/PerformanceEstimate.tsx +++ b/app/performance/PerformanceEstimate.tsx @@ -230,10 +230,6 @@ export default function QuickEstimate() { const resetToDefaults = () => applyPreset(DEFAULT_WORKLOAD); - // Live pricing from Cloudflare Worker - const [livePricing, setLivePricing] = React.useState>({}); - - // Add loading state const [isCalculating, setIsCalculating] = React.useState(false); // Fetch HF config when model changes @@ -351,34 +347,6 @@ export default function QuickEstimate() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [calcTrigger]); - // Fetch live pricing from Cloudflare Worker - React.useEffect(() => { - const fetchPricing = async () => { - try { - const response = await fetch('/api/gpus?live_pricing=true'); - const data = await response.json(); - - if (data.status === 'success' && data.data?.gpus) { - const pricing: Record = {}; - data.data.gpus.forEach((gpu: any) => { - if (gpu.live_pricing?.onDemand?.median) { - pricing[gpu.name] = gpu.live_pricing.onDemand.median; - } - }); - setLivePricing(pricing); - console.log('โœ… Loaded live pricing for', Object.keys(pricing).length, 'GPUs'); - console.log('๐Ÿ“Š Live pricing data:', pricing); - } - } catch (error) { - console.error('Failed to fetch live pricing:', error); - } - }; - - fetchPricing(); - const REFRESH_MS = 5 * 60 * 1000; // refresh live pricing every 5 minutes - const interval = setInterval(fetchPricing, REFRESH_MS); - return () => clearInterval(interval); - }, []); // Check if user has seen the tour before React.useEffect(() => { @@ -505,15 +473,11 @@ export default function QuickEstimate() { gpuLabel.includes('L40S') ? 'L40S' : gpuLabel.includes('MI300X') ? 'MI300X' : gpuLabel; - // Resolve a cloud $/hr from the costings API (preferred provider โ†’ cheapest - // on-demand โ†’ cheapest spot), falling back to the legacy live-pricing worker. const resolvedCloudRate = resolveCloudRate(costings.gpuCloudRates.get(gpu), preferredCloudProvider) - const workerRate = livePricing[gpuPricingKey] ?? null - const gpuPricePerHour: number | null = resolvedCloudRate?.rate ?? workerRate; - // Honest label for where the rate came from โ€” never assume a provider. + const gpuPricePerHour: number | null = resolvedCloudRate?.rate ?? null; const cloudRateLabel = resolvedCloudRate ? `${resolvedCloudRate.provider.replace('.', ' ยท ')} ${resolvedCloudRate.kind === 'spot' ? 'spot' : 'on-demand'}` - : (workerRate != null ? 'live rate' : ''); + : ''; const realMonthlyCost = testResult && gpuPricePerHour != null ? realGpuCount * gpuPricePerHour * HOURS_PER_MONTH : diff --git a/app/recommend/AdvancedEstimate.tsx b/app/recommend/AdvancedEstimate.tsx index 57e19f1..f801ada 100644 --- a/app/recommend/AdvancedEstimate.tsx +++ b/app/recommend/AdvancedEstimate.tsx @@ -171,9 +171,6 @@ export default function AdvancedEstimate() { // Additional constraints accordion const [expanded, setExpanded] = React.useState(['perf']); - // Live pricing - const [livePricing, setLivePricing] = React.useState>({}); - const [islInput, setIslInput] = React.useState('2048'); const [oslInput, setOslInput] = React.useState('128'); const [ttftInput, setTtftInput] = React.useState('1000'); @@ -255,25 +252,6 @@ export default function AdvancedEstimate() { }, [model, hfToken, MODEL_OPTIONS, catalogLoading, hydrated]); // Fetch live pricing - React.useEffect(() => { - const fetchPricing = async () => { - try { - const res = await fetch('/api/gpus?live_pricing=true'); - if (!res.ok) return; - const data = await res.json(); - if (!data?.data?.gpus) return; - const prices: Record = {}; - for (const g of data.data.gpus) { - if (g.live_pricing?.onDemand?.median) { - const shortName = g.name.replace(/NVIDIA\s+/i, '').replace(/AMD\s+/i, '').split(' ')[0]; - prices[shortName] = g.live_pricing.onDemand.median; - } - } - setLivePricing(prices); - } catch { /* ignore */ } - }; - fetchPricing(); - }, []); const currentGpuOption = aicGpus.find(g => g.systemId === gpuSystem) ?? aicGpus[0] ?? null; @@ -311,18 +289,12 @@ export default function AdvancedEstimate() { const tpsVal = useCountUp(result?.throughput.tokensPerSecond ?? 0, 750, 0); const memVal = useCountUp(result?.memory.value ?? 0, 750, 1); - // Cost calculations. Prefer a live cloud rate from the costings API (same - // resolver the Performance page uses), then the legacy pricing worker, then an - // amortized hardware cost so a GPU with a known price still shows an estimate. - const gpuShortName = (currentGpuOption?.label ?? '').replace(/NVIDIA\s+/i, '').replace(/AMD\s+/i, '').split(' ')[0]; - const livePrice = livePricing[gpuShortName]; const hwCost = costings.gpuHardwareCosts.get(gpuSystem)?.new_usd ?? null; const amortizedHwPerHour = hwCost != null ? hwCost / (AMORT_MONTHS_3YR * HOURS_PER_MONTH) : null; const resolvedCloudRate = resolveCloudRate(costings.gpuCloudRates.get(gpuSystem), preferredCloudProvider); - const pricePerHour = resolvedCloudRate?.rate ?? livePrice ?? amortizedHwPerHour ?? null; + const pricePerHour = resolvedCloudRate?.rate ?? amortizedHwPerHour ?? null; const rateBasis = resolvedCloudRate ? `${resolvedCloudRate.provider.replace('.', ' ยท ')} ${resolvedCloudRate.kind === 'spot' ? 'spot' : 'on-demand'}` - : livePrice != null ? 'live rate' : amortizedHwPerHour != null ? 'amortized hardware' : ''; const numGpus = result?.recommendation.totalGpus ?? 0; diff --git a/app/routing/RoutingEconomics.tsx b/app/routing/RoutingEconomics.tsx index d05c728..0153075 100644 --- a/app/routing/RoutingEconomics.tsx +++ b/app/routing/RoutingEconomics.tsx @@ -4,7 +4,6 @@ import * as React from 'react' import { Switch, FormSelect, FormSelectOption } from '@patternfly/react-core' import { useCountUp } from '@/app/performance/quickEstimateHelpers' import { FRONTIER_MODELS } from '@/lib/pricing/frontier-models' -import { getCloudRate, getOwnedRate } from '@/lib/pricing/gpu-rates' import { useCostings, resolveCloudRate } from '@/lib/hooks/useCostings' import { useSettings } from '@/contexts/SettingsContext' import { DEFAULT_TIERS } from '@/lib/routing/tier-defaults' @@ -65,7 +64,6 @@ export default function RoutingEconomics() { const [compareOpen, setCompareOpen] = React.useState(false) const [flipped, setFlipped] = React.useState>({}) const [tiers, setTiers] = React.useState(initTiers) - const [livePricing, setLivePricing] = React.useState>({}) const { modelOptions: aicModels, gpuOptions: aicGpus } = useAicCatalog() const { costingsEnabled, preferredCloudProvider, pricingSource } = useSettings() const costings = useCostings(costingsEnabled, pricingSource) @@ -78,39 +76,16 @@ export default function RoutingEconomics() { ['Meta', 'Mistral', 'Google', 'Qwen', 'DeepSeek', 'NVIDIA', 'MiniMax', 'Moonshot'].includes(m.vendor) ), [aicModels]) - React.useEffect(() => { - const fetchPricing = async () => { - try { - const response = await fetch('/api/gpus?live_pricing=true') - const data = await response.json() - if (data.status === 'success' && data.data?.gpus) { - const pricing: Record = {} - data.data.gpus.forEach((gpu: { name: string; live_pricing?: { onDemand?: { median?: number } } }) => { - if (gpu.live_pricing?.onDemand?.median) { - pricing[gpu.name] = gpu.live_pricing.onDemand.median - } - }) - setLivePricing(pricing) - } - } catch { /* no live pricing available */ } - } - fetchPricing() - }, []) const getRate = React.useCallback( (gpuId: string): number | null => { if (mode === 'cloud') { - // Prefer an aicostings rate (preferred provider โ†’ cheapest on-demand โ†’ - // cheapest spot); fall back to the legacy live-pricing worker. - if (costings.gpuCloudRates.size > 0) { - const resolved = resolveCloudRate(costings.gpuCloudRates.get(gpuId), preferredCloudProvider) - if (resolved) return resolved.rate - } - return getCloudRate(gpuId, livePricing) + const resolved = resolveCloudRate(costings.gpuCloudRates.get(gpuId), preferredCloudProvider) + return resolved?.rate ?? null } - return getOwnedRate(gpuId) + return null }, - [mode, livePricing, costings.gpuCloudRates, preferredCloudProvider], + [mode, costings.gpuCloudRates, preferredCloudProvider], ) const result = React.useMemo( diff --git a/lib/api/cloudflare.ts b/lib/api/cloudflare.ts deleted file mode 100644 index b901dd0..0000000 --- a/lib/api/cloudflare.ts +++ /dev/null @@ -1,125 +0,0 @@ -// Cloudflare Worker integration for live GPU pricing -// Fetches from deployed gpu-pricing-worker - -const CLOUDFLARE_WORKER_URL = 'https://gpu-pricing-worker.vikasgrover2004.workers.dev' -const CACHE_TTL_SECONDS = 60 * 60 * 6 // 6 hours (match Cloudflare KV) - -export interface CloudflareGPUPrice { - id: string - provider: string - category: 'gpu_cloud' | 'api_token' - gpu: string | null - vram_gb: number | null - model: string | null - pricing_type: 'on_demand' | 'spot' | 'reserved_1yr' | 'api' | 'spot_median' - price_usd: number - unit: string - gpu_count: number - region: string | null - source_url: string - confidence: 'high' | 'medium' | 'low' - status: 'approved' | 'pending_review' | 'rejected' - fetched_at: string - updated_at: string -} - -export interface CloudflarePricesResponse { - prices: CloudflareGPUPrice[] - count: number - source: 'cache' | 'db' - timestamp: string - filters_applied?: string[] -} - -/** - * Fetch GPU pricing from Cloudflare Worker - * @param filters - Query parameters for filtering (gpu, provider, pricing_type) - */ -export async function fetchGPUPricing( - filters?: { - gpu?: string - provider?: string - pricing_type?: string - category?: string - } -): Promise { - const params = new URLSearchParams({ - category: 'gpu_cloud', - ...filters - }) - - const url = `${CLOUDFLARE_WORKER_URL}/prices?${params.toString()}` - - const response = await fetch(url, { - next: { revalidate: CACHE_TTL_SECONDS } - }) - - if (!response.ok) { - throw new Error(`Cloudflare Worker returned ${response.status}`) - } - - return response.json() -} - -/** - * Get pricing statistics for a specific GPU type - * Returns min, median, max pricing across providers - */ -export function aggregateGPUPricing( - prices: CloudflareGPUPrice[], - pricingType: 'on_demand' | 'spot' = 'on_demand' -): { - min: number | null - median: number | null - max: number | null - count: number - providers: Array<{ provider: string; price_per_gpu: number; region: string }> -} { - const filtered = prices - .filter(p => p.pricing_type === pricingType && p.price_usd > 0) - .map(p => ({ - provider: p.provider, - price_per_gpu: p.price_usd / (p.gpu_count || 1), - region: p.region || 'global' - })) - .sort((a, b) => a.price_per_gpu - b.price_per_gpu) - - if (filtered.length === 0) { - return { min: null, median: null, max: null, count: 0, providers: [] } - } - - return { - min: filtered[0].price_per_gpu, - median: filtered[Math.floor(filtered.length / 2)].price_per_gpu, - max: filtered[filtered.length - 1].price_per_gpu, - count: filtered.length, - providers: filtered - } -} - -/** - * Fetch API token pricing (for model inference cost estimation) - */ -export async function fetchAPITokenPricing( - filters?: { - provider?: string - model?: string - } -): Promise { - const params = new URLSearchParams({ - category: 'api_token', - ...filters - }) - - const url = `${CLOUDFLARE_WORKER_URL}/prices?${params.toString()}` - - const response = await fetch(url, { - next: { revalidate: CACHE_TTL_SECONDS } - }) - - if (!response.ok) { - throw new Error(`Cloudflare Worker returned ${response.status}`) - } - - return response.json() -} diff --git a/lib/api/responses.ts b/lib/api/responses.ts index c1e782f..6e538f8 100644 --- a/lib/api/responses.ts +++ b/lib/api/responses.ts @@ -20,8 +20,6 @@ export function formatGpuCatalogResponse(gpus: GpuSpec[]) { tflops: gpu.tflops, power_watts: gpu.powerWatts, cloud_availability_pct: gpu.cloudAvailabilityPct, - // Include live pricing if available - ...(gpu.livePricing && { live_pricing: gpu.livePricing }) })), count: gpus.length } diff --git a/lib/gpu-math/gpus.ts b/lib/gpu-math/gpus.ts index 727e549..b72dd4f 100644 --- a/lib/gpu-math/gpus.ts +++ b/lib/gpu-math/gpus.ts @@ -36,23 +36,4 @@ export interface GpuSpec { powerWatts: number cloudAvailabilityPct: number tpuAvailabilityPct: number - - // Live pricing from Cloudflare Worker (optional - populated at runtime) - livePricing?: { - onDemand?: { - min: number | null - median: number | null - max: number | null - count: number - providers: Array<{ provider: string; price_per_gpu: number; region: string }> - } - spot?: { - min: number | null - median: number | null - max: number | null - count: number - providers: Array<{ provider: string; price_per_gpu: number; region: string }> - } - lastUpdated: string - } } diff --git a/lib/pricing/gpu-rates.ts b/lib/pricing/gpu-rates.ts deleted file mode 100644 index 59f5d00..0000000 --- a/lib/pricing/gpu-rates.ts +++ /dev/null @@ -1,20 +0,0 @@ -// TODO (Costings REST API): replace live pricing lookup with Costings API call. -// Until then, functions return null when pricing is unknown โ€” callers must grey -// out cost-dependent UI rather than showing fabricated numbers. - -export function getCloudRate( - gpuId: string, - livePricing?: Record, -): number | null { - if (livePricing && livePricing[gpuId] !== undefined) { - return livePricing[gpuId] - } - return null -} - -export function getOwnedRate( - gpuId: string, -): number | null { - // TODO (Costings REST API): return hardware amortisation rate from Costings API - return null -}