diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..00786d4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "token-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "token-monitor", + "version": "0.1.0", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/providers/anthropic-teams.js b/providers/anthropic-teams.js index 3a0991f..ad974cb 100644 --- a/providers/anthropic-teams.js +++ b/providers/anthropic-teams.js @@ -116,6 +116,25 @@ export async function probeTeamsProvider(providerName, baseUrl, apiKey) { }), }); + // HTTP 400 with "extra_usage" policy: Anthropic changed billing April 4 2026. + // Third-party apps (incl. pi) no longer draw from Teams plan limits. + // Rate-limit headers ARE present but the provider is unusable for sessions. + // Detect this before parseTeamsHeaders so we surface a clear status. + if (response.status === 400) { + let bodyText = ''; + try { bodyText = await response.text(); } catch (_) {} + if (bodyText.includes('extra usage') || bodyText.includes('invalid_request_error')) { + // Still parse headers for quota visibility, but override status + const base = parseTeamsHeaders(response.headers, response.status, providerName); + return { + ...base, + status: 'policy_rejected', + policy_message: 'Third-party apps blocked (Anthropic April 2026 billing change)', + severity: 'critical', + }; + } + } + return parseTeamsHeaders(response.headers, response.status, providerName); } catch (err) { return { diff --git a/report.js b/report.js index 2654bbc..d3ce6d1 100644 --- a/report.js +++ b/report.js @@ -10,6 +10,7 @@ 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'; @@ -99,6 +100,8 @@ export function generateReport(result) { 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}`;