SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
8.8 KB · 213 lines typescript
Raw Blame History
1import "server-only";2import { and, desc, eq, inArray, sql } from "drizzle-orm";3import { getDb, models, modelSyncRuns, userModelFavorites, userModelRecents, type ModelRow } from "@/db";4import { ids } from "@/lib/ids";5import { log } from "@/lib/log";6import { PROVIDER_IDS, type PolyModel, type ProviderId } from "@/lib/ai/core/types";7import { getAdapter } from "@/lib/ai/providers";8import { getCatalog } from "./catalog";9import { ownerKey } from "@/lib/ai/providers/env-keys";1011export function rowToModel(r: ModelRow): PolyModel {12  return {13    key: r.key,14    id: r.modelId,15    provider: r.provider,16    displayName: r.displayName,17    family: r.family ?? undefined,18    capabilities: r.capabilities,19    limits: r.limits,20    parameters: r.parameters,21    status: r.status,22    pricing: r.pricing ?? null,23    metadata: { ...r.metadata, sortWeight: r.sortWeight, source: r.source, firstSeenAt: r.firstSeenAt.toISOString(), lastSeenAt: r.lastSeenAt.toISOString() },24  };25}2627/** Merge discovered models (from the provider API) with the documented catalog (pricing, limits, quirks). */28export function mergeWithCatalog(discovered: PolyModel[], provider: ProviderId): PolyModel[] {29  const catalog = getCatalog(provider);30  const byId = new Map(discovered.map((m) => [m.id, m]));31  const merged: PolyModel[] = [];32  for (const m of discovered) {33    const c = catalog.get(m.id) ?? catalog.get(m.id.replace(/-\d{4}-\d{2}-\d{2}$|-\d{8}$/, ""));34    if (!c) {35      merged.push({ ...m, metadata: { ...m.metadata, source: "discovery" } });36      continue;37    }38    merged.push({39      ...m,40      displayName: c.displayName ?? m.displayName,41      family: m.family ?? c.family,42      capabilities: { ...c.capabilities, ...m.capabilities, ...(c.capabilityOverrides ?? {}) },43      limits: { ...(c.limits ?? {}), ...(m.limits ?? {}) },44      parameters: { ...(c.parameters ?? {}), ...m.parameters, ...(c.parameterOverrides ?? {}) },45      pricing: m.pricing ?? c.pricing ?? null,46      status: c.status ?? m.status,47      metadata: { ...(c.metadata ?? {}), ...m.metadata, source: "merged", sortWeight: c.sortWeight ?? (m.metadata?.sortWeight as number | undefined) ?? 0 },48    });49  }50  // Catalog-only entries (documented but not returned by this key) are not added: the key can't use them.51  void byId;52  return merged;53}5455export interface SyncResult {56  provider: ProviderId;57  ok: boolean;58  found: number;59  added: number;60  removed: number;61  latencyMs: number;62  error?: string;63}6465/**66 * Sync one provider's model list into the registry. Models not seen anymore are kept but67 * marked `hidden` (so historical usage keeps its metadata) unless they reappear.68 */69export async function syncProvider(provider: ProviderId, apiKey: string, triggeredBy = "schedule"): Promise<SyncResult> {70  const db = getDb();71  const runId = ids.sync();72  const t0 = Date.now();73  await db.insert(modelSyncRuns).values({ id: runId, provider, triggeredBy });74  try {75    const discovered = await getAdapter(provider).listModels(apiKey);76    const merged = mergeWithCatalog(discovered, provider);77    const existing = await db.select({ key: models.key, hidden: models.hidden }).from(models).where(eq(models.provider, provider));78    const existingKeys = new Set(existing.map((e) => e.key));79    const seen = new Set<string>();80    let added = 0;81    const now = new Date();82    for (const m of merged) {83      seen.add(m.key);84      if (!existingKeys.has(m.key)) added++;85      const { sortWeight, source, ...meta } = (m.metadata ?? {}) as Record<string, unknown>;86      await db87        .insert(models)88        .values({89          key: m.key,90          provider,91          modelId: m.id,92          displayName: m.displayName,93          family: m.family ?? null,94          capabilities: m.capabilities,95          limits: m.limits ?? {},96          parameters: m.parameters,97          pricing: m.pricing ?? null,98          status: m.status,99          source: (source as string) ?? "discovery",100          hidden: false,101          sortWeight: Number(sortWeight ?? 0),102          metadata: meta,103          firstSeenAt: now,104          lastSeenAt: now,105          updatedAt: now,106        })107        .onConflictDoUpdate({108          target: models.key,109          set: {110            displayName: m.displayName,111            family: m.family ?? null,112            capabilities: m.capabilities,113            limits: m.limits ?? {},114            parameters: m.parameters,115            pricing: m.pricing ?? null,116            status: m.status,117            source: (source as string) ?? "discovery",118            hidden: false,119            sortWeight: Number(sortWeight ?? 0),120            metadata: meta,121            lastSeenAt: now,122            updatedAt: now,123          },124        });125    }126    const gone = existing.filter((e) => !seen.has(e.key) && !e.hidden).map((e) => e.key);127    if (gone.length) await db.update(models).set({ hidden: true, updatedAt: now }).where(inArray(models.key, gone));128    const latencyMs = Date.now() - t0;129    await db.update(modelSyncRuns).set({ finishedAt: new Date(), ok: true, modelsFound: merged.length, modelsAdded: added, modelsRemoved: gone.length, latencyMs }).where(eq(modelSyncRuns.id, runId));130    log.info("model sync ok", { provider, found: merged.length, added, removed: gone.length, latencyMs, triggeredBy });131    return { provider, ok: true, found: merged.length, added, removed: gone.length, latencyMs };132  } catch (e) {133    const err = e as { code?: string; message?: string };134    const latencyMs = Date.now() - t0;135    await db.update(modelSyncRuns).set({ finishedAt: new Date(), ok: false, latencyMs, errorCode: err.code ?? "UNKNOWN", errorMessage: (err.message ?? String(e)).slice(0, 500) }).where(eq(modelSyncRuns.id, runId));136    log.warn("model sync failed", { provider, code: err.code, error: err.message, triggeredBy });137    return { provider, ok: false, found: 0, added: 0, removed: 0, latencyMs, error: err.message };138  }139}140141/** Sync every provider for which an owner/dev key exists in the environment. */142export async function syncAllWithEnvKeys(triggeredBy = "schedule"): Promise<SyncResult[]> {143  const results: SyncResult[] = [];144  await Promise.all(145    PROVIDER_IDS.map(async (p) => {146      const k = ownerKey(p);147      if (!k) return;148      results.push(await syncProvider(p, k, triggeredBy));149    }),150  );151  return results;152}153154export async function listRegistryModels(opts: { provider?: ProviderId; includeHidden?: boolean; includeDeprecated?: boolean } = {}): Promise<PolyModel[]> {155  const db = getDb();156  const conds = [];157  if (opts.provider) conds.push(eq(models.provider, opts.provider));158  if (!opts.includeHidden) conds.push(eq(models.hidden, false));159  const rows = await db160    .select()161    .from(models)162    .where(conds.length ? and(...conds) : undefined)163    .orderBy(desc(models.sortWeight), models.displayName);164  return rows.filter((r) => opts.includeDeprecated || r.status !== "deprecated").map(rowToModel);165}166167export async function getModel(key: string): Promise<PolyModel | null> {168  const [row] = await getDb().select().from(models).where(eq(models.key, key)).limit(1);169  return row ? rowToModel(row) : null;170}171172export async function registryStats() {173  const db = getDb();174  const counts = await db175    .select({ provider: models.provider, total: sql<number>`count(*)::int`, hidden: sql<number>`sum(case when ${models.hidden} then 1 else 0 end)::int` })176    .from(models)177    .groupBy(models.provider);178  const lastRuns = await db.select().from(modelSyncRuns).orderBy(desc(modelSyncRuns.startedAt)).limit(40);179  return { counts, lastRuns };180}181182export async function favoriteModels(userId: string): Promise<string[]> {183  const rows = await getDb().select({ key: userModelFavorites.modelKey }).from(userModelFavorites).where(eq(userModelFavorites.userId, userId));184  return rows.map((r) => r.key);185}186187export async function toggleFavorite(userId: string, key: string): Promise<boolean> {188  const db = getDb();189  const existing = await db190    .select()191    .from(userModelFavorites)192    .where(and(eq(userModelFavorites.userId, userId), eq(userModelFavorites.modelKey, key)))193    .limit(1);194  if (existing.length) {195    await db.delete(userModelFavorites).where(and(eq(userModelFavorites.userId, userId), eq(userModelFavorites.modelKey, key)));196    return false;197  }198  await db.insert(userModelFavorites).values({ userId, modelKey: key });199  return true;200}201202export async function recentModels(userId: string, limit = 8): Promise<string[]> {203  const rows = await getDb().select({ key: userModelRecents.modelKey }).from(userModelRecents).where(eq(userModelRecents.userId, userId)).orderBy(desc(userModelRecents.usedAt)).limit(limit);204  return rows.map((r) => r.key);205}206207export async function touchRecent(userId: string, key: string) {208  await getDb()209    .insert(userModelRecents)210    .values({ userId, modelKey: key })211    .onConflictDoUpdate({ target: [userModelRecents.userId, userModelRecents.modelKey], set: { usedAt: new Date(), uses: sql`${userModelRecents.uses} + 1` } });212}213