@lobehub/chat 1.133.1 → 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.
Files changed (130) hide show
  1. package/.cursor/rules/project-introduce.mdc +19 -25
  2. package/.cursor/rules/project-structure.mdc +102 -221
  3. package/.cursor/rules/{rules-attach.mdc → rules-index.mdc} +2 -11
  4. package/.cursor/rules/typescript.mdc +3 -53
  5. package/.vscode/settings.json +2 -1
  6. package/AGENTS.md +33 -54
  7. package/CHANGELOG.md +58 -0
  8. package/CLAUDE.md +1 -26
  9. package/changelog/v1.json +21 -0
  10. package/locales/ar/chat.json +5 -0
  11. package/locales/ar/image.json +7 -0
  12. package/locales/ar/models.json +2 -2
  13. package/locales/bg-BG/chat.json +5 -0
  14. package/locales/bg-BG/image.json +7 -0
  15. package/locales/de-DE/chat.json +5 -0
  16. package/locales/de-DE/image.json +7 -0
  17. package/locales/en-US/chat.json +5 -0
  18. package/locales/en-US/image.json +7 -0
  19. package/locales/es-ES/chat.json +5 -0
  20. package/locales/es-ES/image.json +7 -0
  21. package/locales/es-ES/tool.json +1 -1
  22. package/locales/fa-IR/chat.json +5 -0
  23. package/locales/fa-IR/image.json +7 -0
  24. package/locales/fa-IR/models.json +2 -2
  25. package/locales/fr-FR/chat.json +5 -0
  26. package/locales/fr-FR/image.json +7 -0
  27. package/locales/fr-FR/models.json +2 -2
  28. package/locales/it-IT/chat.json +5 -0
  29. package/locales/it-IT/image.json +7 -0
  30. package/locales/ja-JP/chat.json +5 -0
  31. package/locales/ja-JP/image.json +7 -0
  32. package/locales/ko-KR/chat.json +5 -0
  33. package/locales/ko-KR/image.json +7 -0
  34. package/locales/nl-NL/chat.json +5 -0
  35. package/locales/nl-NL/image.json +7 -0
  36. package/locales/pl-PL/chat.json +5 -0
  37. package/locales/pl-PL/image.json +7 -0
  38. package/locales/pt-BR/chat.json +5 -0
  39. package/locales/pt-BR/image.json +7 -0
  40. package/locales/ru-RU/chat.json +5 -0
  41. package/locales/ru-RU/image.json +7 -0
  42. package/locales/ru-RU/tool.json +1 -1
  43. package/locales/tr-TR/chat.json +5 -0
  44. package/locales/tr-TR/image.json +7 -0
  45. package/locales/tr-TR/models.json +2 -2
  46. package/locales/vi-VN/chat.json +5 -0
  47. package/locales/vi-VN/image.json +7 -0
  48. package/locales/zh-CN/chat.json +5 -0
  49. package/locales/zh-CN/image.json +7 -0
  50. package/locales/zh-TW/chat.json +5 -0
  51. package/locales/zh-TW/image.json +7 -0
  52. package/package.json +4 -5
  53. package/packages/const/package.json +4 -0
  54. package/packages/const/src/currency.ts +2 -0
  55. package/packages/const/src/index.ts +1 -0
  56. package/packages/model-bank/package.json +2 -1
  57. package/packages/model-bank/src/aiModels/google.ts +6 -0
  58. package/packages/model-bank/src/aiModels/openai.ts +6 -22
  59. package/packages/model-bank/src/standard-parameters/index.ts +56 -46
  60. package/packages/model-runtime/package.json +1 -0
  61. package/packages/model-runtime/src/core/RouterRuntime/createRuntime.ts +4 -2
  62. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +12 -2
  63. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +16 -5
  64. package/packages/model-runtime/src/core/streams/anthropic.ts +25 -36
  65. package/packages/model-runtime/src/core/streams/google/google-ai.test.ts +1 -1
  66. package/packages/model-runtime/src/core/streams/google/index.ts +18 -42
  67. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +7 -10
  68. package/packages/model-runtime/src/core/streams/openai/openai.ts +14 -11
  69. package/packages/model-runtime/src/core/streams/openai/responsesStream.ts +11 -5
  70. package/packages/model-runtime/src/core/streams/protocol.ts +25 -6
  71. package/packages/model-runtime/src/core/streams/qwen.ts +2 -2
  72. package/packages/model-runtime/src/core/streams/spark.ts +3 -3
  73. package/packages/model-runtime/src/core/streams/vertex-ai.test.ts +2 -2
  74. package/packages/model-runtime/src/core/streams/vertex-ai.ts +14 -23
  75. package/packages/model-runtime/src/core/usageConverters/anthropic.test.ts +99 -0
  76. package/packages/model-runtime/src/core/usageConverters/anthropic.ts +73 -0
  77. package/packages/model-runtime/src/core/usageConverters/google-ai.test.ts +88 -0
  78. package/packages/model-runtime/src/core/usageConverters/google-ai.ts +55 -0
  79. package/packages/model-runtime/src/core/usageConverters/index.ts +4 -0
  80. package/packages/model-runtime/src/core/usageConverters/openai.test.ts +429 -0
  81. package/packages/model-runtime/src/core/usageConverters/openai.ts +152 -0
  82. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.test.ts +455 -0
  83. package/packages/model-runtime/src/core/usageConverters/utils/computeChatCost.ts +293 -0
  84. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.test.ts +47 -0
  85. package/packages/model-runtime/src/core/usageConverters/utils/computeImageCost.ts +121 -0
  86. package/packages/model-runtime/src/core/usageConverters/utils/index.ts +11 -0
  87. package/packages/model-runtime/src/core/usageConverters/utils/withUsageCost.ts +19 -0
  88. package/packages/model-runtime/src/index.ts +2 -0
  89. package/packages/model-runtime/src/providers/anthropic/index.ts +48 -1
  90. package/packages/model-runtime/src/providers/google/createImage.ts +11 -2
  91. package/packages/model-runtime/src/providers/google/index.ts +8 -1
  92. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +7 -0
  93. package/packages/model-runtime/src/providers/zhipu/index.ts +3 -1
  94. package/packages/model-runtime/src/types/chat.ts +5 -3
  95. package/packages/model-runtime/src/types/image.ts +20 -9
  96. package/packages/model-runtime/src/utils/getModelPricing.ts +36 -0
  97. package/packages/obervability-otel/package.json +2 -2
  98. package/packages/ssrf-safe-fetch/index.test.ts +343 -0
  99. package/packages/ssrf-safe-fetch/index.ts +37 -0
  100. package/packages/ssrf-safe-fetch/package.json +17 -0
  101. package/packages/ssrf-safe-fetch/vitest.config.mts +10 -0
  102. package/packages/types/src/message/base.ts +43 -17
  103. package/packages/utils/src/client/apiKeyManager.test.ts +70 -0
  104. package/packages/utils/src/client/apiKeyManager.ts +41 -0
  105. package/packages/utils/src/client/index.ts +2 -0
  106. package/packages/utils/src/fetch/fetchSSE.ts +4 -4
  107. package/packages/utils/src/index.ts +1 -0
  108. package/packages/utils/src/toolManifest.ts +2 -1
  109. package/src/app/(backend)/webapi/proxy/route.ts +2 -13
  110. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/default.tsx +2 -0
  111. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatMinimap/index.tsx +335 -0
  112. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/TopicPanel.tsx +4 -0
  113. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/QualitySelect.tsx +23 -0
  114. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  115. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +13 -13
  116. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +1 -1
  117. package/src/features/Conversation/components/ChatItem/index.tsx +56 -2
  118. package/src/features/Conversation/components/VirtualizedList/VirtuosoContext.ts +88 -0
  119. package/src/features/Conversation/components/VirtualizedList/index.tsx +15 -1
  120. package/src/locales/default/chat.ts +5 -0
  121. package/src/locales/default/image.ts +7 -0
  122. package/src/server/modules/EdgeConfig/index.ts +1 -1
  123. package/src/server/routers/async/image.ts +9 -1
  124. package/src/services/_auth.ts +12 -12
  125. package/src/services/chat/contextEngineering.ts +2 -3
  126. package/.cursor/rules/backend-architecture.mdc +0 -176
  127. package/.cursor/rules/code-review.mdc +0 -58
  128. package/.cursor/rules/cursor-ux.mdc +0 -32
  129. package/.cursor/rules/define-database-model.mdc +0 -8
  130. package/.cursor/rules/system-role.mdc +0 -31
@@ -0,0 +1,293 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { CREDITS_PER_DOLLAR } from '@lobechat/const/currency';
3
+ import debug from 'debug';
4
+ import {
5
+ FixedPricingUnit,
6
+ LookupPricingUnit,
7
+ Pricing,
8
+ PricingUnit,
9
+ PricingUnitName,
10
+ TieredPricingUnit,
11
+ } from 'model-bank';
12
+
13
+ import { ModelTokensUsage } from '@/types/message';
14
+
15
+ const log = debug('lobe-cost:computeChatPricing');
16
+
17
+ export interface PricingUnitBreakdown {
18
+ cost: number;
19
+ credits: number;
20
+ /**
21
+ * For lookup strategies we expose the resolved key.
22
+ */
23
+ lookupKey?: string;
24
+ quantity: number;
25
+ /**
26
+ * Extra details for tiered strategies to help consumers render ladders.
27
+ */
28
+ segments?: Array<{ credits: number; quantity: number; rate: number }>;
29
+ unit: PricingUnit;
30
+ }
31
+
32
+ export interface PricingComputationIssue {
33
+ reason: string;
34
+ unit: PricingUnit;
35
+ }
36
+
37
+ export interface ComputeChatCostOptions {
38
+ /**
39
+ * Input parameters used by lookup strategies (e.g. ttl, thinkingMode).
40
+ */
41
+ lookupParams?: Record<string, string | number | boolean>;
42
+ }
43
+
44
+ export interface PricingComputationResult {
45
+ breakdown: PricingUnitBreakdown[];
46
+ issues: PricingComputationIssue[];
47
+ totalCost: number;
48
+ totalCredits: number;
49
+ }
50
+
51
+ type UnitQuantityResolver = (usage: ModelTokensUsage) => number | undefined;
52
+
53
+ const UNIT_QUANTITY_RESOLVERS: Partial<Record<PricingUnitName, UnitQuantityResolver>> = {
54
+ textInput: (usage) => {
55
+ if (usage.inputCacheMissTokens !== undefined) {
56
+ return usage.inputCacheMissTokens;
57
+ }
58
+
59
+ if (typeof usage.inputCachedTokens === 'number' && typeof usage.totalInputTokens === 'number') {
60
+ throw new Error(
61
+ 'Missing inputCacheMissTokens! You can set it by inputCacheMissTokens = totalInputTokens - inputCachedTokens',
62
+ );
63
+ }
64
+
65
+ return usage.inputTextTokens ?? usage.totalInputTokens;
66
+ },
67
+ textInput_cacheRead: (usage) => usage.inputCachedTokens,
68
+ textInput_cacheWrite: (usage) => usage.inputWriteCacheTokens,
69
+ // reasoning tokens cost within output tokens
70
+ textOutput: (usage) => {
71
+ const { outputTextTokens, totalOutputTokens, outputReasoningTokens = 0 } = usage;
72
+ const reasoningTokens = outputReasoningTokens;
73
+
74
+ if (typeof outputTextTokens === 'number') {
75
+ return outputTextTokens + reasoningTokens;
76
+ }
77
+
78
+ if (typeof totalOutputTokens === 'number') {
79
+ return totalOutputTokens;
80
+ }
81
+
82
+ if (typeof usage.outputReasoningTokens === 'number') {
83
+ return usage.outputReasoningTokens;
84
+ }
85
+
86
+ return undefined;
87
+ },
88
+
89
+ imageInput: (usage) => usage.inputImageTokens,
90
+ imageInput_cacheRead: () => undefined,
91
+ imageOutput: (usage) => usage.outputImageTokens,
92
+
93
+ imageGeneration: () => undefined,
94
+
95
+ audioInput: (usage) => usage.inputAudioTokens,
96
+ // TODO: Support this when ModelTokensUsage includes this data
97
+ audioInput_cacheRead: () => undefined,
98
+ audioOutput: (usage) => usage.outputAudioTokens,
99
+ };
100
+
101
+ const creditsToUSD = (credits: number) => credits / CREDITS_PER_DOLLAR;
102
+
103
+ /**
104
+ * Returns raw credits, which will be rounded up uniformly at the final aggregation stage.
105
+ */
106
+ const computeFixedCredits = (unit: FixedPricingUnit, quantity: number) => quantity * unit.rate;
107
+
108
+ /**
109
+ * Google provider uses new pricing for entire input and output when exceeding threshold, not tiered calculation
110
+ * TODO: Some providers do use tiered calculation, such as Zhipu
111
+ */
112
+ const computeTieredCredits = (
113
+ unit: TieredPricingUnit,
114
+ quantity: number,
115
+ ): { credits: number; segments: Array<{ credits: number; quantity: number; rate: number }> } => {
116
+ if (quantity <= 0) return { credits: 0, segments: [] };
117
+
118
+ const segments: Array<{ credits: number; quantity: number; rate: number }> = [];
119
+ const tiers = unit.tiers ?? [];
120
+ if (tiers.length === 0) return { credits: 0, segments };
121
+
122
+ // Google and other providers charge the entire quantity at the new rate when exceeding threshold
123
+ const matchedTier =
124
+ tiers.find((tier) => {
125
+ const limit = tier.upTo === 'infinity' ? Number.POSITIVE_INFINITY : tier.upTo;
126
+ return quantity <= limit;
127
+ }) ?? tiers.at(-1);
128
+
129
+ if (!matchedTier) return { credits: 0, segments };
130
+
131
+ const credits = quantity * matchedTier.rate;
132
+ segments.push({ credits, quantity, rate: matchedTier.rate });
133
+
134
+ return { credits, segments };
135
+ };
136
+
137
+ const resolveLookupKey = (
138
+ unit: LookupPricingUnit,
139
+ options: ComputeChatCostOptions | undefined,
140
+ ): { key?: string; missingParams?: string[] } => {
141
+ if (!unit.lookup?.pricingParams?.length) return { key: undefined };
142
+
143
+ const missingParams: string[] = [];
144
+ const params = unit.lookup.pricingParams.map((param) => {
145
+ const source = options?.lookupParams?.[param];
146
+ if (source === undefined || source === null) {
147
+ missingParams.push(param);
148
+ return 'undefined';
149
+ }
150
+
151
+ if (typeof source === 'boolean') return String(source);
152
+ return String(source);
153
+ });
154
+
155
+ if (missingParams.length > 0) return { key: undefined, missingParams };
156
+
157
+ return { key: params.join('_') };
158
+ };
159
+
160
+ const computeLookupCredits = (
161
+ unit: LookupPricingUnit,
162
+ quantity: number,
163
+ options: ComputeChatCostOptions | undefined,
164
+ ): { credits: number; issues?: PricingComputationIssue; key?: string } => {
165
+ const { key, missingParams } = resolveLookupKey(unit, options);
166
+
167
+ if (missingParams && missingParams.length > 0) {
168
+ return {
169
+ credits: 0,
170
+ issues: {
171
+ reason: `Missing lookup params: ${missingParams.join(', ')}`,
172
+ unit,
173
+ },
174
+ };
175
+ }
176
+
177
+ if (!key) {
178
+ return {
179
+ credits: 0,
180
+ issues: {
181
+ reason: 'Lookup key could not be resolved',
182
+ unit,
183
+ },
184
+ };
185
+ }
186
+
187
+ const lookupRate = unit.lookup.prices?.[key];
188
+ if (typeof lookupRate !== 'number') {
189
+ return {
190
+ credits: 0,
191
+ issues: {
192
+ reason: `Lookup price not found for key "${key}"`,
193
+ unit,
194
+ },
195
+ key,
196
+ };
197
+ }
198
+
199
+ return {
200
+ credits: quantity * lookupRate,
201
+ key,
202
+ };
203
+ };
204
+
205
+ const resolveQuantity = (unit: PricingUnit, usage: ModelTokensUsage) => {
206
+ const resolver = UNIT_QUANTITY_RESOLVERS[unit.name as PricingUnitName];
207
+ const quantity = resolver?.(usage);
208
+ return typeof quantity === 'number' ? quantity : undefined;
209
+ };
210
+
211
+ /**
212
+ * 1. Keep raw credits for each item (may be decimal)
213
+ * 2. Round up uniformly at the totals stage to prevent cost undercounting
214
+ */
215
+ export const computeChatCost = (
216
+ pricing: Pricing | undefined,
217
+ usage: ModelTokensUsage,
218
+ options?: ComputeChatCostOptions,
219
+ ): PricingComputationResult | undefined => {
220
+ if (!pricing) return undefined;
221
+
222
+ const breakdown: PricingUnitBreakdown[] = [];
223
+ const issues: PricingComputationIssue[] = [];
224
+
225
+ for (const unit of pricing.units) {
226
+ const quantity = resolveQuantity(unit, usage);
227
+ if (quantity === undefined) continue;
228
+
229
+ if (unit.strategy === 'fixed') {
230
+ if (unit.unit !== 'millionTokens')
231
+ throw new Error(`Unsupported chat pricing unit: ${unit.unit}`);
232
+
233
+ const fixedUnit = unit as FixedPricingUnit;
234
+ const credits = computeFixedCredits(fixedUnit, quantity);
235
+ breakdown.push({
236
+ cost: creditsToUSD(credits),
237
+ credits,
238
+ quantity,
239
+ unit,
240
+ });
241
+ continue;
242
+ }
243
+
244
+ if (unit.strategy === 'tiered') {
245
+ const tieredUnit = unit as TieredPricingUnit;
246
+ const { credits, segments } = computeTieredCredits(tieredUnit, quantity);
247
+ breakdown.push({
248
+ cost: creditsToUSD(credits),
249
+ credits,
250
+ quantity,
251
+ segments,
252
+ unit,
253
+ });
254
+ continue;
255
+ }
256
+
257
+ if (unit.strategy === 'lookup') {
258
+ const lookupUnit = unit as LookupPricingUnit;
259
+ const {
260
+ credits,
261
+ key,
262
+ issues: lookupIssue,
263
+ } = computeLookupCredits(lookupUnit, quantity, options);
264
+
265
+ if (lookupIssue) issues.push(lookupIssue);
266
+
267
+ breakdown.push({
268
+ cost: creditsToUSD(credits),
269
+ credits,
270
+ lookupKey: key,
271
+ quantity,
272
+ unit,
273
+ });
274
+ continue;
275
+ }
276
+
277
+ issues.push({ reason: 'Unsupported pricing strategy', unit });
278
+ }
279
+
280
+ const rawTotalCredits = breakdown.reduce((sum, item) => sum + item.credits, 0);
281
+ const totalCredits = Math.ceil(rawTotalCredits);
282
+ // !: totalCredits has been uniformly rounded up to integer credits, divided by CREDITS_PER_DOLLAR naturally retains only 6 decimal places, no additional processing needed
283
+ const totalCost = creditsToUSD(totalCredits);
284
+
285
+ log(`computeChatPricing breakdown: ${JSON.stringify(breakdown, null, 2)}`);
286
+
287
+ return {
288
+ breakdown,
289
+ issues,
290
+ totalCost,
291
+ totalCredits,
292
+ };
293
+ };
@@ -0,0 +1,47 @@
1
+ import type { Pricing } from 'model-bank';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { ImageGenerationParams, computeImageCost } from './computeImageCost';
5
+
6
+ describe('computeImageCost', () => {
7
+ it('should compute dall-e-3 lookup pricing correctly', () => {
8
+ // Arrange - Based on actual production logs
9
+ const pricing: Pricing = {
10
+ units: [
11
+ {
12
+ name: 'imageGeneration',
13
+ strategy: 'lookup',
14
+ unit: 'image',
15
+ lookup: {
16
+ pricingParams: ['quality', 'size'],
17
+ prices: {
18
+ standard_1024x1024: 0.04,
19
+ standard_1024x1792: 0.08,
20
+ standard_1792x1024: 0.08,
21
+ hd_1024x1024: 0.08,
22
+ hd_1024x1792: 0.12,
23
+ hd_1792x1024: 0.12,
24
+ },
25
+ },
26
+ },
27
+ ],
28
+ };
29
+
30
+ const params: ImageGenerationParams = {
31
+ quality: 'standard',
32
+ size: '1024x1024',
33
+ prompt: '一条边牧',
34
+ };
35
+
36
+ // Act
37
+ const result = computeImageCost(pricing, params, 1);
38
+
39
+ // Assert - Match the production log output
40
+ expect(result).toBeDefined();
41
+ expect(result?.totalCost).toBe(0.04);
42
+ expect(result?.totalCredits).toBe(40000); // $0.04 * 100000 credits per dollar
43
+ expect(result?.breakdown?.lookupKey).toBe('standard_1024x1024');
44
+ expect(result?.breakdown?.pricePerImage).toBe(0.04);
45
+ expect(result?.breakdown?.imageCount).toBe(1);
46
+ });
47
+ });
@@ -0,0 +1,121 @@
1
+ import { CREDITS_PER_DOLLAR } from '@lobechat/const/currency';
2
+ import debug from 'debug';
3
+ import { FixedPricingUnit, LookupPricingUnit, Pricing } from 'model-bank';
4
+
5
+ const log = debug('lobe-cost:computeImagePricing');
6
+
7
+ export interface ImageGenerationParams {
8
+ // Other possible parameters for future extensions
9
+ [key: string]: any;
10
+ quality?: 'standard' | 'hd';
11
+ size?: string;
12
+ }
13
+
14
+ export interface ImageCostResult {
15
+ breakdown?: {
16
+ imageCount: number;
17
+ lookupKey?: string;
18
+ pricePerImage: number; // Price per image in USD
19
+ };
20
+ totalCost: number; // Total cost in USD
21
+ totalCredits: number; // Total credits (USD * CREDITS_PER_DOLLAR)
22
+ }
23
+
24
+ /**
25
+ * Compute the cost for image generation based on pricing configuration
26
+ * @param pricing - The pricing configuration for the model
27
+ * @param params - Image generation parameters (quality, size, etc.)
28
+ * @param imageNum - Number of images to generate
29
+ * @returns ImageCostResult with total cost in USD and credits, or undefined if pricing not found
30
+ */
31
+ export const computeImageCost = (
32
+ pricing: Pricing,
33
+ params: ImageGenerationParams,
34
+ imageNum: number,
35
+ ): ImageCostResult | undefined => {
36
+ // Find imageGeneration pricing unit
37
+ const imageGenUnit = pricing.units.find((unit) => unit.name === 'imageGeneration');
38
+ if (!imageGenUnit) {
39
+ log('No imageGeneration unit found in pricing configuration');
40
+ return undefined;
41
+ }
42
+
43
+ let pricePerImageInUSD = 0;
44
+ let lookupKey: string | undefined;
45
+
46
+ switch (imageGenUnit.strategy) {
47
+ case 'fixed': {
48
+ const fixedUnit = imageGenUnit as FixedPricingUnit;
49
+ if (fixedUnit.unit !== 'image') {
50
+ log(`Unsupported unit type for fixed pricing: ${fixedUnit.unit}`);
51
+ return undefined;
52
+ }
53
+ pricePerImageInUSD = fixedUnit.rate;
54
+ log(`Fixed pricing: $${pricePerImageInUSD} per image`);
55
+
56
+ break;
57
+ }
58
+ case 'lookup': {
59
+ const lookupUnit = imageGenUnit as LookupPricingUnit;
60
+
61
+ // Build lookup key from params
62
+ const lookupParams: string[] = [];
63
+
64
+ // Check required pricing params
65
+ if (lookupUnit.lookup?.pricingParams) {
66
+ for (const paramName of lookupUnit.lookup.pricingParams) {
67
+ const paramValue = params[paramName];
68
+ if (paramValue === undefined || paramValue === null) {
69
+ log(`Missing required lookup param: ${paramName}`);
70
+ return undefined;
71
+ }
72
+ lookupParams.push(String(paramValue));
73
+ }
74
+ lookupKey = lookupParams.join('_');
75
+ } else {
76
+ log('No pricing params defined for lookup strategy');
77
+ return undefined;
78
+ }
79
+
80
+ // Find price for the lookup key
81
+ const lookupPrice = lookupUnit.lookup?.prices?.[lookupKey];
82
+ if (typeof lookupPrice !== 'number') {
83
+ log(`No price found for lookup key: ${lookupKey}`);
84
+ return undefined;
85
+ }
86
+
87
+ pricePerImageInUSD = lookupPrice;
88
+ log(`Lookup pricing for key "${lookupKey}": $${pricePerImageInUSD} per image`);
89
+
90
+ break;
91
+ }
92
+ case 'tiered': {
93
+ // TODO: Implement tiered pricing when needed
94
+ log('Tiered pricing strategy not yet implemented for image generation');
95
+ return undefined;
96
+ }
97
+ default: {
98
+ // @ts-expect-error - PricingUnit strategy may have unsupported values
99
+ log(`Unsupported pricing strategy: ${imageGenUnit.strategy}`);
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ // Calculate total cost in USD first, then convert to credits
105
+ const totalCost = pricePerImageInUSD * imageNum;
106
+ const totalCredits = Math.ceil(totalCost * CREDITS_PER_DOLLAR);
107
+
108
+ log(
109
+ `Image cost calculation: ${imageNum} images × $${pricePerImageInUSD} = $${totalCost} (${totalCredits} credits)`,
110
+ );
111
+
112
+ return {
113
+ breakdown: {
114
+ imageCount: imageNum,
115
+ lookupKey,
116
+ pricePerImage: pricePerImageInUSD,
117
+ },
118
+ totalCost,
119
+ totalCredits,
120
+ };
121
+ };
@@ -0,0 +1,11 @@
1
+ export {
2
+ computeChatCost,
3
+ type ComputeChatCostOptions,
4
+ type PricingComputationResult,
5
+ } from './computeChatCost';
6
+ export {
7
+ computeImageCost,
8
+ type ImageCostResult,
9
+ type ImageGenerationParams,
10
+ } from './computeImageCost';
11
+ export { withUsageCost } from './withUsageCost';
@@ -0,0 +1,19 @@
1
+ import type { Pricing } from 'model-bank';
2
+
3
+ import type { ModelUsage } from '@/types/message';
4
+
5
+ import { computeChatCost } from './computeChatCost';
6
+ import type { ComputeChatCostOptions } from './computeChatCost';
7
+
8
+ export const withUsageCost = (
9
+ usage: ModelUsage,
10
+ pricing?: Pricing,
11
+ options?: ComputeChatCostOptions,
12
+ ): ModelUsage => {
13
+ if (!pricing) return usage;
14
+
15
+ const pricingResult = computeChatCost(pricing, usage, options);
16
+ if (!pricingResult) return usage;
17
+
18
+ return { ...usage, cost: pricingResult.totalCost };
19
+ };
@@ -2,6 +2,7 @@ export * from './core/BaseAI';
2
2
  export { ModelRuntime } from './core/ModelRuntime';
3
3
  export { createOpenAICompatibleRuntime } from './core/openaiCompatibleFactory';
4
4
  export * from './core/RouterRuntime';
5
+ export * from './core/usageConverters';
5
6
  export * from './helpers';
6
7
  export { LobeAkashChatAI } from './providers/akashchat';
7
8
  export { LobeAnthropicAI } from './providers/anthropic';
@@ -33,5 +34,6 @@ export * from './types';
33
34
  export * from './types/error';
34
35
  export { AgentRuntimeError } from './utils/createError';
35
36
  export { getModelPropertyWithFallback } from './utils/getFallbackModelProperty';
37
+ export { getModelPricing } from './utils/getModelPricing';
36
38
  export { pruneReasoningPayload } from './utils/openaiHelpers';
37
39
  export { parseDataUri } from './utils/uriParser';
@@ -15,6 +15,7 @@ import { buildAnthropicMessages, buildAnthropicTools } from '../../utils/anthrop
15
15
  import { AgentRuntimeError } from '../../utils/createError';
16
16
  import { debugStream } from '../../utils/debugStream';
17
17
  import { desensitizeUrl } from '../../utils/desensitizeUrl';
18
+ import { getModelPricing } from '../../utils/getModelPricing';
18
19
  import { MODEL_LIST_CONFIGS, processModelList } from '../../utils/modelParse';
19
20
  import { StreamingResponse } from '../../utils/response';
20
21
  import { createAnthropicGenerateObject } from './generateObject';
@@ -38,6 +39,44 @@ const modelsWithTempAndTopPConflict = new Set([
38
39
  ]);
39
40
 
40
41
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
42
+ const DEFAULT_CACHE_TTL = '5m' as const;
43
+
44
+ type CacheTTL = Anthropic.Messages.CacheControlEphemeral['ttl'];
45
+
46
+ /**
47
+ * Resolves cache TTL from Anthropic payload or request settings
48
+ * Returns the first valid TTL found in system messages or content blocks
49
+ */
50
+ const resolveCacheTTL = (
51
+ requestPayload: ChatStreamPayload,
52
+ anthropicPayload: Anthropic.MessageCreateParams,
53
+ ): CacheTTL | undefined => {
54
+ // Check system messages for cache TTL
55
+ if (Array.isArray(anthropicPayload.system)) {
56
+ for (const block of anthropicPayload.system) {
57
+ const ttl = block.cache_control?.ttl;
58
+ if (ttl) return ttl;
59
+ }
60
+ }
61
+
62
+ // Check message content blocks for cache TTL
63
+ for (const message of anthropicPayload.messages ?? []) {
64
+ if (!Array.isArray(message.content)) continue;
65
+
66
+ for (const block of message.content) {
67
+ // Message content blocks might have cache_control property
68
+ const ttl = ('cache_control' in block && block.cache_control?.ttl) as CacheTTL | undefined;
69
+ if (ttl) return ttl;
70
+ }
71
+ }
72
+
73
+ // Use default TTL if context caching is enabled
74
+ if (requestPayload.enabledContextCaching) {
75
+ return DEFAULT_CACHE_TTL;
76
+ }
77
+
78
+ return undefined;
79
+ };
41
80
 
42
81
  interface AnthropicAIParams extends ClientOptions {
43
82
  id?: string;
@@ -103,8 +142,16 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
103
142
  debugStream(debug.toReadableStream()).catch(console.error);
104
143
  }
105
144
 
145
+ const pricing = await getModelPricing(payload.model, this.id);
146
+ const cacheTTL = resolveCacheTTL(payload, anthropicPayload);
147
+ const pricingOptions = cacheTTL ? { lookupParams: { ttl: cacheTTL } } : undefined;
148
+
106
149
  return StreamingResponse(
107
- AnthropicStream(prod, { callbacks: options?.callback, inputStartAt }),
150
+ AnthropicStream(prod, {
151
+ callbacks: options?.callback,
152
+ inputStartAt,
153
+ payload: { model: payload.model, pricing, pricingOptions, provider: this.id },
154
+ }),
108
155
  {
109
156
  headers: options?.headers,
110
157
  },
@@ -1,7 +1,9 @@
1
1
  import { Content, GoogleGenAI, Part } from '@google/genai';
2
2
 
3
+ import { convertGoogleAIUsage } from '../../core/usageConverters/google-ai';
3
4
  import { CreateImagePayload, CreateImageResponse } from '../../types/image';
4
5
  import { AgentRuntimeError } from '../../utils/createError';
6
+ import { getModelPricing } from '../../utils/getModelPricing';
5
7
  import { parseGoogleErrorMessage } from '../../utils/googleErrorParser';
6
8
  import { imageUrlToBase64 } from '../../utils/imageToBase64';
7
9
  import { parseDataUri } from '../../utils/uriParser';
@@ -101,6 +103,7 @@ async function generateByImageModel(
101
103
  async function generateImageByChatModel(
102
104
  client: GoogleGenAI,
103
105
  payload: CreateImagePayload,
106
+ provider: string,
104
107
  ): Promise<CreateImageResponse> {
105
108
  const { model, params } = payload;
106
109
  const actualModel = model.replace(':image', '');
@@ -146,7 +149,13 @@ async function generateImageByChatModel(
146
149
  model: actualModel,
147
150
  });
148
151
 
149
- return extractImageFromResponse(response);
152
+ const imageResponse = extractImageFromResponse(response);
153
+ if (response.usageMetadata) {
154
+ const pricing = await getModelPricing(model, provider);
155
+ imageResponse.modelUsage = convertGoogleAIUsage(response.usageMetadata, pricing);
156
+ }
157
+
158
+ return imageResponse;
150
159
  }
151
160
 
152
161
  /**
@@ -162,7 +171,7 @@ export async function createGoogleImage(
162
171
 
163
172
  // Handle Gemini 2.5 Flash Image models that use generateContent
164
173
  if (model.endsWith(':image')) {
165
- return await generateImageByChatModel(client, payload);
174
+ return await generateImageByChatModel(client, payload, provider);
166
175
  }
167
176
 
168
177
  // Handle traditional Imagen models that use generateImages
@@ -27,6 +27,7 @@ import { AgentRuntimeErrorType } from '../../types/error';
27
27
  import { CreateImagePayload, CreateImageResponse } from '../../types/image';
28
28
  import { AgentRuntimeError } from '../../utils/createError';
29
29
  import { debugStream } from '../../utils/debugStream';
30
+ import { getModelPricing } from '../../utils/getModelPricing';
30
31
  import { parseGoogleErrorMessage } from '../../utils/googleErrorParser';
31
32
  import { imageUrlToBase64 } from '../../utils/imageToBase64';
32
33
  import { StreamingResponse } from '../../utils/response';
@@ -244,8 +245,14 @@ export class LobeGoogleAI implements LobeRuntimeAI {
244
245
  }
245
246
 
246
247
  // Convert the response into a friendly text-stream
248
+ const pricing = await getModelPricing(model, this.provider);
249
+
247
250
  const Stream = this.isVertexAi ? VertexAIStream : GoogleGenerativeAIStream;
248
- const stream = Stream(prod, { callbacks: options?.callback, inputStartAt });
251
+ const stream = Stream(prod, {
252
+ callbacks: options?.callback,
253
+ inputStartAt,
254
+ payload: { model, pricing, provider: this.provider },
255
+ });
249
256
 
250
257
  // Respond with the stream
251
258
  return StreamingResponse(stream, { headers: options?.headers });
@@ -268,6 +268,13 @@ exports[`LobeOpenAI > models > should get models 1`] = `
268
268
  "prompt": {
269
269
  "default": "",
270
270
  },
271
+ "quality": {
272
+ "default": "standard",
273
+ "enum": [
274
+ "standard",
275
+ "hd",
276
+ ],
277
+ },
271
278
  "size": {
272
279
  "default": "1024x1024",
273
280
  "enum": [
@@ -108,7 +108,9 @@ export const LobeZhipuAI = createOpenAICompatibleRuntime({
108
108
  return OpenAIStream(preprocessedStream, {
109
109
  callbacks,
110
110
  inputStartAt,
111
- provider: 'zhipu',
111
+ payload: {
112
+ provider: 'zhipu',
113
+ },
112
114
  });
113
115
  },
114
116
  },