// POST /relay/analyze — forwards an analysis prompt to the chosen // backend and returns the standard envelope. // // Request body (application/json): // { prompt: string } // // Headers: same as /relay/transcribe (X-Recap-Install-Id required, // X-Recap-Job-Id optional, Authorization optional Bearer license). // // Same charge-once-per-job semantics: a Recap summarize job pairs // transcribe + analyze with the same X-Recap-Job-Id. The first call // (whichever endpoint) charges 1 credit; the second is free. // // Every outcome (success / quota-refused / backend-error) writes one // row to the audit log so the admin dashboard can compute cost, // margin, and speed metrics. import express from "express"; import { resolveLicense } from "../keysat-client.js"; import { getOrCreateRow, planBackend, commitCredit } from "../credits.js"; import { lookupJob, markJobCharged, refundJob } from "../job-credits.js"; import { getConfigSnapshot, getTierQuotas } from "../config.js"; import { createGeminiBackend } from "../backends/gemini.js"; import { createHardwareBackend } from "../backends/hardware.js"; import { envelope, errorEnvelope } from "./envelope.js"; import { recordCall } from "../audit-log.js"; import { calcGeminiCost } from "../pricing.js"; export function analyzeRouter() { const router = express.Router(); router.post("/analyze", express.json({ limit: "10mb" }), async (req, res) => { const t0 = Date.now(); const installId = req.header("X-Recap-Install-Id"); const jobId = req.header("X-Recap-Job-Id") || null; const auth = req.header("Authorization"); if (!installId) { const e = await errorEnvelope({ error: "missing X-Recap-Install-Id header", statusHint: 400, }); return res.status(400).json(e.body); } const prompt = req.body?.prompt; if (!prompt || typeof prompt !== "string") { const e = await errorEnvelope({ error: "missing or non-string body.prompt", installId, statusHint: 400, }); return res.status(400).json(e.body); } const license = await resolveLicense(auth); const tier = license.tier; const row = await getOrCreateRow(installId); row.tier_snapshot = tier; let reusedJob = false; let chosenBackend = null; const existingJob = lookupJob(installId, jobId); if (existingJob) { reusedJob = true; chosenBackend = existingJob.backend; } else { const cfg = await getConfigSnapshot(); const hasHardware = !!cfg.relay_gemma_base_url; const quota = await getTierQuotas(); const preference = cfg.relay_analyze_backend_preference || "gemini_first"; const plan = planBackend(row, quota, { hasHardware, preference }); if (!plan.allowed) { await recordCall({ install_id: installId, tier, pipeline: "analyze", backend: null, model: null, status: "refused", credit_charged: 0, duration_ms: Date.now() - t0, cost_usd: 0, job_id: jobId, error: plan.reason, }); const e = await errorEnvelope({ error: plan.reason, installId, tier, statusHint: 402, }); return res.status(402).json(e.body); } chosenBackend = plan.backend; } const cfg = await getConfigSnapshot(); let result; try { if (chosenBackend === "gemini") { const backend = createGeminiBackend({ apiKey: cfg.relay_gemini_api_key, transcriptionModel: cfg.relay_gemini_transcription_model, analysisModel: cfg.relay_gemini_analysis_model, }); result = await backend.analyzeText({ prompt }); } else { const backend = createHardwareBackend({ parakeetBaseURL: cfg.relay_parakeet_base_url, gemmaBaseURL: cfg.relay_gemma_base_url, parakeetModel: cfg.relay_parakeet_model, gemmaModel: cfg.relay_gemma_model, }); result = await backend.analyzeText({ prompt }); } } catch (err) { if (reusedJob) refundJob(installId, jobId); console.error(`[relay/analyze] backend error: ${err?.message}`); await recordCall({ install_id: installId, tier, pipeline: "analyze", backend: chosenBackend, model: chosenBackend === "gemini" ? cfg.relay_gemini_analysis_model : cfg.relay_gemma_model, status: "error", credit_charged: 0, duration_ms: Date.now() - t0, cost_usd: 0, job_id: jobId, error: (err?.message || String(err)).slice(0, 200), }); const e = await errorEnvelope({ error: err?.message || "backend_error", installId, tier, statusHint: err?.status || 502, }); return res.status(e.statusHint).json(e.body); } let creditCharged = 0; if (!reusedJob) { await commitCredit(installId, { backend: chosenBackend, tier }); markJobCharged(installId, jobId, { backend: chosenBackend, tier }); creditCharged = 1; } const costDetails = chosenBackend === "gemini" && result.usage ? calcGeminiCost(result.model, result.usage) : { input_tokens: 0, output_tokens: 0, thinking_tokens: 0, cost_usd: 0, }; await recordCall({ install_id: installId, tier, pipeline: "analyze", backend: chosenBackend, model: result?.model || null, status: "success", credit_charged: creditCharged, duration_ms: Date.now() - t0, job_id: jobId, ...costDetails, }); const body = await envelope({ result, installId, tier, creditCharged }); res.json(body); }); return router; }