interface ModelsDevModel { id: string name?: string family?: string attachment?: boolean reasoning?: boolean tool_call?: boolean structured_output?: boolean temperature?: boolean knowledge?: string | null release_date?: string last_updated?: string modalities?: { input?: string[] output?: string[] } open_weights?: boolean weights?: Array<{ label: string url: string }> | null benchmarks?: Array<{ name: string score: number metric: string harness?: string source: string }> | null pricing?: { input?: number output?: number currency?: string } | null limit?: { context?: number input?: number output?: number } } let modelsDevCache: Map | null = null export async function fetchModelsDevData(): Promise> { if (modelsDevCache) return modelsDevCache try { const response = await (globalThis as any).fetch('https://models.dev/models.json') if (!response.ok) { return new Map() } const data = await response.json() as Record modelsDevCache = new Map() for (const [, model] of Object.entries(data)) { if (model.id) { modelsDevCache.set(model.id, model) } } return modelsDevCache } catch { return new Map() } } function calculatePrefixScore(modelA: string, modelB: string): number { const partsA = modelA.split('-') const partsB = modelB.split('-') const shorter = partsA.length <= partsB.length ? partsA : partsB const longer = partsA.length <= partsB.length ? partsB : partsA for (let i = 0; i < shorter.length; i++) { if (shorter[i] !== longer[i]) { return 0 } } const extraParts = longer.length - shorter.length return Math.max(0, 100 - (extraParts * 10)) } export function lookupModelsDevData( modelId: string, cache: Map ): ModelsDevModel | undefined { // Level 1: Exact match if (cache.has(modelId)) return cache.get(modelId) // Parse model ID: last part = model, second-to-last = provider const parts = modelId.split('/') const litellmModel = parts[parts.length - 1] const litellmProvider = parts.length >= 2 ? parts[parts.length - 2] : null // Level 2: Provider + Model match if (litellmProvider) { const litellmModelNormalized = litellmModel.replace(/\b(\d+)\.(\d+)\b/g, '$1-$2') for (const [key, value] of cache.entries()) { const devParts = key.split('/') if (devParts.length >= 2) { const devProvider = devParts[devParts.length - 2] const devModel = devParts[devParts.length - 1] if (devProvider === litellmProvider && (devModel === litellmModel || devModel === litellmModelNormalized)) { return value } } } } // Level 3: Model name only (ignore provider) const modelNameLower = litellmModel.toLowerCase() for (const [key, value] of cache.entries()) { const devModel = key.split('/').pop()!.toLowerCase() if (modelNameLower === devModel) { return value } } // Level 3.5: Normalize version dots to dashes if (modelNameLower.includes('.')) { const modelNameNormalized = modelNameLower.replace(/\b(\d+)\.(\d+)\b/g, '$1-$2') if (modelNameNormalized !== modelNameLower) { for (const [key, value] of cache.entries()) { const devModel = key.split('/').pop()!.toLowerCase() if (modelNameNormalized === devModel) { return value } } } } // Level 4: Prefix-based matching with score let bestMatch: ModelsDevModel | undefined let bestScore = 0 const modelNameForScoring = modelNameLower.includes('.') ? modelNameLower.replace(/\b(\d+)\.(\d+)\b/g, '$1-$2') : modelNameLower for (const [key, value] of cache.entries()) { const devModel = key.split('/').pop()!.toLowerCase() const score = calculatePrefixScore(modelNameForScoring, devModel) if (score > bestScore) { bestScore = score bestMatch = value } } return bestMatch } export type { ModelsDevModel }