@lobehub/chat 1.133.2 → 1.133.3
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 +33 -0
- package/changelog/v1.json +12 -0
- package/locales/ar/image.json +7 -0
- package/locales/ar/models.json +1 -1
- package/locales/bg-BG/image.json +7 -0
- package/locales/de-DE/image.json +7 -0
- package/locales/en-US/image.json +7 -0
- package/locales/es-ES/image.json +7 -0
- package/locales/es-ES/tool.json +1 -1
- package/locales/fa-IR/image.json +7 -0
- package/locales/fa-IR/models.json +1 -1
- package/locales/fr-FR/image.json +7 -0
- package/locales/fr-FR/models.json +1 -1
- package/locales/it-IT/image.json +7 -0
- package/locales/ja-JP/image.json +7 -0
- package/locales/ko-KR/image.json +7 -0
- package/locales/nl-NL/image.json +7 -0
- package/locales/pl-PL/image.json +7 -0
- package/locales/pt-BR/image.json +7 -0
- package/locales/ru-RU/image.json +7 -0
- package/locales/ru-RU/tool.json +1 -1
- package/locales/tr-TR/image.json +7 -0
- package/locales/tr-TR/models.json +1 -1
- package/locales/vi-VN/image.json +7 -0
- package/locales/zh-CN/image.json +7 -0
- package/locales/zh-TW/image.json +7 -0
- package/package.json +4 -5
- package/packages/const/package.json +4 -0
- package/packages/const/src/currency.ts +2 -0
- package/packages/const/src/index.ts +1 -0
- package/packages/model-bank/package.json +2 -1
- package/packages/model-bank/src/aiModels/google.ts +6 -0
- package/packages/model-bank/src/aiModels/openai.ts +6 -22
- package/packages/model-bank/src/standard-parameters/index.ts +56 -46
- package/packages/model-runtime/package.json +1 -0
- package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
- package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
- package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
- package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
- package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
- package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
- package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
- package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
- package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
- package/packages/model-runtime/src/core/streams/spark.ts +3 -3
- package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
- package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
- package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
- package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
- package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
- package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
- package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
- package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
- package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
- package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
- package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
- package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
- package/packages/model-runtime/src/index.ts +2 -0
- package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
- package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
- package/packages/model-runtime/src/providers/google/index.ts +8 -1
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
- package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
- package/packages/model-runtime/src/types/chat.ts +5 -3
- package/packages/model-runtime/src/types/image.ts +20 -9
- package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
- package/packages/obervability-otel/package.json +2 -2
- package/packages/ssrf-safe-fetch/index.test.ts +343 -0
- package/packages/ssrf-safe-fetch/index.ts +37 -0
- package/packages/ssrf-safe-fetch/package.json +17 -0
- package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
- package/packages/types/src/message/base.ts +43 -17
- package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
- package/packages/utils/src/client/apiKeyManager.ts +41 -0
- package/packages/utils/src/client/index.ts +2 -0
- package/packages/utils/src/fetch/fetchSSE.ts +4 -4
- package/packages/utils/src/index.ts +1 -0
- package/packages/utils/src/toolManifest.ts +2 -1
- package/src/app/(backend)/webapi/proxy/route.ts +2 -13
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
- package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
- package/src/locales/default/image.ts +7 -0
- package/src/server/modules/EdgeConfig/index.ts +1 -1
- package/src/server/routers/async/image.ts +9 -1
- package/src/services/_auth.ts +12 -12
- package/src/services/chat/contextEngineering.ts +2 -3
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import anthropicChatModels from 'model-bank/anthropic';
|
|
2
|
+
import googleChatModels from 'model-bank/google';
|
|
3
|
+
import lobehubChatModels from 'model-bank/lobehub';
|
|
4
|
+
import openaiChatModels from 'model-bank/openai';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { ModelTokensUsage } from '@/types/message';
|
|
8
|
+
|
|
9
|
+
import { computeChatCost } from './computeChatCost';
|
|
10
|
+
|
|
11
|
+
describe('computeChatPricing', () => {
|
|
12
|
+
describe('OpenAI', () => {
|
|
13
|
+
it('handles simple request without cache for gpt-4.1', () => {
|
|
14
|
+
const pricing = openaiChatModels.find(
|
|
15
|
+
(model: { id: string }) => model.id === 'gpt-4.1',
|
|
16
|
+
)?.pricing;
|
|
17
|
+
expect(pricing).toBeDefined();
|
|
18
|
+
|
|
19
|
+
const usage: ModelTokensUsage = {
|
|
20
|
+
inputCacheMissTokens: 8,
|
|
21
|
+
inputTextTokens: 8,
|
|
22
|
+
outputTextTokens: 11,
|
|
23
|
+
totalInputTokens: 8,
|
|
24
|
+
totalOutputTokens: 11,
|
|
25
|
+
totalTokens: 19,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const result = computeChatCost(pricing, usage);
|
|
29
|
+
expect(result).toBeDefined();
|
|
30
|
+
expect(result?.issues).toHaveLength(0);
|
|
31
|
+
|
|
32
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
33
|
+
expect(breakdown).toHaveLength(2); // Only input and output, no cache
|
|
34
|
+
|
|
35
|
+
// Verify input tokens
|
|
36
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
37
|
+
expect(input?.quantity).toBe(8);
|
|
38
|
+
expect(input?.credits).toBe(16); // 8 * 2 = 16
|
|
39
|
+
|
|
40
|
+
// Verify output tokens
|
|
41
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
42
|
+
expect(output?.quantity).toBe(11);
|
|
43
|
+
expect(output?.credits).toBe(88); // 11 * 8 = 88
|
|
44
|
+
|
|
45
|
+
// Verify totals match the actual billing log
|
|
46
|
+
expect(totalCredits).toBe(104); // 16 + 88 = 104
|
|
47
|
+
expect(totalCost).toBeCloseTo(0.000104, 6); // 104 credits = $0.000104
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles request with cache read for gpt-4.1', () => {
|
|
51
|
+
const pricing = openaiChatModels.find(
|
|
52
|
+
(model: { id: string }) => model.id === 'gpt-4.1',
|
|
53
|
+
)?.pricing;
|
|
54
|
+
expect(pricing).toBeDefined();
|
|
55
|
+
|
|
56
|
+
const usage: ModelTokensUsage = {
|
|
57
|
+
inputCacheMissTokens: 145,
|
|
58
|
+
inputCachedTokens: 1024,
|
|
59
|
+
inputTextTokens: 1169,
|
|
60
|
+
outputTextTokens: 59,
|
|
61
|
+
totalInputTokens: 1169,
|
|
62
|
+
totalOutputTokens: 59,
|
|
63
|
+
totalTokens: 1228,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const result = computeChatCost(pricing, usage);
|
|
67
|
+
expect(result).toBeDefined();
|
|
68
|
+
expect(result?.issues).toHaveLength(0);
|
|
69
|
+
|
|
70
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
71
|
+
expect(breakdown).toHaveLength(3); // Input, output, and cache read
|
|
72
|
+
|
|
73
|
+
// Verify cache miss tokens (regular input)
|
|
74
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
75
|
+
expect(input?.quantity).toBe(145);
|
|
76
|
+
expect(input?.credits).toBe(290); // 145 * 2 = 290
|
|
77
|
+
|
|
78
|
+
// Verify output tokens
|
|
79
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
80
|
+
expect(output?.quantity).toBe(59);
|
|
81
|
+
expect(output?.credits).toBe(472); // 59 * 8 = 472
|
|
82
|
+
|
|
83
|
+
// Verify cached tokens (discounted rate)
|
|
84
|
+
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
|
85
|
+
expect(cached?.quantity).toBe(1024);
|
|
86
|
+
expect(cached?.credits).toBe(512); // 1024 * 0.5 = 512
|
|
87
|
+
|
|
88
|
+
// Verify totals match the actual billing log
|
|
89
|
+
expect(totalCredits).toBe(1274); // 290 + 472 + 512 = 1274
|
|
90
|
+
expect(totalCost).toBeCloseTo(0.001274, 6); // 1274 credits = $0.001274
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('handles reasoning tokens in output pricing for o3 model', () => {
|
|
94
|
+
const pricing = openaiChatModels.find(
|
|
95
|
+
(model: { id: string }) => model.id === 'gpt-4.1',
|
|
96
|
+
)?.pricing;
|
|
97
|
+
expect(pricing).toBeDefined();
|
|
98
|
+
|
|
99
|
+
const usage: ModelTokensUsage = {
|
|
100
|
+
inputCacheMissTokens: 58,
|
|
101
|
+
inputTextTokens: 58,
|
|
102
|
+
outputReasoningTokens: 384,
|
|
103
|
+
outputTextTokens: 1243,
|
|
104
|
+
totalInputTokens: 58,
|
|
105
|
+
totalOutputTokens: 1627, // 1243 + 384
|
|
106
|
+
totalTokens: 1685,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result = computeChatCost(pricing, usage);
|
|
110
|
+
expect(result).toBeDefined();
|
|
111
|
+
expect(result?.issues).toHaveLength(0);
|
|
112
|
+
|
|
113
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
114
|
+
expect(breakdown).toHaveLength(2); // Input and output
|
|
115
|
+
|
|
116
|
+
// Verify input tokens
|
|
117
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
118
|
+
expect(input?.quantity).toBe(58);
|
|
119
|
+
expect(input?.credits).toBe(116); // 58 * 2 = 116
|
|
120
|
+
|
|
121
|
+
// Verify output tokens include reasoning tokens
|
|
122
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
123
|
+
expect(output?.quantity).toBe(1627); // 1243 + 384 (reasoning tokens included)
|
|
124
|
+
expect(output?.credits).toBe(13_016); // 1627 * 8 = 13016
|
|
125
|
+
|
|
126
|
+
// Verify totals match the actual billing log
|
|
127
|
+
expect(totalCredits).toBe(13_132); // 116 + 13016 = 13132
|
|
128
|
+
expect(totalCost).toBeCloseTo(0.013132, 6); // 13132 credits = $0.013132
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Google', () => {
|
|
133
|
+
it('computes tiered pricing with reasoning tokens for large context conversation', () => {
|
|
134
|
+
const pricing = googleChatModels.find(
|
|
135
|
+
(model: { id: string }) => model.id === 'gemini-2.5-pro',
|
|
136
|
+
)?.pricing;
|
|
137
|
+
expect(pricing).toBeDefined();
|
|
138
|
+
|
|
139
|
+
const usage: ModelTokensUsage = {
|
|
140
|
+
inputCachedTokens: 253_891,
|
|
141
|
+
inputCacheMissTokens: 4_275, // totalInputTokens - inputCachedTokens = 258_166 - 253_891
|
|
142
|
+
inputTextTokens: 258_166,
|
|
143
|
+
outputReasoningTokens: 1_601,
|
|
144
|
+
outputTextTokens: 1_462,
|
|
145
|
+
totalInputTokens: 258_166,
|
|
146
|
+
totalOutputTokens: 3_063,
|
|
147
|
+
totalTokens: 261_229,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = computeChatCost(pricing, usage);
|
|
151
|
+
expect(result).toBeDefined();
|
|
152
|
+
expect(result?.issues).toHaveLength(0);
|
|
153
|
+
|
|
154
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
155
|
+
expect(breakdown).toHaveLength(3); // Input, cache read, and output
|
|
156
|
+
|
|
157
|
+
// Verify cached tokens (over 200k threshold, use higher tier rate)
|
|
158
|
+
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
|
159
|
+
expect(cached?.quantity).toBe(253_891);
|
|
160
|
+
expect(cached?.credits).toBeCloseTo(158_681.875, 6);
|
|
161
|
+
expect(cached?.segments).toEqual([{ quantity: 253_891, rate: 0.625, credits: 158_681.875 }]);
|
|
162
|
+
|
|
163
|
+
// Verify input cache miss tokens (calculated as totalInputTokens - inputCachedTokens = 4275)
|
|
164
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
165
|
+
expect(input?.quantity).toBe(4_275); // 258_166 - 253_891 = 4_275 (cache miss)
|
|
166
|
+
expect(input?.credits).toBeCloseTo(5_343.75, 6);
|
|
167
|
+
expect(input?.segments).toEqual([{ quantity: 4_275, rate: 1.25, credits: 5_343.75 }]);
|
|
168
|
+
|
|
169
|
+
// Verify output tokens include reasoning tokens (under 200k threshold, use lower tier rate)
|
|
170
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
171
|
+
expect(output?.quantity).toBe(3_063); // 1462 + 1601 = 3063 (reasoning tokens included)
|
|
172
|
+
expect(output?.credits).toBe(30_630); // 3063 * 10 = 30630
|
|
173
|
+
expect(output?.segments).toEqual([{ quantity: 3_063, rate: 10, credits: 30_630 }]);
|
|
174
|
+
|
|
175
|
+
// Verify corrected totals (no double counting of cached tokens)
|
|
176
|
+
expect(totalCredits).toBe(194_656); // ceil(158681.875 + 5343.75 + 30630) = 194656
|
|
177
|
+
expect(totalCost).toBeCloseTo(0.194656, 6); // 194656 credits = $0.194656
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('supports multi-modal fixed units for Gemini 2.5 Flash Image Preview', () => {
|
|
181
|
+
const pricing = googleChatModels.find(
|
|
182
|
+
(model: { id: string }) => model.id === 'gemini-2.5-flash-image-preview',
|
|
183
|
+
)?.pricing;
|
|
184
|
+
expect(pricing).toBeDefined();
|
|
185
|
+
|
|
186
|
+
const usage: ModelTokensUsage = {
|
|
187
|
+
inputCacheMissTokens: 10_000,
|
|
188
|
+
outputTextTokens: 5_000,
|
|
189
|
+
outputImageTokens: 400,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const result = computeChatCost(pricing, usage);
|
|
193
|
+
expect(result).toBeDefined();
|
|
194
|
+
expect(result?.issues).toHaveLength(0);
|
|
195
|
+
expect(result?.totalCredits).toBe(27_500);
|
|
196
|
+
expect(result?.totalCost).toBeCloseTo(0.0275, 10);
|
|
197
|
+
|
|
198
|
+
const input = result?.breakdown.find((item) => item.unit.name === 'textInput');
|
|
199
|
+
expect(input?.credits).toBe(3_000);
|
|
200
|
+
|
|
201
|
+
const outputText = result?.breakdown.find((item) => item.unit.name === 'textOutput');
|
|
202
|
+
expect(outputText?.credits).toBe(12_500);
|
|
203
|
+
|
|
204
|
+
const imageOutput = result?.breakdown.find((item) => item.unit.name === 'imageOutput');
|
|
205
|
+
expect(imageOutput?.credits).toBe(12_000);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('handles multi-modal image generation for Nano Banana', () => {
|
|
209
|
+
const pricing = googleChatModels.find(
|
|
210
|
+
(model: { id: string }) => model.id === 'gemini-2.5-flash-image-preview',
|
|
211
|
+
)?.pricing;
|
|
212
|
+
expect(pricing).toBeDefined();
|
|
213
|
+
|
|
214
|
+
const usage: ModelTokensUsage = {
|
|
215
|
+
inputImageTokens: 5160,
|
|
216
|
+
inputTextTokens: 60,
|
|
217
|
+
outputImageTokens: 1290,
|
|
218
|
+
outputTextTokens: 0,
|
|
219
|
+
totalInputTokens: 5220,
|
|
220
|
+
totalOutputTokens: 1290,
|
|
221
|
+
totalTokens: 6510,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = computeChatCost(pricing, usage);
|
|
225
|
+
expect(result).toBeDefined();
|
|
226
|
+
expect(result?.issues).toHaveLength(0);
|
|
227
|
+
expect(result?.totalCredits).toBe(40_266);
|
|
228
|
+
expect(result?.totalCost).toBeCloseTo(0.040266, 6);
|
|
229
|
+
|
|
230
|
+
const { breakdown } = result!;
|
|
231
|
+
expect(breakdown).toHaveLength(4); // Text input, image input, text output, image output
|
|
232
|
+
|
|
233
|
+
const textInput = result?.breakdown.find((item) => item.unit.name === 'textInput');
|
|
234
|
+
expect(textInput?.quantity).toBe(60);
|
|
235
|
+
expect(textInput?.credits).toBe(18); // 60 * 0.3 = 18
|
|
236
|
+
|
|
237
|
+
const imageInput = result?.breakdown.find((item) => item.unit.name === 'imageInput');
|
|
238
|
+
expect(imageInput?.quantity).toBe(5160);
|
|
239
|
+
expect(imageInput?.credits).toBe(1_548); // 5160 * 0.3 = 1548
|
|
240
|
+
|
|
241
|
+
const textOutput = result?.breakdown.find((item) => item.unit.name === 'textOutput');
|
|
242
|
+
expect(textOutput?.quantity).toBe(0);
|
|
243
|
+
expect(textOutput?.credits).toBe(0); // 0 * 2.5 = 0
|
|
244
|
+
|
|
245
|
+
const imageOutput = result?.breakdown.find((item) => item.unit.name === 'imageOutput');
|
|
246
|
+
expect(imageOutput?.quantity).toBe(1290);
|
|
247
|
+
expect(imageOutput?.credits).toBe(38_700); // 1290 * 30 = 38700
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('handles large context conversation with cache cross-tier pricing for Gemini 2.5 Pro', () => {
|
|
251
|
+
const pricing = googleChatModels.find(
|
|
252
|
+
(model: { id: string }) => model.id === 'gemini-2.5-pro',
|
|
253
|
+
)?.pricing;
|
|
254
|
+
expect(pricing).toBeDefined();
|
|
255
|
+
|
|
256
|
+
const usage: ModelTokensUsage = {
|
|
257
|
+
inputCachedTokens: 257_955,
|
|
258
|
+
inputCacheMissTokens: 5_005,
|
|
259
|
+
inputTextTokens: 262_960,
|
|
260
|
+
outputTextTokens: 1_744,
|
|
261
|
+
totalInputTokens: 262_960,
|
|
262
|
+
totalOutputTokens: 1_744,
|
|
263
|
+
totalTokens: 264_704,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = computeChatCost(pricing, usage);
|
|
267
|
+
expect(result).toBeDefined();
|
|
268
|
+
expect(result?.issues).toHaveLength(0);
|
|
269
|
+
|
|
270
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
271
|
+
expect(breakdown).toHaveLength(3); // Cache read, input, and output
|
|
272
|
+
|
|
273
|
+
// Verify cached tokens (cross-tier: over 200k threshold, use higher tier rate)
|
|
274
|
+
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
|
275
|
+
expect(cached?.quantity).toBe(257_955);
|
|
276
|
+
|
|
277
|
+
expect(cached?.credits).toBeCloseTo(161_221.875, 6);
|
|
278
|
+
expect(cached?.segments).toEqual([{ quantity: 257_955, rate: 0.625, credits: 161_221.875 }]);
|
|
279
|
+
|
|
280
|
+
// Verify input cache miss tokens (under 200k tier, use lower rate)
|
|
281
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
282
|
+
expect(input?.quantity).toBe(5_005);
|
|
283
|
+
expect(input?.credits).toBeCloseTo(6_256.25, 6);
|
|
284
|
+
expect(input?.segments).toEqual([{ quantity: 5_005, rate: 1.25, credits: 6_256.25 }]);
|
|
285
|
+
|
|
286
|
+
// Verify output tokens (under 200k threshold, use lower tier rate)
|
|
287
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
288
|
+
expect(output?.quantity).toBe(1_744);
|
|
289
|
+
expect(output?.credits).toBe(17_440); // 1744 * 10 = 17440
|
|
290
|
+
expect(output?.segments).toEqual([{ quantity: 1_744, rate: 10, credits: 17_440 }]);
|
|
291
|
+
|
|
292
|
+
// Verify totals match actual billing log
|
|
293
|
+
expect(totalCredits).toBe(184_919); // ceil(161221.875 + 6256.25 + 17440) = 184919
|
|
294
|
+
expect(totalCost).toBeCloseTo(0.184919, 6); // 184919 credits = $0.184919
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('Anthropic', () => {
|
|
299
|
+
it('handles lookup pricing with TTL for Claude Opus 4.1', () => {
|
|
300
|
+
const pricing = anthropicChatModels.find(
|
|
301
|
+
(model: { id: string }) => model.id === 'claude-opus-4-1-20250805',
|
|
302
|
+
)?.pricing;
|
|
303
|
+
expect(pricing).toBeDefined();
|
|
304
|
+
|
|
305
|
+
const usage: ModelTokensUsage = {
|
|
306
|
+
inputCacheMissTokens: 1_000,
|
|
307
|
+
inputCachedTokens: 200,
|
|
308
|
+
inputWriteCacheTokens: 300,
|
|
309
|
+
outputTextTokens: 500,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const result = computeChatCost(pricing, usage, { lookupParams: { ttl: '5m' } });
|
|
313
|
+
expect(result).toBeDefined();
|
|
314
|
+
expect(result?.issues).toHaveLength(0);
|
|
315
|
+
expect(result?.totalCredits).toBe(58_425);
|
|
316
|
+
expect(result?.totalCost).toBeCloseTo(0.058425, 10);
|
|
317
|
+
|
|
318
|
+
const cacheWrite = result?.breakdown.find(
|
|
319
|
+
(item) => item.unit.name === 'textInput_cacheWrite',
|
|
320
|
+
);
|
|
321
|
+
expect(cacheWrite?.lookupKey).toBe('5m');
|
|
322
|
+
expect(cacheWrite?.credits).toBe(5_625);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles simple request without thinking for Claude Sonnet 4', () => {
|
|
326
|
+
const pricing = anthropicChatModels.find(
|
|
327
|
+
(model: { id: string }) => model.id === 'claude-sonnet-4-20250514',
|
|
328
|
+
)?.pricing;
|
|
329
|
+
expect(pricing).toBeDefined();
|
|
330
|
+
|
|
331
|
+
const usage: ModelTokensUsage = {
|
|
332
|
+
inputCacheMissTokens: 8,
|
|
333
|
+
totalInputTokens: 8,
|
|
334
|
+
totalOutputTokens: 24,
|
|
335
|
+
totalTokens: 32,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const result = computeChatCost(pricing, usage);
|
|
339
|
+
expect(result).toBeDefined();
|
|
340
|
+
expect(result?.issues).toHaveLength(0);
|
|
341
|
+
|
|
342
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
343
|
+
expect(breakdown).toHaveLength(2); // Only input and output
|
|
344
|
+
|
|
345
|
+
// Verify input tokens
|
|
346
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
347
|
+
expect(input?.quantity).toBe(8);
|
|
348
|
+
expect(input?.credits).toBe(24); // 8 * 3 = 24
|
|
349
|
+
|
|
350
|
+
// Verify output tokens
|
|
351
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
352
|
+
expect(output?.quantity).toBe(24);
|
|
353
|
+
expect(output?.credits).toBe(360); // 24 * 15 = 360
|
|
354
|
+
|
|
355
|
+
// Verify totals match the actual billing log
|
|
356
|
+
expect(totalCredits).toBe(384); // 24 + 360 = 384
|
|
357
|
+
expect(totalCost).toBeCloseTo(0.000384, 6); // 384 credits = $0.000384
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('handles request with cache read and write for Claude Sonnet 4', () => {
|
|
361
|
+
const pricing = anthropicChatModels.find(
|
|
362
|
+
(model: { id: string }) => model.id === 'claude-sonnet-4-20250514',
|
|
363
|
+
)?.pricing;
|
|
364
|
+
expect(pricing).toBeDefined();
|
|
365
|
+
|
|
366
|
+
const usage: ModelTokensUsage = {
|
|
367
|
+
inputCacheMissTokens: 4,
|
|
368
|
+
inputCachedTokens: 1183,
|
|
369
|
+
inputWriteCacheTokens: 458,
|
|
370
|
+
totalInputTokens: 1645,
|
|
371
|
+
totalOutputTokens: 522,
|
|
372
|
+
totalTokens: 2167,
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const result = computeChatCost(pricing, usage, { lookupParams: { ttl: '5m' } });
|
|
376
|
+
expect(result).toBeDefined();
|
|
377
|
+
expect(result?.issues).toHaveLength(0);
|
|
378
|
+
|
|
379
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
380
|
+
expect(breakdown).toHaveLength(4); // Input, output, cache read, cache write
|
|
381
|
+
|
|
382
|
+
// Verify cache miss tokens (regular input)
|
|
383
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
384
|
+
expect(input?.quantity).toBe(4);
|
|
385
|
+
expect(input?.credits).toBe(12); // 4 * 3 = 12
|
|
386
|
+
|
|
387
|
+
// Verify output tokens
|
|
388
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
389
|
+
expect(output?.quantity).toBe(522);
|
|
390
|
+
expect(output?.credits).toBe(7_830); // 522 * 15 = 7830
|
|
391
|
+
|
|
392
|
+
// Verify cached tokens (discounted rate)
|
|
393
|
+
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
|
394
|
+
expect(cached?.quantity).toBe(1183);
|
|
395
|
+
expect(cached?.credits).toBeCloseTo(354.9, 6);
|
|
396
|
+
|
|
397
|
+
// Verify cache write tokens
|
|
398
|
+
const cacheWrite = breakdown.find((item) => item.unit.name === 'textInput_cacheWrite');
|
|
399
|
+
expect(cacheWrite?.quantity).toBe(458);
|
|
400
|
+
expect(cacheWrite?.lookupKey).toBe('5m');
|
|
401
|
+
expect(cacheWrite?.credits).toBeCloseTo(1_717.5, 6);
|
|
402
|
+
|
|
403
|
+
// Verify totals match the actual billing log
|
|
404
|
+
expect(totalCredits).toBe(9_915); // ceil(12 + 7830 + 354.9 + 1717.5) = 9915
|
|
405
|
+
expect(totalCost).toBeCloseTo(0.009915, 6); // 9915 credits = $0.009915
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('handles complex scenario with all cache types for Claude Sonnet 4 Latest', () => {
|
|
409
|
+
const pricing = anthropicChatModels.find(
|
|
410
|
+
(model: { id: string }) => model.id === 'claude-sonnet-4-20250514',
|
|
411
|
+
)?.pricing;
|
|
412
|
+
expect(pricing).toBeDefined();
|
|
413
|
+
|
|
414
|
+
const usage: ModelTokensUsage = {
|
|
415
|
+
inputCacheMissTokens: 10,
|
|
416
|
+
inputCachedTokens: 3021,
|
|
417
|
+
inputWriteCacheTokens: 1697,
|
|
418
|
+
totalInputTokens: 4728,
|
|
419
|
+
totalOutputTokens: 2841,
|
|
420
|
+
totalTokens: 7569,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = computeChatCost(pricing, usage, { lookupParams: { ttl: '5m' } });
|
|
424
|
+
expect(result).toBeDefined();
|
|
425
|
+
expect(result?.issues).toHaveLength(0);
|
|
426
|
+
|
|
427
|
+
const { breakdown, totalCost, totalCredits } = result!;
|
|
428
|
+
expect(breakdown).toHaveLength(4); // Input, output, cache read, cache write
|
|
429
|
+
|
|
430
|
+
// Verify cache miss tokens (regular input)
|
|
431
|
+
const input = breakdown.find((item) => item.unit.name === 'textInput');
|
|
432
|
+
expect(input?.quantity).toBe(10);
|
|
433
|
+
expect(input?.credits).toBe(30); // 10 * 3 = 30
|
|
434
|
+
|
|
435
|
+
// Verify output tokens
|
|
436
|
+
const output = breakdown.find((item) => item.unit.name === 'textOutput');
|
|
437
|
+
expect(output?.quantity).toBe(2841);
|
|
438
|
+
expect(output?.credits).toBe(42_615); // 2841 * 15 = 42615
|
|
439
|
+
|
|
440
|
+
// Verify cached tokens (discounted rate)
|
|
441
|
+
const cached = breakdown.find((item) => item.unit.name === 'textInput_cacheRead');
|
|
442
|
+
expect(cached?.quantity).toBe(3021);
|
|
443
|
+
expect(cached?.credits).toBeCloseTo(906.3, 6);
|
|
444
|
+
|
|
445
|
+
// Verify cache write tokens (fixed strategy in lobehub model)
|
|
446
|
+
const cacheWrite = breakdown.find((item) => item.unit.name === 'textInput_cacheWrite');
|
|
447
|
+
expect(cacheWrite?.quantity).toBe(1697);
|
|
448
|
+
expect(cacheWrite?.credits).toBeCloseTo(6_363.75, 6);
|
|
449
|
+
|
|
450
|
+
// Verify totals match the actual billing log
|
|
451
|
+
expect(totalCredits).toBe(49_916); // ceil(30 + 42615 + 906.3 + 6363.75) = 49916
|
|
452
|
+
expect(totalCost).toBeCloseTo(0.049916, 6); // 49916 credits = $0.049916
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
});
|