feat: enhance proxy usage parser to include cache read and creation tokens feat: add billing details to ProxyLog interface and implement related formatting functions fix: correct mojibake issues in source files and add regression test for mojibake detection chore: add SQL migration for billing details in proxy_logs table test: improve site speed button tests to ensure proper state updates without DOM lookups
173 lines
4.0 KiB
TypeScript
173 lines
4.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
mergeProxyUsage,
|
|
parseProxyUsage,
|
|
pullSseDataEvents,
|
|
} from './proxyUsageParser.js';
|
|
|
|
describe('proxyUsageParser', () => {
|
|
it('parses standard OpenAI usage fields', () => {
|
|
const usage = parseProxyUsage({
|
|
usage: {
|
|
prompt_tokens: 123,
|
|
completion_tokens: 45,
|
|
total_tokens: 168,
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 123,
|
|
completionTokens: 45,
|
|
totalTokens: 168,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
});
|
|
});
|
|
|
|
it('parses input/output token style usage fields', () => {
|
|
const usage = parseProxyUsage({
|
|
usage: {
|
|
input_tokens: 80,
|
|
output_tokens: 20,
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 80,
|
|
completionTokens: 20,
|
|
totalTokens: 100,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
});
|
|
});
|
|
|
|
it('parses Gemini usageMetadata shape', () => {
|
|
const usage = parseProxyUsage({
|
|
usageMetadata: {
|
|
promptTokenCount: 12,
|
|
candidatesTokenCount: 34,
|
|
totalTokenCount: 46,
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 12,
|
|
completionTokens: 34,
|
|
totalTokens: 46,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
});
|
|
});
|
|
|
|
it('parses deeply nested usage payloads', () => {
|
|
const usage = parseProxyUsage({
|
|
data: {
|
|
result: {
|
|
response: {
|
|
usage: {
|
|
prompt_tokens: 210,
|
|
completion_tokens: 40,
|
|
total_tokens: 250,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 210,
|
|
completionTokens: 40,
|
|
totalTokens: 250,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
});
|
|
});
|
|
|
|
it('falls back to usage detail objects when aggregate fields are absent', () => {
|
|
const usage = parseProxyUsage({
|
|
usage: {
|
|
prompt_tokens_details: {
|
|
text_tokens: 7,
|
|
cached_tokens: 3,
|
|
},
|
|
completion_tokens_details: {
|
|
reasoning_tokens: 20,
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 10,
|
|
completionTokens: 20,
|
|
totalTokens: 30,
|
|
cacheReadTokens: 3,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: true,
|
|
});
|
|
});
|
|
|
|
it('parses anthropic cache usage fields without treating input tokens as cache-inclusive', () => {
|
|
const usage = parseProxyUsage({
|
|
usage: {
|
|
input_tokens: 120,
|
|
output_tokens: 30,
|
|
cache_read_input_tokens: 1000,
|
|
cache_creation_input_tokens: 40,
|
|
},
|
|
});
|
|
|
|
expect(usage).toEqual({
|
|
promptTokens: 120,
|
|
completionTokens: 30,
|
|
totalTokens: 150,
|
|
cacheReadTokens: 1000,
|
|
cacheCreationTokens: 40,
|
|
promptTokensIncludeCache: false,
|
|
});
|
|
});
|
|
|
|
it('merges usage snapshots by keeping richer values', () => {
|
|
const merged = mergeProxyUsage(
|
|
{
|
|
promptTokens: 0,
|
|
completionTokens: 0,
|
|
totalTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
},
|
|
{
|
|
promptTokens: 90,
|
|
completionTokens: 30,
|
|
totalTokens: 120,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
},
|
|
);
|
|
|
|
expect(merged).toEqual({
|
|
promptTokens: 90,
|
|
completionTokens: 30,
|
|
totalTokens: 120,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
promptTokensIncludeCache: null,
|
|
});
|
|
});
|
|
|
|
it('pulls SSE data events across chunk boundaries', () => {
|
|
const first = pullSseDataEvents('data: {"a":1}\n\ndata: {"b":');
|
|
expect(first.events).toEqual(['{"a":1}']);
|
|
expect(first.rest).toBe('data: {"b":');
|
|
|
|
const second = pullSseDataEvents(`${first.rest}2}\n\ndata: [DONE]\n\n`);
|
|
expect(second.events).toEqual(['{"b":2}']);
|
|
expect(second.rest).toBe('');
|
|
});
|
|
});
|