/** * What the AI budget actually looks like right now. * * The Space settings page shows which secrets EXIST. It cannot show whether the * running process read them, and a key named `GROQ_API_KEY_3` with no `_2` * beside it is silently ignored — the pool stops at the first gap. That is a * misconfiguration nobody would notice until the day capacity was needed and * was not there. * * So this reports the keys in rotation and how much of each is gone, from the * same state the router uses. Admin only: it names the environment variables * and how much budget is left, which together tell an outsider exactly when the * platform is cheapest to knock over. */ import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { RolesGuard } from '../auth/roles.guard'; import { Roles } from '../auth/roles.decorator'; import { GroqService } from './groq.service'; import { ALERT_THRESHOLDS } from './groq-key-pool'; @ApiTags('AI (admin)') @ApiBearerAuth() @Controller('admin/ai') @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') export class AiAdminController { constructor(private readonly groq: GroqService) {} @Get('keys') @ApiOperation({ summary: 'Which Groq keys are in rotation, and how much of each is left' }) @ApiOkResponse({ description: 'Usage is what Groq itself reported on the last response each key served, ' + 'so a key that has not been used yet reads as unmeasured rather than full.', }) keys() { const keys = this.groq.keyUsage(); return { /* * The count is the first thing to check after adding a secret. Three * secrets on the settings page and two keys here means the names do not * match what the pool reads, or the Space has not restarted since. */ configured: keys.length, available: keys.filter((k) => k.available).length, alertsAt: ALERT_THRESHOLDS.map((t) => `${Math.round(t * 100)}%`), keys: keys.map((k) => ({ name: k.name, // Null until this key has served a request — the pool spreads load, so // a quiet key can legitimately have no reading yet. usedPercent: k.used === null ? null : Math.round(k.used * 100), remainingRequests: k.remainingRequests, limitRequests: k.limitRequests, resetsAt: k.resetsAt, available: k.available, })), }; } }