feat(llm): add per-model usage counters for observability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { createUsageCounters } from '../usageCounters.js';
|
||||
|
||||
describe('usageCounters', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('tracks requests per model', () => {
|
||||
const counters = createUsageCounters();
|
||||
counters.record('free/model');
|
||||
counters.record('free/model');
|
||||
counters.record('paid/model');
|
||||
|
||||
const stats = counters.getStats();
|
||||
expect(stats['free/model'].total).toBe(2);
|
||||
expect(stats['paid/model'].total).toBe(1);
|
||||
});
|
||||
|
||||
it('resets daily counts at midnight UTC', () => {
|
||||
// Set time to 23:59:59 UTC
|
||||
const nearMidnight = new Date('2026-03-09T23:59:59Z');
|
||||
vi.setSystemTime(nearMidnight);
|
||||
|
||||
const counters = createUsageCounters();
|
||||
counters.record('free/model');
|
||||
expect(counters.getStats()['free/model'].total).toBe(1);
|
||||
|
||||
// Advance past midnight UTC
|
||||
vi.advanceTimersByTime(2000);
|
||||
counters.record('free/model');
|
||||
|
||||
// After reset, count should be 1 (just the new one)
|
||||
expect(counters.getStats()['free/model'].total).toBe(1);
|
||||
});
|
||||
|
||||
it('returns empty stats when no requests recorded', () => {
|
||||
const counters = createUsageCounters();
|
||||
expect(counters.getStats()).toEqual({});
|
||||
});
|
||||
|
||||
it('formats a summary string', () => {
|
||||
const counters = createUsageCounters();
|
||||
counters.record('free/model');
|
||||
counters.record('free/model');
|
||||
counters.record('paid/model');
|
||||
|
||||
const summary = counters.getSummary();
|
||||
expect(summary).toContain('free/model: 2');
|
||||
expect(summary).toContain('paid/model: 1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
interface ModelStats {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UsageCounters {
|
||||
record(model: string): void;
|
||||
getStats(): Record<string, ModelStats>;
|
||||
getSummary(): string;
|
||||
}
|
||||
|
||||
export function createUsageCounters(): UsageCounters {
|
||||
let counts = new Map<string, number>();
|
||||
let resetTime = getNextMidnightUTC();
|
||||
|
||||
function getNextMidnightUTC(): number {
|
||||
const now = new Date();
|
||||
const midnight = new Date(Date.UTC(
|
||||
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1,
|
||||
0, 0, 0, 0,
|
||||
));
|
||||
return midnight.getTime();
|
||||
}
|
||||
|
||||
function checkReset(): void {
|
||||
if (Date.now() >= resetTime) {
|
||||
counts = new Map();
|
||||
resetTime = getNextMidnightUTC();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
record(model: string): void {
|
||||
checkReset();
|
||||
counts.set(model, (counts.get(model) ?? 0) + 1);
|
||||
},
|
||||
|
||||
getStats(): Record<string, ModelStats> {
|
||||
checkReset();
|
||||
const result: Record<string, ModelStats> = {};
|
||||
for (const [model, total] of counts) {
|
||||
result[model] = { total };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
getSummary(): string {
|
||||
checkReset();
|
||||
if (counts.size === 0) return 'No LLM requests today';
|
||||
const parts: string[] = [];
|
||||
for (const [model, total] of counts) {
|
||||
parts.push(`${model}: ${total}`);
|
||||
}
|
||||
return `LLM usage today — ${parts.join(', ')}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user