/** * Refreshes the model registry in the database: * 1. live listing from both providers (what the account can see today), * 2. merges benchmarks/capability-matrix.json (verified capabilities, benchmarks, prices), * 3. marks ids that disappeared as unavailable, flags brand-new ids as untested. * Safe to run on a schedule. Does not run inference. */ import OpenAI from "openai"; import Anthropic from "@anthropic-ai/sdk"; import fs from "node:fs"; import path from "node:path"; import postgres from "postgres"; const url = process.env.DATABASE_URL; if (!url) { console.error("DATABASE_URL required"); process.exit(1); } const sql = postgres(url, { max: 1, onnotice: () => {} }); const benchDir = process.env.BENCHMARKS_DIR ?? "benchmarks"; function displayName(id: string, provider: string, given?: string) { if (given) return given; if (provider === "openai") return id.replace(/^gpt-/, "GPT-").replace(/-(\d{4}-\d{2}-\d{2})$/, " ($1)").replace(/-/g, " ").replace(/\bpro\b/, "Pro"); return id; } async function main() { const matrix = fs.existsSync(path.join(benchDir, "capability-matrix.json")) ? JSON.parse(fs.readFileSync(path.join(benchDir, "capability-matrix.json"), "utf8")).entries as any[] : []; const byId = new Map(matrix.map(e => [e.id, e])); const pricing = JSON.parse(fs.readFileSync(path.join(benchDir, "pricing.json"), "utf8")); const live: { id: string; provider: "openai" | "anthropic"; display?: string; ctx?: number; out?: number; caps?: any }[] = []; const present: Record = { openai: [], anthropic: [] }; if (process.env.OPENAI_API_KEY) { try { for await (const m of (await new OpenAI().models.list())) { live.push({ id: m.id, provider: "openai" }); present.openai.push(m.id); } } catch (e: any) { console.error("openai listing failed:", e.message); } } if (process.env.ANTHROPIC_API_KEY) { 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); } } catch (e: any) { console.error("anthropic listing failed:", e.message); } } let upserted = 0, fresh = 0; for (const m of live) { const e = byId.get(m.id); const p = pricing[m.provider][m.id] ?? pricing[m.provider][m.id.replace(/-\d{8}$/, "")] ?? null; const caps = e?.capabilities ?? {}; if (m.caps) { // Anthropic Models API capability tree if (caps.vision === undefined || caps.vision === "unknown") caps.vision = !!m.caps.image_input?.supported; if (caps.structuredOutput === undefined || caps.structuredOutput === "unknown") caps.structuredOutput = !!m.caps.structured_outputs?.supported; caps.reasoningNative = !!m.caps.thinking?.supported; } const status = e?.status ?? "untested"; const available = e ? e.available : false; if (!e) fresh++; 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) 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")}, ${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()) 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, tested = EXCLUDED.tested OR models.tested, unavailable_reason = EXCLUDED.unavailable_reason, capabilities = EXCLUDED.capabilities, context_window = COALESCE(EXCLUDED.context_window, models.context_window), max_output_tokens = COALESCE(EXCLUDED.max_output_tokens, models.max_output_tokens), pricing = COALESCE(EXCLUDED.pricing, models.pricing), benchmark = COALESCE(EXCLUDED.benchmark, models.benchmark), 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()`; upserted++; } for (const prov of ["openai", "anthropic"] as const) if (present[prov].length) 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]})`; // default chat model: Claude Opus 5.5 when verified (100 % on the suite, mid price), else the economy/quality fallback is chosen in-app const cur = await sql<{ value: { defaultModel?: string } }[]>`SELECT value FROM settings WHERE key = 'models'`; const def = cur[0]?.value?.defaultModel; const defOk = def ? (await sql<{ ok: boolean }[]>`SELECT available AS ok FROM models WHERE id = ${def}`)[0]?.ok : false; if (!defOk) { 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; 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()`; } 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()`; console.log(`registry: ${upserted} models upserted (${fresh} new/untested), openai=${present.openai.length} anthropic=${present.anthropic.length}`); await sql.end(); } function guessKind(id: string) { if (/^text-embedding/.test(id)) return "embedding"; if (/moderation/.test(id)) return "moderation"; if (/^sora/.test(id)) return "video"; if (/realtime|^gpt-live/.test(id)) return "realtime"; if (/^gpt-image|chatgpt-image/.test(id)) return "image"; if (/tts/.test(id)) return "tts"; if (/whisper|transcribe/.test(id)) return "stt"; if (/^gpt-audio/.test(id)) return "audio-chat"; if (/deep-research/.test(id)) return "deep-research"; if (/-pro$/.test(id)) return "chat-pro"; return "chat"; } main().catch(e => { console.error(e); process.exit(1); });