Files
hanzo.ai/lib/models/served.ts
zeekay 715912e9bf The model pages build from a committed catalogue, so a blip at api.hanzo.ai no longer fails a deploy and a price change alone rebuilds no page
scripts/sync-models.mjs runs in prebuild and saves what the pages read of
/v1/models and /v1/pricing as lib/data/served.json: names, words, windows,
specs, features, tiers and links, no rate and no time. An unreachable API, an
error, a body that is not the document, or a list under half the snapshot's
keeps the committed file and the build goes on; every cloud restart during a
build used to fail the deploy. Rates are read in the browser as before, so a
catalogue that changed only its prices writes the same file and the pages come
out byte for byte (two builds of one tree: no model page differs on data).

lib/api/build.ts and the HANZO_BUILD stamp that keyed its fetch cache go.
2026-10-18 16:48:30 -07:00

139 lines
6.8 KiB
TypeScript

/**
* The models this build writes a page for: every model the gateway answers for
* (GET /v1/models), named, described and windowed from the price list where the
* model is ours (GET /v1/pricing) and from the Zen catalogue where Zoo Labs
* published it (lib/data/zen.json: @zenlm/models as scripts/zen.mjs writes it)
* — as lib/data/served.json holds them (scripts/sync-models.mjs writes it before
* every build, and keeps it when the API cannot answer). No rate is carried: a
* page states what a model costs by reading the price list when it opens.
*
* Server only: the pages that exist, their titles and the sitemap are decided
* here, and nothing here ships to a browser.
*
* A model's public repositories come from its catalogue record (the price
* list's `links`, the Zen catalogue's `github` and `huggingface`) and are
* printed only if they answer 200 when the build asks: Hugging Face answers a
* missing or private repository 401 and GitHub 404, so a link that would be
* dead is dropped here, never drawn.
*/
import type { ZenModel } from '@zenlm/models'
import zen from '@/lib/data/zen.json' with { type: 'json' }
import catalogue from '@/lib/data/served.json' with { type: 'json' }
import type { Own, Served } from '@/lib/api/read'
import { described, getOrgAndSlug, zenToModelData, type ModelData, type ModelsResponse } from '@/lib/models'
import { distinct } from './distinct'
const ZEN = new Map((zen as ZenModel[]).map((z) => [z.id, zenToModelData(z)]))
const OWN = new Map((catalogue.own as Own[]).map((o) => [o.name, o]))
function row(s: Served, ours: Own | undefined): ModelData {
const z = ZEN.get(s.id)
const lab = s.id.includes('/') && !s.family
const isKai = s.id === 'kai' || s.id === 'hanzo/kai' || s.family === 'kai'
return described({
id: s.id,
name: isKai ? 'Kai' : (ours?.fullName ?? z?.name ?? s.name ?? s.id),
fullName: isKai ? 'Kai — Decision Intelligence' : (ours?.fullName ?? z?.fullName),
provider: isKai ? 'hanzo' : (s.owned_by ?? ''),
description: isKai
? 'Hanzo Kai is a structured decision model answering typed questions at /v1/decisions with calibrated probabilities and zero output tokens. Full multimodal decision architecture (~850M parameters, 32K reach); native vision and audio towers: Kai-1.7 target.'
: (ours?.description ?? z?.description ?? s.description),
context: isKai ? 32768 : (s.context_window ?? ours?.context ?? z?.context),
modalities: isKai ? ['Text', 'State', 'Vision (Kai-1.7 target)', 'Audio (Kai-1.7 target)'] : (s.outputs ?? z?.modalities),
status: 'available',
category: isKai ? 'decision' : (lab ? 'third-party' : (s.family ?? z?.category ?? 'chat')),
spec: isKai
? {
params: '~850M multimodal decision core',
activeParams: '~850M (multimodal decision architecture)',
context: 32768,
arch: 'Decision Transformer',
}
: (z?.spec ?? (ours?.specs ? { params: ours.specs.params, arch: ours.specs.arch } : undefined)),
features: isKai
? [
'Live: High-throughput text & structured state decision core',
'Live: Calibrated probability distributions over typed options (bool, choice, score)',
'Live: <1 ms p50 (one question, CUDA bf16)',
'Live: 32,768 positional reach via YaRN rotary stretching (1,024 trained base)',
'Live: Zero output tokens generated or billed ($0.021/MTok base input)',
'Experimental: Nio policy profiles decide from stated consequences and escalate when they cannot be counted',
'Kai-1.7 target: Multimodal vision, audio, and video decision towers (~850M total arch)',
'Kai-1.7 target: Dynamic adaptive reasoning depth (reasoning: "auto" | "fast")',
]
: (ours?.features ?? z?.features),
huggingface: ours?.links?.huggingface ?? z?.huggingface,
github: isKai ? 'https://github.com/hanzoai/kai' : (ours?.links?.github ?? z?.github),
family: s.family,
endpoint: s.outputs?.includes('decision') ? '/v1/decisions' : '/v1/chat/completions',
aliases: isKai ? ['kai', 'hanzo/kai', 'kai-1.6', 'kai-1.5'] : z?.aliases,
tier: isKai ? 'Enterprise' : (ours?.tier ?? z?.tier),
generation: isKai ? 'Kai 1.6' : z?.generation,
})
}
/** Whether a public page answers 200, asked once per URL a build. */
const asked = new Map<string, Promise<boolean>>()
function answers(url: string): Promise<boolean> {
let got = asked.get(url)
if (!got) {
got = fetch(url, { method: 'HEAD', redirect: 'follow', signal: AbortSignal.timeout(15_000) }).then(
(res) => res.status === 200,
() => false,
)
asked.set(url, got)
}
return got
}
/** The row with only the repositories that answer. */
async function live(m: ModelData): Promise<ModelData> {
const [github, huggingface] = await Promise.all([m.github, m.huggingface].map((u) => (u ? answers(u) : Promise.resolve(false))))
return { ...m, github: github ? m.github : undefined, huggingface: huggingface ? m.huggingface : undefined }
}
/** Whether a row's name is its own: the price list's row for this id, or this id's Zen release. */
const named = (s: Served, ours: Own | undefined): boolean => Boolean(ours?.fullName ?? ZEN.get(s.id)?.name)
/**
* Every page its own name. The gateway gives the ids of one family the family's
* name — enso-auto is "Enso" beside enso, kai-1.4 and kai-1.5 are "Kai" beside
* kai — and a page is titled by its row's name, so two rows of one name are two
* pages a reader cannot tell apart. Where rows share a name, the one row named
* for itself keeps it, and every other says its id beside it: "Kai (kai-1.4)".
*/
function unique(rows: { model: ModelData; own: boolean }[]): ModelData[] {
const groups = new Map<string, { model: ModelData; own: boolean }[]>()
for (const r of rows) groups.set(r.model.name, [...(groups.get(r.model.name) ?? []), r])
return rows.map(({ model, own }) => {
const group = groups.get(model.name)!
if (group.length === 1 || (own && group.filter((r) => r.own).length === 1)) return model
return { ...model, name: `${model.name} (${getOrgAndSlug(model.id).slug})` }
})
}
let models: Promise<ModelsResponse> | null = null
/**
* The catalogue this build renders: one row per model page (`distinct`: `kai`
* and `hanzo/kai` are one page), each under a name no other page has (`unique`),
* in the gateway's order.
*/
export function fetchModels(): Promise<ModelsResponse> {
models ??= (async () => {
const rows = unique(
await Promise.all(
distinct(
(catalogue.served as Served[]).map((s) => {
const ours = OWN.get(s.id)
return { model: row(s, ours), own: named(s, ours) }
}),
(r) => r.model.id,
).map(async (r) => ({ ...r, model: await live(r.model) })),
),
)
return { total: rows.length, models: rows }
})()
return models
}