OpenRouter: show all 343 models (free + paid) with pricing and filter

- Endpoint now returns all models, not just free ones
- Each model includes: name, context_length, free flag, prompt/completion cost
- UI shows pricing: "128K ctx · $2.50/M tok" for paid, "128K ctx · free" for free
- Filter dropdown: All Models / Free Only / Paid Only
- Search still works alongside the filter
- 29 free + 314 paid models available (GPT-5.4, Grok 4.20, Gemini 3.1, etc)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 23:22:51 -05:00
parent fa6ccff079
commit 98bda6e337

View File

@ -5050,8 +5050,15 @@ ADMIN_HTML = r"""
<!-- OPENROUTER TAB -->
<div id="tab-openrouter" class="tab-content">
<div class="card">
<h3>Free Models on OpenRouter <button class="btn btn-primary" style="margin-left:auto" onclick="fetchORModels()">Fetch Models</button></h3>
<input class="search-input" id="or-search" placeholder="Search models..." oninput="filterOR()">
<h3>Models on OpenRouter <button class="btn btn-primary" style="margin-left:auto" onclick="fetchORModels()">Fetch Models</button></h3>
<div style="display:flex;gap:8px;margin-bottom:8px;align-items:center">
<input class="search-input" id="or-search" placeholder="Search models..." oninput="filterOR()" style="margin-bottom:0;flex:1">
<select id="or-filter" onchange="filterOR()" style="padding:8px;background:var(--card);border:1px solid var(--border);border-radius:6px;color:var(--text1);font-size:12px">
<option value="all">All Models</option>
<option value="free">Free Only</option>
<option value="paid">Paid Only</option>
</select>
</div>
<div class="or-list" id="or-model-list"><div class="empty">Click "Fetch Models" to load the list.</div></div>
</div>
</div>
@ -5322,21 +5329,50 @@ async function fetchORModels() {
function renderORModels() {
const q = (document.getElementById('or-search').value || '').toLowerCase();
const filtered = q ? orModels.filter(m => m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q)) : orModels;
const tier = document.getElementById('or-filter').value;
let filtered = orModels;
if (q) filtered = filtered.filter(m => m.name.toLowerCase().includes(q) || m.id.toLowerCase().includes(q));
if (tier === 'free') filtered = filtered.filter(m => m.free);
if (tier === 'paid') filtered = filtered.filter(m => !m.free);
const el = document.getElementById('or-model-list');
if (!filtered.length) { el.innerHTML = '<div class="empty">No models found.</div>'; return; }
if (!filtered.length) { el.textContent = 'No models found.'; return; }
const existing = new Set((config.cloud_models||[]).map(m=>m.id));
el.innerHTML = filtered.map(m => {
el.textContent = '';
filtered.forEach(function(m) {
const added = existing.has('openrouter::'+m.id);
const ctx = m.context_length ? (m.context_length/1000).toFixed(0)+'K' : '?';
return `<div class="model-row">
<span class="name">${m.name}</span>
<span class="meta">${ctx} ctx</span>
${added
? '<button class="btn btn-sm" disabled style="opacity:0.4">Added</button>'
: `<button class="btn btn-sm btn-green" onclick="addOR('${m.id}','${m.name.replace(/'/g,"\\'")}')">Add</button>`}
</div>`;
}).join('');
const row = document.createElement('div');
row.className = 'model-row';
const nameEl = document.createElement('span');
nameEl.className = 'name';
nameEl.textContent = m.name;
const meta = document.createElement('span');
meta.className = 'meta';
if (m.free) {
meta.textContent = ctx + ' ctx · free';
meta.style.color = 'var(--green)';
} else {
const cost = (m.prompt_cost * 1e6).toFixed(2);
meta.textContent = ctx + ' ctx · $' + cost + '/M tok';
}
row.appendChild(nameEl);
row.appendChild(meta);
if (added) {
const btn = document.createElement('button');
btn.className = 'btn btn-sm';
btn.disabled = true;
btn.style.opacity = '0.4';
btn.textContent = 'Added';
row.appendChild(btn);
} else {
const btn = document.createElement('button');
btn.className = 'btn btn-sm btn-green';
btn.textContent = 'Add';
btn.onclick = function() { addOR(m.id, m.name); };
row.appendChild(btn);
}
el.appendChild(row);
});
}
function filterOR() { renderORModels(); }
@ -7205,15 +7241,22 @@ def admin_openrouter_models():
try:
r = requests.get("https://openrouter.ai/api/v1/models", headers=headers, timeout=15)
r.raise_for_status()
free = []
models = []
for m in r.json().get("data", []):
pricing = m.get("pricing", {})
if pricing.get("prompt") == "0" and pricing.get("completion") == "0":
free.append({"id": m["id"], "name": m.get("name", m["id"]),
"context_length": m.get("context_length", 0)})
_or_models_cache["data"] = free
prompt_cost = float(pricing.get("prompt", "0") or "0")
completion_cost = float(pricing.get("completion", "0") or "0")
is_free = prompt_cost == 0 and completion_cost == 0
models.append({
"id": m["id"], "name": m.get("name", m["id"]),
"context_length": m.get("context_length", 0),
"free": is_free,
"prompt_cost": prompt_cost,
"completion_cost": completion_cost,
})
_or_models_cache["data"] = models
_or_models_cache["ts"] = now
return jsonify({"models": free})
return jsonify({"models": models})
except Exception as e:
return jsonify({"models": [], "error": str(e)})