spb/chat-spboucher-ai
Public
1import "server-only";2import { sql } from "../db";3import type { AIModel, Provider } from "./types";45type Row = {6 id: string; provider: Provider; display_name: string; kind: string; api: AIModel["api"]; status: AIModel["status"]; available: boolean; tested: boolean;7 unavailable_reason: string | null; capabilities: AIModel["capabilities"]; context_window: number | null; max_output_tokens: number | null;8 pricing: AIModel["pricing"]; benchmark: AIModel["benchmark"]; hidden: boolean; favorite: boolean; last_verified_at: Date | null;9};1011function rowToModel(r: Row): AIModel {12 return {13 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,14 unavailableReason: r.unavailable_reason, capabilities: r.capabilities ?? {}, contextWindow: r.context_window, maxOutputTokens: r.max_output_tokens,15 pricing: r.pricing, benchmark: r.benchmark, hidden: r.hidden, favorite: r.favorite, lastVerifiedAt: r.last_verified_at?.toISOString() ?? null,16 };17}1819export async function listModels(opts: { includeHidden?: boolean; onlyChat?: boolean } = {}): Promise<AIModel[]> {20 const rows = await sql<Row[]>`21 SELECT * FROM models22 WHERE (${opts.includeHidden ?? false} OR hidden = false)23 AND (${!(opts.onlyChat ?? false)} OR kind IN ('chat','chat-pro','search'))24 ORDER BY provider, favorite DESC, (benchmark->>'qualityScore')::int DESC NULLS LAST, sort_order, id`;25 return rows.map(rowToModel);26}2728export async function getModel(id: string): Promise<AIModel | null> {29 const rows = await sql<Row[]>`SELECT * FROM models WHERE id = ${id}`;30 return rows[0] ? rowToModel(rows[0]) : null;31}3233export async function requireChatModel(id: string): Promise<AIModel> {34 const m = await getModel(id);35 if (!m) throw Object.assign(new Error(`Unknown model ${id}`), { status: 400 });36 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 });37 return m;38}3940export async function updateModelFlags(id: string, flags: { hidden?: boolean; favorite?: boolean }) {41 await sql`UPDATE models SET hidden = COALESCE(${flags.hidden ?? null}, hidden), favorite = COALESCE(${flags.favorite ?? null}, favorite), updated_at = now() WHERE id = ${id}`;42}4344export interface RegistryEntryInput extends Omit<AIModel, "hidden" | "favorite"> { hidden?: boolean }4546/** Upsert registry entries (from the benchmark capability matrix or a live refresh). Preserves owner flags. */47export async function upsertModels(entries: RegistryEntryInput[]) {48 for (const e of entries) {49 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)50 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())51 ON CONFLICT (id) DO UPDATE SET provider = EXCLUDED.provider, display_name = EXCLUDED.display_name, kind = EXCLUDED.kind, api = COALESCE(EXCLUDED.api, models.api),52 status = EXCLUDED.status, available = EXCLUDED.available, tested = EXCLUDED.tested OR models.tested, unavailable_reason = EXCLUDED.unavailable_reason,53 capabilities = EXCLUDED.capabilities, context_window = COALESCE(EXCLUDED.context_window, models.context_window), max_output_tokens = COALESCE(EXCLUDED.max_output_tokens, models.max_output_tokens),54 pricing = COALESCE(EXCLUDED.pricing, models.pricing), benchmark = COALESCE(EXCLUDED.benchmark, models.benchmark),55 hidden = CASE WHEN EXCLUDED.available = false THEN true ELSE models.hidden END,56 last_verified_at = COALESCE(EXCLUDED.last_verified_at, models.last_verified_at), updated_at = now()`;57 }58}5960/** Mark models that disappeared from the provider listing as unavailable. */61export async function markMissing(provider: Provider, presentIds: string[]) {62 await sql`UPDATE models SET available = false, status = 'unavailable', unavailable_reason = 'No longer returned by the provider model listing', hidden = true, updated_at = now()63 WHERE provider = ${provider} AND id <> ALL(${presentIds}) AND available = true`;64}6566export function displayNameFor(id: string, provider: Provider, given?: string): string {67 if (given) return given;68 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");69 return id;70}71