feat: implement Tier 3 features — lane queue, credential redaction, token dashboard, xAI, Voyage AI
- Lane Queue: per-session FIFO queue in gateway replacing reject-when-busy (9 tests) - Credential Redaction: redactConfig() expanded to cover 18+ secret fields (16 tests) - Web UI Token Dashboard: system.tokenUsage endpoint + Usage page with summary cards - xAI (Grok) Provider: OpenAI-compatible client with model pricing - Voyage AI Embeddings: new embedding provider with configurable dimensions (5 tests) - Update gap analysis: 90→95 match (70%→74%), Tier 3 section marked DONE - Update state.json: test count 1001→1034, add tier3_completion entry Total: 1034 tests passing across 85 files, typecheck clean
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Flynn Token Usage Page
|
||||
*
|
||||
* Shows per-session token usage breakdown including input/output tokens,
|
||||
* API calls, estimated cost, and delegation details.
|
||||
* Auto-refreshes every 30 seconds.
|
||||
*/
|
||||
|
||||
let _timer = null;
|
||||
|
||||
function formatNumber(n) {
|
||||
return (n ?? 0).toLocaleString();
|
||||
}
|
||||
|
||||
function formatCost(n) {
|
||||
if (!n || n === 0) return '$0.00';
|
||||
if (n < 0.01) return `$${n.toFixed(4)}`;
|
||||
return `$${n.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function truncateId(id) {
|
||||
if (!id) return '-';
|
||||
if (id.length <= 24) return id;
|
||||
return id.slice(0, 24) + '\u2026';
|
||||
}
|
||||
|
||||
async function loadUsage(el, client) {
|
||||
let data;
|
||||
|
||||
try {
|
||||
data = await client.call('system.tokenUsage');
|
||||
} catch (err) {
|
||||
el.innerHTML = `<div class="empty-state">Failed to load usage: ${err.message}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const sessions = data?.sessions ?? [];
|
||||
|
||||
// Compute totals across all sessions
|
||||
let totalInput = 0;
|
||||
let totalOutput = 0;
|
||||
let totalCalls = 0;
|
||||
let totalCost = 0;
|
||||
|
||||
for (const s of sessions) {
|
||||
totalInput += s.total?.inputTokens ?? 0;
|
||||
totalOutput += s.total?.outputTokens ?? 0;
|
||||
totalCalls += s.total?.calls ?? 0;
|
||||
totalCost += s.total?.estimatedCost ?? 0;
|
||||
}
|
||||
|
||||
// Summary cards
|
||||
const summaryHtml = `
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Input Tokens</div>
|
||||
<div class="stat-value">${formatNumber(totalInput)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Output Tokens</div>
|
||||
<div class="stat-value">${formatNumber(totalOutput)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Total Tokens</div>
|
||||
<div class="stat-value">${formatNumber(totalInput + totalOutput)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">API Calls</div>
|
||||
<div class="stat-value">${formatNumber(totalCalls)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Estimated Cost</div>
|
||||
<div class="stat-value">${formatCost(totalCost)}</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">Active Sessions</div>
|
||||
<div class="stat-value">${sessions.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Per-session table
|
||||
let tableHtml = '';
|
||||
if (sessions.length === 0) {
|
||||
tableHtml = '<div class="empty-state">No active sessions with usage data</div>';
|
||||
} else {
|
||||
const rows = sessions.map(s => {
|
||||
const inTok = s.total?.inputTokens ?? 0;
|
||||
const outTok = s.total?.outputTokens ?? 0;
|
||||
const calls = s.total?.calls ?? 0;
|
||||
const cost = s.total?.estimatedCost ?? 0;
|
||||
|
||||
// Build delegation breakdown if present
|
||||
const delegationEntries = Object.entries(s.delegation ?? {});
|
||||
let delegationCell = '<span class="text-muted">-</span>';
|
||||
if (delegationEntries.length > 0) {
|
||||
delegationCell = delegationEntries.map(([tier, stats]) =>
|
||||
`<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`
|
||||
).join('<br>');
|
||||
}
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td title="${s.sessionId}">${truncateId(s.sessionId)}</td>
|
||||
<td>${formatNumber(inTok)}</td>
|
||||
<td>${formatNumber(outTok)}</td>
|
||||
<td>${formatNumber(inTok + outTok)}</td>
|
||||
<td>${formatNumber(calls)}</td>
|
||||
<td>${formatCost(cost)}</td>
|
||||
<td>${delegationCell}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
tableHtml = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session</th>
|
||||
<th>Input</th>
|
||||
<th>Output</th>
|
||||
<th>Total</th>
|
||||
<th>Calls</th>
|
||||
<th>Cost</th>
|
||||
<th>Delegation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="usage-header">
|
||||
<h1 class="page-title">Token Usage</h1>
|
||||
<button class="btn btn-secondary" id="usage-refresh-btn">Refresh</button>
|
||||
</div>
|
||||
${summaryHtml}
|
||||
<h2 class="section-title">Per-Session Breakdown</h2>
|
||||
${tableHtml}
|
||||
`;
|
||||
|
||||
// Wire up refresh button
|
||||
const refreshBtn = el.querySelector('#usage-refresh-btn');
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener('click', () => {
|
||||
loadUsage(el, client).catch(() => {});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const UsagePage = {
|
||||
async render(el, client) {
|
||||
await loadUsage(el, client);
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
_timer = setInterval(() => {
|
||||
loadUsage(el, client).catch(() => {});
|
||||
}, 30000);
|
||||
},
|
||||
|
||||
teardown() {
|
||||
if (_timer) {
|
||||
clearInterval(_timer);
|
||||
_timer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user