0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
253 lines
6.3 KiB
TypeScript
253 lines
6.3 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import logger from '../../src/logger';
|
|
import { TokenUsageTracker } from '../../src/util/tokenUsage';
|
|
|
|
import type { TokenUsage } from '../../src/types/shared';
|
|
|
|
describe('TokenUsageTracker', () => {
|
|
it('redacts URL credentials in provider ids when logging tracked usage', () => {
|
|
// The debug message string is the only redaction layer for provider ids —
|
|
// logger sanitizes context objects, never message strings.
|
|
const debugSpy = vi.spyOn(logger, 'debug').mockImplementation(() => logger);
|
|
try {
|
|
TokenUsageTracker.getInstance().trackUsage(
|
|
'https://api.example.com/v1?api_key=sk-12345678901234567890 (HttpProvider)',
|
|
{ total: 1 },
|
|
);
|
|
const messages = debugSpy.mock.calls.map((call) => String(call[0])).join('\n');
|
|
expect(messages).toContain('api_key=%5BREDACTED%5D');
|
|
expect(messages).toContain('(HttpProvider)');
|
|
expect(messages).not.toContain('sk-12345678901234567890');
|
|
} finally {
|
|
debugSpy.mockRestore();
|
|
TokenUsageTracker.getInstance().resetAllUsage();
|
|
}
|
|
});
|
|
|
|
let tracker: TokenUsageTracker;
|
|
|
|
beforeEach(() => {
|
|
tracker = TokenUsageTracker.getInstance();
|
|
tracker.resetAllUsage();
|
|
});
|
|
|
|
afterEach(() => {
|
|
tracker.cleanup();
|
|
});
|
|
|
|
it('should track token usage for a provider', () => {
|
|
const usage: TokenUsage = {
|
|
total: 100,
|
|
prompt: 50,
|
|
completion: 50,
|
|
cached: 10,
|
|
numRequests: 1,
|
|
completionDetails: {
|
|
reasoning: 20,
|
|
acceptedPrediction: 15,
|
|
rejectedPrediction: 5,
|
|
},
|
|
assertions: {
|
|
total: 30,
|
|
prompt: 10,
|
|
completion: 15,
|
|
cached: 5,
|
|
},
|
|
};
|
|
|
|
tracker.trackUsage('test-provider', usage);
|
|
const tracked = tracker.getProviderUsage('test-provider');
|
|
|
|
expect(tracked).toEqual({
|
|
...usage,
|
|
completionDetails: {
|
|
reasoning: 20,
|
|
acceptedPrediction: 15,
|
|
rejectedPrediction: 5,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
assertions: {
|
|
...usage.assertions,
|
|
numRequests: 0,
|
|
completionDetails: {
|
|
reasoning: 0,
|
|
acceptedPrediction: 0,
|
|
rejectedPrediction: 0,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should handle undefined token usage', () => {
|
|
tracker.trackUsage('test-provider', undefined);
|
|
expect(tracker.getProviderUsage('test-provider')).toEqual(
|
|
expect.objectContaining({ numRequests: 1 }),
|
|
);
|
|
});
|
|
|
|
it('should merge token usage for the same provider', () => {
|
|
const usage1: TokenUsage = {
|
|
total: 100,
|
|
prompt: 50,
|
|
completion: 50,
|
|
cached: 10,
|
|
numRequests: 1,
|
|
completionDetails: {
|
|
reasoning: 20,
|
|
acceptedPrediction: 15,
|
|
rejectedPrediction: 5,
|
|
},
|
|
assertions: {
|
|
total: 30,
|
|
prompt: 10,
|
|
completion: 15,
|
|
cached: 5,
|
|
},
|
|
};
|
|
|
|
const usage2: TokenUsage = {
|
|
total: 200,
|
|
prompt: 100,
|
|
completion: 100,
|
|
cached: 20,
|
|
numRequests: 2,
|
|
completionDetails: {
|
|
reasoning: 40,
|
|
acceptedPrediction: 30,
|
|
rejectedPrediction: 10,
|
|
},
|
|
assertions: {
|
|
total: 60,
|
|
prompt: 20,
|
|
completion: 30,
|
|
cached: 10,
|
|
},
|
|
};
|
|
|
|
tracker.trackUsage('test-provider', usage1);
|
|
tracker.trackUsage('test-provider', usage2);
|
|
|
|
const merged = tracker.getProviderUsage('test-provider');
|
|
expect(merged).toEqual({
|
|
total: 300,
|
|
prompt: 150,
|
|
completion: 150,
|
|
cached: 30,
|
|
numRequests: 3,
|
|
completionDetails: {
|
|
reasoning: 60,
|
|
acceptedPrediction: 45,
|
|
rejectedPrediction: 15,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
assertions: {
|
|
total: 90,
|
|
prompt: 30,
|
|
completion: 45,
|
|
cached: 15,
|
|
numRequests: 0,
|
|
completionDetails: {
|
|
reasoning: 0,
|
|
acceptedPrediction: 0,
|
|
rejectedPrediction: 0,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should get provider IDs', () => {
|
|
tracker.trackUsage('provider1', { total: 100 });
|
|
tracker.trackUsage('provider2', { total: 200 });
|
|
|
|
expect(tracker.getProviderIds()).toEqual(['provider1', 'provider2']);
|
|
});
|
|
|
|
it('should get total usage across all providers', () => {
|
|
tracker.trackUsage('provider1', {
|
|
total: 100,
|
|
prompt: 50,
|
|
completion: 50,
|
|
cached: 10,
|
|
numRequests: 1,
|
|
completionDetails: {
|
|
reasoning: 20,
|
|
acceptedPrediction: 15,
|
|
rejectedPrediction: 5,
|
|
},
|
|
assertions: {
|
|
total: 30,
|
|
prompt: 10,
|
|
completion: 15,
|
|
cached: 5,
|
|
},
|
|
});
|
|
|
|
tracker.trackUsage('provider2', {
|
|
total: 200,
|
|
prompt: 100,
|
|
completion: 100,
|
|
cached: 20,
|
|
numRequests: 2,
|
|
completionDetails: {
|
|
reasoning: 40,
|
|
acceptedPrediction: 30,
|
|
rejectedPrediction: 10,
|
|
},
|
|
assertions: {
|
|
total: 60,
|
|
prompt: 20,
|
|
completion: 30,
|
|
cached: 10,
|
|
},
|
|
});
|
|
|
|
expect(tracker.getTotalUsage()).toEqual({
|
|
total: 300,
|
|
prompt: 150,
|
|
completion: 150,
|
|
cached: 30,
|
|
numRequests: 3,
|
|
completionDetails: {
|
|
reasoning: 60,
|
|
acceptedPrediction: 45,
|
|
rejectedPrediction: 15,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
assertions: {
|
|
total: 90,
|
|
prompt: 30,
|
|
completion: 45,
|
|
cached: 15,
|
|
numRequests: 0,
|
|
completionDetails: {
|
|
reasoning: 0,
|
|
acceptedPrediction: 0,
|
|
rejectedPrediction: 0,
|
|
cacheReadInputTokens: 0,
|
|
cacheCreationInputTokens: 0,
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should reset provider usage', () => {
|
|
tracker.trackUsage('provider1', { total: 100 });
|
|
tracker.resetProviderUsage('provider1');
|
|
expect(tracker.getProviderUsage('provider1')).toBeUndefined();
|
|
});
|
|
|
|
it('should reset all usage', () => {
|
|
tracker.trackUsage('provider1', { total: 100 });
|
|
tracker.trackUsage('provider2', { total: 200 });
|
|
tracker.resetAllUsage();
|
|
expect(tracker.getProviderIds()).toHaveLength(0);
|
|
});
|
|
});
|