Skip to content

Commit

Permalink
refactor token calc
Browse files Browse the repository at this point in the history
  • Loading branch information
arvinxx committed Mar 6, 2025
1 parent 989f4cf commit 60d96da
Show file tree
Hide file tree
Showing 10 changed files with 705 additions and 80 deletions.
16 changes: 9 additions & 7 deletions src/features/Conversation/Extras/Usage/UsageDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
].filter(Boolean) as TokenProgressItem[];

const outputDetails = [
!!detailTokens.reasoning && {
!!detailTokens.outputReasoning && {
color: theme.pink,
id: 'reasoning',
title: t('messages.tokenDetails.reasoning'),
value: isShowCredit ? detailTokens.reasoning.credit : detailTokens.reasoning.token,
value: isShowCredit
? detailTokens.outputReasoning.credit
: detailTokens.outputReasoning.token,
},
!!detailTokens.outputAudio && {
color: theme.cyan9,
Expand All @@ -73,18 +75,18 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
].filter(Boolean) as TokenProgressItem[];

const totalDetail = [
!!detailTokens.uncachedInput && {
!!detailTokens.inputCached && {
color: theme.colorFill,

id: 'uncachedInput',
title: t('messages.tokenDetails.inputUncached'),
value: isShowCredit ? detailTokens.uncachedInput.credit : detailTokens.uncachedInput.token,
value: isShowCredit ? detailTokens.inputCached.credit : detailTokens.inputCached.token,
},
!!detailTokens.cachedInput && {
!!detailTokens.inputCacheMiss && {
color: theme.orange,
id: 'cachedInput',
id: 'inputCacheMiss',
title: t('messages.tokenDetails.inputCached'),
value: isShowCredit ? detailTokens.cachedInput.credit : detailTokens.cachedInput.token,
value: isShowCredit ? detailTokens.inputCacheMiss.credit : detailTokens.inputCacheMiss.token,
},
!!detailTokens.totalOutput && {
color: theme.colorSuccess,
Expand Down
253 changes: 253 additions & 0 deletions src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import { describe, expect, it } from 'vitest';

import { LobeDefaultAiModelListItem } from '@/types/aiModel';
import { ModelTokensUsage } from '@/types/message';

import { getDetailsToken } from './tokens';

describe('getDetailsToken', () => {
// 基本测试数据
const mockModelCard: LobeDefaultAiModelListItem = {
pricing: {
input: 0.01,
output: 0.02,
cachedInput: 0.005,
audioInput: 0.03,
audioOutput: 0.04,
},
} as LobeDefaultAiModelListItem;

it('should return empty object when usage is empty', () => {
const usage: ModelTokensUsage = {};
const result = getDetailsToken(usage);

expect(result).toEqual({
cachedInput: undefined,
inputAudio: undefined,
inputCitation: undefined,
inputText: undefined,
outputAudio: undefined,
outputText: undefined,
reasoning: undefined,
totalOutput: undefined,
totalTokens: undefined,
uncachedInput: undefined,
});
});

it('should handle inputTextTokens correctly', () => {
const usage: ModelTokensUsage = {
inputTextTokens: 100,
};

const result = getDetailsToken(usage, mockModelCard);

expect(result.inputText).toEqual({
credit: 1, // 100 * 0.01 = 1
token: 100,
});
});

it('should handle legacy inputTokens property', () => {
const usage = {
inputTokens: 100,
} as any;

const result = getDetailsToken(usage, mockModelCard);

expect(result.inputText).toEqual({
credit: 1, // 100 * 0.01 = 1
token: 100,
});
});

it('should handle cachedTokens correctly', () => {
const usage: ModelTokensUsage = {
totalInputTokens: 200,
inputCachedTokens: 50,
};

const result = getDetailsToken(usage, mockModelCard);

expect(result.inputCached).toEqual({
credit: 0, // 50 * 0.005 = 0.25, rounded to 0
token: 50,
});

expect(result.inputCacheMiss).toEqual({
credit: 2, // (200 - 50) * 0.01 = 1.5, rounded to 2
token: 150,
});
});

it('should handle outputTokens correctly', () => {
const usage = { outputTokens: 150 } as ModelTokensUsage;

const result = getDetailsToken(usage, mockModelCard);

expect(result.outputText).toEqual({
credit: 3, // 150 * 0.02 = 3
token: 150,
});

expect(result.totalOutput).toEqual({
credit: 3,
token: 150,
});
});

it('should handle reasoningTokens correctly', () => {
const usage = {
outputTokens: 200,
reasoningTokens: 50,
} as ModelTokensUsage;

const result = getDetailsToken(usage, mockModelCard);

expect(result.outputReasoning).toEqual({
credit: 1, // 50 * 0.02 = 1
token: 50,
});

expect(result.outputText).toEqual({
credit: 3, // (200 - 50) * 0.02 = 3
token: 150,
});
});

it('should handle audio tokens correctly', () => {
const usage = {
inputAudioTokens: 100,
outputAudioTokens: 50,
outputTokens: 150,
} as ModelTokensUsage;

const result = getDetailsToken(usage, mockModelCard);

expect(result.inputAudio).toEqual({
credit: 3, // 100 * 0.03 = 3
token: 100,
});

expect(result.outputAudio).toEqual({
credit: 2, // 50 * 0.04 = 2
id: 'outputAudio',
token: 50,
});

expect(result.outputText).toEqual({
credit: 2, // (150 - 50) * 0.02 = 2
token: 100,
});
});

it('should handle inputCitationTokens correctly', () => {
const usage: ModelTokensUsage = {
inputCitationTokens: 75,
};

const result = getDetailsToken(usage, mockModelCard);

expect(result.inputCitation).toEqual({
credit: 1, // 75 * 0.01 = 0.75, rounded to 1
token: 75,
});
});

it('should handle totalTokens correctly', () => {
const usage = {
totalTokens: 500,
totalInputTokens: 200,
inputCachedTokens: 50,
outputTokens: 300,
} as ModelTokensUsage;

const result = getDetailsToken(usage, mockModelCard);

// uncachedInput: (200 - 50) * 0.01 = 1.5 -> 2
// cachedInput: 50 * 0.005 = 0.25 -> 0
// totalOutput: 300 * 0.02 = 6
// totalCredit = 2 + 0 + 6 = 8

expect(result.totalTokens).toEqual({
credit: 8,
token: 500,
});
});

it('should handle missing pricing information', () => {
const usage = { inputTextTokens: 100, outputTokens: 200 } as ModelTokensUsage;

const result = getDetailsToken(usage);

expect(result.inputText).toEqual({
credit: '-',
token: 100,
});

expect(result.outputText).toEqual({
credit: '-',
token: 200,
});
});

it('should handle complex scenario with all token types', () => {
const usage: ModelTokensUsage = {
totalTokens: 1000,
totalInputTokens: 400,
inputTextTokens: 300,
inputAudioTokens: 50,
inputCitationTokens: 50,
inputCachedTokens: 100,
totalOutputTokens: 600,
outputAudioTokens: 100,
outputReasoningTokens: 200,
};

const result = getDetailsToken(usage, mockModelCard);

expect(result).toMatchObject({
inputCached: {
credit: 1, // 100 * 0.005 = 0.5, rounded to 1
token: 100,
},
inputCacheMiss: {
credit: 3, // (400 - 100) * 0.01 = 3
token: 300,
},
inputText: {
credit: 3, // 300 * 0.01 = 3
token: 300,
},
inputAudio: {
credit: 2, // 50 * 0.03 = 1.5, rounded to 2
token: 50,
},
inputCitation: {
credit: 1, // 50 * 0.01 = 0.5, rounded to 1
token: 50,
},
outputAudio: {
credit: 4, // 100 * 0.04 = 4
id: 'outputAudio',
token: 100,
},
outputReasoning: {
credit: 4, // 200 * 0.02 = 4
token: 200,
},
outputText: {
credit: 6, // (600 - 200 - 100) * 0.02 = 6
token: 300,
},
totalOutput: {
credit: 12, // 600 * 0.02 = 12
token: 600,
},
totalTokens: {
credit: 16, // 3 + 1 + 12 = 16
token: 1000,
},
});
});
});
Loading

0 comments on commit 60d96da

Please sign in to comment.