spb/note-spboucher-ai
Public
1import { db, schema } from "@/db";2import { eq } from "drizzle-orm";3import OpenAI from "openai";4import Anthropic from "@anthropic-ai/sdk";5import type { Provider } from "./types";67export type ModelRow = typeof schema.aiModels.$inferSelect;8let cache: { at: number; rows: ModelRow[] } | null = null;9export async function listModels(force = false): Promise<ModelRow[]> {10 if (!force && cache && Date.now() - cache.at < 60_000) return cache.rows;11 const rows = await db.select().from(schema.aiModels);12 cache = { at: Date.now(), rows }; return rows;13}14export async function getModel(id: string) { return (await listModels()).find((m) => m.id === id) ?? null; }15export function invalidateCatalog() { cache = null; }1617export function costUsd(m: ModelRow | null, u: { inputTokens: number; outputTokens: number; cachedTokens?: number }) {18 if (!m || m.inputPrice == null || m.outputPrice == null) return null;19 const cached = u.cachedTokens ?? 0;20 return ((u.inputTokens - cached) * m.inputPrice + cached * (m.cachedPrice ?? m.inputPrice) + u.outputTokens * m.outputPrice) / 1e6;21}2223export function providerConfigured(p: Provider) { return Boolean(p === "openai" ? process.env.OPENAI_API_KEY : process.env.ANTHROPIC_API_KEY); }2425/** Live discovery: re-list both providers and flip `available`; new ids are inserted as untested (enabled=false). */26export async function refreshDiscovery() {27 const now = new Date(); const seen = new Set<string>(); const report: Record<string, { listed: number; status: string }> = {};28 const existing = await listModels(true);29 for (const p of ["openai", "anthropic"] as Provider[]) {30 if (!providerConfigured(p)) { report[p] = { listed: 0, status: "no key" }; await db.update(schema.aiProviders).set({ status: "no_key", lastDiscoveryAt: now }).where(eq(schema.aiProviders.id, p)); continue; }31 try {32 const ids: { id: string; display?: string; created?: string }[] = [];33 if (p === "openai") { const c = new OpenAI(); for await (const m of c.models.list() as any) ids.push({ id: m.id, created: new Date(m.created * 1000).toISOString() }); }34 else { const c = new Anthropic(); for await (const m of c.models.list({ limit: 100 }) as any) ids.push({ id: m.id, display: m.display_name, created: m.created_at }); }35 for (const m of ids) {36 seen.add(m.id);37 const ex = existing.find((e) => e.id === m.id);38 if (ex) { if (!ex.available) await db.update(schema.aiModels).set({ available: true, discoveredAt: now, updatedAt: now }).where(eq(schema.aiModels.id, m.id)); }39 else if (p === "anthropic" || /^(gpt|o\d|chat-latest)/.test(m.id) && !/(image|audio|tts|transcribe|whisper|realtime|live|sora|embedding|moderation|search|deep-research|instruct|davinci|babbage)/.test(m.id)) {40 await db.insert(schema.aiModels).values({ id: m.id, provider: p, displayName: m.display ?? m.id, category: "text", available: true, enabled: false, notes: ["discovered live; not probed/benchmarked yet — run pnpm bench:discover"], discoveredAt: now }).onConflictDoNothing();41 }42 }43 for (const e of existing) if (e.provider === p && e.available && !seen.has(e.id)) await db.update(schema.aiModels).set({ available: false, updatedAt: now }).where(eq(schema.aiModels.id, e.id));44 report[p] = { listed: ids.length, status: "ok" };45 await db.update(schema.aiProviders).set({ status: "ok", statusDetail: `${ids.length} models`, lastDiscoveryAt: now }).where(eq(schema.aiProviders.id, p));46 } catch (e: any) { report[p] = { listed: 0, status: e.message }; await db.update(schema.aiProviders).set({ status: "error", statusDetail: String(e.message).slice(0, 200), lastDiscoveryAt: now }).where(eq(schema.aiProviders.id, p)); }47 }48 invalidateCatalog(); return report;49}50