@lobehub/chat 1.43.5 → 1.44.0

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 (299) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/self-hosting/server-database/docker-compose.mdx +2 -2
  4. package/locales/ar/common.json +1 -0
  5. package/locales/ar/modelProvider.json +176 -0
  6. package/locales/ar/setting.json +1 -0
  7. package/locales/bg-BG/common.json +1 -0
  8. package/locales/bg-BG/modelProvider.json +176 -0
  9. package/locales/bg-BG/setting.json +1 -0
  10. package/locales/de-DE/common.json +1 -0
  11. package/locales/de-DE/modelProvider.json +176 -0
  12. package/locales/de-DE/setting.json +1 -0
  13. package/locales/en-US/common.json +1 -0
  14. package/locales/en-US/modelProvider.json +176 -0
  15. package/locales/en-US/setting.json +1 -0
  16. package/locales/es-ES/common.json +1 -0
  17. package/locales/es-ES/modelProvider.json +176 -0
  18. package/locales/es-ES/setting.json +1 -0
  19. package/locales/fa-IR/common.json +1 -0
  20. package/locales/fa-IR/modelProvider.json +176 -0
  21. package/locales/fa-IR/setting.json +1 -0
  22. package/locales/fr-FR/common.json +1 -0
  23. package/locales/fr-FR/modelProvider.json +176 -0
  24. package/locales/fr-FR/setting.json +1 -0
  25. package/locales/it-IT/common.json +1 -0
  26. package/locales/it-IT/modelProvider.json +176 -0
  27. package/locales/it-IT/setting.json +1 -0
  28. package/locales/ja-JP/common.json +1 -0
  29. package/locales/ja-JP/modelProvider.json +176 -0
  30. package/locales/ja-JP/setting.json +1 -0
  31. package/locales/ko-KR/common.json +1 -0
  32. package/locales/ko-KR/modelProvider.json +176 -0
  33. package/locales/ko-KR/setting.json +1 -0
  34. package/locales/nl-NL/common.json +1 -0
  35. package/locales/nl-NL/modelProvider.json +176 -0
  36. package/locales/nl-NL/setting.json +1 -0
  37. package/locales/pl-PL/common.json +1 -0
  38. package/locales/pl-PL/modelProvider.json +176 -0
  39. package/locales/pl-PL/setting.json +1 -0
  40. package/locales/pt-BR/common.json +1 -0
  41. package/locales/pt-BR/modelProvider.json +176 -0
  42. package/locales/pt-BR/setting.json +1 -0
  43. package/locales/ru-RU/common.json +1 -0
  44. package/locales/ru-RU/modelProvider.json +176 -0
  45. package/locales/ru-RU/setting.json +1 -0
  46. package/locales/tr-TR/common.json +1 -0
  47. package/locales/tr-TR/modelProvider.json +176 -0
  48. package/locales/tr-TR/setting.json +1 -0
  49. package/locales/vi-VN/common.json +1 -0
  50. package/locales/vi-VN/modelProvider.json +176 -0
  51. package/locales/vi-VN/setting.json +1 -0
  52. package/locales/zh-CN/common.json +1 -0
  53. package/locales/zh-CN/modelProvider.json +176 -0
  54. package/locales/zh-CN/setting.json +1 -0
  55. package/locales/zh-TW/common.json +1 -0
  56. package/locales/zh-TW/modelProvider.json +176 -0
  57. package/locales/zh-TW/setting.json +1 -0
  58. package/package.json +4 -4
  59. package/src/app/(main)/(mobile)/me/settings/features/Category.tsx +1 -1
  60. package/src/app/(main)/(mobile)/me/settings/features/useCategory.tsx +12 -5
  61. package/src/app/(main)/changelog/features/VersionTag.tsx +1 -2
  62. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/Thread.tsx +1 -1
  63. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/ThreadItem.tsx +1 -2
  64. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +0 -1
  65. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +1 -1
  66. package/src/app/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +1 -1
  67. package/src/app/(main)/chat/(workspace)/@conversation/features/ZenModeToast/Toast.tsx +1 -1
  68. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/ThreadItem/index.tsx +0 -2
  69. package/src/app/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/index.tsx +0 -1
  70. package/src/app/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Tags.tsx +2 -3
  71. package/src/app/(main)/chat/(workspace)/_layout/Desktop/index.tsx +6 -1
  72. package/src/app/(main)/chat/@session/features/SessionListContent/CollapseGroup/index.tsx +1 -1
  73. package/src/app/(main)/chat/features/Migration/Start.tsx +1 -1
  74. package/src/app/(main)/discover/(detail)/assistant/[slug]/features/ConversationExample/TopicList.tsx +4 -3
  75. package/src/app/(main)/discover/(detail)/assistant/[slug]/features/Header.tsx +1 -1
  76. package/src/app/(main)/discover/(detail)/features/ShareButton.tsx +2 -1
  77. package/src/app/(main)/discover/(detail)/model/[...slugs]/features/Header.tsx +1 -1
  78. package/src/app/(main)/discover/(detail)/plugin/[slug]/features/Header.tsx +1 -1
  79. package/src/app/(main)/discover/(detail)/provider/[slug]/features/Header.tsx +1 -1
  80. package/src/app/(main)/discover/(list)/_layout/Desktop/Nav.tsx +0 -1
  81. package/src/app/(main)/discover/(list)/assistants/features/Card.tsx +1 -1
  82. package/src/app/(main)/discover/(list)/models/features/Card.tsx +1 -1
  83. package/src/app/(main)/discover/(list)/plugins/features/Card.tsx +1 -1
  84. package/src/app/(main)/discover/(list)/providers/features/Card.tsx +1 -1
  85. package/src/app/(main)/discover/components/GridLoadingCard.tsx +2 -1
  86. package/src/app/(main)/discover/components/Title.tsx +1 -1
  87. package/src/app/(main)/files/(content)/@menu/features/KnowledgeBase/EmptyStatus.tsx +1 -1
  88. package/src/app/(main)/files/(content)/@menu/features/KnowledgeBase/Item/index.tsx +0 -1
  89. package/src/app/(main)/files/(content)/@modal/(.)[id]/FullscreenModal.tsx +2 -2
  90. package/src/app/(main)/files/(content)/NotSupportClient.tsx +2 -2
  91. package/src/app/(main)/profile/_layout/Desktop/SideBar.tsx +1 -1
  92. package/src/app/(main)/profile/stats/features/ShareButton/Preview.tsx +5 -5
  93. package/src/app/(main)/repos/[id]/evals/components/Container.tsx +1 -1
  94. package/src/app/(main)/repos/[id]/evals/dataset/DatasetList/Item.tsx +0 -1
  95. package/src/app/(main)/settings/_layout/Desktop/SideBar.tsx +1 -1
  96. package/src/app/(main)/settings/about/features/ItemCard.tsx +3 -3
  97. package/src/app/(main)/settings/about/features/Version.tsx +1 -1
  98. package/src/app/(main)/settings/hooks/useCategory.tsx +22 -9
  99. package/src/app/(main)/settings/llm/components/ProviderConfig/index.tsx +2 -1
  100. package/src/app/(main)/settings/llm/components/ProviderModelList/ModelFetcher.tsx +0 -1
  101. package/src/app/(main)/settings/provider/(detail)/[id]/index.tsx +19 -0
  102. package/src/app/(main)/settings/provider/(detail)/[id]/page.tsx +95 -0
  103. package/src/app/(main)/settings/provider/(detail)/azure/page.tsx +119 -0
  104. package/src/app/(main)/settings/provider/(detail)/bedrock/page.tsx +91 -0
  105. package/src/app/(main)/settings/provider/(detail)/cloudflare/page.tsx +58 -0
  106. package/src/app/(main)/settings/provider/(detail)/github/page.tsx +67 -0
  107. package/src/app/(main)/settings/provider/(detail)/huggingface/page.tsx +67 -0
  108. package/src/app/(main)/settings/provider/(detail)/ollama/Checker.tsx +73 -0
  109. package/src/app/(main)/settings/provider/(detail)/ollama/page.tsx +34 -0
  110. package/src/app/(main)/settings/provider/(detail)/openai/page.tsx +23 -0
  111. package/src/app/(main)/settings/provider/(detail)/wenxin/page.tsx +61 -0
  112. package/src/app/(main)/settings/provider/(list)/Footer.tsx +36 -0
  113. package/src/app/(main)/settings/provider/(list)/ProviderGrid/Card.tsx +134 -0
  114. package/src/app/(main)/settings/provider/(list)/ProviderGrid/index.tsx +91 -0
  115. package/src/app/(main)/settings/provider/(list)/index.tsx +19 -0
  116. package/src/app/(main)/settings/provider/ProviderMenu/AddNew.tsx +28 -0
  117. package/src/app/(main)/settings/provider/ProviderMenu/All.tsx +29 -0
  118. package/src/app/(main)/settings/provider/ProviderMenu/Item.tsx +69 -0
  119. package/src/app/(main)/settings/provider/ProviderMenu/List.tsx +76 -0
  120. package/src/app/(main)/settings/provider/ProviderMenu/SearchResult.tsx +43 -0
  121. package/src/app/(main)/settings/provider/ProviderMenu/SkeletonList.tsx +60 -0
  122. package/src/app/(main)/settings/provider/ProviderMenu/SortProviderModal/GroupItem.tsx +30 -0
  123. package/src/app/(main)/settings/provider/ProviderMenu/SortProviderModal/index.tsx +91 -0
  124. package/src/app/(main)/settings/provider/ProviderMenu/index.tsx +80 -0
  125. package/src/app/(main)/settings/provider/_layout/Desktop.tsx +37 -0
  126. package/src/app/(main)/settings/provider/_layout/Mobile.tsx +14 -0
  127. package/src/app/(main)/settings/provider/const.ts +20 -0
  128. package/src/app/(main)/settings/provider/features/CreateNewProvider/index.tsx +146 -0
  129. package/src/app/(main)/settings/provider/features/ModelList/CreateNewModelModal/Form.tsx +105 -0
  130. package/src/app/(main)/settings/provider/features/ModelList/CreateNewModelModal/index.tsx +69 -0
  131. package/src/app/(main)/settings/provider/features/ModelList/DisabledModels.tsx +29 -0
  132. package/src/app/(main)/settings/provider/features/ModelList/EmptyModels.tsx +101 -0
  133. package/src/app/(main)/settings/provider/features/ModelList/EnabledModelList/index.tsx +85 -0
  134. package/src/app/(main)/settings/provider/features/ModelList/ModelConfigModal/Form.tsx +109 -0
  135. package/src/app/(main)/settings/provider/features/ModelList/ModelConfigModal/index.tsx +76 -0
  136. package/src/app/(main)/settings/provider/features/ModelList/ModelItem.tsx +346 -0
  137. package/src/app/(main)/settings/provider/features/ModelList/ModelTitle/Search.tsx +37 -0
  138. package/src/app/(main)/settings/provider/features/ModelList/ModelTitle/index.tsx +145 -0
  139. package/src/app/(main)/settings/provider/features/ModelList/SearchResult.tsx +67 -0
  140. package/src/app/(main)/settings/provider/features/ModelList/SkeletonList.tsx +63 -0
  141. package/src/app/(main)/settings/provider/features/ModelList/SortModelModal/ListItem.tsx +20 -0
  142. package/src/app/(main)/settings/provider/features/ModelList/SortModelModal/index.tsx +96 -0
  143. package/src/app/(main)/settings/provider/features/ModelList/index.tsx +59 -0
  144. package/src/app/(main)/settings/provider/features/ProviderConfig/Checker.tsx +120 -0
  145. package/src/app/(main)/settings/provider/features/ProviderConfig/SkeletonInput.tsx +5 -0
  146. package/src/app/(main)/settings/provider/features/ProviderConfig/UpdateProviderInfo/SettingModal.tsx +137 -0
  147. package/src/app/(main)/settings/provider/features/ProviderConfig/UpdateProviderInfo/index.tsx +49 -0
  148. package/src/app/(main)/settings/provider/features/ProviderConfig/index.tsx +343 -0
  149. package/src/app/(main)/settings/provider/layout.tsx +21 -0
  150. package/src/app/(main)/settings/provider/page.tsx +17 -0
  151. package/src/app/(main)/settings/provider/type.ts +5 -0
  152. package/src/app/(main)/settings/sync/features/DeviceInfo/Card.tsx +1 -1
  153. package/src/app/(main)/settings/sync/features/DeviceInfo/index.tsx +1 -1
  154. package/src/app/@modal/(.)changelog/modal/features/ReadDetail.tsx +1 -1
  155. package/src/app/@modal/(.)changelog/modal/features/VersionTag.tsx +1 -2
  156. package/src/app/@modal/(.)changelog/modal/layout.tsx +1 -1
  157. package/src/components/Cell/index.tsx +1 -1
  158. package/src/components/DragUpload/index.tsx +2 -3
  159. package/src/components/FeatureList/index.tsx +1 -1
  160. package/src/components/FileParsingStatus/EmbeddingStatus.tsx +1 -1
  161. package/src/components/FileParsingStatus/index.tsx +1 -1
  162. package/src/components/FunctionModal/style.tsx +2 -2
  163. package/src/components/GoBack/index.tsx +1 -2
  164. package/src/components/HotKeys/index.tsx +1 -1
  165. package/src/components/InstantSwitch/index.tsx +28 -0
  166. package/src/components/Menu/index.tsx +1 -1
  167. package/src/components/ModelSelect/index.tsx +2 -3
  168. package/src/components/Notification/index.tsx +2 -1
  169. package/src/components/StatisticCard/index.tsx +5 -6
  170. package/src/config/aiModels/ai21.ts +38 -0
  171. package/src/config/aiModels/ai360.ts +71 -0
  172. package/src/config/aiModels/anthropic.ts +152 -0
  173. package/src/config/aiModels/azure.ts +86 -0
  174. package/src/config/aiModels/baichuan.ts +107 -0
  175. package/src/config/aiModels/bedrock.ts +315 -0
  176. package/src/config/aiModels/cloudflare.ts +88 -0
  177. package/src/config/aiModels/deepseek.ts +27 -0
  178. package/src/config/aiModels/fireworksai.ts +232 -0
  179. package/src/config/aiModels/giteeai.ts +137 -0
  180. package/src/config/aiModels/github.ts +273 -0
  181. package/src/config/aiModels/google.ts +317 -0
  182. package/src/config/aiModels/groq.ts +202 -0
  183. package/src/config/aiModels/higress.ts +2828 -0
  184. package/src/config/aiModels/huggingface.ts +56 -0
  185. package/src/config/aiModels/hunyuan.ts +151 -0
  186. package/src/config/aiModels/index.ts +98 -0
  187. package/src/config/aiModels/internlm.ts +40 -0
  188. package/src/config/aiModels/minimax.ts +55 -0
  189. package/src/config/aiModels/mistral.ts +172 -0
  190. package/src/config/aiModels/moonshot.ts +44 -0
  191. package/src/config/aiModels/novita.ts +124 -0
  192. package/src/config/aiModels/ollama.ts +412 -0
  193. package/src/config/aiModels/openai.ts +537 -0
  194. package/src/config/aiModels/openrouter.ts +252 -0
  195. package/src/config/aiModels/perplexity.ts +67 -0
  196. package/src/config/aiModels/qwen.ts +302 -0
  197. package/src/config/aiModels/sensenova.ts +114 -0
  198. package/src/config/aiModels/siliconcloud.ts +679 -0
  199. package/src/config/aiModels/spark.ts +68 -0
  200. package/src/config/aiModels/stepfun.ts +153 -0
  201. package/src/config/aiModels/taichu.ts +19 -0
  202. package/src/config/aiModels/togetherai.ts +334 -0
  203. package/src/config/aiModels/upstage.ts +37 -0
  204. package/src/config/aiModels/wenxin.ts +171 -0
  205. package/src/config/aiModels/xai.ts +72 -0
  206. package/src/config/aiModels/zeroone.ts +156 -0
  207. package/src/config/aiModels/zhipu.ts +235 -0
  208. package/src/config/featureFlags/schema.ts +3 -0
  209. package/src/config/modelProviders/anthropic.ts +1 -0
  210. package/src/config/modelProviders/github.ts +0 -1
  211. package/src/config/modelProviders/google.ts +1 -0
  212. package/src/config/modelProviders/stepfun.ts +2 -0
  213. package/src/database/migrations/0013_add_ai_infra.sql +44 -0
  214. package/src/database/migrations/meta/0013_snapshot.json +3598 -0
  215. package/src/database/migrations/meta/_journal.json +7 -0
  216. package/src/database/repositories/aiInfra/index.ts +115 -0
  217. package/src/database/schemas/aiInfra.ts +69 -0
  218. package/src/database/schemas/index.ts +1 -0
  219. package/src/database/server/models/__tests__/aiModel.test.ts +318 -0
  220. package/src/database/server/models/__tests__/aiProvider.test.ts +373 -0
  221. package/src/database/server/models/aiModel.ts +250 -0
  222. package/src/database/server/models/aiProvider.ts +234 -0
  223. package/src/features/AgentSetting/AgentPrompt/index.tsx +2 -2
  224. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -1
  225. package/src/features/ChatInput/ActionBar/Tools/index.tsx +2 -3
  226. package/src/features/ChatInput/ActionBar/Upload/ServerMode.tsx +2 -3
  227. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +3 -2
  228. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +2 -2
  229. package/src/features/ChatInput/Mobile/Files/FileItem/File.tsx +2 -2
  230. package/src/features/ChatInput/Mobile/InputArea/index.tsx +1 -1
  231. package/src/features/ChatInput/STT/common.tsx +1 -1
  232. package/src/features/Conversation/Error/style.tsx +2 -2
  233. package/src/features/Conversation/Messages/Assistant/FileChunks/Item/style.ts +2 -2
  234. package/src/features/Conversation/Messages/Assistant/FileChunks/index.tsx +1 -1
  235. package/src/features/Conversation/Messages/Assistant/ToolCallItem/Inspector/style.ts +2 -3
  236. package/src/features/Conversation/Messages/Assistant/ToolCallItem/style.ts +2 -3
  237. package/src/features/Conversation/Messages/User/FileListViewer/Item.tsx +0 -1
  238. package/src/features/Conversation/components/BackBottom/style.ts +2 -2
  239. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/Icon.tsx +2 -3
  240. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +3 -3
  241. package/src/features/Conversation/components/MarkdownElements/LobeThinking/Render.tsx +1 -1
  242. package/src/features/Conversation/components/OTPInput.tsx +2 -2
  243. package/src/features/DataImporter/Loading.tsx +1 -1
  244. package/src/features/FileManager/FileList/EmptyStatus.tsx +1 -1
  245. package/src/features/FileManager/FileList/index.tsx +1 -1
  246. package/src/features/FileManager/UploadDock/Item.tsx +1 -1
  247. package/src/features/FileManager/UploadDock/index.tsx +4 -4
  248. package/src/features/FileViewer/NotSupport/index.tsx +1 -1
  249. package/src/features/FileViewer/Renderer/MSDoc/index.tsx +0 -1
  250. package/src/features/FileViewer/Renderer/TXT/index.tsx +1 -1
  251. package/src/features/InitClientDB/EnableModal.tsx +1 -1
  252. package/src/features/InitClientDB/ErrorResult.tsx +1 -1
  253. package/src/features/InitClientDB/InitIndicator.tsx +1 -1
  254. package/src/features/KnowledgeBaseModal/AddFilesToKnowledgeBase/SelectForm.tsx +0 -1
  255. package/src/features/ModelSwitchPanel/index.tsx +2 -2
  256. package/src/features/PluginsUI/Render/Loading.tsx +0 -1
  257. package/src/features/Portal/Home/Body/Files/FileList/Item.tsx +1 -1
  258. package/src/features/Portal/Home/Body/Plugins/ArtifactList/Item/style.ts +1 -2
  259. package/src/features/Setting/SettingContainer.tsx +8 -1
  260. package/src/features/ShareModal/ShareImage/style.ts +2 -2
  261. package/src/features/ShareModal/style.ts +2 -2
  262. package/src/features/User/DataStatistics.tsx +1 -1
  263. package/src/hooks/useEnabledChatModels.ts +10 -1
  264. package/src/hooks/useModelSupportToolUse.ts +15 -0
  265. package/src/hooks/useModelSupportVision.ts +15 -0
  266. package/src/layout/AuthProvider/Clerk/useAppearance.ts +3 -3
  267. package/src/layout/GlobalProvider/AppTheme.tsx +1 -1
  268. package/src/layout/GlobalProvider/StoreInitialization.tsx +5 -0
  269. package/src/locales/default/common.ts +1 -0
  270. package/src/locales/default/modelProvider.ts +178 -0
  271. package/src/locales/default/setting.ts +1 -0
  272. package/src/server/modules/KeyVaultsEncrypt/index.ts +1 -1
  273. package/src/server/routers/lambda/aiModel.ts +128 -0
  274. package/src/server/routers/lambda/aiProvider.ts +127 -0
  275. package/src/server/routers/lambda/index.ts +4 -0
  276. package/src/services/__tests__/_auth.test.ts +16 -49
  277. package/src/services/__tests__/chat.test.ts +2 -0
  278. package/src/services/_auth.ts +42 -25
  279. package/src/services/aiModel.ts +52 -0
  280. package/src/services/aiProvider.ts +47 -0
  281. package/src/services/chat.ts +62 -18
  282. package/src/store/aiInfra/index.ts +2 -0
  283. package/src/store/aiInfra/initialState.ts +11 -0
  284. package/src/store/aiInfra/selectors.ts +2 -0
  285. package/src/store/aiInfra/slices/aiModel/action.ts +146 -0
  286. package/src/store/aiInfra/slices/aiModel/index.ts +3 -0
  287. package/src/store/aiInfra/slices/aiModel/initialState.ts +14 -0
  288. package/src/store/aiInfra/slices/aiModel/selectors.ts +63 -0
  289. package/src/store/aiInfra/slices/aiProvider/action.ts +208 -0
  290. package/src/store/aiInfra/slices/aiProvider/index.ts +3 -0
  291. package/src/store/aiInfra/slices/aiProvider/initialState.ts +32 -0
  292. package/src/store/aiInfra/slices/aiProvider/selectors.ts +99 -0
  293. package/src/store/aiInfra/store.ts +25 -0
  294. package/src/store/global/initialState.ts +1 -0
  295. package/src/store/serverConfig/selectors.test.ts +1 -0
  296. package/src/styles/global.ts +1 -1
  297. package/src/types/aiModel.ts +32 -6
  298. package/src/types/aiProvider.ts +11 -4
  299. package/src/utils/fetch/fetchSSE.ts +3 -1
@@ -0,0 +1,373 @@
1
+ // @vitest-environment node
2
+ import { eq } from 'drizzle-orm/expressions';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { getTestDBInstance } from '@/database/server/core/dbForTest';
6
+ import { ModelProvider } from '@/libs/agent-runtime';
7
+
8
+ import { aiProviders, users } from '../../../schemas';
9
+ import { AiProviderModel } from '../aiProvider';
10
+
11
+ let serverDB = await getTestDBInstance();
12
+
13
+ const userId = 'session-group-model-test-user-id';
14
+ const aiProviderModel = new AiProviderModel(serverDB, userId);
15
+
16
+ beforeEach(async () => {
17
+ await serverDB.delete(users);
18
+ await serverDB.insert(users).values([{ id: userId }, { id: 'user2' }]);
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await serverDB.delete(users).where(eq(users.id, userId));
23
+ await serverDB.delete(aiProviders).where(eq(aiProviders.userId, userId));
24
+ });
25
+
26
+ describe('AiProviderModel', () => {
27
+ describe('create', () => {
28
+ it('should create a new ai provider', async () => {
29
+ const params = {
30
+ name: 'AiHubMix',
31
+ id: 'aihubmix',
32
+ source: 'custom',
33
+ } as const;
34
+
35
+ const result = await aiProviderModel.create(params);
36
+ expect(result.id).toBeDefined();
37
+ expect(result).toMatchObject({ ...params, userId });
38
+
39
+ const group = await serverDB.query.aiProviders.findFirst({
40
+ where: eq(aiProviders.id, result.id),
41
+ });
42
+ expect(group).toMatchObject({ ...params, userId });
43
+ });
44
+ });
45
+ describe('delete', () => {
46
+ it('should delete a ai provider by id', async () => {
47
+ const { id } = await aiProviderModel.create({
48
+ name: 'AiHubMix',
49
+ id: 'aihubmix',
50
+ source: 'custom',
51
+ });
52
+
53
+ await aiProviderModel.delete(id);
54
+
55
+ const group = await serverDB.query.aiProviders.findFirst({
56
+ where: eq(aiProviders.id, id),
57
+ });
58
+ expect(group).toBeUndefined();
59
+ });
60
+ });
61
+ describe('deleteAll', () => {
62
+ it('should delete all ai providers for the user', async () => {
63
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix' });
64
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix-2' });
65
+
66
+ await aiProviderModel.deleteAll();
67
+
68
+ const userGroups = await serverDB.query.aiProviders.findMany({
69
+ where: eq(aiProviders.userId, userId),
70
+ });
71
+ expect(userGroups).toHaveLength(0);
72
+ });
73
+ it('should only delete ai providers for the user, not others', async () => {
74
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix' });
75
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix-2' });
76
+
77
+ const anotherAiProviderModel = new AiProviderModel(serverDB, 'user2');
78
+ await anotherAiProviderModel.create({
79
+ id: 'aihubmix',
80
+ source: 'custom',
81
+ name: 'Another provider',
82
+ });
83
+
84
+ await aiProviderModel.deleteAll();
85
+
86
+ const userGroups = await serverDB.query.aiProviders.findMany({
87
+ where: eq(aiProviders.userId, userId),
88
+ });
89
+ const total = await serverDB.query.aiProviders.findMany();
90
+ expect(userGroups).toHaveLength(0);
91
+ expect(total).toHaveLength(1);
92
+ });
93
+ });
94
+
95
+ describe('query', () => {
96
+ it('should query ai providers for the user', async () => {
97
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix' });
98
+ await aiProviderModel.create({ name: 'AiHubMix', source: 'custom', id: 'aihubmix-2' });
99
+
100
+ const userGroups = await aiProviderModel.query();
101
+ expect(userGroups).toHaveLength(2);
102
+ expect(userGroups[0].id).toBe('aihubmix-2');
103
+ expect(userGroups[1].id).toBe('aihubmix');
104
+ });
105
+ });
106
+
107
+ describe('findById', () => {
108
+ it('should find a ai provider by id', async () => {
109
+ const { id } = await aiProviderModel.create({
110
+ name: 'AiHubMix',
111
+ source: 'custom',
112
+ id: 'aihubmix',
113
+ });
114
+
115
+ const group = await aiProviderModel.findById(id);
116
+ expect(group).toMatchObject({
117
+ id,
118
+ name: 'AiHubMix',
119
+ userId,
120
+ });
121
+ });
122
+ });
123
+
124
+ describe('update', () => {
125
+ it('should update a ai provider', async () => {
126
+ const { id } = await aiProviderModel.create({
127
+ name: 'AiHubMix',
128
+ source: 'custom',
129
+ id: 'aihubmix',
130
+ });
131
+
132
+ await aiProviderModel.update(id, { name: 'Updated Test Group', sort: 3 });
133
+
134
+ const updatedGroup = await serverDB.query.aiProviders.findFirst({
135
+ where: eq(aiProviders.id, id),
136
+ });
137
+ expect(updatedGroup).toMatchObject({
138
+ id,
139
+ name: 'Updated Test Group',
140
+ sort: 3,
141
+ userId,
142
+ });
143
+ });
144
+ });
145
+
146
+ describe('updateOrder', () => {
147
+ it('should update order of ai providers', async () => {
148
+ const group1 = await aiProviderModel.create({
149
+ name: 'AiHubMix',
150
+ source: 'custom',
151
+ id: 'aihubmix',
152
+ });
153
+ const group2 = await aiProviderModel.create({
154
+ name: 'AiHubMix',
155
+ source: 'custom',
156
+ id: 'aihubmix-2',
157
+ });
158
+
159
+ await aiProviderModel.updateOrder([
160
+ { id: group1.id, sort: 3 },
161
+ { id: group2.id, sort: 4 },
162
+ ]);
163
+
164
+ const updatedGroup1 = await serverDB.query.aiProviders.findFirst({
165
+ where: eq(aiProviders.id, group1.id),
166
+ });
167
+ const updatedGroup2 = await serverDB.query.aiProviders.findFirst({
168
+ where: eq(aiProviders.id, group2.id),
169
+ });
170
+
171
+ expect(updatedGroup1?.sort).toBe(3);
172
+ expect(updatedGroup2?.sort).toBe(4);
173
+ });
174
+ });
175
+
176
+ describe('getAiProviderList', () => {
177
+ it('should return a list of ai providers with selected fields', async () => {
178
+ await serverDB.insert(aiProviders).values({
179
+ description: 'Test description',
180
+ enabled: true,
181
+ id: 'aihubmix',
182
+ logo: 'test-logo',
183
+ name: 'AiHubMix',
184
+ sort: 1,
185
+ source: 'custom',
186
+ userId,
187
+ });
188
+
189
+ const list = await aiProviderModel.getAiProviderList();
190
+ expect(list).toHaveLength(1);
191
+ expect(list[0]).toMatchObject({
192
+ description: 'Test description',
193
+ enabled: true,
194
+ id: 'aihubmix',
195
+ logo: 'test-logo',
196
+ name: 'AiHubMix',
197
+ sort: 1,
198
+ source: 'custom',
199
+ });
200
+ });
201
+ });
202
+
203
+ describe('updateConfig', () => {
204
+ it('should update provider config with encryption', async () => {
205
+ const providerId = 'aihubmix';
206
+ await serverDB.insert(aiProviders).values({
207
+ id: providerId,
208
+ keyVaults: JSON.stringify({ key: 'value' }),
209
+ name: 'AiHubMix',
210
+ source: 'custom',
211
+ userId,
212
+ });
213
+
214
+ const mockEncryptor = vi.fn().mockResolvedValue('encrypted-data');
215
+ await aiProviderModel.updateConfig(
216
+ providerId,
217
+ {
218
+ keyVaults: { newKey: 'newValue' },
219
+ fetchOnClient: true,
220
+ },
221
+ mockEncryptor,
222
+ );
223
+
224
+ const updated = await serverDB.query.aiProviders.findFirst({
225
+ where: eq(aiProviders.id, providerId),
226
+ });
227
+
228
+ expect(mockEncryptor).toHaveBeenCalledWith(JSON.stringify({ newKey: 'newValue' }));
229
+ expect(updated?.keyVaults).toBe('encrypted-data');
230
+ expect(updated?.fetchOnClient).toBeTruthy();
231
+ });
232
+
233
+ it('should update provider config without encryption', async () => {
234
+ const providerId = 'aihubmix';
235
+ await serverDB.insert(aiProviders).values({
236
+ id: providerId,
237
+ keyVaults: JSON.stringify({ key: 'value' }),
238
+ name: 'AiHubMix',
239
+ source: 'custom',
240
+ userId,
241
+ });
242
+
243
+ await aiProviderModel.updateConfig(providerId, {
244
+ keyVaults: { newKey: 'newValue' },
245
+ });
246
+
247
+ const updated = await serverDB.query.aiProviders.findFirst({
248
+ where: eq(aiProviders.id, providerId),
249
+ });
250
+
251
+ expect(updated?.keyVaults).toBe(JSON.stringify({ newKey: 'newValue' }));
252
+ });
253
+ });
254
+
255
+ describe('toggleProviderEnabled', () => {
256
+ it('should toggle builtin provider enabled status', async () => {
257
+ const builtinId = ModelProvider.OpenAI;
258
+ await aiProviderModel.toggleProviderEnabled(builtinId, true);
259
+
260
+ const provider = await serverDB.query.aiProviders.findFirst({
261
+ where: eq(aiProviders.id, builtinId),
262
+ });
263
+
264
+ expect(provider?.enabled).toBe(true);
265
+ expect(provider?.source).toBe('builtin');
266
+ });
267
+
268
+ it('should toggle custom provider enabled status', async () => {
269
+ const customId = 'custom-provider';
270
+ await aiProviderModel.toggleProviderEnabled(customId, false);
271
+
272
+ const provider = await serverDB.query.aiProviders.findFirst({
273
+ where: eq(aiProviders.id, customId),
274
+ });
275
+
276
+ expect(provider?.enabled).toBe(false);
277
+ expect(provider?.source).toBe('custom');
278
+ });
279
+ });
280
+
281
+ describe('getAiProviderById', () => {
282
+ it('should get provider details with decryption', async () => {
283
+ const providerId = 'aihubmix';
284
+ const mockDecryptor = vi.fn().mockResolvedValue({ decryptedKey: 'value' });
285
+
286
+ await serverDB.insert(aiProviders).values({
287
+ id: providerId,
288
+ keyVaults: JSON.stringify({ key: 'value' }),
289
+ name: 'AiHubMix',
290
+ settings: { setting1: true } as any,
291
+ source: 'custom',
292
+ userId,
293
+ });
294
+
295
+ const provider = await aiProviderModel.getAiProviderById(providerId, mockDecryptor);
296
+
297
+ expect(provider).toBeDefined();
298
+ expect(provider?.keyVaults).toEqual({ decryptedKey: 'value' });
299
+ });
300
+
301
+ it('should handle non-existent provider for builtin provider', async () => {
302
+ const builtinId = ModelProvider.OpenAI;
303
+ const provider = await aiProviderModel.getAiProviderById(builtinId, (text) =>
304
+ JSON.parse(text as string),
305
+ );
306
+
307
+ expect(provider).toBeDefined();
308
+ expect(provider?.source).toBe('builtin');
309
+ });
310
+
311
+ it('should return undefined for non-existent custom provider', async () => {
312
+ const provider = await aiProviderModel.getAiProviderById('non-existent', (text) =>
313
+ JSON.parse(text as string),
314
+ );
315
+
316
+ expect(provider).toBeUndefined();
317
+ });
318
+
319
+ it('should handle null keyVaults', async () => {
320
+ const providerId = 'aihubmix';
321
+ await serverDB.insert(aiProviders).values({
322
+ id: providerId,
323
+ name: 'AiHubMix',
324
+ source: 'custom',
325
+ userId,
326
+ });
327
+
328
+ const provider = await aiProviderModel.getAiProviderById(providerId, (text) =>
329
+ JSON.parse(text as string),
330
+ );
331
+
332
+ expect(provider?.keyVaults).toEqual({});
333
+ });
334
+ });
335
+
336
+ describe('getAiProviderRuntimeConfig', () => {
337
+ it('should get runtime config for all providers', async () => {
338
+ const mockDecryptor = vi.fn().mockResolvedValue({ decryptedKey: 'value' });
339
+
340
+ await serverDB.insert(aiProviders).values([
341
+ {
342
+ fetchOnClient: true,
343
+ id: 'provider1',
344
+ keyVaults: JSON.stringify({ key: 'value' }),
345
+ name: 'Provider 1',
346
+ settings: { setting1: true } as any,
347
+ source: 'custom',
348
+ userId,
349
+ },
350
+ {
351
+ id: 'provider2',
352
+ name: 'Provider 2',
353
+ source: 'custom',
354
+ userId,
355
+ },
356
+ ]);
357
+
358
+ const config = await aiProviderModel.getAiProviderRuntimeConfig(mockDecryptor);
359
+
360
+ expect(config.provider1).toEqual({
361
+ fetchOnClient: true,
362
+ keyVaults: { decryptedKey: 'value' },
363
+ settings: { setting1: true },
364
+ });
365
+
366
+ expect(config.provider2).toEqual({
367
+ fetchOnClient: undefined,
368
+ keyVaults: {},
369
+ settings: {},
370
+ });
371
+ });
372
+ });
373
+ });
@@ -0,0 +1,250 @@
1
+ import { and, asc, desc, eq, inArray } from 'drizzle-orm/expressions';
2
+ import pMap from 'p-map';
3
+
4
+ import { LobeChatDatabase } from '@/database/type';
5
+ import {
6
+ AiModelSortMap,
7
+ AiModelSourceEnum,
8
+ AiProviderModelListItem,
9
+ ToggleAiModelEnableParams,
10
+ } from '@/types/aiModel';
11
+
12
+ import { AiModelSelectItem, NewAiModelItem, aiModels } from '../../schemas';
13
+
14
+ export class AiModelModel {
15
+ private userId: string;
16
+ private db: LobeChatDatabase;
17
+
18
+ constructor(db: LobeChatDatabase, userId: string) {
19
+ this.userId = userId;
20
+ this.db = db;
21
+ }
22
+
23
+ create = async (params: NewAiModelItem) => {
24
+ const [result] = await this.db
25
+ .insert(aiModels)
26
+ .values({
27
+ ...params,
28
+ enabled: true, // enabled by default
29
+ source: AiModelSourceEnum.Custom,
30
+ userId: this.userId,
31
+ })
32
+ .returning();
33
+
34
+ return result;
35
+ };
36
+
37
+ delete = async (id: string, providerId: string) => {
38
+ return this.db
39
+ .delete(aiModels)
40
+ .where(
41
+ and(
42
+ eq(aiModels.id, id),
43
+ eq(aiModels.providerId, providerId),
44
+ eq(aiModels.userId, this.userId),
45
+ ),
46
+ );
47
+ };
48
+
49
+ deleteAll = async () => {
50
+ return this.db.delete(aiModels).where(eq(aiModels.userId, this.userId));
51
+ };
52
+
53
+ query = async () => {
54
+ return this.db.query.aiModels.findMany({
55
+ orderBy: [desc(aiModels.updatedAt)],
56
+ where: eq(aiModels.userId, this.userId),
57
+ });
58
+ };
59
+
60
+ getModelListByProviderId = async (providerId: string) => {
61
+ const result = await this.db
62
+ .select({
63
+ abilities: aiModels.abilities,
64
+ config: aiModels.config,
65
+ contextWindowTokens: aiModels.contextWindowTokens,
66
+ description: aiModels.description,
67
+ displayName: aiModels.displayName,
68
+ enabled: aiModels.enabled,
69
+ id: aiModels.id,
70
+ pricing: aiModels.pricing,
71
+ source: aiModels.source,
72
+ type: aiModels.type,
73
+ })
74
+ .from(aiModels)
75
+ .where(and(eq(aiModels.providerId, providerId), eq(aiModels.userId, this.userId)))
76
+ .orderBy(
77
+ asc(aiModels.sort),
78
+ desc(aiModels.enabled),
79
+ desc(aiModels.releasedAt),
80
+ desc(aiModels.updatedAt),
81
+ );
82
+
83
+ return result as AiProviderModelListItem[];
84
+ };
85
+
86
+ getEnabledModels = async () => {
87
+ return this.db
88
+ .select({
89
+ abilities: aiModels.abilities,
90
+ config: aiModels.config,
91
+ contextWindowTokens: aiModels.contextWindowTokens,
92
+ displayName: aiModels.displayName,
93
+ enabled: aiModels.enabled,
94
+ id: aiModels.id,
95
+ providerId: aiModels.providerId,
96
+ source: aiModels.source,
97
+ type: aiModels.type,
98
+ })
99
+ .from(aiModels)
100
+ .where(and(eq(aiModels.userId, this.userId), eq(aiModels.enabled, true)));
101
+ };
102
+
103
+ findById = async (id: string) => {
104
+ return this.db.query.aiModels.findFirst({
105
+ where: and(eq(aiModels.id, id), eq(aiModels.userId, this.userId)),
106
+ });
107
+ };
108
+
109
+ update = async (id: string, providerId: string, value: Partial<AiModelSelectItem>) => {
110
+ return this.db
111
+ .insert(aiModels)
112
+ .values({ ...value, id, providerId, updatedAt: new Date(), userId: this.userId })
113
+ .onConflictDoUpdate({
114
+ set: value,
115
+ target: [aiModels.id, aiModels.providerId, aiModels.userId],
116
+ });
117
+ };
118
+
119
+ toggleModelEnabled = async (value: ToggleAiModelEnableParams) => {
120
+ return this.db
121
+ .insert(aiModels)
122
+ .values({ ...value, updatedAt: new Date(), userId: this.userId })
123
+ .onConflictDoUpdate({
124
+ set: { enabled: value.enabled, updatedAt: new Date() },
125
+ target: [aiModels.id, aiModels.providerId, aiModels.userId],
126
+ });
127
+ };
128
+
129
+ batchUpdateAiModels = async (providerId: string, models: AiProviderModelListItem[]) => {
130
+ return this.db.transaction(async (trx) => {
131
+ const records = models.map(({ id, ...model }) => ({
132
+ ...model,
133
+ id,
134
+ providerId,
135
+ updatedAt: new Date(),
136
+ userId: this.userId,
137
+ }));
138
+
139
+ // 第一步:尝试插入所有记录,忽略冲突
140
+ const insertedRecords = await trx
141
+ .insert(aiModels)
142
+ .values(records)
143
+ .onConflictDoNothing({
144
+ target: [aiModels.id, aiModels.userId, aiModels.providerId],
145
+ })
146
+ .returning();
147
+ // 第二步:找出需要更新的记录(即插入时发生冲突的记录)
148
+ // 找出未能插入的记录(需要更新的记录)
149
+ const insertedIds = new Set(insertedRecords.map((r) => r.id));
150
+ const recordsToUpdate = records.filter((r) => !insertedIds.has(r.id));
151
+
152
+ // 第三步:更新已存在的记录
153
+ if (recordsToUpdate.length > 0) {
154
+ await pMap(
155
+ recordsToUpdate,
156
+ async (record) => {
157
+ await trx
158
+ .update(aiModels)
159
+ .set({
160
+ ...record,
161
+ updatedAt: new Date(),
162
+ })
163
+ .where(
164
+ and(
165
+ eq(aiModels.id, record.id),
166
+ eq(aiModels.userId, this.userId),
167
+ eq(aiModels.providerId, providerId),
168
+ ),
169
+ );
170
+ },
171
+ { concurrency: 10 }, // 限制并发数为 10
172
+ );
173
+ }
174
+ });
175
+ };
176
+
177
+ batchToggleAiModels = async (providerId: string, models: string[], enabled: boolean) => {
178
+ return this.db.transaction(async (trx) => {
179
+ // 1. insert models that are not in the db
180
+ const insertedRecords = await trx
181
+ .insert(aiModels)
182
+ .values(
183
+ models.map((i) => ({
184
+ enabled,
185
+ id: i,
186
+ providerId,
187
+ // if the model is not in the db, it's a builtin model
188
+ source: AiModelSourceEnum.Builtin,
189
+ updatedAt: new Date(),
190
+ userId: this.userId,
191
+ })),
192
+ )
193
+ .onConflictDoNothing({
194
+ target: [aiModels.id, aiModels.userId, aiModels.providerId],
195
+ })
196
+ .returning();
197
+
198
+ // 2. update models that are in the db
199
+ const insertedIds = new Set(insertedRecords.map((r) => r.id));
200
+ const recordsToUpdate = models.filter((r) => !insertedIds.has(r));
201
+
202
+ await trx
203
+ .update(aiModels)
204
+ .set({ enabled })
205
+ .where(
206
+ and(
207
+ eq(aiModels.providerId, providerId),
208
+ inArray(aiModels.id, recordsToUpdate),
209
+ eq(aiModels.userId, this.userId),
210
+ ),
211
+ );
212
+ });
213
+ };
214
+
215
+ clearRemoteModels(providerId: string) {
216
+ return this.db
217
+ .delete(aiModels)
218
+ .where(
219
+ and(
220
+ eq(aiModels.providerId, providerId),
221
+ eq(aiModels.source, AiModelSourceEnum.Remote),
222
+ eq(aiModels.userId, this.userId),
223
+ ),
224
+ );
225
+ }
226
+
227
+ updateModelsOrder = async (providerId: string, sortMap: AiModelSortMap[]) => {
228
+ await this.db.transaction(async (tx) => {
229
+ const updates = sortMap.map(({ id, sort }) => {
230
+ return tx
231
+ .insert(aiModels)
232
+ .values({
233
+ enabled: true,
234
+ id,
235
+ providerId,
236
+ sort,
237
+ // source: isBuiltin ? 'builtin' : 'custom',
238
+ updatedAt: new Date(),
239
+ userId: this.userId,
240
+ })
241
+ .onConflictDoUpdate({
242
+ set: { sort, updatedAt: new Date() },
243
+ target: [aiModels.id, aiModels.userId, aiModels.providerId],
244
+ });
245
+ });
246
+
247
+ await Promise.all(updates);
248
+ });
249
+ };
250
+ }