|
|
|
|
|
let allModels = []; |
|
|
let filteredModels = []; |
|
|
|
|
|
|
|
|
const vendorNameMap = { |
|
|
'qwen': 'Qwen', |
|
|
'meta-llama': 'Meta', |
|
|
'x-ai': 'xAI', |
|
|
'z-ai': 'Zhipu AI', |
|
|
'google': 'Google', |
|
|
'openai': 'OpenAI', |
|
|
'anthropic': 'Anthropic', |
|
|
'mistralai': 'Mistral AI', |
|
|
'deepseek': 'DeepSeek', |
|
|
'alibaba': 'Alibaba', |
|
|
'amazon': 'Amazon', |
|
|
'microsoft': 'Microsoft', |
|
|
'nvidia': 'NVIDIA', |
|
|
'cohere': 'Cohere', |
|
|
'ai21': 'AI21 Labs', |
|
|
'minimax': 'MiniMax', |
|
|
'moonshotai': 'Moonshot AI', |
|
|
'stepfun-ai': 'StepFun', |
|
|
'inclusionai': 'Inclusion AI', |
|
|
'deepcogito': 'Deep Cogito', |
|
|
'baidu': 'Baidu', |
|
|
'nousresearch': 'Nous Research', |
|
|
'arcee-ai': 'Arcee AI', |
|
|
'inception': 'Inception', |
|
|
'sao10k': 'Sao10K', |
|
|
'thedrummer': 'TheDrummer', |
|
|
'tngtech': 'TNG Technology', |
|
|
'meituan': 'Meituan' |
|
|
}; |
|
|
|
|
|
function normalizeVendorName(vendor) { |
|
|
return vendorNameMap[vendor] || vendor; |
|
|
} |
|
|
|
|
|
|
|
|
async function loadData() { |
|
|
try { |
|
|
const response = await fetch('quadrants.csv'); |
|
|
const csvText = await response.text(); |
|
|
|
|
|
Papa.parse(csvText, { |
|
|
header: true, |
|
|
dynamicTyping: true, |
|
|
complete: function(results) { |
|
|
allModels = results.data.filter(row => row.model_name); |
|
|
|
|
|
allModels.forEach(model => { |
|
|
model.displayVendor = normalizeVendorName(model.vendor); |
|
|
}); |
|
|
filteredModels = [...allModels]; |
|
|
initializeApp(); |
|
|
} |
|
|
}); |
|
|
} catch (error) { |
|
|
console.error('Error loading data:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let currentSort = { |
|
|
column: 'model_name', |
|
|
direction: 'asc' |
|
|
}; |
|
|
|
|
|
|
|
|
function initializeApp() { |
|
|
updateStats(); |
|
|
populateTable(); |
|
|
setupTableControls(); |
|
|
setupSorting(); |
|
|
setupTabs(); |
|
|
createQuadrantChart(); |
|
|
createAllQuadrantsChart(); |
|
|
createIndividualQuadrantCharts(); |
|
|
} |
|
|
|
|
|
|
|
|
function setupTabs() { |
|
|
const tabButtons = document.querySelectorAll('.tab-button'); |
|
|
const tabContents = document.querySelectorAll('.tab-content'); |
|
|
|
|
|
tabButtons.forEach(button => { |
|
|
button.addEventListener('click', () => { |
|
|
const targetTab = button.dataset.tab; |
|
|
|
|
|
|
|
|
tabButtons.forEach(btn => btn.classList.remove('active')); |
|
|
tabContents.forEach(content => content.classList.remove('active')); |
|
|
|
|
|
|
|
|
button.classList.add('active'); |
|
|
document.getElementById(`tab-${targetTab}`).classList.add('active'); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function updateStats() { |
|
|
const costs = allModels.map(m => m.avg_cost); |
|
|
const contexts = allModels.map(m => m.context_length); |
|
|
const inputPrices = allModels.map(m => m.input_price_usd_per_m); |
|
|
const outputPrices = allModels.map(m => m.output_price_usd_per_m); |
|
|
const multiples = allModels.map(m => m.output_input_multiple); |
|
|
const minContext = Math.min(...contexts); |
|
|
const maxContext = Math.max(...contexts); |
|
|
const vendors = new Set(allModels.map(m => m.displayVendor)).size; |
|
|
|
|
|
|
|
|
const medianCost = median(costs); |
|
|
const medianContext = median(contexts); |
|
|
const medianInput = median(inputPrices); |
|
|
const medianOutput = median(outputPrices); |
|
|
const medianMultiple = median(multiples); |
|
|
|
|
|
document.getElementById('total-models').textContent = allModels.length; |
|
|
document.getElementById('vendors').textContent = vendors; |
|
|
document.getElementById('median-input').textContent = `$${medianInput.toFixed(2)}`; |
|
|
document.getElementById('median-output').textContent = `$${medianOutput.toFixed(2)}`; |
|
|
document.getElementById('median-multiple').textContent = `${medianMultiple.toFixed(2)}x`; |
|
|
document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`; |
|
|
|
|
|
|
|
|
document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`; |
|
|
document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`; |
|
|
} |
|
|
|
|
|
|
|
|
function median(arr) { |
|
|
const sorted = [...arr].sort((a, b) => a - b); |
|
|
const mid = Math.floor(sorted.length / 2); |
|
|
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; |
|
|
} |
|
|
|
|
|
|
|
|
function createQuadrantChart() { |
|
|
const ctx = document.getElementById('quadrantChart').getContext('2d'); |
|
|
|
|
|
const quadrantColors = { |
|
|
'Low Cost / High Context': '#10b981', |
|
|
'High Cost / High Context': '#2563eb', |
|
|
'Low Cost / Low Context': '#f59e0b', |
|
|
'High Cost / Low Context': '#ef4444' |
|
|
}; |
|
|
|
|
|
const datasets = Object.keys(quadrantColors).map(quadrant => { |
|
|
const models = allModels.filter(m => m.quadrant === quadrant); |
|
|
return { |
|
|
label: quadrant, |
|
|
data: models.map(m => ({ |
|
|
x: m.avg_cost, |
|
|
y: m.context_length / 1000, |
|
|
model: m |
|
|
})), |
|
|
backgroundColor: quadrantColors[quadrant] + '80', |
|
|
borderColor: quadrantColors[quadrant], |
|
|
borderWidth: 2, |
|
|
pointRadius: 5, |
|
|
pointHoverRadius: 8 |
|
|
}; |
|
|
}); |
|
|
|
|
|
|
|
|
const medianCost = median(allModels.map(m => m.avg_cost)); |
|
|
const medianContext = median(allModels.map(m => m.context_length / 1000)); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'scatter', |
|
|
data: { datasets }, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
title: { |
|
|
display: false |
|
|
}, |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top' |
|
|
}, |
|
|
tooltip: { |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
const model = context.raw.model; |
|
|
return [ |
|
|
model.model_name, |
|
|
`Vendor: ${model.displayVendor}`, |
|
|
`Context: ${(model.context_length / 1000).toFixed(0)}K tokens`, |
|
|
`Input: $${model.input_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Output: $${model.output_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Avg: $${model.avg_cost.toFixed(2)}/M` |
|
|
]; |
|
|
} |
|
|
} |
|
|
}, |
|
|
annotation: { |
|
|
annotations: { |
|
|
verticalLine: { |
|
|
type: 'line', |
|
|
xMin: medianCost, |
|
|
xMax: medianCost, |
|
|
borderColor: '#64748b', |
|
|
borderWidth: 2, |
|
|
borderDash: [5, 5], |
|
|
label: { |
|
|
display: true, |
|
|
content: `Median Cost: $${medianCost.toFixed(2)}`, |
|
|
position: 'start' |
|
|
} |
|
|
}, |
|
|
horizontalLine: { |
|
|
type: 'line', |
|
|
yMin: medianContext, |
|
|
yMax: medianContext, |
|
|
borderColor: '#64748b', |
|
|
borderWidth: 2, |
|
|
borderDash: [5, 5], |
|
|
label: { |
|
|
display: true, |
|
|
content: `Median Context: ${medianContext.toFixed(0)}K`, |
|
|
position: 'end' |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
type: 'logarithmic', |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Average Cost ($/M tokens, log scale)' |
|
|
}, |
|
|
min: 0.01 |
|
|
}, |
|
|
y: { |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Context Window (K tokens)' |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
plugins: [window['chartjs-plugin-annotation']] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getCostTier(cost) { |
|
|
if (cost < 0.10) return 'cost-very-low'; |
|
|
if (cost < 0.50) return 'cost-low'; |
|
|
if (cost < 2.00) return 'cost-medium'; |
|
|
if (cost < 40.00) return 'cost-high'; |
|
|
return 'cost-very-high'; |
|
|
} |
|
|
|
|
|
|
|
|
function getContextTier(context) { |
|
|
if (context < 32000) return 'context-small'; |
|
|
if (context < 128000) return 'context-medium'; |
|
|
if (context < 256000) return 'context-large'; |
|
|
if (context < 1000000) return 'context-very-large'; |
|
|
return 'context-ultra'; |
|
|
} |
|
|
|
|
|
|
|
|
function getMultipleTier(multiple) { |
|
|
if (multiple <= 1) return 'multiple-equal'; |
|
|
if (multiple < 2) return 'multiple-low'; |
|
|
if (multiple < 5) return 'multiple-medium'; |
|
|
if (multiple < 10) return 'multiple-high'; |
|
|
return 'multiple-very-high'; |
|
|
} |
|
|
|
|
|
|
|
|
function cleanModelName(modelName, vendor) { |
|
|
|
|
|
const patterns = [ |
|
|
new RegExp(`^${vendor}:\\s*`, 'i'), |
|
|
/^Qwen:\s*/i, |
|
|
/^Meta:\s*/i, |
|
|
/^Google:\s*/i, |
|
|
/^OpenAI:\s*/i, |
|
|
/^Anthropic:\s*/i, |
|
|
/^DeepSeek:\s*/i, |
|
|
/^Mistral:\s*/i, |
|
|
/^NVIDIA:\s*/i, |
|
|
/^Amazon:\s*/i, |
|
|
/^Microsoft:\s*/i, |
|
|
/^xAI:\s*/i, |
|
|
/^Zhipu AI:\s*/i, |
|
|
/^MoonshotAI:\s*/i, |
|
|
/^Moonshot AI:\s*/i, |
|
|
/^Alibaba:\s*/i |
|
|
]; |
|
|
|
|
|
let cleaned = modelName; |
|
|
for (const pattern of patterns) { |
|
|
cleaned = cleaned.replace(pattern, ''); |
|
|
} |
|
|
return cleaned; |
|
|
} |
|
|
|
|
|
|
|
|
function populateTable(models = filteredModels) { |
|
|
const tbody = document.getElementById('table-body'); |
|
|
|
|
|
|
|
|
const sortedModels = [...models].sort((a, b) => { |
|
|
let aVal = a[currentSort.column]; |
|
|
let bVal = b[currentSort.column]; |
|
|
|
|
|
|
|
|
if (typeof aVal === 'string') { |
|
|
aVal = aVal.toLowerCase(); |
|
|
bVal = bVal.toLowerCase(); |
|
|
} |
|
|
|
|
|
if (currentSort.direction === 'asc') { |
|
|
return aVal > bVal ? 1 : -1; |
|
|
} else { |
|
|
return aVal < bVal ? 1 : -1; |
|
|
} |
|
|
}); |
|
|
|
|
|
let lastVendor = null; |
|
|
tbody.innerHTML = sortedModels.map((m, index) => { |
|
|
const isNewVendor = m.displayVendor !== lastVendor; |
|
|
lastVendor = m.displayVendor; |
|
|
const vendorClass = isNewVendor ? 'vendor-group-start' : ''; |
|
|
|
|
|
return ` |
|
|
<tr class="${vendorClass}" data-vendor="${m.displayVendor}"> |
|
|
<td>${cleanModelName(m.model_name, m.displayVendor)}</td> |
|
|
<td class="vendor-cell">${m.displayVendor}</td> |
|
|
<td class="${getContextTier(m.context_length)}" style="font-weight: 600;"> |
|
|
${(m.context_length / 1000).toFixed(0)}K |
|
|
</td> |
|
|
<td class="${getCostTier(m.input_price_usd_per_m)}" style="font-weight: 600;"> |
|
|
$${m.input_price_usd_per_m.toFixed(2)} |
|
|
</td> |
|
|
<td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;"> |
|
|
$${m.output_price_usd_per_m.toFixed(2)} |
|
|
</td> |
|
|
<td class="${getMultipleTier(m.output_input_multiple)}" style="font-weight: 600;"> |
|
|
${m.output_input_multiple.toFixed(2)}x |
|
|
</td> |
|
|
<td><span class="quadrant-badge">${m.quadrant}</span></td> |
|
|
</tr> |
|
|
`; |
|
|
}).join(''); |
|
|
} |
|
|
|
|
|
|
|
|
function setupSorting() { |
|
|
const headers = document.querySelectorAll('th.sortable'); |
|
|
|
|
|
headers.forEach(header => { |
|
|
header.addEventListener('click', () => { |
|
|
const column = header.dataset.sort; |
|
|
|
|
|
|
|
|
if (currentSort.column === column) { |
|
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; |
|
|
} else { |
|
|
currentSort.column = column; |
|
|
currentSort.direction = 'asc'; |
|
|
} |
|
|
|
|
|
|
|
|
headers.forEach(h => { |
|
|
h.classList.remove('sort-asc', 'sort-desc'); |
|
|
}); |
|
|
header.classList.add(`sort-${currentSort.direction}`); |
|
|
|
|
|
|
|
|
populateTable(filteredModels); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`); |
|
|
if (initialHeader) { |
|
|
initialHeader.classList.add('sort-asc'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function setupTableControls() { |
|
|
const searchInput = document.getElementById('search'); |
|
|
const quadrantFilter = document.getElementById('quadrant-filter'); |
|
|
|
|
|
function applyFilters() { |
|
|
const searchTerm = searchInput.value.toLowerCase(); |
|
|
const quadrant = quadrantFilter.value; |
|
|
|
|
|
filteredModels = allModels.filter(m => { |
|
|
const matchesSearch = !searchTerm || |
|
|
m.model_name.toLowerCase().includes(searchTerm) || |
|
|
m.displayVendor.toLowerCase().includes(searchTerm); |
|
|
const matchesQuadrant = !quadrant || m.quadrant === quadrant; |
|
|
return matchesSearch && matchesQuadrant; |
|
|
}); |
|
|
|
|
|
populateTable(filteredModels); |
|
|
} |
|
|
|
|
|
searchInput.addEventListener('input', applyFilters); |
|
|
quadrantFilter.addEventListener('change', applyFilters); |
|
|
} |
|
|
|
|
|
|
|
|
function createAllQuadrantsChart() { |
|
|
const ctx = document.getElementById('allQuadrantsChart').getContext('2d'); |
|
|
|
|
|
const quadrantColors = { |
|
|
'Low Cost / High Context': '#10b981', |
|
|
'High Cost / High Context': '#2563eb', |
|
|
'Low Cost / Low Context': '#f59e0b', |
|
|
'High Cost / Low Context': '#ef4444' |
|
|
}; |
|
|
|
|
|
const datasets = Object.keys(quadrantColors).map(quadrant => { |
|
|
const models = allModels.filter(m => m.quadrant === quadrant); |
|
|
return { |
|
|
label: quadrant, |
|
|
data: models.map(m => ({ |
|
|
x: m.avg_cost, |
|
|
y: m.context_length / 1000, |
|
|
model: m |
|
|
})), |
|
|
backgroundColor: quadrantColors[quadrant] + '80', |
|
|
borderColor: quadrantColors[quadrant], |
|
|
borderWidth: 2, |
|
|
pointRadius: 5, |
|
|
pointHoverRadius: 8 |
|
|
}; |
|
|
}); |
|
|
|
|
|
const medianCost = median(allModels.map(m => m.avg_cost)); |
|
|
const medianContext = median(allModels.map(m => m.context_length / 1000)); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'scatter', |
|
|
data: { datasets }, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
title: { |
|
|
display: false |
|
|
}, |
|
|
legend: { |
|
|
display: true, |
|
|
position: 'top' |
|
|
}, |
|
|
tooltip: { |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
const model = context.raw.model; |
|
|
return [ |
|
|
model.model_name, |
|
|
`Vendor: ${model.displayVendor}`, |
|
|
`Context: ${(model.context_length / 1000).toFixed(0)}K tokens`, |
|
|
`Input: $${model.input_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Output: $${model.output_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Avg: $${model.avg_cost.toFixed(2)}/M` |
|
|
]; |
|
|
} |
|
|
} |
|
|
}, |
|
|
annotation: { |
|
|
annotations: { |
|
|
verticalLine: { |
|
|
type: 'line', |
|
|
xMin: medianCost, |
|
|
xMax: medianCost, |
|
|
borderColor: '#64748b', |
|
|
borderWidth: 2, |
|
|
borderDash: [5, 5], |
|
|
label: { |
|
|
display: true, |
|
|
content: `Median: $${medianCost.toFixed(2)}`, |
|
|
position: 'start' |
|
|
} |
|
|
}, |
|
|
horizontalLine: { |
|
|
type: 'line', |
|
|
yMin: medianContext, |
|
|
yMax: medianContext, |
|
|
borderColor: '#64748b', |
|
|
borderWidth: 2, |
|
|
borderDash: [5, 5], |
|
|
label: { |
|
|
display: true, |
|
|
content: `Median: ${medianContext.toFixed(0)}K`, |
|
|
position: 'end' |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
type: 'logarithmic', |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Average Cost ($/M tokens, log scale)' |
|
|
}, |
|
|
min: 0.01 |
|
|
}, |
|
|
y: { |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Context Window (K tokens)' |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
plugins: [window['chartjs-plugin-annotation']] |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function createIndividualQuadrantCharts() { |
|
|
const quadrants = [ |
|
|
{ name: 'Low Cost / High Context', id: 'lchc', color: '#10b981' }, |
|
|
{ name: 'High Cost / High Context', id: 'hchc', color: '#2563eb' }, |
|
|
{ name: 'Low Cost / Low Context', id: 'lclc', color: '#f59e0b' }, |
|
|
{ name: 'High Cost / Low Context', id: 'hclc', color: '#ef4444' } |
|
|
]; |
|
|
|
|
|
quadrants.forEach(quadrant => { |
|
|
const ctx = document.getElementById(`quadrant-${quadrant.id}`).getContext('2d'); |
|
|
const models = allModels.filter(m => m.quadrant === quadrant.name); |
|
|
|
|
|
if (models.length === 0) return; |
|
|
|
|
|
const data = models.map(m => ({ |
|
|
x: m.avg_cost, |
|
|
y: m.context_length / 1000, |
|
|
model: m |
|
|
})); |
|
|
|
|
|
new Chart(ctx, { |
|
|
type: 'scatter', |
|
|
data: { |
|
|
datasets: [{ |
|
|
label: quadrant.name, |
|
|
data: data, |
|
|
backgroundColor: quadrant.color + '80', |
|
|
borderColor: quadrant.color, |
|
|
borderWidth: 2, |
|
|
pointRadius: 6, |
|
|
pointHoverRadius: 9 |
|
|
}] |
|
|
}, |
|
|
options: { |
|
|
responsive: true, |
|
|
maintainAspectRatio: false, |
|
|
plugins: { |
|
|
title: { |
|
|
display: false |
|
|
}, |
|
|
legend: { |
|
|
display: false |
|
|
}, |
|
|
tooltip: { |
|
|
callbacks: { |
|
|
label: function(context) { |
|
|
const model = context.raw.model; |
|
|
return [ |
|
|
model.model_name, |
|
|
`Vendor: ${model.displayVendor}`, |
|
|
`Context: ${(model.context_length / 1000).toFixed(0)}K tokens`, |
|
|
`Input: $${model.input_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Output: $${model.output_price_usd_per_m.toFixed(2)}/M`, |
|
|
`Avg: $${model.avg_cost.toFixed(2)}/M` |
|
|
]; |
|
|
} |
|
|
} |
|
|
} |
|
|
}, |
|
|
scales: { |
|
|
x: { |
|
|
type: 'logarithmic', |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Average Cost ($/M tokens, log scale)', |
|
|
font: { |
|
|
size: 11 |
|
|
} |
|
|
} |
|
|
}, |
|
|
y: { |
|
|
title: { |
|
|
display: true, |
|
|
text: 'Context (K tokens)', |
|
|
font: { |
|
|
size: 11 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', loadData); |
|
|
|