Spaces:
Running
Running
| // AI request + cost logging. A middleware records every AI/render request (user, route, duration, status, | |
| // rough est. cost) to Postgres (ai_request_log) or a JSONL file fallback, tracks a rolling daily spend | |
| // estimate, and warns when it crosses DAILY_SPEND_ALERT_USD. No prompts/PII are stored here. | |
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import { fileURLToPath } from "node:url"; | |
| import { pgEnabled, pgQuery } from "./db.mjs"; | |
| const __dirname = path.dirname(fileURLToPath(import.meta.url)); | |
| const LOG_FILE = path.join(__dirname, "..", ".cache", "ai-usage.jsonl"); | |
| try { fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true }); } catch { /* ignore */ } | |
| // coarse $ per call by route (tune from real provider bills). Only billed for requests that actually ran. | |
| const COST_PER_CALL = { | |
| chat: 0.0015, transcribe: 0.01, script: 0.0015, "generate-scenes": 0.004, | |
| "fill-scene": 0.0015, translate: 0.003, analyze: 0.002, export: 0.05, "image-to-scene": 0.004, | |
| }; | |
| const ROUTE_RE = /^\/api\/(chat\/stream|chat|transcribe-src|transcribe|script|generate-scenes|fill-scene|translate|analyze|export|image-to-scene)\b/; | |
| const routeKey = (p) => { | |
| if (/^\/api\/projects\/[^/]+\/import\b/.test(p)) return "transcribe"; // base-audio import runs STT | |
| const m = p.match(ROUTE_RE); | |
| return m ? m[1].replace("/stream", "").replace("-src", "") : null; | |
| }; | |
| let day = "", daySpend = 0, alerted = false; | |
| const ALERT = Number(process.env.DAILY_SPEND_ALERT_USD) || 0; | |
| function trackSpend(cost) { | |
| const today = new Date().toISOString().slice(0, 10); | |
| if (today !== day) { day = today; daySpend = 0; alerted = false; } | |
| daySpend += cost; | |
| if (ALERT > 0 && daySpend >= ALERT && !alerted) { | |
| alerted = true; | |
| console.warn(`[cost] ⚠ daily AI spend estimate crossed $${ALERT} — now ~$${daySpend.toFixed(2)}`); | |
| } | |
| } | |
| async function persist(rec) { | |
| if (pgEnabled()) { | |
| try { | |
| await pgQuery( | |
| `INSERT INTO ai_request_log (user_id, route, model, in_tokens, out_tokens, est_cost, duration_ms, ok) | |
| VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`, | |
| [rec.userId, rec.route, rec.model ?? null, rec.inTokens ?? null, rec.outTokens ?? null, rec.cost, rec.durationMs, rec.ok], | |
| ); | |
| } catch { /* logging must never break a request */ } | |
| } else { | |
| try { fs.appendFileSync(LOG_FILE, JSON.stringify(rec) + "\n"); } catch { /* ignore */ } | |
| } | |
| } | |
| export function aiUsageLogger() { | |
| return (req, res, next) => { | |
| const route = routeKey(req.path); | |
| if (!route) return next(); | |
| const t0 = Date.now(); | |
| res.on("finish", () => { | |
| const ok = res.statusCode < 400; | |
| const cost = ok ? (COST_PER_CALL[route] ?? 0.002) : 0; // don't bill rejected (402/429/4xx) requests | |
| if (cost) trackSpend(cost); | |
| persist({ userId: req.userId || null, route, durationMs: Date.now() - t0, ok, cost, status: res.statusCode, at: new Date().toISOString() }); | |
| }); | |
| next(); | |
| }; | |
| } | |
| export const dailySpend = () => ({ day, usd: +daySpend.toFixed(4) }); | |