@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.
- package/CHANGELOG.md +50 -0
- package/docs/self-hosting/advanced/feature-flags.mdx +45 -0
- package/docs/self-hosting/advanced/feature-flags.zh-CN.mdx +42 -0
- package/docs/self-hosting/environment-variables/basic.mdx +11 -2
- package/docs/self-hosting/environment-variables/basic.zh-CN.mdx +11 -2
- package/package.json +1 -1
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/DragUpload.tsx +92 -42
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +2 -2
- package/src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files.tsx +4 -3
- package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +5 -2
- package/src/app/(main)/settings/llm/components/ProviderModelList/CustomModelOption.tsx +6 -7
- package/src/app/(main)/settings/llm/components/ProviderModelList/{ModelConfigModal.tsx → ModelConfigModal/Form.tsx} +19 -63
- package/src/app/(main)/settings/llm/components/ProviderModelList/ModelConfigModal/index.tsx +78 -0
- package/src/app/(main)/settings/llm/components/ProviderModelList/Option.tsx +35 -11
- package/src/app/(main)/settings/llm/components/ProviderModelList/index.tsx +15 -18
- package/src/app/(main)/settings/system-agent/features/Translation.tsx +0 -2
- package/src/components/FileList/ImageFileItem.tsx +1 -1
- package/src/components/ModelProviderIcon/index.tsx +2 -2
- package/src/components/ModelSelect/index.tsx +5 -14
- package/src/const/meta.ts +1 -2
- package/src/features/User/UserPanel/PanelContent.tsx +1 -1
- package/src/hooks/useSyncData.ts +3 -1
- package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -1
- package/src/layout/GlobalProvider/StoreInitialization.tsx +17 -9
- package/src/layout/GlobalProvider/index.tsx +1 -1
- package/src/locales/default/components.ts +1 -0
- package/src/services/message/client.test.ts +0 -24
- package/src/services/message/client.ts +0 -5
- package/src/services/message/type.ts +0 -1
- package/src/services/user/client.test.ts +100 -0
- package/src/services/user/client.ts +16 -14
- package/src/services/user/index.ts +0 -2
- package/src/services/user/type.ts +2 -4
- package/src/store/user/initialState.ts +10 -1
- package/src/store/user/selectors.ts +3 -7
- package/src/store/user/slices/auth/action.test.ts +5 -87
- package/src/store/user/slices/auth/action.ts +3 -58
- package/src/store/user/slices/auth/initialState.ts +2 -1
- package/src/store/user/slices/common/action.test.ts +196 -20
- package/src/store/user/slices/common/action.ts +55 -26
- package/src/store/user/slices/common/initialState.ts +9 -0
- package/src/store/user/slices/modelList/action.test.ts +363 -0
- package/src/store/user/slices/{settings/actions/llm.ts → modelList/action.ts} +66 -60
- package/src/store/user/slices/modelList/initialState.ts +15 -0
- package/src/store/user/slices/modelList/selectors/index.ts +2 -0
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.test.ts +3 -2
- package/src/store/user/slices/{settings → modelList}/selectors/modelConfig.ts +1 -1
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.test.ts +7 -7
- package/src/store/user/slices/{settings → modelList}/selectors/modelProvider.ts +2 -4
- package/src/store/user/slices/preference/action.test.ts +0 -52
- package/src/store/user/slices/preference/action.ts +1 -17
- package/src/store/user/slices/preference/initialState.ts +0 -5
- package/src/store/user/slices/preference/selectors.test.ts +2 -2
- package/src/store/user/slices/preference/selectors.ts +1 -1
- package/src/store/user/slices/settings/{actions/general.ts → action.ts} +5 -5
- package/src/store/user/slices/settings/initialState.ts +0 -12
- package/src/store/user/slices/settings/selectors/index.ts +0 -3
- package/src/store/user/slices/sync/action.test.ts +19 -5
- package/src/store/user/slices/sync/action.ts +9 -6
- package/src/store/user/slices/{settings/selectors/sync.ts → sync/selectors.ts} +2 -2
- package/src/store/user/store.ts +5 -2
- package/src/styles/antdOverride.ts +6 -0
- package/src/types/serverConfig.ts +3 -1
- package/src/types/user/index.ts +13 -0
- package/src/utils/parseModels.test.ts +121 -1
- package/src/utils/parseModels.ts +9 -4
- package/src/store/user/slices/settings/actions/index.ts +0 -18
- package/src/store/user/slices/settings/actions/llm.test.ts +0 -136
- /package/src/app/(main)/settings/llm/components/ProviderModelList/{MaxTokenSlider.tsx → ModelConfigModal/MaxTokenSlider.tsx} +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.test.ts +0 -0
- /package/src/store/user/slices/{settings → modelList}/reducers/customModelCard.ts +0 -0
- /package/src/store/user/slices/settings/{actions/general.test.ts → action.test.ts} +0 -0
- /package/src/store/user/slices/settings/selectors/__snapshots__/{selectors.test.ts.snap → settings.test.ts.snap} +0 -0
- /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
|
-
|
|
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 {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
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
|
|
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
|
|
50
|
+
export const createModelListSlice: StateCreator<
|
|
62
51
|
UserStore,
|
|
63
52
|
[['zustand/devtools', never]],
|
|
64
53
|
[],
|
|
65
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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,
|
|
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
|
+
};
|
|
@@ -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 {
|
|
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
|
|
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 '
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 '
|
|
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.
|
|
18
|
-
| ServerModelProviderConfig
|
|
19
|
-
| undefined;
|
|
17
|
+
const config = s.serverLanguageModel?.[provider] as ServerModelProviderConfig | undefined;
|
|
20
18
|
|
|
21
19
|
if (!config) return;
|
|
22
20
|
|