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:
parent
fa6ccff079
commit
98bda6e337
@ -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)})
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user