Files
zeekay edb6963531 Plans, prices and the model table are read from api.hanzo.ai when a page opens, and no page carries a copy
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.
2026-10-15 11:07:56 -07:00

94 lines
4.3 KiB
TypeScript

import { test, expect } from '@playwright/test'
import {
context,
formatPrice,
getOrgAndSlug,
canonicalOrg,
modelPagePath,
formatContext,
orgDisplayName,
} from '@/lib/models'
import { distinct } from '@/lib/models/distinct'
import { catalog } from '@/lib/models/catalog'
import { toPricing, toServed } from '@/lib/api/read'
import { MODELS, PRICING } from '../gates/api'
test.describe('models utility unit tests', () => {
test('formatPrice formats null, free, micro-usd and standard usd properly', () => {
expect(formatPrice(null)).toBeNull()
expect(formatPrice(undefined)).toBeNull()
expect(formatPrice(0)).toBe('Free')
expect(formatPrice(0.123)).toBe('$0.123 / M tokens')
expect(formatPrice(0.05)).toBe('$0.050 / M tokens')
expect(formatPrice(2.5)).toBe('$2.50 / M tokens')
expect(formatPrice(15)).toBe('$15.00 / M tokens')
})
test('getOrgAndSlug splits namespaces cleanly', () => {
expect(getOrgAndSlug('openai/gpt-4o')).toEqual({ org: 'openai', slug: 'gpt-4o' })
expect(getOrgAndSlug('meta-llama/llama-3-70b')).toEqual({ org: 'meta-llama', slug: 'llama-3-70b' })
expect(getOrgAndSlug('zen-3')).toEqual({ org: 'hanzo', slug: 'zen-3' })
expect(getOrgAndSlug('foo/bar/baz')).toEqual({ org: 'foo', slug: 'bar/baz' })
})
test('canonicalOrg folds aliases and strips leading ~', () => {
expect(canonicalOrg('~openai')).toBe('openai')
expect(canonicalOrg('~meta-llama')).toBe('meta')
expect(canonicalOrg('meta-llama')).toBe('meta')
expect(canonicalOrg('bytedance-seed')).toBe('bytedance')
expect(canonicalOrg('hanzo')).toBe('hanzo')
expect(canonicalOrg('anthropic')).toBe('anthropic')
expect(canonicalOrg('google')).toBe('google')
})
test('modelPagePath constructs canonical URLs', () => {
expect(modelPagePath('openai/gpt-4o')).toBe('/models/openai/gpt-4o')
expect(modelPagePath('meta-llama/llama-3-70b')).toBe('/models/meta/llama-3-70b')
expect(modelPagePath('~meta-llama/llama-3-8b')).toBe('/models/meta/llama-3-8b')
expect(modelPagePath('zen-nano')).toBe('/models/hanzo/zen-nano')
})
test('formatContext handles numbers and abbreviations', () => {
expect(formatContext(undefined)).toBe('N/A')
expect(formatContext(0)).toBe('N/A')
expect(formatContext(512)).toBe('512')
expect(formatContext(8192)).toBe('8K')
expect(formatContext(128000)).toBe('128K')
expect(formatContext(1000000)).toBe('1M')
expect(formatContext(2000000)).toBe('2M')
})
test('orgDisplayName maps aliases to human-readable names', () => {
expect(orgDisplayName('hanzo')).toBe('Hanzo')
expect(orgDisplayName('openai')).toBe('OpenAI')
expect(orgDisplayName('anthropic')).toBe('Anthropic')
expect(orgDisplayName('unknown-org')).toBe('unknown-org')
})
test('distinct keeps one row per model page, under its bare id', () => {
// The gateway lists Kai and Enso under the bare id and the canonical id, and
// both are one page. Two Jev ids are two pages, and so two models.
const ids = ['hanzo/kai', 'kai', 'enso', 'hanzo/enso', 'typesafe/jev-1.13', '~typesafe/jev-latest', 'openai/gpt-5']
expect(distinct(ids, (id) => id)).toEqual(['kai', 'enso', 'typesafe/jev-1.13', '~typesafe/jev-latest', 'openai/gpt-5'])
})
test('the catalogue keeps one row per model page, ours at the price list rate and a lab at its own', () => {
const rows = catalog(toServed(MODELS)!, toPricing(PRICING)!)
const pages = rows.map((r) => r.href)
expect(pages.filter((p, i) => pages.indexOf(p) !== i), 'a page listed twice').toEqual([])
expect(rows.find((r) => r.id === 'hanzo/kai'), 'kai and hanzo/kai are one row').toBeUndefined()
const enso = rows.find((r) => r.id === 'enso')!
expect([enso.lab, enso.in, enso.out]).toEqual(['hanzo', 7, 31])
const opus = rows.find((r) => r.id === 'anthropic/claude-opus-4.6')!
expect([opus.lab, opus.in, opus.out], "a lab's rate is the routing table's, not the price list's").toEqual(['anthropic', 6.4, 30.6])
expect(rows.find((r) => r.id === 'gpt-5')!.lab, 'a bare name no one claims is an alias').toBe('alias')
expect(rows[0].lab, 'ours lead').toBe('hanzo')
})
test('context is the joined window, then the spec, then none', () => {
expect(context({ context: 262_144 })).toBe(262_144)
expect(context({ spec: { context: 32_768 } })).toBe(32_768)
expect(context({})).toBe(0)
})
})