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
187 lines
4.3 KiB
TypeScript
187 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
calculateModelUsageBreakdown,
|
|
calculateModelUsageCost,
|
|
fallbackTokenCost,
|
|
type PricingModel,
|
|
} from './modelPricingService.js';
|
|
|
|
describe('modelPricingService', () => {
|
|
it('calculates token-based cost from model ratio and completion ratio', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'gpt-4o',
|
|
quotaType: 0,
|
|
modelRatio: 2,
|
|
completionRatio: 1.5,
|
|
modelPrice: null,
|
|
enableGroups: ['vip'],
|
|
};
|
|
|
|
const cost = calculateModelUsageCost(
|
|
model,
|
|
{
|
|
promptTokens: 1000,
|
|
completionTokens: 500,
|
|
totalTokens: 1500,
|
|
},
|
|
{ default: 1, vip: 2 },
|
|
);
|
|
|
|
expect(cost).toBe(0.014);
|
|
});
|
|
|
|
it('falls back to total tokens when split token usage is missing', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'claude-sonnet',
|
|
quotaType: 0,
|
|
modelRatio: 1,
|
|
completionRatio: 2,
|
|
modelPrice: null,
|
|
enableGroups: ['default'],
|
|
};
|
|
|
|
const cost = calculateModelUsageCost(
|
|
model,
|
|
{
|
|
promptTokens: 0,
|
|
completionTokens: 0,
|
|
totalTokens: 2000,
|
|
},
|
|
{ default: 1 },
|
|
);
|
|
|
|
expect(cost).toBe(0.004);
|
|
});
|
|
|
|
it('calculates per-call cost when quota type is call-based', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'gpt-image-1',
|
|
quotaType: 1,
|
|
modelRatio: 1,
|
|
completionRatio: 1,
|
|
modelPrice: 0.3,
|
|
enableGroups: ['vip'],
|
|
};
|
|
|
|
const cost = calculateModelUsageCost(
|
|
model,
|
|
{
|
|
promptTokens: 0,
|
|
completionTokens: 0,
|
|
totalTokens: 0,
|
|
},
|
|
{ default: 1, vip: 1.5 },
|
|
);
|
|
|
|
expect(cost).toBe(0.45);
|
|
});
|
|
|
|
it('calculates times-based per-call cost from input ratio only', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'flux-kontext-pro',
|
|
quotaType: 1,
|
|
modelRatio: 1,
|
|
completionRatio: 1,
|
|
modelPrice: { input: 1, output: 3 },
|
|
enableGroups: ['vip'],
|
|
};
|
|
|
|
const cost = calculateModelUsageCost(
|
|
model,
|
|
{
|
|
promptTokens: 0,
|
|
completionTokens: 0,
|
|
totalTokens: 0,
|
|
},
|
|
{ default: 1, vip: 2 },
|
|
);
|
|
|
|
expect(cost).toBe(0.004);
|
|
});
|
|
|
|
it('splits cache read and cache creation costs from prompt cost', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'gpt-4o',
|
|
quotaType: 0,
|
|
modelRatio: 2.5,
|
|
completionRatio: 5,
|
|
cacheRatio: 0.1,
|
|
cacheCreationRatio: 1.25,
|
|
modelPrice: null,
|
|
enableGroups: ['default'],
|
|
};
|
|
|
|
const detail = calculateModelUsageBreakdown(
|
|
model,
|
|
{
|
|
promptTokens: 146638,
|
|
completionTokens: 172,
|
|
totalTokens: 146810,
|
|
cacheReadTokens: 145692,
|
|
cacheCreationTokens: 945,
|
|
promptTokensIncludeCache: true,
|
|
},
|
|
{ default: 1 },
|
|
);
|
|
|
|
expect(detail).toMatchObject({
|
|
usage: {
|
|
billablePromptTokens: 1,
|
|
cacheReadTokens: 145692,
|
|
cacheCreationTokens: 945,
|
|
},
|
|
pricing: {
|
|
modelRatio: 2.5,
|
|
completionRatio: 5,
|
|
cacheRatio: 0.1,
|
|
cacheCreationRatio: 1.25,
|
|
groupRatio: 1,
|
|
},
|
|
breakdown: {
|
|
inputPerMillion: 5,
|
|
outputPerMillion: 25,
|
|
cacheReadPerMillion: 0.5,
|
|
cacheCreationPerMillion: 6.25,
|
|
inputCost: 0.000005,
|
|
outputCost: 0.0043,
|
|
cacheReadCost: 0.072846,
|
|
cacheCreationCost: 0.005906,
|
|
totalCost: 0.083057,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('keeps prompt tokens intact when upstream reports cache tokens separately', () => {
|
|
const model: PricingModel = {
|
|
modelName: 'claude-sonnet',
|
|
quotaType: 0,
|
|
modelRatio: 3,
|
|
completionRatio: 5,
|
|
cacheRatio: 0.3,
|
|
cacheCreationRatio: 1.25,
|
|
modelPrice: null,
|
|
enableGroups: ['default'],
|
|
};
|
|
|
|
const cost = calculateModelUsageCost(
|
|
model,
|
|
{
|
|
promptTokens: 120,
|
|
completionTokens: 30,
|
|
totalTokens: 150,
|
|
cacheReadTokens: 1000,
|
|
cacheCreationTokens: 40,
|
|
promptTokensIncludeCache: false,
|
|
},
|
|
{ default: 1 },
|
|
);
|
|
|
|
expect(cost).toBe(0.00372);
|
|
});
|
|
|
|
it('uses platform-specific fallback token divisor', () => {
|
|
expect(fallbackTokenCost(1500, 'new-api')).toBe(0.003);
|
|
expect(fallbackTokenCost(1500, 'veloera')).toBe(0.0015);
|
|
});
|
|
});
|