@lobehub/lobehub 2.0.0-next.54 → 2.0.0-next.55
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 +27 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/file.json +85 -2
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/file.json +85 -2
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/file.json +85 -2
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/file.json +85 -2
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/file.json +85 -2
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/file.json +85 -2
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/file.json +85 -2
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/file.json +85 -2
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/file.json +85 -2
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/file.json +85 -2
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/file.json +85 -2
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/file.json +85 -2
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/file.json +85 -2
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/file.json +85 -2
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/file.json +85 -2
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/file.json +85 -2
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/file.json +85 -2
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/file.json +85 -2
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/file.test.ts +94 -29
- package/packages/database/src/models/file.ts +15 -4
- package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
- package/packages/database/src/repositories/knowledge/index.ts +420 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
- package/packages/model-bank/src/aiModels/google.ts +9 -5
- package/packages/model-bank/src/aiModels/openai.ts +2 -35
- package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
- package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
- package/packages/model-bank/src/types/aiModel.ts +15 -2
- package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
- package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
- package/packages/types/src/document/index.ts +14 -2
- package/packages/types/src/files/index.ts +2 -0
- package/packages/types/src/files/list.ts +10 -0
- package/packages/types/src/llm.ts +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
- package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
- package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
- package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
- package/src/components/FileIcon/index.tsx +3 -1
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
- package/src/features/FileSidePanel/index.tsx +1 -1
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
- package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
- package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
- package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
- package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
- package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
- package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
- package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
- package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
- package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
- package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
- package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
- package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
- package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
- package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
- package/src/features/KnowledgeManager/Home/index.tsx +221 -0
- package/src/features/KnowledgeManager/index.tsx +75 -0
- package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/file.ts +85 -2
- package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
- package/src/server/routers/lambda/document.ts +57 -0
- package/src/server/routers/lambda/file.ts +72 -0
- package/src/server/routers/lambda/knowledge.ts +94 -0
- package/src/server/services/document/index.ts +103 -0
- package/src/services/document/index.ts +44 -0
- package/src/services/file/index.ts +5 -3
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
- package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
- package/src/store/file/initialState.ts +6 -1
- package/src/store/file/slices/chat/action.ts +3 -3
- package/src/store/file/slices/document/action.ts +359 -0
- package/src/store/file/slices/document/index.ts +3 -0
- package/src/store/file/slices/document/initialState.ts +22 -0
- package/src/store/file/slices/document/selectors.ts +25 -0
- package/src/store/file/slices/fileManager/action.test.ts +16 -9
- package/src/store/file/slices/fileManager/action.ts +11 -11
- package/src/store/file/store.ts +3 -0
- package/src/store/global/initialState.ts +3 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
- package/src/features/FileManager/index.tsx +0 -36
- /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
|
@@ -42,11 +42,13 @@ export class FileService {
|
|
|
42
42
|
await lambdaClient.file.removeAllFiles.mutate();
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// V2.0 Migrate from getFiles to getKnowledgeItems
|
|
46
|
+
getKnowledgeItems = async (params: QueryFileListParams) => {
|
|
47
|
+
return lambdaClient.file.getKnowledgeItems.query(params as QueryFileListSchemaType);
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
// V2.0 Migrate from getFileItem to getKnowledgeItem
|
|
51
|
+
getKnowledgeItem = async (id: string) => {
|
|
50
52
|
return lambdaClient.file.getFileItemById.query({ id });
|
|
51
53
|
};
|
|
52
54
|
|
|
@@ -1,276 +1,172 @@
|
|
|
1
1
|
import * as runtimeModule from '@lobechat/model-runtime';
|
|
2
|
-
import type { EnabledAiModel,
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
): EnabledAiModel => ({
|
|
13
|
-
|
|
14
|
-
|
|
2
|
+
import type { AIImageModelCard, EnabledAiModel, ModelParamsSchema } from 'model-bank';
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getChatModelList,
|
|
7
|
+
getImageModelList,
|
|
8
|
+
normalizeChatModel,
|
|
9
|
+
normalizeImageModel,
|
|
10
|
+
} from '../action';
|
|
11
|
+
|
|
12
|
+
const createChatModel = (overrides: Partial<EnabledAiModel> = {}): EnabledAiModel => ({
|
|
13
|
+
abilities: overrides.abilities ?? { functionCall: true },
|
|
14
|
+
contextWindowTokens: overrides.contextWindowTokens ?? 8192,
|
|
15
|
+
displayName: overrides.displayName ?? 'Chat Model',
|
|
16
|
+
enabled: overrides.enabled ?? true,
|
|
17
|
+
id: overrides.id ?? 'chat-model',
|
|
18
|
+
providerId: overrides.providerId ?? 'openai',
|
|
15
19
|
type: 'chat',
|
|
16
|
-
abilities: { functionCall: true, files: true } satisfies ModelAbilities,
|
|
17
|
-
contextWindowTokens: 8192,
|
|
18
|
-
displayName: `${id} model`,
|
|
19
|
-
enabled: true,
|
|
20
20
|
...overrides,
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
type ImageEnabledModel = EnabledAiModel & AIImageModelCard;
|
|
24
|
+
|
|
25
|
+
const createImageModel = (overrides: Partial<ImageEnabledModel> = {}): ImageEnabledModel => ({
|
|
26
|
+
abilities: overrides.abilities ?? {},
|
|
27
|
+
contextWindowTokens: overrides.contextWindowTokens,
|
|
28
|
+
displayName: overrides.displayName ?? 'Image Model',
|
|
29
|
+
enabled: overrides.enabled ?? true,
|
|
30
|
+
id: overrides.id ?? 'image-model',
|
|
31
|
+
providerId: overrides.providerId ?? 'openai',
|
|
30
32
|
type: 'image',
|
|
31
|
-
abilities: {} satisfies ModelAbilities,
|
|
32
|
-
displayName: `${id} model`,
|
|
33
|
-
enabled: true,
|
|
34
33
|
...overrides,
|
|
35
34
|
});
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
abilities: { functionCall: true, files: true } satisfies ModelAbilities,
|
|
42
|
-
}),
|
|
43
|
-
createChatModel('gpt-3.5-turbo', 'openai', {
|
|
44
|
-
displayName: 'GPT-3.5 Turbo',
|
|
45
|
-
abilities: { functionCall: true } satisfies ModelAbilities,
|
|
46
|
-
contextWindowTokens: 4096,
|
|
47
|
-
}),
|
|
48
|
-
createChatModel('claude-3-opus', 'anthropic', {
|
|
49
|
-
displayName: 'Claude 3 Opus',
|
|
50
|
-
abilities: { functionCall: false, files: true } satisfies ModelAbilities,
|
|
51
|
-
contextWindowTokens: 200000,
|
|
52
|
-
}),
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
const mockImageModels = [
|
|
56
|
-
createImageModel('dall-e-3', 'openai', {
|
|
57
|
-
displayName: 'DALL-E 3',
|
|
58
|
-
parameters: {
|
|
59
|
-
prompt: { default: '' },
|
|
60
|
-
size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
|
|
61
|
-
},
|
|
62
|
-
}),
|
|
63
|
-
createImageModel('midjourney', 'midjourney', {
|
|
64
|
-
displayName: 'Midjourney',
|
|
65
|
-
}),
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const allModels = [...mockChatModels, ...mockImageModels];
|
|
69
|
-
|
|
70
|
-
describe('getModelListByType', () => {
|
|
71
|
-
describe('Core Functionality', () => {
|
|
72
|
-
it('should filter models by providerId and type correctly', async () => {
|
|
73
|
-
const result = await getModelListByType(allModels, 'openai', 'chat');
|
|
74
|
-
|
|
75
|
-
expect(result).toHaveLength(2);
|
|
76
|
-
expect(result.map((m) => m.id)).toEqual(['gpt-4', 'gpt-3.5-turbo']);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should return correct model structure for chat models', async () => {
|
|
80
|
-
const result = await getModelListByType(allModels, 'openai', 'chat');
|
|
36
|
+
describe('aiProvider action helpers', () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
vi.restoreAllMocks();
|
|
39
|
+
});
|
|
81
40
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
41
|
+
describe('normalizeChatModel', () => {
|
|
42
|
+
it('fills missing optional fields with safe defaults', () => {
|
|
43
|
+
const model = createChatModel({
|
|
44
|
+
abilities: undefined,
|
|
45
|
+
contextWindowTokens: undefined,
|
|
46
|
+
displayName: undefined,
|
|
87
47
|
});
|
|
88
|
-
});
|
|
89
48
|
|
|
90
|
-
|
|
91
|
-
const result = await getModelListByType(allModels, 'openai', 'image');
|
|
49
|
+
const result = normalizeChatModel(model);
|
|
92
50
|
|
|
93
|
-
expect(result
|
|
51
|
+
expect(result).toEqual({
|
|
94
52
|
abilities: {},
|
|
95
53
|
contextWindowTokens: undefined,
|
|
96
|
-
displayName: '
|
|
97
|
-
id: '
|
|
98
|
-
parameters: {
|
|
99
|
-
prompt: { default: '' },
|
|
100
|
-
size: { default: '1024x1024', enum: ['512x512', '1024x1024', '1536x1536'] },
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should exclude parameters field from chat models', async () => {
|
|
106
|
-
const result = await getModelListByType(mockChatModels, 'openai', 'chat');
|
|
107
|
-
|
|
108
|
-
result.forEach((model) => {
|
|
109
|
-
expect(model).not.toHaveProperty('parameters');
|
|
54
|
+
displayName: '',
|
|
55
|
+
id: 'chat-model',
|
|
110
56
|
});
|
|
111
57
|
});
|
|
112
|
-
|
|
113
|
-
it('should remove duplicate model IDs', async () => {
|
|
114
|
-
const duplicateModels = [
|
|
115
|
-
createChatModel('gpt-4', 'openai', {
|
|
116
|
-
displayName: 'GPT-4 Version 1',
|
|
117
|
-
abilities: { functionCall: true } satisfies ModelAbilities,
|
|
118
|
-
}),
|
|
119
|
-
createChatModel('gpt-4', 'openai', {
|
|
120
|
-
displayName: 'GPT-4 Version 2',
|
|
121
|
-
abilities: { functionCall: false } satisfies ModelAbilities,
|
|
122
|
-
}),
|
|
123
|
-
];
|
|
124
|
-
|
|
125
|
-
const result = await getModelListByType(duplicateModels, 'openai', 'chat');
|
|
126
|
-
|
|
127
|
-
expect(result).toHaveLength(1);
|
|
128
|
-
expect(result[0].displayName).toBe('GPT-4 Version 1');
|
|
129
|
-
});
|
|
130
58
|
});
|
|
131
59
|
|
|
132
|
-
describe('
|
|
133
|
-
it('
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const modelWithMissingProps = createChatModel('test-model', 'test', {
|
|
146
|
-
displayName: undefined,
|
|
147
|
-
abilities: undefined,
|
|
148
|
-
contextWindowTokens: undefined,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const result = await getModelListByType([modelWithMissingProps], 'test', 'chat');
|
|
152
|
-
|
|
153
|
-
expect(result[0].displayName).toBe('');
|
|
154
|
-
expect(result[0].abilities).toEqual({});
|
|
155
|
-
expect(result[0].contextWindowTokens).toBeUndefined();
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('should preserve complex model properties', async () => {
|
|
159
|
-
const complexModel = createChatModel('complex-model', 'test', {
|
|
160
|
-
displayName: 'Complex Model with All Properties',
|
|
161
|
-
abilities: {
|
|
162
|
-
functionCall: true,
|
|
163
|
-
files: true,
|
|
164
|
-
vision: false,
|
|
165
|
-
} satisfies ModelAbilities,
|
|
166
|
-
contextWindowTokens: 128000,
|
|
60
|
+
describe('normalizeImageModel', () => {
|
|
61
|
+
it('preserves inline metadata and pricing information', async () => {
|
|
62
|
+
const model = createImageModel({
|
|
63
|
+
abilities: { vision: true },
|
|
64
|
+
contextWindowTokens: 4096,
|
|
65
|
+
displayName: 'Inline Model',
|
|
66
|
+
parameters: {
|
|
67
|
+
prompt: { default: '' },
|
|
68
|
+
size: { default: '1024x1024', enum: ['512x512', '1024x1024'] },
|
|
69
|
+
} as ModelParamsSchema,
|
|
70
|
+
pricing: {
|
|
71
|
+
units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
|
|
72
|
+
},
|
|
167
73
|
});
|
|
168
74
|
|
|
169
|
-
const result = await
|
|
75
|
+
const result = await normalizeImageModel(model);
|
|
170
76
|
|
|
171
|
-
expect(result
|
|
172
|
-
|
|
173
|
-
displayName: '
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
vision: false,
|
|
77
|
+
expect(result).toMatchObject({
|
|
78
|
+
abilities: { vision: true },
|
|
79
|
+
displayName: 'Inline Model',
|
|
80
|
+
parameters: { size: { default: '1024x1024', enum: ['512x512', '1024x1024'] } },
|
|
81
|
+
pricing: {
|
|
82
|
+
units: [{ name: 'imageGeneration', rate: 0.04, strategy: 'fixed', unit: 'image' }],
|
|
178
83
|
},
|
|
179
|
-
contextWindowTokens: 128000,
|
|
180
84
|
});
|
|
181
85
|
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('Image Model Parameter Handling', () => {
|
|
185
|
-
it('should use fallback parameters for image models without parameters', async () => {
|
|
186
|
-
vi.spyOn(runtimeModule, 'getModelPropertyWithFallback').mockResolvedValueOnce({
|
|
187
|
-
size: '1024x1024',
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const result = await getModelListByType(allModels, 'midjourney', 'image');
|
|
191
86
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
87
|
+
it('fetches fallback description/parameters/pricing when missing', async () => {
|
|
88
|
+
const fallbackSpy = vi
|
|
89
|
+
.spyOn(runtimeModule, 'getModelPropertyWithFallback')
|
|
90
|
+
.mockImplementation(async (_id, key) => {
|
|
91
|
+
if (key === 'parameters')
|
|
92
|
+
return {
|
|
93
|
+
prompt: { default: '' },
|
|
94
|
+
size: { default: '768x768', enum: ['512x512', '768x768'] },
|
|
95
|
+
} satisfies ModelParamsSchema;
|
|
96
|
+
if (key === 'pricing')
|
|
97
|
+
return {
|
|
98
|
+
units: [{ name: 'imageGeneration', rate: 0.02, strategy: 'fixed', unit: 'image' }],
|
|
99
|
+
};
|
|
100
|
+
if (key === 'description') return 'Fallback description';
|
|
101
|
+
return undefined;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const model = createImageModel({
|
|
105
|
+
id: 'stable-diffusion',
|
|
106
|
+
providerId: 'stability',
|
|
107
|
+
parameters: undefined,
|
|
108
|
+
pricing: undefined,
|
|
198
109
|
});
|
|
199
|
-
});
|
|
200
110
|
|
|
201
|
-
|
|
202
|
-
const imageModelsWithoutParams = [
|
|
203
|
-
createImageModel('stable-diffusion', 'stability', { displayName: 'Stable Diffusion' }),
|
|
204
|
-
createImageModel('flux-schnell', 'fal', { displayName: 'FLUX Schnell' }),
|
|
205
|
-
];
|
|
111
|
+
const result = await normalizeImageModel(model);
|
|
206
112
|
|
|
207
|
-
|
|
113
|
+
expect(result.parameters).toEqual({
|
|
208
114
|
prompt: { default: '' },
|
|
209
|
-
|
|
210
|
-
height: { default: 512, min: 256, max: 2048 },
|
|
115
|
+
size: { default: '768x768', enum: ['512x512', '768x768'] },
|
|
211
116
|
});
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
expect(result).toHaveLength(1);
|
|
216
|
-
expect(result[0].parameters).toEqual({
|
|
217
|
-
prompt: { default: '' },
|
|
218
|
-
width: { default: 512, min: 256, max: 2048 },
|
|
219
|
-
height: { default: 512, min: 256, max: 2048 },
|
|
117
|
+
expect(result.pricing).toEqual({
|
|
118
|
+
units: [{ name: 'imageGeneration', rate: 0.02, strategy: 'fixed', unit: 'image' }],
|
|
220
119
|
});
|
|
221
|
-
|
|
222
|
-
expect(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
120
|
+
expect(result.description).toBe('Fallback description');
|
|
121
|
+
expect(fallbackSpy).toHaveBeenCalledWith('stable-diffusion', 'parameters', 'stability');
|
|
122
|
+
expect(fallbackSpy).toHaveBeenCalledWith('stable-diffusion', 'pricing', 'stability');
|
|
123
|
+
expect(fallbackSpy).toHaveBeenCalledWith('stable-diffusion', 'description', 'stability');
|
|
226
124
|
});
|
|
125
|
+
});
|
|
227
126
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
})
|
|
127
|
+
describe('getChatModelList', () => {
|
|
128
|
+
const chatModels = [
|
|
129
|
+
createChatModel({ id: 'gpt-4', providerId: 'openai', displayName: 'GPT-4' }),
|
|
130
|
+
createChatModel({ id: 'gpt-3.5', providerId: 'openai', displayName: 'GPT-3.5' }),
|
|
131
|
+
createChatModel({ id: 'claude-3', providerId: 'anthropic', displayName: 'Claude 3' }),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
it('filters by provider and deduplicates IDs', async () => {
|
|
135
|
+
const duplicated = [
|
|
136
|
+
...chatModels,
|
|
137
|
+
createChatModel({ id: 'gpt-4', providerId: 'openai', displayName: 'GPT-4 Duplicate' }),
|
|
138
|
+
];
|
|
232
139
|
|
|
233
|
-
|
|
140
|
+
const result = await getChatModelList(duplicated, 'openai');
|
|
234
141
|
|
|
235
|
-
|
|
142
|
+
expect(result).toHaveLength(2);
|
|
143
|
+
expect(result.map((m) => m.id)).toEqual(['gpt-4', 'gpt-3.5']);
|
|
144
|
+
expect(result[0].displayName).toBe('GPT-4');
|
|
145
|
+
});
|
|
236
146
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
expect(result
|
|
147
|
+
it('returns empty array when provider has no chat models', async () => {
|
|
148
|
+
const result = await getChatModelList(chatModels, 'nonexistent');
|
|
149
|
+
expect(result).toEqual([]);
|
|
240
150
|
});
|
|
241
151
|
});
|
|
242
152
|
|
|
243
|
-
describe('
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
abilities: { functionCall: i % 2 === 0 } satisfies ModelAbilities,
|
|
249
|
-
contextWindowTokens: 4096 + i * 1000,
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
153
|
+
describe('getImageModelList', () => {
|
|
154
|
+
const imageModels = [
|
|
155
|
+
createImageModel({ id: 'dall-e-3', providerId: 'openai', displayName: 'DALL-E 3' }),
|
|
156
|
+
createImageModel({ id: 'midjourney', providerId: 'midjourney', displayName: 'Midjourney' }),
|
|
157
|
+
];
|
|
252
158
|
|
|
253
|
-
|
|
159
|
+
it('collects normalized image models for a provider', async () => {
|
|
160
|
+
const result = await getImageModelList(imageModels, 'openai');
|
|
254
161
|
|
|
255
|
-
expect(result).toHaveLength(
|
|
256
|
-
expect(result.
|
|
257
|
-
|
|
258
|
-
result.forEach((model, index) => {
|
|
259
|
-
expect(model.abilities.functionCall).toBe(index % 2 === 0);
|
|
260
|
-
expect(model.contextWindowTokens).toBe(4096 + index * 1000);
|
|
261
|
-
});
|
|
162
|
+
expect(result).toHaveLength(1);
|
|
163
|
+
expect(result[0].id).toBe('dall-e-3');
|
|
164
|
+
expect(result[0].displayName).toBe('DALL-E 3');
|
|
262
165
|
});
|
|
263
166
|
|
|
264
|
-
it('
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
createChatModel('second-model', 'test', { displayName: 'Second Model' }),
|
|
268
|
-
createChatModel('third-model', 'test', { displayName: 'Third Model' }),
|
|
269
|
-
];
|
|
270
|
-
|
|
271
|
-
const result = await getModelListByType(orderedModels, 'test', 'chat');
|
|
272
|
-
|
|
273
|
-
expect(result.map((m) => m.id)).toEqual(['first-model', 'second-model', 'third-model']);
|
|
167
|
+
it('returns empty array when provider has no image models', async () => {
|
|
168
|
+
const result = await getImageModelList(imageModels, 'unknown');
|
|
169
|
+
expect(result).toEqual([]);
|
|
274
170
|
});
|
|
275
171
|
});
|
|
276
172
|
});
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { isDeprecatedEdition, isDesktop, isUsePgliteDB } from '@lobechat/const';
|
|
2
|
-
import { getModelPropertyWithFallback } from '@lobechat/model-runtime';
|
|
2
|
+
import { getModelPropertyWithFallback, resolveImageSinglePrice } from '@lobechat/model-runtime';
|
|
3
3
|
import { uniqBy } from 'lodash-es';
|
|
4
4
|
import {
|
|
5
5
|
AIImageModelCard,
|
|
6
6
|
EnabledAiModel,
|
|
7
7
|
LobeDefaultAiModelListItem,
|
|
8
8
|
ModelAbilities,
|
|
9
|
+
ModelParamsSchema,
|
|
10
|
+
Pricing,
|
|
9
11
|
} from 'model-bank';
|
|
10
12
|
import { SWRResponse, mutate } from 'swr';
|
|
11
13
|
import { StateCreator } from 'zustand/vanilla';
|
|
@@ -28,52 +30,130 @@ import {
|
|
|
28
30
|
UpdateAiProviderParams,
|
|
29
31
|
} from '@/types/aiProvider';
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
export type ProviderModelListItem = {
|
|
34
|
+
abilities: ModelAbilities;
|
|
35
|
+
approximatePricePerImage?: number;
|
|
36
|
+
contextWindowTokens?: number;
|
|
37
|
+
description?: string;
|
|
38
|
+
displayName: string;
|
|
39
|
+
id: string;
|
|
40
|
+
parameters?: ModelParamsSchema;
|
|
41
|
+
pricePerImage?: number;
|
|
42
|
+
pricing?: Pricing;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type ModelNormalizer = (model: EnabledAiModel) => Promise<ProviderModelListItem>;
|
|
46
|
+
|
|
47
|
+
const dedupeById = (models: ProviderModelListItem[]) => uniqBy(models, 'id');
|
|
48
|
+
|
|
49
|
+
const createProviderModelCollector = (
|
|
50
|
+
type: EnabledAiModel['type'],
|
|
51
|
+
normalizer: ModelNormalizer,
|
|
38
52
|
) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
return async (enabledAiModels: EnabledAiModel[], providerId: string) => {
|
|
54
|
+
const filteredModels = enabledAiModels.filter(
|
|
55
|
+
(model) => model.providerId === providerId && model.type === type,
|
|
56
|
+
);
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
if (!filteredModels.length) return [];
|
|
59
|
+
|
|
60
|
+
const normalized = await Promise.all(filteredModels.map((model) => normalizer(model)));
|
|
61
|
+
return dedupeById(normalized);
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const normalizeChatModel = (model: EnabledAiModel): ProviderModelListItem => ({
|
|
66
|
+
abilities: (model.abilities || {}) as ModelAbilities,
|
|
67
|
+
contextWindowTokens: model.contextWindowTokens,
|
|
68
|
+
displayName: model.displayName ?? '',
|
|
69
|
+
id: model.id,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const normalizeImageModel = async (
|
|
73
|
+
model: EnabledAiModel,
|
|
74
|
+
): Promise<ProviderModelListItem> => {
|
|
75
|
+
const fallbackParametersPromise = model.parameters
|
|
76
|
+
? Promise.resolve<ModelParamsSchema | undefined>(model.parameters)
|
|
77
|
+
: getModelPropertyWithFallback<ModelParamsSchema | undefined>(
|
|
78
|
+
model.id,
|
|
79
|
+
'parameters',
|
|
80
|
+
model.providerId,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const modelWithPricing = model as AIImageModelCard;
|
|
84
|
+
const fallbackPricingPromise = modelWithPricing.pricing
|
|
85
|
+
? Promise.resolve<Pricing | undefined>(modelWithPricing.pricing)
|
|
86
|
+
: getModelPropertyWithFallback<Pricing | undefined>(model.id, 'pricing', model.providerId);
|
|
87
|
+
|
|
88
|
+
const fallbackDescriptionPromise = getModelPropertyWithFallback<string | undefined>(
|
|
89
|
+
model.id,
|
|
90
|
+
'description',
|
|
91
|
+
model.providerId,
|
|
55
92
|
);
|
|
56
93
|
|
|
57
|
-
|
|
94
|
+
const [fallbackParameters, fallbackPricing, fallbackDescription] = await Promise.all([
|
|
95
|
+
fallbackParametersPromise,
|
|
96
|
+
fallbackPricingPromise,
|
|
97
|
+
fallbackDescriptionPromise,
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const parameters = model.parameters ?? fallbackParameters;
|
|
101
|
+
const pricing = fallbackPricing;
|
|
102
|
+
const description = fallbackDescription;
|
|
103
|
+
const { price, approximatePrice } = resolveImageSinglePrice(pricing);
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
abilities: (model.abilities || {}) as ModelAbilities,
|
|
107
|
+
contextWindowTokens: model.contextWindowTokens,
|
|
108
|
+
displayName: model.displayName ?? '',
|
|
109
|
+
id: model.id,
|
|
110
|
+
...(parameters && { parameters }),
|
|
111
|
+
...(description && { description }),
|
|
112
|
+
...(pricing && { pricing }),
|
|
113
|
+
...(typeof approximatePrice === 'number' && { approximatePricePerImage: approximatePrice }),
|
|
114
|
+
...(typeof price === 'number' && { pricePerImage: price }),
|
|
115
|
+
};
|
|
58
116
|
};
|
|
59
117
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
118
|
+
export const getChatModelList = createProviderModelCollector('chat', async (model) =>
|
|
119
|
+
normalizeChatModel(model),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
export const getImageModelList = createProviderModelCollector('image', normalizeImageModel);
|
|
123
|
+
|
|
63
124
|
const buildProviderModelLists = async (
|
|
64
125
|
providers: EnabledProvider[],
|
|
65
126
|
enabledAiModels: EnabledAiModel[],
|
|
66
|
-
|
|
127
|
+
collector: (
|
|
128
|
+
enabledAiModels: EnabledAiModel[],
|
|
129
|
+
providerId: string,
|
|
130
|
+
) => Promise<ProviderModelListItem[]>,
|
|
67
131
|
) => {
|
|
68
132
|
return Promise.all(
|
|
69
133
|
providers.map(async (provider) => ({
|
|
70
134
|
...provider,
|
|
71
|
-
children: await
|
|
135
|
+
children: await collector(enabledAiModels, provider.id),
|
|
72
136
|
name: provider.name || provider.id,
|
|
73
137
|
})),
|
|
74
138
|
);
|
|
75
139
|
};
|
|
76
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Build image provider model lists with proper async handling
|
|
143
|
+
*/
|
|
144
|
+
const buildImageProviderModelLists = async (
|
|
145
|
+
providers: EnabledProvider[],
|
|
146
|
+
enabledAiModels: EnabledAiModel[],
|
|
147
|
+
) => buildProviderModelLists(providers, enabledAiModels, getImageModelList);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build chat provider model lists with proper async handling
|
|
151
|
+
*/
|
|
152
|
+
const buildChatProviderModelLists = async (
|
|
153
|
+
providers: EnabledProvider[],
|
|
154
|
+
enabledAiModels: EnabledAiModel[],
|
|
155
|
+
) => buildProviderModelLists(providers, enabledAiModels, getChatModelList);
|
|
156
|
+
|
|
77
157
|
enum AiProviderSwrKey {
|
|
78
158
|
fetchAiProviderItem = 'FETCH_AI_PROVIDER_ITEM',
|
|
79
159
|
fetchAiProviderList = 'FETCH_AI_PROVIDER',
|
|
@@ -252,8 +332,8 @@ export const createAiProviderSlice: StateCreator<
|
|
|
252
332
|
|
|
253
333
|
// Build model lists with proper async handling
|
|
254
334
|
const [enabledChatModelList, enabledImageModelList] = await Promise.all([
|
|
255
|
-
|
|
256
|
-
|
|
335
|
+
buildChatProviderModelLists(data.enabledChatAiProviders, data.enabledAiModels),
|
|
336
|
+
buildImageProviderModelLists(data.enabledImageAiProviders, data.enabledAiModels),
|
|
257
337
|
]);
|
|
258
338
|
|
|
259
339
|
return {
|
|
@@ -285,8 +365,8 @@ export const createAiProviderSlice: StateCreator<
|
|
|
285
365
|
// Build model lists for non-login state as well
|
|
286
366
|
const enabledAiModels = builtinAiModelList.filter((m) => m.enabled);
|
|
287
367
|
const [enabledChatModelList, enabledImageModelList] = await Promise.all([
|
|
288
|
-
|
|
289
|
-
|
|
368
|
+
buildChatProviderModelLists(enabledChatAiProviders, enabledAiModels),
|
|
369
|
+
buildImageProviderModelLists(enabledImageAiProviders, enabledAiModels),
|
|
290
370
|
]);
|
|
291
371
|
|
|
292
372
|
return {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { ImageFileState, initialImageFileState } from './slices/chat';
|
|
2
2
|
import { FileChunkState, initialFileChunkState } from './slices/chunk';
|
|
3
|
+
import { DocumentState, initialDocumentState } from './slices/document';
|
|
3
4
|
import { FileManagerState, initialFileManagerState } from './slices/fileManager';
|
|
4
5
|
|
|
5
|
-
export type FilesStoreState = ImageFileState &
|
|
6
|
+
export type FilesStoreState = ImageFileState &
|
|
7
|
+
DocumentState &
|
|
8
|
+
FileManagerState &
|
|
9
|
+
FileChunkState;
|
|
6
10
|
|
|
7
11
|
export const initialState: FilesStoreState = {
|
|
8
12
|
...initialImageFileState,
|
|
13
|
+
...initialDocumentState,
|
|
9
14
|
...initialFileManagerState,
|
|
10
15
|
...initialFileChunkState,
|
|
11
16
|
};
|