Move tracker hive to admin and expand nodes panel.

Give Nodes & coverage full width on overview with inference prices and live speed, and expose model pricing on /v1/models.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dobromir Popov
2026-07-14 12:19:25 +02:00
parent 5d87e81bc9
commit d1a1400db9
2 changed files with 96 additions and 16 deletions

View File

@@ -247,8 +247,7 @@
</nav>
<main>
<section data-tab="overview" id="account-section"><h2>Account</h2><div id="account">loading…</div></section>
<section data-tab="overview"><h2>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Nodes &amp; coverage</h2><div id="nodes" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Nodes &amp; coverage</h2><div id="nodes" class="empty">loading…</div></section>
<section data-tab="overview"><h2>Model usage (RPM)</h2><div id="stats" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Routing (learned)</h2><div id="routing" class="empty">loading…</div></section>
<section data-tab="overview" class="wide"><h2>Model inference speed</h2><div id="model-speed-chart" class="empty">loading…</div></section>
@@ -287,6 +286,7 @@
<section data-tab="billing"><h2>Request history</h2><div id="billing-usage" class="empty">login required</div></section>
<section data-tab="billing" data-admin-only><h2>Node pending payouts</h2><div id="pending" class="empty">admin login required</div></section>
<section data-tab="billing" data-admin-only><h2>Settlement history</h2><div id="settlements" class="empty">admin login required</div></section>
<section data-tab="admin"><h2>Tracker hive</h2><div id="hive" class="empty">loading…</div></section>
<section data-tab="admin" id="admin-section"><h2>All accounts (admin)</h2><div id="admin" class="empty"></div></section>
<section data-tab="admin" data-admin-only><h2>Strikes / bans / forfeitures</h2><div id="fraud" class="empty">admin login required</div></section>
<section data-tab="admin"><h2>Client balances</h2><div id="clients" class="empty">admin login required</div></section>
@@ -524,7 +524,67 @@ function renderHive(raft) {
(raft.peers ? `<br>peers: ${esc(Array.isArray(raft.peers) ? raft.peers.join(", ") : raft.peers)}` : "");
}
function renderNodes(map) {
function nodeLiveTps(node) {
const reqs = (node.stats && node.stats.current_requests) || [];
let best = null;
for (const req of reqs) {
const rate = req.tokens_per_sec;
if (rate == null) continue;
if (best === null || rate > best) best = rate;
}
return best;
}
function buildModelPriceMap(modelsResp) {
const prices = new Map();
for (const model of (modelsResp && modelsResp.data) || []) {
if (!model.pricing) continue;
for (const key of modelLookupKeys(model)) {
prices.set(key, model.pricing);
prices.set(modelAliasKey(key), model.pricing);
}
}
return prices;
}
function lookupModelPricing(modelKey, aliasMap, priceMap) {
const candidates = [modelKey];
const alias = aliasMap.get(modelAliasKey(modelKey));
if (alias) candidates.push(alias);
for (const candidate of candidates) {
const hit = priceMap.get(candidate) || priceMap.get(modelAliasKey(candidate));
if (hit) return hit;
}
return null;
}
function formatInferencePrice(pricing) {
if (!pricing) return '<span class="dim">—</span>';
const input = pricing.input_per_1k_usdt;
const output = pricing.output_per_1k_usdt;
if (input == null && output == null) return '<span class="dim">—</span>';
if (input === output || output == null) {
return `<span class="num">${usdt(input ?? output)}/1k</span>`;
}
return `<span class="num">${usdt(input)}/${usdt(output)}</span> <span class="dim">in/out per 1k</span>`;
}
function modelGroupTypicalTps(modelKey, aliasMap, routing) {
const sampleNode = { hf_repo: modelKey, model: modelKey };
const resolved = resolveModelGroup(sampleNode, aliasMap);
const model = {
id: resolved,
name: resolved,
hf_repo: modelKey.includes("/") ? modelKey : null,
aliases: [modelKey, resolved].filter(Boolean),
};
return chatModelTypicalTps(model, routing);
}
function renderNodes(payload) {
const map = payload && payload.map;
const routing = payload && payload.routing;
const priceMap = payload && payload.priceMap;
const nodes = (map && map.nodes) || [];
if (!nodes.length) {
$("nodes").innerHTML = '<div class="empty">no nodes registered</div>'; return;
@@ -543,16 +603,22 @@ function renderNodes(map) {
.map(n => n.model_supply && n.model_supply.served_model_copies)
.filter(v => v !== null && v !== undefined);
const served = servedValues.length ? Math.max(...servedValues) : undefined;
html += `<div><b>${esc(model)}</b> <span class="dim">(${group.length} node${group.length===1?"":"s"} · ${esc(copies(served))} served)</span></div>`;
html += table(["node", "shard", "tps (1h)", "queue", "served"], group.map(n => {
const typicalTps = modelGroupTypicalTps(model, aliasMap, routing);
const pricing = lookupModelPricing(model, aliasMap, priceMap);
html += `<div style="margin-top:8px"><b>${esc(model)}</b> <span class="dim">(` +
`${group.length} node${group.length === 1 ? "" : "s"} · ${esc(copies(served))} served · ` +
`${esc(tps(typicalTps))} tok/s · price ${formatInferencePrice(pricing)})</span></div>`;
html += table(["node", "shard", "last tps", "tps (1h)", "queue", "served"], group.map(n => {
const modelStats = (n.throughput && (n.throughput[n.hf_repo] || n.throughput[n.model])) || {};
return [
nodeDisplayCell(n),
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
`<span class="num">${esc(tps(modelStats.tokens_per_sec_last_hour))}</span>`,
esc(String((n.stats && n.stats.queue_depth) ?? 0)),
`<span class="num">${esc(copies(n.model_supply && n.model_supply.served_model_copies))}</span>`,
]; }));
nodeDisplayCell(n),
esc(`${n.shard_start ?? "?"}-${n.shard_end ?? "?"}`),
`<span class="num">${esc(tps(nodeLiveTps(n)))}</span>`,
`<span class="num">${esc(tps(modelStats.tokens_per_sec_last_hour))}</span>`,
esc(String((n.stats && n.stats.queue_depth) ?? 0)),
`<span class="num">${esc(copies(n.model_supply && n.model_supply.served_model_copies))}</span>`,
];
}));
}
$("nodes").innerHTML = html;
}
@@ -2293,21 +2359,21 @@ function markRefreshed() {
async function fetchOverviewTab() {
const fetches = [
fetchJson("/v1/raft/status"),
fetchJson("/v1/network/map"),
fetchJson("/v1/models"),
fetchJson("/v1/stats"),
fetchJson("/v1/routing"),
fetchJson("/v1/console"),
];
if (isLoggedIn) fetches.push(apiCall("/v1/account"));
const results = await Promise.all(fetches);
const [raft, map, stats, routing, consoleData, accountResp] = results;
const [map, models, stats, routing, consoleData, accountResp] = results;
lastStats = stats;
lastRouting = routing;
lastNetworkMap = map;
if (accountResp && accountResp.ok) applyAccountSummary(accountResp.data, false);
renderIfChanged("hive", raft, renderHive);
renderIfChanged("nodes", map, renderNodes);
const nodesPayload = { map, routing, priceMap: buildModelPriceMap(models) };
renderIfChanged("nodes", nodesPayload, renderNodes);
renderIfChanged("stats", stats, renderStats);
renderIfChanged("routing", routing, renderRouting);
const speedModels = Object.keys((routing && routing.models) || {});
@@ -2348,13 +2414,15 @@ async function fetchBillingTab() {
async function fetchAdminTab() {
const fetches = [
fetchJson("/v1/raft/status"),
fetchJson("/v1/console"),
fetchJson("/v1/billing/summary"),
fetchJson("/v1/registry/wallets"),
];
if (isAdmin) fetches.push(apiCall("/v1/admin/accounts"));
const results = await Promise.all(fetches);
const [consoleData, summary, wallets, adminResp] = results;
const [raft, consoleData, summary, wallets, adminResp] = results;
renderIfChanged("hive", raft, renderHive);
renderIfChanged("console", consoleData, renderConsole);
renderIfChanged("billing-summary", summary, data => renderBilling(data));
renderIfChanged("fraud", { wallets, summary }, data => renderFraud(data.wallets, data.summary));

View File

@@ -3145,6 +3145,16 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
self.send_response(404)
self.end_headers()
def _model_pricing_payload(self, model: str) -> dict | None:
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
if server.billing is None:
return None
in_rate, out_rate = server.billing.prices_for(model)
return {
"input_per_1k_usdt": in_rate,
"output_per_1k_usdt": out_rate,
}
def _handle_models(self):
server: _TrackerHTTPServer = self.server # type: ignore[assignment]
created = int(time.time())
@@ -3200,6 +3210,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
"shard_coverage_percentage": coverage,
"served_model_copies": served_copies,
"quantizations": quantizations,
"pricing": self._model_pricing_payload(name),
})
seen_ids.add(name)
# Note: the preset's hf_repo is deliberately NOT added to seen_ids —
@@ -3251,6 +3262,7 @@ class _TrackerHandler(http.server.BaseHTTPRequestHandler):
required_start,
required_end,
),
"pricing": self._model_pricing_payload(model_id),
})
seen_ids.add(model_id)
self._send_json(200, {"object": "list", "data": data})