Files
OpenSquawk/server/models/UsageEvent.ts
leubeem f8fdd8bc79 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>
2026-06-10 23:17:03 +02:00

46 lines
1.5 KiB
TypeScript

import mongoose from 'mongoose'
const { Schema } = mongoose
export type UsageKind = 'stt' | 'tts' | 'llm'
export type UsageProvider = 'openai' | 'speaches' | 'piper' | 'cache'
export interface UsageEventDocument extends mongoose.Document {
user?: mongoose.Types.ObjectId
sessionId?: string
kind: UsageKind
provider: UsageProvider
model: string
endpoint: string
/** Audio length for STT in seconds */
audioSeconds?: number
/** Synthesized text length for TTS */
characters?: number
inputTokens?: number
outputTokens?: number
/** Estimated cost in USD (0 for self-hosted providers and cache hits) */
costUsd: number
createdAt: Date
}
const usageEventSchema = new mongoose.Schema<UsageEventDocument>({
user: { type: Schema.Types.ObjectId, ref: 'User', index: true },
sessionId: { type: String },
kind: { type: String, enum: ['stt', 'tts', 'llm'], required: true },
provider: { type: String, enum: ['openai', 'speaches', 'piper', 'cache'], required: true },
model: { type: String, required: true },
endpoint: { type: String, required: true },
audioSeconds: { type: Number },
characters: { type: Number },
inputTokens: { type: Number },
outputTokens: { type: Number },
costUsd: { type: Number, required: true, default: 0 },
createdAt: { type: Date, default: () => new Date(), index: true },
})
usageEventSchema.index({ user: 1, createdAt: -1 })
export const UsageEvent =
(mongoose.models.UsageEvent as mongoose.Model<UsageEventDocument> | undefined) ||
mongoose.model<UsageEventDocument>('UsageEvent', usageEventSchema)