/** * Cost guards for analyzer LLM operations. * * Spec: docs/ticket-analyzer-phase2-spec.md → Section D.8. * * Three thresholds: * • $5 per request → require explicit confirmation in UI * • $20 per user/day → soft warn (returned in preflight; UI can surface) * • $50 per user/day → hard block (unless user is in override list) * * Override env var: ANALYZER_DAILY_COST_OVERRIDE_USERS (comma-separated user ids) */ import postgresClient from '@/lib/services/postgres-client'; export const REQUIRES_CONFIRMATION_USD = 5.0; export const SOFT_WARN_DAILY_USD = 20.0; export const HARD_BLOCK_DAILY_USD = 50.0; export type CostDecision = | 'approved' | 'requires_confirmation' | 'blocked' | 'overridden'; export interface CostEvaluation { estimatedCost: number; dailySpendBefore: number; decision: CostDecision; decisionReason: string | null; softWarn: boolean; hardBlocked: boolean; requiresConfirmation: boolean; isOverride: boolean; } /** * Pessimistic cost estimate for an aggregate report. Assumes Sonnet pricing * and a payload size proportional to the number of fingerprints (each ~2KB * after compaction). Conservative — actual cost is usually lower. */ export function estimateAggregateReportCost(input: { ticketCount: number; includeItglueContext: boolean; }): number { // Inputs: // per-fingerprint input tokens ≈ 700 (compacted JSON) → 700 * N // distributions + system prompt ≈ 2000 tokens // IT Glue context (if included): up to 200 doc titles * 50 tokens = 10K tokens // Outputs: cap ≈ 6K tokens (we set max_tokens 16K but real outputs are smaller). const inputTokens = 700 * input.ticketCount + 2000 + (input.includeItglueContext ? 10_000 : 0); const outputTokens = 6_000; // Sonnet pricing per Phase 1 pricing table: $3/$15 per 1M tokens. const cost = (inputTokens / 1_000_000) * 3 + (outputTokens / 1_000_000) * 15; return Math.round(cost * 10_000) / 10_000; } function getOverrideUsers(): Set { return new Set( (process.env.ANALYZER_DAILY_COST_OVERRIDE_USERS ?? '') .split(',') .map((s) => s.trim()) .filter(Boolean) ); } export async function getUserDailySpend(userId: string | null): Promise { if (!userId) return 0; // Sum from analyzer_aggregate_reports + analyzer_analyses for the trailing 24h. const res = await postgresClient.query<{ total: string }>( `SELECT COALESCE(SUM(amt), 0)::text AS total FROM ( SELECT estimated_cost_usd AS amt FROM analyzer_aggregate_reports WHERE generated_by_user_id = $1 AND generated_at >= NOW() - INTERVAL '24 hours' AND estimated_cost_usd IS NOT NULL UNION ALL SELECT estimated_cost_usd AS amt FROM analyzer_analyses WHERE triggered_by_user_id = $1 AND triggered_at >= NOW() - INTERVAL '24 hours' AND status = 'complete' ) x`, [userId] ); return Number(res.rows[0]?.total ?? 0); } export async function evaluateCost(input: { userId: string | null; estimatedCost: number; confirmedCost: boolean; }): Promise { const dailySpendBefore = await getUserDailySpend(input.userId); const projectedDailySpend = dailySpendBefore + input.estimatedCost; const overrideUsers = getOverrideUsers(); const isOverride = input.userId !== null && overrideUsers.has(input.userId); const softWarn = projectedDailySpend >= SOFT_WARN_DAILY_USD; const hardBlocked = projectedDailySpend >= HARD_BLOCK_DAILY_USD; const requiresConfirmation = input.estimatedCost > REQUIRES_CONFIRMATION_USD && !input.confirmedCost; let decision: CostDecision; let decisionReason: string | null = null; if (hardBlocked && !isOverride) { decision = 'blocked'; decisionReason = `Projected daily spend $${projectedDailySpend.toFixed(2)} would exceed hard limit $${HARD_BLOCK_DAILY_USD.toFixed(2)}`; } else if (hardBlocked && isOverride) { decision = 'overridden'; decisionReason = `Override allowed (user in ANALYZER_DAILY_COST_OVERRIDE_USERS); projected $${projectedDailySpend.toFixed(2)}`; } else if (requiresConfirmation) { decision = 'requires_confirmation'; decisionReason = `Per-request cost $${input.estimatedCost.toFixed(2)} > confirmation threshold $${REQUIRES_CONFIRMATION_USD.toFixed(2)}`; } else { decision = 'approved'; } return { estimatedCost: input.estimatedCost, dailySpendBefore, decision, decisionReason, softWarn, hardBlocked, requiresConfirmation, isOverride, }; } export async function recordCostAuditDecision(input: { userId: string | null; action: string; evaluation: CostEvaluation; context?: Record; }): Promise { await postgresClient.query( `INSERT INTO analyzer_cost_audit (user_id, action, estimated_cost, daily_spend_before, decision, decision_reason, context) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)`, [ input.userId, input.action, input.evaluation.estimatedCost, input.evaluation.dailySpendBefore, input.evaluation.decision, input.evaluation.decisionReason, JSON.stringify(input.context ?? {}), ] ); }