import type { PolyModel, ProviderId } from "@/lib/client/types"; /** A model retiring within 90 days (metadata.shutdownDate) is treated like a deprecation warning. */ export const RETIRE_SOON_MS = 90 * 86_400_000; export interface DeprecationNotice { kind: "deprecated" | "retiring"; shutdownDate: string | null; } export function deprecationNotice(model: PolyModel | null | undefined, now: number): DeprecationNotice | null { if (!model) return null; const shutdown = typeof model.metadata?.shutdownDate === "string" ? (model.metadata.shutdownDate as string) : null; if (model.status === "deprecated") return { kind: "deprecated", shutdownDate: shutdown }; if (shutdown) { const t = new Date(shutdown).getTime(); if (!Number.isNaN(t) && t - now < RETIRE_SOON_MS) return { kind: "retiring", shutdownDate: shutdown }; } return null; } function weight(m: PolyModel): number { const w = Number((m.metadata as { sortWeight?: unknown } | undefined)?.sortWeight ?? 0); return Number.isFinite(w) ? w : 0; } function releaseTime(m: PolyModel): number { const r = (m.metadata as { releaseDate?: unknown } | undefined)?.releaseDate; const t = typeof r === "string" ? new Date(r).getTime() : NaN; return Number.isNaN(t) ? 0 : t; } /** * Newest active model from the same provider that the user can actually use (connected), preferring * the same family and models that cover the deprecated model's capabilities. Never returns the model itself. */ export function suggestReplacement(model: PolyModel, models: PolyModel[], connected?: Set): PolyModel | null { const candidates = models.filter((m) => m.key !== model.key && m.provider === model.provider && m.status === "active" && (!connected || connected.has(m.provider)) && !(typeof m.metadata?.shutdownDate === "string")); if (!candidates.length) return null; const score = (m: PolyModel) => { let s = 0; if (model.family && m.family === model.family) s += 100; if (model.capabilities.vision && m.capabilities.vision) s += 5; if (model.capabilities.reasoning && m.capabilities.reasoning) s += 5; if (model.capabilities.tools && m.capabilities.tools) s += 3; if ((m.limits?.contextTokens ?? 0) >= (model.limits?.contextTokens ?? 0)) s += 2; return s; }; return [...candidates].sort((a, b) => score(b) - score(a) || releaseTime(b) - releaseTime(a) || weight(b) - weight(a) || a.displayName.localeCompare(b.displayName))[0] ?? null; } /** Connected model with the largest context window that is strictly larger than the current one. */ export function largerContextModel(current: PolyModel | null | undefined, models: PolyModel[], connected: Set): PolyModel | null { const cur = current?.limits?.contextTokens ?? 0; const usable = models.filter((m) => connected.has(m.provider) && m.status !== "deprecated" && (m.limits?.contextTokens ?? 0) > cur && m.key !== current?.key); if (!usable.length) return null; // Prefer the same provider when it has a bigger model; otherwise the largest overall. const same = usable.filter((m) => m.provider === current?.provider).sort((a, b) => (b.limits?.contextTokens ?? 0) - (a.limits?.contextTokens ?? 0)); if (same.length) return same[0]; return usable.sort((a, b) => (b.limits?.contextTokens ?? 0) - (a.limits?.contextTokens ?? 0))[0]; }