Add output/input price multiple analysis
Browse files- Calculate output/input price multiple for all models (median: 4.00x)
- Add Out/In Multiple column to main data table with color coding
- Update statistics to show median input ($0.40/M) and output ($1.60/M) prices
- Create dedicated output/input-analysis.html page with:
* Comprehensive explanation of what the metric means
* Important caveats about prompt caching and bulk pricing
* Quadrant chart: multiple vs average cost
* Distribution bar chart showing multiple ranges
* Filterable table sorted by multiple
- Update cost tier threshold: "very high" now = $40+/M (was $5+)
- Add navigation links between main dashboard and analysis page
- Add info-box and warning-box CSS styles for contextual content
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add_output_input_multiple.py +40 -0
- app.js +32 -7
- index.html +26 -6
- output-input-analysis.html +125 -0
- output-input-analysis.js +440 -0
- quadrants.csv +0 -0
- style.css +88 -0
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Add output/input price multiple to the quadrants CSV.
|
| 4 |
+
This calculates how many times more expensive output tokens are compared to input tokens.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import pandas as pd
|
| 8 |
+
|
| 9 |
+
# Read the CSV
|
| 10 |
+
df = pd.read_csv('quadrants.csv')
|
| 11 |
+
|
| 12 |
+
# Calculate output/input multiple
|
| 13 |
+
# Handle edge cases where input price might be 0 (shouldn't happen, but be safe)
|
| 14 |
+
df['output_input_multiple'] = df.apply(
|
| 15 |
+
lambda row: row['output_price_usd_per_m'] / row['input_price_usd_per_m']
|
| 16 |
+
if row['input_price_usd_per_m'] > 0 else 0,
|
| 17 |
+
axis=1
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
# Save updated CSV
|
| 21 |
+
df.to_csv('quadrants.csv', index=False)
|
| 22 |
+
|
| 23 |
+
# Calculate and display statistics
|
| 24 |
+
median_multiple = df['output_input_multiple'].median()
|
| 25 |
+
mean_multiple = df['output_input_multiple'].mean()
|
| 26 |
+
min_multiple = df['output_input_multiple'].min()
|
| 27 |
+
max_multiple = df['output_input_multiple'].max()
|
| 28 |
+
|
| 29 |
+
print(f"Output/Input Price Multiple Statistics:")
|
| 30 |
+
print(f" Median: {median_multiple:.2f}x")
|
| 31 |
+
print(f" Mean: {mean_multiple:.2f}x")
|
| 32 |
+
print(f" Min: {min_multiple:.2f}x")
|
| 33 |
+
print(f" Max: {max_multiple:.2f}x")
|
| 34 |
+
print(f"\nTotal models: {len(df)}")
|
| 35 |
+
print(f"\nModels by output/input multiple:")
|
| 36 |
+
print(f" 1x (equal pricing): {len(df[df['output_input_multiple'] == 1])}")
|
| 37 |
+
print(f" <2x: {len(df[df['output_input_multiple'] < 2])}")
|
| 38 |
+
print(f" 2-5x: {len(df[(df['output_input_multiple'] >= 2) & (df['output_input_multiple'] < 5)])}")
|
| 39 |
+
print(f" 5-10x: {len(df[(df['output_input_multiple'] >= 5) & (df['output_input_multiple'] < 10)])}")
|
| 40 |
+
print(f" 10x+: {len(df[df['output_input_multiple'] >= 10])}")
|
|
@@ -78,6 +78,7 @@ function initializeApp() {
|
|
| 78 |
createQuadrantChart();
|
| 79 |
createAllQuadrantsChart();
|
| 80 |
createIndividualQuadrantCharts();
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
// Setup tab navigation
|
|
@@ -104,24 +105,36 @@ function setupTabs() {
|
|
| 104 |
function updateStats() {
|
| 105 |
const costs = allModels.map(m => m.avg_cost);
|
| 106 |
const contexts = allModels.map(m => m.context_length);
|
| 107 |
-
const
|
| 108 |
-
const
|
|
|
|
| 109 |
const minContext = Math.min(...contexts);
|
| 110 |
const maxContext = Math.max(...contexts);
|
| 111 |
const vendors = new Set(allModels.map(m => m.displayVendor)).size;
|
| 112 |
|
| 113 |
-
// Calculate medians
|
| 114 |
const medianCost = median(costs);
|
| 115 |
const medianContext = median(contexts);
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
document.getElementById('total-models').textContent = allModels.length;
|
| 118 |
-
document.getElementById('price-range').textContent = `$${minCost.toFixed(2)} - $${maxCost.toFixed(2)}`;
|
| 119 |
-
document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`;
|
| 120 |
document.getElementById('vendors').textContent = vendors;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
// Update methodology section with actual median values
|
| 123 |
document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`;
|
| 124 |
document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
// Calculate median for quadrant lines
|
|
@@ -250,8 +263,8 @@ function getCostTier(cost) {
|
|
| 250 |
if (cost < 0.10) return 'cost-very-low';
|
| 251 |
if (cost < 0.50) return 'cost-low';
|
| 252 |
if (cost < 2.00) return 'cost-medium';
|
| 253 |
-
if (cost <
|
| 254 |
-
return 'cost-very-high';
|
| 255 |
}
|
| 256 |
|
| 257 |
// Get tier class for context
|
|
@@ -263,6 +276,15 @@ function getContextTier(context) {
|
|
| 263 |
return 'context-ultra';
|
| 264 |
}
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
// Clean up model name by removing redundant vendor prefix
|
| 267 |
function cleanModelName(modelName, vendor) {
|
| 268 |
// Common patterns to remove
|
|
@@ -333,6 +355,9 @@ function populateTable(models = filteredModels) {
|
|
| 333 |
<td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;">
|
| 334 |
$${m.output_price_usd_per_m.toFixed(2)}
|
| 335 |
</td>
|
|
|
|
|
|
|
|
|
|
| 336 |
<td><span class="quadrant-badge">${m.quadrant}</span></td>
|
| 337 |
</tr>
|
| 338 |
`;
|
|
|
|
| 78 |
createQuadrantChart();
|
| 79 |
createAllQuadrantsChart();
|
| 80 |
createIndividualQuadrantCharts();
|
| 81 |
+
createMultipleQuadrantChart();
|
| 82 |
}
|
| 83 |
|
| 84 |
// Setup tab navigation
|
|
|
|
| 105 |
function updateStats() {
|
| 106 |
const costs = allModels.map(m => m.avg_cost);
|
| 107 |
const contexts = allModels.map(m => m.context_length);
|
| 108 |
+
const inputPrices = allModels.map(m => m.input_price_usd_per_m);
|
| 109 |
+
const outputPrices = allModels.map(m => m.output_price_usd_per_m);
|
| 110 |
+
const multiples = allModels.map(m => m.output_input_multiple);
|
| 111 |
const minContext = Math.min(...contexts);
|
| 112 |
const maxContext = Math.max(...contexts);
|
| 113 |
const vendors = new Set(allModels.map(m => m.displayVendor)).size;
|
| 114 |
|
| 115 |
+
// Calculate medians
|
| 116 |
const medianCost = median(costs);
|
| 117 |
const medianContext = median(contexts);
|
| 118 |
+
const medianInput = median(inputPrices);
|
| 119 |
+
const medianOutput = median(outputPrices);
|
| 120 |
+
const medianMultiple = median(multiples);
|
| 121 |
|
| 122 |
document.getElementById('total-models').textContent = allModels.length;
|
|
|
|
|
|
|
| 123 |
document.getElementById('vendors').textContent = vendors;
|
| 124 |
+
document.getElementById('median-input').textContent = `$${medianInput.toFixed(2)}`;
|
| 125 |
+
document.getElementById('median-output').textContent = `$${medianOutput.toFixed(2)}`;
|
| 126 |
+
document.getElementById('median-multiple').textContent = `${medianMultiple.toFixed(2)}x`;
|
| 127 |
+
document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`;
|
| 128 |
|
| 129 |
// Update methodology section with actual median values
|
| 130 |
document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`;
|
| 131 |
document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`;
|
| 132 |
+
|
| 133 |
+
// Update multiple display if it exists
|
| 134 |
+
const multipleDisplay = document.getElementById('median-multiple-display');
|
| 135 |
+
if (multipleDisplay) {
|
| 136 |
+
multipleDisplay.textContent = `${medianMultiple.toFixed(2)}x`;
|
| 137 |
+
}
|
| 138 |
}
|
| 139 |
|
| 140 |
// Calculate median for quadrant lines
|
|
|
|
| 263 |
if (cost < 0.10) return 'cost-very-low';
|
| 264 |
if (cost < 0.50) return 'cost-low';
|
| 265 |
if (cost < 2.00) return 'cost-medium';
|
| 266 |
+
if (cost < 40.00) return 'cost-high';
|
| 267 |
+
return 'cost-very-high'; // $40+ per million tokens
|
| 268 |
}
|
| 269 |
|
| 270 |
// Get tier class for context
|
|
|
|
| 276 |
return 'context-ultra';
|
| 277 |
}
|
| 278 |
|
| 279 |
+
// Get tier class for output/input multiple
|
| 280 |
+
function getMultipleTier(multiple) {
|
| 281 |
+
if (multiple <= 1) return 'multiple-equal'; // Equal or cheaper output
|
| 282 |
+
if (multiple < 2) return 'multiple-low'; // Less than 2x
|
| 283 |
+
if (multiple < 5) return 'multiple-medium'; // 2-5x (most common)
|
| 284 |
+
if (multiple < 10) return 'multiple-high'; // 5-10x
|
| 285 |
+
return 'multiple-very-high'; // 10x+
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
// Clean up model name by removing redundant vendor prefix
|
| 289 |
function cleanModelName(modelName, vendor) {
|
| 290 |
// Common patterns to remove
|
|
|
|
| 355 |
<td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;">
|
| 356 |
$${m.output_price_usd_per_m.toFixed(2)}
|
| 357 |
</td>
|
| 358 |
+
<td class="${getMultipleTier(m.output_input_multiple)}" style="font-weight: 600;">
|
| 359 |
+
${m.output_input_multiple.toFixed(2)}x
|
| 360 |
+
</td>
|
| 361 |
<td><span class="quadrant-badge">${m.quadrant}</span></td>
|
| 362 |
</tr>
|
| 363 |
`;
|
|
@@ -14,6 +14,9 @@
|
|
| 14 |
<h1>Agentic LLM Price Comparison</h1>
|
| 15 |
<p class="subtitle">Cost-effectiveness analysis of 218 tool-calling models from OpenRouter</p>
|
| 16 |
<p class="date">Data snapshot: November 8, 2025</p>
|
|
|
|
|
|
|
|
|
|
| 17 |
</header>
|
| 18 |
|
| 19 |
<main>
|
|
@@ -51,16 +54,24 @@
|
|
| 51 |
<div class="stat-label">Models Analyzed</div>
|
| 52 |
</div>
|
| 53 |
<div class="stat-card">
|
| 54 |
-
<div class="stat-number" id="
|
| 55 |
-
<div class="stat-label">
|
| 56 |
</div>
|
| 57 |
<div class="stat-card">
|
| 58 |
-
<div class="stat-number" id="
|
| 59 |
-
<div class="stat-label">
|
| 60 |
</div>
|
| 61 |
<div class="stat-card">
|
| 62 |
-
<div class="stat-number" id="
|
| 63 |
-
<div class="stat-label">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
</div>
|
| 65 |
</section>
|
| 66 |
|
|
@@ -125,6 +136,7 @@
|
|
| 125 |
<th class="sortable" data-sort="context_length">Context <span class="sort-indicator"></span></th>
|
| 126 |
<th class="sortable" data-sort="input_price_usd_per_m">Input ($/M) <span class="sort-indicator"></span></th>
|
| 127 |
<th class="sortable" data-sort="output_price_usd_per_m">Output ($/M) <span class="sort-indicator"></span></th>
|
|
|
|
| 128 |
<th class="sortable" data-sort="quadrant">Quadrant <span class="sort-indicator"></span></th>
|
| 129 |
</tr>
|
| 130 |
</thead>
|
|
@@ -209,6 +221,14 @@
|
|
| 209 |
<canvas id="quadrantChart"></canvas>
|
| 210 |
</div>
|
| 211 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
</div>
|
| 213 |
</main>
|
| 214 |
|
|
|
|
| 14 |
<h1>Agentic LLM Price Comparison</h1>
|
| 15 |
<p class="subtitle">Cost-effectiveness analysis of 218 tool-calling models from OpenRouter</p>
|
| 16 |
<p class="date">Data snapshot: November 8, 2025</p>
|
| 17 |
+
<nav class="header-nav">
|
| 18 |
+
<a href="output-input-analysis.html">🔢 Output/Input Price Analysis →</a>
|
| 19 |
+
</nav>
|
| 20 |
</header>
|
| 21 |
|
| 22 |
<main>
|
|
|
|
| 54 |
<div class="stat-label">Models Analyzed</div>
|
| 55 |
</div>
|
| 56 |
<div class="stat-card">
|
| 57 |
+
<div class="stat-number" id="vendors">-</div>
|
| 58 |
+
<div class="stat-label">Vendors</div>
|
| 59 |
</div>
|
| 60 |
<div class="stat-card">
|
| 61 |
+
<div class="stat-number" id="median-input">-</div>
|
| 62 |
+
<div class="stat-label">Median Input ($/M)</div>
|
| 63 |
</div>
|
| 64 |
<div class="stat-card">
|
| 65 |
+
<div class="stat-number" id="median-output">-</div>
|
| 66 |
+
<div class="stat-label">Median Output ($/M)</div>
|
| 67 |
+
</div>
|
| 68 |
+
<div class="stat-card">
|
| 69 |
+
<div class="stat-number" id="median-multiple">-</div>
|
| 70 |
+
<div class="stat-label">Median Out/In Multiple</div>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="stat-card">
|
| 73 |
+
<div class="stat-number" id="context-range">-</div>
|
| 74 |
+
<div class="stat-label">Context Range</div>
|
| 75 |
</div>
|
| 76 |
</section>
|
| 77 |
|
|
|
|
| 136 |
<th class="sortable" data-sort="context_length">Context <span class="sort-indicator"></span></th>
|
| 137 |
<th class="sortable" data-sort="input_price_usd_per_m">Input ($/M) <span class="sort-indicator"></span></th>
|
| 138 |
<th class="sortable" data-sort="output_price_usd_per_m">Output ($/M) <span class="sort-indicator"></span></th>
|
| 139 |
+
<th class="sortable" data-sort="output_input_multiple">Out/In Multiple <span class="sort-indicator"></span></th>
|
| 140 |
<th class="sortable" data-sort="quadrant">Quadrant <span class="sort-indicator"></span></th>
|
| 141 |
</tr>
|
| 142 |
</thead>
|
|
|
|
| 221 |
<canvas id="quadrantChart"></canvas>
|
| 222 |
</div>
|
| 223 |
</section>
|
| 224 |
+
|
| 225 |
+
<section class="chart-section">
|
| 226 |
+
<h2>Output/Input Price Multiple Analysis</h2>
|
| 227 |
+
<p>This chart shows how much more expensive output tokens are compared to input tokens (median: <strong><span id="median-multiple-display">4.00x</span></strong>). Lower multiples are better for generation-heavy workloads.</p>
|
| 228 |
+
<div class="chart-container">
|
| 229 |
+
<canvas id="multipleQuadrantChart"></canvas>
|
| 230 |
+
</div>
|
| 231 |
+
</section>
|
| 232 |
</div>
|
| 233 |
</main>
|
| 234 |
|
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Output/Input Price Multiple Analysis - LLM Pricing</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
|
| 9 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1"></script>
|
| 10 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<header>
|
| 14 |
+
<h1>🔢 Output/Input Price Multiple Analysis</h1>
|
| 15 |
+
<p class="subtitle">Understanding the Cost Differential Between Input and Output Tokens</p>
|
| 16 |
+
<nav class="header-nav">
|
| 17 |
+
<a href="index.html">← Back to Main Dashboard</a>
|
| 18 |
+
</nav>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<main class="container">
|
| 22 |
+
<section class="methodology">
|
| 23 |
+
<h2>Understanding the Output/Input Multiple</h2>
|
| 24 |
+
<div class="info-box">
|
| 25 |
+
<h3>What is the Output/Input Multiple?</h3>
|
| 26 |
+
<p>The output/input multiple shows how much more expensive <strong>output tokens</strong> (generation) are compared to <strong>input tokens</strong> (prompt processing). For example, a 4x multiple means that generating tokens costs 4 times more than reading them.</p>
|
| 27 |
+
|
| 28 |
+
<p><strong>Median Multiple: <span id="median-multiple-stat">4.00x</span></strong></p>
|
| 29 |
+
|
| 30 |
+
<h4>Why Does This Matter?</h4>
|
| 31 |
+
<ul>
|
| 32 |
+
<li><strong>Generation-Heavy Workloads:</strong> If your application generates long outputs (content creation, code generation, long-form answers), lower multiples save you money</li>
|
| 33 |
+
<li><strong>Analysis-Heavy Workloads:</strong> If you send large prompts but get short answers (document analysis, classification), the multiple matters less</li>
|
| 34 |
+
<li><strong>Batch Processing:</strong> For agentic workflows with multiple round-trips, output from one call becomes input for the next - multiples compound</li>
|
| 35 |
+
</ul>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div class="warning-box">
|
| 39 |
+
<h3>⚠️ Important Caveats</h3>
|
| 40 |
+
<p><strong>These ratios likely understate the actual cost differential</strong> because they don't account for:</p>
|
| 41 |
+
<ul>
|
| 42 |
+
<li><strong>Prompt Caching:</strong> Many providers (Anthropic, OpenAI, Google) offer prompt caching that significantly reduces input token costs for repeated prefixes. When caching is active, the effective cost of input tokens can drop to 10% or even 1% of the listed rate, making the output/input multiple dramatically higher in practice.</li>
|
| 43 |
+
<li><strong>Bulk Pricing:</strong> Some providers offer volume discounts on input tokens or special pricing for batch API calls, which aren't reflected in the base per-token rates shown here.</li>
|
| 44 |
+
<li><strong>Free Tiers:</strong> Certain context amounts may be free or discounted (e.g., first N tokens free), affecting the real-world cost structure.</li>
|
| 45 |
+
</ul>
|
| 46 |
+
<p class="emphasis">In real-world scenarios with prompt caching and bulk discounts, output tokens often cost 10x-100x more than input tokens, not just the 2-5x shown in base pricing.</p>
|
| 47 |
+
</div>
|
| 48 |
+
</section>
|
| 49 |
+
|
| 50 |
+
<section class="stats-grid">
|
| 51 |
+
<div class="stat-card">
|
| 52 |
+
<div class="stat-number" id="total-models">198</div>
|
| 53 |
+
<div class="stat-label">Models Analyzed</div>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="stat-card">
|
| 56 |
+
<div class="stat-number" id="median-multiple">4.00x</div>
|
| 57 |
+
<div class="stat-label">Median Multiple</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="stat-card">
|
| 60 |
+
<div class="stat-number" id="equal-pricing-count">-</div>
|
| 61 |
+
<div class="stat-label">Equal Pricing (1x)</div>
|
| 62 |
+
</div>
|
| 63 |
+
<div class="stat-card">
|
| 64 |
+
<div class="stat-number" id="high-multiple-count">-</div>
|
| 65 |
+
<div class="stat-label">High Multiple (5x+)</div>
|
| 66 |
+
</div>
|
| 67 |
+
</section>
|
| 68 |
+
|
| 69 |
+
<section class="chart-section">
|
| 70 |
+
<h2>Output/Input Multiple vs Average Cost</h2>
|
| 71 |
+
<p>This quadrant chart shows how the output/input pricing structure relates to overall model cost. Models are divided by the median multiple (<strong><span id="median-line-value">4.00x</span></strong>) and median average cost.</p>
|
| 72 |
+
<div class="chart-container">
|
| 73 |
+
<canvas id="multipleQuadrantChart"></canvas>
|
| 74 |
+
</div>
|
| 75 |
+
</section>
|
| 76 |
+
|
| 77 |
+
<section class="chart-section">
|
| 78 |
+
<h2>Distribution of Output/Input Multiples</h2>
|
| 79 |
+
<p>How common are different pricing structures across all models?</p>
|
| 80 |
+
<div class="chart-container">
|
| 81 |
+
<canvas id="multipleDistribution"></canvas>
|
| 82 |
+
</div>
|
| 83 |
+
</section>
|
| 84 |
+
|
| 85 |
+
<section class="data-section">
|
| 86 |
+
<h2>All Models by Output/Input Multiple</h2>
|
| 87 |
+
<div class="table-controls">
|
| 88 |
+
<input type="text" id="search" placeholder="Search models or vendors..." />
|
| 89 |
+
<select id="multiple-filter">
|
| 90 |
+
<option value="">All Multiples</option>
|
| 91 |
+
<option value="equal">Equal Pricing (≤1x)</option>
|
| 92 |
+
<option value="low">Low Multiple (<2x)</option>
|
| 93 |
+
<option value="medium">Medium Multiple (2-5x)</option>
|
| 94 |
+
<option value="high">High Multiple (5-10x)</option>
|
| 95 |
+
<option value="very-high">Very High (10x+)</option>
|
| 96 |
+
</select>
|
| 97 |
+
</div>
|
| 98 |
+
<div class="table-wrapper">
|
| 99 |
+
<table id="models-table">
|
| 100 |
+
<thead>
|
| 101 |
+
<tr>
|
| 102 |
+
<th class="sortable" data-sort="model_name">Model Name <span class="sort-indicator"></span></th>
|
| 103 |
+
<th class="sortable" data-sort="displayVendor">Vendor <span class="sort-indicator"></span></th>
|
| 104 |
+
<th class="sortable" data-sort="input_price_usd_per_m">Input ($/M) <span class="sort-indicator"></span></th>
|
| 105 |
+
<th class="sortable" data-sort="output_price_usd_per_m">Output ($/M) <span class="sort-indicator"></span></th>
|
| 106 |
+
<th class="sortable" data-sort="output_input_multiple">Out/In Multiple <span class="sort-indicator"></span></th>
|
| 107 |
+
<th class="sortable" data-sort="avg_cost">Avg Cost ($/M) <span class="sort-indicator"></span></th>
|
| 108 |
+
</tr>
|
| 109 |
+
</thead>
|
| 110 |
+
<tbody id="table-body">
|
| 111 |
+
<!-- Populated by JavaScript -->
|
| 112 |
+
</tbody>
|
| 113 |
+
</table>
|
| 114 |
+
</div>
|
| 115 |
+
</section>
|
| 116 |
+
</main>
|
| 117 |
+
|
| 118 |
+
<footer>
|
| 119 |
+
<p>Data source: <a href="https://openrouter.ai/" target="_blank">OpenRouter API</a> | <a href="index.html">Main Dashboard</a></p>
|
| 120 |
+
<p class="disclaimer">Prices are subject to change. Actual costs may vary significantly with prompt caching and volume discounts.</p>
|
| 121 |
+
</footer>
|
| 122 |
+
|
| 123 |
+
<script src="output-input-analysis.js"></script>
|
| 124 |
+
</body>
|
| 125 |
+
</html>
|
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Global data storage
|
| 2 |
+
let allModels = [];
|
| 3 |
+
let filteredModels = [];
|
| 4 |
+
|
| 5 |
+
// Vendor name mapping (same as main app)
|
| 6 |
+
const vendorNameMap = {
|
| 7 |
+
'qwen': 'Qwen',
|
| 8 |
+
'meta-llama': 'Meta',
|
| 9 |
+
'x-ai': 'xAI',
|
| 10 |
+
'z-ai': 'Zhipu AI',
|
| 11 |
+
'google': 'Google',
|
| 12 |
+
'openai': 'OpenAI',
|
| 13 |
+
'anthropic': 'Anthropic',
|
| 14 |
+
'mistralai': 'Mistral AI',
|
| 15 |
+
'deepseek': 'DeepSeek',
|
| 16 |
+
'alibaba': 'Alibaba',
|
| 17 |
+
'amazon': 'Amazon',
|
| 18 |
+
'microsoft': 'Microsoft',
|
| 19 |
+
'nvidia': 'NVIDIA',
|
| 20 |
+
'cohere': 'Cohere',
|
| 21 |
+
'ai21': 'AI21 Labs',
|
| 22 |
+
'minimax': 'MiniMax',
|
| 23 |
+
'moonshotai': 'Moonshot AI',
|
| 24 |
+
'stepfun-ai': 'StepFun',
|
| 25 |
+
'inclusionai': 'Inclusion AI',
|
| 26 |
+
'deepcogito': 'Deep Cogito',
|
| 27 |
+
'baidu': 'Baidu',
|
| 28 |
+
'nousresearch': 'Nous Research',
|
| 29 |
+
'arcee-ai': 'Arcee AI',
|
| 30 |
+
'inception': 'Inception',
|
| 31 |
+
'sao10k': 'Sao10K',
|
| 32 |
+
'thedrummer': 'TheDrummer',
|
| 33 |
+
'tngtech': 'TNG Technology',
|
| 34 |
+
'meituan': 'Meituan'
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
function normalizeVendorName(vendor) {
|
| 38 |
+
return vendorNameMap[vendor] || vendor;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Load and process CSV data
|
| 42 |
+
async function loadData() {
|
| 43 |
+
try {
|
| 44 |
+
const response = await fetch('quadrants.csv');
|
| 45 |
+
const csvText = await response.text();
|
| 46 |
+
|
| 47 |
+
Papa.parse(csvText, {
|
| 48 |
+
header: true,
|
| 49 |
+
dynamicTyping: true,
|
| 50 |
+
complete: function(results) {
|
| 51 |
+
allModels = results.data.filter(row => row.model_name);
|
| 52 |
+
allModels.forEach(model => {
|
| 53 |
+
model.displayVendor = normalizeVendorName(model.vendor);
|
| 54 |
+
});
|
| 55 |
+
filteredModels = [...allModels];
|
| 56 |
+
initializeApp();
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
} catch (error) {
|
| 60 |
+
console.error('Error loading data:', error);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Sorting state
|
| 65 |
+
let currentSort = {
|
| 66 |
+
column: 'output_input_multiple',
|
| 67 |
+
direction: 'desc' // Start with highest multiples first
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
// Initialize application
|
| 71 |
+
function initializeApp() {
|
| 72 |
+
updateStats();
|
| 73 |
+
populateTable();
|
| 74 |
+
setupTableControls();
|
| 75 |
+
setupSorting();
|
| 76 |
+
createMultipleQuadrantChart();
|
| 77 |
+
createDistributionChart();
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Calculate median
|
| 81 |
+
function median(arr) {
|
| 82 |
+
const sorted = [...arr].sort((a, b) => a - b);
|
| 83 |
+
const mid = Math.floor(sorted.length / 2);
|
| 84 |
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Update statistics
|
| 88 |
+
function updateStats() {
|
| 89 |
+
const multiples = allModels.map(m => m.output_input_multiple);
|
| 90 |
+
const medianMultiple = median(multiples);
|
| 91 |
+
|
| 92 |
+
const equalPricing = allModels.filter(m => m.output_input_multiple <= 1).length;
|
| 93 |
+
const highMultiple = allModels.filter(m => m.output_input_multiple >= 5).length;
|
| 94 |
+
|
| 95 |
+
document.getElementById('total-models').textContent = allModels.length;
|
| 96 |
+
document.getElementById('median-multiple').textContent = `${medianMultiple.toFixed(2)}x`;
|
| 97 |
+
document.getElementById('median-multiple-stat').textContent = `${medianMultiple.toFixed(2)}x`;
|
| 98 |
+
document.getElementById('median-line-value').textContent = `${medianMultiple.toFixed(2)}x`;
|
| 99 |
+
document.getElementById('equal-pricing-count').textContent = equalPricing;
|
| 100 |
+
document.getElementById('high-multiple-count').textContent = highMultiple;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Get tier class for cost
|
| 104 |
+
function getCostTier(cost) {
|
| 105 |
+
if (cost < 0.10) return 'cost-very-low';
|
| 106 |
+
if (cost < 0.50) return 'cost-low';
|
| 107 |
+
if (cost < 2.00) return 'cost-medium';
|
| 108 |
+
if (cost < 40.00) return 'cost-high';
|
| 109 |
+
return 'cost-very-high';
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Get tier class for output/input multiple
|
| 113 |
+
function getMultipleTier(multiple) {
|
| 114 |
+
if (multiple <= 1) return 'multiple-equal';
|
| 115 |
+
if (multiple < 2) return 'multiple-low';
|
| 116 |
+
if (multiple < 5) return 'multiple-medium';
|
| 117 |
+
if (multiple < 10) return 'multiple-high';
|
| 118 |
+
return 'multiple-very-high';
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Clean model name
|
| 122 |
+
function cleanModelName(modelName, vendor) {
|
| 123 |
+
const patterns = [
|
| 124 |
+
new RegExp(`^${vendor}:\\s*`, 'i'),
|
| 125 |
+
/^Qwen:\s*/i, /^Meta:\s*/i, /^Google:\s*/i, /^OpenAI:\s*/i,
|
| 126 |
+
/^Anthropic:\s*/i, /^DeepSeek:\s*/i, /^Mistral:\s*/i,
|
| 127 |
+
/^NVIDIA:\s*/i, /^Amazon:\s*/i, /^Microsoft:\s*/i,
|
| 128 |
+
/^xAI:\s*/i, /^Zhipu AI:\s*/i, /^MoonshotAI:\s*/i,
|
| 129 |
+
/^Moonshot AI:\s*/i, /^Alibaba:\s*/i
|
| 130 |
+
];
|
| 131 |
+
let cleaned = modelName;
|
| 132 |
+
for (const pattern of patterns) {
|
| 133 |
+
cleaned = cleaned.replace(pattern, '');
|
| 134 |
+
}
|
| 135 |
+
return cleaned;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// Populate table
|
| 139 |
+
function populateTable(models = filteredModels) {
|
| 140 |
+
const tbody = document.getElementById('table-body');
|
| 141 |
+
|
| 142 |
+
const sortedModels = [...models].sort((a, b) => {
|
| 143 |
+
let aVal = a[currentSort.column];
|
| 144 |
+
let bVal = b[currentSort.column];
|
| 145 |
+
|
| 146 |
+
if (typeof aVal === 'string') {
|
| 147 |
+
aVal = aVal.toLowerCase();
|
| 148 |
+
bVal = bVal.toLowerCase();
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
if (currentSort.direction === 'asc') {
|
| 152 |
+
return aVal > bVal ? 1 : -1;
|
| 153 |
+
} else {
|
| 154 |
+
return aVal < bVal ? 1 : -1;
|
| 155 |
+
}
|
| 156 |
+
});
|
| 157 |
+
|
| 158 |
+
let lastVendor = null;
|
| 159 |
+
tbody.innerHTML = sortedModels.map((m, index) => {
|
| 160 |
+
const isNewVendor = m.displayVendor !== lastVendor;
|
| 161 |
+
lastVendor = m.displayVendor;
|
| 162 |
+
const vendorClass = isNewVendor ? 'vendor-group-start' : '';
|
| 163 |
+
|
| 164 |
+
return `
|
| 165 |
+
<tr class="${vendorClass}" data-vendor="${m.displayVendor}">
|
| 166 |
+
<td>${cleanModelName(m.model_name, m.displayVendor)}</td>
|
| 167 |
+
<td class="vendor-cell">${m.displayVendor}</td>
|
| 168 |
+
<td class="${getCostTier(m.input_price_usd_per_m)}" style="font-weight: 600;">
|
| 169 |
+
$${m.input_price_usd_per_m.toFixed(2)}
|
| 170 |
+
</td>
|
| 171 |
+
<td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;">
|
| 172 |
+
$${m.output_price_usd_per_m.toFixed(2)}
|
| 173 |
+
</td>
|
| 174 |
+
<td class="${getMultipleTier(m.output_input_multiple)}" style="font-weight: 600;">
|
| 175 |
+
${m.output_input_multiple.toFixed(2)}x
|
| 176 |
+
</td>
|
| 177 |
+
<td class="${getCostTier(m.avg_cost)}" style="font-weight: 600;">
|
| 178 |
+
$${m.avg_cost.toFixed(2)}
|
| 179 |
+
</td>
|
| 180 |
+
</tr>
|
| 181 |
+
`;
|
| 182 |
+
}).join('');
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Setup sorting
|
| 186 |
+
function setupSorting() {
|
| 187 |
+
const headers = document.querySelectorAll('th.sortable');
|
| 188 |
+
|
| 189 |
+
headers.forEach(header => {
|
| 190 |
+
header.addEventListener('click', () => {
|
| 191 |
+
const column = header.dataset.sort;
|
| 192 |
+
|
| 193 |
+
if (currentSort.column === column) {
|
| 194 |
+
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
| 195 |
+
} else {
|
| 196 |
+
currentSort.column = column;
|
| 197 |
+
currentSort.direction = 'asc';
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
headers.forEach(h => {
|
| 201 |
+
h.classList.remove('sort-asc', 'sort-desc');
|
| 202 |
+
});
|
| 203 |
+
header.classList.add(`sort-${currentSort.direction}`);
|
| 204 |
+
|
| 205 |
+
populateTable(filteredModels);
|
| 206 |
+
});
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
+
// Set initial sort indicator
|
| 210 |
+
const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`);
|
| 211 |
+
if (initialHeader) {
|
| 212 |
+
initialHeader.classList.add('sort-desc');
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
// Setup table controls
|
| 217 |
+
function setupTableControls() {
|
| 218 |
+
const searchInput = document.getElementById('search');
|
| 219 |
+
const multipleFilter = document.getElementById('multiple-filter');
|
| 220 |
+
|
| 221 |
+
function applyFilters() {
|
| 222 |
+
const searchTerm = searchInput.value.toLowerCase();
|
| 223 |
+
const multipleCategory = multipleFilter.value;
|
| 224 |
+
|
| 225 |
+
filteredModels = allModels.filter(m => {
|
| 226 |
+
const matchesSearch = !searchTerm ||
|
| 227 |
+
m.model_name.toLowerCase().includes(searchTerm) ||
|
| 228 |
+
m.displayVendor.toLowerCase().includes(searchTerm);
|
| 229 |
+
|
| 230 |
+
let matchesMultiple = true;
|
| 231 |
+
if (multipleCategory === 'equal') matchesMultiple = m.output_input_multiple <= 1;
|
| 232 |
+
else if (multipleCategory === 'low') matchesMultiple = m.output_input_multiple < 2;
|
| 233 |
+
else if (multipleCategory === 'medium') matchesMultiple = m.output_input_multiple >= 2 && m.output_input_multiple < 5;
|
| 234 |
+
else if (multipleCategory === 'high') matchesMultiple = m.output_input_multiple >= 5 && m.output_input_multiple < 10;
|
| 235 |
+
else if (multipleCategory === 'very-high') matchesMultiple = m.output_input_multiple >= 10;
|
| 236 |
+
|
| 237 |
+
return matchesSearch && matchesMultiple;
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
populateTable(filteredModels);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
searchInput.addEventListener('input', applyFilters);
|
| 244 |
+
multipleFilter.addEventListener('change', applyFilters);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
// Create quadrant chart: Output/Input Multiple vs Average Cost
|
| 248 |
+
function createMultipleQuadrantChart() {
|
| 249 |
+
const ctx = document.getElementById('multipleQuadrantChart').getContext('2d');
|
| 250 |
+
|
| 251 |
+
const multiples = allModels.map(m => m.output_input_multiple);
|
| 252 |
+
const costs = allModels.map(m => m.avg_cost);
|
| 253 |
+
const medianMultiple = median(multiples);
|
| 254 |
+
const medianCost = median(costs);
|
| 255 |
+
|
| 256 |
+
// Divide into quadrants
|
| 257 |
+
const quadrants = {
|
| 258 |
+
'Low Multiple / Low Cost': { color: '#10b981', models: [] },
|
| 259 |
+
'High Multiple / Low Cost': { color: '#f59e0b', models: [] },
|
| 260 |
+
'Low Multiple / High Cost': { color: '#2563eb', models: [] },
|
| 261 |
+
'High Multiple / High Cost': { color: '#ef4444', models: [] }
|
| 262 |
+
};
|
| 263 |
+
|
| 264 |
+
allModels.forEach(m => {
|
| 265 |
+
const isLowMultiple = m.output_input_multiple < medianMultiple;
|
| 266 |
+
const isLowCost = m.avg_cost < medianCost;
|
| 267 |
+
|
| 268 |
+
if (isLowMultiple && isLowCost) quadrants['Low Multiple / Low Cost'].models.push(m);
|
| 269 |
+
else if (!isLowMultiple && isLowCost) quadrants['High Multiple / Low Cost'].models.push(m);
|
| 270 |
+
else if (isLowMultiple && !isLowCost) quadrants['Low Multiple / High Cost'].models.push(m);
|
| 271 |
+
else quadrants['High Multiple / High Cost'].models.push(m);
|
| 272 |
+
});
|
| 273 |
+
|
| 274 |
+
const datasets = Object.keys(quadrants).map(quadrant => {
|
| 275 |
+
const q = quadrants[quadrant];
|
| 276 |
+
return {
|
| 277 |
+
label: quadrant,
|
| 278 |
+
data: q.models.map(m => ({
|
| 279 |
+
x: m.output_input_multiple,
|
| 280 |
+
y: m.avg_cost,
|
| 281 |
+
model: m
|
| 282 |
+
})),
|
| 283 |
+
backgroundColor: q.color + '80',
|
| 284 |
+
borderColor: q.color,
|
| 285 |
+
borderWidth: 2,
|
| 286 |
+
pointRadius: 5,
|
| 287 |
+
pointHoverRadius: 8
|
| 288 |
+
};
|
| 289 |
+
});
|
| 290 |
+
|
| 291 |
+
new Chart(ctx, {
|
| 292 |
+
type: 'scatter',
|
| 293 |
+
data: { datasets },
|
| 294 |
+
options: {
|
| 295 |
+
responsive: true,
|
| 296 |
+
maintainAspectRatio: false,
|
| 297 |
+
plugins: {
|
| 298 |
+
legend: {
|
| 299 |
+
display: true,
|
| 300 |
+
position: 'top'
|
| 301 |
+
},
|
| 302 |
+
tooltip: {
|
| 303 |
+
callbacks: {
|
| 304 |
+
label: function(context) {
|
| 305 |
+
const model = context.raw.model;
|
| 306 |
+
return [
|
| 307 |
+
model.model_name,
|
| 308 |
+
`Vendor: ${model.displayVendor}`,
|
| 309 |
+
`Out/In Multiple: ${model.output_input_multiple.toFixed(2)}x`,
|
| 310 |
+
`Input: $${model.input_price_usd_per_m.toFixed(2)}/M`,
|
| 311 |
+
`Output: $${model.output_price_usd_per_m.toFixed(2)}/M`,
|
| 312 |
+
`Avg: $${model.avg_cost.toFixed(2)}/M`
|
| 313 |
+
];
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
+
},
|
| 317 |
+
annotation: {
|
| 318 |
+
annotations: {
|
| 319 |
+
verticalLine: {
|
| 320 |
+
type: 'line',
|
| 321 |
+
xMin: medianMultiple,
|
| 322 |
+
xMax: medianMultiple,
|
| 323 |
+
borderColor: '#64748b',
|
| 324 |
+
borderWidth: 2,
|
| 325 |
+
borderDash: [5, 5],
|
| 326 |
+
label: {
|
| 327 |
+
display: true,
|
| 328 |
+
content: `Median Multiple: ${medianMultiple.toFixed(2)}x`,
|
| 329 |
+
position: 'start'
|
| 330 |
+
}
|
| 331 |
+
},
|
| 332 |
+
horizontalLine: {
|
| 333 |
+
type: 'line',
|
| 334 |
+
yMin: medianCost,
|
| 335 |
+
yMax: medianCost,
|
| 336 |
+
borderColor: '#64748b',
|
| 337 |
+
borderWidth: 2,
|
| 338 |
+
borderDash: [5, 5],
|
| 339 |
+
label: {
|
| 340 |
+
display: true,
|
| 341 |
+
content: `Median Cost: $${medianCost.toFixed(2)}`,
|
| 342 |
+
position: 'end'
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
}
|
| 346 |
+
}
|
| 347 |
+
},
|
| 348 |
+
scales: {
|
| 349 |
+
x: {
|
| 350 |
+
title: {
|
| 351 |
+
display: true,
|
| 352 |
+
text: 'Output/Input Price Multiple'
|
| 353 |
+
}
|
| 354 |
+
},
|
| 355 |
+
y: {
|
| 356 |
+
type: 'logarithmic',
|
| 357 |
+
title: {
|
| 358 |
+
display: true,
|
| 359 |
+
text: 'Average Cost ($/M tokens, log scale)'
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
},
|
| 364 |
+
plugins: [window['chartjs-plugin-annotation']]
|
| 365 |
+
});
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
// Create distribution bar chart
|
| 369 |
+
function createDistributionChart() {
|
| 370 |
+
const ctx = document.getElementById('multipleDistribution').getContext('2d');
|
| 371 |
+
|
| 372 |
+
const bins = {
|
| 373 |
+
'≤1x (Equal)': allModels.filter(m => m.output_input_multiple <= 1).length,
|
| 374 |
+
'<2x (Low)': allModels.filter(m => m.output_input_multiple > 1 && m.output_input_multiple < 2).length,
|
| 375 |
+
'2-5x (Medium)': allModels.filter(m => m.output_input_multiple >= 2 && m.output_input_multiple < 5).length,
|
| 376 |
+
'5-10x (High)': allModels.filter(m => m.output_input_multiple >= 5 && m.output_input_multiple < 10).length,
|
| 377 |
+
'10x+ (Very High)': allModels.filter(m => m.output_input_multiple >= 10).length
|
| 378 |
+
};
|
| 379 |
+
|
| 380 |
+
new Chart(ctx, {
|
| 381 |
+
type: 'bar',
|
| 382 |
+
data: {
|
| 383 |
+
labels: Object.keys(bins),
|
| 384 |
+
datasets: [{
|
| 385 |
+
label: 'Number of Models',
|
| 386 |
+
data: Object.values(bins),
|
| 387 |
+
backgroundColor: [
|
| 388 |
+
'#10b981', // Equal
|
| 389 |
+
'#84cc16', // Low
|
| 390 |
+
'#fde047', // Medium
|
| 391 |
+
'#f97316', // High
|
| 392 |
+
'#ef4444' // Very High
|
| 393 |
+
],
|
| 394 |
+
borderColor: [
|
| 395 |
+
'#059669',
|
| 396 |
+
'#65a30d',
|
| 397 |
+
'#eab308',
|
| 398 |
+
'#ea580c',
|
| 399 |
+
'#dc2626'
|
| 400 |
+
],
|
| 401 |
+
borderWidth: 2
|
| 402 |
+
}]
|
| 403 |
+
},
|
| 404 |
+
options: {
|
| 405 |
+
responsive: true,
|
| 406 |
+
maintainAspectRatio: false,
|
| 407 |
+
plugins: {
|
| 408 |
+
legend: {
|
| 409 |
+
display: false
|
| 410 |
+
},
|
| 411 |
+
tooltip: {
|
| 412 |
+
callbacks: {
|
| 413 |
+
label: function(context) {
|
| 414 |
+
const percentage = ((context.parsed.y / allModels.length) * 100).toFixed(1);
|
| 415 |
+
return `${context.parsed.y} models (${percentage}%)`;
|
| 416 |
+
}
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
scales: {
|
| 421 |
+
y: {
|
| 422 |
+
beginAtZero: true,
|
| 423 |
+
title: {
|
| 424 |
+
display: true,
|
| 425 |
+
text: 'Number of Models'
|
| 426 |
+
}
|
| 427 |
+
},
|
| 428 |
+
x: {
|
| 429 |
+
title: {
|
| 430 |
+
display: true,
|
| 431 |
+
text: 'Output/Input Multiple Range'
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
});
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// Initialize on page load
|
| 440 |
+
document.addEventListener('DOMContentLoaded', loadData);
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -34,6 +34,28 @@ header {
|
|
| 34 |
box-shadow: var(--shadow-lg);
|
| 35 |
}
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
header h1 {
|
| 38 |
font-size: 2.5rem;
|
| 39 |
font-weight: 700;
|
|
@@ -430,6 +452,13 @@ h3 {
|
|
| 430 |
.context-very-large { background: #60a5fa; }
|
| 431 |
.context-ultra { background: #34d399; }
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
/* Table */
|
| 434 |
.table-controls {
|
| 435 |
display: flex;
|
|
@@ -612,3 +641,62 @@ footer a:hover {
|
|
| 612 |
font-size: 0.9rem;
|
| 613 |
}
|
| 614 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
box-shadow: var(--shadow-lg);
|
| 35 |
}
|
| 36 |
|
| 37 |
+
.header-nav {
|
| 38 |
+
margin-top: 1.5rem;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.header-nav a {
|
| 42 |
+
display: inline-block;
|
| 43 |
+
background: rgba(255, 255, 255, 0.2);
|
| 44 |
+
color: white;
|
| 45 |
+
padding: 0.75rem 1.5rem;
|
| 46 |
+
border-radius: 8px;
|
| 47 |
+
text-decoration: none;
|
| 48 |
+
font-weight: 600;
|
| 49 |
+
transition: all 0.3s ease;
|
| 50 |
+
backdrop-filter: blur(10px);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.header-nav a:hover {
|
| 54 |
+
background: rgba(255, 255, 255, 0.3);
|
| 55 |
+
transform: translateY(-2px);
|
| 56 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
header h1 {
|
| 60 |
font-size: 2.5rem;
|
| 61 |
font-weight: 700;
|
|
|
|
| 452 |
.context-very-large { background: #60a5fa; }
|
| 453 |
.context-ultra { background: #34d399; }
|
| 454 |
|
| 455 |
+
/* Output/Input Multiple Tier Colors */
|
| 456 |
+
.multiple-equal { background: #10b981; } /* <=1x - Equal or cheaper output (best for users) */
|
| 457 |
+
.multiple-low { background: #84cc16; } /* <2x - Low markup */
|
| 458 |
+
.multiple-medium { background: #fde047; } /* 2-5x - Standard industry practice */
|
| 459 |
+
.multiple-high { background: #f97316; } /* 5-10x - High markup */
|
| 460 |
+
.multiple-very-high { background: #ef4444; } /* 10x+ - Very high markup */
|
| 461 |
+
|
| 462 |
/* Table */
|
| 463 |
.table-controls {
|
| 464 |
display: flex;
|
|
|
|
| 641 |
font-size: 0.9rem;
|
| 642 |
}
|
| 643 |
}
|
| 644 |
+
|
| 645 |
+
/* Info and Warning Boxes */
|
| 646 |
+
.info-box {
|
| 647 |
+
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
|
| 648 |
+
border-left: 4px solid #3b82f6;
|
| 649 |
+
padding: 1.5rem;
|
| 650 |
+
border-radius: 8px;
|
| 651 |
+
margin: 1.5rem 0;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.info-box h3 {
|
| 655 |
+
color: #1e40af;
|
| 656 |
+
margin-top: 0;
|
| 657 |
+
margin-bottom: 1rem;
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.info-box h4 {
|
| 661 |
+
color: #1e3a8a;
|
| 662 |
+
margin-top: 1.5rem;
|
| 663 |
+
margin-bottom: 0.75rem;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.info-box ul {
|
| 667 |
+
margin: 0.75rem 0;
|
| 668 |
+
padding-left: 1.5rem;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
.info-box li {
|
| 672 |
+
margin: 0.5rem 0;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.warning-box {
|
| 676 |
+
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
| 677 |
+
border-left: 4px solid #f59e0b;
|
| 678 |
+
padding: 1.5rem;
|
| 679 |
+
border-radius: 8px;
|
| 680 |
+
margin: 1.5rem 0;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
.warning-box h3 {
|
| 684 |
+
color: #92400e;
|
| 685 |
+
margin-top: 0;
|
| 686 |
+
margin-bottom: 1rem;
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
.warning-box ul {
|
| 690 |
+
margin: 0.75rem 0;
|
| 691 |
+
padding-left: 1.5rem;
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.warning-box li {
|
| 695 |
+
margin: 0.5rem 0;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
.emphasis {
|
| 699 |
+
font-weight: 600;
|
| 700 |
+
color: #78350f;
|
| 701 |
+
margin-top: 1rem;
|
| 702 |
+
}
|