Files
OpenSquawk/server/api/service/tools/latency.get.ts
leubeem 7367f7cdb7 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

66 lines
2.3 KiB
TypeScript

// server/api/llm/latency.get.ts
import { createError } from 'h3'
import { getOpenAIClient } from '../../../utils/openai'
import { getServerRuntimeConfig } from '../../../utils/runtimeConfig'
import { requireUserSession } from '../../../utils/auth'
import { enforceRateLimit } from '../../../utils/rateLimit'
import { recordUsage } from '../../../utils/usage'
const SYSTEM_PROMPT =
'Check if the pilot readback contains ALL of: Frankfurt or EDDF, FL320, and 120.8 MHz. ' +
'Reply with single digit only: 1 = all present, 0 = one or more missing, 2 = invalid/unrelated.';
const READBACK =
'Lufthanser four seven eight cleared fra via NORDA1A, climb 5000 feet, expect flight level tree too zero, dep 120 decimal 8, squawk 4213.';
export default defineEventHandler(async (event) => {
const user = await requireUserSession(event)
enforceRateLimit(event, 'tools-latency', String(user._id), 5)
const client = getOpenAIClient()
const { llmModel } = getServerRuntimeConfig()
const model = llmModel || 'chatgpt-5-nano'
const started = Date.now()
try {
const response = await client.chat.completions.create({
model,
n: 1,
messages: [
{ role: 'system', content: SYSTEM_PROMPT },
{ role: 'user', content: `${READBACK}` }
],
})
const raw = response.choices?.[0]?.message?.content?.trim() || ''
const parsed = Number.parseInt(raw, 10)
const validResult = Number.isInteger(parsed) && parsed >= 0 && parsed <= 2 ? parsed : null
const latencyMs = Date.now() - started
await recordUsage({
user: String(user._id),
kind: 'llm',
provider: 'openai',
model,
endpoint: '/api/service/tools/latency',
inputTokens: response.usage?.prompt_tokens,
outputTokens: response.usage?.completion_tokens,
})
return {
result: validResult,
raw,
latency_ms: latencyMs
}
} catch (error: any) {
const latencyMs = Date.now() - started
console.error('[LLM] Latency check failed:', error)
throw createError({
statusCode: error?.status ?? 500,
statusMessage: error?.message ?? 'Latency check failed',
data: { latency_ms: latencyMs }
})
}
})