import type { PolyModel } from "@/lib/ai/core/types"; import { sortWeightOf } from "./badges"; /** * URL slugs for public model pages: `/compare/gpt-5.5-vs-claude-sonnet-5`. * Dots are kept (they are the natural version separators in model ids); everything else * outside [a-z0-9] becomes a hyphen. `-vs-` is the pair delimiter. */ export const COMPARE_DELIMITER = "-vs-"; export const MAX_COMPARE = 4; export function slugify(input: string): string { return input .toLowerCase() .replace(/[^a-z0-9.]+/g, "-") .replace(/-{2,}/g, "-") .replace(/^[-.]+|[-.]+$/g, ""); } /** Canonical slug of a model: its id (e.g. `gpt-5.5`, `claude-sonnet-5`, `openai-gpt-5.5` for OpenRouter ids). */ export function modelSlug(m: Pick): string { return slugify(m.id); } export function compareSlug(models: Pick[]): string { return models.map(modelSlug).join(COMPARE_DELIMITER); } /** Split `a-vs-b[-vs-c]` into parts; null when malformed (fewer than 2 or more than MAX_COMPARE parts, empties). */ export function parseCompareSlug(slug: string): string[] | null { if (typeof slug !== "string") return null; const parts = slug.toLowerCase().split(COMPARE_DELIMITER).map((p) => p.trim()); if (parts.length < 2 || parts.length > MAX_COMPARE) return null; if (parts.some((p) => !p || !/^[a-z0-9.-]+$/.test(p))) return null; return parts; } function candidates(m: PolyModel): string[] { return [modelSlug(m), slugify(m.displayName), slugify(`${m.provider}-${m.id}`), slugify(`${m.provider} ${m.displayName}`)]; } /** * Resolve one slug part to a model. Matches id, display name, or provider-prefixed forms. * Prefers non-deprecated models on native providers (not OpenRouter), then the highest sort weight. */ export function findModelBySlug(models: PolyModel[], part: string): PolyModel | undefined { const p = part.toLowerCase(); const hits = models.filter((m) => candidates(m).includes(p)); if (!hits.length) return undefined; return hits.sort((a, b) => { const dep = Number(a.status === "deprecated") - Number(b.status === "deprecated"); if (dep) return dep; const or = Number(a.provider === "openrouter") - Number(b.provider === "openrouter"); if (or) return or; const exact = Number(modelSlug(b) === p) - Number(modelSlug(a) === p); if (exact) return exact; return sortWeightOf(b) - sortWeightOf(a); })[0]; } /** Resolve a full compare slug; null when any part is unknown or the same model appears twice. */ export function resolveCompareSlug(models: PolyModel[], slug: string): PolyModel[] | null { const parts = parseCompareSlug(slug); if (!parts) return null; const out: PolyModel[] = []; for (const part of parts) { const m = findModelBySlug(models, part); if (!m || out.some((x) => x.key === m.key)) return null; out.push(m); } return out; }