token-monitor/report.js
Vigilio Desto ab9c60b67c
Handle policy_rejected status (Anthropic April 4 billing change)
- 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
2026-04-08 05:38:46 +00:00

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');
}