@lobehub/chat 1.111.1 → 1.111.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 (131) hide show
  1. package/.cursor/rules/code-review.mdc +2 -19
  2. package/.cursor/rules/cursor-ux.mdc +0 -72
  3. package/.cursor/rules/project-introduce.mdc +5 -5
  4. package/.cursor/rules/react-component.mdc +92 -73
  5. package/.cursor/rules/rules-attach.mdc +28 -61
  6. package/.cursor/rules/system-role.mdc +8 -20
  7. package/.cursor/rules/typescript.mdc +55 -14
  8. package/CHANGELOG.md +52 -0
  9. package/changelog/v1.json +14 -0
  10. package/locales/ar/models.json +21 -3
  11. package/locales/bg-BG/models.json +21 -3
  12. package/locales/de-DE/models.json +21 -3
  13. package/locales/en-US/models.json +21 -3
  14. package/locales/es-ES/models.json +21 -3
  15. package/locales/fa-IR/models.json +21 -3
  16. package/locales/fr-FR/models.json +21 -3
  17. package/locales/it-IT/models.json +21 -3
  18. package/locales/ja-JP/models.json +21 -3
  19. package/locales/ko-KR/models.json +21 -3
  20. package/locales/nl-NL/models.json +21 -3
  21. package/locales/pl-PL/models.json +21 -3
  22. package/locales/pt-BR/models.json +21 -3
  23. package/locales/ru-RU/models.json +21 -3
  24. package/locales/tr-TR/models.json +21 -3
  25. package/locales/vi-VN/models.json +21 -3
  26. package/locales/zh-CN/models.json +21 -3
  27. package/locales/zh-TW/models.json +21 -3
  28. package/package.json +1 -1
  29. package/packages/types/src/aiModel.ts +67 -46
  30. package/packages/types/src/hotkey.ts +2 -0
  31. package/packages/types/src/llm.ts +3 -3
  32. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/PinList/index.tsx +3 -3
  33. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.test.tsx +1 -0
  34. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -2
  35. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +2 -2
  36. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +2 -2
  37. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +2 -2
  38. package/src/app/[variants]/(main)/discover/(detail)/model/[...slugs]/features/Details/Overview/ProviderList/index.tsx +23 -12
  39. package/src/app/[variants]/(main)/discover/(detail)/provider/[...slugs]/features/Details/Overview/ModelList/index.tsx +23 -10
  40. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +21 -12
  41. package/src/components/Thinking/index.tsx +53 -13
  42. package/src/config/aiModels/ai21.ts +8 -4
  43. package/src/config/aiModels/ai360.ts +28 -14
  44. package/src/config/aiModels/aihubmix.ts +174 -86
  45. package/src/config/aiModels/anthropic.ts +97 -38
  46. package/src/config/aiModels/azure.ts +54 -32
  47. package/src/config/aiModels/azureai.ts +63 -37
  48. package/src/config/aiModels/baichuan.ts +24 -12
  49. package/src/config/aiModels/bedrock.ts +60 -30
  50. package/src/config/aiModels/cohere.ts +60 -30
  51. package/src/config/aiModels/deepseek.ts +10 -6
  52. package/src/config/aiModels/fireworksai.ts +88 -44
  53. package/src/config/aiModels/giteeai.ts +1 -1
  54. package/src/config/aiModels/github.ts +44 -26
  55. package/src/config/aiModels/google.ts +119 -68
  56. package/src/config/aiModels/groq.ts +48 -24
  57. package/src/config/aiModels/higress.ts +617 -310
  58. package/src/config/aiModels/hunyuan.ts +105 -54
  59. package/src/config/aiModels/infiniai.ts +104 -52
  60. package/src/config/aiModels/internlm.ts +16 -8
  61. package/src/config/aiModels/jina.ts +4 -2
  62. package/src/config/aiModels/minimax.ts +11 -10
  63. package/src/config/aiModels/mistral.ts +40 -20
  64. package/src/config/aiModels/moonshot.ts +42 -22
  65. package/src/config/aiModels/novita.ts +196 -98
  66. package/src/config/aiModels/openai.ts +270 -137
  67. package/src/config/aiModels/openrouter.ts +205 -100
  68. package/src/config/aiModels/perplexity.ts +36 -6
  69. package/src/config/aiModels/ppio.ts +76 -38
  70. package/src/config/aiModels/qwen.ts +257 -133
  71. package/src/config/aiModels/sambanova.ts +56 -28
  72. package/src/config/aiModels/sensenova.ts +100 -50
  73. package/src/config/aiModels/siliconcloud.ts +224 -112
  74. package/src/config/aiModels/stepfun.ts +44 -22
  75. package/src/config/aiModels/taichu.ts +8 -4
  76. package/src/config/aiModels/tencentcloud.ts +12 -6
  77. package/src/config/aiModels/upstage.ts +8 -4
  78. package/src/config/aiModels/v0.ts +15 -12
  79. package/src/config/aiModels/vertexai.ts +49 -27
  80. package/src/config/aiModels/volcengine.ts +110 -51
  81. package/src/config/aiModels/wenxin.ts +179 -73
  82. package/src/config/aiModels/xai.ts +33 -19
  83. package/src/config/aiModels/zeroone.ts +48 -24
  84. package/src/config/aiModels/zhipu.ts +118 -69
  85. package/src/config/modelProviders/ai21.ts +0 -8
  86. package/src/config/modelProviders/ai360.ts +0 -20
  87. package/src/config/modelProviders/anthropic.ts +0 -56
  88. package/src/config/modelProviders/baichuan.ts +0 -30
  89. package/src/config/modelProviders/bedrock.ts +0 -74
  90. package/src/config/modelProviders/deepseek.ts +0 -13
  91. package/src/config/modelProviders/fireworksai.ts +0 -88
  92. package/src/config/modelProviders/google.ts +0 -59
  93. package/src/config/modelProviders/groq.ts +0 -48
  94. package/src/config/modelProviders/higress.ts +0 -727
  95. package/src/config/modelProviders/hunyuan.ts +0 -45
  96. package/src/config/modelProviders/infiniai.ts +0 -60
  97. package/src/config/modelProviders/internlm.ts +0 -8
  98. package/src/config/modelProviders/mistral.ts +0 -48
  99. package/src/config/modelProviders/modelscope.ts +2 -1
  100. package/src/config/modelProviders/openai.ts +5 -100
  101. package/src/config/modelProviders/openrouter.ts +0 -77
  102. package/src/config/modelProviders/ppio.ts +0 -95
  103. package/src/config/modelProviders/qwen.ts +0 -165
  104. package/src/config/modelProviders/sensenova.ts +0 -45
  105. package/src/config/modelProviders/siliconcloud.ts +0 -266
  106. package/src/config/modelProviders/stepfun.ts +0 -60
  107. package/src/config/modelProviders/taichu.ts +0 -10
  108. package/src/config/modelProviders/wenxin.ts +0 -90
  109. package/src/config/modelProviders/xai.ts +0 -16
  110. package/src/config/modelProviders/zeroone.ts +0 -60
  111. package/src/config/modelProviders/zhipu.ts +0 -80
  112. package/src/const/hotkeys.ts +6 -0
  113. package/src/features/Conversation/Extras/Usage/UsageDetail/ModelCard.tsx +4 -3
  114. package/src/features/Conversation/Extras/Usage/UsageDetail/pricing.ts +25 -15
  115. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.test.ts +7 -5
  116. package/src/features/Conversation/Extras/Usage/UsageDetail/tokens.ts +6 -5
  117. package/src/hooks/useHotkeys/chatScope.ts +2 -2
  118. package/src/hooks/useHotkeys/globalScope.ts +16 -4
  119. package/src/hooks/usePinnedAgentState.ts +21 -0
  120. package/src/hooks/useSwitchSession.ts +1 -1
  121. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +54 -8
  122. package/src/locales/default/hotkey.ts +4 -0
  123. package/src/server/routers/lambda/agent.ts +2 -2
  124. package/src/server/routers/lambda/config/__snapshots__/index.test.ts.snap +0 -28
  125. package/src/server/services/discover/index.ts +7 -6
  126. package/src/server/services/user/index.ts +1 -2
  127. package/src/utils/__snapshots__/parseModels.test.ts.snap +28 -4
  128. package/src/utils/_deprecated/__snapshots__/parseModels.test.ts.snap +0 -8
  129. package/src/utils/parseModels.test.ts +60 -9
  130. package/src/utils/pricing.test.ts +183 -0
  131. package/src/utils/pricing.ts +90 -0
@@ -107,6 +107,65 @@ export interface ChatModelPricing extends BasicModelPricing {
107
107
  writeCacheInput?: number;
108
108
  }
109
109
 
110
+ // New pricing system types
111
+ export type PricingUnitName =
112
+ // Text-based pricing units
113
+ | 'textInput' // corresponds to ChatModelPricing.input
114
+ | 'textOutput' // corresponds to ChatModelPricing.output
115
+ | 'textInput_cacheRead' // corresponds to ChatModelPricing.cachedInput
116
+ | 'textInput_cacheWrite' // corresponds to ChatModelPricing.writeCacheInput
117
+
118
+ // Audio-based pricing units
119
+ | 'audioInput' // corresponds to ChatModelPricing.audioInput
120
+ | 'audioOutput' // corresponds to ChatModelPricing.audioOutput
121
+ | 'audioInput_cacheRead' // corresponds to ChatModelPricing.cachedAudioInput
122
+
123
+ // Image-based pricing units
124
+ | 'imageGeneration'; // for image generation models
125
+
126
+ export type PricingUnitType =
127
+ | 'millionTokens' // per 1M tokens
128
+ | 'millionCharacters' // per 1M characters
129
+ | 'image' // per image
130
+ | 'megapixel' // per megapixel
131
+ | 'second'; // per second
132
+
133
+ export type PricingStrategy = 'fixed' | 'tiered' | 'lookup';
134
+
135
+ export interface PricingUnitBase {
136
+ name: PricingUnitName;
137
+ strategy: PricingStrategy;
138
+ unit: PricingUnitType;
139
+ }
140
+
141
+ export interface FixedPricingUnit extends PricingUnitBase {
142
+ rate: number;
143
+ strategy: 'fixed';
144
+ }
145
+
146
+ export interface TieredPricingUnit extends PricingUnitBase {
147
+ strategy: 'tiered';
148
+ tiers: Array<{
149
+ rate: number;
150
+ upTo: number | 'infinity';
151
+ }>;
152
+ }
153
+
154
+ export interface LookupPricingUnit extends PricingUnitBase {
155
+ lookup: {
156
+ prices: Record<string, number>;
157
+ pricingParams: string[];
158
+ };
159
+ strategy: 'lookup';
160
+ }
161
+
162
+ export type PricingUnit = FixedPricingUnit | TieredPricingUnit | LookupPricingUnit;
163
+
164
+ export interface Pricing {
165
+ currency?: ModelPriceCurrency;
166
+ units: PricingUnit[];
167
+ }
168
+
110
169
  export interface AIBaseModelCard {
111
170
  /**
112
171
  * the context window (or input + output tokens limit)
@@ -167,69 +226,31 @@ export interface AIChatModelCard extends AIBaseModelCard {
167
226
  abilities?: ModelAbilities;
168
227
  config?: AiModelConfig;
169
228
  maxOutput?: number;
170
- pricing?: ChatModelPricing;
229
+ pricing?: Pricing;
171
230
  settings?: AiModelSettings;
172
231
  type: 'chat';
173
232
  }
174
233
 
175
234
  export interface AIEmbeddingModelCard extends AIBaseModelCard {
176
235
  maxDimension: number;
177
- pricing?: {
178
- /**
179
- * the currency of the pricing
180
- * @default USD
181
- */
182
- currency?: ModelPriceCurrency;
183
- /**
184
- * the input pricing, e.g. $1 / 1M tokens
185
- */
186
- input?: number;
187
- };
236
+ pricing?: Pricing;
188
237
  type: 'embedding';
189
238
  }
190
239
 
191
240
  export interface AIImageModelCard extends AIBaseModelCard {
192
241
  parameters?: ModelParamsSchema;
193
- pricing?: {
194
- /**
195
- * the currency of the pricing
196
- * @default USD
197
- */
198
- currency?: ModelPriceCurrency;
199
- } & Record<string, number>;
242
+ pricing?: Pricing;
200
243
  resolutions?: string[];
201
244
  type: 'image';
202
245
  }
203
246
 
204
247
  export interface AITTSModelCard extends AIBaseModelCard {
205
- pricing?: {
206
- /**
207
- * the currency of the pricing
208
- * @default USD
209
- */
210
- currency?: ModelPriceCurrency;
211
- /**
212
- * the input pricing, e.g. $1 / 1M tokens
213
- */
214
- input?: number;
215
- output?: number;
216
- };
248
+ pricing?: Pricing;
217
249
  type: 'tts';
218
250
  }
219
251
 
220
252
  export interface AISTTModelCard extends AIBaseModelCard {
221
- pricing?: {
222
- /**
223
- * the currency of the pricing
224
- * @default USD
225
- */
226
- currency?: ModelPriceCurrency;
227
- /**
228
- * the input pricing, e.g. $1 / 1M tokens
229
- */
230
- input?: number;
231
- output?: number;
232
- };
253
+ pricing?: Pricing;
233
254
  type: 'stt';
234
255
  }
235
256
 
@@ -257,7 +278,7 @@ export interface AIRealtimeModelCard extends AIBaseModelCard {
257
278
  */
258
279
  deploymentName?: string;
259
280
  maxOutput?: number;
260
- pricing?: ChatModelPricing;
281
+ pricing?: Pricing;
261
282
  type: 'realtime';
262
283
  }
263
284
 
@@ -269,7 +290,7 @@ export interface AiFullModelCard extends AIBaseModelCard {
269
290
  id: string;
270
291
  maxDimension?: number;
271
292
  parameters?: ModelParamsSchema;
272
- pricing?: ChatModelPricing;
293
+ pricing?: Pricing;
273
294
  type: AiModelType;
274
295
  }
275
296
 
@@ -304,7 +325,7 @@ export interface AiProviderModelListItem {
304
325
  enabled: boolean;
305
326
  id: string;
306
327
  parameters?: Record<string, any>;
307
- pricing?: ChatModelPricing;
328
+ pricing?: Pricing;
308
329
  releasedAt?: string;
309
330
  settings?: AiModelSettings;
310
331
  source?: AiModelSourceType;
@@ -54,12 +54,14 @@ export const KeyEnum = {
54
54
  Space: 'space',
55
55
  Tab: 'tab',
56
56
  Up: 'up',
57
+ Zero: '0',
57
58
  } as const;
58
59
 
59
60
  export const HotkeyEnum = {
60
61
  AddUserMessage: 'addUserMessage',
61
62
  ClearCurrentMessages: 'clearCurrentMessages',
62
63
  EditMessage: 'editMessage',
64
+ NavigateToChat: 'navigateToChat',
63
65
  OpenChatSettings: 'openChatSettings',
64
66
  OpenHotkeyHelper: 'openHotkeyHelper',
65
67
  RegenerateMessage: 'regenerateMessage',
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from 'react';
2
2
 
3
- import { AiModelType, ChatModelPricing } from './aiModel';
4
- import { AiProviderSettings } from './aiProvider';
3
+ import { AiModelType, Pricing } from '@/types/aiModel';
4
+ import { AiProviderSettings } from '@/types/aiProvider';
5
5
 
6
6
  export type ModelPriceCurrency = 'CNY' | 'USD';
7
7
 
@@ -41,7 +41,7 @@ export interface ChatModelCard {
41
41
  */
42
42
  legacy?: boolean;
43
43
  maxOutput?: number;
44
- pricing?: ChatModelPricing;
44
+ pricing?: Pricing;
45
45
 
46
46
  /**
47
47
  * whether model supports reasoning
@@ -2,9 +2,9 @@ import { Avatar, Tooltip } from '@lobehub/ui';
2
2
  import { Divider } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import isEqual from 'fast-deep-equal';
5
- import { parseAsBoolean, useQueryState } from 'nuqs';
6
5
  import { Flexbox } from 'react-layout-kit';
7
6
 
7
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
8
8
  import { useSwitchSession } from '@/hooks/useSwitchSession';
9
9
  import { useSessionStore } from '@/store/session';
10
10
  import { sessionHelpers } from '@/store/session/helpers';
@@ -71,11 +71,11 @@ const PinList = () => {
71
71
  const switchSession = useSwitchSession();
72
72
  const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.SwitchAgent));
73
73
  const hasList = list.length > 0;
74
- const [isPinned, setPinned] = useQueryState('pinned', parseAsBoolean);
74
+ const [isPinned, { pinAgent }] = usePinnedAgentState();
75
75
 
76
76
  const switchAgent = (id: string) => {
77
77
  switchSession(id);
78
- setPinned(true);
78
+ pinAgent();
79
79
  };
80
80
 
81
81
  return (
@@ -38,6 +38,7 @@ vi.mock('@lobehub/ui', () => ({
38
38
  ActionIcon: vi.fn(({ title }) => <div>{title}</div>),
39
39
  combineKeys: vi.fn((keys) => keys.join('+')),
40
40
  KeyMapEnum: { Alt: 'alt', Ctrl: 'ctrl', Shift: 'shift' },
41
+ Hotkey: vi.fn(({ keys = [] }) => <div>{keys}</div>),
41
42
  }));
42
43
 
43
44
  vi.mock('react-i18next', () => ({
@@ -1,4 +1,4 @@
1
- import { ActionIcon, ActionIconProps } from '@lobehub/ui';
1
+ import { ActionIcon, ActionIconProps, Hotkey } from '@lobehub/ui';
2
2
  import { Compass, FolderClosed, MessageSquare, Palette } from 'lucide-react';
3
3
  import Link from 'next/link';
4
4
  import { memo } from 'react';
@@ -9,6 +9,9 @@ import { useGlobalStore } from '@/store/global';
9
9
  import { SidebarTabKey } from '@/store/global/initialState';
10
10
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
11
11
  import { useSessionStore } from '@/store/session';
12
+ import { useUserStore } from '@/store/user';
13
+ import { settingsSelectors } from '@/store/user/selectors';
14
+ import { HotkeyEnum } from '@/types/hotkey';
12
15
 
13
16
  const ICON_SIZE: ActionIconProps['size'] = {
14
17
  blockSize: 40,
@@ -25,6 +28,7 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
25
28
  const { t } = useTranslation('common');
26
29
  const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
27
30
  const { showMarket, enableKnowledgeBase } = useServerConfigStore(featureFlagsSelectors);
31
+ const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
28
32
 
29
33
  const isChatActive = tab === SidebarTabKey.Chat && !isPinned;
30
34
  const isFilesActive = tab === SidebarTabKey.Files;
@@ -51,7 +55,12 @@ const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
51
55
  active={isChatActive}
52
56
  icon={MessageSquare}
53
57
  size={ICON_SIZE}
54
- title={t('tab.chat')}
58
+ title={
59
+ <Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
60
+ <span>{t('tab.chat')}</span>
61
+ <Hotkey inverseTheme keys={hotkey} />
62
+ </Flexbox>
63
+ }
55
64
  tooltipProps={{ placement: 'right' }}
56
65
  />
57
66
  </Link>
@@ -2,11 +2,11 @@
2
2
 
3
3
  import { SideNav } from '@lobehub/ui';
4
4
  import { useTheme } from 'antd-style';
5
- import { parseAsBoolean, useQueryState } from 'nuqs';
6
5
  import { Suspense, memo } from 'react';
7
6
 
8
7
  import { isDesktop } from '@/const/version';
9
8
  import { useActiveTabKey } from '@/hooks/useActiveTabKey';
9
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
10
10
  import { useGlobalStore } from '@/store/global';
11
11
  import { systemStatusSelectors } from '@/store/global/selectors';
12
12
  import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
@@ -18,7 +18,7 @@ import PinList from './PinList';
18
18
  import TopActions from './TopActions';
19
19
 
20
20
  const Top = () => {
21
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
21
+ const [isPinned] = usePinnedAgentState();
22
22
  const sidebarKey = useActiveTabKey();
23
23
 
24
24
  return <TopActions isPinned={isPinned} tab={sidebarKey} />;
@@ -3,13 +3,13 @@
3
3
  import { Avatar } from '@lobehub/ui';
4
4
  import { Skeleton } from 'antd';
5
5
  import { createStyles } from 'antd-style';
6
- import { parseAsBoolean, useQueryState } from 'nuqs';
7
6
  import { Suspense, memo } from 'react';
8
7
  import { useTranslation } from 'react-i18next';
9
8
  import { Flexbox } from 'react-layout-kit';
10
9
 
11
10
  import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
12
11
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
12
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
13
13
  import { useGlobalStore } from '@/store/global';
14
14
  import { systemStatusSelectors } from '@/store/global/selectors';
15
15
  import { useSessionStore } from '@/store/session';
@@ -44,7 +44,7 @@ const Main = memo<{ className?: string }>(({ className }) => {
44
44
  const { t } = useTranslation(['chat', 'hotkey']);
45
45
  const { styles } = useStyles();
46
46
  useInitAgentConfig();
47
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
47
+ const [isPinned] = usePinnedAgentState();
48
48
 
49
49
  const [init, isInbox, title, avatar, backgroundColor] = useSessionStore((s) => [
50
50
  sessionSelectors.isSomeSessionActive(s),
@@ -3,11 +3,11 @@
3
3
  import { DraggablePanel, DraggablePanelContainer, type DraggablePanelProps } from '@lobehub/ui';
4
4
  import { createStyles, useResponsive } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
- import { parseAsBoolean, useQueryState } from 'nuqs';
7
6
  import { PropsWithChildren, memo, useEffect, useState } from 'react';
8
7
 
9
8
  import { withSuspense } from '@/components/withSuspense';
10
9
  import { FOLDER_WIDTH } from '@/const/layoutTokens';
10
+ import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
11
11
  import { useGlobalStore } from '@/store/global';
12
12
  import { systemStatusSelectors } from '@/store/global/selectors';
13
13
 
@@ -35,7 +35,7 @@ export const useStyles = createStyles(({ css, token }) => ({
35
35
  const SessionPanel = memo<PropsWithChildren>(({ children }) => {
36
36
  const { md = true } = useResponsive();
37
37
 
38
- const [isPinned] = useQueryState('pinned', parseAsBoolean);
38
+ const [isPinned] = usePinnedAgentState();
39
39
 
40
40
  const { styles } = useStyles();
41
41
  const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
@@ -14,6 +14,7 @@ import InlineTable from '@/components/InlineTable';
14
14
  import { ModelInfoTags } from '@/components/ModelSelect';
15
15
  import { BASE_PROVIDER_DOC_URL } from '@/const/url';
16
16
  import { formatPriceByCurrency, formatTokenNumber } from '@/utils/format';
17
+ import { getTextInputUnitRate, getTextOutputUnitRate } from '@/utils/pricing';
17
18
 
18
19
  import { useDetailContext } from '../../../DetailProvider';
19
20
 
@@ -89,12 +90,17 @@ const ProviderList = memo(() => {
89
90
  {
90
91
  dataIndex: 'model.inputPrice',
91
92
  key: 'inputPrice',
92
- render: (_, record) =>
93
- record.model?.pricing?.input
94
- ? '$' +
95
- formatPriceByCurrency(record.model.pricing.input, record.model.pricing?.currency)
96
- : '--',
97
- sorter: (a, b) => (a.model?.pricing?.input || 0) - (b.model?.pricing?.input || 0),
93
+ render: (_, record) => {
94
+ const inputRate = getTextInputUnitRate(record.model?.pricing);
95
+ return inputRate
96
+ ? '$' + formatPriceByCurrency(inputRate, record.model.pricing?.currency)
97
+ : '--';
98
+ },
99
+ sorter: (a, b) => {
100
+ const aRate = getTextInputUnitRate(a.model?.pricing) || 0;
101
+ const bRate = getTextInputUnitRate(b.model?.pricing) || 0;
102
+ return aRate - bRate;
103
+ },
98
104
  title: (
99
105
  <Tooltip title={t('models.providerInfo.inputTooltip')}>
100
106
  {t('models.providerInfo.input')}
@@ -105,12 +111,17 @@ const ProviderList = memo(() => {
105
111
  {
106
112
  dataIndex: 'model.outputPrice',
107
113
  key: 'outputPrice',
108
- render: (_, record) =>
109
- record.model?.pricing?.output
110
- ? '$' +
111
- formatPriceByCurrency(record.model.pricing.output, record.model.pricing?.currency)
112
- : '--',
113
- sorter: (a, b) => (a.model?.pricing?.output || 0) - (b.model?.pricing?.output || 0),
114
+ render: (_, record) => {
115
+ const outputRate = getTextOutputUnitRate(record.model?.pricing);
116
+ return outputRate
117
+ ? '$' + formatPriceByCurrency(outputRate, record.model.pricing?.currency)
118
+ : '--';
119
+ },
120
+ sorter: (a, b) => {
121
+ const aRate = getTextOutputUnitRate(a.model?.pricing) || 0;
122
+ const bRate = getTextOutputUnitRate(b.model?.pricing) || 0;
123
+ return aRate - bRate;
124
+ },
114
125
  title: (
115
126
  <Tooltip title={t('models.providerInfo.outputTooltip')}>
116
127
  {t('models.providerInfo.output')}
@@ -13,6 +13,7 @@ import urlJoin from 'url-join';
13
13
  import InlineTable from '@/components/InlineTable';
14
14
  import { ModelInfoTags } from '@/components/ModelSelect';
15
15
  import { formatPriceByCurrency, formatTokenNumber } from '@/utils/format';
16
+ import { getTextInputUnitRate, getTextOutputUnitRate } from '@/utils/pricing';
16
17
 
17
18
  import { useDetailContext } from '../../../DetailProvider';
18
19
 
@@ -82,11 +83,17 @@ const ModelList = memo(() => {
82
83
  {
83
84
  dataIndex: 'inputPrice',
84
85
  key: 'inputPrice',
85
- render: (_, record) =>
86
- record.pricing?.input
87
- ? '$' + formatPriceByCurrency(record.pricing.input, record.pricing?.currency)
88
- : '--',
89
- sorter: (a, b) => (a.pricing?.input || 0) - (b.pricing?.input || 0),
86
+ render: (_, record) => {
87
+ const inputRate = getTextInputUnitRate(record.pricing);
88
+ return inputRate
89
+ ? '$' + formatPriceByCurrency(inputRate, record.pricing?.currency)
90
+ : '--';
91
+ },
92
+ sorter: (a, b) => {
93
+ const aRate = getTextInputUnitRate(a.pricing) || 0;
94
+ const bRate = getTextInputUnitRate(b.pricing) || 0;
95
+ return aRate - bRate;
96
+ },
90
97
  title: (
91
98
  <Tooltip title={t('models.providerInfo.inputTooltip')}>
92
99
  {t('models.providerInfo.input')}
@@ -97,11 +104,17 @@ const ModelList = memo(() => {
97
104
  {
98
105
  dataIndex: 'outputPrice',
99
106
  key: 'outputPrice',
100
- render: (_, record) =>
101
- record.pricing?.output
102
- ? '$' + formatPriceByCurrency(record.pricing.output, record.pricing?.currency)
103
- : '--',
104
- sorter: (a, b) => (a.pricing?.output || 0) - (b.pricing?.output || 0),
107
+ render: (_, record) => {
108
+ const outputRate = getTextOutputUnitRate(record.pricing);
109
+ return outputRate
110
+ ? '$' + formatPriceByCurrency(outputRate, record.pricing?.currency)
111
+ : '--';
112
+ },
113
+ sorter: (a, b) => {
114
+ const aRate = getTextOutputUnitRate(a.pricing) || 0;
115
+ const bRate = getTextOutputUnitRate(b.pricing) || 0;
116
+ return aRate - bRate;
117
+ },
105
118
  title: (
106
119
  <Tooltip title={t('models.providerInfo.outputTooltip')}>
107
120
  {t('models.providerInfo.output')}
@@ -10,8 +10,13 @@ import { Flexbox } from 'react-layout-kit';
10
10
  import { ModelInfoTags } from '@/components/ModelSelect';
11
11
  import { useIsMobile } from '@/hooks/useIsMobile';
12
12
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
13
- import { AiModelSourceEnum, AiProviderModelListItem, ChatModelPricing } from '@/types/aiModel';
13
+ import { AiModelSourceEnum, AiProviderModelListItem } from '@/types/aiModel';
14
14
  import { formatPriceByCurrency } from '@/utils/format';
15
+ import {
16
+ getAudioInputUnitRate,
17
+ getTextInputUnitRate,
18
+ getTextOutputUnitRate,
19
+ } from '@/utils/pricing';
15
20
 
16
21
  import ModelConfigModal from './ModelConfigModal';
17
22
  import { ProviderSettingsContext } from './ProviderSettingsContext';
@@ -54,7 +59,6 @@ interface ModelItemProps extends AiProviderModelListItem {
54
59
  enabled: boolean;
55
60
  id: string;
56
61
  isAzure?: boolean;
57
- pricing?: ChatModelPricing;
58
62
  releasedAt?: string;
59
63
  removed?: boolean;
60
64
  }
@@ -94,38 +98,43 @@ const ModelItem = memo<ModelItemProps>(
94
98
 
95
99
  switch (type) {
96
100
  case 'chat': {
101
+ const inputRate = getTextInputUnitRate(pricing);
102
+ const outputRate = getTextOutputUnitRate(pricing);
97
103
  return [
98
- typeof pricing.input === 'number' &&
104
+ typeof inputRate === 'number' &&
99
105
  t('providerModels.item.pricing.inputTokens', {
100
- amount: formatPriceByCurrency(pricing.input, pricing?.currency),
106
+ amount: formatPriceByCurrency(inputRate, pricing?.currency),
101
107
  }),
102
- typeof pricing.output === 'number' &&
108
+ typeof outputRate === 'number' &&
103
109
  t('providerModels.item.pricing.outputTokens', {
104
- amount: formatPriceByCurrency(pricing.output, pricing?.currency),
110
+ amount: formatPriceByCurrency(outputRate, pricing?.currency),
105
111
  }),
106
112
  ].filter(Boolean) as string[];
107
113
  }
108
114
  case 'embedding': {
115
+ const inputRate = getTextInputUnitRate(pricing);
109
116
  return [
110
- typeof pricing.input === 'number' &&
117
+ typeof inputRate === 'number' &&
111
118
  t('providerModels.item.pricing.inputTokens', {
112
- amount: formatPriceByCurrency(pricing.input, pricing?.currency),
119
+ amount: formatPriceByCurrency(inputRate, pricing?.currency),
113
120
  }),
114
121
  ].filter(Boolean) as string[];
115
122
  }
116
123
  case 'tts': {
124
+ const inputRate = getAudioInputUnitRate(pricing);
117
125
  return [
118
- typeof pricing.input === 'number' &&
126
+ typeof inputRate === 'number' &&
119
127
  t('providerModels.item.pricing.inputCharts', {
120
- amount: formatPriceByCurrency(pricing.input, pricing?.currency),
128
+ amount: formatPriceByCurrency(inputRate, pricing?.currency),
121
129
  }),
122
130
  ].filter(Boolean) as string[];
123
131
  }
124
132
  case 'stt': {
133
+ const inputRate = getAudioInputUnitRate(pricing);
125
134
  return [
126
- typeof pricing.input === 'number' &&
135
+ typeof inputRate === 'number' &&
127
136
  t('providerModels.item.pricing.inputMinutes', {
128
- amount: formatPriceByCurrency(pricing.input, pricing?.currency),
137
+ amount: formatPriceByCurrency(inputRate, pricing?.currency),
129
138
  }),
130
139
  ].filter(Boolean) as string[];
131
140
  }
@@ -3,7 +3,7 @@ import { createStyles } from 'antd-style';
3
3
  import { AnimatePresence, motion } from 'framer-motion';
4
4
  import { AtomIcon, ChevronDown, ChevronRight } from 'lucide-react';
5
5
  import { rgba } from 'polished';
6
- import { CSSProperties, memo, useEffect, useState } from 'react';
6
+ import { CSSProperties, RefObject, memo, useEffect, useRef, useState } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
@@ -16,11 +16,19 @@ const useStyles = createStyles(({ css, token }) => ({
16
16
  color: ${token.colorTextTertiary};
17
17
  transition: all 0.2s ${token.motionEaseOut};
18
18
  `,
19
+ contentScroll: css`
20
+ scroll-behavior: auto;
21
+
22
+ overflow-y: auto;
23
+ overscroll-behavior: contain;
24
+
25
+ max-height: 40vh;
26
+ padding-block-end: 12px;
27
+ padding-inline: 12px;
28
+ `,
19
29
  expand: css`
20
- color: ${token.colorTextSecondary};
21
- background: ${token.colorFillTertiary};
30
+ color: ${token.colorTextTertiary};
22
31
  `,
23
-
24
32
  header: css`
25
33
  padding-block: 4px;
26
34
  padding-inline: 8px 4px;
@@ -34,7 +42,6 @@ const useStyles = createStyles(({ css, token }) => ({
34
42
 
35
43
  headerExpand: css`
36
44
  color: ${token.colorTextSecondary};
37
- background: ${token.colorFillQuaternary};
38
45
  `,
39
46
  shinyText: css`
40
47
  color: ${rgba(token.colorText, 0.45)};
@@ -86,11 +93,39 @@ const Thinking = memo<ThinkingProps>((props) => {
86
93
  const { styles, cx, theme } = useStyles();
87
94
 
88
95
  const [showDetail, setShowDetail] = useState(false);
96
+ const contentRef = useRef<HTMLDivElement | null>(null);
89
97
 
90
98
  useEffect(() => {
91
99
  setShowDetail(!!thinking);
92
100
  }, [thinking]);
93
101
 
102
+ // 当内容变更且正在思考时,如果用户接近底部则自动滚动到底部
103
+ useEffect(() => {
104
+ if (!thinking || !showDetail) return;
105
+ const container = contentRef.current;
106
+ if (!container) return;
107
+
108
+ // 仅当用户接近底部时才自动滚动,避免打断用户查看上方内容
109
+ const distanceToBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
110
+ const isNearBottom = distanceToBottom < 60;
111
+
112
+ if (isNearBottom) {
113
+ requestAnimationFrame(() => {
114
+ container.scrollTop = container.scrollHeight;
115
+ });
116
+ }
117
+ }, [content, thinking, showDetail]);
118
+
119
+ // 展开时滚动到底部,便于查看最新内容
120
+ useEffect(() => {
121
+ if (!showDetail) return;
122
+ const container = contentRef.current;
123
+ if (!container) return;
124
+ requestAnimationFrame(() => {
125
+ container.scrollTop = container.scrollHeight;
126
+ });
127
+ }, [showDetail]);
128
+
94
129
  return (
95
130
  <Flexbox
96
131
  className={cx(styles.container, showDetail && styles.expand)}
@@ -145,7 +180,7 @@ const Thinking = memo<ThinkingProps>((props) => {
145
180
  animate="open"
146
181
  exit="collapsed"
147
182
  initial="collapsed"
148
- style={{ overflow: 'hidden', padding: 12 }}
183
+ style={{ overflow: 'hidden' }}
149
184
  transition={{
150
185
  duration: 0.2,
151
186
  ease: [0.4, 0, 0.2, 1], // 使用 ease-out 缓动函数
@@ -155,13 +190,18 @@ const Thinking = memo<ThinkingProps>((props) => {
155
190
  open: { opacity: 1, width: 'auto' },
156
191
  }}
157
192
  >
158
- {typeof content === 'string' ? (
159
- <Markdown animated={thinkingAnimated} citations={citations} variant={'chat'}>
160
- {content}
161
- </Markdown>
162
- ) : (
163
- content
164
- )}
193
+ <div
194
+ className={styles.contentScroll}
195
+ ref={contentRef as unknown as RefObject<HTMLDivElement>}
196
+ >
197
+ {typeof content === 'string' ? (
198
+ <Markdown animated={thinkingAnimated} citations={citations} variant={'chat'}>
199
+ {content}
200
+ </Markdown>
201
+ ) : (
202
+ content
203
+ )}
204
+ </div>
165
205
  </motion.div>
166
206
  )}
167
207
  </AnimatePresence>
@@ -11,8 +11,10 @@ const ai21ChatModels: AIChatModelCard[] = [
11
11
  enabled: true,
12
12
  id: 'jamba-mini',
13
13
  pricing: {
14
- input: 0.2,
15
- output: 0.4,
14
+ units: [
15
+ { name: 'textInput', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
16
+ { name: 'textOutput', rate: 0.4, strategy: 'fixed', unit: 'millionTokens' },
17
+ ],
16
18
  },
17
19
  releasedAt: '2025-03-06',
18
20
  type: 'chat',
@@ -27,8 +29,10 @@ const ai21ChatModels: AIChatModelCard[] = [
27
29
  enabled: true,
28
30
  id: 'jamba-large',
29
31
  pricing: {
30
- input: 2,
31
- output: 8,
32
+ units: [
33
+ { name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
34
+ { name: 'textOutput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
35
+ ],
32
36
  },
33
37
  releasedAt: '2025-03-06',
34
38
  type: 'chat',