Files
hanzo.ai/lib/models.ts
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

153 lines
5.6 KiB
TypeScript

import type { ZenModel } from '@zenlm/models'
import { canonicalOrg, getOrgAndSlug, modelPagePath } from '@hanzo/app/lib/path'
// What a provider and a model are called: the rooms' rules, the one copy.
export { ORG_DISPLAY_NAMES, orgDisplayName, modelName } from '@hanzo/app/lib/models'
export interface ModelSpec {
params?: string
activeParams?: string
context?: number
arch?: string
}
export interface ModelPrice {
input?: number | null
output?: number | null
}
export interface ModelData {
id: string
name: string
fullName?: string
provider: string
description?: string
context?: number
modalities?: string[]
/** @zenlm/models' own union. Widened to `string` here once, which is what let
* six `status !== 'coming-soon'` filters compile and silently match every
* model — dead code that read as a safeguard. Narrow, so the next one fails. */
status: 'available' | 'cloud-only'
category: string
spec?: ModelSpec
features?: string[]
huggingface?: string
github?: string
aliases?: string[]
tier?: string
generation?: string
/** USD per million tokens, as the live catalog states it. */
pricing?: ModelPrice | null
/** The family the gateway files it under (`kai`, `enso`, `zen`), if one of ours. */
family?: string
/** Where it is called: a model whose output is a decision answers at /v1/decisions. */
endpoint?: '/v1/decisions' | '/v1/chat/completions'
}
export interface ModelsResponse {
total: number
models: ModelData[]
}
/**
* A description a page is allowed to print.
*
* The live catalog fills an absent description with the placeholder
* `Zen model: <id>`, and that string is wrong twice over. It repeats the slug
* already printed beside it, and it calls a ROUTED model a Zen model — Zen is
* Hanzo's own family, so `Zen model: claude-opus-4.5` states something false
* about a model the platform routes rather than trains. The ids themselves are
* correct and public: they are what a caller passes to api.hanzo.ai, which is
* why the quick-start on each page prints them.
*
* Dropped HERE, at the one boundary both the live catalog and the static
* fallback pass through, so no page has to remember. The index page carried its
* own guard and the detail page did not, which put the placeholder in the
* visible lede, the meta description, the Open Graph and Twitter cards and the
* JSON-LD `description` of 116 pages — four of those being what a search engine
* indexes rather than what a visitor reads.
*/
export function described(m: ModelData): ModelData {
const d = m.description?.trim()
return d && !/^zen model:/i.test(d) ? m : { ...m, description: undefined }
}
export function zenToModelData(z: ZenModel): ModelData {
return {
id: z.id,
name: z.name,
fullName: z.fullName,
provider: 'hanzo',
description: z.description,
context: z.spec?.context,
modalities: z.modalities as string[],
status: z.status,
category: z.category,
spec: z.spec ? {
params: z.spec.params,
activeParams: z.spec.activeParams ?? undefined,
context: z.spec.context,
arch: z.spec.arch,
} : undefined,
features: z.features,
huggingface: z.huggingface ?? undefined,
github: z.github ?? undefined,
aliases: z.aliases,
tier: z.tier,
generation: z.generation,
}
}
/**
* A price per million tokens, as a person reads it. Free is stated as Free rather
* than as $0.00, because that is the fact somebody is looking for; a price nobody
* published is stated as nothing at all rather than as zero.
*
* Three decimals under a dollar and two above it: at $0.05 the second decimal is
* the whole number, and at $15 the third is noise. THAT RULE IS STATED HERE ONLY.
* A column of rates wants the money and not the unit — the unit belongs to the
* column head, said once, rather than to each of five hundred cells — so `rate`
* is the money and `formatPrice` is the money with its unit, and neither can
* round differently from the other.
*/
export function rate(usdPerMillion?: number | null): string | null {
if (usdPerMillion === null || usdPerMillion === undefined) return null
if (usdPerMillion === 0) return 'Free'
return `$${usdPerMillion.toFixed(usdPerMillion < 1 ? 3 : 2)}`
}
export function formatPrice(usdPerMillion?: number | null): string | null {
const money = rate(usdPerMillion)
return money === null || money === 'Free' ? money : `${money} / M tokens`
}
export { canonicalOrg, getOrgAndSlug, modelPagePath }
/**
* How many tokens a model holds. The routing table states no window for most
* models of ours, so the build joins the price list's in (lib/models/served):
* by the time a page holds a row, `context` is already the whole answer, and
* the spec's is what the Zen release states where nothing else does.
*/
export function context(m: Pick<ModelData, 'context' | 'spec'>): number {
return m.context ?? m.spec?.context ?? 0
}
export function formatContext(ctx: number | undefined): string {
if (!ctx) return 'N/A'
if (ctx >= 1_000_000) return `${Math.round(ctx / 1_000_000)}M`
if (ctx >= 1_000) return `${Math.round(ctx / 1000)}K`
return String(ctx)
}
// Monochrome: modality badges are distinguished by their label, not by hue.
//
// `MODALITY_STYLES` used to live here — seven keys, every one answering the
// same pair of utility classes, so a lookup table was deciding a single value.
// Worse, its three readers each wrote `?? { bg: 'bg-secondary', text:
// 'text-muted-foreground' }`, so an unlisted modality rendered in a DIFFERENT
// grey than a listed one and nothing said which. The chip states the value now:
// `components/models/kit`'s `Modality`.