- anthropic-teams.js: detect HTTP 400 extra-usage policy blocks, return status='policy_rejected' with quota headers still readable - report.js: display policy_rejected as CRITICAL with 'POLICY BLOCKED' label - getSeverity: treat policy_rejected as critical Currently the direct API (used by monitor) returns 200; pi's OAuth path returns 400. This fix future-proofs against the block extending to direct API calls, and correctly classifies the status if it does. Refs: trentuna/commons#17, trentuna/token-monitor#4
174 lines
5.9 KiB
JavaScript
174 lines
5.9 KiB
JavaScript
/**
|
|
* report.js — human-readable summary generator + severity logic
|
|
*/
|
|
|
|
/**
|
|
* Compute severity for a parsed provider result.
|
|
* @param {Object} provider
|
|
* @returns {'critical'|'warning'|'ok'|'unknown'}
|
|
*/
|
|
export function getSeverity(provider) {
|
|
if (provider.type === 'teams-direct') {
|
|
if (provider.status === 'rejected') return 'critical';
|
|
if (provider.status === 'policy_rejected') return 'critical';
|
|
if (provider.utilization_7d > 0.85) return 'warning';
|
|
if (provider.utilization_5h > 0.7) return 'warning';
|
|
return 'ok';
|
|
}
|
|
if (provider.type === 'shelley-proxy') {
|
|
const tokenPct = 1 - (provider.tokens_remaining / provider.tokens_limit);
|
|
if (tokenPct > 0.85) return 'warning';
|
|
return 'ok';
|
|
}
|
|
if (provider.type === 'gemini-api') {
|
|
if (provider.status === 'exhausted') return 'critical';
|
|
if (provider.status === 'ok') return 'ok';
|
|
return 'unknown';
|
|
}
|
|
if (provider.type === 'xai-api') {
|
|
if (provider.status === 'rate_limited') return 'critical';
|
|
if (provider.status === 'ok') {
|
|
const reqPct = provider.requests_remaining / provider.requests_limit;
|
|
const tokPct = provider.tokens_remaining / provider.tokens_limit;
|
|
if ((reqPct < 0.1) || (tokPct < 0.1)) return 'warning';
|
|
return 'ok';
|
|
}
|
|
return 'unknown';
|
|
}
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Format seconds as "Xh Ym" or "Xm" or "Xs".
|
|
*/
|
|
function formatDuration(seconds) {
|
|
if (seconds == null || isNaN(seconds) || seconds < 0) return '?';
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
if (h > 0) return `${h}h ${m}m`;
|
|
if (m > 0) return `${m}m`;
|
|
return `${s}s`;
|
|
}
|
|
|
|
/**
|
|
* Format a float as a percentage string.
|
|
*/
|
|
function pct(v) {
|
|
if (v == null) return '?';
|
|
return `${Math.round(v * 100)}%`;
|
|
}
|
|
|
|
/**
|
|
* Severity badge string.
|
|
*/
|
|
function badge(severity) {
|
|
switch (severity) {
|
|
case 'critical': return '[CRITICAL]';
|
|
case 'warning': return '[WARNING] ';
|
|
case 'ok': return '[OK] ';
|
|
default: return '[UNKNOWN] ';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a human-readable summary from a full monitor result.
|
|
* @param {Object} result — { timestamp, providers: { name: {...} } }
|
|
* @returns {string}
|
|
*/
|
|
export function generateReport(result) {
|
|
const ts = result.timestamp
|
|
? result.timestamp.replace('T', ' ').replace(/\.\d+Z$/, ' UTC').replace('Z', ' UTC')
|
|
: new Date().toUTCString();
|
|
|
|
const lines = [];
|
|
const width = 60;
|
|
|
|
lines.push(`Token Monitor — ${ts}`);
|
|
lines.push('═'.repeat(width));
|
|
lines.push('');
|
|
|
|
const counts = { critical: 0, warning: 0, ok: 0, unknown: 0 };
|
|
|
|
for (const [name, p] of Object.entries(result.providers)) {
|
|
const sev = p.severity || getSeverity(p);
|
|
counts[sev] = (counts[sev] || 0) + 1;
|
|
|
|
const b = badge(sev);
|
|
let detail = '';
|
|
|
|
if (p.type === 'teams-direct') {
|
|
if (p.status === 'invalid_key') {
|
|
detail = 'Invalid API key (401)';
|
|
} else if (p.status === 'policy_rejected') {
|
|
detail = `POLICY BLOCKED — 7d: ${pct(p.utilization_7d)} | extra-usage billing required`;
|
|
} else if (p.status === 'rejected') {
|
|
const resetIn = formatDuration(p.reset_in_seconds);
|
|
detail = `MAXED — 7d: ${pct(p.utilization_7d)} | resets in ${resetIn}`;
|
|
} else {
|
|
detail = `5h: ${pct(p.utilization_5h)} | 7d: ${pct(p.utilization_7d)}`;
|
|
if (p.reset_in_seconds != null) {
|
|
detail += ` | resets in ${formatDuration(p.reset_in_seconds)}`;
|
|
}
|
|
}
|
|
} else if (p.type === 'shelley-proxy') {
|
|
if (p.status === 'error') {
|
|
detail = `Error: ${p.message || 'unknown'}`;
|
|
} else {
|
|
detail = `tokens: ${p.tokens_remaining?.toLocaleString()}/${p.tokens_limit?.toLocaleString()}`;
|
|
if (p.cost_per_call_usd != null) {
|
|
detail += ` | cost: $${p.cost_per_call_usd}/call`;
|
|
}
|
|
}
|
|
} else if (p.type === 'api-direct') {
|
|
detail = p.message || 'No billing data available';
|
|
} else if (p.type === 'gemini-api') {
|
|
if (p.status === 'invalid_key') {
|
|
detail = 'Invalid API key (401)';
|
|
} else if (p.status === 'no_key') {
|
|
detail = 'No API key configured';
|
|
} else if (p.status === 'exhausted') {
|
|
const retryIn = p.retry_delay_seconds ? formatDuration(p.retry_delay_seconds) : '?';
|
|
const violated = p.quota_violations?.length || 0;
|
|
detail = `EXHAUSTED — retry in ${retryIn} | ${violated} quota(s) violated`;
|
|
} else if (p.status === 'ok') {
|
|
detail = 'Reachable — no quota depth visible';
|
|
} else if (p.status === 'error') {
|
|
detail = `Error: ${p.message || 'unknown'}`;
|
|
}
|
|
} else if (p.type === 'xai-api') {
|
|
if (p.status === 'no_key') {
|
|
detail = 'No API key configured';
|
|
} else if (p.status === 'invalid_key') {
|
|
detail = 'Invalid API key (401)';
|
|
} else if (p.status === 'rate_limited') {
|
|
detail = 'Rate limited (429)';
|
|
} else if (p.status === 'ok') {
|
|
const reqPct = p.requests_limit ? `${p.requests_remaining}/${p.requests_limit} req` : '';
|
|
const tokPct = p.tokens_limit ? ` | ${p.tokens_remaining?.toLocaleString()}/${p.tokens_limit?.toLocaleString()} tok` : '';
|
|
detail = reqPct + tokPct || 'OK';
|
|
} else if (p.status === 'error') {
|
|
detail = `Error: ${p.message || 'unknown'}`;
|
|
}
|
|
} else {
|
|
detail = p.message || '';
|
|
}
|
|
|
|
// Pad provider name to 14 chars
|
|
const paddedName = name.padEnd(14);
|
|
lines.push(`${paddedName} ${b} ${detail}`);
|
|
}
|
|
|
|
lines.push('');
|
|
lines.push('─'.repeat(width));
|
|
|
|
const parts = [];
|
|
if (counts.critical) parts.push(`${counts.critical} CRITICAL`);
|
|
if (counts.warning) parts.push(`${counts.warning} WARNING`);
|
|
if (counts.ok) parts.push(`${counts.ok} OK`);
|
|
if (counts.unknown) parts.push(`${counts.unknown} UNKNOWN`);
|
|
lines.push(`Overall: ${parts.join(', ') || 'no providers'}`);
|
|
lines.push('');
|
|
|
|
return lines.join('\n');
|
|
}
|