Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

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);
});
});