/** * Claude model string parser utility. * Parses model identifiers into friendly display names and metadata. */ /** Fallback context window size when a more exact model-specific window is unavailable. */ export const DEFAULT_CONTEXT_WINDOW = 200_000; /** Known model families with specific styling */ export type KnownModelFamily = 'sonnet' | 'opus' | 'haiku'; /** Model family can be a known family or any arbitrary string for new/unknown models */ export type ModelFamily = KnownModelFamily | (string & Record); export interface ModelInfo { /** Friendly name like "sonnet4.5" */ name: string; /** Model family: sonnet, opus, haiku, or any other string for unknown families */ family: ModelFamily; /** Major version like 4 or 3 */ majorVersion: number; /** Minor version like 5 or 1 (null if not present) */ minorVersion: number | null; } const KNOWN_FAMILIES: KnownModelFamily[] = ['sonnet', 'opus', 'haiku']; /** * Parses a Claude model string into friendly display info. * Returns null if model string is invalid, synthetic, or empty. * * Supported formats: * - New format: claude-{family}-{major}-{minor}-{date} (e.g., "claude-sonnet-4-5-20250929") * - Old format: claude-{major}-{family}-{date} (e.g., "claude-3-opus-20240229") * - Old format with minor: claude-{major}-{minor}-{family}-{date} (e.g., "claude-3-5-sonnet-20241022") */ export function parseModelString(model: string | undefined): ModelInfo | null { // Handle null, undefined, empty, or synthetic models if (!model || model.trim() === '' || model === '') { return null; } const normalized = model.toLowerCase().trim(); // Must start with "claude" if (!normalized.startsWith('claude')) { return null; } // Split into parts (e.g., ["claude", "sonnet", "4", "5", "20250929"]) const parts = normalized.split('-'); if (parts.length < 3) { return null; } // Detect model family - first check known families, then accept any non-numeric string let family: ModelFamily | null = null; let familyIndex = -1; // First pass: look for known families for (let i = 1; i < parts.length; i++) { const part = parts[i]; if (KNOWN_FAMILIES.includes(part as KnownModelFamily)) { family = part as KnownModelFamily; familyIndex = i; break; } } // Second pass: if no known family found, look for any non-numeric, non-date string as family if (family === null) { for (let i = 1; i < parts.length; i++) { const part = parts[i]; // Skip numeric parts and date-like parts (8 digits) if (!/^\d+$/.test(part) && !/^\d{8}$/.test(part) && part.length > 1) { family = part; familyIndex = i; break; } } } if (family === null || familyIndex === -1) { return null; } let majorVersion: number; let minorVersion: number | null = null; // Determine format based on family position if (familyIndex === 1) { // New format: claude-{family}-{major}-{minor}-{date} // e.g., claude-sonnet-4-5-20250929 -> ["claude", "sonnet", "4", "5", "20250929"] if (parts.length < 4) { return null; } majorVersion = parseInt(parts[2], 10); if (isNaN(majorVersion)) { return null; } // Check if there's a minor version (next part is a number and not a date) if (parts.length >= 4 && parts[3].length <= 2) { const potentialMinor = parseInt(parts[3], 10); if (!isNaN(potentialMinor)) { minorVersion = potentialMinor; } } } else { // Old format: claude-{major}[-{minor}]-{family}-{date} // e.g., claude-3-opus-20240229 -> ["claude", "3", "opus", "20240229"] // e.g., claude-3-5-sonnet-20241022 -> ["claude", "3", "5", "sonnet", "20241022"] majorVersion = parseInt(parts[1], 10); if (isNaN(majorVersion)) { return null; } // Check if there's a minor version between major and family if (familyIndex > 2) { const potentialMinor = parseInt(parts[2], 10); if (!isNaN(potentialMinor)) { minorVersion = potentialMinor; } } } // Build friendly name const versionString = minorVersion !== null ? `${majorVersion}.${minorVersion}` : `${majorVersion}`; const name = `${family}${versionString}`; return { name, family, majorVersion, minorVersion, }; } /** * Gets the color class for a model family (for Tailwind). * Uses consistent neutral gray styling for a clean, Linear-like design. * All models use the same muted color for visual consistency. */ export function getModelColorClass(family: ModelFamily): string { // All families use consistent neutral gray for clean design switch (family) { case 'opus': case 'sonnet': case 'haiku': return 'text-zinc-400'; default: return 'text-zinc-500'; } }