@lobehub/chat 1.98.2 → 1.99.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 (456) hide show
  1. package/.cursor/rules/backend-architecture.mdc +93 -17
  2. package/.cursor/rules/cursor-ux.mdc +45 -35
  3. package/.cursor/rules/project-introduce.mdc +72 -6
  4. package/.cursor/rules/rules-attach.mdc +16 -7
  5. package/.eslintrc.js +10 -0
  6. package/CHANGELOG.md +27 -0
  7. package/apps/desktop/README.md +7 -0
  8. package/apps/desktop/electron-builder.js +5 -0
  9. package/apps/desktop/package.json +2 -1
  10. package/apps/desktop/src/main/const/dir.ts +3 -0
  11. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  12. package/apps/desktop/src/main/core/App.ts +8 -0
  13. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  14. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  15. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  16. package/changelog/v1.json +9 -0
  17. package/docs/development/database-schema.dbml +70 -0
  18. package/locales/ar/common.json +2 -0
  19. package/locales/ar/components.json +35 -0
  20. package/locales/ar/error.json +2 -0
  21. package/locales/ar/image.json +100 -0
  22. package/locales/ar/metadata.json +4 -0
  23. package/locales/ar/modelProvider.json +1 -0
  24. package/locales/ar/models.json +15 -0
  25. package/locales/ar/plugin.json +22 -0
  26. package/locales/ar/providers.json +3 -0
  27. package/locales/ar/setting.json +5 -0
  28. package/locales/bg-BG/common.json +2 -0
  29. package/locales/bg-BG/components.json +35 -0
  30. package/locales/bg-BG/error.json +2 -0
  31. package/locales/bg-BG/image.json +100 -0
  32. package/locales/bg-BG/metadata.json +4 -0
  33. package/locales/bg-BG/modelProvider.json +1 -0
  34. package/locales/bg-BG/models.json +15 -0
  35. package/locales/bg-BG/plugin.json +22 -0
  36. package/locales/bg-BG/providers.json +3 -0
  37. package/locales/bg-BG/setting.json +5 -0
  38. package/locales/de-DE/common.json +2 -0
  39. package/locales/de-DE/components.json +35 -0
  40. package/locales/de-DE/error.json +2 -0
  41. package/locales/de-DE/image.json +100 -0
  42. package/locales/de-DE/metadata.json +4 -0
  43. package/locales/de-DE/modelProvider.json +1 -0
  44. package/locales/de-DE/models.json +15 -0
  45. package/locales/de-DE/plugin.json +22 -0
  46. package/locales/de-DE/providers.json +3 -0
  47. package/locales/de-DE/setting.json +5 -0
  48. package/locales/en-US/common.json +2 -0
  49. package/locales/en-US/components.json +35 -0
  50. package/locales/en-US/error.json +2 -0
  51. package/locales/en-US/image.json +100 -0
  52. package/locales/en-US/metadata.json +4 -0
  53. package/locales/en-US/modelProvider.json +1 -0
  54. package/locales/en-US/models.json +15 -0
  55. package/locales/en-US/plugin.json +22 -0
  56. package/locales/en-US/providers.json +3 -0
  57. package/locales/en-US/setting.json +5 -0
  58. package/locales/es-ES/common.json +2 -0
  59. package/locales/es-ES/components.json +35 -0
  60. package/locales/es-ES/error.json +2 -0
  61. package/locales/es-ES/image.json +100 -0
  62. package/locales/es-ES/metadata.json +4 -0
  63. package/locales/es-ES/modelProvider.json +1 -0
  64. package/locales/es-ES/models.json +15 -0
  65. package/locales/es-ES/plugin.json +22 -0
  66. package/locales/es-ES/providers.json +3 -0
  67. package/locales/es-ES/setting.json +5 -0
  68. package/locales/fa-IR/common.json +2 -0
  69. package/locales/fa-IR/components.json +35 -0
  70. package/locales/fa-IR/error.json +2 -0
  71. package/locales/fa-IR/image.json +100 -0
  72. package/locales/fa-IR/metadata.json +4 -0
  73. package/locales/fa-IR/modelProvider.json +1 -0
  74. package/locales/fa-IR/models.json +15 -0
  75. package/locales/fa-IR/plugin.json +22 -0
  76. package/locales/fa-IR/providers.json +3 -0
  77. package/locales/fa-IR/setting.json +5 -0
  78. package/locales/fr-FR/common.json +2 -0
  79. package/locales/fr-FR/components.json +35 -0
  80. package/locales/fr-FR/error.json +2 -0
  81. package/locales/fr-FR/image.json +100 -0
  82. package/locales/fr-FR/metadata.json +4 -0
  83. package/locales/fr-FR/modelProvider.json +1 -0
  84. package/locales/fr-FR/models.json +15 -0
  85. package/locales/fr-FR/plugin.json +22 -0
  86. package/locales/fr-FR/providers.json +3 -0
  87. package/locales/fr-FR/setting.json +5 -0
  88. package/locales/it-IT/common.json +2 -0
  89. package/locales/it-IT/components.json +35 -0
  90. package/locales/it-IT/error.json +2 -0
  91. package/locales/it-IT/image.json +100 -0
  92. package/locales/it-IT/metadata.json +4 -0
  93. package/locales/it-IT/modelProvider.json +1 -0
  94. package/locales/it-IT/models.json +15 -0
  95. package/locales/it-IT/plugin.json +22 -0
  96. package/locales/it-IT/providers.json +3 -0
  97. package/locales/it-IT/setting.json +5 -0
  98. package/locales/ja-JP/common.json +2 -0
  99. package/locales/ja-JP/components.json +35 -0
  100. package/locales/ja-JP/error.json +2 -0
  101. package/locales/ja-JP/image.json +100 -0
  102. package/locales/ja-JP/metadata.json +4 -0
  103. package/locales/ja-JP/modelProvider.json +1 -0
  104. package/locales/ja-JP/models.json +15 -0
  105. package/locales/ja-JP/plugin.json +22 -0
  106. package/locales/ja-JP/providers.json +3 -0
  107. package/locales/ja-JP/setting.json +5 -0
  108. package/locales/ko-KR/common.json +2 -0
  109. package/locales/ko-KR/components.json +35 -0
  110. package/locales/ko-KR/error.json +2 -0
  111. package/locales/ko-KR/image.json +100 -0
  112. package/locales/ko-KR/metadata.json +4 -0
  113. package/locales/ko-KR/modelProvider.json +1 -0
  114. package/locales/ko-KR/models.json +15 -0
  115. package/locales/ko-KR/plugin.json +22 -0
  116. package/locales/ko-KR/providers.json +3 -0
  117. package/locales/ko-KR/setting.json +5 -0
  118. package/locales/nl-NL/common.json +2 -0
  119. package/locales/nl-NL/components.json +35 -0
  120. package/locales/nl-NL/error.json +2 -0
  121. package/locales/nl-NL/image.json +100 -0
  122. package/locales/nl-NL/metadata.json +4 -0
  123. package/locales/nl-NL/modelProvider.json +1 -0
  124. package/locales/nl-NL/models.json +15 -0
  125. package/locales/nl-NL/plugin.json +22 -0
  126. package/locales/nl-NL/providers.json +3 -0
  127. package/locales/nl-NL/setting.json +5 -0
  128. package/locales/pl-PL/common.json +2 -0
  129. package/locales/pl-PL/components.json +35 -0
  130. package/locales/pl-PL/error.json +2 -0
  131. package/locales/pl-PL/image.json +100 -0
  132. package/locales/pl-PL/metadata.json +4 -0
  133. package/locales/pl-PL/modelProvider.json +1 -0
  134. package/locales/pl-PL/models.json +15 -0
  135. package/locales/pl-PL/plugin.json +22 -0
  136. package/locales/pl-PL/providers.json +3 -0
  137. package/locales/pl-PL/setting.json +5 -0
  138. package/locales/pt-BR/common.json +2 -0
  139. package/locales/pt-BR/components.json +35 -0
  140. package/locales/pt-BR/error.json +2 -0
  141. package/locales/pt-BR/image.json +100 -0
  142. package/locales/pt-BR/metadata.json +4 -0
  143. package/locales/pt-BR/modelProvider.json +1 -0
  144. package/locales/pt-BR/models.json +15 -0
  145. package/locales/pt-BR/plugin.json +22 -0
  146. package/locales/pt-BR/providers.json +3 -0
  147. package/locales/pt-BR/setting.json +5 -0
  148. package/locales/ru-RU/common.json +2 -0
  149. package/locales/ru-RU/components.json +35 -0
  150. package/locales/ru-RU/error.json +2 -0
  151. package/locales/ru-RU/image.json +100 -0
  152. package/locales/ru-RU/metadata.json +4 -0
  153. package/locales/ru-RU/modelProvider.json +1 -0
  154. package/locales/ru-RU/models.json +15 -0
  155. package/locales/ru-RU/plugin.json +22 -0
  156. package/locales/ru-RU/providers.json +3 -0
  157. package/locales/ru-RU/setting.json +5 -0
  158. package/locales/tr-TR/common.json +2 -0
  159. package/locales/tr-TR/components.json +35 -0
  160. package/locales/tr-TR/error.json +2 -0
  161. package/locales/tr-TR/image.json +100 -0
  162. package/locales/tr-TR/metadata.json +4 -0
  163. package/locales/tr-TR/modelProvider.json +1 -0
  164. package/locales/tr-TR/models.json +15 -0
  165. package/locales/tr-TR/plugin.json +22 -0
  166. package/locales/tr-TR/providers.json +3 -0
  167. package/locales/tr-TR/setting.json +5 -0
  168. package/locales/vi-VN/common.json +2 -0
  169. package/locales/vi-VN/components.json +35 -0
  170. package/locales/vi-VN/error.json +2 -0
  171. package/locales/vi-VN/image.json +100 -0
  172. package/locales/vi-VN/metadata.json +4 -0
  173. package/locales/vi-VN/modelProvider.json +1 -0
  174. package/locales/vi-VN/models.json +15 -0
  175. package/locales/vi-VN/plugin.json +22 -0
  176. package/locales/vi-VN/providers.json +3 -0
  177. package/locales/vi-VN/setting.json +5 -0
  178. package/locales/zh-CN/common.json +2 -0
  179. package/locales/zh-CN/components.json +35 -0
  180. package/locales/zh-CN/error.json +2 -0
  181. package/locales/zh-CN/image.json +100 -0
  182. package/locales/zh-CN/metadata.json +4 -0
  183. package/locales/zh-CN/modelProvider.json +1 -0
  184. package/locales/zh-CN/models.json +15 -0
  185. package/locales/zh-CN/plugin.json +22 -0
  186. package/locales/zh-CN/providers.json +3 -0
  187. package/locales/zh-CN/setting.json +5 -0
  188. package/locales/zh-TW/common.json +2 -0
  189. package/locales/zh-TW/components.json +35 -0
  190. package/locales/zh-TW/error.json +2 -0
  191. package/locales/zh-TW/image.json +100 -0
  192. package/locales/zh-TW/metadata.json +4 -0
  193. package/locales/zh-TW/modelProvider.json +1 -0
  194. package/locales/zh-TW/models.json +15 -0
  195. package/locales/zh-TW/plugin.json +22 -0
  196. package/locales/zh-TW/providers.json +3 -0
  197. package/locales/zh-TW/setting.json +5 -0
  198. package/package.json +11 -4
  199. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  200. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  201. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  202. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  203. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  204. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  205. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  206. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  207. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  221. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  222. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  223. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  229. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  230. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  231. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  233. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  234. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  235. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  236. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +43 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +156 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  246. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  247. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  250. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  251. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  252. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  253. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  254. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  255. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  256. package/src/chains/summaryGenerationTitle.ts +25 -0
  257. package/src/components/ImageItem/index.tsx +9 -6
  258. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  259. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  260. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  262. package/src/components/InvalidAPIKey/index.tsx +30 -0
  263. package/src/components/KeyValueEditor/index.tsx +203 -0
  264. package/src/components/KeyValueEditor/utils.ts +42 -0
  265. package/src/config/aiModels/fal.ts +52 -0
  266. package/src/config/aiModels/index.ts +3 -0
  267. package/src/config/aiModels/openai.ts +20 -6
  268. package/src/config/llm.ts +6 -0
  269. package/src/config/modelProviders/fal.ts +21 -0
  270. package/src/config/modelProviders/index.ts +3 -0
  271. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  272. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  273. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  274. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  275. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  276. package/src/const/hotkeys.ts +2 -2
  277. package/src/const/image.ts +6 -0
  278. package/src/const/settings/systemAgent.ts +1 -0
  279. package/src/database/client/migrations.json +27 -0
  280. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  281. package/src/database/migrations/0027_ai_image.sql +47 -0
  282. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  283. package/src/database/migrations/meta/_journal.json +7 -0
  284. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  285. package/src/database/models/__tests__/file.test.ts +287 -0
  286. package/src/database/models/__tests__/generation.test.ts +786 -0
  287. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  288. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  289. package/src/database/models/aiModel.ts +2 -0
  290. package/src/database/models/asyncTask.ts +1 -1
  291. package/src/database/models/file.ts +28 -20
  292. package/src/database/models/generation.ts +197 -0
  293. package/src/database/models/generationBatch.ts +212 -0
  294. package/src/database/models/generationTopic.ts +131 -0
  295. package/src/database/repositories/aiInfra/index.test.ts +151 -1
  296. package/src/database/repositories/aiInfra/index.ts +28 -19
  297. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  298. package/src/database/schemas/file.ts +8 -0
  299. package/src/database/schemas/generation.ts +127 -0
  300. package/src/database/schemas/index.ts +1 -0
  301. package/src/database/schemas/relations.ts +45 -1
  302. package/src/database/type.ts +2 -0
  303. package/src/database/utils/idGenerator.ts +3 -0
  304. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  305. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  306. package/src/features/Conversation/Error/index.tsx +3 -3
  307. package/src/features/ImageSidePanel/index.tsx +83 -0
  308. package/src/features/ImageTopicPanel/index.tsx +79 -0
  309. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  310. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  312. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  313. package/src/hooks/useDownloadImage.ts +31 -0
  314. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  315. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  316. package/src/libs/mcp/client.ts +55 -22
  317. package/src/libs/mcp/types.ts +42 -6
  318. package/src/libs/model-runtime/BaseAI.ts +3 -1
  319. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  320. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  321. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  322. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  323. package/src/libs/model-runtime/fal/index.ts +88 -0
  324. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  325. package/src/libs/model-runtime/openai/index.ts +129 -3
  326. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  327. package/src/libs/model-runtime/types/image.ts +25 -0
  328. package/src/libs/model-runtime/types/type.ts +1 -0
  329. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  330. package/src/libs/standard-parameters/index.ts +1 -0
  331. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  332. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  333. package/src/libs/swr/index.ts +1 -0
  334. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  335. package/src/libs/trpc/async/context.ts +42 -4
  336. package/src/libs/trpc/async/index.ts +17 -4
  337. package/src/libs/trpc/async/init.ts +8 -0
  338. package/src/libs/trpc/client/lambda.ts +19 -2
  339. package/src/locales/default/common.ts +2 -0
  340. package/src/locales/default/components.ts +35 -0
  341. package/src/locales/default/error.ts +2 -0
  342. package/src/locales/default/image.ts +100 -0
  343. package/src/locales/default/index.ts +2 -0
  344. package/src/locales/default/metadata.ts +4 -0
  345. package/src/locales/default/modelProvider.ts +2 -0
  346. package/src/locales/default/plugin.ts +22 -0
  347. package/src/locales/default/setting.ts +5 -0
  348. package/src/middleware.ts +1 -0
  349. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  350. package/src/server/modules/S3/index.ts +15 -0
  351. package/src/server/routers/async/caller.ts +9 -1
  352. package/src/server/routers/async/image.ts +253 -0
  353. package/src/server/routers/async/index.ts +2 -0
  354. package/src/server/routers/lambda/aiProvider.test.ts +1 -0
  355. package/src/server/routers/lambda/generation.test.ts +267 -0
  356. package/src/server/routers/lambda/generation.ts +86 -0
  357. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  358. package/src/server/routers/lambda/generationBatch.ts +56 -0
  359. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  360. package/src/server/routers/lambda/generationTopic.ts +93 -0
  361. package/src/server/routers/lambda/image.ts +248 -0
  362. package/src/server/routers/lambda/index.ts +8 -0
  363. package/src/server/routers/tools/mcp.ts +15 -0
  364. package/src/server/services/file/__tests__/index.test.ts +135 -0
  365. package/src/server/services/file/impls/local.test.ts +153 -52
  366. package/src/server/services/file/impls/local.ts +70 -46
  367. package/src/server/services/file/impls/s3.test.ts +114 -0
  368. package/src/server/services/file/impls/s3.ts +40 -0
  369. package/src/server/services/file/impls/type.ts +10 -0
  370. package/src/server/services/file/index.ts +14 -0
  371. package/src/server/services/generation/index.ts +239 -0
  372. package/src/server/services/mcp/index.ts +20 -2
  373. package/src/services/__tests__/generation.test.ts +40 -0
  374. package/src/services/__tests__/generationBatch.test.ts +36 -0
  375. package/src/services/__tests__/generationTopic.test.ts +72 -0
  376. package/src/services/electron/file.ts +3 -1
  377. package/src/services/generation.ts +16 -0
  378. package/src/services/generationBatch.ts +25 -0
  379. package/src/services/generationTopic.ts +28 -0
  380. package/src/services/image.ts +33 -0
  381. package/src/services/mcp.ts +12 -7
  382. package/src/services/upload.ts +43 -9
  383. package/src/store/aiInfra/slices/aiProvider/action.ts +25 -5
  384. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  385. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  386. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  387. package/src/store/chat/slices/message/action.ts +2 -2
  388. package/src/store/chat/slices/translate/action.ts +1 -1
  389. package/src/store/global/initialState.ts +9 -0
  390. package/src/store/global/selectors/systemStatus.ts +8 -0
  391. package/src/store/image/index.ts +2 -0
  392. package/src/store/image/initialState.ts +25 -0
  393. package/src/store/image/selectors.ts +4 -0
  394. package/src/store/image/slices/createImage/action.test.ts +330 -0
  395. package/src/store/image/slices/createImage/action.ts +134 -0
  396. package/src/store/image/slices/createImage/initialState.ts +9 -0
  397. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  398. package/src/store/image/slices/createImage/selectors.ts +9 -0
  399. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  400. package/src/store/image/slices/generationBatch/action.ts +303 -0
  401. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  402. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  403. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  404. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  405. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  406. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  407. package/src/store/image/slices/generationConfig/action.ts +295 -0
  408. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  409. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  410. package/src/store/image/slices/generationConfig/index.ts +1 -0
  411. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  412. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  413. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  414. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  415. package/src/store/image/slices/generationTopic/action.ts +319 -0
  416. package/src/store/image/slices/generationTopic/index.ts +2 -0
  417. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  418. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  419. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  420. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  421. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  422. package/src/store/image/store.ts +42 -0
  423. package/src/store/image/utils/size.ts +51 -0
  424. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  425. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  426. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  427. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  428. package/src/types/aiModel.ts +8 -3
  429. package/src/types/aiProvider.ts +1 -0
  430. package/src/types/asyncTask.ts +2 -0
  431. package/src/types/files/index.ts +5 -0
  432. package/src/types/generation/index.ts +80 -0
  433. package/src/types/hotkey.ts +2 -0
  434. package/src/types/plugins/mcp.ts +2 -6
  435. package/src/types/tool/plugin.ts +8 -0
  436. package/src/types/user/settings/keyVaults.ts +5 -0
  437. package/src/types/user/settings/systemAgent.ts +1 -0
  438. package/src/utils/client/downloadFile.ts +33 -4
  439. package/src/utils/number.test.ts +105 -0
  440. package/src/utils/number.ts +25 -0
  441. package/src/utils/server/__tests__/geo.test.ts +6 -3
  442. package/src/utils/storeDebug.test.ts +152 -0
  443. package/src/utils/storeDebug.ts +16 -7
  444. package/src/utils/time.test.ts +259 -0
  445. package/src/utils/time.ts +18 -0
  446. package/src/utils/units.ts +61 -0
  447. package/src/utils/url.test.ts +358 -9
  448. package/src/utils/url.ts +105 -3
  449. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  450. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  451. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  452. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  453. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  454. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  455. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  456. /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -8,6 +8,7 @@ import { JWTPayload } from '@/const/auth';
8
8
  import { TraceNameMap } from '@/const/trace';
9
9
  import { AgentRuntime, ChatStreamPayload, LobeOpenAI, ModelProvider } from '@/libs/model-runtime';
10
10
  import { providerRuntimeMap } from '@/libs/model-runtime/runtimeMap';
11
+ import { CreateImagePayload } from '@/libs/model-runtime/types/image';
11
12
  import { createTraceOptions } from '@/server/modules/AgentRuntime';
12
13
 
13
14
  import { AgentChatOptions } from './ModelRuntime';
@@ -241,4 +242,83 @@ describe('AgentRuntime', () => {
241
242
  });
242
243
  });
243
244
  });
245
+
246
+ describe('AgentRuntime createImage method', () => {
247
+ it('should run correctly', async () => {
248
+ const payload: CreateImagePayload = {
249
+ model: 'dall-e-3',
250
+ params: {
251
+ prompt: 'A beautiful sunset over mountains',
252
+ width: 1024,
253
+ height: 1024,
254
+ },
255
+ };
256
+
257
+ const mockResponse = {
258
+ imageUrl: 'https://example.com/image.jpg',
259
+ width: 1024,
260
+ height: 1024,
261
+ };
262
+
263
+ vi.spyOn(LobeOpenAI.prototype, 'createImage').mockResolvedValue(mockResponse);
264
+
265
+ const result = await mockModelRuntime.createImage(payload);
266
+
267
+ expect(LobeOpenAI.prototype.createImage).toHaveBeenCalledWith(payload);
268
+ expect(result).toBe(mockResponse);
269
+ });
270
+
271
+ it('should handle undefined createImage method gracefully', async () => {
272
+ const payload: CreateImagePayload = {
273
+ model: 'dall-e-3',
274
+ params: {
275
+ prompt: 'A beautiful sunset over mountains',
276
+ width: 1024,
277
+ height: 1024,
278
+ },
279
+ };
280
+
281
+ // Mock runtime without createImage method
282
+ const runtimeWithoutCreateImage = {
283
+ createImage: undefined,
284
+ };
285
+
286
+ // @ts-ignore - testing edge case
287
+ mockModelRuntime['_runtime'] = runtimeWithoutCreateImage;
288
+
289
+ const result = await mockModelRuntime.createImage(payload);
290
+
291
+ expect(result).toBeUndefined();
292
+ });
293
+ });
294
+
295
+ describe('AgentRuntime models method', () => {
296
+ it('should run correctly', async () => {
297
+ const mockModels = [
298
+ { id: 'gpt-4', name: 'GPT-4' },
299
+ { id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo' },
300
+ ];
301
+
302
+ vi.spyOn(LobeOpenAI.prototype, 'models').mockResolvedValue(mockModels);
303
+
304
+ const result = await mockModelRuntime.models();
305
+
306
+ expect(LobeOpenAI.prototype.models).toHaveBeenCalled();
307
+ expect(result).toBe(mockModels);
308
+ });
309
+
310
+ it('should handle undefined models method gracefully', async () => {
311
+ // Mock runtime without models method
312
+ const runtimeWithoutModels = {
313
+ models: undefined,
314
+ };
315
+
316
+ // @ts-ignore - testing edge case
317
+ mockModelRuntime['_runtime'] = runtimeWithoutModels;
318
+
319
+ const result = await mockModelRuntime.models();
320
+
321
+ expect(result).toBeUndefined();
322
+ });
323
+ });
244
324
  });
@@ -1,3 +1,4 @@
1
+ import { log } from 'debug';
1
2
  import { ClientOptions } from 'openai';
2
3
 
3
4
  import type { TracePayload } from '@/const/trace';
@@ -17,6 +18,7 @@ import {
17
18
  TextToImagePayload,
18
19
  TextToSpeechPayload,
19
20
  } from './types';
21
+ import { CreateImagePayload } from './types/image';
20
22
 
21
23
  export interface AgentChatOptions {
22
24
  enableTrace?: boolean;
@@ -61,13 +63,17 @@ class ModelRuntime {
61
63
  * ```
62
64
  */
63
65
  async chat(payload: ChatStreamPayload, options?: ChatMethodOptions) {
64
- return this._runtime.chat(payload, options);
66
+ return this._runtime.chat!(payload, options);
65
67
  }
66
68
 
67
69
  async textToImage(payload: TextToImagePayload) {
68
70
  return this._runtime.textToImage?.(payload);
69
71
  }
70
72
 
73
+ async createImage(payload: CreateImagePayload) {
74
+ return this._runtime.createImage?.(payload);
75
+ }
76
+
71
77
  async models() {
72
78
  return this._runtime.models?.();
73
79
  }
@@ -106,6 +112,14 @@ class ModelRuntime {
106
112
  LobeCloudflareParams & { apiKey?: string; apiVersion?: string; baseURL?: string }
107
113
  >,
108
114
  ) {
115
+ log('Initializing runtime with provider: %s and params: %O', provider, params);
116
+ // @ts-expect-error ignore
117
+ if (providerRuntimeMap[provider]) {
118
+ log('Provider runtime map found for provider: %s', provider);
119
+ } else {
120
+ log('Provider runtime map not found for provider: %s', provider);
121
+ }
122
+
109
123
  // @ts-expect-error runtime map not include vertex so it will be undefined
110
124
  const providerAI = providerRuntimeMap[provider] ?? LobeOpenAI;
111
125
  const runtimeModel: LobeRuntimeAI = new providerAI(params);
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @see https://github.com/lobehub/lobe-chat/discussions/6563
3
+ */
1
4
  import { LobeRuntimeAI } from '../BaseAI';
2
5
  import { LobeOpenAI } from '../openai';
3
6
  import { providerRuntimeMap } from '../runtimeMap';
@@ -74,7 +77,7 @@ class UniformRuntime {
74
77
  try {
75
78
  const runtime = this.getRuntimeByModel(payload.model);
76
79
 
77
- return await runtime.chat(payload, options);
80
+ return await runtime.chat!(payload, options);
78
81
  } catch (e) {
79
82
  if (this._options.chat?.handleError) {
80
83
  const error = this._options.chat.handleError(e);
@@ -0,0 +1,442 @@
1
+ // @vitest-environment node
2
+ import { fal } from '@fal-ai/client';
3
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { CreateImagePayload } from '@/libs/model-runtime/types/image';
6
+
7
+ import { LobeFalAI } from './index';
8
+
9
+ // Mock the fal client
10
+ vi.mock('@fal-ai/client', () => ({
11
+ fal: {
12
+ config: vi.fn(),
13
+ subscribe: vi.fn(),
14
+ },
15
+ }));
16
+
17
+ // Get the mocked fal instance
18
+ const mockFal = vi.mocked(fal);
19
+
20
+ // Mock the console.error to avoid polluting test output
21
+ vi.spyOn(console, 'error').mockImplementation(() => {});
22
+
23
+ const provider = 'fal';
24
+ const bizErrorType = 'ProviderBizError';
25
+ const invalidErrorType = 'InvalidProviderAPIKey';
26
+
27
+ let instance: LobeFalAI;
28
+
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ instance = new LobeFalAI({ apiKey: 'test-api-key' });
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ describe('LobeFalAI', () => {
39
+ describe('init', () => {
40
+ it('should correctly initialize with an API key', () => {
41
+ const instance = new LobeFalAI({ apiKey: 'test_api_key' });
42
+ expect(instance).toBeInstanceOf(LobeFalAI);
43
+ expect(mockFal.config).toHaveBeenCalledWith({
44
+ credentials: 'test_api_key',
45
+ });
46
+ });
47
+
48
+ it('should throw InvalidProviderAPIKey if no apiKey is provided', () => {
49
+ expect(() => {
50
+ new LobeFalAI({});
51
+ }).toThrow();
52
+ });
53
+
54
+ it('should throw InvalidProviderAPIKey if apiKey is undefined', () => {
55
+ expect(() => {
56
+ new LobeFalAI({ apiKey: undefined });
57
+ }).toThrow();
58
+ });
59
+ });
60
+
61
+ describe('createImage', () => {
62
+ it('should create image successfully with basic parameters', async () => {
63
+ // Arrange
64
+ const mockImageResponse = {
65
+ requestId: 'test-request-id',
66
+ data: {
67
+ images: [
68
+ {
69
+ url: 'https://example.com/image.jpg',
70
+ width: 1024,
71
+ height: 1024,
72
+ },
73
+ ],
74
+ },
75
+ };
76
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
77
+
78
+ const payload: CreateImagePayload = {
79
+ model: 'flux/dev',
80
+ params: {
81
+ prompt: 'A beautiful landscape',
82
+ width: 1024,
83
+ height: 1024,
84
+ },
85
+ };
86
+
87
+ // Act
88
+ const result = await instance.createImage(payload);
89
+
90
+ // Assert
91
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/flux/dev', {
92
+ input: {
93
+ enable_safety_checker: false,
94
+ num_images: 1,
95
+ prompt: 'A beautiful landscape',
96
+ image_size: {
97
+ width: 1024,
98
+ height: 1024,
99
+ },
100
+ },
101
+ });
102
+ expect(result).toEqual({
103
+ imageUrl: 'https://example.com/image.jpg',
104
+ width: 1024,
105
+ height: 1024,
106
+ });
107
+ });
108
+
109
+ it('should map standard parameters to fal-specific parameters', async () => {
110
+ // Arrange
111
+ const mockImageResponse = {
112
+ requestId: 'test-request-id',
113
+ data: {
114
+ images: [
115
+ {
116
+ url: 'https://example.com/image.jpg',
117
+ width: 512,
118
+ height: 512,
119
+ },
120
+ ],
121
+ },
122
+ };
123
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
124
+
125
+ const payload: CreateImagePayload = {
126
+ model: 'flux/dev',
127
+ params: {
128
+ prompt: 'Test image',
129
+ width: 512,
130
+ height: 512,
131
+ steps: 20,
132
+ cfg: 7.5,
133
+ imageUrl: 'https://example.com/input.jpg',
134
+ },
135
+ };
136
+
137
+ // Act
138
+ await instance.createImage(payload);
139
+
140
+ // Assert
141
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/flux/dev', {
142
+ input: {
143
+ enable_safety_checker: false,
144
+ num_images: 1,
145
+ prompt: 'Test image',
146
+ image_size: {
147
+ width: 512,
148
+ height: 512,
149
+ },
150
+ num_inference_steps: 20,
151
+ guidance_scale: 7.5,
152
+ image_url: 'https://example.com/input.jpg',
153
+ },
154
+ });
155
+ });
156
+
157
+ it('should handle parameters without width and height', async () => {
158
+ // Arrange
159
+ const mockImageResponse = {
160
+ requestId: 'test-request-id',
161
+ data: {
162
+ images: [
163
+ {
164
+ url: 'https://example.com/image.jpg',
165
+ width: 1024,
166
+ height: 1024,
167
+ },
168
+ ],
169
+ },
170
+ };
171
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
172
+
173
+ const payload: CreateImagePayload = {
174
+ model: 'flux/schnell',
175
+ params: {
176
+ prompt: 'Simple test',
177
+ steps: 10,
178
+ },
179
+ };
180
+
181
+ // Act
182
+ await instance.createImage(payload);
183
+
184
+ // Assert
185
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/flux/schnell', {
186
+ input: {
187
+ enable_safety_checker: false,
188
+ num_images: 1,
189
+ prompt: 'Simple test',
190
+ num_inference_steps: 10,
191
+ },
192
+ });
193
+ });
194
+
195
+ it('should handle custom parameters that are not in the mapping', async () => {
196
+ // Arrange
197
+ const mockImageResponse = {
198
+ requestId: 'test-request-id',
199
+ data: {
200
+ images: [
201
+ {
202
+ url: 'https://example.com/image.jpg',
203
+ width: 768,
204
+ height: 768,
205
+ },
206
+ ],
207
+ },
208
+ };
209
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
210
+
211
+ const payload: CreateImagePayload = {
212
+ model: 'flux/dev',
213
+ params: {
214
+ prompt: 'Custom test',
215
+ width: 768,
216
+ height: 768,
217
+ seed: 12345,
218
+ } as any, // Use any to allow custom parameters
219
+ };
220
+
221
+ // Act
222
+ await instance.createImage(payload);
223
+
224
+ // Assert
225
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/flux/dev', {
226
+ input: {
227
+ enable_safety_checker: false,
228
+ num_images: 1,
229
+ prompt: 'Custom test',
230
+ image_size: {
231
+ width: 768,
232
+ height: 768,
233
+ },
234
+ seed: 12345,
235
+ },
236
+ });
237
+ });
238
+
239
+ it('should return only imageUrl when width and height are not provided in response', async () => {
240
+ // Arrange
241
+ const mockImageResponse = {
242
+ requestId: 'test-request-id',
243
+ data: {
244
+ images: [
245
+ {
246
+ url: 'https://example.com/image.jpg',
247
+ },
248
+ ],
249
+ },
250
+ };
251
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
252
+
253
+ const payload: CreateImagePayload = {
254
+ model: 'flux/dev',
255
+ params: {
256
+ prompt: 'Test without dimensions',
257
+ },
258
+ };
259
+
260
+ // Act
261
+ const result = await instance.createImage(payload);
262
+
263
+ // Assert
264
+ expect(result).toEqual({
265
+ imageUrl: 'https://example.com/image.jpg',
266
+ });
267
+ });
268
+
269
+ describe('Error handling', () => {
270
+ it('should throw InvalidProviderAPIKey on 401 error', async () => {
271
+ // Arrange
272
+ const apiError = new Error('Unauthorized') as Error & { status: number };
273
+ apiError.status = 401;
274
+ mockFal.subscribe.mockRejectedValue(apiError);
275
+
276
+ const payload: CreateImagePayload = {
277
+ model: 'flux/dev',
278
+ params: {
279
+ prompt: 'Test image',
280
+ },
281
+ };
282
+
283
+ // Act & Assert
284
+ await expect(instance.createImage(payload)).rejects.toEqual({
285
+ error: { error: apiError },
286
+ errorType: invalidErrorType,
287
+ });
288
+ });
289
+
290
+ it('should throw ProviderBizError on other errors', async () => {
291
+ // Arrange
292
+ const apiError = new Error('Some other error');
293
+ mockFal.subscribe.mockRejectedValue(apiError);
294
+
295
+ const payload: CreateImagePayload = {
296
+ model: 'flux/dev',
297
+ params: {
298
+ prompt: 'Test image',
299
+ },
300
+ };
301
+
302
+ // Act & Assert
303
+ await expect(instance.createImage(payload)).rejects.toEqual({
304
+ error: { error: apiError },
305
+ errorType: bizErrorType,
306
+ });
307
+ });
308
+
309
+ it('should throw ProviderBizError on non-401 status errors', async () => {
310
+ // Arrange
311
+ const apiError = new Error('Server error') as Error & { status: number };
312
+ apiError.status = 500;
313
+ mockFal.subscribe.mockRejectedValue(apiError);
314
+
315
+ const payload: CreateImagePayload = {
316
+ model: 'flux/dev',
317
+ params: {
318
+ prompt: 'Test image',
319
+ },
320
+ };
321
+
322
+ // Act & Assert
323
+ await expect(instance.createImage(payload)).rejects.toEqual({
324
+ error: { error: apiError },
325
+ errorType: bizErrorType,
326
+ });
327
+ });
328
+ });
329
+
330
+ describe('Edge cases', () => {
331
+ it('should handle empty params object', async () => {
332
+ // Arrange
333
+ const mockImageResponse = {
334
+ requestId: 'test-request-id',
335
+ data: {
336
+ images: [
337
+ {
338
+ url: 'https://example.com/image.jpg',
339
+ },
340
+ ],
341
+ },
342
+ };
343
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
344
+
345
+ const payload: CreateImagePayload = {
346
+ model: 'flux/dev',
347
+ params: {
348
+ prompt: 'Empty params test',
349
+ },
350
+ };
351
+
352
+ // Act
353
+ const result = await instance.createImage(payload);
354
+
355
+ // Assert
356
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/flux/dev', {
357
+ input: {
358
+ enable_safety_checker: false,
359
+ num_images: 1,
360
+ prompt: 'Empty params test',
361
+ },
362
+ });
363
+ expect(result).toEqual({
364
+ imageUrl: 'https://example.com/image.jpg',
365
+ });
366
+ });
367
+
368
+ it('should handle model with different format', async () => {
369
+ // Arrange
370
+ const mockImageResponse = {
371
+ requestId: 'test-request-id',
372
+ data: {
373
+ images: [
374
+ {
375
+ url: 'https://example.com/image.jpg',
376
+ },
377
+ ],
378
+ },
379
+ };
380
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
381
+
382
+ const payload: CreateImagePayload = {
383
+ model: 'some-custom-model',
384
+ params: {
385
+ prompt: 'Test with custom model',
386
+ },
387
+ };
388
+
389
+ // Act
390
+ await instance.createImage(payload);
391
+
392
+ // Assert
393
+ expect(mockFal.subscribe).toHaveBeenCalledWith('fal-ai/some-custom-model', {
394
+ input: {
395
+ enable_safety_checker: false,
396
+ num_images: 1,
397
+ prompt: 'Test with custom model',
398
+ },
399
+ });
400
+ });
401
+
402
+ it('should handle response with multiple images (take first one)', async () => {
403
+ // Arrange
404
+ const mockImageResponse = {
405
+ requestId: 'test-request-id',
406
+ data: {
407
+ images: [
408
+ {
409
+ url: 'https://example.com/image1.jpg',
410
+ width: 1024,
411
+ height: 1024,
412
+ },
413
+ {
414
+ url: 'https://example.com/image2.jpg',
415
+ width: 512,
416
+ height: 512,
417
+ },
418
+ ],
419
+ },
420
+ };
421
+ mockFal.subscribe.mockResolvedValue(mockImageResponse as any);
422
+
423
+ const payload: CreateImagePayload = {
424
+ model: 'flux/dev',
425
+ params: {
426
+ prompt: 'Multiple images test',
427
+ },
428
+ };
429
+
430
+ // Act
431
+ const result = await instance.createImage(payload);
432
+
433
+ // Assert
434
+ expect(result).toEqual({
435
+ imageUrl: 'https://example.com/image1.jpg',
436
+ width: 1024,
437
+ height: 1024,
438
+ });
439
+ });
440
+ });
441
+ });
442
+ });
@@ -0,0 +1,88 @@
1
+ import { fal } from '@fal-ai/client';
2
+ import debug from 'debug';
3
+ import { pick } from 'lodash-es';
4
+ import { ClientOptions } from 'openai';
5
+
6
+ import { RuntimeImageGenParamsValue } from '@/libs/standard-parameters/meta-schema';
7
+
8
+ import { LobeRuntimeAI } from '../BaseAI';
9
+ import { AgentRuntimeErrorType } from '../error';
10
+ import { CreateImagePayload, CreateImageResponse } from '../types/image';
11
+ import { AgentRuntimeError } from '../utils/createError';
12
+
13
+ // Create debug logger
14
+ const log = debug('lobe-image:fal');
15
+
16
+ type FluxDevOutput = Awaited<ReturnType<typeof fal.subscribe<'fal-ai/flux/dev'>>>['data'];
17
+
18
+ export class LobeFalAI implements LobeRuntimeAI {
19
+ constructor({ apiKey }: ClientOptions = {}) {
20
+ if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
21
+
22
+ fal.config({
23
+ credentials: apiKey,
24
+ });
25
+ log('FalAI initialized with apiKey: %s', apiKey ? '*****' : 'Not set');
26
+ }
27
+
28
+ async createImage(payload: CreateImagePayload): Promise<CreateImageResponse> {
29
+ const { model, params } = payload;
30
+ log('Creating image with model: %s and params: %O', model, params);
31
+
32
+ const paramsMap = new Map<RuntimeImageGenParamsValue, string>([
33
+ ['steps', 'num_inference_steps'],
34
+ ['cfg', 'guidance_scale'],
35
+ ['imageUrl', 'image_url'],
36
+ ]);
37
+
38
+ const defaultInput = {
39
+ enable_safety_checker: false,
40
+ num_images: 1,
41
+ };
42
+ const userInput = Object.fromEntries(
43
+ (Object.entries(params) as [keyof typeof params, any][]).map(([key, value]) => [
44
+ paramsMap.get(key) ?? key,
45
+ value,
46
+ ]),
47
+ );
48
+
49
+ if ('width' in userInput && 'height' in userInput) {
50
+ userInput.image_size = {
51
+ height: userInput.height,
52
+ width: userInput.width,
53
+ };
54
+ delete userInput.width;
55
+ delete userInput.height;
56
+ }
57
+
58
+ const endpoint = `fal-ai/${model}`;
59
+ log('Calling fal.subscribe with endpoint: %s and input: %O', endpoint, {
60
+ ...defaultInput,
61
+ ...userInput,
62
+ });
63
+
64
+ try {
65
+ const { data } = await fal.subscribe(endpoint, {
66
+ input: {
67
+ ...defaultInput,
68
+ ...userInput,
69
+ },
70
+ });
71
+ const image = (data as FluxDevOutput).images[0];
72
+ log('Received image data: %O', image);
73
+
74
+ return {
75
+ imageUrl: image.url,
76
+ ...pick(image, ['width', 'height']),
77
+ };
78
+ } catch (error) {
79
+ if (error instanceof Error && 'status' in error && (error as any).status === 401) {
80
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey, {
81
+ error,
82
+ });
83
+ }
84
+
85
+ throw AgentRuntimeError.createError(AgentRuntimeErrorType.ProviderBizError, { error });
86
+ }
87
+ }
88
+ }