agent-ecosystem/src/shared/utils/modelParser.ts
2026-04-18 12:10:51 +03:00

158 lines
4.7 KiB
TypeScript

/**
* 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<never, never>);
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 === '<synthetic>') {
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';
}
}