@lobehub/chat 1.68.9 → 1.68.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/changelog/v1.json +18 -0
- package/docs/usage/providers/ppio.mdx +5 -5
- package/docs/usage/providers/ppio.zh-CN.mdx +7 -7
- package/locales/ar/chat.json +5 -1
- package/locales/ar/models.json +12 -9
- package/locales/bg-BG/chat.json +5 -1
- package/locales/bg-BG/models.json +12 -9
- package/locales/de-DE/chat.json +5 -1
- package/locales/de-DE/models.json +12 -9
- package/locales/en-US/chat.json +5 -1
- package/locales/en-US/models.json +12 -9
- package/locales/es-ES/chat.json +5 -1
- package/locales/es-ES/models.json +12 -9
- package/locales/fa-IR/chat.json +5 -1
- package/locales/fa-IR/models.json +12 -9
- package/locales/fr-FR/chat.json +5 -1
- package/locales/fr-FR/models.json +12 -9
- package/locales/it-IT/chat.json +5 -1
- package/locales/it-IT/models.json +12 -9
- package/locales/ja-JP/chat.json +5 -1
- package/locales/ja-JP/models.json +12 -9
- package/locales/ko-KR/chat.json +5 -1
- package/locales/ko-KR/models.json +12 -9
- package/locales/nl-NL/chat.json +5 -1
- package/locales/nl-NL/models.json +12 -9
- package/locales/pl-PL/chat.json +5 -1
- package/locales/pl-PL/models.json +12 -9
- package/locales/pt-BR/chat.json +5 -1
- package/locales/pt-BR/models.json +12 -9
- package/locales/ru-RU/chat.json +5 -1
- package/locales/ru-RU/models.json +12 -9
- package/locales/tr-TR/chat.json +5 -1
- package/locales/tr-TR/models.json +12 -9
- package/locales/vi-VN/chat.json +5 -1
- package/locales/vi-VN/models.json +12 -9
- package/locales/zh-CN/chat.json +5 -1
- package/locales/zh-CN/models.json +12 -9
- package/locales/zh-TW/chat.json +5 -1
- package/locales/zh-TW/models.json +12 -9
- package/package.json +1 -1
- package/src/config/aiModels/google.ts +37 -0
- package/src/config/aiModels/perplexity.ts +36 -20
- package/src/config/aiModels/qwen.ts +64 -25
- package/src/config/modelProviders/ppio.ts +1 -1
- package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +27 -9
- package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +77 -35
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +253 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +65 -46
- package/src/libs/agent-runtime/baichuan/index.test.ts +58 -1
- package/src/libs/agent-runtime/groq/index.test.ts +36 -284
- package/src/libs/agent-runtime/mistral/index.test.ts +39 -300
- package/src/libs/agent-runtime/perplexity/index.test.ts +12 -10
- package/src/libs/agent-runtime/providerTestUtils.ts +58 -0
- package/src/libs/agent-runtime/togetherai/index.test.ts +7 -295
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +3 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +5 -2
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +89 -5
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +25 -8
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +188 -84
- package/src/libs/agent-runtime/utils/streams/openai.ts +8 -17
- package/src/libs/agent-runtime/utils/usageConverter.test.ts +249 -0
- package/src/libs/agent-runtime/utils/usageConverter.ts +50 -0
- package/src/libs/agent-runtime/zeroone/index.test.ts +7 -294
- package/src/locales/default/chat.ts +4 -0
- package/src/types/message/base.ts +14 -4
- package/src/utils/filter.test.ts +0 -122
- package/src/utils/filter.ts +0 -29
@@ -2,7 +2,7 @@ import { ModelIcon } from '@lobehub/icons';
|
|
2
2
|
import { Icon, Tooltip } from '@lobehub/ui';
|
3
3
|
import { Segmented } from 'antd';
|
4
4
|
import { createStyles } from 'antd-style';
|
5
|
-
import { ArrowDownToDot, ArrowUpFromDot, CircleFadingArrowUp } from 'lucide-react';
|
5
|
+
import { ArrowDownToDot, ArrowUpFromDot, BookUp2Icon, CircleFadingArrowUp } from 'lucide-react';
|
6
6
|
import { memo } from 'react';
|
7
7
|
import { useTranslation } from 'react-i18next';
|
8
8
|
import { Flexbox } from 'react-layout-kit';
|
@@ -45,12 +45,16 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
|
|
45
45
|
pricing?.cachedInput,
|
46
46
|
pricing?.currency as ModelPriceCurrency,
|
47
47
|
);
|
48
|
+
const writeCacheInputPrice = formatPriceByCurrency(
|
49
|
+
pricing?.writeCacheInput,
|
50
|
+
pricing?.currency as ModelPriceCurrency,
|
51
|
+
);
|
48
52
|
const outputPrice = formatPriceByCurrency(
|
49
53
|
pricing?.output,
|
50
54
|
pricing?.currency as ModelPriceCurrency,
|
51
55
|
);
|
52
56
|
return (
|
53
|
-
|
57
|
+
<Flexbox gap={8}>
|
54
58
|
<Flexbox
|
55
59
|
align={'center'}
|
56
60
|
className={styles.container}
|
@@ -91,26 +95,38 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
|
|
91
95
|
</Flexbox>
|
92
96
|
)}
|
93
97
|
</Flexbox>
|
94
|
-
{isShowCredit
|
98
|
+
{isShowCredit ? (
|
95
99
|
<Flexbox horizontal justify={'space-between'}>
|
96
100
|
<div />
|
97
101
|
<Flexbox align={'center'} className={styles.pricing} gap={8} horizontal>
|
98
102
|
{t('messages.modelCard.creditPricing')}:
|
103
|
+
{pricing?.cachedInput && (
|
104
|
+
<Tooltip
|
105
|
+
title={t('messages.modelCard.pricing.inputCachedTokens', {
|
106
|
+
amount: cachedInputPrice,
|
107
|
+
})}
|
108
|
+
>
|
109
|
+
<Flexbox gap={2} horizontal>
|
110
|
+
<Icon icon={CircleFadingArrowUp} />
|
111
|
+
{cachedInputPrice}
|
112
|
+
</Flexbox>
|
113
|
+
</Tooltip>
|
114
|
+
)}
|
99
115
|
<Tooltip title={t('messages.modelCard.pricing.inputTokens', { amount: inputPrice })}>
|
100
116
|
<Flexbox gap={2} horizontal>
|
101
117
|
<Icon icon={ArrowUpFromDot} />
|
102
118
|
{inputPrice}
|
103
119
|
</Flexbox>
|
104
120
|
</Tooltip>
|
105
|
-
{pricing?.
|
121
|
+
{pricing?.writeCacheInput && (
|
106
122
|
<Tooltip
|
107
|
-
title={t('messages.modelCard.pricing.
|
108
|
-
amount:
|
123
|
+
title={t('messages.modelCard.pricing.writeCacheInputTokens', {
|
124
|
+
amount: writeCacheInputPrice,
|
109
125
|
})}
|
110
126
|
>
|
111
127
|
<Flexbox gap={2} horizontal>
|
112
|
-
<Icon icon={
|
113
|
-
{
|
128
|
+
<Icon icon={BookUp2Icon} />
|
129
|
+
{writeCacheInputPrice}
|
114
130
|
</Flexbox>
|
115
131
|
</Tooltip>
|
116
132
|
)}
|
@@ -122,8 +138,10 @@ const ModelCard = memo<ModelCardProps>(({ pricing, id, provider, displayName })
|
|
122
138
|
</Tooltip>
|
123
139
|
</Flexbox>
|
124
140
|
</Flexbox>
|
141
|
+
) : (
|
142
|
+
<div style={{ height: 18 }} />
|
125
143
|
)}
|
126
|
-
|
144
|
+
</Flexbox>
|
127
145
|
);
|
128
146
|
});
|
129
147
|
|
@@ -37,6 +37,12 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
|
37
37
|
title: t('messages.tokenDetails.inputAudio'),
|
38
38
|
value: isShowCredit ? detailTokens.inputAudio.credit : detailTokens.inputAudio.token,
|
39
39
|
},
|
40
|
+
!!detailTokens.inputCitation && {
|
41
|
+
color: theme.orange,
|
42
|
+
id: 'inputText',
|
43
|
+
title: t('messages.tokenDetails.inputCitation'),
|
44
|
+
value: isShowCredit ? detailTokens.inputCitation.credit : detailTokens.inputCitation.token,
|
45
|
+
},
|
40
46
|
!!detailTokens.inputText && {
|
41
47
|
color: theme.green,
|
42
48
|
id: 'inputText',
|
@@ -46,11 +52,13 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
|
46
52
|
].filter(Boolean) as TokenProgressItem[];
|
47
53
|
|
48
54
|
const outputDetails = [
|
49
|
-
!!detailTokens.
|
55
|
+
!!detailTokens.outputReasoning && {
|
50
56
|
color: theme.pink,
|
51
57
|
id: 'reasoning',
|
52
58
|
title: t('messages.tokenDetails.reasoning'),
|
53
|
-
value: isShowCredit
|
59
|
+
value: isShowCredit
|
60
|
+
? detailTokens.outputReasoning.credit
|
61
|
+
: detailTokens.outputReasoning.token,
|
54
62
|
},
|
55
63
|
!!detailTokens.outputAudio && {
|
56
64
|
color: theme.cyan9,
|
@@ -67,18 +75,26 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
|
67
75
|
].filter(Boolean) as TokenProgressItem[];
|
68
76
|
|
69
77
|
const totalDetail = [
|
70
|
-
!!detailTokens.
|
78
|
+
!!detailTokens.inputCacheMiss && {
|
71
79
|
color: theme.colorFill,
|
72
80
|
|
73
81
|
id: 'uncachedInput',
|
74
82
|
title: t('messages.tokenDetails.inputUncached'),
|
75
|
-
value: isShowCredit ? detailTokens.
|
83
|
+
value: isShowCredit ? detailTokens.inputCacheMiss.credit : detailTokens.inputCacheMiss.token,
|
76
84
|
},
|
77
|
-
!!detailTokens.
|
85
|
+
!!detailTokens.inputCached && {
|
78
86
|
color: theme.orange,
|
79
|
-
id: '
|
87
|
+
id: 'inputCached',
|
80
88
|
title: t('messages.tokenDetails.inputCached'),
|
81
|
-
value: isShowCredit ? detailTokens.
|
89
|
+
value: isShowCredit ? detailTokens.inputCached.credit : detailTokens.inputCached.token,
|
90
|
+
},
|
91
|
+
!!detailTokens.inputCachedWrite && {
|
92
|
+
color: theme.yellow,
|
93
|
+
id: 'cachedWriteInput',
|
94
|
+
title: t('messages.tokenDetails.inputWriteCached'),
|
95
|
+
value: isShowCredit
|
96
|
+
? detailTokens.inputCachedWrite.credit
|
97
|
+
: detailTokens.inputCachedWrite.token,
|
82
98
|
},
|
83
99
|
!!detailTokens.totalOutput && {
|
84
100
|
color: theme.colorSuccess,
|
@@ -91,43 +107,69 @@ const TokenDetail = memo<TokenDetailProps>(({ usage, model, provider }) => {
|
|
91
107
|
const displayTotal =
|
92
108
|
isShowCredit && !!detailTokens.totalTokens
|
93
109
|
? formatNumber(detailTokens.totalTokens.credit)
|
94
|
-
: formatNumber(
|
110
|
+
: formatNumber(detailTokens.totalTokens!.token);
|
95
111
|
|
112
|
+
const averagePricing = formatNumber(
|
113
|
+
detailTokens.totalTokens!.credit / detailTokens.totalTokens!.token,
|
114
|
+
2,
|
115
|
+
);
|
96
116
|
return (
|
97
117
|
<Popover
|
98
118
|
arrow={false}
|
99
119
|
content={
|
100
|
-
<Flexbox gap={
|
120
|
+
<Flexbox gap={8} style={{ minWidth: 200 }}>
|
101
121
|
{modelCard && <ModelCard {...modelCard} provider={provider} />}
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
122
|
+
|
123
|
+
<Flexbox gap={20}>
|
124
|
+
{inputDetails.length > 1 && (
|
125
|
+
<Flexbox gap={4}>
|
126
|
+
<Flexbox
|
127
|
+
align={'center'}
|
128
|
+
gap={4}
|
129
|
+
horizontal
|
130
|
+
justify={'space-between'}
|
131
|
+
width={'100%'}
|
132
|
+
>
|
133
|
+
<div style={{ color: theme.colorTextDescription, fontSize: 12 }}>
|
134
|
+
{t('messages.tokenDetails.inputTitle')}
|
135
|
+
</div>
|
136
|
+
</Flexbox>
|
137
|
+
<TokenProgress data={inputDetails} showIcon />
|
108
138
|
</Flexbox>
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
{
|
139
|
+
)}
|
140
|
+
{outputDetails.length > 1 && (
|
141
|
+
<>
|
142
|
+
<Flexbox
|
143
|
+
align={'center'}
|
144
|
+
gap={4}
|
145
|
+
horizontal
|
146
|
+
justify={'space-between'}
|
147
|
+
width={'100%'}
|
148
|
+
>
|
149
|
+
<div style={{ color: theme.colorTextDescription }}>
|
150
|
+
{t('messages.tokenDetails.outputTitle')}
|
151
|
+
</div>
|
152
|
+
</Flexbox>
|
153
|
+
<TokenProgress data={outputDetails} showIcon />
|
154
|
+
</>
|
155
|
+
)}
|
156
|
+
<Flexbox>
|
157
|
+
<TokenProgress data={totalDetail} showIcon />
|
158
|
+
<Divider style={{ marginBlock: 8 }} />
|
159
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'}>
|
160
|
+
<div style={{ color: theme.colorTextSecondary }}>
|
161
|
+
{t('messages.tokenDetails.total')}
|
117
162
|
</div>
|
163
|
+
<div style={{ fontWeight: 500 }}>{displayTotal}</div>
|
118
164
|
</Flexbox>
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
<div style={{ color: theme.colorTextSecondary }}>
|
128
|
-
{t('messages.tokenDetails.total')}
|
129
|
-
</div>
|
130
|
-
<div style={{ fontWeight: 500 }}>{displayTotal}</div>
|
165
|
+
{isShowCredit && (
|
166
|
+
<Flexbox align={'center'} gap={4} horizontal justify={'space-between'}>
|
167
|
+
<div style={{ color: theme.colorTextSecondary }}>
|
168
|
+
{t('messages.tokenDetails.average')}
|
169
|
+
</div>
|
170
|
+
<div style={{ fontWeight: 500 }}>{averagePricing}</div>
|
171
|
+
</Flexbox>
|
172
|
+
)}
|
131
173
|
</Flexbox>
|
132
174
|
</Flexbox>
|
133
175
|
</Flexbox>
|
@@ -0,0 +1,253 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import { LobeDefaultAiModelListItem } from '@/types/aiModel';
|
4
|
+
import { ModelTokensUsage } from '@/types/message';
|
5
|
+
|
6
|
+
import { getDetailsToken } from './tokens';
|
7
|
+
|
8
|
+
describe('getDetailsToken', () => {
|
9
|
+
// 基本测试数据
|
10
|
+
const mockModelCard: LobeDefaultAiModelListItem = {
|
11
|
+
pricing: {
|
12
|
+
input: 0.01,
|
13
|
+
output: 0.02,
|
14
|
+
cachedInput: 0.005,
|
15
|
+
audioInput: 0.03,
|
16
|
+
audioOutput: 0.04,
|
17
|
+
},
|
18
|
+
} as LobeDefaultAiModelListItem;
|
19
|
+
|
20
|
+
it('should return empty object when usage is empty', () => {
|
21
|
+
const usage: ModelTokensUsage = {};
|
22
|
+
const result = getDetailsToken(usage);
|
23
|
+
|
24
|
+
expect(result).toEqual({
|
25
|
+
cachedInput: undefined,
|
26
|
+
inputAudio: undefined,
|
27
|
+
inputCitation: undefined,
|
28
|
+
inputText: undefined,
|
29
|
+
outputAudio: undefined,
|
30
|
+
outputText: undefined,
|
31
|
+
reasoning: undefined,
|
32
|
+
totalOutput: undefined,
|
33
|
+
totalTokens: undefined,
|
34
|
+
uncachedInput: undefined,
|
35
|
+
});
|
36
|
+
});
|
37
|
+
|
38
|
+
it('should handle inputTextTokens correctly', () => {
|
39
|
+
const usage: ModelTokensUsage = {
|
40
|
+
inputTextTokens: 100,
|
41
|
+
};
|
42
|
+
|
43
|
+
const result = getDetailsToken(usage, mockModelCard);
|
44
|
+
|
45
|
+
expect(result.inputText).toEqual({
|
46
|
+
credit: 1, // 100 * 0.01 = 1
|
47
|
+
token: 100,
|
48
|
+
});
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should handle legacy inputTokens property', () => {
|
52
|
+
const usage = {
|
53
|
+
inputTokens: 100,
|
54
|
+
} as any;
|
55
|
+
|
56
|
+
const result = getDetailsToken(usage, mockModelCard);
|
57
|
+
|
58
|
+
expect(result.inputText).toEqual({
|
59
|
+
credit: 1, // 100 * 0.01 = 1
|
60
|
+
token: 100,
|
61
|
+
});
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should handle cachedTokens correctly', () => {
|
65
|
+
const usage = {
|
66
|
+
totalInputTokens: 200,
|
67
|
+
cachedTokens: 50,
|
68
|
+
} as ModelTokensUsage;
|
69
|
+
|
70
|
+
const result = getDetailsToken(usage, mockModelCard);
|
71
|
+
|
72
|
+
expect(result.inputCached).toEqual({
|
73
|
+
credit: 0, // 50 * 0.005 = 0.25, rounded to 0
|
74
|
+
token: 50,
|
75
|
+
});
|
76
|
+
|
77
|
+
expect(result.inputCacheMiss).toEqual({
|
78
|
+
credit: 2, // (200 - 50) * 0.01 = 1.5, rounded to 2
|
79
|
+
token: 150,
|
80
|
+
});
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should handle outputTokens correctly', () => {
|
84
|
+
const usage = { outputTokens: 150 } as ModelTokensUsage;
|
85
|
+
|
86
|
+
const result = getDetailsToken(usage, mockModelCard);
|
87
|
+
|
88
|
+
expect(result.outputText).toEqual({
|
89
|
+
credit: 3, // 150 * 0.02 = 3
|
90
|
+
token: 150,
|
91
|
+
});
|
92
|
+
|
93
|
+
expect(result.totalOutput).toEqual({
|
94
|
+
credit: 3,
|
95
|
+
token: 150,
|
96
|
+
});
|
97
|
+
});
|
98
|
+
|
99
|
+
it('should handle reasoningTokens correctly', () => {
|
100
|
+
const usage = {
|
101
|
+
outputTokens: 200,
|
102
|
+
reasoningTokens: 50,
|
103
|
+
} as ModelTokensUsage;
|
104
|
+
|
105
|
+
const result = getDetailsToken(usage, mockModelCard);
|
106
|
+
|
107
|
+
expect(result.outputReasoning).toEqual({
|
108
|
+
credit: 1, // 50 * 0.02 = 1
|
109
|
+
token: 50,
|
110
|
+
});
|
111
|
+
|
112
|
+
expect(result.outputText).toEqual({
|
113
|
+
credit: 3, // (200 - 50) * 0.02 = 3
|
114
|
+
token: 150,
|
115
|
+
});
|
116
|
+
});
|
117
|
+
|
118
|
+
it('should handle audio tokens correctly', () => {
|
119
|
+
const usage = {
|
120
|
+
inputAudioTokens: 100,
|
121
|
+
outputAudioTokens: 50,
|
122
|
+
outputTokens: 150,
|
123
|
+
} as ModelTokensUsage;
|
124
|
+
|
125
|
+
const result = getDetailsToken(usage, mockModelCard);
|
126
|
+
|
127
|
+
expect(result.inputAudio).toEqual({
|
128
|
+
credit: 3, // 100 * 0.03 = 3
|
129
|
+
token: 100,
|
130
|
+
});
|
131
|
+
|
132
|
+
expect(result.outputAudio).toEqual({
|
133
|
+
credit: 2, // 50 * 0.04 = 2
|
134
|
+
id: 'outputAudio',
|
135
|
+
token: 50,
|
136
|
+
});
|
137
|
+
|
138
|
+
expect(result.outputText).toEqual({
|
139
|
+
credit: 2, // (150 - 50) * 0.02 = 2
|
140
|
+
token: 100,
|
141
|
+
});
|
142
|
+
});
|
143
|
+
|
144
|
+
it('should handle inputCitationTokens correctly', () => {
|
145
|
+
const usage: ModelTokensUsage = {
|
146
|
+
inputCitationTokens: 75,
|
147
|
+
};
|
148
|
+
|
149
|
+
const result = getDetailsToken(usage, mockModelCard);
|
150
|
+
|
151
|
+
expect(result.inputCitation).toEqual({
|
152
|
+
credit: 1, // 75 * 0.01 = 0.75, rounded to 1
|
153
|
+
token: 75,
|
154
|
+
});
|
155
|
+
});
|
156
|
+
|
157
|
+
it('should handle totalTokens correctly', () => {
|
158
|
+
const usage = {
|
159
|
+
totalTokens: 500,
|
160
|
+
totalInputTokens: 200,
|
161
|
+
inputCachedTokens: 50,
|
162
|
+
outputTokens: 300,
|
163
|
+
} as ModelTokensUsage;
|
164
|
+
|
165
|
+
const result = getDetailsToken(usage, mockModelCard);
|
166
|
+
|
167
|
+
// uncachedInput: (200 - 50) * 0.01 = 1.5 -> 2
|
168
|
+
// cachedInput: 50 * 0.005 = 0.25 -> 0
|
169
|
+
// totalOutput: 300 * 0.02 = 6
|
170
|
+
// totalCredit = 2 + 0 + 6 = 8
|
171
|
+
|
172
|
+
expect(result.totalTokens).toEqual({
|
173
|
+
credit: 8,
|
174
|
+
token: 500,
|
175
|
+
});
|
176
|
+
});
|
177
|
+
|
178
|
+
it('should handle missing pricing information', () => {
|
179
|
+
const usage = { inputTextTokens: 100, outputTokens: 200 } as ModelTokensUsage;
|
180
|
+
|
181
|
+
const result = getDetailsToken(usage);
|
182
|
+
|
183
|
+
expect(result.inputText).toEqual({
|
184
|
+
credit: '-',
|
185
|
+
token: 100,
|
186
|
+
});
|
187
|
+
|
188
|
+
expect(result.outputText).toEqual({
|
189
|
+
credit: '-',
|
190
|
+
token: 200,
|
191
|
+
});
|
192
|
+
});
|
193
|
+
|
194
|
+
it('should handle complex scenario with all token types', () => {
|
195
|
+
const usage: ModelTokensUsage = {
|
196
|
+
totalTokens: 1000,
|
197
|
+
totalInputTokens: 400,
|
198
|
+
inputTextTokens: 300,
|
199
|
+
inputAudioTokens: 50,
|
200
|
+
inputCitationTokens: 50,
|
201
|
+
inputCachedTokens: 100,
|
202
|
+
totalOutputTokens: 600,
|
203
|
+
outputAudioTokens: 100,
|
204
|
+
outputReasoningTokens: 200,
|
205
|
+
};
|
206
|
+
|
207
|
+
const result = getDetailsToken(usage, mockModelCard);
|
208
|
+
|
209
|
+
expect(result).toMatchObject({
|
210
|
+
inputCached: {
|
211
|
+
credit: 1, // 100 * 0.005 = 0.5, rounded to 1
|
212
|
+
token: 100,
|
213
|
+
},
|
214
|
+
inputCacheMiss: {
|
215
|
+
credit: 3, // (400 - 100) * 0.01 = 3
|
216
|
+
token: 300,
|
217
|
+
},
|
218
|
+
inputText: {
|
219
|
+
credit: 3, // 300 * 0.01 = 3
|
220
|
+
token: 300,
|
221
|
+
},
|
222
|
+
inputAudio: {
|
223
|
+
credit: 2, // 50 * 0.03 = 1.5, rounded to 2
|
224
|
+
token: 50,
|
225
|
+
},
|
226
|
+
inputCitation: {
|
227
|
+
credit: 1, // 50 * 0.01 = 0.5, rounded to 1
|
228
|
+
token: 50,
|
229
|
+
},
|
230
|
+
outputAudio: {
|
231
|
+
credit: 4, // 100 * 0.04 = 4
|
232
|
+
id: 'outputAudio',
|
233
|
+
token: 100,
|
234
|
+
},
|
235
|
+
outputReasoning: {
|
236
|
+
credit: 4, // 200 * 0.02 = 4
|
237
|
+
token: 200,
|
238
|
+
},
|
239
|
+
outputText: {
|
240
|
+
credit: 6, // (600 - 200 - 100) * 0.02 = 6
|
241
|
+
token: 300,
|
242
|
+
},
|
243
|
+
totalOutput: {
|
244
|
+
credit: 12, // 600 * 0.02 = 12
|
245
|
+
token: 600,
|
246
|
+
},
|
247
|
+
totalTokens: {
|
248
|
+
credit: 16, // 3 + 1 + 12 = 16
|
249
|
+
token: 1000,
|
250
|
+
},
|
251
|
+
});
|
252
|
+
});
|
253
|
+
});
|
@@ -11,43 +11,75 @@ export const getDetailsToken = (
|
|
11
11
|
usage: ModelTokensUsage,
|
12
12
|
modelCard?: LobeDefaultAiModelListItem,
|
13
13
|
) => {
|
14
|
-
const
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
const inputTextTokens = usage.inputTextTokens || (usage as any).inputTokens || 0;
|
15
|
+
const totalInputTokens = usage.totalInputTokens || (usage as any).inputTokens || 0;
|
16
|
+
|
17
|
+
const totalOutputTokens = usage.totalOutputTokens || (usage as any).outputTokens || 0;
|
18
|
+
|
19
|
+
const outputReasoningTokens = usage.outputReasoningTokens || (usage as any).reasoningTokens || 0;
|
20
|
+
|
21
|
+
const outputTextTokens = usage.outputTextTokens
|
22
|
+
? usage.outputTextTokens
|
23
|
+
: totalOutputTokens - outputReasoningTokens - (usage.outputAudioTokens || 0);
|
24
|
+
|
25
|
+
const inputWriteCacheTokens = usage.inputWriteCacheTokens || 0;
|
26
|
+
const inputCacheTokens = usage.inputCachedTokens || (usage as any).cachedTokens || 0;
|
27
|
+
|
28
|
+
const inputCacheMissTokens = usage?.inputCacheMissTokens
|
29
|
+
? usage?.inputCacheMissTokens
|
30
|
+
: totalInputTokens - (inputCacheTokens || 0);
|
31
|
+
|
32
|
+
const inputCacheMissCredit = (
|
33
|
+
!!inputCacheMissTokens ? calcCredit(inputCacheMissTokens, modelCard?.pricing?.input) : 0
|
18
34
|
) as number;
|
19
35
|
|
20
|
-
const
|
21
|
-
!!
|
36
|
+
const inputCachedCredit = (
|
37
|
+
!!inputCacheTokens ? calcCredit(inputCacheTokens, modelCard?.pricing?.cachedInput) : 0
|
22
38
|
) as number;
|
23
39
|
|
24
|
-
const
|
25
|
-
|
40
|
+
const inputWriteCachedCredit = !!inputWriteCacheTokens
|
41
|
+
? (calcCredit(inputWriteCacheTokens, modelCard?.pricing?.writeCacheInput) as number)
|
42
|
+
: 0;
|
43
|
+
|
44
|
+
const totalOutputCredit = (
|
45
|
+
!!totalOutputTokens ? calcCredit(totalOutputTokens, modelCard?.pricing?.output) : 0
|
26
46
|
) as number;
|
47
|
+
const totalInputCredit = (
|
48
|
+
!!totalInputTokens ? calcCredit(totalInputTokens, modelCard?.pricing?.output) : 0
|
49
|
+
) as number;
|
50
|
+
|
51
|
+
const totalCredit =
|
52
|
+
inputCacheMissCredit + inputCachedCredit + inputWriteCachedCredit + totalOutputCredit;
|
27
53
|
|
28
|
-
const totalTokens = uncachedInputCredit + cachedInputCredit + totalOutput;
|
29
54
|
return {
|
30
|
-
cachedInput: !!usage.cachedTokens
|
31
|
-
? {
|
32
|
-
credit: cachedInputCredit,
|
33
|
-
token: usage.cachedTokens,
|
34
|
-
}
|
35
|
-
: undefined,
|
36
55
|
inputAudio: !!usage.inputAudioTokens
|
37
56
|
? {
|
38
57
|
credit: calcCredit(usage.inputAudioTokens, modelCard?.pricing?.audioInput),
|
39
58
|
token: usage.inputAudioTokens,
|
40
59
|
}
|
41
60
|
: undefined,
|
42
|
-
|
61
|
+
inputCacheMiss: !!inputCacheMissTokens
|
62
|
+
? { credit: inputCacheMissCredit, token: inputCacheMissTokens }
|
63
|
+
: undefined,
|
64
|
+
inputCached: !!inputCacheTokens
|
65
|
+
? { credit: inputCachedCredit, token: inputCacheTokens }
|
66
|
+
: undefined,
|
67
|
+
inputCachedWrite: !!inputWriteCacheTokens
|
68
|
+
? { credit: inputWriteCachedCredit, token: inputWriteCacheTokens }
|
69
|
+
: undefined,
|
70
|
+
inputCitation: !!usage.inputCitationTokens
|
71
|
+
? {
|
72
|
+
credit: calcCredit(usage.inputCitationTokens, modelCard?.pricing?.input),
|
73
|
+
token: usage.inputCitationTokens,
|
74
|
+
}
|
75
|
+
: undefined,
|
76
|
+
inputText: !!inputTextTokens
|
43
77
|
? {
|
44
|
-
credit: calcCredit(
|
45
|
-
|
46
|
-
modelCard?.pricing?.input,
|
47
|
-
),
|
48
|
-
token: usage.inputTokens - (usage.inputAudioTokens || 0),
|
78
|
+
credit: calcCredit(inputTextTokens, modelCard?.pricing?.input),
|
79
|
+
token: inputTextTokens,
|
49
80
|
}
|
50
81
|
: undefined,
|
82
|
+
|
51
83
|
outputAudio: !!usage.outputAudioTokens
|
52
84
|
? {
|
53
85
|
credit: calcCredit(usage.outputAudioTokens, modelCard?.pricing?.audioOutput),
|
@@ -55,40 +87,27 @@ export const getDetailsToken = (
|
|
55
87
|
token: usage.outputAudioTokens,
|
56
88
|
}
|
57
89
|
: undefined,
|
58
|
-
|
59
|
-
outputText: !!usage.outputTokens
|
90
|
+
outputReasoning: !!outputReasoningTokens
|
60
91
|
? {
|
61
|
-
credit: calcCredit(
|
62
|
-
|
63
|
-
modelCard?.pricing?.output,
|
64
|
-
),
|
65
|
-
token: usage.outputTokens - (usage.reasoningTokens || 0) - (usage.outputAudioTokens || 0),
|
92
|
+
credit: calcCredit(outputReasoningTokens, modelCard?.pricing?.output),
|
93
|
+
token: outputReasoningTokens,
|
66
94
|
}
|
67
95
|
: undefined,
|
68
|
-
|
96
|
+
outputText: !!outputTextTokens
|
69
97
|
? {
|
70
|
-
credit: calcCredit(
|
71
|
-
token:
|
98
|
+
credit: calcCredit(outputTextTokens, modelCard?.pricing?.output),
|
99
|
+
token: outputTextTokens,
|
72
100
|
}
|
73
101
|
: undefined,
|
74
102
|
|
75
|
-
|
76
|
-
? {
|
77
|
-
credit: totalOutput,
|
78
|
-
token: usage.outputTokens,
|
79
|
-
}
|
103
|
+
totalInput: !!totalInputTokens
|
104
|
+
? { credit: totalInputCredit, token: totalInputTokens }
|
80
105
|
: undefined,
|
81
|
-
|
82
|
-
? {
|
83
|
-
credit: totalTokens,
|
84
|
-
token: usage.totalTokens,
|
85
|
-
}
|
106
|
+
totalOutput: !!totalOutputTokens
|
107
|
+
? { credit: totalOutputCredit, token: totalOutputTokens }
|
86
108
|
: undefined,
|
87
|
-
|
88
|
-
? {
|
89
|
-
credit: uncachedInputCredit,
|
90
|
-
token: usage.inputTokens - (usage.cachedTokens || 0),
|
91
|
-
}
|
109
|
+
totalTokens: !!usage.totalTokens
|
110
|
+
? { credit: totalCredit, token: usage.totalTokens }
|
92
111
|
: undefined,
|
93
112
|
};
|
94
113
|
};
|