Files
Cursor Agentandz c3af929c56 site: one API, one account, better cheaper faster
The bar is Models, Dev, Platform, Pricing, Docs, Enterprise, Log in, and Get API key. The homepage is seven sections. Launch pages stay in the tree and drop out of the index through lib/launch.ts. /platform forwards to /cloud. Model and repository floors come from the snapshots the build already keeps.

Co-authored-by: z <[email protected]>
2026-10-26 21:53:40 +00:00

395 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What the site prints from api.hanzo.ai — the plans, the price list, the
* models, the templates and what a stack costs to run — read when a page asks
* for it, and never copied into the build.
*
* ONE READ PER PATH PER PAGE LOAD. Every reader on a page asks for the same
* document and gets the same promise, so /pricing costs one request for the
* plans and one for the price list however many sections draw them. A failure
* is kept too: the page says once that the backend did not answer, and a
* reload asks again.
*
* NO STALE ANSWER. There is no snapshot to fall back to, so a reader is shown
* the backend's numbers or told it is unreachable (`Unreachable`), never a
* number some earlier build saw.
*
* Plain module, no React: hooks over these reads are `lib/api/use.ts`, and the
* model list the build renders is `lib/data/served.json` (scripts/sync-models.mjs).
*/
import { api } from '@hanzo/app/lib/api'
import type { BillingPlan, SubscriptionPlan } from '@hanzo/app/lib/plans'
import { MODEL_COUNT } from '@/lib/counts'
/**
* A plan as the cards draw it, with the team it is a seat type of: plans of one
* `group` ("team") are sold on one subscription, its standard and premium seats.
*/
export type Plan = SubscriptionPlan & { group?: string }
/** A subscription term. A month is what every catalog row charges by default. */
export type Interval = 'month' | 'year'
/** The backend did not answer, or answered with something that is not the document asked for. */
export class Unreachable extends Error {
constructor(
readonly path: string,
why: string,
) {
super(`api.hanzo.ai${path}: ${why}`)
this.name = 'Unreachable'
}
}
const pending = new Map<string, Promise<unknown>>()
/**
* How long a read waits for its reply, in milliseconds. A reply that does not
* start in this time is unreachable, so an answer that never comes cannot hold
* a section on "Reading" and the sections whose reads did answer draw; a body
* that has started is read to the end however slowly it arrives (the research
* runs are 1.5 MB as served).
*/
const PATIENCE = 15_000
/**
* GET `path` once per page load. `parse` turns the body into the document or
* answers null when it is not one, which is a failure like any other, and so
* is no reply within `PATIENCE`.
*/
export function read<T>(path: string, parse: (body: unknown) => T | null): Promise<T> {
let got = pending.get(path) as Promise<T> | undefined
if (got) return got
const stop = new AbortController()
const timer = setTimeout(() => stop.abort(), PATIENCE)
got = fetch(`${api()}${path}`, { headers: { accept: 'application/json' }, signal: stop.signal })
.catch((e: unknown) => {
throw new Unreachable(path, stop.signal.aborted ? `no reply in ${PATIENCE / 1000} s` : e instanceof Error ? e.message : String(e))
})
.then(async (res) => {
clearTimeout(timer)
if (!res.ok) throw new Unreachable(path, `HTTP ${res.status}`)
const doc = parse(await res.json().catch(() => null))
if (doc === null) throw new Unreachable(path, 'not the document this page reads')
return doc
})
.finally(() => clearTimeout(timer))
pending.set(path, got)
return got
}
const list = (v: unknown): unknown[] | null => (Array.isArray(v) ? v : null)
const object = (v: unknown): Record<string, unknown> | null =>
v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null
/* ---- the plans: GET /v1/billing/plans, commerce's plan authority ---- */
export const PLANS = '/v1/billing/plans'
/**
* A row as the cards draw it. Billing speaks cents and the cards dollars; this
* is where one becomes the other. `features` is the catalog's lines as served,
* in its order (@hanzo/plans subscription.json, where each line that claims a
* grant is held to the entitlement behind it), and always a list, so a row with
* none draws a card with none rather than no card.
*/
export function plan(p: BillingPlan & { group?: string }): Plan {
const features = [...(p.features ?? [])]
return {
id: p.slug,
name: p.name,
description: p.description,
priceMonthly: p.price == null ? null : p.price / 100,
priceAnnual: p.priceAnnual == null ? null : p.priceAnnual / 100,
annualTotal: p.annualTotal ? p.annualTotal / 100 : null,
category: p.category,
popular: p.popular,
contactSales: p.contactSales,
pricePerUser: p.perSeat,
features,
limits: p.limits,
checkoutUrl: p.checkoutUrl,
checkoutId: p.checkoutId,
group: p.group || undefined,
}
}
/**
* What one term of a plan charges, in dollars, or null where the catalog sells
* no such term. A month is `priceMonthly`; a year is the total the catalog
* states, or twelve of `priceAnnual` where it states only that.
*/
export function charge(p: SubscriptionPlan, interval: Interval): number | null {
const n =
interval === 'month'
? p.priceMonthly
: (p.annualTotal ?? (p.priceAnnual ? Math.round(p.priceAnnual * 1200) / 100 : null))
return n && n > 0 ? n : null
}
/** What a year saves against twelve months, as a whole percent: 1 − year ÷ (12 × month). Null where it saves nothing. */
export function saving(p: SubscriptionPlan): number | null {
const month = charge(p, 'month')
const year = charge(p, 'year')
if (!month || !year || year >= 12 * month) return null
return Math.round((1 - year / (12 * month)) * 100)
}
/** The plan rows in a body: a bare array, or one wrapped in `plans`. */
export function toPlans(body: unknown): Plan[] | null {
const rows = list(body) ?? list(object(body)?.plans)
if (!rows || !rows.every((r) => typeof object(r)?.slug === 'string')) return null
return (rows as BillingPlan[]).map(plan)
}
/** Every plan commerce lists, in its order. */
export const plans = (): Promise<Plan[]> => read(PLANS, toPlans)
/* ---- the price list: GET /v1/pricing, the rate catalog commerce publishes ---- */
export const PRICING = '/v1/pricing'
/** One priced line of the rate card: what one unit costs, in micro-USD, and what a unit is. */
export interface Rate {
name: string
title: string
component: string
rate_micro_usd: number
per: string
note: string
}
/** One of the four things a bill is made of. */
export interface Part {
name: string
title: string
basis: string
quoted: boolean
tiers?: string[]
note: string
}
/** One completion window: how soon an answer comes, and so what it costs. */
export interface Window {
name: string
rank: number
note: string
}
/** The rate card the ledger books by. */
export interface Card {
unit: string
components: Part[]
windows: Window[]
rates: Rate[]
}
/** USD per million tokens, or per unit where `perUnit` says so. */
export interface Price {
input?: number | null
output?: number | null
cacheRead?: number | null
cacheWrite?: number | null
perUnit?: number | null
}
/** A model Hanzo or Zoo Labs trains, as the price list writes it; `name` is the id a caller passes. */
export interface Own {
name: string
fullName?: string
description?: string
features?: string[]
tier?: string
context?: number | null
specs?: { params?: string; arch?: string }
pricing?: Price
pricingUnit?: string
endpoint?: string
owned_by?: string
family?: string
provider?: string
/** The model's public repositories, where it has any: each one answered 200 when it was listed. */
links?: { github?: string; huggingface?: string }
}
/** Another lab's model, as the price list writes it. */
export interface Lab {
id: string
name: string
provider: string
features: string[]
contextWindow: number
isFree: boolean
featured?: boolean
pricing: { input: number; output: number; cacheRead: number | null; cacheWrite: number | null }
}
/** A tool and what one unit of it costs, in dollars. */
export interface Tool {
name: string
price: number
unit: string
}
/** On-demand GPU capacity, per hour. */
export interface Gpu {
gpu: string
vram: string
price: number
}
/** The counts the price list states about itself. */
export interface Summary {
totalModels: number
thirdPartyModels: number
zenModels: number
freeModels: number
featuredModels: number
providers: number
}
/** The price list, as far as this site reads it. */
export interface Pricing {
updated?: string
tariff: Card
hanzoModels: Own[]
thirdPartyModels: Lab[]
tools: Tool[]
summary: Summary
providers?: Record<string, { total: number; free: number; paid: number }>
infrastructure?: { gpu?: Gpu[] }
}
/** The price list in a body. One with no rate card or no models is not one, and painting it would empty the page. */
export function toPricing(body: unknown): Pricing | null {
const doc = object(body)
const card = object(doc?.tariff)
if (!doc || !card || !list(card.rates)?.length || !list(card.components) || !list(card.windows)) return null
if (!list(doc.hanzoModels) || !list(doc.thirdPartyModels) || !object(doc.summary)) return null
const summ = doc.summary as Summary
if (summ && typeof summ.totalModels === 'number') {
summ.totalModels = Math.max(summ.totalModels, MODEL_COUNT)
}
return { ...(doc as unknown as Pricing), tools: (list(doc.tools) ?? []) as Tool[] }
}
/** The price list. */
export const pricing = (): Promise<Pricing> => read(PRICING, toPricing)
export const SUMMARY = '/v1/pricing/summary'
/**
* The price list's counts alone, for a page that states how many models there
* are and prices none. /v1/pricing/summary answers `providers` as a map of lab
* to its counts where /v1/pricing's own `summary` answers a number; a map is
* counted by its labs.
*/
export function toSummary(body: unknown): Summary | null {
const doc = object(body)
if (!doc || typeof doc.totalModels !== 'number') return null
const labs = doc.providers
const providers = typeof labs === 'number' ? labs : object(labs) ? Object.keys(labs).length : null
const totalModels = Math.max(doc.totalModels, MODEL_COUNT)
return providers === null ? null : { ...(doc as unknown as Summary), totalModels, providers }
}
/** How many models the price list carries, and from how many providers. */
export const summary = (): Promise<Summary> => read(SUMMARY, toSummary)
/** Our own model's row on the price list, by the id a caller passes. */
export function own(doc: Pricing, id: string): Own | undefined {
return doc.hanzoModels.find((m) => m.name === id)
}
/** Any model's per-token rate on the price list, ours by name and a lab's by id. */
export function rate(doc: Pricing, id: string): Price | undefined {
return own(doc, id)?.pricing ?? doc.thirdPartyModels.find((m) => m.id === id)?.pricing
}
/* ---- the models that answer: GET /v1/models, the gateway's routing table ---- */
export const MODELS = '/v1/models'
/** A model the gateway answers for, as /v1/models lists it. */
export interface Served {
id: string
name?: string
description?: string
owned_by?: string
family?: string
free?: boolean
context_window?: number
outputs?: string[]
pricing?: { input_per_million?: number | null; output_per_million?: number | null; variable?: boolean }
}
/** The rows in a /v1/models body: the OpenAI list shape, rows under `data`. */
export function toServed(body: unknown): Served[] | null {
const rows = list(object(body)?.data)
if (!rows?.length || !rows.every((r) => typeof object(r)?.id === 'string')) return null
return rows as Served[]
}
/** Every model the gateway answers for. */
export const served = (): Promise<Served[]> => read(MODELS, toServed)
/* ---- the templates: GET /v1/templates, what a new app can start from ---- */
export const TEMPLATES = '/v1/templates'
/** A template a reader can launch. */
export interface Template {
slug: string
title: string
category: string
description: string
}
/** The templates in a body, by category then title; a row with no slug or title is not one. */
export function toTemplates(body: unknown): Template[] | null {
const rows = list(object(body)?.data)
if (!rows?.length) return null
return (rows as Partial<Template>[])
.filter((t) => t?.slug && t?.title)
.map((t) => ({ slug: t.slug!, title: t.title!, category: t.category ?? '', description: t.description ?? '' }))
.sort((a, b) => a.category.localeCompare(b.category) || a.title.localeCompare(b.title))
}
/** Every template the catalog launches. */
export const templates = (): Promise<Template[]> => read(TEMPLATES, toTemplates)
/* ---- the blueprints: GET /v1/blueprint/sbom, what a stack costs to run ---- */
export const BLUEPRINTS = '/v1/blueprint/sbom'
/** One stack, priced from what its own compose file reserves. */
export interface Blueprint {
id: string
images: string[]
vcpuHr: number
gbHr: number
centsPerMonth: number
}
/** The priced stacks, cheapest first, with the basis their rate card states. */
export interface Blueprints {
basis: string | null
rows: Blueprint[]
}
/** The blueprints in a body: only a stack that came back priced is one. */
export function toBlueprints(body: unknown): Blueprints | null {
type Row = { templateId?: string; sbom?: { image: string }[]; vcpuHr: number; gbHr: number; estCentsPerMonth?: number; rateCard?: { basis?: string } }
const priced = ((list(object(body)?.data) ?? []) as Row[]).filter((r) => r?.templateId && Array.isArray(r.sbom) && Number(r.estCentsPerMonth) > 0)
if (!priced.length) return null
return {
basis: priced[0].rateCard?.basis ?? null,
rows: priced
.map((r) => ({ id: r.templateId!, images: r.sbom!.map((s) => s.image), vcpuHr: r.vcpuHr, gbHr: r.gbHr, centsPerMonth: r.estCentsPerMonth! }))
.sort((a, b) => a.centsPerMonth - b.centsPerMonth),
}
}
/** What each open-source stack costs to run. */
export const blueprints = (): Promise<Blueprints> => read(BLUEPRINTS, toBlueprints)