Spaces:
Running
Running
| import { useState, useMemo, useCallback, useEffect } from 'react' | |
| import providersData from '../data/providers.json' | |
| import benchmarksData from '../data/benchmarks.json' | |
| import { ManagementPanel } from './components/ManagementPanel' | |
| interface Model { | |
| name: string | |
| category?: string | |
| type: string | |
| size_b?: number | |
| input_price_per_1m?: number | |
| output_price_per_1m?: number | |
| price_per_image?: number | |
| price_per_minute?: number | |
| audio_price_per_1m?: number | |
| price_per_1m_tokens_30d?: number | |
| currency: string | |
| capabilities?: string[] | |
| display_name?: string; | |
| hf_id?: string; | |
| canonical_id?: string; | |
| ollama_id?: string; | |
| hf_private?: boolean; | |
| size_source?: 'hf-total' | 'hf-config-estimate' | 'hf-card' | 'ollama' | 'manual' | 'benchmark' | 'openrouter'; | |
| provider?: Provider; | |
| complianceStatus?: string; | |
| } | |
| interface Provider { | |
| name: string | |
| url: string | |
| headquarters: string | |
| region: string | |
| gdpr_compliant: boolean | |
| eu_endpoints: boolean | |
| models: Model[] | |
| } | |
| type SortConfig = { | |
| key: string; | |
| direction: 'asc' | 'desc'; | |
| } | null; | |
| interface BenchmarkEntry { | |
| slug?: string; // LLMStats slug | |
| hf_id?: string; // HF leaderboard fullname | |
| name: string; | |
| // LLMStats benchmarks | |
| mmlu?: number; | |
| mmlu_pro?: number; | |
| gpqa?: number; | |
| human_eval?: number; | |
| math?: number; | |
| gsm8k?: number; | |
| mmmu?: number; | |
| hellaswag?: number; | |
| ifeval?: number; | |
| arc?: number; | |
| drop?: number; | |
| mbpp?: number; | |
| mgsm?: number; | |
| bbh?: number; | |
| // HF-specific | |
| hf_math_lvl5?: number; | |
| hf_musr?: number; | |
| hf_avg?: number; | |
| params_b?: number; | |
| // LiveBench (livebench.ai β contamination-free, monthly updated) | |
| lb_name?: string; | |
| lb_global?: number; | |
| lb_reasoning?: number; | |
| lb_coding?: number; | |
| lb_math?: number; | |
| lb_language?: number; | |
| lb_if?: number; | |
| lb_data_analysis?: number; | |
| // Chatbot Arena (lmarena.ai β real human preference votes) | |
| arena_name?: string; | |
| arena_elo?: number; // raw ELO score ~800-1500 (higher = better) | |
| arena_rank?: number; | |
| arena_votes?: number; | |
| // Aider code editing benchmark (aider.chat) | |
| aider_pass_rate?: number; // 0-1, first-pass success on 133 coding tasks | |
| // Artificial Analysis (artificialanalysis.ai) | |
| aa_id?: string; | |
| aa_name?: string; | |
| aa_slug?: string; | |
| aa_intelligence?: number; // 0-100 intelligence index | |
| aa_coding?: number; // 0-100 coding index | |
| aa_math?: number; // 0-100 math index | |
| aa_mmlu_pro?: number; | |
| aa_gpqa?: number; | |
| aa_livecodebench?: number; | |
| aa_hle?: number; | |
| aa_scicode?: number; | |
| aa_math_500?: number; | |
| aa_aime?: number; | |
| aa_tokens_per_s?: number; | |
| aa_latency_s?: number; | |
| // MTEB (Massive Text Embedding Benchmark) | |
| mteb_avg?: number; | |
| mteb_retrieval?: number; | |
| // OCR Benchmark | |
| ocr_avg?: number; | |
| } | |
| const normalizeName = (s?: string) => | |
| (s || '').toLowerCase().replace(/[-_.]/g, ' ').replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim(); | |
| const EXCHANGE_RATE_EUR_TO_USD = 1.05 | |
| const CAP_ICON: Record<string, string> = { | |
| // Input Modalities | |
| vision: 'π', // Image Input | |
| video: 'π¬', // Video Input | |
| audio: 'π€', // Audio Input (ASR) | |
| files: 'π', // File/PDF Input | |
| // Output Modalities | |
| 'image-out': 'π¨', // Image Generation | |
| 'video-out': 'π₯', // Video Generation | |
| 'audio-out': 'π', // Audio Generation (TTS) | |
| // Functional Capabilities | |
| tools: 'π§', | |
| reasoning: 'π‘', | |
| embedding: 'π§©', | |
| 'eu-endpoint': 'πͺπΊ', | |
| } | |
| function App() { | |
| const [searchTerm, setSearchTerm] = useState('') | |
| const [selectedType, setSelectedType] = useState('all') | |
| const [selectedRegion, setSelectedRegion] = useState('all') | |
| const [sortConfig, setSortConfig] = useState<SortConfig>({ key: 'price', direction: 'asc' }); | |
| const [groupByModel, setGroupByModel] = useState(false); | |
| const [showBenchmarks, setShowBenchmarks] = useState(false); | |
| const [showManagement, setShowManagement] = useState(false); | |
| const [dataVersion, setDataVersion] = useState(0); | |
| // Live data β initialized from bundled JSON, refreshed from /api/* when available | |
| const [liveProviders, setLiveProviders] = useState<Provider[]>((providersData as any).providers); | |
| const [liveBenchmarks, setLiveBenchmarks] = useState<BenchmarkEntry[]>(benchmarksData as BenchmarkEntry[]); | |
| const getGroupKey = useCallback((m: Model) => { | |
| return (m.hf_id || m.canonical_id || m.name || '').toLowerCase(); | |
| }, []); | |
| useEffect(() => { | |
| fetch('/api/data') | |
| .then(r => r.ok ? r.json() : null) | |
| .then(d => { if (d?.providers) setLiveProviders(d.providers); }) | |
| .catch(() => {}); | |
| fetch('/api/benchmarks') | |
| .then(r => r.ok ? r.json() : null) | |
| .then(d => { if (Array.isArray(d)) setLiveBenchmarks(d); }) | |
| .catch(() => {}); | |
| }, [dataVersion]); | |
| const fmtNum = (v?: number, decimals = 0) => (v !== undefined && Number.isFinite(v)) ? v.toFixed(decimals) : 'β'; | |
| const fmtPct = (v?: number) => (v !== undefined && Number.isFinite(v)) ? `${(v * 100).toFixed(0)}%` : 'β'; | |
| // Build benchmark lookup maps | |
| const { nameMap, hfIdMap } = useMemo(() => { | |
| const nameMap = new Map<string, BenchmarkEntry>(); | |
| const hfIdMap = new Map<string, BenchmarkEntry>(); | |
| for (const b of liveBenchmarks) { | |
| nameMap.set(normalizeName(b.name), b); | |
| if (b.slug) { | |
| const slugModel = b.slug.split('/').pop() || ''; | |
| if (slugModel) nameMap.set(normalizeName(slugModel), b); | |
| } | |
| if (b.hf_id) { | |
| hfIdMap.set(normalizeName(b.hf_id), b); | |
| const modelPart = b.hf_id.split('/').pop() || ''; | |
| const normModel = normalizeName(modelPart); | |
| nameMap.set(normModel, b); | |
| const words = normModel.split(' '); | |
| if (words.length > 1) nameMap.set(words.slice(1).join(' '), b); | |
| } | |
| if (b.lb_name) nameMap.set(normalizeName(b.lb_name), b); | |
| if (b.arena_name) nameMap.set(normalizeName(b.arena_name), b); | |
| if (b.aa_name) nameMap.set(normalizeName(b.aa_name), b); | |
| if (b.aa_slug) nameMap.set(normalizeName(b.aa_slug), b); | |
| } | |
| return { nameMap, hfIdMap }; | |
| }, [liveBenchmarks]); | |
| const findBenchmark = useCallback((modelName: string): BenchmarkEntry | undefined => { | |
| // Strip @region (e.g. @us-east-1) and :effort (e.g. :high) suffixes before normalizing | |
| const cleanName = modelName.replace(/@[^/]+$/, '').replace(/:[^/]+$/, ''); | |
| const norm = normalizeName(cleanName); | |
| // Direct HF ID match (for providers that use "org/model-id" format) | |
| let modelPart = ''; | |
| if (cleanName.includes('/')) { | |
| if (hfIdMap.has(norm)) return hfIdMap.get(norm)!; | |
| // Try model part only (after "/") | |
| modelPart = normalizeName(cleanName.split('/').pop() || ''); | |
| if (nameMap.has(modelPart)) return nameMap.get(modelPart)!; | |
| // Strip first word from model part β handles "Meta-Llama-..." vs "Llama-..." | |
| const modelWords = modelPart.split(' '); | |
| if (modelWords.length > 1) { | |
| const stripped = modelWords.slice(1).join(' '); | |
| if (nameMap.has(stripped)) return nameMap.get(stripped)!; | |
| } | |
| } | |
| if (nameMap.has(norm)) return nameMap.get(norm); | |
| // Longest startsWith match β handles date-suffixed variants like "claude-3-5-sonnet-20241022" | |
| let best: BenchmarkEntry | undefined; | |
| let bestLen = 0; | |
| for (const [key, val] of nameMap) { | |
| if (norm.startsWith(key)) { | |
| const rest = norm.slice(key.length); | |
| if ((rest === '' || /^ \d/.test(rest)) && key.length > bestLen) { | |
| best = val; | |
| bestLen = key.length; | |
| } | |
| } | |
| } | |
| if (best) return best; | |
| // Reverse prefix: benchmark key starts with provider name β handles cases where the | |
| // benchmark stores a base name longer than the provider's model ID. | |
| // Check both norm and modelPart (for versionless names like "anthropic/claude-haiku-4-5"). | |
| // Pick the highest lb_global among all matches (best variant of the model). | |
| let bestReverse: BenchmarkEntry | undefined; | |
| let bestReverseScore = -1; | |
| for (const [key, val] of nameMap) { | |
| const score = val.lb_global ?? 0; | |
| if ( | |
| (key.startsWith(norm + ' ') || (modelPart && key.startsWith(modelPart + ' '))) && | |
| score > bestReverseScore | |
| ) { | |
| bestReverse = val; | |
| bestReverseScore = score; | |
| } | |
| } | |
| return bestReverse; | |
| }, [nameMap, hfIdMap]); | |
| const handleDataUpdated = useCallback(() => { | |
| // Increment version to signal that a page reload would show fresh data | |
| setDataVersion((v) => v + 1); | |
| }, []); | |
| const getComplianceStatus = (provider: Provider, model?: Model) => { | |
| const hq = provider.headquarters.toLowerCase(); | |
| const isEU = provider.region === 'EU' || hq === 'germany' || hq === 'france' || hq === 'netherlands'; | |
| const isUS = hq === 'usa' || provider.region === 'US'; | |
| const isEEA = provider.region === 'EEA Equivalent' || hq === 'switzerland'; | |
| // Model-level override for OpenRouter or similar aggregators | |
| const hasEuEndpoint = model?.capabilities?.includes('eu-endpoint') || provider.eu_endpoints; | |
| if (isEU) return 'EU'; | |
| if (isEEA) return 'EEA'; | |
| if (isUS && hasEuEndpoint) return 'US/EU'; | |
| if (isUS) return 'US'; | |
| return 'Other'; | |
| }; | |
| const allModels = useMemo(() => { | |
| const rawModels: any[] = [] | |
| liveProviders.forEach((provider: Provider) => { | |
| provider.models.forEach((model) => { | |
| const status = getComplianceStatus(provider, model); | |
| let cleanName = model.name; | |
| // Strip provider/ prefix for all models | |
| if (cleanName.includes('/')) { | |
| cleanName = cleanName.split('/').pop() || cleanName; | |
| } | |
| rawModels.push({ | |
| ...model, | |
| name: cleanName, | |
| provider, | |
| complianceStatus: status | |
| }) | |
| }) | |
| }) | |
| // De-duplicate: filter out models with same name, provider, and price | |
| const seen = new Set<string>(); | |
| const uniqueModels = rawModels.filter(m => { | |
| const key = `${m.provider?.name}|${m.name}|${m.input_price_per_1m}|${m.output_price_per_1m}`; | |
| if (seen.has(key)) return false; | |
| seen.add(key); | |
| return true; | |
| }); | |
| return uniqueModels; | |
| }, [liveProviders]) | |
| const filteredModels = useMemo(() => { | |
| return allModels.filter((model) => { | |
| const providerName = model.provider?.name || ''; | |
| const matchesSearch = model.name.toLowerCase().includes(searchTerm.toLowerCase()) || | |
| providerName.toLowerCase().includes(searchTerm.toLowerCase()) | |
| const caps = model.capabilities || []; | |
| const matchesType = selectedType === 'all' || | |
| model.type === selectedType || | |
| (selectedType === 'audio' && (caps.includes('audio') || caps.includes('audio-out'))) || | |
| (selectedType === 'vision' && (caps.includes('vision') || caps.includes('video'))) || | |
| (selectedType === 'chat' && model.type === 'chat'); | |
| const status = model.complianceStatus || 'Other'; | |
| let matchesRegion = selectedRegion === 'all' || status === selectedRegion; | |
| // US filter includes US/EU | |
| if (selectedRegion === 'US' && status === 'US/EU') matchesRegion = true; | |
| return matchesSearch && matchesType && matchesRegion | |
| }) | |
| }, [searchTerm, selectedType, selectedRegion, allModels]) | |
| const getNormalizedPriceUSD = (model: Model) => { | |
| const price = model.input_price_per_1m || model.price_per_image || model.price_per_minute || 0 | |
| return model.currency === 'EUR' ? price * EXCHANGE_RATE_EUR_TO_USD : price | |
| } | |
| const sortedModels = useMemo(() => { | |
| return [...filteredModels].sort((a, b) => { | |
| if (!sortConfig) return 0; | |
| let aValue: any; | |
| let bValue: any; | |
| switch (sortConfig.key) { | |
| case 'provider': | |
| aValue = a.provider.name; | |
| bValue = b.provider.name; | |
| break; | |
| case 'model': | |
| aValue = a.name; | |
| bValue = b.name; | |
| break; | |
| case 'size': | |
| aValue = a.size_b || 0; | |
| bValue = b.size_b || 0; | |
| break; | |
| case 'price': | |
| aValue = getNormalizedPriceUSD(a); | |
| bValue = getNormalizedPriceUSD(b); | |
| break; | |
| case 'output_price': | |
| aValue = a.output_price_per_1m ?? 0; | |
| bValue = b.output_price_per_1m ?? 0; | |
| break; | |
| case 'compliance': | |
| aValue = a.complianceStatus; | |
| bValue = b.complianceStatus; | |
| break; | |
| case 'mmlu': | |
| case 'gpqa': | |
| case 'human_eval': | |
| case 'math': | |
| case 'gsm8k': | |
| case 'mmmu': | |
| case 'ifeval': | |
| case 'bbh': | |
| case 'hf_math_lvl5': | |
| case 'hf_musr': | |
| case 'hf_avg': | |
| case 'lb_global': | |
| case 'lb_reasoning': | |
| case 'lb_coding': | |
| case 'lb_math': | |
| case 'lb_language': | |
| case 'lb_if': | |
| case 'lb_data_analysis': | |
| case 'arena_elo': | |
| case 'aider_pass_rate': | |
| case 'aa_intelligence': | |
| case 'aa_tokens_per_s': | |
| case 'mteb_avg': | |
| case 'mteb_retrieval': { | |
| try { | |
| const bA = findBenchmark(a.name); | |
| const bB = findBenchmark(b.name); | |
| aValue = bA?.[sortConfig.key as keyof BenchmarkEntry] as number ?? -1; | |
| bValue = bB?.[sortConfig.key as keyof BenchmarkEntry] as number ?? -1; | |
| } catch (e) { | |
| aValue = -1; | |
| bValue = -1; | |
| } | |
| break; | |
| } | |
| default: | |
| return 0; | |
| } | |
| if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1; | |
| if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1; | |
| return 0; | |
| }); | |
| }, [filteredModels, sortConfig]) | |
| const displayModels = useMemo(() => { | |
| if (!groupByModel) return sortedModels; | |
| const groups: Record<string, typeof sortedModels> = {}; | |
| const groupOrder: string[] = []; | |
| sortedModels.forEach(m => { | |
| const key = getGroupKey(m); | |
| if (!groups[key]) { | |
| groups[key] = []; | |
| groupOrder.push(key); | |
| } | |
| groups[key].push(m); | |
| }); | |
| const result: typeof sortedModels = []; | |
| groupOrder.forEach(key => { | |
| result.push(...groups[key]); | |
| }); | |
| return result; | |
| }, [sortedModels, groupByModel, getGroupKey]); | |
| const requestSort = (key: string) => { | |
| let direction: 'asc' | 'desc' = 'asc'; | |
| if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') { | |
| direction = 'desc'; | |
| } | |
| setSortConfig({ key, direction }); | |
| }; | |
| const formatPrice = (price: number | undefined, currency: string) => { | |
| if (price === undefined) return '-' | |
| if (price === 0) return 'Free' | |
| const symbol = currency === 'USD' ? '$' : 'β¬' | |
| return `${symbol}${price.toFixed(4)}` | |
| } | |
| const getSortIcon = (key: string) => { | |
| if (sortConfig?.key !== key) return 'βοΈ'; | |
| return sortConfig.direction === 'asc' ? 'πΌ' : 'π½'; | |
| } | |
| return ( | |
| <div className="container"> | |
| <header> | |
| <div className="header-row"> | |
| <div> | |
| <h1>AI Provider Comparison</h1> | |
| <p>Analyze costs, data sovereignty, and model efficiency.</p> | |
| </div> | |
| <div className="header-actions"> | |
| {dataVersion > 0 && ( | |
| <span className="data-stale-hint" title="Data refreshed from server"> | |
| β» data updated | |
| </span> | |
| )} | |
| <a | |
| className="btn-github" | |
| href="https://github.com/CrispStrobe/LLMProviders" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| title="View source on GitHub" | |
| > | |
| GitHub | |
| </a> | |
| <button className="btn-manage" onClick={() => setShowManagement(true)}> | |
| β Manage Data | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| {showManagement && ( | |
| <ManagementPanel | |
| onClose={() => setShowManagement(false)} | |
| onDataUpdated={handleDataUpdated} | |
| /> | |
| )} | |
| <div className="controls"> | |
| <input | |
| type="text" | |
| placeholder="Search models or providers..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="search-input" | |
| /> | |
| <select value={selectedType} onChange={(e) => setSelectedType(e.target.value)} className="type-select"> | |
| <option value="all">All Types</option> | |
| <option value="chat">Chat (LLM)</option> | |
| <option value="vision">Vision</option> | |
| <option value="embedding">Embedding</option> | |
| <option value="image">Image</option> | |
| <option value="audio">Audio</option> | |
| </select> | |
| <select value={selectedRegion} onChange={(e) => setSelectedRegion(e.target.value)} className="type-select"> | |
| <option value="all">All Jurisdictions</option> | |
| <option value="EU">EU (Sovereign)</option> | |
| <option value="EEA">EEA (Swiss/EEA)</option> | |
| <option value="US/EU">US/EU (EU Endpoint)</option> | |
| <option value="US">US (Cloud Act)</option> | |
| <option value="Other">Other</option> | |
| </select> | |
| <label className="checkbox-label"> | |
| <input | |
| type="checkbox" | |
| checked={groupByModel} | |
| onChange={(e) => setGroupByModel(e.target.checked)} | |
| /> | |
| Group by Model | |
| </label> | |
| <label className="checkbox-label"> | |
| <input | |
| type="checkbox" | |
| checked={showBenchmarks} | |
| onChange={(e) => setShowBenchmarks(e.target.checked)} | |
| /> | |
| Show Benchmarks | |
| </label> | |
| </div> | |
| <div className="table-container"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th onClick={() => requestSort('provider')} className="sortable">Provider {getSortIcon('provider')}</th> | |
| <th onClick={() => requestSort('compliance')} className="sortable">Jurisdiction {getSortIcon('compliance')}</th> | |
| <th onClick={() => requestSort('model')} className="sortable">Model {getSortIcon('model')}</th> | |
| <th>Caps</th> | |
| <th onClick={() => requestSort('size')} className="sortable">Size (B) {getSortIcon('size')}</th> | |
| <th onClick={() => requestSort('price')} className="sortable">Input Price (USD) {getSortIcon('price')}</th> | |
| <th onClick={() => requestSort('output_price')} className="sortable">Output Price {getSortIcon('output_price')}</th> | |
| {showBenchmarks && <> | |
| <th onClick={() => requestSort('arena_elo')} className="sortable" title="Chatbot Arena ELO (human preference votes)">Arena ELO {getSortIcon('arena_elo')}</th> | |
| <th onClick={() => requestSort('aider_pass_rate')} className="sortable" title="Aider code editing benchmark (pass rate, 133 tasks)">Aider {getSortIcon('aider_pass_rate')}</th> | |
| <th onClick={() => requestSort('aa_intelligence')} className="sortable" title="Artificial Analysis Intelligence Index (0-100)">AA Intel {getSortIcon('aa_intelligence')}</th> | |
| <th onClick={() => requestSort('aa_tokens_per_s')} className="sortable" title="Artificial Analysis Median Speed (Tokens per Second)">AA Speed {getSortIcon('aa_tokens_per_s')}</th> | |
| <th onClick={() => requestSort('mteb_avg')} className="sortable" title="MTEB (Massive Text Embedding Benchmark) Average">MTEB {getSortIcon('mteb_avg')}</th> | |
| <th onClick={() => requestSort('mteb_retrieval')} className="sortable" title="MTEB Retrieval Average">MTEB-Ret {getSortIcon('mteb_retrieval')}</th> | |
| <th onClick={() => requestSort('lb_global')} className="sortable" title="LiveBench overall average (contamination-free)">LB {getSortIcon('lb_global')}</th> | |
| <th onClick={() => requestSort('lb_math')} className="sortable" title="LiveBench Mathematics">LB-Math {getSortIcon('lb_math')}</th> | |
| <th onClick={() => requestSort('lb_coding')} className="sortable" title="LiveBench Coding + Agentic Coding">LB-Code {getSortIcon('lb_coding')}</th> | |
| <th onClick={() => requestSort('lb_reasoning')} className="sortable" title="LiveBench Reasoning">LB-Reas {getSortIcon('lb_reasoning')}</th> | |
| <th onClick={() => requestSort('gpqa')} className="sortable" title="Graduate-level reasoning (GPQA)">GPQA {getSortIcon('gpqa')}</th> | |
| <th onClick={() => requestSort('mmlu_pro')} className="sortable" title="MMLU-Pro knowledge">MMLU-PRO {getSortIcon('mmlu_pro')}</th> | |
| <th onClick={() => requestSort('ifeval')} className="sortable" title="Instruction following (IFEval)">IFEval {getSortIcon('ifeval')}</th> | |
| <th onClick={() => requestSort('bbh')} className="sortable" title="Big-Bench Hard reasoning">BBH {getSortIcon('bbh')}</th> | |
| <th onClick={() => requestSort('hf_math_lvl5')} className="sortable" title="MATH Level 5 (HF leaderboard)">MATH L5 {getSortIcon('hf_math_lvl5')}</th> | |
| <th onClick={() => requestSort('hf_musr')} className="sortable" title="Multi-step Soft Reasoning (HF leaderboard)">MUSR {getSortIcon('hf_musr')}</th> | |
| <th onClick={() => requestSort('mmlu')} className="sortable" title="Classic MMLU (LLMStats)">MMLU {getSortIcon('mmlu')}</th> | |
| <th onClick={() => requestSort('human_eval')} className="sortable" title="HumanEval coding (LLMStats)">HumanEval {getSortIcon('human_eval')}</th> | |
| </>} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {displayModels.map((model, idx) => { | |
| const prev = displayModels[idx - 1]; | |
| const isGroupStart = groupByModel && ( | |
| idx === 0 || getGroupKey(prev) !== getGroupKey(model) | |
| ); | |
| const bm = findBenchmark(model.name); | |
| return ( | |
| <tr key={`${model.provider.name}-${model.name}-${idx}`} className={isGroupStart ? 'group-divider' : ''}> | |
| <td className="provider-cell">{model.provider.name}</td> | |
| <td> | |
| <span className={`status-badge ${model.complianceStatus.replace(/\s+|[\(\)\/]/g, '-').toLowerCase()}`}> | |
| {model.complianceStatus} | |
| </span> | |
| </td> | |
| <td className="model-name"> | |
| <div className="model-name-wrapper"> | |
| {model.display_name ?? model.name} | |
| <div className="model-info-container"> | |
| <span className="info-icon">β</span> | |
| <div className="model-tooltip"> | |
| <div className="tooltip-row"><strong>Type:</strong> {model.type}</div> | |
| {model.size_b && <div className="tooltip-row"><strong>Size:</strong> {model.size_b}B</div>} | |
| {model.hf_id && ( | |
| <div className="tooltip-row"> | |
| <strong>HF:</strong> | |
| <a href={`https://huggingface.co/${model.hf_id}`} target="_blank" rel="noopener noreferrer" className="hf-link"> | |
| {model.hf_id} β | |
| </a> | |
| </div> | |
| )} | |
| {!model.hf_id && model.hf_private && ( | |
| <div className="tooltip-row"><strong>HF:</strong> Proprietary API</div> | |
| )} | |
| {model.ollama_id && ( | |
| <div className="tooltip-row"> | |
| <strong>Ollama:</strong> | |
| <a href={`https://ollama.com/library/${model.ollama_id}`} target="_blank" rel="noopener noreferrer" className="hf-link"> | |
| {model.ollama_id} β | |
| </a> | |
| </div> | |
| )} | |
| {model.capabilities && model.capabilities.length > 0 && ( | |
| <div className="tooltip-row"><strong>Caps:</strong> {model.capabilities.join(', ')}</div> | |
| )} | |
| {bm?.ocr_avg !== undefined && ( | |
| <div className="tooltip-row"><strong>OCR:</strong> {bm.ocr_avg.toFixed(1)} (olmOCR-bench)</div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| </td> | |
| <td className="caps-cell"> | |
| {model.type === 'embedding' && ( | |
| <span className="cap-badge cap-embedding" title="embedding">{CAP_ICON.embedding}</span> | |
| )} | |
| {(model.capabilities || []).map((cap: string) => ( | |
| <span key={cap} className={`cap-badge cap-${cap}`} title={cap}>{CAP_ICON[cap] ?? cap}</span> | |
| ))} | |
| </td> | |
| <td className="size-cell">{model.size_b ? `${model.size_b}B` : '-'}</td> | |
| <td> | |
| <div className="price-stack"> | |
| {model.price_per_image !== undefined && !model.input_price_per_1m | |
| ? `$${model.price_per_image}/MP` | |
| : model.price_per_minute !== undefined | |
| ? `${formatPrice(model.price_per_minute, model.currency)}/min` | |
| : formatPrice(model.input_price_per_1m, model.currency)} | |
| {model.audio_price_per_1m !== undefined && ( | |
| <div className="price-subtext" title="Audio token price"> | |
| {CAP_ICON.audio} {formatPrice(model.audio_price_per_1m, model.currency)}/M | |
| </div> | |
| )} | |
| </div> | |
| </td> | |
| <td> | |
| {model.price_per_image !== undefined && !model.output_price_per_1m | |
| ? 'β' | |
| : model.price_per_minute !== undefined | |
| ? 'β' | |
| : formatPrice(model.output_price_per_1m, model.currency)} | |
| </td> | |
| {showBenchmarks && ( | |
| <> | |
| <td className="benchmark-cell">{fmtNum(bm?.arena_elo)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.aider_pass_rate)}</td> | |
| <td className="benchmark-cell">{fmtNum(bm?.aa_intelligence)}</td> | |
| <td className="benchmark-cell">{fmtNum(bm?.aa_tokens_per_s)}</td> | |
| <td className="benchmark-cell">{fmtNum(bm?.mteb_avg, 1)}</td> | |
| <td className="benchmark-cell">{fmtNum(bm?.mteb_retrieval, 1)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.lb_global)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.lb_math)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.lb_coding)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.lb_reasoning)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.gpqa)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.mmlu_pro)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.ifeval)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.bbh)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.hf_math_lvl5)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.hf_musr)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.mmlu)}</td> | |
| <td className="benchmark-cell">{fmtPct(bm?.human_eval)}</td> | |
| </> | |
| )} | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| <footer> | |
| <p>* All prices normalized to USD for comparison using 1 EUR = {EXCHANGE_RATE_EUR_TO_USD} USD.</p> | |
| <p>Benchmark data from LLMStats, HF Leaderboard, LiveBench, Chatbot Arena, Aider, MTEB, and <a href="https://artificialanalysis.ai/" target="_blank" rel="noopener noreferrer">Artificial Analysis</a>.</p> | |
| <p>Sorted by input price by default.</p> | |
| </footer> | |
| </div> | |
| ) | |
| } | |
| export default App | |