@lobehub/chat 0.161.8 → 0.161.10

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 (74) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/docs/self-hosting/advanced/feature-flags.mdx +45 -0
  3. package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +42 -0
  4. package/docs/self-hosting/environment-variables/basic.mdx +11 -2
  5. package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +11 -2
  6. package/package.json +1 -1
  7. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +92 -42
  8. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
  9. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +4 -3
  10. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
  11. package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
  12. package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
  13. package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
  14. package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
  15. package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
  16. package/src/app/(main)/settings/system-agent/features/Translation.tsx +0 -2
  17. package/src/components/FileList/ImageFileItem.tsx +1 -1
  18. package/src/components/ModelProviderIcon/index.tsx +2 -2
  19. package/src/components/ModelSelect/index.tsx +5 -14
  20. package/src/const/meta.ts +1 -2
  21. package/src/features/User/UserPanel/PanelContent.tsx +1 -1
  22. package/src/hooks/useSyncData.ts +3 -1
  23. package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -1
  24. package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
  25. package/src/layout/GlobalProvider/index.tsx +1 -1
  26. package/src/locales/default/components.ts +1 -0
  27. package/src/services/message/client.test.ts +0 -24
  28. package/src/services/message/client.ts +0 -5
  29. package/src/services/message/type.ts +0 -1
  30. package/src/services/user/client.test.ts +100 -0
  31. package/src/services/user/client.ts +16 -14
  32. package/src/services/user/index.ts +0 -2
  33. package/src/services/user/type.ts +2 -4
  34. package/src/store/user/initialState.ts +10 -1
  35. package/src/store/user/selectors.ts +3 -7
  36. package/src/store/user/slices/auth/action.test.ts +5 -87
  37. package/src/store/user/slices/auth/action.ts +3 -58
  38. package/src/store/user/slices/auth/initialState.ts +2 -1
  39. package/src/store/user/slices/common/action.test.ts +196 -20
  40. package/src/store/user/slices/common/action.ts +55 -26
  41. package/src/store/user/slices/common/initialState.ts +9 -0
  42. package/src/store/user/slices/modelList/action.test.ts +363 -0
  43. package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
  44. package/src/store/user/slices/modelList/initialState.ts +15 -0
  45. package/src/store/user/slices/modelList/selectors/index.ts +2 -0
  46. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
  47. package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
  48. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
  49. package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
  50. package/src/store/user/slices/preference/action.test.ts +0 -52
  51. package/src/store/user/slices/preference/action.ts +1 -17
  52. package/src/store/user/slices/preference/initialState.ts +0 -5
  53. package/src/store/user/slices/preference/selectors.test.ts +2 -2
  54. package/src/store/user/slices/preference/selectors.ts +1 -1
  55. package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
  56. package/src/store/user/slices/settings/initialState.ts +0 -12
  57. package/src/store/user/slices/settings/selectors/index.ts +0 -3
  58. package/src/store/user/slices/sync/action.test.ts +19 -5
  59. package/src/store/user/slices/sync/action.ts +9 -6
  60. package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
  61. package/src/store/user/store.ts +5 -2
  62. package/src/styles/antdOverride.ts +6 -0
  63. package/src/types/serverConfig.ts +3 -1
  64. package/src/types/user/index.ts +13 -0
  65. package/src/utils/parseModels.test.ts +121 -1
  66. package/src/utils/parseModels.ts +9 -4
  67. package/src/store/user/slices/settings/actions/index.ts +0 -18
  68. package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
  69. /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
  70. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
  71. /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
  72. /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
  73. /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
  74. /package/src/store/user/slices/settings/selectors/{selectors.test.ts → settings.test.ts} +0 -0
@@ -0,0 +1,363 @@
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { modelsService } from '@/services/models';
5
+ import { userService } from '@/services/user';
6
+ import { useUserStore } from '@/store/user';
7
+ import { GeneralModelProviderConfig } from '@/types/settings';
8
+
9
+ import { settingsSelectors } from '../settings/selectors';
10
+ import { CustomModelCardDispatch } from './reducers/customModelCard';
11
+ import { modelProviderSelectors } from './selectors';
12
+
13
+ // Mock userService
14
+ vi.mock('@/services/user', () => ({
15
+ userService: {
16
+ updateUserSettings: vi.fn(),
17
+ resetUserSettings: vi.fn(),
18
+ },
19
+ }));
20
+
21
+ vi.mock('zustand/traditional');
22
+
23
+ describe('LLMSettingsSliceAction', () => {
24
+ describe('setModelProviderConfig', () => {
25
+ it('should set OpenAI configuration', async () => {
26
+ const { result } = renderHook(() => useUserStore());
27
+ const openAIConfig: Partial<GeneralModelProviderConfig> = { apiKey: 'test-key' };
28
+
29
+ // Perform the action
30
+ await act(async () => {
31
+ await result.current.setModelProviderConfig('openai', openAIConfig);
32
+ });
33
+
34
+ // Assert that updateUserSettings was called with the correct OpenAI configuration
35
+ expect(userService.updateUserSettings).toHaveBeenCalledWith({
36
+ languageModel: {
37
+ openai: openAIConfig,
38
+ },
39
+ });
40
+ });
41
+ });
42
+
43
+ describe('dispatchCustomModelCards', () => {
44
+ it('should return early when prevState does not exist', async () => {
45
+ const { result } = renderHook(() => useUserStore());
46
+ const provider = 'openai';
47
+ const payload: CustomModelCardDispatch = { type: 'add', modelCard: { id: 'test-id' } };
48
+
49
+ // Mock the selector to return undefined
50
+ vi.spyOn(settingsSelectors, 'providerConfig').mockReturnValueOnce(() => undefined);
51
+ vi.spyOn(result.current, 'setModelProviderConfig');
52
+
53
+ await act(async () => {
54
+ await result.current.dispatchCustomModelCards(provider, payload);
55
+ });
56
+
57
+ // Assert that setModelProviderConfig was not called
58
+ expect(result.current.setModelProviderConfig).not.toHaveBeenCalled();
59
+ });
60
+ });
61
+
62
+ describe('refreshDefaultModelProviderList', () => {
63
+ it('default', async () => {
64
+ const { result } = renderHook(() => useUserStore());
65
+
66
+ act(() => {
67
+ useUserStore.setState({
68
+ serverLanguageModel: {
69
+ azure: { serverModelCards: [{ id: 'abc', deploymentName: 'abc' }] },
70
+ },
71
+ });
72
+ });
73
+
74
+ act(() => {
75
+ result.current.refreshDefaultModelProviderList();
76
+ });
77
+
78
+ // Assert that setModelProviderConfig was not called
79
+ const azure = result.current.defaultModelProviderList.find((m) => m.id === 'azure');
80
+ expect(azure?.chatModels).toEqual([{ id: 'abc', deploymentName: 'abc' }]);
81
+ });
82
+
83
+ it('openai', () => {
84
+ const { result } = renderHook(() => useUserStore());
85
+ act(() => {
86
+ useUserStore.setState({
87
+ serverLanguageModel: {
88
+ openai: {
89
+ enabled: true,
90
+ enabledModels: ['gpt-4-0125-preview', 'gpt-4-turbo-2024-04-09'],
91
+ serverModelCards: [
92
+ {
93
+ displayName: 'ChatGPT-4',
94
+ functionCall: true,
95
+ id: 'gpt-4-0125-preview',
96
+ tokens: 128000,
97
+ enabled: true,
98
+ },
99
+ {
100
+ displayName: 'ChatGPT-4 Vision',
101
+ functionCall: true,
102
+ id: 'gpt-4-turbo-2024-04-09',
103
+ tokens: 128000,
104
+ vision: true,
105
+ enabled: true,
106
+ },
107
+ ],
108
+ },
109
+ },
110
+ });
111
+ });
112
+
113
+ act(() => {
114
+ result.current.refreshDefaultModelProviderList();
115
+ });
116
+
117
+ // Assert that setModelProviderConfig was not called
118
+ const openai = result.current.defaultModelProviderList.find((m) => m.id === 'openai');
119
+ expect(openai?.chatModels).toEqual([
120
+ {
121
+ displayName: 'ChatGPT-4',
122
+ enabled: true,
123
+ functionCall: true,
124
+ id: 'gpt-4-0125-preview',
125
+ tokens: 128000,
126
+ },
127
+ {
128
+ displayName: 'ChatGPT-4 Vision',
129
+ enabled: true,
130
+ functionCall: true,
131
+ id: 'gpt-4-turbo-2024-04-09',
132
+ tokens: 128000,
133
+ vision: true,
134
+ },
135
+ ]);
136
+ });
137
+ });
138
+
139
+ describe('refreshModelProviderList', () => {
140
+ it('visible', async () => {
141
+ const { result } = renderHook(() => useUserStore());
142
+ act(() => {
143
+ useUserStore.setState({
144
+ settings: {
145
+ languageModel: {
146
+ ollama: { enabledModels: ['llava'] },
147
+ },
148
+ },
149
+ });
150
+ });
151
+
152
+ act(() => {
153
+ result.current.refreshModelProviderList();
154
+ });
155
+
156
+ const ollamaList = result.current.modelProviderList.find((r) => r.id === 'ollama');
157
+ // Assert that setModelProviderConfig was not called
158
+ expect(ollamaList?.chatModels.find((c) => c.id === 'llava')).toEqual({
159
+ displayName: 'LLaVA 7B',
160
+ enabled: true,
161
+ id: 'llava',
162
+ tokens: 4096,
163
+ vision: true,
164
+ });
165
+ });
166
+
167
+ it('modelProviderListForModelSelect should return only enabled providers', () => {
168
+ const { result } = renderHook(() => useUserStore());
169
+
170
+ act(() => {
171
+ useUserStore.setState({
172
+ settings: {
173
+ languageModel: {
174
+ perplexity: { enabled: true },
175
+ azure: { enabled: false },
176
+ },
177
+ },
178
+ });
179
+ });
180
+
181
+ act(() => {
182
+ result.current.refreshModelProviderList();
183
+ });
184
+
185
+ const enabledProviders = modelProviderSelectors.modelProviderListForModelSelect(
186
+ result.current,
187
+ );
188
+ expect(enabledProviders).toHaveLength(3);
189
+ expect(enabledProviders.at(-1)!.id).toBe('perplexity');
190
+ });
191
+ });
192
+
193
+ describe('removeEnabledModels', () => {
194
+ it('should remove the specified model from enabledModels', async () => {
195
+ const { result } = renderHook(() => useUserStore());
196
+ const model = 'gpt-3.5-turbo';
197
+
198
+ const spyOn = vi.spyOn(userService, 'updateUserSettings');
199
+
200
+ act(() => {
201
+ useUserStore.setState({
202
+ settings: {
203
+ languageModel: {
204
+ azure: { enabledModels: ['gpt-3.5-turbo', 'gpt-4'] },
205
+ },
206
+ },
207
+ });
208
+ });
209
+
210
+ await act(async () => {
211
+ console.log(JSON.stringify(result.current.settings));
212
+ await result.current.removeEnabledModels('azure', model);
213
+ });
214
+
215
+ expect(spyOn).toHaveBeenCalledWith({
216
+ languageModel: {
217
+ azure: { enabledModels: ['gpt-4'] },
218
+ },
219
+ });
220
+ });
221
+ });
222
+
223
+ describe('toggleEditingCustomModelCard', () => {
224
+ it('should update editingCustomCardModel when params are provided', () => {
225
+ const { result } = renderHook(() => useUserStore());
226
+
227
+ act(() => {
228
+ result.current.toggleEditingCustomModelCard({ id: 'test-id', provider: 'openai' });
229
+ });
230
+
231
+ expect(result.current.editingCustomCardModel).toEqual({ id: 'test-id', provider: 'openai' });
232
+ });
233
+
234
+ it('should reset editingCustomCardModel when no params are provided', () => {
235
+ const { result } = renderHook(() => useUserStore());
236
+
237
+ act(() => {
238
+ result.current.toggleEditingCustomModelCard();
239
+ });
240
+
241
+ expect(result.current.editingCustomCardModel).toBeUndefined();
242
+ });
243
+ });
244
+
245
+ describe('toggleProviderEnabled', () => {
246
+ it('should enable the provider', async () => {
247
+ const { result } = renderHook(() => useUserStore());
248
+
249
+ await act(async () => {
250
+ await result.current.toggleProviderEnabled('minimax', true);
251
+ });
252
+
253
+ expect(userService.updateUserSettings).toHaveBeenCalledWith({
254
+ languageModel: {
255
+ minimax: { enabled: true },
256
+ },
257
+ });
258
+ });
259
+
260
+ it('should disable the provider', async () => {
261
+ const { result } = renderHook(() => useUserStore());
262
+ const provider = 'openai';
263
+
264
+ await act(async () => {
265
+ await result.current.toggleProviderEnabled(provider, false);
266
+ });
267
+
268
+ expect(userService.updateUserSettings).toHaveBeenCalledWith({
269
+ languageModel: {
270
+ openai: { enabled: false },
271
+ },
272
+ });
273
+ });
274
+ });
275
+
276
+ describe('updateEnabledModels', () => {
277
+ // TODO: 有待 updateEnabledModels 实现的同步改造
278
+ it('should add new custom model to customModelCards', async () => {
279
+ const { result } = renderHook(() => useUserStore());
280
+ const provider = 'openai';
281
+ const modelKeys = ['gpt-3.5-turbo', 'custom-model'];
282
+ const options = [{ value: 'gpt-3.5-turbo' }, {}];
283
+
284
+ await act(async () => {
285
+ await result.current.updateEnabledModels(provider, modelKeys, options);
286
+ });
287
+
288
+ expect(userService.updateUserSettings).toHaveBeenCalledWith({
289
+ languageModel: {
290
+ openai: {
291
+ customModelCards: [{ id: 'custom-model' }],
292
+ // TODO:目标单测中需要包含下面这一行
293
+ // enabledModels: ['gpt-3.5-turbo', 'custom-model'],
294
+ },
295
+ },
296
+ });
297
+ });
298
+
299
+ it('should not add removed model to customModelCards', async () => {
300
+ const { result } = renderHook(() => useUserStore());
301
+ const provider = 'openai';
302
+ const modelKeys = ['gpt-3.5-turbo'];
303
+ const options = [{ value: 'gpt-3.5-turbo' }];
304
+
305
+ act(() => {
306
+ useUserStore.setState({
307
+ settings: {
308
+ languageModel: {
309
+ openai: { enabledModels: ['gpt-3.5-turbo', 'gpt-4'] },
310
+ },
311
+ },
312
+ });
313
+ });
314
+
315
+ await act(async () => {
316
+ await result.current.updateEnabledModels(provider, modelKeys, options);
317
+ });
318
+
319
+ expect(userService.updateUserSettings).toHaveBeenCalledWith({
320
+ languageModel: {
321
+ openai: { enabledModels: ['gpt-3.5-turbo'] },
322
+ },
323
+ });
324
+ });
325
+ });
326
+
327
+ describe('useFetchProviderModelList', () => {
328
+ it('should fetch data when enabledAutoFetch is true', async () => {
329
+ const { result } = renderHook(() => useUserStore());
330
+ const provider = 'openai';
331
+ const enabledAutoFetch = true;
332
+
333
+ const spyOn = vi.spyOn(result.current, 'refreshDefaultModelProviderList');
334
+
335
+ vi.spyOn(modelsService, 'getChatModels').mockResolvedValueOnce([]);
336
+
337
+ renderHook(() => result.current.useFetchProviderModelList(provider, enabledAutoFetch));
338
+
339
+ await waitFor(() => {
340
+ expect(spyOn).toHaveBeenCalled();
341
+ });
342
+
343
+ // expect(result.current.settings.languageModel.openai?.latestFetchTime).toBeDefined();
344
+ // expect(result.current.settings.languageModel.openai?.remoteModelCards).toBeDefined();
345
+ });
346
+
347
+ it('should not fetch data when enabledAutoFetch is false', async () => {
348
+ const { result } = renderHook(() => useUserStore());
349
+ const provider = 'openai';
350
+ const enabledAutoFetch = false;
351
+
352
+ const spyOn = vi.spyOn(result.current, 'refreshDefaultModelProviderList');
353
+
354
+ vi.spyOn(modelsService, 'getChatModels').mockResolvedValueOnce([]);
355
+
356
+ renderHook(() => result.current.useFetchProviderModelList(provider, enabledAutoFetch));
357
+
358
+ await waitFor(() => {
359
+ expect(spyOn).not.toHaveBeenCalled();
360
+ });
361
+ });
362
+ });
363
+ });
@@ -1,39 +1,21 @@
1
+ import { produce } from 'immer';
1
2
  import useSWR, { SWRResponse } from 'swr';
2
3
  import type { StateCreator } from 'zustand/vanilla';
3
4
 
4
- import {
5
- AnthropicProviderCard,
6
- AzureProviderCard,
7
- BedrockProviderCard,
8
- DeepSeekProviderCard,
9
- GoogleProviderCard,
10
- GroqProviderCard,
11
- MinimaxProviderCard,
12
- MistralProviderCard,
13
- MoonshotProviderCard,
14
- OllamaProviderCard,
15
- OpenAIProviderCard,
16
- OpenRouterProviderCard,
17
- PerplexityProviderCard,
18
- TogetherAIProviderCard,
19
- ZeroOneProviderCard,
20
- ZhiPuProviderCard,
21
- } from '@/config/modelProviders';
5
+ import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
6
+ import { ModelProvider } from '@/libs/agent-runtime';
22
7
  import { UserStore } from '@/store/user';
23
8
  import { ChatModelCard } from '@/types/llm';
24
9
  import { GlobalLLMConfig, GlobalLLMProviderKey } from '@/types/settings';
25
- import { setNamespace } from '@/utils/storeDebug';
26
10
 
27
- import { CustomModelCardDispatch, customModelCardsReducer } from '../reducers/customModelCard';
28
- import { modelProviderSelectors } from '../selectors/modelProvider';
29
- import { settingsSelectors } from '../selectors/settings';
30
-
31
- const n = setNamespace('settings');
11
+ import { settingsSelectors } from '../settings/selectors';
12
+ import { CustomModelCardDispatch, customModelCardsReducer } from './reducers/customModelCard';
13
+ import { modelProviderSelectors } from './selectors/modelProvider';
32
14
 
33
15
  /**
34
16
  * 设置操作
35
17
  */
36
- export interface LLMSettingsAction {
18
+ export interface ModelListAction {
37
19
  dispatchCustomModelCards: (
38
20
  provider: GlobalLLMProviderKey,
39
21
  payload: CustomModelCardDispatch,
@@ -48,21 +30,28 @@ export interface LLMSettingsAction {
48
30
  provider: T,
49
31
  config: Partial<GlobalLLMConfig[T]>,
50
32
  ) => Promise<void>;
33
+
51
34
  toggleEditingCustomModelCard: (params?: { id: string; provider: GlobalLLMProviderKey }) => void;
52
35
 
53
36
  toggleProviderEnabled: (provider: GlobalLLMProviderKey, enabled: boolean) => Promise<void>;
54
37
 
38
+ updateEnabledModels: (
39
+ provider: GlobalLLMProviderKey,
40
+ modelKeys: string[],
41
+ options: { label?: string; value?: string }[],
42
+ ) => Promise<void>;
43
+
55
44
  useFetchProviderModelList: (
56
45
  provider: GlobalLLMProviderKey,
57
46
  enabledAutoFetch: boolean,
58
47
  ) => SWRResponse;
59
48
  }
60
49
 
61
- export const llmSettingsSlice: StateCreator<
50
+ export const createModelListSlice: StateCreator<
62
51
  UserStore,
63
52
  [['zustand/devtools', never]],
64
53
  [],
65
- LLMSettingsAction
54
+ ModelListAction
66
55
  > = (set, get) => ({
67
56
  dispatchCustomModelCards: async (provider, payload) => {
68
57
  const prevState = settingsSelectors.providerConfig(provider)(get());
@@ -73,7 +62,6 @@ export const llmSettingsSlice: StateCreator<
73
62
 
74
63
  await get().setModelProviderConfig(provider, { customModelCards: nextState });
75
64
  },
76
-
77
65
  refreshDefaultModelProviderList: (params) => {
78
66
  /**
79
67
  * Because we have several model cards sources, we need to merge the model cards
@@ -92,39 +80,27 @@ export const llmSettingsSlice: StateCreator<
92
80
  return serverChatModels ?? remoteChatModels ?? defaultChatModels;
93
81
  };
94
82
 
95
- const defaultModelProviderList = [
96
- {
97
- ...OpenAIProviderCard,
98
- chatModels: mergeModels('openai', OpenAIProviderCard.chatModels),
99
- },
100
- { ...AzureProviderCard, chatModels: mergeModels('azure', []) },
101
- { ...OllamaProviderCard, chatModels: mergeModels('ollama', OllamaProviderCard.chatModels) },
102
- AnthropicProviderCard,
103
- GoogleProviderCard,
104
- {
105
- ...OpenRouterProviderCard,
106
- chatModels: mergeModels('openrouter', OpenRouterProviderCard.chatModels),
107
- },
108
- {
109
- ...TogetherAIProviderCard,
110
- chatModels: mergeModels('togetherai', TogetherAIProviderCard.chatModels),
111
- },
112
- BedrockProviderCard,
113
- DeepSeekProviderCard,
114
- PerplexityProviderCard,
115
- MinimaxProviderCard,
116
- MistralProviderCard,
117
- GroqProviderCard,
118
- MoonshotProviderCard,
119
- ZeroOneProviderCard,
120
- ZhiPuProviderCard,
121
- ];
122
-
123
- set({ defaultModelProviderList }, false, n(`refreshDefaultModelList - ${params?.trigger}`));
83
+ const defaultModelProviderList = produce(DEFAULT_MODEL_PROVIDER_LIST, (draft) => {
84
+ const openai = draft.find((d) => d.id === ModelProvider.OpenAI);
85
+ if (openai) openai.chatModels = mergeModels('openai', openai.chatModels);
86
+
87
+ const azure = draft.find((d) => d.id === ModelProvider.Azure);
88
+ if (azure) azure.chatModels = mergeModels('azure', azure.chatModels);
89
+
90
+ const ollama = draft.find((d) => d.id === ModelProvider.Ollama);
91
+ if (ollama) ollama.chatModels = mergeModels('ollama', ollama.chatModels);
92
+
93
+ const openrouter = draft.find((d) => d.id === ModelProvider.OpenRouter);
94
+ if (openrouter) openrouter.chatModels = mergeModels('openrouter', openrouter.chatModels);
95
+
96
+ const togetherai = draft.find((d) => d.id === ModelProvider.TogetherAI);
97
+ if (togetherai) togetherai.chatModels = mergeModels('togetherai', togetherai.chatModels);
98
+ });
99
+
100
+ set({ defaultModelProviderList }, false, `refreshDefaultModelList - ${params?.trigger}`);
124
101
 
125
102
  get().refreshModelProviderList({ trigger: 'refreshDefaultModelList' });
126
103
  },
127
-
128
104
  refreshModelProviderList: (params) => {
129
105
  const modelProviderList = get().defaultModelProviderList.map((list) => ({
130
106
  ...list,
@@ -143,7 +119,7 @@ export const llmSettingsSlice: StateCreator<
143
119
  enabled: modelProviderSelectors.isProviderEnabled(list.id as any)(get()),
144
120
  }));
145
121
 
146
- set({ modelProviderList }, false, n(`refreshModelList - ${params?.trigger}`));
122
+ set({ modelProviderList }, false, `refreshModelList - ${params?.trigger}`);
147
123
  },
148
124
 
149
125
  removeEnabledModels: async (provider, model) => {
@@ -157,14 +133,44 @@ export const llmSettingsSlice: StateCreator<
157
133
  setModelProviderConfig: async (provider, config) => {
158
134
  await get().setSettings({ languageModel: { [provider]: config } });
159
135
  },
136
+
160
137
  toggleEditingCustomModelCard: (params) => {
161
138
  set({ editingCustomCardModel: params }, false, 'toggleEditingCustomModelCard');
162
139
  },
163
-
164
140
  toggleProviderEnabled: async (provider, enabled) => {
165
141
  await get().setSettings({ languageModel: { [provider]: { enabled } } });
166
142
  },
167
143
 
144
+ updateEnabledModels: async (provider, value, options) => {
145
+ const { dispatchCustomModelCards, setModelProviderConfig } = get();
146
+ const enabledModels = modelProviderSelectors.getEnableModelsById(provider)(get());
147
+
148
+ // if there is a new model, add it to `customModelCards`
149
+ const pools = options.map(async (option: { label?: string; value?: string }, index: number) => {
150
+ // if is a known model, it should have value
151
+ // if is an unknown model, the option will be {}
152
+ if (option.value) return;
153
+
154
+ const modelId = value[index];
155
+
156
+ // if is in enabledModels, it means it's a removed model
157
+ if (enabledModels?.some((m) => modelId === m)) return;
158
+
159
+ await dispatchCustomModelCards(provider, {
160
+ modelCard: { id: modelId },
161
+ type: 'add',
162
+ });
163
+ });
164
+
165
+ // TODO: 当前的这个 pool 方法并不是最好的实现,因为它会触发 setModelProviderConfig 的多次更新。
166
+ // 理论上应该合并这些变更,然后最后只做一次触发
167
+ // 因此后续的做法应该是将 dispatchCustomModelCards 改造为同步方法,并在最后做一次异步更新
168
+ // 对应需要改造 'should add new custom model to customModelCards' 这一个单测
169
+ await Promise.all(pools);
170
+
171
+ await setModelProviderConfig(provider, { enabledModels: value.filter(Boolean) });
172
+ },
173
+
168
174
  useFetchProviderModelList: (provider, enabledAutoFetch) =>
169
175
  useSWR<ChatModelCard[] | undefined>(
170
176
  [provider, enabledAutoFetch],
@@ -0,0 +1,15 @@
1
+ import { DEFAULT_MODEL_PROVIDER_LIST } from '@/config/modelProviders';
2
+ import { ModelProviderCard } from '@/types/llm';
3
+ import { ServerLanguageModel } from '@/types/serverConfig';
4
+
5
+ export interface ModelListState {
6
+ defaultModelProviderList: ModelProviderCard[];
7
+ editingCustomCardModel?: { id: string; provider: string } | undefined;
8
+ modelProviderList: ModelProviderCard[];
9
+ serverLanguageModel?: ServerLanguageModel;
10
+ }
11
+
12
+ export const initialModelListState: ModelListState = {
13
+ defaultModelProviderList: DEFAULT_MODEL_PROVIDER_LIST,
14
+ modelProviderList: DEFAULT_MODEL_PROVIDER_LIST,
15
+ };
@@ -0,0 +1,2 @@
1
+ export { modelConfigSelectors } from './modelConfig';
2
+ export { modelProviderSelectors } from './modelProvider';
@@ -3,7 +3,8 @@ import { describe, expect, it } from 'vitest';
3
3
  import { UserStore } from '@/store/user';
4
4
  import { merge } from '@/utils/merge';
5
5
 
6
- import { UserSettingsState, initialSettingsState } from '../initialState';
6
+ import { UserState } from '../../../initialState';
7
+ import { UserSettingsState, initialSettingsState } from '../../settings/initialState';
7
8
  import { modelConfigSelectors } from './modelConfig';
8
9
 
9
10
  describe('modelConfigSelectors', () => {
@@ -121,7 +122,7 @@ describe('modelConfigSelectors', () => {
121
122
  id: 'custom-model-2',
122
123
  provider: 'perplexity',
123
124
  },
124
- } as UserSettingsState) as unknown as UserStore;
125
+ } as UserState) as unknown as UserStore;
125
126
 
126
127
  const currentEditingModelCard = modelConfigSelectors.currentEditingCustomModelCard(s);
127
128
 
@@ -1,7 +1,7 @@
1
1
  import { GlobalLLMProviderKey } from '@/types/settings';
2
2
 
3
3
  import { UserStore } from '../../../store';
4
- import { currentLLMSettings, getProviderConfigById } from './settings';
4
+ import { currentLLMSettings, getProviderConfigById } from '../../settings/selectors/settings';
5
5
 
6
6
  const isProviderEnabled = (provider: GlobalLLMProviderKey) => (s: UserStore) =>
7
7
  getProviderConfigById(provider)(s)?.enabled || false;
@@ -2,21 +2,21 @@ import { describe, expect, it } from 'vitest';
2
2
 
3
3
  import { merge } from '@/utils/merge';
4
4
 
5
+ import { UserState, initialState } from '../../../initialState';
5
6
  import { UserStore, useUserStore } from '../../../store';
6
- import { UserSettingsState, initialSettingsState } from '../initialState';
7
7
  import { getDefaultModeProviderById, modelProviderSelectors } from './modelProvider';
8
8
 
9
9
  describe('modelProviderSelectors', () => {
10
10
  describe('getDefaultModeProviderById', () => {
11
11
  it('should return the correct ModelProviderCard when provider ID matches', () => {
12
- const s = merge(initialSettingsState, {}) as unknown as UserStore;
12
+ const s = merge(initialState, {}) as unknown as UserStore;
13
13
 
14
14
  const result = getDefaultModeProviderById('openai')(s);
15
15
  expect(result).not.toBeUndefined();
16
16
  });
17
17
 
18
18
  it('should return undefined when provider ID does not exist', () => {
19
- const s = merge(initialSettingsState, {}) as unknown as UserStore;
19
+ const s = merge(initialState, {}) as unknown as UserStore;
20
20
  const result = getDefaultModeProviderById('nonExistingProvider')(s);
21
21
  expect(result).toBeUndefined();
22
22
  });
@@ -24,7 +24,7 @@ describe('modelProviderSelectors', () => {
24
24
 
25
25
  describe('getModelCardsById', () => {
26
26
  it('should return model cards including custom model cards', () => {
27
- const s = merge(initialSettingsState, {
27
+ const s = merge(initialState, {
28
28
  settings: {
29
29
  languageModel: {
30
30
  perplexity: {
@@ -32,7 +32,7 @@ describe('modelProviderSelectors', () => {
32
32
  },
33
33
  },
34
34
  },
35
- } as UserSettingsState) as unknown as UserStore;
35
+ } as UserState) as unknown as UserStore;
36
36
 
37
37
  const modelCards = modelProviderSelectors.getModelCardsById('perplexity')(s);
38
38
 
@@ -46,14 +46,14 @@ describe('modelProviderSelectors', () => {
46
46
 
47
47
  describe('defaultEnabledProviderModels', () => {
48
48
  it('should return enabled models for a given provider', () => {
49
- const s = merge(initialSettingsState, {}) as unknown as UserStore;
49
+ const s = merge(initialState, {}) as unknown as UserStore;
50
50
 
51
51
  const result = modelProviderSelectors.getDefaultEnabledModelsById('openai')(s);
52
52
  expect(result).toEqual(['gpt-3.5-turbo', 'gpt-4-turbo', 'gpt-4o']);
53
53
  });
54
54
 
55
55
  it('should return undefined for a non-existing provider', () => {
56
- const s = merge(initialSettingsState, {}) as unknown as UserStore;
56
+ const s = merge(initialState, {}) as unknown as UserStore;
57
57
 
58
58
  const result = modelProviderSelectors.getDefaultEnabledModelsById('nonExistingProvider')(s);
59
59
  expect(result).toBeUndefined();
@@ -6,7 +6,7 @@ import { ServerModelProviderConfig } from '@/types/serverConfig';
6
6
  import { GlobalLLMProviderKey } from '@/types/settings';
7
7
 
8
8
  import { UserStore } from '../../../store';
9
- import { currentSettings, getProviderConfigById } from './settings';
9
+ import { currentSettings, getProviderConfigById } from '../../settings/selectors/settings';
10
10
 
11
11
  /**
12
12
  * get the server side model cards
@@ -14,9 +14,7 @@ import { currentSettings, getProviderConfigById } from './settings';
14
14
  const serverProviderModelCards =
15
15
  (provider: GlobalLLMProviderKey) =>
16
16
  (s: UserStore): ChatModelCard[] | undefined => {
17
- const config = s.serverConfig.languageModel?.[provider] as
18
- | ServerModelProviderConfig
19
- | undefined;
17
+ const config = s.serverLanguageModel?.[provider] as ServerModelProviderConfig | undefined;
20
18
 
21
19
  if (!config) return;
22
20