@lobehub/chat 1.116.4 → 1.117.1

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 (64) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/changelog/v1.json +21 -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 -2
  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/components/ChatItem/ChatItem.tsx +183 -0
  46. package/src/components/ChatItem/components/Actions.tsx +25 -0
  47. package/src/components/ChatItem/components/Avatar.tsx +50 -0
  48. package/src/components/ChatItem/components/BorderSpacing.tsx +13 -0
  49. package/src/components/ChatItem/components/ErrorContent.tsx +24 -0
  50. package/src/components/ChatItem/components/Loading.tsx +26 -0
  51. package/src/components/ChatItem/components/MessageContent.tsx +76 -0
  52. package/src/components/ChatItem/components/Title.tsx +43 -0
  53. package/src/components/ChatItem/index.ts +2 -0
  54. package/src/components/ChatItem/style.ts +208 -0
  55. package/src/components/ChatItem/type.ts +80 -0
  56. package/src/config/aiModels/google.ts +42 -22
  57. package/src/config/aiModels/openrouter.ts +33 -0
  58. package/src/config/aiModels/vertexai.ts +4 -4
  59. package/src/features/ChatItem/index.tsx +1 -1
  60. package/src/features/Conversation/Extras/Usage/UsageDetail/index.tsx +6 -0
  61. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +38 -0
  62. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +13 -1
  63. package/src/locales/default/chat.ts +1 -0
  64. package/packages/model-runtime/src/UniformRuntime/index.ts +0 -117
@@ -0,0 +1,43 @@
1
+ import dayjs from 'dayjs';
2
+ import { memo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { useStyles } from '../style';
6
+ import { ChatItemProps } from '../type';
7
+
8
+ export interface TitleProps {
9
+ avatar: ChatItemProps['avatar'];
10
+ placement?: ChatItemProps['placement'];
11
+ showTitle?: ChatItemProps['showTitle'];
12
+ time?: ChatItemProps['time'];
13
+ }
14
+
15
+ const formatTime = (time: number): string => {
16
+ const now = dayjs();
17
+ const target = dayjs(time);
18
+
19
+ if (target.isSame(now, 'day')) {
20
+ return target.format('HH:mm:ss');
21
+ } else if (target.isSame(now, 'year')) {
22
+ return target.format('MM-DD HH:mm:ss');
23
+ } else {
24
+ return target.format('YYYY-MM-DD HH:mm:ss');
25
+ }
26
+ };
27
+
28
+ const Title = memo<TitleProps>(({ showTitle, placement, time, avatar }) => {
29
+ const { styles } = useStyles({ placement, showTitle, time });
30
+
31
+ return (
32
+ <Flexbox
33
+ className={styles.name}
34
+ direction={placement === 'left' ? 'horizontal' : 'horizontal-reverse'}
35
+ gap={4}
36
+ >
37
+ {showTitle ? avatar.title || 'untitled' : undefined}
38
+ {time && <time>{formatTime(time)}</time>}
39
+ </Flexbox>
40
+ );
41
+ });
42
+
43
+ export default Title;
@@ -0,0 +1,2 @@
1
+ export { default as ChatItem } from './ChatItem';
2
+ export type * from './type';
@@ -0,0 +1,208 @@
1
+ import { createStyles } from 'antd-style';
2
+ import { rgba } from 'polished';
3
+
4
+ export const useStyles = createStyles(
5
+ (
6
+ { cx, css, token, responsive },
7
+ {
8
+ placement,
9
+ variant,
10
+ title,
11
+ avatarSize,
12
+ editing,
13
+ time,
14
+ }: {
15
+ avatarSize?: number;
16
+ editing?: boolean;
17
+ placement?: 'left' | 'right';
18
+ primary?: boolean;
19
+ showTitle?: boolean;
20
+ time?: number;
21
+ title?: string;
22
+ variant?: 'bubble' | 'docs';
23
+ },
24
+ ) => {
25
+ const blockStylish = css`
26
+ padding-block: 8px;
27
+ padding-inline: 12px;
28
+ border: 1px solid ${rgba(token.colorBorderSecondary, 0.66)};
29
+ border-radius: ${token.borderRadiusLG}px;
30
+
31
+ background-color: ${token.colorBgContainer};
32
+ `;
33
+
34
+ const rawStylish = css`
35
+ padding-block-start: ${title ? 0 : '6px'};
36
+ `;
37
+
38
+ const rawContainerStylish = css`
39
+ margin-block-end: -16px;
40
+ transition: background-color 100ms ${token.motionEaseOut};
41
+ `;
42
+
43
+ const typeStylish = variant === 'bubble' ? blockStylish : rawStylish;
44
+
45
+ const editingStylish =
46
+ editing &&
47
+ css`
48
+ width: 100%;
49
+ `;
50
+
51
+ return {
52
+ actions: cx(
53
+ css`
54
+ flex: none;
55
+ align-self: ${variant === 'bubble'
56
+ ? 'flex-end'
57
+ : placement === 'left'
58
+ ? 'flex-start'
59
+ : 'flex-end'};
60
+ justify-content: ${placement === 'left' ? 'flex-end' : 'flex-start'};
61
+ `,
62
+ editing &&
63
+ css`
64
+ pointer-events: none !important;
65
+ opacity: 0 !important;
66
+ `,
67
+ ),
68
+ avatarContainer: css`
69
+ position: relative;
70
+ flex: none;
71
+ width: ${avatarSize}px;
72
+ height: ${avatarSize}px;
73
+ `,
74
+ avatarGroupContainer: css`
75
+ width: ${avatarSize}px;
76
+ `,
77
+ container: cx(
78
+ variant === 'docs' && rawContainerStylish,
79
+ css`
80
+ position: relative;
81
+
82
+ width: 100%;
83
+ max-width: 100vw;
84
+ padding-block: 24px 12px;
85
+ padding-inline: 12px;
86
+
87
+ time {
88
+ display: inline-block;
89
+ white-space: nowrap;
90
+ }
91
+
92
+ div[role='menubar'] {
93
+ display: flex;
94
+ }
95
+
96
+ time,
97
+ div[role='menubar'] {
98
+ pointer-events: none;
99
+ opacity: 0;
100
+ transition: opacity 200ms ${token.motionEaseOut};
101
+ }
102
+
103
+ &:hover {
104
+ time,
105
+ div[role='menubar'] {
106
+ pointer-events: unset;
107
+ opacity: 1;
108
+ }
109
+ }
110
+
111
+ ${responsive.mobile} {
112
+ padding-block-start: ${variant === 'docs' ? '16px' : '12px'};
113
+ padding-inline: 8px;
114
+ }
115
+ `,
116
+ ),
117
+ editingContainer: cx(
118
+ editingStylish,
119
+ css`
120
+ padding-block: 8px 12px;
121
+ padding-inline: 12px;
122
+ border: 1px solid ${token.colorBorderSecondary};
123
+
124
+ &:active,
125
+ &:hover {
126
+ border-color: ${token.colorBorder};
127
+ }
128
+ `,
129
+ variant === 'docs' &&
130
+ css`
131
+ border-radius: ${token.borderRadius}px;
132
+ background: ${token.colorFillQuaternary};
133
+ `,
134
+ ),
135
+ editingInput: css`
136
+ width: 100%;
137
+ `,
138
+ errorContainer: css`
139
+ position: relative;
140
+ overflow: hidden;
141
+ width: 100%;
142
+ `,
143
+
144
+ loading: css`
145
+ position: absolute;
146
+ inset-block-end: 0;
147
+ inset-inline: ${placement === 'right' ? '-4px' : 'unset'}
148
+ ${placement === 'left' ? '-4px' : 'unset'};
149
+
150
+ width: 16px;
151
+ height: 16px;
152
+ border-radius: 50%;
153
+
154
+ color: ${token.colorBgLayout};
155
+
156
+ background: ${token.colorPrimary};
157
+ `,
158
+ message: cx(
159
+ typeStylish,
160
+ css`
161
+ position: relative;
162
+ overflow: hidden;
163
+ max-width: 100%;
164
+
165
+ ${responsive.mobile} {
166
+ width: 100%;
167
+ }
168
+ `,
169
+ ),
170
+ messageContainer: cx(
171
+ editingStylish,
172
+ css`
173
+ position: relative;
174
+ overflow: hidden;
175
+ max-width: 100%;
176
+ margin-block-start: ${time ? -16 : 0}px;
177
+
178
+ ${responsive.mobile} {
179
+ overflow-x: auto;
180
+ }
181
+ `,
182
+ ),
183
+ messageContent: cx(
184
+ editingStylish,
185
+ css`
186
+ position: relative;
187
+ overflow: hidden;
188
+ max-width: 100%;
189
+
190
+ ${responsive.mobile} {
191
+ flex-direction: column !important;
192
+ }
193
+ `,
194
+ ),
195
+ messageExtra: cx('message-extra'),
196
+ name: css`
197
+ pointer-events: none;
198
+
199
+ margin-block-end: 6px;
200
+
201
+ font-size: 12px;
202
+ line-height: 1;
203
+ color: ${token.colorTextDescription};
204
+ text-align: ${placement === 'left' ? 'left' : 'right'};
205
+ `,
206
+ };
207
+ },
208
+ );
@@ -0,0 +1,80 @@
1
+ import { AlertProps, AvatarProps, DivProps, MarkdownProps } from '@lobehub/ui';
2
+ import { EditableMessageProps, MetaData } from '@lobehub/ui/chat';
3
+ import { ReactNode } from 'react';
4
+ import { FlexboxProps } from 'react-layout-kit';
5
+
6
+ export interface ChatItemProps extends Omit<FlexboxProps, 'children' | 'onChange'> {
7
+ aboveMessage?: ReactNode;
8
+ /**
9
+ * @description Actions to be displayed in the chat item
10
+ */
11
+ actions?: ReactNode;
12
+ actionsWrapWidth?: number;
13
+ /**
14
+ * @description Metadata for the avatar
15
+ */
16
+ avatar: MetaData;
17
+ avatarAddon?: ReactNode;
18
+ avatarProps?: AvatarProps;
19
+ belowMessage?: ReactNode;
20
+ /**
21
+ * @description Whether the chat item is in editing mode
22
+ */
23
+ editing?: boolean;
24
+ /**
25
+ * @description Props for Error render
26
+ */
27
+ error?: AlertProps;
28
+ errorMessage?: ReactNode;
29
+ fontSize?: number;
30
+ /**
31
+ * @description Whether the chat item is in loading state
32
+ */
33
+ loading?: boolean;
34
+ markdownProps?: Omit<MarkdownProps, 'className' | 'style' | 'children'>;
35
+ /**
36
+ * @description The message content of the chat item
37
+ */
38
+ message?: ReactNode;
39
+ messageExtra?: ReactNode;
40
+ onAvatarClick?: () => void;
41
+ /**
42
+ * @description Callback when the message content changes
43
+ * @param value - The new message content
44
+ */
45
+ onChange?: (value: string) => void;
46
+ onDoubleClick?: DivProps['onDoubleClick'];
47
+ /**
48
+ * @description Callback when the editing mode changes
49
+ * @param editing - The new editing mode
50
+ */
51
+ onEditingChange?: (editing: boolean) => void;
52
+ /**
53
+ * @default "..."
54
+ */
55
+ placeholderMessage?: string;
56
+ /**
57
+ * @description The placement of the chat item
58
+ * @default 'left'
59
+ */
60
+ placement?: 'left' | 'right';
61
+ /**
62
+ * @description Whether the chat item is primary
63
+ */
64
+ primary?: boolean;
65
+ renderMessage?: (content: ReactNode) => ReactNode;
66
+ /**
67
+ * @description Whether to show the title of the chat item
68
+ */
69
+ showTitle?: boolean;
70
+ text?: EditableMessageProps['text'];
71
+ /**
72
+ * @description The timestamp of the chat item
73
+ */
74
+ time?: number;
75
+ /**
76
+ * @description The type of the chat item
77
+ * @default 'bubble'
78
+ */
79
+ variant?: 'bubble' | 'docs';
80
+ }
@@ -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
  {
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
- import { ChatItemProps, ChatItem as ChatItemRaw } from '@lobehub/ui/chat';
4
3
  import isEqual from 'fast-deep-equal';
5
4
  import { memo, useMemo } from 'react';
6
5
 
6
+ import { ChatItemProps, ChatItem as ChatItemRaw } from '@/components/ChatItem';
7
7
  import { isDesktop } from '@/const/version';
8
8
  import { useElectronStore } from '@/store/electron';
9
9
  import { electronSyncSelectors } from '@/store/electron/selectors';
@@ -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: '深度思考',