SPB Git
6.7 KB · 85 lines typescript
1/**2 * Refreshes the model registry in the database:3 *  1. live listing from both providers (what the account can see today),4 *  2. merges benchmarks/capability-matrix.json (verified capabilities, benchmarks, prices),5 *  3. marks ids that disappeared as unavailable, flags brand-new ids as untested.6 * Safe to run on a schedule. Does not run inference.7 */8import OpenAI from "openai";9import Anthropic from "@anthropic-ai/sdk";10import fs from "node:fs";11import path from "node:path";12import postgres from "postgres";1314const url = process.env.DATABASE_URL; if (!url) { console.error("DATABASE_URL required"); process.exit(1); }15const sql = postgres(url, { max: 1, onnotice: () => {} });16const benchDir = process.env.BENCHMARKS_DIR ?? "benchmarks";1718function displayName(id: string, provider: string, given?: string) {19  if (given) return given;20  if (provider === "openai") return id.replace(/^gpt-/, "GPT-").replace(/-(\d{4}-\d{2}-\d{2})$/, " ($1)").replace(/-/g, " ").replace(/\bpro\b/, "Pro");21  return id;22}2324async function main() {25  const matrix = fs.existsSync(path.join(benchDir, "capability-matrix.json")) ? JSON.parse(fs.readFileSync(path.join(benchDir, "capability-matrix.json"), "utf8")).entries as any[] : [];26  const byId = new Map(matrix.map(e => [e.id, e]));27  const pricing = JSON.parse(fs.readFileSync(path.join(benchDir, "pricing.json"), "utf8"));2829  const live: { id: string; provider: "openai" | "anthropic"; display?: string; ctx?: number; out?: number; caps?: any }[] = [];30  const present: Record<string, string[]> = { openai: [], anthropic: [] };31  if (process.env.OPENAI_API_KEY) {32    try { for await (const m of (await new OpenAI().models.list())) { live.push({ id: m.id, provider: "openai" }); present.openai.push(m.id); } }33    catch (e: any) { console.error("openai listing failed:", e.message); }34  }35  if (process.env.ANTHROPIC_API_KEY) {36    try { for await (const m of new Anthropic().models.list()) { const a = m as any; live.push({ id: m.id, provider: "anthropic", display: m.display_name, ctx: a.max_input_tokens, out: a.max_tokens, caps: a.capabilities }); present.anthropic.push(m.id); } }37    catch (e: any) { console.error("anthropic listing failed:", e.message); }38  }3940  let upserted = 0, fresh = 0;41  for (const m of live) {42    const e = byId.get(m.id);43    const p = pricing[m.provider][m.id] ?? pricing[m.provider][m.id.replace(/-\d{8}$/, "")] ?? null;44    const caps = e?.capabilities ?? {};45    if (m.caps) { // Anthropic Models API capability tree46      if (caps.vision === undefined || caps.vision === "unknown") caps.vision = !!m.caps.image_input?.supported;47      if (caps.structuredOutput === undefined || caps.structuredOutput === "unknown") caps.structuredOutput = !!m.caps.structured_outputs?.supported;48      caps.reasoningNative = !!m.caps.thinking?.supported;49    }50    const status = e?.status ?? "untested";51    const available = e ? e.available : false;52    if (!e) fresh++;53    await sql`INSERT INTO models (id, provider, display_name, kind, api, status, available, tested, unavailable_reason, capabilities, context_window, max_output_tokens, pricing, benchmark, hidden, last_verified_at, updated_at)54      VALUES (${m.id}, ${m.provider}, ${displayName(m.id, m.provider, m.display)}, ${e?.kind ?? guessKind(m.id)}, ${e?.api ?? null}, ${status}, ${available}, ${!!e?.tested}, ${e?.unavailableReason ?? (e ? null : "Listed by the provider but not yet tested — run the benchmark to verify")},55              ${sql.json(caps)}, ${m.ctx ?? e?.contextWindow ?? null}, ${m.out ?? e?.maxOutputTokens ?? null}, ${p ? sql.json(p) : null}, ${e?.benchmark ? sql.json(e.benchmark) : null}, ${!available}, ${e?.lastVerifiedAt ? new Date(e.lastVerifiedAt) : null}, now())56      ON CONFLICT (id) DO UPDATE SET display_name = EXCLUDED.display_name, kind = EXCLUDED.kind, api = COALESCE(EXCLUDED.api, models.api), status = EXCLUDED.status, available = EXCLUDED.available,57        tested = EXCLUDED.tested OR models.tested, unavailable_reason = EXCLUDED.unavailable_reason, capabilities = EXCLUDED.capabilities,58        context_window = COALESCE(EXCLUDED.context_window, models.context_window), max_output_tokens = COALESCE(EXCLUDED.max_output_tokens, models.max_output_tokens),59        pricing = COALESCE(EXCLUDED.pricing, models.pricing), benchmark = COALESCE(EXCLUDED.benchmark, models.benchmark),60        hidden = CASE WHEN EXCLUDED.available = false THEN true ELSE models.hidden END, last_verified_at = COALESCE(EXCLUDED.last_verified_at, models.last_verified_at), updated_at = now()`;61    upserted++;62  }63  for (const prov of ["openai", "anthropic"] as const) if (present[prov].length)64    await sql`UPDATE models SET available = false, status = 'unavailable', hidden = true, unavailable_reason = 'No longer returned by the provider model listing', updated_at = now() WHERE provider = ${prov} AND available = true AND id <> ALL(${present[prov]})`;65  // default chat model: Claude Opus 5.5 when verified (100 % on the suite, mid price), else the economy/quality fallback is chosen in-app66  const cur = await sql<{ value: { defaultModel?: string } }[]>`SELECT value FROM settings WHERE key = 'models'`;67  const def = cur[0]?.value?.defaultModel;68  const defOk = def ? (await sql<{ ok: boolean }[]>`SELECT available AS ok FROM models WHERE id = ${def}`)[0]?.ok : false;69  if (!defOk) {70    const pick = (await sql<{ id: string }[]>`SELECT id FROM models WHERE available AND kind = 'chat' ORDER BY (id = 'claude-opus-5-5') DESC, (benchmark->>'qualityScore')::int DESC NULLS LAST, (pricing->>'input')::float ASC LIMIT 1`)[0]?.id;71    if (pick) await sql`INSERT INTO settings (key, value, updated_at) VALUES ('models', ${sql.json({ ...(cur[0]?.value ?? {}), defaultModel: pick })}, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`;72  }73  await sql`INSERT INTO settings (key, value, updated_at) VALUES ('registry', ${sql.json({ refreshedAt: new Date().toISOString(), live: live.length, upserted, fresh })}, now()) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`;74  console.log(`registry: ${upserted} models upserted (${fresh} new/untested), openai=${present.openai.length} anthropic=${present.anthropic.length}`);75  await sql.end();76}7778function guessKind(id: string) {79  if (/^text-embedding/.test(id)) return "embedding"; if (/moderation/.test(id)) return "moderation"; if (/^sora/.test(id)) return "video";80  if (/realtime|^gpt-live/.test(id)) return "realtime"; if (/^gpt-image|chatgpt-image/.test(id)) return "image"; if (/tts/.test(id)) return "tts";81  if (/whisper|transcribe/.test(id)) return "stt"; if (/^gpt-audio/.test(id)) return "audio-chat"; if (/deep-research/.test(id)) return "deep-research";82  if (/-pro$/.test(id)) return "chat-pro"; return "chat";83}84main().catch(e => { console.error(e); process.exit(1); });85
Files · laptop-build-20261101
Menu

Theme