import "server-only"; import { sql } from "../db"; import type { AIModel, Provider } from "./types"; type Row = { id: string; provider: Provider; display_name: string; kind: string; api: AIModel["api"]; status: AIModel["status"]; available: boolean; tested: boolean; unavailable_reason: string | null; capabilities: AIModel["capabilities"]; context_window: number | null; max_output_tokens: number | null; pricing: AIModel["pricing"]; benchmark: AIModel["benchmark"]; hidden: boolean; favorite: boolean; last_verified_at: Date | null; }; function rowToModel(r: Row): AIModel { return { id: r.id, provider: r.provider, displayName: r.display_name, kind: r.kind, api: r.api, status: r.status, available: r.available, tested: r.tested, unavailableReason: r.unavailable_reason, capabilities: r.capabilities ?? {}, contextWindow: r.context_window, maxOutputTokens: r.max_output_tokens, pricing: r.pricing, benchmark: r.benchmark, hidden: r.hidden, favorite: r.favorite, lastVerifiedAt: r.last_verified_at?.toISOString() ?? null, }; } export async function listModels(opts: { includeHidden?: boolean; onlyChat?: boolean } = {}): Promise { const rows = await sql` SELECT * FROM models WHERE (${opts.includeHidden ?? false} OR hidden = false) AND (${!(opts.onlyChat ?? false)} OR kind IN ('chat','chat-pro','search')) ORDER BY provider, favorite DESC, (benchmark->>'qualityScore')::int DESC NULLS LAST, sort_order, id`; return rows.map(rowToModel); } export async function getModel(id: string): Promise { const rows = await sql`SELECT * FROM models WHERE id = ${id}`; return rows[0] ? rowToModel(rows[0]) : null; } export async function requireChatModel(id: string): Promise { const m = await getModel(id); if (!m) throw Object.assign(new Error(`Unknown model ${id}`), { status: 400 }); if (!m.available || !["chat", "chat-pro", "search"].includes(m.kind)) throw Object.assign(new Error(`Model ${id} is not available for chat (${m.unavailableReason ?? m.status})`), { status: 400 }); return m; } export async function updateModelFlags(id: string, flags: { hidden?: boolean; favorite?: boolean }) { await sql`UPDATE models SET hidden = COALESCE(${flags.hidden ?? null}, hidden), favorite = COALESCE(${flags.favorite ?? null}, favorite), updated_at = now() WHERE id = ${id}`; } export interface RegistryEntryInput extends Omit { hidden?: boolean } /** Upsert registry entries (from the benchmark capability matrix or a live refresh). Preserves owner flags. */ export async function upsertModels(entries: RegistryEntryInput[]) { for (const e of entries) { 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 (${e.id}, ${e.provider}, ${e.displayName}, ${e.kind}, ${e.api ?? null}, ${e.status}, ${e.available}, ${e.tested}, ${e.unavailableReason ?? null}, ${sql.json(e.capabilities as never)}, ${e.contextWindow ?? null}, ${e.maxOutputTokens ?? null}, ${e.pricing ? sql.json(e.pricing as never) : null}, ${e.benchmark ? sql.json(e.benchmark as never) : null}, ${e.hidden ?? !e.available}, ${e.lastVerifiedAt ? new Date(e.lastVerifiedAt) : null}, now()) ON CONFLICT (id) DO UPDATE SET provider = EXCLUDED.provider, 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()`; } } /** Mark models that disappeared from the provider listing as unavailable. */ export async function markMissing(provider: Provider, presentIds: string[]) { await sql`UPDATE models SET available = false, status = 'unavailable', unavailable_reason = 'No longer returned by the provider model listing', hidden = true, updated_at = now() WHERE provider = ${provider} AND id <> ALL(${presentIds}) AND available = true`; } export function displayNameFor(id: string, provider: Provider, given?: string): string { if (given) return given; if (provider === "openai") return id.replace(/^gpt-/, "GPT-").replace(/^o(\d)/, "o$1").replace(/-(\d{4}-\d{2}-\d{2})$/, " ($1)").replace(/-/g, " ").replace(/\bmini\b/, "mini").replace(/\bpro\b/, "Pro"); return id; }