feat: xAI Management API billing module (token-monitor#2)

- providers/xai-billing.js: fetchXaiUsage, fetchXaiInvoicePreview,
  aggregateByKey, renderXaiSection
- analyze.js: --xai flag for standalone view; xAI section appended
  to full report when XAI_MANAGEMENT_KEY is set
- Verified against live API: per-key spend by unit type, prepaid
  balance, current billing period total
- Usage endpoint: POST /v1/billing/teams/{id}/usage (analyticsRequest)
- Invoice endpoint: GET /v1/billing/teams/{id}/postpaid/invoice/preview
This commit is contained in:
Vigilio Desto 2026-04-06 08:19:27 +00:00
parent b504977853
commit 2371e02d57
Signed by: vigilio
GPG key ID: 159D6AD58C8E55E9
2 changed files with 298 additions and 3 deletions

View file

@ -19,6 +19,7 @@
* node analyze.js --json # JSON output (all sections)
* node analyze.js --provider team-nadja # filter to one provider
* node analyze.js --prune [--dry-run] # log hygiene
* node analyze.js --xai # xAI spend section only
*/
import {
@ -26,6 +27,7 @@ import {
} from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { fetchXaiUsage, fetchXaiInvoicePreview, renderXaiSection } from './providers/xai-billing.js';
const LOG_DIR = join(homedir(), '.logs', 'token-monitor');
const TEAMS = ['team-vigilio', 'team-ludo', 'team-molto', 'team-nadja', 'team-buio'];
@ -359,7 +361,7 @@ function pruneLogs(dryRun = false) {
// ── Report generation ─────────────────────────────────────────────────────────
function generateFullReport(entries) {
function generateFullReport(entries, xaiSection = null) {
const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
const width = 56;
const lines = [
@ -452,6 +454,12 @@ function generateFullReport(entries) {
lines.push('No log data found. Run monitor.js to start accumulating data.');
}
// ── xAI spend (optional — only when management key is available)
if (xaiSection) {
lines.push(xaiSection);
lines.push('');
}
return lines.join('\n');
}
@ -462,12 +470,13 @@ const showBurnRate = args.includes('--burn-rate');
const showWeekly = args.includes('--weekly');
const showStagger = args.includes('--stagger');
const showRotation = args.includes('--rotation');
const showXai = args.includes('--xai');
const isJson = args.includes('--json');
const isPrune = args.includes('--prune');
const isDryRun = args.includes('--dry-run');
const providerIdx = args.indexOf('--provider');
const providerFilter = providerIdx !== -1 ? args[providerIdx + 1] : null;
const showAll = !showBurnRate && !showWeekly && !showStagger && !showRotation && !isPrune;
const showAll = !showBurnRate && !showWeekly && !showStagger && !showRotation && !showXai && !isPrune;
if (isPrune) {
pruneLogs(isDryRun);
@ -476,6 +485,33 @@ if (isPrune) {
const entries = loadLogs(providerFilter);
// ── xAI billing helpers ────────────────────────────────────────────────────
const XAI_MANAGEMENT_KEY = process.env.XAI_MANAGEMENT_KEY || null;
const XAI_TEAM_ID = process.env.XAI_TEAM_ID || null;
async function getXaiSection() {
if (!XAI_MANAGEMENT_KEY || !XAI_TEAM_ID) return null;
try {
const now = new Date();
// Current billing period: 1st of this month through today
const startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const endDate = now;
const startDateStr = startDate.toISOString().slice(0, 10);
const endDateStr = endDate.toISOString().slice(0, 10);
const [usageRows, preview] = await Promise.all([
fetchXaiUsage({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID, startDate, endDate }),
fetchXaiInvoicePreview({ managementKey: XAI_MANAGEMENT_KEY, teamId: XAI_TEAM_ID }),
]);
return renderXaiSection({ usageRows, preview, startDateStr, endDateStr });
} catch (err) {
return `xAI Spend\n (error: ${err.message})`;
}
}
// All remaining paths may need xAI data (async), wrap in IIFE
(async () => {
if (isJson) {
const burnRates = {};
for (const name of TEAMS) {
@ -494,7 +530,19 @@ if (isJson) {
}
if (showAll) {
console.log(generateFullReport(entries));
const xaiSection = await getXaiSection();
console.log(generateFullReport(entries, xaiSection));
process.exit(0);
}
if (showXai) {
const xaiSection = await getXaiSection();
if (xaiSection) {
console.log(xaiSection);
} else {
console.log('xAI Spend');
console.log(' (XAI_MANAGEMENT_KEY or XAI_TEAM_ID not set — source ~/.secrets/keys.env)');
}
process.exit(0);
}
@ -544,3 +592,5 @@ if (showRotation) {
console.log(` ${i + 1}. ${provider.padEnd(16)} ${icon} ${reason}`);
});
}
})();