Spaces:
Sleeping
Sleeping
Neutral price list labels
Browse files- api-tester.html +29 -29
api-tester.html
CHANGED
|
@@ -549,9 +549,9 @@
|
|
| 549 |
</div>
|
| 550 |
</div>
|
| 551 |
<div class="field">
|
| 552 |
-
<label title="Live price list
|
| 553 |
-
<div class="price-status" id="
|
| 554 |
-
<datalist id="
|
| 555 |
</div>
|
| 556 |
</div>
|
| 557 |
|
|
@@ -937,7 +937,7 @@
|
|
| 937 |
updatePrompts('lane-1'); // Generate all 4 canonical prompts
|
| 938 |
updateLogCount();
|
| 939 |
updatePresetDropdown();
|
| 940 |
-
|
| 941 |
});
|
| 942 |
|
| 943 |
function addLane() {
|
|
@@ -979,8 +979,8 @@
|
|
| 979 |
</div>
|
| 980 |
</div>
|
| 981 |
<div class="field">
|
| 982 |
-
<label title="
|
| 983 |
-
<input type="text" id="${laneId}-priceAs" list="
|
| 984 |
</div>
|
| 985 |
<div class="field">
|
| 986 |
<label>System Prompt (optional)</label>
|
|
@@ -1417,55 +1417,55 @@
|
|
| 1417 |
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
| 1418 |
}
|
| 1419 |
|
| 1420 |
-
// ===== Live price list
|
| 1421 |
-
const
|
| 1422 |
-
const
|
| 1423 |
-
const
|
| 1424 |
// Every gateway alias runs on the fused Qwen3.8-Flash-Next runtime, whose
|
| 1425 |
-
// hosted counterpart
|
| 1426 |
const FLASH_ALIASES = new Set([
|
| 1427 |
'chat', 'buddy', 'programmer', 'master', 'vision', 'soap', 'ai-suggestions', 'svetliq', 'gpt-oss-20b'
|
| 1428 |
]);
|
| 1429 |
-
const
|
| 1430 |
|
| 1431 |
-
function
|
| 1432 |
-
|
| 1433 |
const options = [];
|
| 1434 |
for (const model of models) {
|
| 1435 |
if (!model?.id || !model.pricing) continue;
|
| 1436 |
-
|
| 1437 |
options.push(`<option value="${escapeHtml(model.id)}">${escapeHtml(model.name || '')}</option>`);
|
| 1438 |
}
|
| 1439 |
-
const datalist = document.getElementById('
|
| 1440 |
if (datalist) datalist.innerHTML = options.join('');
|
| 1441 |
-
const status = document.getElementById('
|
| 1442 |
-
if (status) status.textContent = `${
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
-
async function
|
| 1446 |
try {
|
| 1447 |
-
const cached = JSON.parse(localStorage.getItem(
|
| 1448 |
-
if (cached?.models?.length && Date.now() - cached.fetchedAt <
|
| 1449 |
-
|
| 1450 |
return;
|
| 1451 |
}
|
| 1452 |
} catch (e) {
|
| 1453 |
// Unreadable cache: fetch a fresh list below.
|
| 1454 |
}
|
| 1455 |
try {
|
| 1456 |
-
const response = await fetch(
|
| 1457 |
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 1458 |
const body = await response.json();
|
| 1459 |
const models = (body.data || []).map(model => ({ id: model.id, name: model.name, pricing: model.pricing }));
|
| 1460 |
const fetchedAt = Date.now();
|
| 1461 |
-
|
| 1462 |
try {
|
| 1463 |
-
localStorage.setItem(
|
| 1464 |
} catch (e) {
|
| 1465 |
// Storage quota: keep the list in memory for this session only.
|
| 1466 |
}
|
| 1467 |
} catch (e) {
|
| 1468 |
-
const status = document.getElementById('
|
| 1469 |
if (status) status.textContent = `unavailable (${e.message})`;
|
| 1470 |
}
|
| 1471 |
}
|
|
@@ -1475,7 +1475,7 @@
|
|
| 1475 |
if (explicit) return explicit;
|
| 1476 |
const name = String(model || '').trim();
|
| 1477 |
const lower = name.toLowerCase();
|
| 1478 |
-
if (
|
| 1479 |
if (lower.includes('qwen3.8-27b')) return 'qwen/qwen3.8-27b';
|
| 1480 |
if (FLASH_ALIASES.has(lower) || lower.includes('qwen3.8-flash')) return 'qwen/qwen3.8-flash';
|
| 1481 |
return null;
|
|
@@ -1714,10 +1714,10 @@
|
|
| 1714 |
if (state.reportedCost !== null && state.reportedCost !== undefined && Number.isFinite(reported)) {
|
| 1715 |
return { value: reported, source: 'reported' };
|
| 1716 |
}
|
| 1717 |
-
const pricing = priceModel ?
|
| 1718 |
const usage = state.usage || {};
|
| 1719 |
if (!pricing || !Number.isFinite(usage.input_tokens) || !Number.isFinite(usage.output_tokens)) return null;
|
| 1720 |
-
//
|
| 1721 |
const rate = key => {
|
| 1722 |
const value = Number(pricing[key]);
|
| 1723 |
return Number.isFinite(value) ? value : null;
|
|
|
|
| 549 |
</div>
|
| 550 |
</div>
|
| 551 |
<div class="field">
|
| 552 |
+
<label title="Live model price list; used when the API reports no cost">Price list</label>
|
| 553 |
+
<div class="price-status" id="priceListStatus">loading…</div>
|
| 554 |
+
<datalist id="priceModels"></datalist>
|
| 555 |
</div>
|
| 556 |
</div>
|
| 557 |
|
|
|
|
| 937 |
updatePrompts('lane-1'); // Generate all 4 canonical prompts
|
| 938 |
updateLogCount();
|
| 939 |
updatePresetDropdown();
|
| 940 |
+
loadPriceList();
|
| 941 |
});
|
| 942 |
|
| 943 |
function addLane() {
|
|
|
|
| 979 |
</div>
|
| 980 |
</div>
|
| 981 |
<div class="field">
|
| 982 |
+
<label title="Model whose list price estimates this lane's cost; empty = auto from the model name">Price as</label>
|
| 983 |
+
<input type="text" id="${laneId}-priceAs" list="priceModels" placeholder="auto">
|
| 984 |
</div>
|
| 985 |
<div class="field">
|
| 986 |
<label>System Prompt (optional)</label>
|
|
|
|
| 1417 |
return value.length > limit ? `${value.slice(0, limit)}…` : value;
|
| 1418 |
}
|
| 1419 |
|
| 1420 |
+
// ===== Live model price list (public, CORS *) =====
|
| 1421 |
+
const PRICE_LIST_URL = 'https://openrouter.ai/api/v1/models';
|
| 1422 |
+
const PRICE_LIST_CACHE_KEY = 'api-tester-price-list';
|
| 1423 |
+
const PRICE_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
| 1424 |
// Every gateway alias runs on the fused Qwen3.8-Flash-Next runtime, whose
|
| 1425 |
+
// hosted counterpart is qwen/qwen3.8-flash.
|
| 1426 |
const FLASH_ALIASES = new Set([
|
| 1427 |
'chat', 'buddy', 'programmer', 'master', 'vision', 'soap', 'ai-suggestions', 'svetliq', 'gpt-oss-20b'
|
| 1428 |
]);
|
| 1429 |
+
const modelPricing = new Map();
|
| 1430 |
|
| 1431 |
+
function applyPriceList(models, fetchedAt) {
|
| 1432 |
+
modelPricing.clear();
|
| 1433 |
const options = [];
|
| 1434 |
for (const model of models) {
|
| 1435 |
if (!model?.id || !model.pricing) continue;
|
| 1436 |
+
modelPricing.set(model.id, model);
|
| 1437 |
options.push(`<option value="${escapeHtml(model.id)}">${escapeHtml(model.name || '')}</option>`);
|
| 1438 |
}
|
| 1439 |
+
const datalist = document.getElementById('priceModels');
|
| 1440 |
if (datalist) datalist.innerHTML = options.join('');
|
| 1441 |
+
const status = document.getElementById('priceListStatus');
|
| 1442 |
+
if (status) status.textContent = `${modelPricing.size} models · ${new Date(fetchedAt).toLocaleString()}`;
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
+
async function loadPriceList() {
|
| 1446 |
try {
|
| 1447 |
+
const cached = JSON.parse(localStorage.getItem(PRICE_LIST_CACHE_KEY) || 'null');
|
| 1448 |
+
if (cached?.models?.length && Date.now() - cached.fetchedAt < PRICE_LIST_CACHE_TTL_MS) {
|
| 1449 |
+
applyPriceList(cached.models, cached.fetchedAt);
|
| 1450 |
return;
|
| 1451 |
}
|
| 1452 |
} catch (e) {
|
| 1453 |
// Unreadable cache: fetch a fresh list below.
|
| 1454 |
}
|
| 1455 |
try {
|
| 1456 |
+
const response = await fetch(PRICE_LIST_URL);
|
| 1457 |
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 1458 |
const body = await response.json();
|
| 1459 |
const models = (body.data || []).map(model => ({ id: model.id, name: model.name, pricing: model.pricing }));
|
| 1460 |
const fetchedAt = Date.now();
|
| 1461 |
+
applyPriceList(models, fetchedAt);
|
| 1462 |
try {
|
| 1463 |
+
localStorage.setItem(PRICE_LIST_CACHE_KEY, JSON.stringify({ fetchedAt, models }));
|
| 1464 |
} catch (e) {
|
| 1465 |
// Storage quota: keep the list in memory for this session only.
|
| 1466 |
}
|
| 1467 |
} catch (e) {
|
| 1468 |
+
const status = document.getElementById('priceListStatus');
|
| 1469 |
if (status) status.textContent = `unavailable (${e.message})`;
|
| 1470 |
}
|
| 1471 |
}
|
|
|
|
| 1475 |
if (explicit) return explicit;
|
| 1476 |
const name = String(model || '').trim();
|
| 1477 |
const lower = name.toLowerCase();
|
| 1478 |
+
if (modelPricing.has(name)) return name;
|
| 1479 |
if (lower.includes('qwen3.8-27b')) return 'qwen/qwen3.8-27b';
|
| 1480 |
if (FLASH_ALIASES.has(lower) || lower.includes('qwen3.8-flash')) return 'qwen/qwen3.8-flash';
|
| 1481 |
return null;
|
|
|
|
| 1714 |
if (state.reportedCost !== null && state.reportedCost !== undefined && Number.isFinite(reported)) {
|
| 1715 |
return { value: reported, source: 'reported' };
|
| 1716 |
}
|
| 1717 |
+
const pricing = priceModel ? modelPricing.get(priceModel)?.pricing : null;
|
| 1718 |
const usage = state.usage || {};
|
| 1719 |
if (!pricing || !Number.isFinite(usage.input_tokens) || !Number.isFinite(usage.output_tokens)) return null;
|
| 1720 |
+
// List prices are USD per token, as strings.
|
| 1721 |
const rate = key => {
|
| 1722 |
const value = Number(pricing[key]);
|
| 1723 |
return Number.isFinite(value) ? value : null;
|