v0.2.8 operator dashboard with per-call audit log + cost tracking

This commit is contained in:
local
2026-05-12 00:26:59 -05:00
parent 9af70302b1
commit 05ebeb5d51
12 changed files with 924 additions and 13 deletions
+45
View File
@@ -12,6 +12,8 @@ import { getConfigSnapshot } from "../config.js";
import { snapshotAll } from "../credits.js";
import { snapshotCache } from "../keysat-client.js";
import { snapshotJobs } from "../job-credits.js";
import { readEntries, aggregate } from "../audit-log.js";
import { GEMINI_PRICING } from "../pricing.js";
import fs from "fs/promises";
import path from "path";
@@ -48,6 +50,49 @@ export function adminRouter({ dataDir }) {
res.json({ entries: snapshotJobs() });
});
// ── Dashboard ─────────────────────────────────────────────────────────
// Time-range aggregations over the per-call audit log. Default range
// is the last 30 days; override with ?days=N or ?since=<ms-epoch>.
// Returns { range, summary, by_tier, by_model, by_pipeline,
// by_backend, by_install, by_hour_utc, cost_vs_speed, pricing }.
router.get("/dashboard", async (req, res) => {
const days =
typeof req.query.days === "string"
? parseInt(req.query.days, 10)
: null;
const explicitSince =
typeof req.query.since === "string"
? parseInt(req.query.since, 10)
: null;
const sinceMs =
explicitSince && Number.isFinite(explicitSince)
? explicitSince
: Date.now() -
(Number.isFinite(days) && days > 0 ? days : 30) *
24 *
3600 *
1000;
try {
const entries = await readEntries({ sinceMs });
const agg = aggregate(entries);
res.json({
range: {
since_ms: sinceMs,
until_ms: Date.now(),
days: Number.isFinite(days) && days > 0 ? days : null,
total_entries: entries.length,
},
...agg,
pricing: GEMINI_PRICING,
});
} catch (err) {
console.error(`[admin/dashboard] failed: ${err?.message || err}`);
res
.status(500)
.json({ error: "dashboard_failed", message: err?.message || String(err) });
}
});
// Adjust the live quotas blob. Same shape the StartOS action writes
// to relay_tier_quotas_json — kept here so the dashboard can tune
// quotas without round-tripping the StartOS UI.
+57
View File
@@ -10,6 +10,10 @@
// 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";
@@ -19,11 +23,14 @@ 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");
@@ -65,6 +72,19 @@ export function analyzeRouter() {
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,
@@ -98,6 +118,21 @@ export function analyzeRouter() {
} 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,
@@ -114,6 +149,28 @@ export function analyzeRouter() {
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);
});
+61 -9
View File
@@ -21,6 +21,10 @@
// result: { text: "[MM:SS] ...", segments: [], duration_seconds: 0 },
// credits_remaining, tier, credit_charged
// }
//
// 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 multer from "multer";
@@ -31,6 +35,8 @@ 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";
const upload = multer({
storage: multer.memoryStorage(),
@@ -41,6 +47,7 @@ export function transcribeRouter() {
const router = express.Router();
router.post("/transcribe", upload.single("audio"), 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");
@@ -60,14 +67,9 @@ export function transcribeRouter() {
const license = await resolveLicense(auth);
const tier = license.tier;
// Persist tier on the row so the admin dashboard reflects the
// most recently seen tier for this install.
const row = await getOrCreateRow(installId);
row.tier_snapshot = tier;
// Job-id dedup. If we've already charged this job, skip the
// credit check entirely — the user is paying once for the whole
// summarize job.
let reusedJob = false;
let chosenBackend = null;
const existingJob = lookupJob(installId, jobId);
@@ -82,6 +84,19 @@ export function transcribeRouter() {
cfg.relay_transcribe_backend_preference || "gemini_first";
const plan = planBackend(row, quota, { hasHardware, preference });
if (!plan.allowed) {
await recordCall({
install_id: installId,
tier,
pipeline: "transcribe",
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,
@@ -93,7 +108,6 @@ export function transcribeRouter() {
chosenBackend = plan.backend;
}
// Build the backend client based on chosenBackend.
const cfg = await getConfigSnapshot();
let result;
try {
@@ -126,10 +140,23 @@ export function transcribeRouter() {
});
}
} catch (err) {
// If we'd charged this job already (rare — most refundable
// failures happen on the FIRST call), refund.
if (reusedJob) refundJob(installId, jobId);
console.error(`[relay/transcribe] backend error: ${err?.message}`);
await recordCall({
install_id: installId,
tier,
pipeline: "transcribe",
backend: chosenBackend,
model: chosenBackend === "gemini"
? cfg.relay_gemini_transcription_model
: cfg.relay_parakeet_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,
@@ -139,7 +166,6 @@ export function transcribeRouter() {
return res.status(e.statusHint).json(e.body);
}
// Commit the credit on success (unless this was a job-id reuse).
let creditCharged = 0;
if (!reusedJob) {
await commitCredit(installId, { backend: chosenBackend, tier });
@@ -147,6 +173,32 @@ export function transcribeRouter() {
creditCharged = 1;
}
// Success — write the audit row with cost details. Gemini's usage
// metadata gives us token counts; calcGeminiCost translates that
// into USD. Hardware-served calls have no token data and we
// report cost_usd: 0 (operator's hardware is fixed-cost).
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: "transcribe",
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);
});