feat(server): per-user AI usage tracking, cost alerting, and endpoint hardening

Usage tracking:
- new UsageEvent collection records every STT/TTS/LLM call per user with
  provider, model, volume (audio seconds, characters, tokens) and an
  estimated USD cost; self-hosted providers (Speaches/Piper) and cache
  hits record at $0
- pricing table for whisper-1, tts-1, gpt-5-nano & co. in server/utils/usage.ts
- weekly KPI mail gains an "AI-Nutzung & Kosten" section: weekly and
  rolling 30-day cost, per-kind breakdown, top 5 users by cost
- quota alert mail when rolling 30-day cost exceeds USAGE_ALERT_USD
  (default $5), at most once per calendar month (UsageAlertDelivery)

Hardening:
- /api/atc/say now requires an authenticated session (middleware
  exemption removed); useFlightLabAudio sends the bearer token
- /api/service/tools/latency requires auth (was a public LLM endpoint)
- per-user rate limits: PTT 20/min, say 60/min, latency 5/min
- cron endpoints (waitlist-drip, weekly-kpi-report) require a shared
  secret via ?secret= or x-cron-secret (CRON_SECRET, falls back to
  KPI_CRON_SECRET); allowed with a warning while unset so existing
  deployments keep working
- PTT records the actual transcribed audio duration for billing accuracy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
leubeem
2026-06-10 23:17:03 +02:00
parent 0907e011fd
commit 0b7345ced7
7 changed files with 355 additions and 14 deletions

View File

@@ -7,6 +7,9 @@ import {normalize, TTS_MODEL, normalizeATC} from "../../utils/normalize";
import { getServerRuntimeConfig } from "../../utils/runtimeConfig";
import {request} from "node:http";
import { TransmissionLog } from "../../models/TransmissionLog";
import { requireUserSession } from "../../utils/auth";
import { enforceRateLimit } from "../../utils/rateLimit";
import { recordUsage } from "../../utils/usage";
function outDir() {
@@ -157,7 +160,12 @@ async function speachesTTS(
return Buffer.from(arr);
}
const SAY_RATE_LIMIT_PER_MINUTE = 60;
export default defineEventHandler(async (event) => {
const user = await requireUserSession(event);
enforceRateLimit(event, 'atc-say', String(user._id), SAY_RATE_LIMIT_PER_MINUTE);
const runtimeConfig = getServerRuntimeConfig();
const body = await readBody<{
text?: string;
@@ -177,8 +185,6 @@ export default defineEventHandler(async (event) => {
preNormalized?: boolean;
}>(event);
// const user = await requireUserSession(event);
const rawSessionId = typeof body?.sessionId === "string"
? body.sessionId.trim()
: "";
@@ -348,9 +354,20 @@ export default defineEventHandler(async (event) => {
}
};
await recordUsage({
user: String(user._id),
sessionId,
kind: 'tts',
// Cache hits cost nothing regardless of which provider filled the cache.
provider: cacheHit ? 'cache' : ttsProvider,
model: modelUsed,
endpoint: '/api/atc/say',
characters: normalized.length,
});
try {
await TransmissionLog.create({
// user: user._id,
user: user._id,
role: "atc",
channel: "say",
direction: "outgoing",