danielrosehill Claude commited on
Commit
fcb188b
·
1 Parent(s): 1b36402

Redesign: Table-focused layout with objective data presentation

Browse files

Major Changes:
- Reorganized into tabbed interface (All Models / Visual Analysis)
- Table is now the centerpiece with color-coded cost and context tiers
- Added sortable columns (click headers to sort)
- Removed subjective "value score" metrics
- Added proper attribution and methodology explanation

Features:
- Color-coded pricing tiers (5 levels from very low to very high)
- Color-coded context window tiers (5 levels from small to ultra)
- Interactive legends explaining tier ranges
- Vendor name normalization (meta-llama → Meta, etc.)
- Separate input/output pricing display
- Tab navigation for better content organization
- Clear quadrant methodology (median-based divisions)

Attribution:
- Data collection by Daniel Rosehill
- Source: OpenRouter API (Nov 8, 2025)
- Notes on pricing variability across providers
- Median cost and context values displayed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (3) hide show
  1. app.js +212 -190
  2. index.html +87 -61
  3. style.css +191 -0
app.js CHANGED
@@ -2,6 +2,42 @@
2
  let allModels = [];
3
  let filteredModels = [];
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  // Load and process CSV data
6
  async function loadData() {
7
  try {
@@ -13,6 +49,10 @@ async function loadData() {
13
  dynamicTyping: true,
14
  complete: function(results) {
15
  allModels = results.data.filter(row => row.model_name);
 
 
 
 
16
  filteredModels = [...allModels];
17
  initializeApp();
18
  }
@@ -22,32 +62,74 @@ async function loadData() {
22
  }
23
  }
24
 
 
 
 
 
 
 
25
  // Initialize all visualizations and features
26
  function initializeApp() {
27
  updateStats();
28
- createQuadrantChart();
29
- populateTopModels();
30
- createDistributionCharts();
31
- generateInsights();
32
  populateTable();
33
  setupTableControls();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
  // Update summary statistics
37
  function updateStats() {
38
- const freeModels = allModels.filter(m => m.avg_cost === 0).length;
39
- const avgContext = Math.round(
40
- allModels.reduce((sum, m) => sum + m.context_length, 0) / allModels.length / 1000
41
- );
42
- const vendors = new Set(allModels.map(m => m.vendor)).size;
 
 
 
 
 
 
43
 
44
  document.getElementById('total-models').textContent = allModels.length;
45
- document.getElementById('free-models').textContent = freeModels;
46
- document.getElementById('avg-context').textContent = avgContext;
47
  document.getElementById('vendors').textContent = vendors;
 
 
 
 
 
 
 
 
 
 
 
48
  }
49
 
50
- // Create main quadrant scatter plot
51
  function createQuadrantChart() {
52
  const ctx = document.getElementById('quadrantChart').getContext('2d');
53
 
@@ -64,7 +146,7 @@ function createQuadrantChart() {
64
  label: quadrant,
65
  data: models.map(m => ({
66
  x: m.avg_cost,
67
- y: m.context_length / 1000, // Convert to K
68
  model: m
69
  })),
70
  backgroundColor: quadrantColors[quadrant] + '80',
@@ -75,6 +157,10 @@ function createQuadrantChart() {
75
  };
76
  });
77
 
 
 
 
 
78
  new Chart(ctx, {
79
  type: 'scatter',
80
  data: { datasets },
@@ -95,13 +181,44 @@ function createQuadrantChart() {
95
  const model = context.raw.model;
96
  return [
97
  model.model_name,
98
- `Vendor: ${model.vendor}`,
99
  `Context: ${(model.context_length / 1000).toFixed(0)}K tokens`,
100
- `Avg Cost: $${model.avg_cost.toFixed(2)}/M tokens`,
101
- `Value Score: ${model.value_score.toFixed(0)}`
 
102
  ];
103
  }
104
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  }
106
  },
107
  scales: {
@@ -120,199 +237,104 @@ function createQuadrantChart() {
120
  }
121
  }
122
  }
123
- }
 
124
  });
125
  }
126
 
127
- // Populate top value models by quadrant
128
- function populateTopModels() {
129
- const quadrants = {
130
- 'Low Cost / High Context': 'lc-hc-models',
131
- 'High Cost / High Context': 'hc-hc-models',
132
- 'Low Cost / Low Context': 'lc-lc-models',
133
- 'High Cost / Low Context': 'hc-lc-models'
134
- };
135
 
136
- Object.entries(quadrants).forEach(([quadrant, elementId]) => {
137
- const models = allModels
138
- .filter(m => m.quadrant === quadrant)
139
- .sort((a, b) => b.value_score - a.value_score)
140
- .slice(0, 5);
141
-
142
- const container = document.getElementById(elementId);
143
- container.innerHTML = models.map(m => `
144
- <div class="model-item">
145
- <div class="model-name">${m.model_name}</div>
146
- <div class="model-vendor">${m.vendor}</div>
147
- <div class="model-stats">
148
- <div class="model-stat">
149
- <span class="model-stat-label">Context</span>
150
- <span class="model-stat-value">${(m.context_length / 1000).toFixed(0)}K</span>
151
- </div>
152
- <div class="model-stat">
153
- <span class="model-stat-label">Cost</span>
154
- <span class="model-stat-value">$${m.avg_cost.toFixed(2)}</span>
155
- </div>
156
- <div class="model-stat">
157
- <span class="model-stat-label">Value</span>
158
- <span class="model-stat-value">${m.value_score.toFixed(0)}</span>
159
- </div>
160
- </div>
161
- </div>
162
- `).join('');
163
- });
164
  }
165
 
166
- // Create distribution charts
167
- function createDistributionCharts() {
168
- // Context distribution histogram
169
- const contextCtx = document.getElementById('contextDistChart').getContext('2d');
170
- const contextBins = {
171
- '<64K': 0,
172
- '64K-128K': 0,
173
- '128K-256K': 0,
174
- '256K-512K': 0,
175
- '512K-1M': 0,
176
- '>1M': 0
177
- };
178
-
179
- allModels.forEach(m => {
180
- const ctx = m.context_length;
181
- if (ctx < 64000) contextBins['<64K']++;
182
- else if (ctx < 128000) contextBins['64K-128K']++;
183
- else if (ctx < 256000) contextBins['128K-256K']++;
184
- else if (ctx < 512000) contextBins['256K-512K']++;
185
- else if (ctx < 1000000) contextBins['512K-1M']++;
186
- else contextBins['>1M']++;
187
- });
188
 
189
- new Chart(contextCtx, {
190
- type: 'bar',
191
- data: {
192
- labels: Object.keys(contextBins),
193
- datasets: [{
194
- label: 'Number of Models',
195
- data: Object.values(contextBins),
196
- backgroundColor: '#2563eb'
197
- }]
198
- },
199
- options: {
200
- responsive: true,
201
- maintainAspectRatio: true,
202
- plugins: {
203
- legend: {
204
- display: false
205
- }
206
- },
207
- scales: {
208
- y: {
209
- beginAtZero: true,
210
- title: {
211
- display: true,
212
- text: 'Count'
213
- }
214
- }
215
- }
216
- }
217
- });
218
 
219
- // Vendor distribution
220
- const vendorCtx = document.getElementById('vendorChart').getContext('2d');
221
- const vendorCounts = {};
222
- allModels.forEach(m => {
223
- vendorCounts[m.vendor] = (vendorCounts[m.vendor] || 0) + 1;
224
- });
225
 
226
- const topVendors = Object.entries(vendorCounts)
227
- .sort((a, b) => b[1] - a[1])
228
- .slice(0, 10);
229
-
230
- new Chart(vendorCtx, {
231
- type: 'bar',
232
- data: {
233
- labels: topVendors.map(v => v[0]),
234
- datasets: [{
235
- label: 'Model Count',
236
- data: topVendors.map(v => v[1]),
237
- backgroundColor: '#10b981'
238
- }]
239
- },
240
- options: {
241
- indexAxis: 'y',
242
- responsive: true,
243
- maintainAspectRatio: true,
244
- plugins: {
245
- legend: {
246
- display: false
247
- }
248
- },
249
- scales: {
250
- x: {
251
- beginAtZero: true,
252
- title: {
253
- display: true,
254
- text: 'Number of Models'
255
- }
256
- }
257
- }
258
  }
259
- });
260
- }
261
 
262
- // Generate insights
263
- function generateInsights() {
264
- const freeModels = allModels.filter(m => m.avg_cost === 0);
265
- const longestContext = allModels.reduce((max, m) =>
266
- m.context_length > max.context_length ? m : max
267
- );
268
- const bestValue = allModels.reduce((max, m) =>
269
- m.value_score > max.value_score ? m : max
270
- );
271
- const avgCost = (allModels.reduce((sum, m) => sum + m.avg_cost, 0) / allModels.length).toFixed(2);
272
-
273
- const insights = [
274
- {
275
- title: 'Free Models Available',
276
- text: `${freeModels.length} models offer free tool-calling capabilities, with ${freeModels.filter(m => m.context_length > 128000).length} providing >128K context windows.`
277
- },
278
- {
279
- title: 'Longest Context',
280
- text: `${longestContext.model_name} by ${longestContext.vendor} offers the largest context window at ${(longestContext.context_length / 1000000).toFixed(1)}M tokens.`
281
- },
282
- {
283
- title: 'Best Value',
284
- text: `${bestValue.model_name} offers the highest value score (${bestValue.value_score.toFixed(0)}), with ${(bestValue.context_length / 1000).toFixed(0)}K context at $${bestValue.avg_cost.toFixed(2)}/M.`
285
- },
286
- {
287
- title: 'Average Pricing',
288
- text: `The average cost across all models is $${avgCost} per million tokens, with significant variation based on context window size.`
289
  }
290
- ];
291
-
292
- const container = document.getElementById('insights-content');
293
- container.innerHTML = insights.map(i => `
294
- <div class="insight-card">
295
- <div class="insight-title">${i.title}</div>
296
- <div class="insight-text">${i.text}</div>
297
- </div>
298
- `).join('');
299
- }
300
 
301
- // Populate data table
302
- function populateTable(models = filteredModels) {
303
- const tbody = document.getElementById('table-body');
304
- tbody.innerHTML = models.map(m => `
305
  <tr>
306
  <td>${m.model_name}</td>
307
- <td>${m.vendor}</td>
308
- <td>${(m.context_length / 1000).toFixed(0)}K</td>
309
- <td>$${m.avg_cost.toFixed(2)}</td>
 
 
 
 
 
 
 
310
  <td><span class="quadrant-badge">${m.quadrant}</span></td>
311
- <td>${m.value_score.toFixed(0)}</td>
312
  </tr>
313
  `).join('');
314
  }
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  // Setup table filtering and search
317
  function setupTableControls() {
318
  const searchInput = document.getElementById('search');
@@ -325,7 +347,7 @@ function setupTableControls() {
325
  filteredModels = allModels.filter(m => {
326
  const matchesSearch = !searchTerm ||
327
  m.model_name.toLowerCase().includes(searchTerm) ||
328
- m.vendor.toLowerCase().includes(searchTerm);
329
  const matchesQuadrant = !quadrant || m.quadrant === quadrant;
330
  return matchesSearch && matchesQuadrant;
331
  });
 
2
  let allModels = [];
3
  let filteredModels = [];
4
 
5
+ // Vendor name mapping
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 {
 
49
  dynamicTyping: true,
50
  complete: function(results) {
51
  allModels = results.data.filter(row => row.model_name);
52
+ // Normalize vendor names
53
+ allModels.forEach(model => {
54
+ model.displayVendor = normalizeVendorName(model.vendor);
55
+ });
56
  filteredModels = [...allModels];
57
  initializeApp();
58
  }
 
62
  }
63
  }
64
 
65
+ // Sorting state
66
+ let currentSort = {
67
+ column: 'model_name',
68
+ direction: 'asc'
69
+ };
70
+
71
  // Initialize all visualizations and features
72
  function initializeApp() {
73
  updateStats();
 
 
 
 
74
  populateTable();
75
  setupTableControls();
76
+ setupSorting();
77
+ setupTabs();
78
+ createQuadrantChart();
79
+ }
80
+
81
+ // Setup tab navigation
82
+ function setupTabs() {
83
+ const tabButtons = document.querySelectorAll('.tab-button');
84
+ const tabContents = document.querySelectorAll('.tab-content');
85
+
86
+ tabButtons.forEach(button => {
87
+ button.addEventListener('click', () => {
88
+ const targetTab = button.dataset.tab;
89
+
90
+ // Remove active class from all buttons and contents
91
+ tabButtons.forEach(btn => btn.classList.remove('active'));
92
+ tabContents.forEach(content => content.classList.remove('active'));
93
+
94
+ // Add active class to clicked button and corresponding content
95
+ button.classList.add('active');
96
+ document.getElementById(`tab-${targetTab}`).classList.add('active');
97
+ });
98
+ });
99
  }
100
 
101
  // Update summary statistics
102
  function updateStats() {
103
+ const costs = allModels.map(m => m.avg_cost);
104
+ const contexts = allModels.map(m => m.context_length);
105
+ const minCost = Math.min(...costs);
106
+ const maxCost = Math.max(...costs);
107
+ const minContext = Math.min(...contexts);
108
+ const maxContext = Math.max(...contexts);
109
+ const vendors = new Set(allModels.map(m => m.displayVendor)).size;
110
+
111
+ // Calculate medians for methodology display
112
+ const medianCost = median(costs);
113
+ const medianContext = median(contexts);
114
 
115
  document.getElementById('total-models').textContent = allModels.length;
116
+ document.getElementById('price-range').textContent = `$${minCost.toFixed(2)} - $${maxCost.toFixed(2)}`;
117
+ document.getElementById('context-range').textContent = `${(minContext/1000).toFixed(0)}K - ${(maxContext/1000000).toFixed(1)}M`;
118
  document.getElementById('vendors').textContent = vendors;
119
+
120
+ // Update methodology section with actual median values
121
+ document.getElementById('median-cost-display').textContent = `$${medianCost.toFixed(2)}/M tokens`;
122
+ document.getElementById('median-context-display').textContent = `${(medianContext/1000).toFixed(0)}K tokens`;
123
+ }
124
+
125
+ // Calculate median for quadrant lines
126
+ function median(arr) {
127
+ const sorted = [...arr].sort((a, b) => a - b);
128
+ const mid = Math.floor(sorted.length / 2);
129
+ return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
130
  }
131
 
132
+ // Create main quadrant scatter plot with division lines
133
  function createQuadrantChart() {
134
  const ctx = document.getElementById('quadrantChart').getContext('2d');
135
 
 
146
  label: quadrant,
147
  data: models.map(m => ({
148
  x: m.avg_cost,
149
+ y: m.context_length / 1000,
150
  model: m
151
  })),
152
  backgroundColor: quadrantColors[quadrant] + '80',
 
157
  };
158
  });
159
 
160
+ // Calculate medians for quadrant lines
161
+ const medianCost = median(allModels.map(m => m.avg_cost));
162
+ const medianContext = median(allModels.map(m => m.context_length / 1000));
163
+
164
  new Chart(ctx, {
165
  type: 'scatter',
166
  data: { datasets },
 
181
  const model = context.raw.model;
182
  return [
183
  model.model_name,
184
+ `Vendor: ${model.displayVendor}`,
185
  `Context: ${(model.context_length / 1000).toFixed(0)}K tokens`,
186
+ `Input: $${model.input_price_usd_per_m.toFixed(2)}/M`,
187
+ `Output: $${model.output_price_usd_per_m.toFixed(2)}/M`,
188
+ `Avg: $${model.avg_cost.toFixed(2)}/M`
189
  ];
190
  }
191
  }
192
+ },
193
+ annotation: {
194
+ annotations: {
195
+ verticalLine: {
196
+ type: 'line',
197
+ xMin: medianCost,
198
+ xMax: medianCost,
199
+ borderColor: '#64748b',
200
+ borderWidth: 2,
201
+ borderDash: [5, 5],
202
+ label: {
203
+ display: true,
204
+ content: `Median Cost: $${medianCost.toFixed(2)}`,
205
+ position: 'start'
206
+ }
207
+ },
208
+ horizontalLine: {
209
+ type: 'line',
210
+ yMin: medianContext,
211
+ yMax: medianContext,
212
+ borderColor: '#64748b',
213
+ borderWidth: 2,
214
+ borderDash: [5, 5],
215
+ label: {
216
+ display: true,
217
+ content: `Median Context: ${medianContext.toFixed(0)}K`,
218
+ position: 'end'
219
+ }
220
+ }
221
+ }
222
  }
223
  },
224
  scales: {
 
237
  }
238
  }
239
  }
240
+ },
241
+ plugins: [window['chartjs-plugin-annotation']]
242
  });
243
  }
244
 
 
 
 
 
 
 
 
 
245
 
246
+ // Get tier class for cost
247
+ function getCostTier(cost) {
248
+ if (cost < 0.10) return 'cost-very-low';
249
+ if (cost < 0.50) return 'cost-low';
250
+ if (cost < 2.00) return 'cost-medium';
251
+ if (cost < 5.00) return 'cost-high';
252
+ return 'cost-very-high';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
 
255
+ // Get tier class for context
256
+ function getContextTier(context) {
257
+ if (context < 32000) return 'context-small';
258
+ if (context < 128000) return 'context-medium';
259
+ if (context < 256000) return 'context-large';
260
+ if (context < 1000000) return 'context-very-large';
261
+ return 'context-ultra';
262
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ // Populate data table with color coding
265
+ function populateTable(models = filteredModels) {
266
+ const tbody = document.getElementById('table-body');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
+ // Sort models
269
+ const sortedModels = [...models].sort((a, b) => {
270
+ let aVal = a[currentSort.column];
271
+ let bVal = b[currentSort.column];
 
 
272
 
273
+ // Handle string comparisons
274
+ if (typeof aVal === 'string') {
275
+ aVal = aVal.toLowerCase();
276
+ bVal = bVal.toLowerCase();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  }
 
 
278
 
279
+ if (currentSort.direction === 'asc') {
280
+ return aVal > bVal ? 1 : -1;
281
+ } else {
282
+ return aVal < bVal ? 1 : -1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
+ });
 
 
 
 
 
 
 
 
 
285
 
286
+ tbody.innerHTML = sortedModels.map(m => `
 
 
 
287
  <tr>
288
  <td>${m.model_name}</td>
289
+ <td>${m.displayVendor}</td>
290
+ <td class="${getContextTier(m.context_length)}" style="font-weight: 600;">
291
+ ${(m.context_length / 1000).toFixed(0)}K
292
+ </td>
293
+ <td class="${getCostTier(m.input_price_usd_per_m)}" style="font-weight: 600;">
294
+ $${m.input_price_usd_per_m.toFixed(2)}
295
+ </td>
296
+ <td class="${getCostTier(m.output_price_usd_per_m)}" style="font-weight: 600;">
297
+ $${m.output_price_usd_per_m.toFixed(2)}
298
+ </td>
299
  <td><span class="quadrant-badge">${m.quadrant}</span></td>
 
300
  </tr>
301
  `).join('');
302
  }
303
 
304
+ // Setup sorting functionality
305
+ function setupSorting() {
306
+ const headers = document.querySelectorAll('th.sortable');
307
+
308
+ headers.forEach(header => {
309
+ header.addEventListener('click', () => {
310
+ const column = header.dataset.sort;
311
+
312
+ // Toggle direction if clicking same column
313
+ if (currentSort.column === column) {
314
+ currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
315
+ } else {
316
+ currentSort.column = column;
317
+ currentSort.direction = 'asc';
318
+ }
319
+
320
+ // Update UI indicators
321
+ headers.forEach(h => {
322
+ h.classList.remove('sort-asc', 'sort-desc');
323
+ });
324
+ header.classList.add(`sort-${currentSort.direction}`);
325
+
326
+ // Repopulate table
327
+ populateTable(filteredModels);
328
+ });
329
+ });
330
+
331
+ // Set initial sort indicator
332
+ const initialHeader = document.querySelector(`th[data-sort="${currentSort.column}"]`);
333
+ if (initialHeader) {
334
+ initialHeader.classList.add('sort-asc');
335
+ }
336
+ }
337
+
338
  // Setup table filtering and search
339
  function setupTableControls() {
340
  const searchInput = document.getElementById('search');
 
347
  filteredModels = allModels.filter(m => {
348
  const matchesSearch = !searchTerm ||
349
  m.model_name.toLowerCase().includes(searchTerm) ||
350
+ m.displayVendor.toLowerCase().includes(searchTerm);
351
  const matchesQuadrant = !quadrant || m.quadrant === quadrant;
352
  return matchesSearch && matchesQuadrant;
353
  });
index.html CHANGED
@@ -6,6 +6,7 @@
6
  <title>LLM Tool-Calling Models: Cost & Context Analysis</title>
7
  <link rel="stylesheet" href="style.css">
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
 
9
  <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
10
  </head>
11
  <body>
@@ -23,6 +24,25 @@
23
  <li><strong>Cost per token</strong> - Economic efficiency for production use</li>
24
  <li><strong>Context window size</strong> - Capability for complex, long-form tasks</li>
25
  </ul>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  </section>
27
 
28
  <section class="stats-grid">
@@ -31,12 +51,12 @@
31
  <div class="stat-label">Models Analyzed</div>
32
  </div>
33
  <div class="stat-card">
34
- <div class="stat-number" id="free-models">-</div>
35
- <div class="stat-label">Free Models</div>
36
  </div>
37
  <div class="stat-card">
38
- <div class="stat-number" id="avg-context">-</div>
39
- <div class="stat-label">Avg Context (K)</div>
40
  </div>
41
  <div class="stat-card">
42
  <div class="stat-number" id="vendors">-</div>
@@ -44,61 +64,47 @@
44
  </div>
45
  </section>
46
 
47
- <section class="chart-section">
48
- <h2>Quadrant Analysis: Cost vs Context</h2>
49
- <p>Models positioned by average cost (input + output) and context window size. Hover for details.</p>
50
- <div class="chart-container">
51
- <canvas id="quadrantChart"></canvas>
52
- </div>
53
- </section>
54
 
55
- <section class="chart-section">
56
- <h2>Top Value Models by Quadrant</h2>
57
- <p>Models with the best context-to-cost ratio in each category</p>
58
- <div class="quadrant-grid">
59
- <div class="quadrant-card">
60
- <h3>Low Cost / High Context</h3>
61
- <div id="lc-hc-models" class="model-list"></div>
62
- </div>
63
- <div class="quadrant-card">
64
- <h3>High Cost / High Context</h3>
65
- <div id="hc-hc-models" class="model-list"></div>
66
- </div>
67
- <div class="quadrant-card">
68
- <h3>Low Cost / Low Context</h3>
69
- <div id="lc-lc-models" class="model-list"></div>
70
- </div>
71
- <div class="quadrant-card">
72
- <h3>High Cost / Low Context</h3>
73
- <div id="hc-lc-models" class="model-list"></div>
74
- </div>
75
- </div>
76
- </section>
77
 
78
- <section class="chart-section">
79
- <h2>Distribution Analysis</h2>
80
- <div class="dual-chart">
81
- <div class="chart-half">
82
- <h3>Context Window Distribution</h3>
83
- <canvas id="contextDistChart"></canvas>
 
 
 
 
 
 
 
 
84
  </div>
85
- <div class="chart-half">
86
- <h3>Top Vendors by Model Count</h3>
87
- <canvas id="vendorChart"></canvas>
 
 
 
 
 
 
88
  </div>
89
  </div>
90
- </section>
91
-
92
- <section class="insights">
93
- <h2>Key Findings</h2>
94
- <div class="insights-grid" id="insights-content">
95
- <!-- Populated by JavaScript -->
96
- </div>
97
- </section>
98
-
99
- <section class="data-table-section">
100
- <h2>All Models</h2>
101
- <p>Complete dataset with filtering and search</p>
102
  <div class="table-controls">
103
  <input type="text" id="search" placeholder="Search models..." />
104
  <select id="quadrant-filter">
@@ -113,12 +119,12 @@
113
  <table id="models-table">
114
  <thead>
115
  <tr>
116
- <th>Model Name</th>
117
- <th>Vendor</th>
118
- <th>Context (tokens)</th>
119
- <th>Avg Cost ($/M)</th>
120
- <th>Quadrant</th>
121
- <th>Value Score</th>
122
  </tr>
123
  </thead>
124
  <tbody id="table-body">
@@ -126,7 +132,27 @@
126
  </tbody>
127
  </table>
128
  </div>
129
- </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  </main>
131
 
132
  <footer>
 
6
  <title>LLM Tool-Calling Models: Cost & Context Analysis</title>
7
  <link rel="stylesheet" href="style.css">
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/papaparse@5.4.1/papaparse.min.js"></script>
11
  </head>
12
  <body>
 
24
  <li><strong>Cost per token</strong> - Economic efficiency for production use</li>
25
  <li><strong>Context window size</strong> - Capability for complex, long-form tasks</li>
26
  </ul>
27
+
28
+ <div class="attribution">
29
+ <h3>Methodology</h3>
30
+ <p><strong>Quadrant Classification:</strong> Models are categorized into four quadrants based on median values calculated from the dataset:</p>
31
+ <ul>
32
+ <li><strong>Cost Division:</strong> Median average cost (mean of input + output pricing) = <span id="median-cost-display">calculated from data</span></li>
33
+ <li><strong>Context Division:</strong> Median context window size = <span id="median-context-display">calculated from data</span></li>
34
+ </ul>
35
+ <p>This creates four categories: Low Cost/High Context, High Cost/High Context, Low Cost/Low Context, and High Cost/Low Context.</p>
36
+
37
+ <h3>Attribution & Data Notes</h3>
38
+ <ul>
39
+ <li><strong>Data Collection:</strong> <a href="https://danielrosehill.com" target="_blank">Daniel Rosehill</a></li>
40
+ <li><strong>Collection Date:</strong> November 8, 2025</li>
41
+ <li><strong>Source:</strong> Prices derived from API calls to <a href="https://openrouter.ai/" target="_blank">OpenRouter</a></li>
42
+ <li><strong>Pricing Variability:</strong> OpenRouter pricing can fluctuate slightly according to the end inference provider. The same models may have slightly different pricing even at the time of capture.</li>
43
+ <li><strong>Data Exclusions:</strong> Free models (cost = $0) were excluded from this analysis</li>
44
+ </ul>
45
+ </div>
46
  </section>
47
 
48
  <section class="stats-grid">
 
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>
 
64
  </div>
65
  </section>
66
 
67
+ <div class="tabs">
68
+ <button class="tab-button active" data-tab="models">All Models</button>
69
+ <button class="tab-button" data-tab="analysis">Visual Analysis</button>
70
+ </div>
 
 
 
71
 
72
+ <div class="tab-content active" id="tab-models">
73
+ <section class="data-notes">
74
+ <h3>Data Notes</h3>
75
+ <ul>
76
+ <li><strong>Cost Calculation:</strong> Input and output pricing shown separately (per million tokens)</li>
77
+ <li><strong>Data Exclusions:</strong> Free models (cost = $0) were excluded from this analysis</li>
78
+ <li><strong>Color Coding:</strong> Cells are color-coded by tier - see legend below for ranges</li>
79
+ </ul>
80
+ </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ <section class="data-table-section">
83
+ <h2>All Models - Comprehensive View</h2>
84
+ <p>Complete dataset with color-coded cost and context tiers. Click column headers to sort.</p>
85
+
86
+ <div class="tier-legends">
87
+ <div class="legend-group">
88
+ <h4>Cost Tiers ($/M tokens)</h4>
89
+ <div class="legend-items">
90
+ <div class="legend-item"><span class="tier-indicator cost-very-low"></span> Very Low (&lt; $0.10)</div>
91
+ <div class="legend-item"><span class="tier-indicator cost-low"></span> Low ($0.10 - $0.50)</div>
92
+ <div class="legend-item"><span class="tier-indicator cost-medium"></span> Medium ($0.50 - $2.00)</div>
93
+ <div class="legend-item"><span class="tier-indicator cost-high"></span> High ($2.00 - $5.00)</div>
94
+ <div class="legend-item"><span class="tier-indicator cost-very-high"></span> Very High (&gt; $5.00)</div>
95
+ </div>
96
  </div>
97
+ <div class="legend-group">
98
+ <h4>Context Window Tiers</h4>
99
+ <div class="legend-items">
100
+ <div class="legend-item"><span class="tier-indicator context-small"></span> Small (&lt; 32K)</div>
101
+ <div class="legend-item"><span class="tier-indicator context-medium"></span> Medium (32K - 128K)</div>
102
+ <div class="legend-item"><span class="tier-indicator context-large"></span> Large (128K - 256K)</div>
103
+ <div class="legend-item"><span class="tier-indicator context-very-large"></span> Very Large (256K - 1M)</div>
104
+ <div class="legend-item"><span class="tier-indicator context-ultra"></span> Ultra (&gt; 1M)</div>
105
+ </div>
106
  </div>
107
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
108
  <div class="table-controls">
109
  <input type="text" id="search" placeholder="Search models..." />
110
  <select id="quadrant-filter">
 
119
  <table id="models-table">
120
  <thead>
121
  <tr>
122
+ <th class="sortable" data-sort="model_name">Model Name <span class="sort-indicator"></span></th>
123
+ <th class="sortable" data-sort="displayVendor">Vendor <span class="sort-indicator"></span></th>
124
+ <th class="sortable" data-sort="context_length">Context <span class="sort-indicator"></span></th>
125
+ <th class="sortable" data-sort="input_price_usd_per_m">Input ($/M) <span class="sort-indicator"></span></th>
126
+ <th class="sortable" data-sort="output_price_usd_per_m">Output ($/M) <span class="sort-indicator"></span></th>
127
+ <th class="sortable" data-sort="quadrant">Quadrant <span class="sort-indicator"></span></th>
128
  </tr>
129
  </thead>
130
  <tbody id="table-body">
 
132
  </tbody>
133
  </table>
134
  </div>
135
+ </section>
136
+ </div>
137
+
138
+ <div class="tab-content" id="tab-analysis">
139
+ <section class="data-notes">
140
+ <h3>Analysis Notes</h3>
141
+ <ul>
142
+ <li><strong>Quadrant Divisions:</strong> Based on median values for cost and context window size</li>
143
+ <li><strong>Logarithmic Scale:</strong> Cost axis uses log scale to better visualize the wide price range</li>
144
+ <li><strong>Median Lines:</strong> Dashed lines show median cost and context values across all models</li>
145
+ </ul>
146
+ </section>
147
+
148
+ <section class="chart-section">
149
+ <h2>Cost vs Context Quadrant Analysis</h2>
150
+ <p>Interactive scatter plot showing the relationship between pricing and context window size. Hover over points for detailed model information.</p>
151
+ <div class="chart-container">
152
+ <canvas id="quadrantChart"></canvas>
153
+ </div>
154
+ </section>
155
+ </div>
156
  </main>
157
 
158
  <footer>
style.css CHANGED
@@ -88,6 +88,94 @@ h3 {
88
  margin-bottom: 0.5rem;
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  /* Stats Grid */
92
  .stats-grid {
93
  display: grid;
@@ -214,6 +302,32 @@ h3 {
214
  font-weight: 600;
215
  }
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  /* Insights */
218
  .insights-grid {
219
  display: grid;
@@ -240,6 +354,58 @@ h3 {
240
  line-height: 1.5;
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  /* Table */
244
  .table-controls {
245
  display: flex;
@@ -294,6 +460,31 @@ th {
294
  letter-spacing: 0.05em;
295
  }
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  tbody tr {
298
  border-bottom: 1px solid var(--border);
299
  transition: background-color 0.2s;
 
88
  margin-bottom: 0.5rem;
89
  }
90
 
91
+ .attribution {
92
+ margin-top: 2rem;
93
+ padding: 1.5rem;
94
+ background: var(--bg);
95
+ border-radius: 8px;
96
+ border-left: 4px solid var(--primary);
97
+ }
98
+
99
+ .attribution h3 {
100
+ color: var(--primary);
101
+ font-size: 1.1rem;
102
+ margin-bottom: 1rem;
103
+ }
104
+
105
+ .attribution ul {
106
+ margin-left: 1.5rem;
107
+ }
108
+
109
+ .attribution li {
110
+ margin-bottom: 0.5rem;
111
+ line-height: 1.6;
112
+ }
113
+
114
+ .attribution a {
115
+ color: var(--primary);
116
+ text-decoration: none;
117
+ font-weight: 600;
118
+ }
119
+
120
+ .attribution a:hover {
121
+ text-decoration: underline;
122
+ }
123
+
124
+ /* Data Notes */
125
+ .data-notes ul {
126
+ margin-left: 1.5rem;
127
+ margin-top: 0.5rem;
128
+ }
129
+
130
+ .data-notes li {
131
+ margin-bottom: 0.5rem;
132
+ line-height: 1.6;
133
+ }
134
+
135
+ /* Tabs */
136
+ .tabs {
137
+ display: flex;
138
+ gap: 0.5rem;
139
+ margin: 2rem 0 0 0;
140
+ border-bottom: 2px solid var(--border);
141
+ }
142
+
143
+ .tab-button {
144
+ padding: 1rem 2rem;
145
+ background: transparent;
146
+ border: none;
147
+ border-bottom: 3px solid transparent;
148
+ color: var(--text-muted);
149
+ font-size: 1rem;
150
+ font-weight: 600;
151
+ cursor: pointer;
152
+ transition: all 0.2s;
153
+ }
154
+
155
+ .tab-button:hover {
156
+ color: var(--text);
157
+ background: var(--bg);
158
+ }
159
+
160
+ .tab-button.active {
161
+ color: var(--primary);
162
+ border-bottom-color: var(--primary);
163
+ }
164
+
165
+ .tab-content {
166
+ display: none;
167
+ animation: fadeIn 0.3s;
168
+ }
169
+
170
+ .tab-content.active {
171
+ display: block;
172
+ }
173
+
174
+ @keyframes fadeIn {
175
+ from { opacity: 0; }
176
+ to { opacity: 1; }
177
+ }
178
+
179
  /* Stats Grid */
180
  .stats-grid {
181
  display: grid;
 
302
  font-weight: 600;
303
  }
304
 
305
+ /* Quadrant Stats */
306
+ .quadrant-stats {
307
+ display: flex;
308
+ flex-direction: column;
309
+ gap: 0.5rem;
310
+ }
311
+
312
+ .quadrant-stat-item {
313
+ display: flex;
314
+ justify-content: space-between;
315
+ padding: 0.5rem;
316
+ background: var(--card-bg);
317
+ border-radius: 4px;
318
+ }
319
+
320
+ .quadrant-stat-item .stat-label {
321
+ color: var(--text-muted);
322
+ font-size: 0.85rem;
323
+ }
324
+
325
+ .quadrant-stat-item .stat-value {
326
+ color: var(--text);
327
+ font-weight: 600;
328
+ font-size: 0.9rem;
329
+ }
330
+
331
  /* Insights */
332
  .insights-grid {
333
  display: grid;
 
354
  line-height: 1.5;
355
  }
356
 
357
+ /* Tier Legends */
358
+ .tier-legends {
359
+ display: grid;
360
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
361
+ gap: 2rem;
362
+ margin-bottom: 2rem;
363
+ padding: 1.5rem;
364
+ background: var(--bg);
365
+ border-radius: 8px;
366
+ }
367
+
368
+ .legend-group h4 {
369
+ color: var(--text);
370
+ font-size: 1rem;
371
+ margin-bottom: 1rem;
372
+ font-weight: 600;
373
+ }
374
+
375
+ .legend-items {
376
+ display: flex;
377
+ flex-direction: column;
378
+ gap: 0.5rem;
379
+ }
380
+
381
+ .legend-item {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 0.75rem;
385
+ font-size: 0.9rem;
386
+ }
387
+
388
+ .tier-indicator {
389
+ width: 24px;
390
+ height: 24px;
391
+ border-radius: 4px;
392
+ display: inline-block;
393
+ }
394
+
395
+ /* Cost Tier Colors */
396
+ .cost-very-low { background: #10b981; }
397
+ .cost-low { background: #84cc16; }
398
+ .cost-medium { background: #f59e0b; }
399
+ .cost-high { background: #f97316; }
400
+ .cost-very-high { background: #ef4444; }
401
+
402
+ /* Context Tier Colors */
403
+ .context-small { background: #fca5a5; }
404
+ .context-medium { background: #fde047; }
405
+ .context-large { background: #a78bfa; }
406
+ .context-very-large { background: #60a5fa; }
407
+ .context-ultra { background: #34d399; }
408
+
409
  /* Table */
410
  .table-controls {
411
  display: flex;
 
460
  letter-spacing: 0.05em;
461
  }
462
 
463
+ th.sortable {
464
+ cursor: pointer;
465
+ user-select: none;
466
+ position: relative;
467
+ }
468
+
469
+ th.sortable:hover {
470
+ background: var(--primary-dark);
471
+ }
472
+
473
+ .sort-indicator {
474
+ margin-left: 0.5rem;
475
+ opacity: 0.5;
476
+ }
477
+
478
+ th.sort-asc .sort-indicator::after {
479
+ content: ' ▲';
480
+ opacity: 1;
481
+ }
482
+
483
+ th.sort-desc .sort-indicator::after {
484
+ content: ' ▼';
485
+ opacity: 1;
486
+ }
487
+
488
  tbody tr {
489
  border-bottom: 1px solid var(--border);
490
  transition: background-color 0.2s;