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:
William Valentin
2026-02-09 10:32:57 -08:00
parent 1d126cddfb
commit 9be8f76bc7
26 changed files with 1395 additions and 105 deletions
+5
View File
@@ -25,6 +25,9 @@
<a href="#/sessions" class="nav-link" data-page="sessions">
<span class="nav-icon">&#9776;</span> Sessions
</a>
<a href="#/usage" class="nav-link" data-page="usage">
<span class="nav-icon">&#9733;</span> Usage
</a>
<a href="#/settings" class="nav-link" data-page="settings">
<span class="nav-icon">&#9881;</span> Settings
</a>
@@ -42,11 +45,13 @@
import { DashboardPage } from './pages/dashboard.js';
import { ChatPage } from './pages/chat.js';
import { SessionsPage } from './pages/sessions.js';
import { UsagePage } from './pages/usage.js';
import { SettingsPage } from './pages/settings.js';
registerPage('/', DashboardPage);
registerPage('/chat', ChatPage);
registerPage('/sessions', SessionsPage);
registerPage('/usage', UsagePage);
registerPage('/settings', SettingsPage);
initStatusIndicator();
+170
View File
@@ -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;
}
},
};
+19
View File
@@ -741,6 +741,25 @@ header #status.status-ok {
margin-top: 24px;
}
/* ── Usage Page Header ─────────────────────────────────────── */
.usage-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.usage-header .page-title {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
flex: 1;
}
.usage-header .btn {
flex-shrink: 0;
}
/* ── Data Tables ────────────────────────────────────────────── */
table {