SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
20.6 KB · 391 lines tsx
Raw Blame History
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