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:
@@ -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 & coverage</h2><div id="nodes" class="empty">loading…</div></section>
|
||||
<section data-tab="overview" class="wide"><h2>Nodes & 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));
|
||||
|
||||
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user