mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
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.
59 lines
2.9 KiB
JavaScript
59 lines
2.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* The catalogue the model pages are built from, as api.hanzo.ai serves it.
|
|
*
|
|
* node scripts/sync-models.mjs [api base URL]
|
|
*
|
|
* Saves what lib/models/served.ts reads as lib/data/served.json: each row of
|
|
* GET /v1/models (its id, name, words, family, window and outputs, in the
|
|
* gateway's order) and each of our own models on GET /v1/pricing (its name,
|
|
* words, window, specs, features, tier and links). No rate is kept, and no
|
|
* time: a page reads its price when it opens, so a catalogue that changed only
|
|
* its prices writes the same file, the build renders the same pages, and the
|
|
* publish uploads none of them.
|
|
*
|
|
* Part of `prebuild`. The committed file is what the build renders when the API
|
|
* cannot answer, so a blip at api.hanzo.ai no longer fails a deploy: an
|
|
* unreachable API, an error, a body that is not the document, or a model list
|
|
* under half the snapshot's (a degraded read, not a retirement) keeps the file
|
|
* byte for byte, and the build goes on.
|
|
*/
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
|
const FILE = new URL('../lib/data/served.json', import.meta.url)
|
|
const API = (process.argv[2] ?? 'https://api.hanzo.ai').replace(/\/$/, '')
|
|
const SERVED = ['id', 'name', 'description', 'owned_by', 'family', 'context_window', 'outputs']
|
|
const OWN = ['name', 'fullName', 'description', 'features', 'tier', 'context', 'specs', 'links']
|
|
|
|
const keep = (why) => {
|
|
console.log(`sync-models: kept the committed catalogue (${why})`)
|
|
process.exit(0)
|
|
}
|
|
|
|
const read = async (path) => {
|
|
const res = await fetch(`${API}${path}`, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(30_000) })
|
|
if (!res.ok) keep(`${path}: ${res.status}`)
|
|
return res.json()
|
|
}
|
|
|
|
const pick = (row, keys) => Object.fromEntries(keys.filter((k) => row[k] != null).map((k) => [k, row[k]]))
|
|
|
|
try {
|
|
const [models, pricing] = await Promise.all([read('/v1/models'), read('/v1/pricing')])
|
|
const served = Array.isArray(models?.data) ? models.data : []
|
|
const own = Array.isArray(pricing?.hanzoModels) ? pricing.hanzoModels : []
|
|
if (!served.length || !served.every((r) => typeof r?.id === 'string')) keep('/v1/models: not the model list')
|
|
if (!own.length || !own.every((r) => typeof r?.name === 'string')) keep('/v1/pricing: not the price list')
|
|
const held = JSON.parse(readFileSync(FILE, 'utf8'))
|
|
if (served.length < held.served.length / 2) keep(`/v1/models lists ${served.length} models where the snapshot holds ${held.served.length}`)
|
|
const text = `${JSON.stringify({ served: served.map((r) => pick(r, SERVED)), own: own.map((r) => pick(r, OWN)) }, null, 1)}\n`
|
|
if (text === readFileSync(FILE, 'utf8')) {
|
|
console.log(`sync-models: ${served.length} models, unchanged`)
|
|
} else {
|
|
writeFileSync(FILE, text)
|
|
console.log(`sync-models: ${served.length} models (the snapshot held ${held.served.length}), written`)
|
|
}
|
|
} catch (e) {
|
|
keep(e instanceof Error ? e.message : String(e))
|
|
}
|