@lobehub/chat 1.53.7 → 1.53.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/self-hosting/advanced/model-list.mdx +9 -7
  4. package/docs/self-hosting/advanced/model-list.zh-CN.mdx +9 -7
  5. package/locales/ar/modelProvider.json +5 -0
  6. package/locales/bg-BG/modelProvider.json +5 -0
  7. package/locales/de-DE/modelProvider.json +5 -0
  8. package/locales/en-US/modelProvider.json +5 -0
  9. package/locales/es-ES/modelProvider.json +5 -0
  10. package/locales/fa-IR/modelProvider.json +5 -0
  11. package/locales/fr-FR/modelProvider.json +5 -0
  12. package/locales/it-IT/modelProvider.json +5 -0
  13. package/locales/ja-JP/modelProvider.json +5 -0
  14. package/locales/ko-KR/modelProvider.json +5 -0
  15. package/locales/nl-NL/modelProvider.json +5 -0
  16. package/locales/pl-PL/modelProvider.json +5 -0
  17. package/locales/pt-BR/modelProvider.json +5 -0
  18. package/locales/ru-RU/modelProvider.json +5 -0
  19. package/locales/tr-TR/modelProvider.json +5 -0
  20. package/locales/vi-VN/modelProvider.json +5 -0
  21. package/locales/zh-CN/auth.json +1 -1
  22. package/locales/zh-CN/changelog.json +1 -1
  23. package/locales/zh-CN/chat.json +1 -1
  24. package/locales/zh-CN/clerk.json +1 -1
  25. package/locales/zh-CN/common.json +1 -1
  26. package/locales/zh-CN/components.json +1 -1
  27. package/locales/zh-CN/discover.json +1 -1
  28. package/locales/zh-CN/error.json +1 -1
  29. package/locales/zh-CN/file.json +1 -1
  30. package/locales/zh-CN/knowledgeBase.json +1 -1
  31. package/locales/zh-CN/metadata.json +1 -1
  32. package/locales/zh-CN/migration.json +1 -1
  33. package/locales/zh-CN/modelProvider.json +6 -1
  34. package/locales/zh-CN/models.json +1049 -1049
  35. package/locales/zh-CN/plugin.json +1 -1
  36. package/locales/zh-CN/portal.json +1 -1
  37. package/locales/zh-CN/providers.json +70 -70
  38. package/locales/zh-CN/ragEval.json +1 -1
  39. package/locales/zh-CN/setting.json +1 -1
  40. package/locales/zh-CN/thread.json +1 -1
  41. package/locales/zh-CN/tool.json +1 -1
  42. package/locales/zh-CN/topic.json +1 -1
  43. package/locales/zh-CN/welcome.json +1 -1
  44. package/locales/zh-TW/modelProvider.json +5 -0
  45. package/package.json +1 -1
  46. package/src/app/[variants]/(main)/settings/provider/(detail)/azure/page.tsx +4 -8
  47. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +67 -61
  48. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelTitle/index.tsx +37 -10
  49. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ProviderSettingsContext.ts +9 -0
  50. package/src/app/[variants]/(main)/settings/provider/features/ModelList/index.tsx +27 -17
  51. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +47 -26
  52. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +5 -1
  53. package/src/config/modelProviders/spark.ts +2 -0
  54. package/src/const/version.ts +1 -2
  55. package/src/database/server/models/aiModel.ts +6 -0
  56. package/src/locales/default/modelProvider.ts +5 -0
  57. package/src/server/routers/lambda/aiModel.ts +5 -0
  58. package/src/services/aiModel/client.ts +4 -0
  59. package/src/services/aiModel/server.test.ts +122 -0
  60. package/src/services/aiModel/server.ts +4 -0
  61. package/src/services/aiModel/type.ts +2 -0
  62. package/src/store/aiInfra/slices/aiModel/action.ts +5 -0
  63. package/src/store/aiInfra/slices/aiModel/selectors.ts +3 -0
  64. package/src/types/aiProvider.ts +7 -1
@@ -1,18 +1,19 @@
1
1
  'use client';
2
2
 
3
3
  import { CheckCircleFilled } from '@ant-design/icons';
4
- import { Alert, Highlighter } from '@lobehub/ui';
5
- import { Button } from 'antd';
4
+ import { ModelIcon } from '@lobehub/icons';
5
+ import { Alert, Highlighter, Icon } from '@lobehub/ui';
6
+ import { Button, Select, Space } from 'antd';
6
7
  import { useTheme } from 'antd-style';
8
+ import { Loader2Icon } from 'lucide-react';
7
9
  import { ReactNode, memo, useState } from 'react';
8
10
  import { useTranslation } from 'react-i18next';
9
11
  import { Flexbox } from 'react-layout-kit';
10
12
 
11
13
  import { TraceNameMap } from '@/const/trace';
12
- import { useIsMobile } from '@/hooks/useIsMobile';
13
14
  import { useProviderName } from '@/hooks/useProviderName';
14
15
  import { chatService } from '@/services/chat';
15
- import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
16
+ import { aiModelSelectors, aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra';
16
17
  import { ChatMessageError } from '@/types/message';
17
18
 
18
19
  const Error = memo<{ error: ChatMessageError }>(({ error }) => {
@@ -20,9 +21,8 @@ const Error = memo<{ error: ChatMessageError }>(({ error }) => {
20
21
  const providerName = useProviderName(error.body?.provider);
21
22
 
22
23
  return (
23
- <Flexbox gap={8} style={{ maxWidth: '600px', width: '100%' }}>
24
+ <Flexbox gap={8} style={{ width: '100%' }}>
24
25
  <Alert
25
- banner
26
26
  extra={
27
27
  <Flexbox>
28
28
  <Highlighter copyButtonSize={'small'} language={'json'} type={'pure'}>
@@ -54,10 +54,15 @@ const Checker = memo<ConnectionCheckerProps>(
54
54
  ({ model, provider, checkErrorRender: CheckErrorRender }) => {
55
55
  const { t } = useTranslation('setting');
56
56
 
57
- const disabled = useAiInfraStore(aiProviderSelectors.isProviderConfigUpdating(provider));
57
+ const isProviderConfigUpdating = useAiInfraStore(
58
+ aiProviderSelectors.isProviderConfigUpdating(provider),
59
+ );
60
+ const totalModels = useAiInfraStore(aiModelSelectors.aiProviderChatModelListIds);
61
+ const updateAiProviderConfig = useAiInfraStore((s) => s.updateAiProviderConfig);
58
62
 
59
63
  const [loading, setLoading] = useState(false);
60
64
  const [pass, setPass] = useState(false);
65
+ const [checkModel, setCheckModel] = useState(model);
61
66
 
62
67
  const theme = useTheme();
63
68
  const [error, setError] = useState<ChatMessageError | undefined>();
@@ -71,6 +76,7 @@ const Checker = memo<ConnectionCheckerProps>(
71
76
  setPass(false);
72
77
  isError = true;
73
78
  },
79
+
74
80
  onFinish: async (value) => {
75
81
  if (!isError && value) {
76
82
  setError(undefined);
@@ -104,7 +110,6 @@ const Checker = memo<ConnectionCheckerProps>(
104
110
  },
105
111
  });
106
112
  };
107
- const isMobile = useIsMobile();
108
113
 
109
114
  const defaultError = error ? <Error error={error as ChatMessageError} /> : null;
110
115
 
@@ -115,26 +120,42 @@ const Checker = memo<ConnectionCheckerProps>(
115
120
  );
116
121
 
117
122
  return (
118
- <Flexbox align={isMobile ? 'flex-start' : 'flex-end'} gap={8}>
119
- <Flexbox
120
- align={'center'}
121
- direction={isMobile ? 'horizontal-reverse' : 'horizontal'}
122
- gap={12}
123
- >
124
- {pass && (
125
- <Flexbox gap={4} horizontal>
126
- <CheckCircleFilled
127
- style={{
128
- color: theme.colorSuccess,
129
- }}
130
- />
131
- {t('llm.checker.pass')}
132
- </Flexbox>
133
- )}
134
- <Button disabled={disabled} loading={loading} onClick={checkConnection}>
123
+ <Flexbox gap={8}>
124
+ <Space.Compact block>
125
+ <Select
126
+ listItemHeight={36}
127
+ onSelect={async (value) => {
128
+ setCheckModel(value);
129
+ await updateAiProviderConfig(provider, { checkModel: value });
130
+ }}
131
+ optionRender={({ value }) => {
132
+ return (
133
+ <Flexbox align={'center'} gap={6} horizontal>
134
+ <ModelIcon model={value as string} size={20} />
135
+ {value}
136
+ </Flexbox>
137
+ );
138
+ }}
139
+ options={totalModels.map((id) => ({ label: id, value: id }))}
140
+ suffixIcon={isProviderConfigUpdating && <Icon icon={Loader2Icon} spin />}
141
+ value={checkModel}
142
+ virtual
143
+ />
144
+ <Button disabled={isProviderConfigUpdating} loading={loading} onClick={checkConnection}>
135
145
  {t('llm.checker.button')}
136
146
  </Button>
137
- </Flexbox>
147
+ </Space.Compact>
148
+
149
+ {pass && (
150
+ <Flexbox gap={4} horizontal>
151
+ <CheckCircleFilled
152
+ style={{
153
+ color: theme.colorSuccess,
154
+ }}
155
+ />
156
+ {t('llm.checker.pass')}
157
+ </Flexbox>
158
+ )}
138
159
  {error && errorContent}
139
160
  </Flexbox>
140
161
  );
@@ -275,7 +275,11 @@ const ProviderConfig = memo<ProviderConfigProps>(
275
275
  children: isLoading ? (
276
276
  <Skeleton.Button active />
277
277
  ) : (
278
- <Checker checkErrorRender={checkErrorRender} model={checkModel!} provider={id} />
278
+ <Checker
279
+ checkErrorRender={checkErrorRender}
280
+ model={data?.checkModel || checkModel!}
281
+ provider={id}
282
+ />
279
283
  ),
280
284
  desc: t('providerModels.config.checker.desc'),
281
285
  label: t('providerModels.config.checker.title'),
@@ -69,7 +69,9 @@ const Spark: ModelProviderCard = {
69
69
  modelsUrl: 'https://xinghuo.xfyun.cn/spark',
70
70
  name: 'Spark',
71
71
  settings: {
72
+ modelEditable: false,
72
73
  sdkType: 'openai',
74
+ showModelFetcher: false,
73
75
  smoothing: {
74
76
  speed: 2,
75
77
  text: true,
@@ -1,11 +1,10 @@
1
1
  import pkg from '@/../package.json';
2
- import { getServerDBConfig } from '@/config/db';
3
2
 
4
3
  import { BRANDING_NAME, ORG_NAME } from './branding';
5
4
 
6
5
  export const CURRENT_VERSION = pkg.version;
7
6
 
8
- export const isServerMode = getServerDBConfig().NEXT_PUBLIC_ENABLED_SERVER_SERVICE;
7
+ export const isServerMode = process.env.NEXT_PUBLIC_SERVICE_MODE === 'server';
9
8
  export const isUsePgliteDB = process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite';
10
9
 
11
10
  export const isDeprecatedEdition = !isServerMode && !isUsePgliteDB;
@@ -197,6 +197,12 @@ export class AiModelModel {
197
197
  );
198
198
  }
199
199
 
200
+ clearModelsByProvider(providerId: string) {
201
+ return this.db
202
+ .delete(aiModels)
203
+ .where(and(eq(aiModels.providerId, providerId), eq(aiModels.userId, this.userId)));
204
+ }
205
+
200
206
  updateModelsOrder = async (providerId: string, sortMap: AiModelSortMap[]) => {
201
207
  await this.db.transaction(async (tx) => {
202
208
  const updates = sortMap.map(({ id, sort }) => {
@@ -278,6 +278,11 @@ export default {
278
278
  latestTime: '上次更新时间:{{time}}',
279
279
  noLatestTime: '暂未获取列表',
280
280
  },
281
+ resetAll: {
282
+ conform: '确认重置当前模型的所有修改?重置后当前模型列表将会回到默认状态',
283
+ success: '重置成功',
284
+ title: '重置所有修改',
285
+ },
281
286
  search: '搜索模型...',
282
287
  searchResult: '搜索到 {{count}} 个模型',
283
288
  title: '模型列表',
@@ -59,6 +59,11 @@ export const aiModelRouter = router({
59
59
  return ctx.aiModelModel.batchUpdateAiModels(input.id, input.models);
60
60
  }),
61
61
 
62
+ clearModelsByProvider: aiModelProcedure
63
+ .input(z.object({ providerId: z.string() }))
64
+ .mutation(async ({ input, ctx }) => {
65
+ return ctx.aiModelModel.clearModelsByProvider(input.providerId);
66
+ }),
62
67
  clearRemoteModels: aiModelProcedure
63
68
  .input(z.object({ providerId: z.string() }))
64
69
  .mutation(async ({ input, ctx }) => {
@@ -47,6 +47,10 @@ export class ClientService extends BaseClientService implements IAiModelService
47
47
  return this.aiModel.clearRemoteModels(providerId);
48
48
  };
49
49
 
50
+ clearModelsByProvider: IAiModelService['clearModelsByProvider'] = async (providerId) => {
51
+ return this.aiModel.clearModelsByProvider(providerId);
52
+ };
53
+
50
54
  updateAiModelOrder: IAiModelService['updateAiModelOrder'] = async (providerId, items) => {
51
55
  return this.aiModel.updateModelsOrder(providerId, items);
52
56
  };
@@ -0,0 +1,122 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
+ import { AiProviderModelListItem } from '@/types/aiModel';
5
+
6
+ import { ServerService } from './server';
7
+
8
+ vi.mock('@/libs/trpc/client', () => ({
9
+ lambdaClient: {
10
+ aiModel: {
11
+ createAiModel: { mutate: vi.fn() },
12
+ getAiProviderModelList: { query: vi.fn() },
13
+ getAiModelById: { query: vi.fn() },
14
+ toggleModelEnabled: { mutate: vi.fn() },
15
+ updateAiModel: { mutate: vi.fn() },
16
+ batchUpdateAiModels: { mutate: vi.fn() },
17
+ batchToggleAiModels: { mutate: vi.fn() },
18
+ clearModelsByProvider: { mutate: vi.fn() },
19
+ clearRemoteModels: { mutate: vi.fn() },
20
+ updateAiModelOrder: { mutate: vi.fn() },
21
+ removeAiModel: { mutate: vi.fn() },
22
+ },
23
+ },
24
+ }));
25
+
26
+ describe('ServerService', () => {
27
+ const service = new ServerService();
28
+
29
+ it('should create AI model', async () => {
30
+ const params = {
31
+ id: 'test-id',
32
+ providerId: 'test-provider',
33
+ displayName: 'Test Model',
34
+ };
35
+ await service.createAiModel(params);
36
+ expect(vi.mocked(lambdaClient.aiModel.createAiModel.mutate)).toHaveBeenCalledWith(params);
37
+ });
38
+
39
+ it('should get AI provider model list', async () => {
40
+ await service.getAiProviderModelList('123');
41
+ expect(vi.mocked(lambdaClient.aiModel.getAiProviderModelList.query)).toHaveBeenCalledWith({
42
+ id: '123',
43
+ });
44
+ });
45
+
46
+ it('should get AI model by id', async () => {
47
+ await service.getAiModelById('123');
48
+ expect(vi.mocked(lambdaClient.aiModel.getAiModelById.query)).toHaveBeenCalledWith({
49
+ id: '123',
50
+ });
51
+ });
52
+
53
+ it('should toggle model enabled', async () => {
54
+ const params = { id: '123', providerId: 'test', enabled: true };
55
+ await service.toggleModelEnabled(params);
56
+ expect(vi.mocked(lambdaClient.aiModel.toggleModelEnabled.mutate)).toHaveBeenCalledWith(params);
57
+ });
58
+
59
+ it('should update AI model', async () => {
60
+ const value = { contextWindowTokens: 4000, displayName: 'Updated Model' };
61
+ await service.updateAiModel('123', 'openai', value);
62
+ expect(vi.mocked(lambdaClient.aiModel.updateAiModel.mutate)).toHaveBeenCalledWith({
63
+ id: '123',
64
+ providerId: 'openai',
65
+ value,
66
+ });
67
+ });
68
+
69
+ it('should batch update AI models', async () => {
70
+ const models: AiProviderModelListItem[] = [
71
+ {
72
+ id: '123',
73
+ enabled: true,
74
+ type: 'chat',
75
+ },
76
+ ];
77
+ await service.batchUpdateAiModels('provider1', models);
78
+ expect(vi.mocked(lambdaClient.aiModel.batchUpdateAiModels.mutate)).toHaveBeenCalledWith({
79
+ id: 'provider1',
80
+ models,
81
+ });
82
+ });
83
+
84
+ it('should batch toggle AI models', async () => {
85
+ const models = ['123', '456'];
86
+ await service.batchToggleAiModels('provider1', models, true);
87
+ expect(vi.mocked(lambdaClient.aiModel.batchToggleAiModels.mutate)).toHaveBeenCalledWith({
88
+ id: 'provider1',
89
+ models,
90
+ enabled: true,
91
+ });
92
+ });
93
+
94
+ it('should clear models by provider', async () => {
95
+ await service.clearModelsByProvider('provider1');
96
+ expect(vi.mocked(lambdaClient.aiModel.clearModelsByProvider.mutate)).toHaveBeenCalledWith({
97
+ providerId: 'provider1',
98
+ });
99
+ });
100
+
101
+ it('should clear remote models', async () => {
102
+ await service.clearRemoteModels('provider1');
103
+ expect(vi.mocked(lambdaClient.aiModel.clearRemoteModels.mutate)).toHaveBeenCalledWith({
104
+ providerId: 'provider1',
105
+ });
106
+ });
107
+
108
+ it('should update AI model order', async () => {
109
+ const items = [{ id: '123', sort: 1 }];
110
+ await service.updateAiModelOrder('provider1', items);
111
+ expect(vi.mocked(lambdaClient.aiModel.updateAiModelOrder.mutate)).toHaveBeenCalledWith({
112
+ providerId: 'provider1',
113
+ sortMap: items,
114
+ });
115
+ });
116
+
117
+ it('should delete AI model', async () => {
118
+ const params = { id: '123', providerId: 'openai' };
119
+ await service.deleteAiModel(params);
120
+ expect(vi.mocked(lambdaClient.aiModel.removeAiModel.mutate)).toHaveBeenCalledWith(params);
121
+ });
122
+ });
@@ -30,6 +30,10 @@ export class ServerService implements IAiModelService {
30
30
  return lambdaClient.aiModel.batchToggleAiModels.mutate({ enabled, id, models });
31
31
  };
32
32
 
33
+ clearModelsByProvider: IAiModelService['clearModelsByProvider'] = async (providerId) => {
34
+ return lambdaClient.aiModel.clearModelsByProvider.mutate({ providerId });
35
+ };
36
+
33
37
  clearRemoteModels: IAiModelService['clearRemoteModels'] = async (providerId) => {
34
38
  return lambdaClient.aiModel.clearRemoteModels.mutate({ providerId });
35
39
  };
@@ -24,6 +24,8 @@ export interface IAiModelService {
24
24
 
25
25
  clearRemoteModels: (providerId: string) => Promise<any>;
26
26
 
27
+ clearModelsByProvider: (providerId: string) => Promise<any>;
28
+
27
29
  updateAiModelOrder: (providerId: string, items: AiModelSortMap[]) => Promise<any>;
28
30
 
29
31
  deleteAiModel: (params: { id: string; providerId: string }) => Promise<any>;
@@ -17,6 +17,7 @@ const FETCH_AI_PROVIDER_MODEL_LIST_KEY = 'FETCH_AI_PROVIDER_MODELS';
17
17
  export interface AiModelAction {
18
18
  batchToggleAiModels: (ids: string[], enabled: boolean) => Promise<void>;
19
19
  batchUpdateAiModels: (models: AiProviderModelListItem[]) => Promise<void>;
20
+ clearModelsByProvider: (provider: string) => Promise<void>;
20
21
  clearRemoteModels: (provider: string) => Promise<void>;
21
22
  createNewAiModel: (params: CreateAiModelParams) => Promise<void>;
22
23
  fetchRemoteModelList: (providerId: string) => Promise<void>;
@@ -55,6 +56,10 @@ export const createAiModelSlice: StateCreator<
55
56
  await aiModelService.batchUpdateAiModels(id, models);
56
57
  await get().refreshAiModelList();
57
58
  },
59
+ clearModelsByProvider: async (provider) => {
60
+ await aiModelService.clearModelsByProvider(provider);
61
+ await get().refreshAiModelList();
62
+ },
58
63
  clearRemoteModels: async (provider) => {
59
64
  await aiModelService.clearRemoteModels(provider);
60
65
  await get().refreshAiModelList();
@@ -1,6 +1,8 @@
1
1
  import { AIProviderStoreState } from '@/store/aiInfra/initialState';
2
2
  import { AiModelSourceEnum } from '@/types/aiModel';
3
3
 
4
+ const aiProviderChatModelListIds = (s: AIProviderStoreState) =>
5
+ s.aiProviderModelList.filter((item) => item.type === 'chat').map((item) => item.id);
4
6
  // List
5
7
  const enabledAiProviderModelList = (s: AIProviderStoreState) =>
6
8
  s.aiProviderModelList.filter((item) => item.enabled);
@@ -68,6 +70,7 @@ const modelContextWindowTokens = (id: string, provider: string) => (s: AIProvide
68
70
  };
69
71
 
70
72
  export const aiModelSelectors = {
73
+ aiProviderChatModelListIds,
71
74
  disabledAiProviderModelList,
72
75
  enabledAiProviderModelList,
73
76
  filteredAiProviderModelList,
@@ -72,6 +72,13 @@ export interface AiProviderSettings {
72
72
  * @default false
73
73
  */
74
74
  disableBrowserRequest?: boolean;
75
+ /**
76
+ * whether provider support edit model
77
+ *
78
+ * @default true
79
+ */
80
+ modelEditable?: boolean;
81
+
75
82
  proxyUrl?:
76
83
  | {
77
84
  desc?: string;
@@ -84,7 +91,6 @@ export interface AiProviderSettings {
84
91
  * default openai
85
92
  */
86
93
  sdkType?: AiProviderSDKType;
87
-
88
94
  showAddNewModel?: boolean;
89
95
  /**
90
96
  * whether show api key in the provider config