@lobehub/chat 1.116.4 → 1.117.0

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 (52) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/changelog/v1.json +12 -0
  3. package/locales/ar/models.json +3 -0
  4. package/locales/bg-BG/models.json +3 -0
  5. package/locales/de-DE/models.json +3 -0
  6. package/locales/en-US/models.json +3 -0
  7. package/locales/es-ES/models.json +3 -0
  8. package/locales/fa-IR/models.json +3 -0
  9. package/locales/fr-FR/models.json +3 -0
  10. package/locales/it-IT/models.json +3 -0
  11. package/locales/ja-JP/models.json +3 -0
  12. package/locales/ko-KR/models.json +3 -0
  13. package/locales/nl-NL/models.json +3 -0
  14. package/locales/pl-PL/models.json +3 -0
  15. package/locales/pt-BR/models.json +3 -0
  16. package/locales/ru-RU/models.json +3 -0
  17. package/locales/tr-TR/models.json +3 -0
  18. package/locales/vi-VN/models.json +3 -0
  19. package/locales/zh-CN/models.json +3 -0
  20. package/locales/zh-TW/models.json +3 -0
  21. package/package.json +1 -1
  22. package/packages/const/src/image.ts +9 -0
  23. package/packages/database/vitest.config.mts +1 -0
  24. package/packages/database/vitest.config.server.mts +1 -0
  25. package/packages/file-loaders/package.json +1 -1
  26. package/packages/model-runtime/src/RouterRuntime/createRuntime.ts +11 -9
  27. package/packages/model-runtime/src/google/createImage.test.ts +657 -0
  28. package/packages/model-runtime/src/google/createImage.ts +152 -0
  29. package/packages/model-runtime/src/google/index.test.ts +0 -328
  30. package/packages/model-runtime/src/google/index.ts +3 -40
  31. package/packages/model-runtime/src/utils/modelParse.ts +2 -1
  32. package/packages/model-runtime/src/utils/openaiCompatibleFactory/createImage.ts +239 -0
  33. package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +22 -22
  34. package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.ts +9 -116
  35. package/packages/model-runtime/src/utils/postProcessModelList.ts +55 -0
  36. package/packages/model-runtime/src/utils/streams/google-ai.test.ts +7 -7
  37. package/packages/model-runtime/src/utils/streams/google-ai.ts +15 -2
  38. package/packages/model-runtime/src/utils/streams/openai/openai.test.ts +41 -0
  39. package/packages/model-runtime/src/utils/streams/openai/openai.ts +38 -2
  40. package/packages/model-runtime/src/utils/streams/protocol.test.ts +32 -0
  41. package/packages/model-runtime/src/utils/streams/protocol.ts +7 -3
  42. package/packages/model-runtime/src/utils/usageConverter.test.ts +58 -0
  43. package/packages/model-runtime/src/utils/usageConverter.ts +5 -1
  44. package/packages/utils/vitest.config.mts +1 -0
  45. package/src/config/aiModels/google.ts +42 -22
  46. package/src/config/aiModels/openrouter.ts +33 -0
  47. package/src/config/aiModels/vertexai.ts +4 -4
  48. package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +6 -0
  49. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +38 -0
  50. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +13 -1
  51. package/src/locales/default/chat.ts +1 -0
  52. package/packages/model-runtime/src/UniformRuntime/index.ts +0 -117
@@ -2271,4 +2271,45 @@ describe('OpenAIStream', () => {
2271
2271
  );
2272
2272
  });
2273
2273
  });
2274
+
2275
+ it('should handle base64_image in delta.images (image_url shape)', async () => {
2276
+ const base64 =
2277
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
2278
+
2279
+ const mockOpenAIStream = new ReadableStream({
2280
+ start(controller) {
2281
+ controller.enqueue({
2282
+ choices: [
2283
+ {
2284
+ delta: {
2285
+ images: [
2286
+ {
2287
+ type: 'image_url',
2288
+ image_url: { url: base64 },
2289
+ index: 0,
2290
+ },
2291
+ ],
2292
+ },
2293
+ index: 0,
2294
+ },
2295
+ ],
2296
+ id: '6',
2297
+ });
2298
+
2299
+ controller.close();
2300
+ },
2301
+ });
2302
+
2303
+ const protocolStream = OpenAIStream(mockOpenAIStream);
2304
+
2305
+ const decoder = new TextDecoder();
2306
+ const chunks = [];
2307
+
2308
+ // @ts-ignore
2309
+ for await (const chunk of protocolStream) {
2310
+ chunks.push(decoder.decode(chunk, { stream: true }));
2311
+ }
2312
+
2313
+ expect(chunks).toEqual(['id: 6\n', 'event: base64_image\n', `data: "${base64}"\n\n`]);
2314
+ });
2274
2315
  });
@@ -96,6 +96,36 @@ const transformOpenAIStream = (
96
96
  }
97
97
  }
98
98
 
99
+ // Handle image preview chunks (e.g. Gemini 2.5 flash image preview)
100
+ // Example shape:
101
+ // choices[0].delta.images = [{ type: 'image_url', image_url: { url: 'data:image/png;base64,...' }, index: 0 }]
102
+ if (
103
+ (item as any).delta &&
104
+ Array.isArray((item as any).delta.images) &&
105
+ (item as any).delta.images.length > 0
106
+ ) {
107
+ const images = (item as any).delta.images as any[];
108
+
109
+ return images
110
+ .map((img) => {
111
+ // support multiple possible shapes for the url
112
+ const url =
113
+ img?.image_url?.url ||
114
+ img?.image_url?.image_url?.url ||
115
+ img?.url ||
116
+ (typeof img === 'string' ? img : undefined);
117
+
118
+ if (!url) return null;
119
+
120
+ return {
121
+ data: url,
122
+ id: chunk.id,
123
+ type: 'base64_image',
124
+ } as StreamProtocolChunk;
125
+ })
126
+ .filter(Boolean) as StreamProtocolChunk[];
127
+ }
128
+
99
129
  // 给定结束原因
100
130
  if (item.finish_reason) {
101
131
  // one-api 的流式接口,会出现既有 finish_reason ,也有 content 的情况
@@ -192,11 +222,11 @@ const transformOpenAIStream = (
192
222
  if ('content' in item.delta && Array.isArray(item.delta.content)) {
193
223
  return item.delta.content
194
224
  .filter((block: any) => block.type === 'thinking' && Array.isArray(block.thinking))
195
- .map((block: any) =>
225
+ .map((block: any) =>
196
226
  block.thinking
197
227
  .filter((thinkItem: any) => thinkItem.type === 'text' && thinkItem.text)
198
228
  .map((thinkItem: any) => thinkItem.text)
199
- .join('')
229
+ .join(''),
200
230
  )
201
231
  .join('');
202
232
  }
@@ -233,6 +263,12 @@ const transformOpenAIStream = (
233
263
  streamContext.thinkingInContent = false;
234
264
  }
235
265
 
266
+ // 如果 content 是空字符串但 chunk 带有 usage,则优先返回 usage(例如 Gemini image-preview 最终会在单独的 chunk 中返回 usage)
267
+ if (content === '' && chunk.usage) {
268
+ const usage = chunk.usage;
269
+ return { data: convertUsage(usage, provider), id: chunk.id, type: 'usage' };
270
+ }
271
+
236
272
  // 判断是否有 citations 内容,更新 returnedCitation 状态
237
273
  if (!streamContext?.returnedCitation) {
238
274
  const citations =
@@ -200,4 +200,36 @@ describe('createTokenSpeedCalculator', async () => {
200
200
  const results = await processChunk(transformer, chunks);
201
201
  expect(results).toHaveLength(chunks.length);
202
202
  });
203
+
204
+ it('should calculate token speed considering outputImageTokens when totalOutputTokens is missing', async () => {
205
+ const chunks = [
206
+ { data: '', id: 'chatcmpl-image-1', type: 'text' },
207
+ { data: 'hi', id: 'chatcmpl-image-1', type: 'text' },
208
+ { data: 'stop', id: 'chatcmpl-image-1', type: 'stop' },
209
+ {
210
+ data: {
211
+ inputTextTokens: 9,
212
+ outputTextTokens: 1,
213
+ outputImageTokens: 4,
214
+ totalInputTokens: 9,
215
+ // totalOutputTokens intentionally omitted to force summation path
216
+ totalTokens: 13,
217
+ },
218
+ id: 'chatcmpl-image-1',
219
+ type: 'usage',
220
+ },
221
+ ];
222
+
223
+ const transformer = createTokenSpeedCalculator((v) => v, { inputStartAt });
224
+ const results = await processChunk(transformer, chunks);
225
+
226
+ // should push an extra speed chunk
227
+ expect(results).toHaveLength(chunks.length + 1);
228
+ const speedChunk = results.slice(-1)[0];
229
+ expect(speedChunk.id).toBe('output_speed');
230
+ expect(speedChunk.type).toBe('speed');
231
+ // tps and ttft should be numeric (avoid flakiness if interval is 0ms)
232
+ expect(speedChunk.data.tps).not.toBeNaN();
233
+ expect(speedChunk.data.ttft).not.toBeNaN();
234
+ });
203
235
  });
@@ -364,10 +364,14 @@ export const createTokenSpeedCalculator = (
364
364
  }
365
365
  // if the chunk is the stop chunk, set as output finish
366
366
  if (inputStartAt && outputStartAt && chunk.type === 'usage') {
367
- const totalOutputTokens = chunk.data?.totalOutputTokens || chunk.data?.outputTextTokens;
368
- const reasoningTokens = chunk.data?.outputReasoningTokens || 0;
367
+ const totalOutputTokens =
368
+ chunk.data?.totalOutputTokens ??
369
+ (chunk.data?.outputTextTokens ?? 0) + (chunk.data?.outputImageTokens ?? 0);
370
+ const reasoningTokens = chunk.data?.outputReasoningTokens ?? 0;
369
371
  const outputTokens =
370
- (outputThinking ?? false) ? totalOutputTokens : totalOutputTokens - reasoningTokens;
372
+ (outputThinking ?? false)
373
+ ? totalOutputTokens
374
+ : Math.max(0, totalOutputTokens - reasoningTokens);
371
375
  result.push({
372
376
  data: {
373
377
  tps: (outputTokens / (Date.now() - outputStartAt)) * 1000,
@@ -290,4 +290,62 @@ describe('convertUsage', () => {
290
290
  totalTokens: 6550,
291
291
  });
292
292
  });
293
+
294
+ it('should handle output image tokens correctly', () => {
295
+ // Arrange
296
+ const usageWithImage = {
297
+ prompt_tokens: 100,
298
+ completion_tokens: 200,
299
+ completion_tokens_details: {
300
+ image_tokens: 60,
301
+ reasoning_tokens: 30,
302
+ },
303
+ total_tokens: 300,
304
+ } as OpenAI.Completions.CompletionUsage;
305
+
306
+ // Act
307
+ const result = convertUsage(usageWithImage);
308
+
309
+ // Assert
310
+ expect(result).toEqual({
311
+ inputTextTokens: 100,
312
+ totalInputTokens: 100,
313
+ totalOutputTokens: 200,
314
+ outputImageTokens: 60,
315
+ outputReasoningTokens: 30,
316
+ outputTextTokens: 110, // 200 - 60 - 30
317
+ totalTokens: 300,
318
+ });
319
+ });
320
+
321
+ it('should handle response output image tokens correctly for ResponseUsage', () => {
322
+ // Arrange
323
+ const responseUsage = {
324
+ input_tokens: 100,
325
+ input_tokens_details: {
326
+ cached_tokens: 0,
327
+ },
328
+ output_tokens: 200,
329
+ output_tokens_details: {
330
+ image_tokens: 60,
331
+ reasoning_tokens: 30,
332
+ },
333
+ total_tokens: 300,
334
+ } as OpenAI.Responses.ResponseUsage;
335
+
336
+ // Act
337
+ const result = convertResponseUsage(responseUsage);
338
+
339
+ // Assert
340
+ expect(result).toEqual({
341
+ inputTextTokens: 100,
342
+ inputCacheMissTokens: 100, // 100 - 0
343
+ totalInputTokens: 100,
344
+ totalOutputTokens: 200,
345
+ outputImageTokens: 60,
346
+ outputReasoningTokens: 30,
347
+ outputTextTokens: 170, // 200 - 30
348
+ totalTokens: 300,
349
+ });
350
+ });
293
351
  });
@@ -20,12 +20,13 @@ export const convertUsage = (
20
20
  const totalOutputTokens = usage.completion_tokens;
21
21
  const outputReasoning = usage.completion_tokens_details?.reasoning_tokens || 0;
22
22
  const outputAudioTokens = usage.completion_tokens_details?.audio_tokens || 0;
23
+ const outputImageTokens = (usage.completion_tokens_details as any)?.image_tokens || 0;
23
24
 
24
25
  // XAI 的 completion_tokens 不包含 reasoning_tokens,需要特殊处理
25
26
  const outputTextTokens =
26
27
  provider === 'xai'
27
28
  ? totalOutputTokens - outputAudioTokens
28
- : totalOutputTokens - outputReasoning - outputAudioTokens;
29
+ : totalOutputTokens - outputReasoning - outputAudioTokens - outputImageTokens;
29
30
 
30
31
  const totalTokens = inputCitationTokens + usage.total_tokens;
31
32
 
@@ -37,6 +38,7 @@ export const convertUsage = (
37
38
  inputCitationTokens: inputCitationTokens,
38
39
  inputTextTokens: inputTextTokens,
39
40
  outputAudioTokens: outputAudioTokens,
41
+ outputImageTokens: outputImageTokens,
40
42
  outputReasoningTokens: outputReasoning,
41
43
  outputTextTokens: outputTextTokens,
42
44
  rejectedPredictionTokens: usage.completion_tokens_details?.rejected_prediction_tokens,
@@ -75,6 +77,7 @@ export const convertResponseUsage = (usage: OpenAI.Responses.ResponseUsage): Mod
75
77
 
76
78
  // For ResponseUsage, outputTextTokens is totalOutputTokens minus reasoning, as no audio output tokens are specified.
77
79
  const outputTextTokens = totalOutputTokens - outputReasoningTokens;
80
+ const outputImageTokens = (usage.output_tokens_details as any)?.image_tokens || 0;
78
81
 
79
82
  // 3. Construct the comprehensive data object (matching ModelTokensUsage structure)
80
83
  const data = {
@@ -87,6 +90,7 @@ export const convertResponseUsage = (usage: OpenAI.Responses.ResponseUsage): Mod
87
90
  inputCitationTokens: undefined, // Not in ResponseUsage
88
91
  inputTextTokens: inputTextTokens,
89
92
  outputAudioTokens: undefined, // Not in ResponseUsage
93
+ outputImageTokens: outputImageTokens,
90
94
  outputReasoningTokens: outputReasoningTokens,
91
95
  outputTextTokens: outputTextTokens,
92
96
  rejectedPredictionTokens: undefined, // Not in ResponseUsage
@@ -7,6 +7,7 @@ export default defineConfig({
7
7
  /* eslint-disable sort-keys-fix/sort-keys-fix */
8
8
  '@/types': resolve(__dirname, '../types/src'),
9
9
  '@/const': resolve(__dirname, '../const/src'),
10
+ '@/libs/model-runtime': resolve(__dirname, '../model-runtime/src'),
10
11
  '@': resolve(__dirname, '../../src'),
11
12
  /* eslint-enable */
12
13
  },
@@ -1,3 +1,4 @@
1
+ import { CHAT_MODEL_IMAGE_GENERATION_PARAMS } from '@/const/image';
1
2
  import { ModelParamsSchema } from '@/libs/standard-parameters';
2
3
  import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
3
4
 
@@ -194,21 +195,21 @@ const googleChatModels: AIChatModelCard[] = [
194
195
  imageOutput: true,
195
196
  vision: true,
196
197
  },
197
- contextWindowTokens: 32_768 + 32_768,
198
+ contextWindowTokens: 32_768 + 8192,
198
199
  description:
199
200
  'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
200
201
  displayName: 'Gemini 2.5 Flash Image Preview',
201
202
  enabled: true,
202
203
  id: 'gemini-2.5-flash-image-preview',
203
- maxOutput: 32_768,
204
+ maxOutput: 8192,
204
205
  pricing: {
205
206
  units: [
206
207
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
207
208
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
208
- { name: 'imageOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
209
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
209
210
  ],
210
211
  },
211
- releasedAt: '2025-08-27',
212
+ releasedAt: '2025-08-26',
212
213
  type: 'chat',
213
214
  },
214
215
  {
@@ -605,71 +606,90 @@ const imagenBaseParameters: ModelParamsSchema = {
605
606
  prompt: { default: '' },
606
607
  };
607
608
 
609
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
608
610
  const googleImageModels: AIImageModelCard[] = [
609
611
  {
610
- description: 'Imagen 4th generation text-to-image model series',
611
- displayName: 'Imagen 4',
612
+ displayName: 'Gemini 2.5 Flash Image Preview',
613
+ id: 'gemini-2.5-flash-image-preview:image',
612
614
  enabled: true,
615
+ type: 'image',
616
+ description:
617
+ 'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
618
+ releasedAt: '2025-08-26',
619
+ parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
620
+ pricing: {
621
+ units: [
622
+ { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
623
+ { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
624
+ { name: 'imageOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
625
+ ],
626
+ },
627
+ },
628
+ {
629
+ displayName: 'Imagen 4',
613
630
  id: 'imagen-4.0-generate-001',
631
+ enabled: true,
632
+ type: 'image',
633
+ description: 'Imagen 4th generation text-to-image model series',
614
634
  organization: 'Deepmind',
635
+ releasedAt: '2025-08-15',
615
636
  parameters: imagenBaseParameters,
616
637
  pricing: {
617
638
  units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
618
639
  },
619
- releasedAt: '2024-08-14',
620
- type: 'image',
621
640
  },
622
641
  {
623
- description: 'Imagen 4th generation text-to-image model series Ultra version',
624
642
  displayName: 'Imagen 4 Ultra',
625
- enabled: true,
626
643
  id: 'imagen-4.0-ultra-generate-001',
644
+ enabled: true,
645
+ type: 'image',
646
+ description: 'Imagen 4th generation text-to-image model series Ultra version',
627
647
  organization: 'Deepmind',
648
+ releasedAt: '2025-08-15',
628
649
  parameters: imagenBaseParameters,
629
650
  pricing: {
630
651
  units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
631
652
  },
632
- releasedAt: '2024-08-14',
633
- type: 'image',
634
653
  },
635
654
  {
636
- description: 'Imagen 4th generation text-to-image model series Fast version',
637
655
  displayName: 'Imagen 4 Fast',
638
- enabled: true,
639
656
  id: 'imagen-4.0-fast-generate-001',
657
+ enabled: true,
658
+ type: 'image',
659
+ description: 'Imagen 4th generation text-to-image model series Fast version',
640
660
  organization: 'Deepmind',
661
+ releasedAt: '2025-08-15',
641
662
  parameters: imagenBaseParameters,
642
663
  pricing: {
643
664
  units: [{ name: 'imageGeneration', rate: 0.02, strategy: 'fixed', unit: 'image' }],
644
665
  },
645
- releasedAt: '2024-08-14',
646
- type: 'image',
647
666
  },
648
667
  {
649
- description: 'Imagen 4th generation text-to-image model series',
650
668
  displayName: 'Imagen 4 Preview 06-06',
651
669
  id: 'imagen-4.0-generate-preview-06-06',
670
+ type: 'image',
671
+ description: 'Imagen 4th generation text-to-image model series',
652
672
  organization: 'Deepmind',
673
+ releasedAt: '2024-06-06',
653
674
  parameters: imagenBaseParameters,
654
675
  pricing: {
655
676
  units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
656
677
  },
657
- releasedAt: '2024-06-06',
658
- type: 'image',
659
678
  },
660
679
  {
661
- description: 'Imagen 4th generation text-to-image model series Ultra version',
662
680
  displayName: 'Imagen 4 Ultra Preview 06-06',
663
681
  id: 'imagen-4.0-ultra-generate-preview-06-06',
682
+ type: 'image',
683
+ description: 'Imagen 4th generation text-to-image model series Ultra version',
664
684
  organization: 'Deepmind',
685
+ releasedAt: '2025-06-11',
665
686
  parameters: imagenBaseParameters,
666
687
  pricing: {
667
688
  units: [{ name: 'imageGeneration', rate: 0.06, strategy: 'fixed', unit: 'image' }],
668
689
  },
669
- releasedAt: '2024-06-06',
670
- type: 'image',
671
690
  },
672
691
  ];
692
+ /* eslint-enable sort-keys-fix/sort-keys-fix */
673
693
 
674
694
  export const allModels = [...googleChatModels, ...googleImageModels];
675
695
 
@@ -11,6 +11,39 @@ const openrouterChatModels: AIChatModelCard[] = [
11
11
  id: 'openrouter/auto',
12
12
  type: 'chat',
13
13
  },
14
+ {
15
+ abilities: {
16
+ imageOutput: true,
17
+ vision: true,
18
+ },
19
+ contextWindowTokens: 32_768 + 8192,
20
+ description: 'Gemini 2.5 Flash 实验模型,支持图像生成',
21
+ displayName: 'Gemini 2.5 Flash Image Preview',
22
+ id: 'google/gemini-2.5-flash-image-preview',
23
+ maxOutput: 8192,
24
+ pricing: {
25
+ units: [
26
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
27
+ { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
28
+ { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
29
+ ],
30
+ },
31
+ releasedAt: '2025-08-26',
32
+ type: 'chat',
33
+ },
34
+ {
35
+ abilities: {
36
+ imageOutput: true,
37
+ vision: true,
38
+ },
39
+ contextWindowTokens: 32_768 + 8192,
40
+ description: 'Gemini 2.5 Flash 实验模型,支持图像生成',
41
+ displayName: 'Gemini 2.5 Flash Image Preview (free)',
42
+ id: 'google/gemini-2.5-flash-image-preview:free',
43
+ maxOutput: 8192,
44
+ releasedAt: '2025-08-26',
45
+ type: 'chat',
46
+ },
14
47
  {
15
48
  abilities: {
16
49
  reasoning: true,
@@ -126,21 +126,21 @@ const vertexaiChatModels: AIChatModelCard[] = [
126
126
  imageOutput: true,
127
127
  vision: true,
128
128
  },
129
- contextWindowTokens: 32_768 + 32_768,
129
+ contextWindowTokens: 32_768 + 8192,
130
130
  description:
131
131
  'Gemini 2.5 Flash Image Preview 是 Google 最新、最快、最高效的原生多模态模型,它允许您通过对话生成和编辑图像。',
132
132
  displayName: 'Gemini 2.5 Flash Image Preview',
133
133
  enabled: true,
134
134
  id: 'gemini-2.5-flash-image-preview',
135
- maxOutput: 32_768,
135
+ maxOutput: 8192,
136
136
  pricing: {
137
137
  units: [
138
138
  { name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
139
139
  { name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
140
- { name: 'imageOutput', rate: 3, strategy: 'fixed', unit: 'millionTokens' },
140
+ { name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
141
141
  ],
142
142
  },
143
- releasedAt: '2025-08-27',
143
+ releasedAt: '2025-08-26',
144
144
  type: 'chat',
145
145
  },
146
146
  {
@@ -61,6 +61,12 @@ const TokenDetail = memo<TokenDetailProps>(({ meta, model, provider }) => {
61
61
  ? detailTokens.outputReasoning.credit
62
62
  : detailTokens.outputReasoning.token,
63
63
  },
64
+ !!detailTokens.outputImage && {
65
+ color: theme.purple,
66
+ id: 'outputImage',
67
+ title: t('messages.tokenDetails.outputImage'),
68
+ value: isShowCredit ? detailTokens.outputImage.credit : detailTokens.outputImage.token,
69
+ },
64
70
  !!detailTokens.outputAudio && {
65
71
  color: theme.cyan9,
66
72
  id: 'outputAudio',
@@ -143,6 +143,44 @@ describe('getDetailsToken', () => {
143
143
  });
144
144
  });
145
145
 
146
+ it('should handle outputImageTokens correctly', () => {
147
+ const usage = {
148
+ inputTextTokens: 100,
149
+ outputImageTokens: 60,
150
+ outputReasoningTokens: 30,
151
+ totalOutputTokens: 200,
152
+ totalTokens: 300,
153
+ } as ModelTokensUsage;
154
+
155
+ const result = getDetailsToken(usage, mockModelCard);
156
+
157
+ expect(result.outputImage).toEqual({
158
+ credit: 1, // 60 * 0.02 = 1.2 -> 1
159
+ id: 'outputImage',
160
+ token: 60,
161
+ });
162
+
163
+ expect(result.outputReasoning).toEqual({
164
+ credit: 1, // 30 * 0.02 = 0.6 -> 1
165
+ token: 30,
166
+ });
167
+
168
+ expect(result.outputText).toEqual({
169
+ credit: 2, // (200 - 30 - 60) * 0.02 = 2.2 -> 2
170
+ token: 110,
171
+ });
172
+
173
+ expect(result.totalOutput).toEqual({
174
+ credit: 4, // 200 * 0.02 = 4
175
+ token: 200,
176
+ });
177
+
178
+ expect(result.totalTokens).toEqual({
179
+ credit: 4, // total credit equals totalOutputCredit here
180
+ token: 300,
181
+ });
182
+ });
183
+
146
184
  it('should handle inputCitationTokens correctly', () => {
147
185
  const usage: ModelTokensUsage = {
148
186
  inputCitationTokens: 75,
@@ -21,9 +21,14 @@ export const getDetailsToken = (
21
21
 
22
22
  const outputReasoningTokens = usage.outputReasoningTokens || (usage as any).reasoningTokens || 0;
23
23
 
24
+ const outputImageTokens = usage.outputImageTokens || (usage as any).imageTokens || 0;
25
+
24
26
  const outputTextTokens = usage.outputTextTokens
25
27
  ? usage.outputTextTokens
26
- : totalOutputTokens - outputReasoningTokens - (usage.outputAudioTokens || 0);
28
+ : totalOutputTokens -
29
+ outputReasoningTokens -
30
+ (usage.outputAudioTokens || 0) -
31
+ outputImageTokens;
27
32
 
28
33
  const inputWriteCacheTokens = usage.inputWriteCacheTokens || 0;
29
34
  const inputCacheTokens = usage.inputCachedTokens || (usage as any).cachedTokens || 0;
@@ -93,6 +98,13 @@ export const getDetailsToken = (
93
98
  token: usage.outputAudioTokens,
94
99
  }
95
100
  : undefined,
101
+ outputImage: !!outputImageTokens
102
+ ? {
103
+ credit: calcCredit(outputImageTokens, formatPrice.output),
104
+ id: 'outputImage',
105
+ token: outputImageTokens,
106
+ }
107
+ : undefined,
96
108
  outputReasoning: !!outputReasoningTokens
97
109
  ? {
98
110
  credit: calcCredit(outputReasoningTokens, formatPrice.output),
@@ -128,6 +128,7 @@ export default {
128
128
  inputWriteCached: '输入缓存写入',
129
129
  output: '输出',
130
130
  outputAudio: '音频输出',
131
+ outputImage: '图像输出',
131
132
  outputText: '文本输出',
132
133
  outputTitle: '输出明细',
133
134
  reasoning: '深度思考',