Files
metapi/src/server/services/proxyUsageParser.test.ts
T
cita 55b1ae564e feat: update error messages in NewApiAdapter tests to use correct Chinese translations
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
2026-03-06 17:10:01 +08:00

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