danielrosehill Claude commited on
Commit
3c9e74a
·
1 Parent(s): 261f212

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 ADDED
@@ -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])}")
app.js CHANGED
@@ -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 minCost = Math.min(...costs);
108
- const maxCost = Math.max(...costs);
 
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 for methodology display
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 < 5.00) return 'cost-high';
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
  `;
index.html CHANGED
@@ -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="price-range">-</div>
55
- <div class="stat-label">Price Range ($/M)</div>
56
  </div>
57
  <div class="stat-card">
58
- <div class="stat-number" id="context-range">-</div>
59
- <div class="stat-label">Context Range (K)</div>
60
  </div>
61
  <div class="stat-card">
62
- <div class="stat-number" id="vendors">-</div>
63
- <div class="stat-label">Vendors</div>
 
 
 
 
 
 
 
 
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
 
output-input-analysis.html ADDED
@@ -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>
output-input-analysis.js ADDED
@@ -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);
quadrants.csv CHANGED
The diff for this file is too large to render. See raw diff
 
style.css CHANGED
@@ -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
+ }