HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2import { ExternalLink } from 'lucide-react';3import Link from 'next/link';4import { usePathname, useSearchParams } from 'next/navigation';5import { useCallback, useEffect, useMemo, useState } from 'react';6import { Chip } from '@/components/ui/badges';7import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';8import { EntityLink } from '@/components/ui/entity';9import { Note } from '@/components/ui/section';10import { clientIntel } from '@/lib/client-api';11import { cn } from '@/lib/cn';12import { fmtAgo, fmtInt, fmtTokens, fmtUsdPerM, hostOf, num } from '@/lib/format';13import { routes } from '@/lib/site';14import type { CostContextPayload, CostPayload } from '@/lib/types';15import { CTRL_LG, Field, Methodology } from './bits';16import { ModelPicker } from './model-picker';1718/*19 Cost calculator (client, same-origin API only). Two tabs:20 - Workload: `/cost?model=&provider=&input_tokens=&output_tokens=&requests_per_day=&cached_share=&batch=` → per request / daily /21 monthly / annual for EVERY current deployment side by side. The cheapest column is bold — that is a fact about this workload,22 not a verdict about the model.23 - Context cost: `/cost/context?tokens=` → "how much does a fully populated N-token context cost?" for every offer whose context ≥ N.24 All inputs live in the URL (shareable); every number and the methodology come from the API.25*/26const PRESETS_CTX = [27 { value: 128_000, label: '128K' },28 { value: 200_000, label: '200K' },29 { value: 1_000_000, label: '1M' },30 { value: 2_000_000, label: '2M' },31];32const fmtMoney = (v: unknown, digits?: number) => {33 const n = num(v);34 if (n === null) return '—';35 if (digits !== undefined) return `$${n.toFixed(digits)}`;36 if (n === 0) return '$0';37 if (n < 0.001) return `$${n.toFixed(6).replace(/0+$/, '')}`;38 if (n < 1) return `$${n.toFixed(4).replace(/0+$/, '').replace(/\.$/, '')}`;39 if (n < 1000) return `$${n.toFixed(2)}`;40 return `$${new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(n)}`;41};4243type State = { model: string; modelName: string; provider: string; input_tokens: string; output_tokens: string; requests_per_day: string; cached: string; batch: boolean; tab: 'workload' | 'context'; tokens: string };4445function read(sp: URLSearchParams): State {46 return {47 model: sp.get('model') ?? '',48 modelName: sp.get('name') ?? '',49 provider: sp.get('provider') ?? '',50 input_tokens: sp.get('input_tokens') ?? '1000',51 output_tokens: sp.get('output_tokens') ?? '500',52 requests_per_day: sp.get('requests_per_day') ?? '1000',53 cached: sp.get('cached') ?? '0',54 batch: sp.get('batch') === '1',55 tab: sp.get('tab') === 'context' ? 'context' : 'workload',56 tokens: sp.get('tokens') ?? '1000000',57 };58}5960export function Calculator() {61 const sp = useSearchParams();62 const pathname = usePathname();63 const [s, setS] = useState<State>(() => read(new URLSearchParams(sp.toString())));64 const [cost, setCost] = useState<CostPayload | null>(null);65 const [ctx, setCtx] = useState<CostContextPayload | null>(null);66 const [busy, setBusy] = useState(false);67 const [error, setError] = useState<string | null>(null);6869 // URL ← state (debounced, replace)70 useEffect(() => {71 const t = setTimeout(() => {72 const p = new URLSearchParams();73 if (s.model) p.set('model', s.model);74 if (s.modelName && s.modelName !== s.model) p.set('name', s.modelName);75 if (s.provider) p.set('provider', s.provider);76 if (s.input_tokens !== '1000') p.set('input_tokens', s.input_tokens);77 if (s.output_tokens !== '500') p.set('output_tokens', s.output_tokens);78 if (s.requests_per_day !== '1000') p.set('requests_per_day', s.requests_per_day);79 if (s.cached !== '0') p.set('cached', s.cached);80 if (s.batch) p.set('batch', '1');81 if (s.tab === 'context') p.set('tab', 'context');82 if (s.tokens !== '1000000') p.set('tokens', s.tokens);83 const q = p.toString();84 const next = q ? `${pathname}?${q}` : pathname;85 const current = `${window.location.pathname}${window.location.search}`;86 // replaceState (not router.replace): the URL mirrors the inputs without a server round trip on every keystroke87 if (next !== current) window.history.replaceState(null, '', next);88 }, 250);89 return () => clearTimeout(t);90 }, [s, pathname]);9192 const costQs = useMemo(() => {93 if (!s.model) return null;94 const p = new URLSearchParams({ model: s.model });95 if (s.provider) p.set('provider', s.provider);96 p.set('input_tokens', String(Math.max(0, Math.round(num(s.input_tokens) ?? 0))));97 p.set('output_tokens', String(Math.max(0, Math.round(num(s.output_tokens) ?? 0))));98 p.set('requests_per_day', String(Math.max(0, num(s.requests_per_day) ?? 0)));99 p.set('cached_share', String(Math.min(1, Math.max(0, (num(s.cached) ?? 0) / 100))));100 p.set('batch', s.batch ? '1' : '0');101 return p.toString();102 }, [s.model, s.provider, s.input_tokens, s.output_tokens, s.requests_per_day, s.cached, s.batch]);103104 useEffect(() => {105 if (s.tab !== 'workload' || !costQs) return;106 const ctrl = new AbortController();107 setBusy(true);108 setError(null);109 const t = setTimeout(() => {110 clientIntel111 .cost(costQs, ctrl.signal)112 .then((r) => {113 setCost(r);114 setBusy(false);115 })116 .catch((e) => {117 if (ctrl.signal.aborted) return;118 setError(e instanceof Error ? e.message : 'unavailable');119 setBusy(false);120 });121 }, 200);122 return () => {123 clearTimeout(t);124 ctrl.abort();125 };126 }, [costQs, s.tab]);127128 const ctxQs = useMemo(() => {129 const t = Math.max(1, Math.round(num(s.tokens) ?? 0));130 const p = new URLSearchParams({ tokens: String(t), limit: '60' });131 if (s.model) p.set('model', s.model);132 return p.toString();133 }, [s.tokens, s.model]);134 useEffect(() => {135 if (s.tab !== 'context') return;136 const ctrl = new AbortController();137 setBusy(true);138 setError(null);139 const t = setTimeout(() => {140 clientIntel141 .costContext(ctxQs, ctrl.signal)142 .then((r) => {143 setCtx(r);144 setBusy(false);145 })146 .catch((e) => {147 if (ctrl.signal.aborted) return;148 setError(e instanceof Error ? e.message : 'unavailable');149 setBusy(false);150 });151 }, 200);152 return () => {153 clearTimeout(t);154 ctrl.abort();155 };156 }, [ctxQs, s.tab]);157158 const set = useCallback(<K extends keyof State>(k: K, v: State[K]) => setS((p) => ({ ...p, [k]: v })), []);159 const providers = useMemo(() => {160 const m = new Map<string, string>();161 for (const it of cost?.items ?? []) m.set(it.deployment.provider.slug, it.deployment.provider.name);162 return [...m.entries()];163 }, [cost]);164 const cheapest = useMemo(() => {165 const vals = (cost?.items ?? []).map((it) => num(it.cost.monthly)).filter((v): v is number => v !== null);166 return vals.length ? Math.min(...vals) : null;167 }, [cost]);168169 const tabBtn = (id: State['tab'], label: string) => (170 <button type="button" role="tab" aria-selected={s.tab === id} onClick={() => set('tab', id)} className={cn('-mb-px flex h-11 items-center border-b-2 px-3 text-sm whitespace-nowrap', s.tab === id ? 'border-ink font-medium text-ink' : 'border-transparent text-ink-2 hover:text-ink')} data-calc-tab={id}>171 {label}172 </button>173 );174175 return (176 <div data-calculator>177 <div role="tablist" aria-label="Calculator mode" className="no-scrollbar -mx-4 flex overflow-x-auto border-b border-rule px-4 md:mx-0 md:px-0">178 {tabBtn('workload', 'Workload cost')}179 {tabBtn('context', 'Context cost')}180 </div>181182 {/* --------------------------------------------------------------------------------------------------- inputs */}183 <div className="grid gap-4 py-5 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)]">184 <Field label="Model" className="lg:col-span-2" hint={s.model ? <span className="mono">{s.model}</span> : 'Any canonical model; suggestions from the atlas.'}>185 <ModelPicker value={s.model} label={s.modelName || null} onSelect={(slug, name) => setS((p) => ({ ...p, model: slug, modelName: name, provider: '' }))} />186 </Field>187 {s.tab === 'workload' ? (188 <>189 <Field label="Provider (optional)">190 <select value={s.provider} onChange={(e) => set('provider', e.target.value)} className={CTRL_LG} disabled={!providers.length && !s.provider}>191 <option value="">All providers</option>192 {providers.map(([slug, name]) => (193 <option key={slug} value={slug}>194 {name}195 </option>196 ))}197 {s.provider && !providers.some(([slug]) => slug === s.provider) && <option value={s.provider}>{s.provider}</option>}198 </select>199 </Field>200 <Field label="Requests / day">201 <input inputMode="numeric" value={s.requests_per_day} onChange={(e) => set('requests_per_day', e.target.value.replace(/[^\d.]/g, ''))} className={CTRL_LG} />202 </Field>203 <Field label="Input tokens / request">204 <input inputMode="numeric" value={s.input_tokens} onChange={(e) => set('input_tokens', e.target.value.replace(/[^\d]/g, ''))} className={CTRL_LG} />205 </Field>206 <Field label="Output tokens / request">207 <input inputMode="numeric" value={s.output_tokens} onChange={(e) => set('output_tokens', e.target.value.replace(/[^\d]/g, ''))} className={CTRL_LG} />208 </Field>209 <Field label="Cached input share" hint="Share of input tokens served from a prompt cache (used only when the provider publishes a cached price).">210 <span className="flex items-center gap-3">211 <input type="range" min={0} max={100} step={5} value={s.cached} onChange={(e) => set('cached', e.target.value)} className="h-11 w-full accent-[var(--accent)]" aria-label="Cached input share (%)" />212 <span className="tnum w-12 shrink-0 text-right text-sm text-ink">{s.cached}%</span>213 </span>214 </Field>215 <Field label="Batch API">216 <span className="flex h-11 items-center">217 <label className="inline-flex min-h-11 cursor-pointer items-center gap-2 text-sm text-ink-2">218 <input type="checkbox" checked={s.batch} onChange={(e) => set('batch', e.target.checked)} className="size-4 accent-[var(--accent)]" /> Use batch prices when published219 </label>220 </span>221 </Field>222 </>223 ) : (224 <Field label="Context to fill (tokens)" className="lg:col-span-2" hint="How much does one fully populated context cost, at the input price? Presets or any number.">225 <span className="flex flex-wrap items-center gap-1.5">226 {PRESETS_CTX.map((p) => (227 <button key={p.value} type="button" onClick={() => set('tokens', String(p.value))} aria-pressed={num(s.tokens) === p.value} className={cn('h-11 border px-3 text-sm font-medium', num(s.tokens) === p.value ? 'border-ink bg-ink text-canvas' : 'border-rule text-ink-2 hover:border-rule-strong hover:text-ink')}>228 {p.label}229 </button>230 ))}231 <input inputMode="numeric" value={s.tokens} onChange={(e) => set('tokens', e.target.value.replace(/[^\d]/g, ''))} className={cn(CTRL_LG, 'w-40 flex-none')} aria-label="Custom token count" />232 </span>233 </Field>234 )}235 </div>236237 {/* --------------------------------------------------------------------------------------------------- results */}238 {s.tab === 'workload' ? (239 !s.model ? (240 <p className="border border-dashed border-rule-strong px-4 py-8 text-center text-sm text-ink-3">Pick a model to price this workload across every provider that currently serves it.</p>241 ) : error ? (242 <p className="border border-dashed border-rule-strong px-4 py-8 text-center text-sm text-ink-3">Cost unavailable ({error}). The slug must be a canonical model — try the suggestions.</p>243 ) : !cost ? (244 <p className="py-8 text-center text-sm text-ink-3" aria-busy="true">245 Computing…246 </p>247 ) : (248 <div aria-busy={busy}>249 <p className="tnum mb-3 flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-ink-2">250 {cost.model && <EntityLink e={cost.model} className="text-[15px] font-medium" />}251 <span>252 {fmtInt(cost.inputs.input_tokens)} in + {fmtInt(cost.inputs.output_tokens)} out tokens × {fmtInt(cost.inputs.requests_per_day)} req/day · cached {Math.round((num(cost.inputs.cached_share) ?? 0) * 100)}% · batch {cost.inputs.batch ? 'on' : 'off'}253 </span>254 <span className="text-ink-3">{fmtInt(cost.total)} current deployment{cost.total === 1 ? '' : 's'}</span>255 </p>256 <DataTable scroll compact>257 <thead>258 <tr>259 <Th>Provider</Th>260 <Th num>Eff. input / 1M</Th>261 <Th num>Eff. output / 1M</Th>262 <Th num>Per request</Th>263 <Th num>Daily</Th>264 <Th num>Monthly</Th>265 <Th num>Annual</Th>266 <Th>Price rows used</Th>267 <Th>Source</Th>268 </tr>269 </thead>270 <tbody>271 {cost.items.length === 0 && <EmptyRow cols={9}>No current deployment for this model{s.provider ? ' at this provider' : ''}. Without a published price there is nothing to compute.</EmptyRow>}272 {cost.items.map((it) => {273 const d = it.deployment;274 const c = it.cost;275 const best = cheapest !== null && num(c.monthly) === cheapest && cost.items.length > 1;276 return (277 <tr key={d.id} className={best ? 'bg-accent-2-soft/40' : undefined} data-cost-row>278 <Td primary>279 <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5">280 <EntityLink e={d.provider} />281 {best && <Chip tone="accent">cheapest here</Chip>}282 {d.status !== 'active' && <Chip>{d.status}</Chip>}283 </span>284 {d.provider_model_id && <span className="mono block text-[11px] text-ink-3">{d.provider_model_id}</span>}285 {c.notes.length > 0 && (286 <ul className="mt-0.5 space-y-0.5 text-[11px] text-ink-3">287 {c.notes.map((n) => (288 <li key={n}>{n}</li>289 ))}290 </ul>291 )}292 </Td>293 <Td num label="Eff. input" className="tnum text-accent-2">{fmtUsdPerM(c.effective_input_per_mtok)}</Td>294 <Td num label="Eff. output" className="tnum text-accent-2">{fmtUsdPerM(c.effective_output_per_mtok)}</Td>295 <Td num label="Per request" className="tnum">{fmtMoney(c.per_request)}</Td>296 <Td num label="Daily" className="tnum">{fmtMoney(c.daily)}</Td>297 <Td num label="Monthly" className={cn('tnum', best && 'font-semibold text-accent-2')}>{fmtMoney(c.monthly)}</Td>298 <Td num label="Annual" className="tnum">{fmtMoney(c.annual)}</Td>299 <Td label="Price rows used" className="tnum text-xs text-ink-2">300 in {fmtUsdPerM(d.prices.input)} · out {fmtUsdPerM(d.prices.output)}301 {num(d.prices.cached_input) !== null && <> · cached {fmtUsdPerM(d.prices.cached_input)}</>}302 {num(d.prices.batch_input) !== null && <> · batch in {fmtUsdPerM(d.prices.batch_input)}</>}303 {num(d.prices.batch_output) !== null && <> · batch out {fmtUsdPerM(d.prices.batch_output)}</>}304 {num(d.prices.per_request) !== null && <> · fee {fmtMoney(d.prices.per_request)}/req</>}305 {num(d.context_length) !== null && <span className="block text-ink-3">context {fmtTokens(d.context_length)} · observed {fmtAgo(d.observed_at)}</span>}306 </Td>307 <Td label="Source">308 {d.source_url ? (309 <a href={d.source_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-ink-2 hover:text-accent">310 {hostOf(d.source_url) ?? 'source'} <span className="mono">T{d.tier}</span> <ExternalLink className="size-3" aria-hidden />311 </a>312 ) : (313 <span className="text-xs text-ink-3">—</span>314 )}315 </Td>316 </tr>317 );318 })}319 </tbody>320 </DataTable>321 <Note className="mt-3">322 Bold = lowest monthly cost <em>for this workload</em> among the deployments listed — not a verdict about the model or the provider. Prices are the current published rows; cached/batch prices apply only when published (otherwise the standard price is used and a note says so).323 </Note>324 <Methodology text={cost.methodology} />325 {cost.note && <Note className="mt-1">{cost.note}</Note>}326 </div>327 )328 ) : error ? (329 <p className="border border-dashed border-rule-strong px-4 py-8 text-center text-sm text-ink-3">Context cost unavailable ({error}).</p>330 ) : !ctx ? (331 <p className="py-8 text-center text-sm text-ink-3" aria-busy="true">332 Computing…333 </p>334 ) : (335 <div aria-busy={busy}>336 <h2 className="mb-3 text-base font-semibold tracking-tight md:text-lg">337 How much does a fully populated {fmtTokens(ctx.tokens)}-token context cost? <span className="tnum text-sm font-normal text-ink-3">{fmtInt(ctx.total)} offers with context ≥ {fmtTokens(ctx.tokens)}</span>338 </h2>339 <DataTable scroll compact>340 <thead>341 <tr>342 <Th>Model</Th>343 <Th>Provider</Th>344 <Th num>Context</Th>345 <Th num>Input / 1M</Th>346 <Th num>Cost per fill</Th>347 <Th>Source</Th>348 </tr>349 </thead>350 <tbody>351 {ctx.items.length === 0 && <EmptyRow cols={6}>No current offer advertises a context window of at least {fmtTokens(ctx.tokens)} tokens{s.model ? ' for this model' : ''}.</EmptyRow>}352 {ctx.items.map((it, i) => {353 const d = it.deployment;354 return (355 <tr key={`${d.id}-${i}`} data-context-row>356 <Td primary>357 <EntityLink e={d.model} />358 {d.model.organization && <span className="ml-2 text-xs text-ink-3">{d.model.organization.name}</span>}359 </Td>360 <Td label="Provider" className="text-ink-2">361 <Link href={routes.entity(d.provider)} className="hover:text-accent">362 {d.provider.name}363 </Link>364 </Td>365 <Td num label="Context" className="tnum">366 {fmtTokens(it.context_length)} <span className="text-[10px] text-ink-3">{it.context_source}</span>367 </Td>368 <Td num label="Input / 1M" className="tnum text-accent-2">{fmtUsdPerM(d.prices.input)}</Td>369 <Td num label="Cost per fill" className={cn('tnum', i === 0 && ctx.items.length > 1 && 'font-semibold text-accent-2')}>{fmtMoney(it.cost_usd)}</Td>370 <Td label="Source">371 {d.source_url ? (372 <a href={d.source_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-ink-2 hover:text-accent">373 {hostOf(d.source_url) ?? 'source'} <span className="mono">T{d.tier}</span> <ExternalLink className="size-3" aria-hidden />374 </a>375 ) : (376 <span className="text-xs text-ink-3">—</span>377 )}378 </Td>379 </tr>380 );381 })}382 </tbody>383 </DataTable>384 <Note className="mt-3">Cheapest first (API order). Bold marks the lowest cost among the offers shown — a fact about the input price, not a quality judgement.</Note>385 <Methodology text={ctx.methodology} />386 </div>387 )}388 </div>389 );390}391