Spaces:
Sleeping
Sleeping
| /** | |
| * Request statistics collector with debounced JSON persistence. | |
| */ | |
| import { readFileSync, writeFileSync, existsSync } from 'fs'; | |
| import { join } from 'path'; | |
| const STATS_FILE = join(process.cwd(), 'stats.json'); | |
| const _state = { | |
| startedAt: Date.now(), | |
| totalRequests: 0, | |
| successCount: 0, | |
| errorCount: 0, | |
| modelCounts: {}, // { "gpt-4o-mini": { requests, success, errors, totalMs } } | |
| accountCounts: {}, // { "abc123": { requests, success, errors } } | |
| hourlyBuckets: [], // [{ hour: "2026-04-09T07:00:00Z", requests, errors }] | |
| }; | |
| // Load persisted stats | |
| try { | |
| if (existsSync(STATS_FILE)) { | |
| const saved = JSON.parse(readFileSync(STATS_FILE, 'utf-8')); | |
| Object.assign(_state, saved); | |
| } | |
| } catch {} | |
| // Debounced save | |
| let _saveTimer = null; | |
| function scheduleSave() { | |
| clearTimeout(_saveTimer); | |
| _saveTimer = setTimeout(() => { | |
| try { | |
| writeFileSync(STATS_FILE, JSON.stringify(_state, null, 2)); | |
| } catch {} | |
| }, 5000); | |
| } | |
| function getHourKey() { | |
| const d = new Date(); | |
| d.setMinutes(0, 0, 0); | |
| return d.toISOString(); | |
| } | |
| /** | |
| * Record a completed request. | |
| */ | |
| export function recordRequest(model, success, durationMs, accountId) { | |
| _state.totalRequests++; | |
| if (success) _state.successCount++; | |
| else _state.errorCount++; | |
| // Per-model stats (includes a small ring buffer for p50/p95 latency) | |
| if (!_state.modelCounts[model]) { | |
| _state.modelCounts[model] = { requests: 0, success: 0, errors: 0, totalMs: 0, recentMs: [] }; | |
| } | |
| const mc = _state.modelCounts[model]; | |
| mc.requests++; | |
| if (success) mc.success++; | |
| else mc.errors++; | |
| mc.totalMs += durationMs; | |
| if (!mc.recentMs) mc.recentMs = []; | |
| if (durationMs > 0) { | |
| mc.recentMs.push(durationMs); | |
| if (mc.recentMs.length > 200) mc.recentMs.shift(); | |
| } | |
| // Per-account stats | |
| if (accountId) { | |
| const aid = typeof accountId === 'string' ? accountId.slice(0, 8) : String(accountId); | |
| if (!_state.accountCounts[aid]) { | |
| _state.accountCounts[aid] = { requests: 0, success: 0, errors: 0 }; | |
| } | |
| const ac = _state.accountCounts[aid]; | |
| ac.requests++; | |
| if (success) ac.success++; | |
| else ac.errors++; | |
| } | |
| // Hourly bucket | |
| const hourKey = getHourKey(); | |
| let bucket = _state.hourlyBuckets.find(b => b.hour === hourKey); | |
| if (!bucket) { | |
| bucket = { hour: hourKey, requests: 0, errors: 0 }; | |
| _state.hourlyBuckets.push(bucket); | |
| // Keep last 72 hours | |
| if (_state.hourlyBuckets.length > 72) _state.hourlyBuckets.shift(); | |
| } | |
| bucket.requests++; | |
| if (!success) bucket.errors++; | |
| scheduleSave(); | |
| } | |
| function percentile(sortedArr, p) { | |
| if (!sortedArr.length) return 0; | |
| const idx = Math.min(sortedArr.length - 1, Math.floor(sortedArr.length * p)); | |
| return sortedArr[idx]; | |
| } | |
| /** Get all stats, with computed latency percentiles per model. */ | |
| export function getStats() { | |
| const out = { ..._state }; | |
| out.modelCounts = {}; | |
| for (const [m, s] of Object.entries(_state.modelCounts)) { | |
| const sorted = (s.recentMs || []).slice().sort((a, b) => a - b); | |
| out.modelCounts[m] = { | |
| requests: s.requests, | |
| success: s.success, | |
| errors: s.errors, | |
| totalMs: s.totalMs, | |
| avgMs: s.requests > 0 ? Math.round(s.totalMs / s.requests) : 0, | |
| p50Ms: Math.round(percentile(sorted, 0.5)), | |
| p95Ms: Math.round(percentile(sorted, 0.95)), | |
| }; | |
| } | |
| return out; | |
| } | |
| /** Reset all stats. */ | |
| export function resetStats() { | |
| _state.totalRequests = 0; | |
| _state.successCount = 0; | |
| _state.errorCount = 0; | |
| _state.modelCounts = {}; | |
| _state.accountCounts = {}; | |
| _state.hourlyBuckets = []; | |
| _state.startedAt = Date.now(); | |
| scheduleSave(); | |
| } | |