mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
The root layout handed every page the plan catalog (lib/rooms.ts configure with lib/data/plans.json), and /pricing, /calculator, /customers and the benchmark pages bundled the 202 KB price list. lib/api is now the one reader: plans() GET /v1/billing/plans, pricing() /v1/pricing, summary(), served() /v1/models, templates(), blueprints() /v1/blueprint/sbom, one promise a path a page load, each drawn as loading, ready or "api.hanzo.ai is unreachable" (components/api) and never an older number. plans.json, pricing.json, models.json, templates.json, blueprints.json and sync-pricing, sync-models, sync-templates, sync-blueprints are gone. What a static export must know before anyone asks, which model pages exist and their titles, is read by the build itself (lib/api/build, Next's fetch cache keyed by the build's stamp: one answer per build) and renders no rate. lib/moved reads the retired model pages against the export, and the 153 pages the old snapshot kept for models the gateway dropped are in retired.json. Kai's price beside Jev's, Enso's tier rates, a model page's rate and the GPU table read the price list in the browser; the site's plan math no longer imports @hanzo/rooms/lib/plans, whose bundled @hanzo/plans catalog rode the home, /pricing and the picker. Home first load 452.7 -> 437.4 KB gzip (budget 440); pages over perf.budget.json 32 -> 9, the rest over for settings code, connectors and the commerce catalog. The gates serve invented answers (e2e/gates/api) and block them; e2e/gates/read.spec.ts holds that no script carries a plan, price or model table.
108 lines
4.3 KiB
TypeScript
108 lines
4.3 KiB
TypeScript
/**
|
|
* The rate card, as the page writes it.
|
|
*
|
|
* ONE WRITER, so there is one place a number can be got wrong. Every figure on
|
|
* /pricing comes from the price list's `tariff` (GET /v1/pricing, read by
|
|
* `lib/api`), which the cloud resolves on every request from commerce's rate
|
|
* authority — the rows a SuperAdmin edits at admin.hanzo.ai — falling back to
|
|
* the floors it charges by (cloud tariff.go). Nothing is baked: the number a
|
|
* reader sees is the number the ledger books, read when the page opens, and
|
|
* scripts/audit-price-literals.mjs is what stops anyone shortcutting that by
|
|
* typing one.
|
|
*
|
|
* MICRO-USD IN, STRING OUT. The card carries integer micro-USD because a web
|
|
* call costs a fifth of a cent and a cent cannot hold it. Turning that into money
|
|
* is a DISPLAY act and it happens here, once — `money` for a rate as stated, and
|
|
* `perSecond` for the hourly compute rates, which are charged by the second and
|
|
* read that way. Neither rounds: the card's own precision survives to the page.
|
|
*/
|
|
|
|
import type { Card, Rate, Tool } from '@/lib/api/read'
|
|
|
|
export type { Card, Part, Rate, Tool, Window } from '@/lib/api/read'
|
|
|
|
/**
|
|
* Micro-USD as money. Zero is a price, and it reads as one. A sub-dollar rate
|
|
* keeps the precision the card states and at least two decimals, so a fifth of
|
|
* a cent reads $0.002 and a dime $0.10.
|
|
*/
|
|
export function money(micros: number): string {
|
|
if (micros === 0) return '$0'
|
|
const v = micros / 1e6
|
|
if (v >= 1) return `$${v.toLocaleString('en-US')}`
|
|
return `$${v.toFixed(Math.max(2, (String(v).split('.')[1] ?? '').length))}`
|
|
}
|
|
|
|
/**
|
|
* What one SECOND of an hourly rate costs.
|
|
*
|
|
* The card stores the hour because that is the unit a span is priced in and
|
|
* because a GiB-second is four and a half micro-USD, which no integer holds. The
|
|
* second is what a reader is actually charged by, so it is derived here rather
|
|
* than published as a second number that could disagree with the first.
|
|
*/
|
|
export function perSecond(micros: number): string {
|
|
return money(micros / 3600)
|
|
}
|
|
|
|
/**
|
|
* How a unit reads beside its price. The card names the unit exactly — a reader
|
|
* never has to guess what a number is per — and this is the short form of that
|
|
* name, the only place the two vocabularies meet.
|
|
*
|
|
* A rate of zero shows no unit, because "$0 per seat" and "$0 per call" both say
|
|
* the same thing and the shorter one says it better.
|
|
*/
|
|
const UNIT: Record<string, string> = {
|
|
'vcpu-hour': '/hr',
|
|
'gib-hour': '/GiB-hr',
|
|
call: '/call',
|
|
}
|
|
|
|
/** A rate as it reads on the card: the price, and what it is per. */
|
|
export function priced(r: Rate): string {
|
|
return r.rate_micro_usd === 0 ? money(0) : money(r.rate_micro_usd) + (UNIT[r.per] ?? '')
|
|
}
|
|
|
|
/** The hourly rates, which are charged by the second and read both ways. */
|
|
const SECOND: Record<string, string> = {
|
|
'vcpu-hour': '/second',
|
|
'gib-hour': '/GiB-second',
|
|
}
|
|
|
|
/** The same rate by the second, or null where the rate is not an hourly one. */
|
|
export function bySecond(r: Rate): string | null {
|
|
const unit = SECOND[r.per]
|
|
return unit && r.rate_micro_usd > 0 ? perSecond(r.rate_micro_usd) + unit : null
|
|
}
|
|
|
|
/**
|
|
* The tool rows the card prices, by the name the list gives them — the cloud's own
|
|
* map (apps/pricing/tariff.go webRows), so a row and a rate are one fact.
|
|
*/
|
|
const CARD_TOOLS: Record<string, string> = { 'Web Search': 'web_search', 'Web Fetch': 'web_fetch' }
|
|
|
|
/**
|
|
* The tool list with every row the card prices read FROM the card.
|
|
*
|
|
* The list owns a row's words and the card owns its number, which is how the
|
|
* cloud publishes the document too (repriceTools). Read through the card, a web
|
|
* row cannot state one price beside a card that charges another for the same
|
|
* call, and a web tool the list never carried still appears.
|
|
*/
|
|
export function pricedTools(tools: Tool[], by: Card): Tool[] {
|
|
const byName = new Map(by.rates.map((r) => [r.name, r]))
|
|
const seen = new Set<string>()
|
|
const out = tools.map((row) => {
|
|
const r = byName.get(CARD_TOOLS[row.name] ?? '')
|
|
if (!r) return row
|
|
seen.add(r.name)
|
|
return { ...row, price: r.rate_micro_usd / 1e6, unit: `per ${r.per}` }
|
|
})
|
|
for (const [name, id] of Object.entries(CARD_TOOLS)) {
|
|
const r = byName.get(id)
|
|
if (r && !seen.has(id)) out.push({ name, price: r.rate_micro_usd / 1e6, unit: `per ${r.per}` })
|
|
}
|
|
return out
|
|
}
|