@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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { FilesTabs, QueryFileListParams, SortType } from '@lobechat/types';
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
import { DocumentModel } from '../../models/document';
|
|
5
|
+
import { FileModel } from '../../models/file';
|
|
6
|
+
import { documents, files, knowledgeBaseFiles } from '../../schemas';
|
|
7
|
+
import { LobeChatDatabase } from '../../type';
|
|
8
|
+
|
|
9
|
+
export interface KnowledgeItem {
|
|
10
|
+
chunkTaskId?: string | null;
|
|
11
|
+
content?: string | null;
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
editorData?: Record<string, any> | null;
|
|
14
|
+
embeddingTaskId?: string | null;
|
|
15
|
+
fileType: string;
|
|
16
|
+
id: string;
|
|
17
|
+
metadata?: Record<string, any> | null;
|
|
18
|
+
name: string;
|
|
19
|
+
size: number;
|
|
20
|
+
/**
|
|
21
|
+
* Source type to distinguish between files and documents
|
|
22
|
+
* - 'file': from files table
|
|
23
|
+
* - 'document': from documents table
|
|
24
|
+
*/
|
|
25
|
+
sourceType: 'file' | 'document';
|
|
26
|
+
updatedAt: Date;
|
|
27
|
+
url?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Knowledge Repository - combines files and documents into a unified interface
|
|
32
|
+
*/
|
|
33
|
+
export class KnowledgeRepo {
|
|
34
|
+
private userId: string;
|
|
35
|
+
private db: LobeChatDatabase;
|
|
36
|
+
private fileModel: FileModel;
|
|
37
|
+
private documentModel: DocumentModel;
|
|
38
|
+
|
|
39
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
|
40
|
+
this.userId = userId;
|
|
41
|
+
this.db = db;
|
|
42
|
+
this.fileModel = new FileModel(db, userId);
|
|
43
|
+
this.documentModel = new DocumentModel(db, userId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Query combined results from files and documents tables
|
|
48
|
+
*/
|
|
49
|
+
async query({
|
|
50
|
+
category,
|
|
51
|
+
q,
|
|
52
|
+
sortType,
|
|
53
|
+
sorter,
|
|
54
|
+
knowledgeBaseId,
|
|
55
|
+
showFilesInKnowledgeBase,
|
|
56
|
+
}: QueryFileListParams = {}): Promise<KnowledgeItem[]> {
|
|
57
|
+
// Build file query
|
|
58
|
+
const fileQuery = this.buildFileQuery({
|
|
59
|
+
category,
|
|
60
|
+
knowledgeBaseId,
|
|
61
|
+
q,
|
|
62
|
+
showFilesInKnowledgeBase,
|
|
63
|
+
sortType,
|
|
64
|
+
sorter,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Build document query (notes)
|
|
68
|
+
const documentQuery = this.buildDocumentQuery({
|
|
69
|
+
category,
|
|
70
|
+
knowledgeBaseId,
|
|
71
|
+
q,
|
|
72
|
+
sortType,
|
|
73
|
+
sorter,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Combine both queries with UNION ALL
|
|
77
|
+
const combinedQuery = sql`
|
|
78
|
+
(${fileQuery})
|
|
79
|
+
UNION ALL
|
|
80
|
+
(${documentQuery})
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
// Add final ordering
|
|
84
|
+
const orderClause = this.buildOrderClause(sortType, sorter);
|
|
85
|
+
const finalQuery = sql`
|
|
86
|
+
SELECT * FROM (${combinedQuery}) as combined
|
|
87
|
+
ORDER BY ${orderClause}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
const result = await this.db.execute(finalQuery);
|
|
91
|
+
|
|
92
|
+
const mappedResults = result.rows.map((row: any) => {
|
|
93
|
+
// Parse editor_data if it's a string (raw SQL returns JSONB as string)
|
|
94
|
+
let editorData = row.editor_data;
|
|
95
|
+
if (typeof editorData === 'string') {
|
|
96
|
+
try {
|
|
97
|
+
editorData = JSON.parse(editorData);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error('[KnowledgeRepo] Failed to parse editor_data:', e);
|
|
100
|
+
editorData = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse metadata if it's a string (raw SQL returns JSONB as string)
|
|
105
|
+
let metadata = row.metadata;
|
|
106
|
+
if (typeof metadata === 'string') {
|
|
107
|
+
try {
|
|
108
|
+
metadata = JSON.parse(metadata);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('[KnowledgeRepo] Failed to parse metadata:', e);
|
|
111
|
+
metadata = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
chunkTaskId: row.chunk_task_id,
|
|
117
|
+
content: row.content,
|
|
118
|
+
createdAt: new Date(row.created_at),
|
|
119
|
+
editorData,
|
|
120
|
+
embeddingTaskId: row.embedding_task_id,
|
|
121
|
+
fileType: row.file_type,
|
|
122
|
+
id: row.id,
|
|
123
|
+
metadata,
|
|
124
|
+
name: row.name,
|
|
125
|
+
size: Number(row.size),
|
|
126
|
+
sourceType: row.source_type,
|
|
127
|
+
updatedAt: new Date(row.updated_at),
|
|
128
|
+
url: row.url,
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
console.log('[KnowledgeRepo.query] Fetched items:', {
|
|
133
|
+
count: mappedResults.length,
|
|
134
|
+
documents: mappedResults.filter((item) => item.sourceType === 'document'),
|
|
135
|
+
sampleEditorData: mappedResults.find((item) => item.sourceType === 'document')?.editorData,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return mappedResults;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Delete item by id - routes to appropriate model based on sourceType
|
|
143
|
+
*/
|
|
144
|
+
async deleteItem(id: string, sourceType: 'file' | 'document'): Promise<void> {
|
|
145
|
+
if (sourceType === 'file') {
|
|
146
|
+
await this.fileModel.delete(id);
|
|
147
|
+
} else {
|
|
148
|
+
await this.documentModel.delete(id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Batch delete items
|
|
154
|
+
*/
|
|
155
|
+
async deleteMany(items: Array<{ id: string; sourceType: 'file' | 'document' }>): Promise<void> {
|
|
156
|
+
const fileIds = items.filter((item) => item.sourceType === 'file').map((item) => item.id);
|
|
157
|
+
const documentIds = items
|
|
158
|
+
.filter((item) => item.sourceType === 'document')
|
|
159
|
+
.map((item) => item.id);
|
|
160
|
+
|
|
161
|
+
await Promise.all([
|
|
162
|
+
fileIds.length > 0 ? this.fileModel.deleteMany(fileIds) : Promise.resolve(),
|
|
163
|
+
documentIds.length > 0
|
|
164
|
+
? Promise.all(documentIds.map((id) => this.documentModel.delete(id)))
|
|
165
|
+
: Promise.resolve(),
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find item by id
|
|
171
|
+
*/
|
|
172
|
+
async findById(id: string, sourceType: 'file' | 'document'): Promise<any> {
|
|
173
|
+
if (sourceType === 'file') {
|
|
174
|
+
return this.fileModel.findById(id);
|
|
175
|
+
} else {
|
|
176
|
+
return this.documentModel.findById(id);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private buildFileQuery({
|
|
181
|
+
category,
|
|
182
|
+
q,
|
|
183
|
+
knowledgeBaseId,
|
|
184
|
+
showFilesInKnowledgeBase,
|
|
185
|
+
}: QueryFileListParams = {}): ReturnType<typeof sql> {
|
|
186
|
+
let whereConditions: any[] = [sql`${files.userId} = ${this.userId}`];
|
|
187
|
+
|
|
188
|
+
// Search filter
|
|
189
|
+
if (q) {
|
|
190
|
+
whereConditions.push(sql`${files.name} ILIKE ${`%${q}%`}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Category filter
|
|
194
|
+
if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
|
|
195
|
+
const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
|
|
196
|
+
if (Array.isArray(fileTypePrefix)) {
|
|
197
|
+
// For multiple file types (e.g., Documents includes 'application' and 'custom')
|
|
198
|
+
const orConditions = fileTypePrefix.map(
|
|
199
|
+
(prefix) => sql`${files.fileType} ILIKE ${`${prefix}%`}`,
|
|
200
|
+
);
|
|
201
|
+
whereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
|
|
202
|
+
} else {
|
|
203
|
+
whereConditions.push(sql`${files.fileType} ILIKE ${`${fileTypePrefix}%`}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Knowledge base filter
|
|
208
|
+
if (knowledgeBaseId) {
|
|
209
|
+
// Build where conditions using proper table references (f.column instead of files.column)
|
|
210
|
+
const kbWhereConditions: any[] = [sql`f.user_id = ${this.userId}`];
|
|
211
|
+
|
|
212
|
+
// Search filter
|
|
213
|
+
if (q) {
|
|
214
|
+
kbWhereConditions.push(sql`f.name ILIKE ${`%${q}%`}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Category filter
|
|
218
|
+
if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
|
|
219
|
+
const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
|
|
220
|
+
if (Array.isArray(fileTypePrefix)) {
|
|
221
|
+
const orConditions = fileTypePrefix.map(
|
|
222
|
+
(prefix) => sql`f.file_type ILIKE ${`${prefix}%`}`,
|
|
223
|
+
);
|
|
224
|
+
kbWhereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
|
|
225
|
+
} else {
|
|
226
|
+
kbWhereConditions.push(sql`f.file_type ILIKE ${`${fileTypePrefix}%`}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return sql`
|
|
231
|
+
SELECT
|
|
232
|
+
f.id,
|
|
233
|
+
f.name,
|
|
234
|
+
f.file_type,
|
|
235
|
+
f.size,
|
|
236
|
+
f.url,
|
|
237
|
+
f.created_at,
|
|
238
|
+
f.updated_at,
|
|
239
|
+
f.chunk_task_id,
|
|
240
|
+
f.embedding_task_id,
|
|
241
|
+
NULL as editor_data,
|
|
242
|
+
NULL as content,
|
|
243
|
+
NULL as metadata,
|
|
244
|
+
'file' as source_type
|
|
245
|
+
FROM ${files} f
|
|
246
|
+
INNER JOIN ${knowledgeBaseFiles} kbf
|
|
247
|
+
ON f.id = kbf.file_id
|
|
248
|
+
AND kbf.knowledge_base_id = ${knowledgeBaseId}
|
|
249
|
+
WHERE ${sql.join(kbWhereConditions, sql` AND `)}
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Exclude files in knowledge base if needed
|
|
254
|
+
if (!showFilesInKnowledgeBase) {
|
|
255
|
+
whereConditions.push(
|
|
256
|
+
sql`
|
|
257
|
+
NOT EXISTS (
|
|
258
|
+
SELECT 1 FROM ${knowledgeBaseFiles}
|
|
259
|
+
WHERE ${knowledgeBaseFiles.fileId} = ${files.id}
|
|
260
|
+
)
|
|
261
|
+
`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return sql`
|
|
266
|
+
SELECT
|
|
267
|
+
id,
|
|
268
|
+
name,
|
|
269
|
+
file_type,
|
|
270
|
+
size,
|
|
271
|
+
url,
|
|
272
|
+
created_at,
|
|
273
|
+
updated_at,
|
|
274
|
+
chunk_task_id,
|
|
275
|
+
embedding_task_id,
|
|
276
|
+
NULL as editor_data,
|
|
277
|
+
NULL as content,
|
|
278
|
+
NULL as metadata,
|
|
279
|
+
'file' as source_type
|
|
280
|
+
FROM ${files}
|
|
281
|
+
WHERE ${sql.join(whereConditions, sql` AND `)}
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private buildDocumentQuery({
|
|
286
|
+
category,
|
|
287
|
+
q,
|
|
288
|
+
knowledgeBaseId,
|
|
289
|
+
}: QueryFileListParams = {}): ReturnType<typeof sql> {
|
|
290
|
+
let whereConditions: any[] = [sql`${documents.userId} = ${this.userId}`];
|
|
291
|
+
|
|
292
|
+
// Search filter
|
|
293
|
+
if (q) {
|
|
294
|
+
whereConditions.push(
|
|
295
|
+
sql`(${documents.title} ILIKE ${`%${q}%`} OR ${documents.filename} ILIKE ${`%${q}%`})`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Category filter - match documents by fileType prefix
|
|
300
|
+
if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
|
|
301
|
+
const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
|
|
302
|
+
if (Array.isArray(fileTypePrefix)) {
|
|
303
|
+
// For multiple file types (e.g., Documents includes 'application' and 'custom')
|
|
304
|
+
const orConditions = fileTypePrefix.map(
|
|
305
|
+
(prefix) => sql`${documents.fileType} ILIKE ${`${prefix}%`}`,
|
|
306
|
+
);
|
|
307
|
+
whereConditions.push(sql`(${sql.join(orConditions, sql` OR `)})`);
|
|
308
|
+
|
|
309
|
+
// Exclude custom/document and source_type='file' from Documents category
|
|
310
|
+
if (category === FilesTabs.Documents) {
|
|
311
|
+
whereConditions.push(sql`${documents.fileType} != ${'custom/document'}`, sql`${documents.sourceType} != ${'file'}`);
|
|
312
|
+
}
|
|
313
|
+
} else if (fileTypePrefix) {
|
|
314
|
+
whereConditions.push(sql`${documents.fileType} ILIKE ${`${fileTypePrefix}%`}`);
|
|
315
|
+
} else {
|
|
316
|
+
// Exclude documents from other categories (Images, Videos, Audios, Websites)
|
|
317
|
+
return sql`
|
|
318
|
+
SELECT
|
|
319
|
+
NULL::varchar(30) as id,
|
|
320
|
+
NULL::text as name,
|
|
321
|
+
NULL::varchar(255) as file_type,
|
|
322
|
+
NULL::integer as size,
|
|
323
|
+
NULL::text as url,
|
|
324
|
+
NULL::timestamp with time zone as created_at,
|
|
325
|
+
NULL::timestamp with time zone as updated_at,
|
|
326
|
+
NULL::uuid as chunk_task_id,
|
|
327
|
+
NULL::uuid as embedding_task_id,
|
|
328
|
+
NULL::jsonb as editor_data,
|
|
329
|
+
NULL::text as content,
|
|
330
|
+
NULL::jsonb as metadata,
|
|
331
|
+
NULL::text as source_type
|
|
332
|
+
WHERE false
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Knowledge base filter for documents
|
|
338
|
+
// Documents don't have knowledge base association currently, so skip if knowledgeBaseId is set
|
|
339
|
+
if (knowledgeBaseId) {
|
|
340
|
+
return sql`
|
|
341
|
+
SELECT
|
|
342
|
+
NULL::varchar(30) as id,
|
|
343
|
+
NULL::text as name,
|
|
344
|
+
NULL::varchar(255) as file_type,
|
|
345
|
+
NULL::integer as size,
|
|
346
|
+
NULL::text as url,
|
|
347
|
+
NULL::timestamp with time zone as created_at,
|
|
348
|
+
NULL::timestamp with time zone as updated_at,
|
|
349
|
+
NULL::uuid as chunk_task_id,
|
|
350
|
+
NULL::uuid as embedding_task_id,
|
|
351
|
+
NULL::jsonb as editor_data,
|
|
352
|
+
NULL::text as content,
|
|
353
|
+
NULL::jsonb as metadata,
|
|
354
|
+
NULL::text as source_type
|
|
355
|
+
WHERE false
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return sql`
|
|
360
|
+
SELECT
|
|
361
|
+
id,
|
|
362
|
+
COALESCE(title, filename, 'Untitled') as name,
|
|
363
|
+
file_type,
|
|
364
|
+
total_char_count as size,
|
|
365
|
+
source as url,
|
|
366
|
+
created_at,
|
|
367
|
+
updated_at,
|
|
368
|
+
NULL as chunk_task_id,
|
|
369
|
+
NULL as embedding_task_id,
|
|
370
|
+
editor_data,
|
|
371
|
+
content,
|
|
372
|
+
metadata,
|
|
373
|
+
'document' as source_type
|
|
374
|
+
FROM ${documents}
|
|
375
|
+
WHERE ${sql.join(whereConditions, sql` AND `)}
|
|
376
|
+
`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private buildOrderClause(
|
|
380
|
+
sortType?: string,
|
|
381
|
+
sorter?: string,
|
|
382
|
+
): ReturnType<typeof sql.raw> | ReturnType<typeof sql> {
|
|
383
|
+
const sortableFields: Record<string, string> = {
|
|
384
|
+
createdAt: 'created_at',
|
|
385
|
+
name: 'name',
|
|
386
|
+
size: 'size',
|
|
387
|
+
updatedAt: 'updated_at',
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (sorter && sortType && sorter in sortableFields) {
|
|
391
|
+
const direction = sortType.toLowerCase() === SortType.Asc ? 'ASC' : 'DESC';
|
|
392
|
+
return sql.raw(`${sortableFields[sorter]} ${direction}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return sql.raw('created_at DESC');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private getFileTypePrefix(category: FilesTabs): string | string[] {
|
|
399
|
+
switch (category) {
|
|
400
|
+
case FilesTabs.Audios: {
|
|
401
|
+
return 'audio';
|
|
402
|
+
}
|
|
403
|
+
case FilesTabs.Documents: {
|
|
404
|
+
return ['application', 'custom'];
|
|
405
|
+
}
|
|
406
|
+
case FilesTabs.Images: {
|
|
407
|
+
return 'image';
|
|
408
|
+
}
|
|
409
|
+
case FilesTabs.Videos: {
|
|
410
|
+
return 'video';
|
|
411
|
+
}
|
|
412
|
+
case FilesTabs.Websites: {
|
|
413
|
+
return 'text/html';
|
|
414
|
+
}
|
|
415
|
+
default: {
|
|
416
|
+
return '';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -1056,6 +1056,7 @@ const aihubmixModels: AIChatModelCard[] = [
|
|
|
1056
1056
|
id: 'gemini-2.5-flash-image',
|
|
1057
1057
|
maxOutput: 8192,
|
|
1058
1058
|
pricing: {
|
|
1059
|
+
approximatePricePerImage: 0.039,
|
|
1059
1060
|
units: [
|
|
1060
1061
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
1061
1062
|
{ name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -370,6 +370,7 @@ const googleChatModels: AIChatModelCard[] = [
|
|
|
370
370
|
id: 'gemini-2.5-flash-image',
|
|
371
371
|
maxOutput: 8192,
|
|
372
372
|
pricing: {
|
|
373
|
+
approximatePricePerImage: 0.039,
|
|
373
374
|
units: [
|
|
374
375
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
375
376
|
{ name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -392,6 +393,7 @@ const googleChatModels: AIChatModelCard[] = [
|
|
|
392
393
|
id: 'gemini-2.5-flash-image-preview',
|
|
393
394
|
maxOutput: 8192,
|
|
394
395
|
pricing: {
|
|
396
|
+
approximatePricePerImage: 0.039,
|
|
395
397
|
units: [
|
|
396
398
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
397
399
|
{ name: 'imageInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -864,6 +866,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
864
866
|
releasedAt: '2025-08-26',
|
|
865
867
|
parameters: nanoBananaParameters,
|
|
866
868
|
pricing: {
|
|
869
|
+
approximatePricePerImage: 0.039,
|
|
867
870
|
units: [
|
|
868
871
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
869
872
|
{ name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -880,6 +883,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
880
883
|
releasedAt: '2025-08-26',
|
|
881
884
|
parameters: CHAT_MODEL_IMAGE_GENERATION_PARAMS,
|
|
882
885
|
pricing: {
|
|
886
|
+
approximatePricePerImage: 0.039,
|
|
883
887
|
units: [
|
|
884
888
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
885
889
|
{ name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -892,7 +896,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
892
896
|
id: 'imagen-4.0-generate-001',
|
|
893
897
|
enabled: true,
|
|
894
898
|
type: 'image',
|
|
895
|
-
description: 'Imagen
|
|
899
|
+
description: 'Imagen 第四代文生图模型系列',
|
|
896
900
|
organization: 'Deepmind',
|
|
897
901
|
releasedAt: '2025-08-15',
|
|
898
902
|
parameters: imagenGenParameters,
|
|
@@ -905,7 +909,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
905
909
|
id: 'imagen-4.0-ultra-generate-001',
|
|
906
910
|
enabled: true,
|
|
907
911
|
type: 'image',
|
|
908
|
-
description: 'Imagen
|
|
912
|
+
description: 'Imagen 第四代文生图模型系列的 Ultra 版本',
|
|
909
913
|
organization: 'Deepmind',
|
|
910
914
|
releasedAt: '2025-08-15',
|
|
911
915
|
parameters: imagenGenParameters,
|
|
@@ -918,7 +922,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
918
922
|
id: 'imagen-4.0-fast-generate-001',
|
|
919
923
|
enabled: true,
|
|
920
924
|
type: 'image',
|
|
921
|
-
description: 'Imagen
|
|
925
|
+
description: 'Imagen 第四代文生图模型系列的快速版本',
|
|
922
926
|
organization: 'Deepmind',
|
|
923
927
|
releasedAt: '2025-08-15',
|
|
924
928
|
parameters: imagenGenParameters,
|
|
@@ -930,7 +934,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
930
934
|
displayName: 'Imagen 4 Preview 06-06',
|
|
931
935
|
id: 'imagen-4.0-generate-preview-06-06',
|
|
932
936
|
type: 'image',
|
|
933
|
-
description: 'Imagen
|
|
937
|
+
description: 'Imagen 第四代文生图模型系列',
|
|
934
938
|
organization: 'Deepmind',
|
|
935
939
|
releasedAt: '2025-06-06',
|
|
936
940
|
parameters: imagenGenParameters,
|
|
@@ -942,7 +946,7 @@ const googleImageModels: AIImageModelCard[] = [
|
|
|
942
946
|
displayName: 'Imagen 4 Ultra Preview 06-06',
|
|
943
947
|
id: 'imagen-4.0-ultra-generate-preview-06-06',
|
|
944
948
|
type: 'image',
|
|
945
|
-
description: 'Imagen
|
|
949
|
+
description: 'Imagen 第四代文生图模型系列的 Ultra 版本',
|
|
946
950
|
organization: 'Deepmind',
|
|
947
951
|
releasedAt: '2025-06-11',
|
|
948
952
|
parameters: imagenGenParameters,
|
|
@@ -1171,31 +1171,13 @@ export const openaiImageModels: AIImageModelCard[] = [
|
|
|
1171
1171
|
id: 'gpt-image-1',
|
|
1172
1172
|
parameters: gptImage1ParamsSchema,
|
|
1173
1173
|
pricing: {
|
|
1174
|
+
approximatePricePerImage: 0.042,
|
|
1174
1175
|
units: [
|
|
1175
1176
|
{ name: 'textInput', rate: 5, strategy: 'fixed', unit: 'millionTokens' },
|
|
1176
1177
|
{ name: 'textInput_cacheRead', rate: 1.25, strategy: 'fixed', unit: 'millionTokens' },
|
|
1177
1178
|
{ name: 'imageInput', rate: 10, strategy: 'fixed', unit: 'millionTokens' },
|
|
1178
1179
|
{ name: 'imageInput_cacheRead', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
1179
1180
|
{ name: 'imageOutput', rate: 40, strategy: 'fixed', unit: 'millionTokens' },
|
|
1180
|
-
{
|
|
1181
|
-
lookup: {
|
|
1182
|
-
prices: {
|
|
1183
|
-
high_1024x1024: 0.167,
|
|
1184
|
-
high_1024x1536: 0.25,
|
|
1185
|
-
high_1536x1024: 0.25,
|
|
1186
|
-
low_1024x1024: 0.011,
|
|
1187
|
-
low_1024x1536: 0.016,
|
|
1188
|
-
low_1536x1024: 0.016,
|
|
1189
|
-
medium_1024x1024: 0.042,
|
|
1190
|
-
medium_1024x1536: 0.063,
|
|
1191
|
-
medium_1536x1024: 0.063,
|
|
1192
|
-
},
|
|
1193
|
-
pricingParams: ['quality', 'size'],
|
|
1194
|
-
},
|
|
1195
|
-
name: 'imageGeneration',
|
|
1196
|
-
strategy: 'lookup',
|
|
1197
|
-
unit: 'image',
|
|
1198
|
-
},
|
|
1199
1181
|
],
|
|
1200
1182
|
},
|
|
1201
1183
|
resolutions: ['1024x1024', '1024x1536', '1536x1024'],
|
|
@@ -1208,28 +1190,13 @@ export const openaiImageModels: AIImageModelCard[] = [
|
|
|
1208
1190
|
id: 'gpt-image-1-mini',
|
|
1209
1191
|
parameters: gptImage1ParamsSchema,
|
|
1210
1192
|
pricing: {
|
|
1193
|
+
approximatePricePerImage: 0.011,
|
|
1211
1194
|
units: [
|
|
1212
1195
|
{ name: 'textInput', rate: 2, strategy: 'fixed', unit: 'millionTokens' },
|
|
1213
1196
|
{ name: 'textInput_cacheRead', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
|
|
1214
1197
|
{ name: 'imageInput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
1215
1198
|
{ name: 'imageInput_cacheRead', rate: 0.25, strategy: 'fixed', unit: 'millionTokens' },
|
|
1216
1199
|
{ name: 'imageOutput', rate: 8, strategy: 'fixed', unit: 'millionTokens' },
|
|
1217
|
-
{
|
|
1218
|
-
lookup: {
|
|
1219
|
-
prices: {
|
|
1220
|
-
low_1024x1024: 0.005,
|
|
1221
|
-
low_1024x1536: 0.006,
|
|
1222
|
-
low_1536x1024: 0.006,
|
|
1223
|
-
medium_1024x1024: 0.011,
|
|
1224
|
-
medium_1024x1536: 0.015,
|
|
1225
|
-
medium_1536x1024: 0.015,
|
|
1226
|
-
},
|
|
1227
|
-
pricingParams: ['quality', 'size'],
|
|
1228
|
-
},
|
|
1229
|
-
name: 'imageGeneration',
|
|
1230
|
-
strategy: 'lookup',
|
|
1231
|
-
unit: 'image',
|
|
1232
|
-
},
|
|
1233
1200
|
],
|
|
1234
1201
|
},
|
|
1235
1202
|
releasedAt: '2025-10-06',
|
|
@@ -41,6 +41,7 @@ const openrouterChatModels: AIChatModelCard[] = [
|
|
|
41
41
|
id: 'google/gemini-2.5-flash-image-preview',
|
|
42
42
|
maxOutput: 8192,
|
|
43
43
|
pricing: {
|
|
44
|
+
approximatePricePerImage: 0.039,
|
|
44
45
|
units: [
|
|
45
46
|
{ name: 'imageOutput', rate: 30, strategy: 'fixed', unit: 'millionTokens' },
|
|
46
47
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -135,6 +135,7 @@ const vertexaiChatModels: AIChatModelCard[] = [
|
|
|
135
135
|
id: 'gemini-2.5-flash-image-preview',
|
|
136
136
|
maxOutput: 8192,
|
|
137
137
|
pricing: {
|
|
138
|
+
approximatePricePerImage: 0.039,
|
|
138
139
|
units: [
|
|
139
140
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
140
141
|
{ name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -291,6 +292,7 @@ const vertexaiImageModels: AIImageModelCard[] = [
|
|
|
291
292
|
releasedAt: '2025-08-26',
|
|
292
293
|
parameters: nanoBananaParameters,
|
|
293
294
|
pricing: {
|
|
295
|
+
approximatePricePerImage: 0.039,
|
|
294
296
|
units: [
|
|
295
297
|
{ name: 'textInput', rate: 0.3, strategy: 'fixed', unit: 'millionTokens' },
|
|
296
298
|
{ name: 'textOutput', rate: 2.5, strategy: 'fixed', unit: 'millionTokens' },
|
|
@@ -185,6 +185,10 @@ export interface LookupPricingUnit extends PricingUnitBase {
|
|
|
185
185
|
export type PricingUnit = FixedPricingUnit | TieredPricingUnit | LookupPricingUnit;
|
|
186
186
|
|
|
187
187
|
export interface Pricing {
|
|
188
|
+
/**
|
|
189
|
+
* Fallback approximate per-image price (USD) when detailed pricing table is unavailable
|
|
190
|
+
*/
|
|
191
|
+
approximatePricePerImage?: number;
|
|
188
192
|
currency?: ModelPriceCurrency;
|
|
189
193
|
units: PricingUnit[];
|
|
190
194
|
}
|
|
@@ -391,13 +395,22 @@ export const ToggleAiModelEnableSchema = z.object({
|
|
|
391
395
|
|
|
392
396
|
export type ToggleAiModelEnableParams = z.infer<typeof ToggleAiModelEnableSchema>;
|
|
393
397
|
|
|
394
|
-
//
|
|
395
|
-
|
|
396
398
|
export interface AiModelForSelect {
|
|
397
399
|
abilities: ModelAbilities;
|
|
400
|
+
/**
|
|
401
|
+
* Approximate per-image price (USD), used when exact calculation is not possible
|
|
402
|
+
*/
|
|
403
|
+
approximatePricePerImage?: number;
|
|
398
404
|
contextWindowTokens?: number;
|
|
405
|
+
description?: string;
|
|
399
406
|
displayName?: string;
|
|
400
407
|
id: string;
|
|
408
|
+
parameters?: ModelParamsSchema;
|
|
409
|
+
/**
|
|
410
|
+
* Exact per-image price (USD) calculated from pricing units
|
|
411
|
+
*/
|
|
412
|
+
pricePerImage?: number;
|
|
413
|
+
pricing?: Pricing;
|
|
401
414
|
}
|
|
402
415
|
|
|
403
416
|
export interface EnabledAiModel {
|
|
@@ -2,3 +2,4 @@ export { convertAnthropicUsage } from './anthropic';
|
|
|
2
2
|
export { convertGoogleAIUsage } from './google-ai';
|
|
3
3
|
export { convertOpenAIResponseUsage, convertOpenAIUsage } from './openai';
|
|
4
4
|
export { computeImageCost } from './utils/computeImageCost';
|
|
5
|
+
export { resolveImageSinglePrice } from './utils/resolveImageSinglePrice';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Pricing } from 'model-bank';
|
|
2
|
+
|
|
3
|
+
export interface ImageSinglePriceResult {
|
|
4
|
+
approximatePrice?: number;
|
|
5
|
+
price?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_REFERENCE_MP = (1024 * 1024) / 1_000_000;
|
|
9
|
+
|
|
10
|
+
export const resolveImageSinglePrice = (pricing?: Pricing): ImageSinglePriceResult => {
|
|
11
|
+
if (!pricing) return {};
|
|
12
|
+
|
|
13
|
+
// Priority 1: Use approximate price if explicitly provided
|
|
14
|
+
if (typeof pricing.approximatePricePerImage === 'number') {
|
|
15
|
+
return { approximatePrice: pricing.approximatePricePerImage };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Priority 2: Calculate exact price from pricing units
|
|
19
|
+
const imageGenerationUnit = pricing.units.find((unit) => unit.name === 'imageGeneration');
|
|
20
|
+
if (!imageGenerationUnit) return {};
|
|
21
|
+
|
|
22
|
+
if (imageGenerationUnit.strategy === 'fixed') {
|
|
23
|
+
if (imageGenerationUnit.unit === 'image') {
|
|
24
|
+
return { price: imageGenerationUnit.rate };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (imageGenerationUnit.unit === 'megapixel') {
|
|
28
|
+
return { price: imageGenerationUnit.rate * DEFAULT_REFERENCE_MP };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Lookup/tiered pricing typically requires explicit configuration; treat as unavailable here.
|
|
33
|
+
return {};
|
|
34
|
+
};
|