libre-webui 0.15.0 → 0.16.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 (231) hide show
  1. package/README.md +26 -10
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +63 -0
  4. package/backend/dist/db.js.map +1 -1
  5. package/backend/dist/index.d.ts.map +1 -1
  6. package/backend/dist/index.js +4 -4
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/routes/chat.d.ts.map +1 -1
  9. package/backend/dist/routes/chat.js +93 -10
  10. package/backend/dist/routes/chat.js.map +1 -1
  11. package/backend/dist/routes/imageGen.d.ts.map +1 -1
  12. package/backend/dist/routes/imageGen.js +52 -29
  13. package/backend/dist/routes/imageGen.js.map +1 -1
  14. package/backend/dist/routes/plugins.d.ts.map +1 -1
  15. package/backend/dist/routes/plugins.js +218 -41
  16. package/backend/dist/routes/plugins.js.map +1 -1
  17. package/backend/dist/routes/preferences.d.ts.map +1 -1
  18. package/backend/dist/routes/preferences.js +5 -4
  19. package/backend/dist/routes/preferences.js.map +1 -1
  20. package/backend/dist/routes/tts.js +2 -2
  21. package/backend/dist/routes/tts.js.map +1 -1
  22. package/backend/dist/services/assistantCompletionService.d.ts +2 -1
  23. package/backend/dist/services/assistantCompletionService.d.ts.map +1 -1
  24. package/backend/dist/services/assistantCompletionService.js +3 -1
  25. package/backend/dist/services/assistantCompletionService.js.map +1 -1
  26. package/backend/dist/services/chatGenerationService.d.ts +5 -3
  27. package/backend/dist/services/chatGenerationService.d.ts.map +1 -1
  28. package/backend/dist/services/chatGenerationService.js +18 -6
  29. package/backend/dist/services/chatGenerationService.js.map +1 -1
  30. package/backend/dist/services/chatRequestService.d.ts +6 -4
  31. package/backend/dist/services/chatRequestService.d.ts.map +1 -1
  32. package/backend/dist/services/chatRequestService.js +10 -2
  33. package/backend/dist/services/chatRequestService.js.map +1 -1
  34. package/backend/dist/services/chatService.d.ts +2 -2
  35. package/backend/dist/services/chatService.d.ts.map +1 -1
  36. package/backend/dist/services/chatService.js +29 -17
  37. package/backend/dist/services/chatService.js.map +1 -1
  38. package/backend/dist/services/embeddingService.js +1 -1
  39. package/backend/dist/services/embeddingService.js.map +1 -1
  40. package/backend/dist/services/pluginActivationService.d.ts +11 -0
  41. package/backend/dist/services/pluginActivationService.d.ts.map +1 -0
  42. package/backend/dist/services/pluginActivationService.js +163 -0
  43. package/backend/dist/services/pluginActivationService.js.map +1 -0
  44. package/backend/dist/services/pluginCapabilityRegistryService.d.ts +1 -1
  45. package/backend/dist/services/pluginCapabilityRegistryService.d.ts.map +1 -1
  46. package/backend/dist/services/pluginCapabilityRegistryService.js +1 -1
  47. package/backend/dist/services/pluginCapabilityRegistryService.js.map +1 -1
  48. package/backend/dist/services/pluginCredentialsService.d.ts +16 -8
  49. package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
  50. package/backend/dist/services/pluginCredentialsService.js +47 -14
  51. package/backend/dist/services/pluginCredentialsService.js.map +1 -1
  52. package/backend/dist/services/pluginEmbeddingService.d.ts +2 -2
  53. package/backend/dist/services/pluginEmbeddingService.d.ts.map +1 -1
  54. package/backend/dist/services/pluginEmbeddingService.js +33 -13
  55. package/backend/dist/services/pluginEmbeddingService.js.map +1 -1
  56. package/backend/dist/services/pluginImageGenerationService.d.ts +11 -7
  57. package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -1
  58. package/backend/dist/services/pluginImageGenerationService.js +265 -81
  59. package/backend/dist/services/pluginImageGenerationService.js.map +1 -1
  60. package/backend/dist/services/pluginService.d.ts +55 -25
  61. package/backend/dist/services/pluginService.d.ts.map +1 -1
  62. package/backend/dist/services/pluginService.js +663 -174
  63. package/backend/dist/services/pluginService.js.map +1 -1
  64. package/backend/dist/services/pluginTTSService.d.ts +4 -4
  65. package/backend/dist/services/pluginTTSService.d.ts.map +1 -1
  66. package/backend/dist/services/pluginTTSService.js +24 -28
  67. package/backend/dist/services/pluginTTSService.js.map +1 -1
  68. package/backend/dist/services/pluginVariablesService.d.ts +6 -1
  69. package/backend/dist/services/pluginVariablesService.d.ts.map +1 -1
  70. package/backend/dist/services/pluginVariablesService.js +59 -1
  71. package/backend/dist/services/pluginVariablesService.js.map +1 -1
  72. package/backend/dist/services/preferencesService.d.ts +2 -2
  73. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  74. package/backend/dist/services/preferencesService.js +78 -10
  75. package/backend/dist/services/preferencesService.js.map +1 -1
  76. package/backend/dist/services/titleGenerationService.d.ts +6 -4
  77. package/backend/dist/services/titleGenerationService.d.ts.map +1 -1
  78. package/backend/dist/services/titleGenerationService.js +9 -5
  79. package/backend/dist/services/titleGenerationService.js.map +1 -1
  80. package/backend/dist/services/workAgentGuidance.d.ts +1 -0
  81. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  82. package/backend/dist/services/workAgentGuidance.js +2 -0
  83. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  84. package/backend/dist/services/workAgentService.d.ts +5 -2
  85. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  86. package/backend/dist/services/workAgentService.js +379 -53
  87. package/backend/dist/services/workAgentService.js.map +1 -1
  88. package/backend/dist/services/workModelProviderService.d.ts +8 -3
  89. package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
  90. package/backend/dist/services/workModelProviderService.js +293 -33
  91. package/backend/dist/services/workModelProviderService.js.map +1 -1
  92. package/backend/dist/services/workTaskService.d.ts +2 -0
  93. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  94. package/backend/dist/services/workTaskService.js +89 -14
  95. package/backend/dist/services/workTaskService.js.map +1 -1
  96. package/backend/dist/storage.d.ts.map +1 -1
  97. package/backend/dist/storage.js +9 -6
  98. package/backend/dist/storage.js.map +1 -1
  99. package/backend/dist/storageMappers.d.ts +3 -0
  100. package/backend/dist/storageMappers.d.ts.map +1 -1
  101. package/backend/dist/storageMappers.js +3 -0
  102. package/backend/dist/storageMappers.js.map +1 -1
  103. package/backend/dist/types/index.d.ts +54 -1
  104. package/backend/dist/types/index.d.ts.map +1 -1
  105. package/backend/dist/types/index.js.map +1 -1
  106. package/backend/dist/types/work.d.ts +1 -1
  107. package/backend/dist/types/work.d.ts.map +1 -1
  108. package/backend/dist/utils/chatContext.d.ts +13 -1
  109. package/backend/dist/utils/chatContext.d.ts.map +1 -1
  110. package/backend/dist/utils/chatContext.js +52 -0
  111. package/backend/dist/utils/chatContext.js.map +1 -1
  112. package/backend/dist/utils/chatProviderSelection.d.ts +15 -0
  113. package/backend/dist/utils/chatProviderSelection.d.ts.map +1 -0
  114. package/backend/dist/utils/chatProviderSelection.js +59 -0
  115. package/backend/dist/utils/chatProviderSelection.js.map +1 -0
  116. package/backend/dist/utils/imageGenerationValidation.d.ts +3 -0
  117. package/backend/dist/utils/imageGenerationValidation.d.ts.map +1 -0
  118. package/backend/dist/utils/imageGenerationValidation.js +55 -0
  119. package/backend/dist/utils/imageGenerationValidation.js.map +1 -0
  120. package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -1
  121. package/backend/dist/utils/ollamaLibrary.js +81 -8
  122. package/backend/dist/utils/ollamaLibrary.js.map +1 -1
  123. package/backend/dist/utils/openAIResponsesAdapter.d.ts +88 -0
  124. package/backend/dist/utils/openAIResponsesAdapter.d.ts.map +1 -0
  125. package/backend/dist/utils/openAIResponsesAdapter.js +521 -0
  126. package/backend/dist/utils/openAIResponsesAdapter.js.map +1 -0
  127. package/backend/dist/utils/pluginChatAdapter.d.ts +7 -4
  128. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
  129. package/backend/dist/utils/pluginChatAdapter.js +33 -5
  130. package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
  131. package/backend/dist/utils/pluginChatContext.d.ts.map +1 -1
  132. package/backend/dist/utils/pluginChatContext.js +5 -1
  133. package/backend/dist/utils/pluginChatContext.js.map +1 -1
  134. package/backend/dist/utils/pluginConnectionVariables.d.ts +6 -0
  135. package/backend/dist/utils/pluginConnectionVariables.d.ts.map +1 -0
  136. package/backend/dist/utils/pluginConnectionVariables.js +52 -0
  137. package/backend/dist/utils/pluginConnectionVariables.js.map +1 -0
  138. package/backend/dist/utils/pluginDefinitionTrust.d.ts +9 -0
  139. package/backend/dist/utils/pluginDefinitionTrust.d.ts.map +1 -0
  140. package/backend/dist/utils/pluginDefinitionTrust.js +71 -0
  141. package/backend/dist/utils/pluginDefinitionTrust.js.map +1 -0
  142. package/backend/dist/utils/pluginStreamAdapter.d.ts +3 -0
  143. package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
  144. package/backend/dist/utils/pluginStreamAdapter.js +410 -0
  145. package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
  146. package/backend/dist/utils/pluginStreaming.d.ts +7 -1
  147. package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
  148. package/backend/dist/utils/pluginStreaming.js +12 -1
  149. package/backend/dist/utils/pluginStreaming.js.map +1 -1
  150. package/backend/dist/utils/pluginValidation.d.ts +21 -2
  151. package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
  152. package/backend/dist/utils/pluginValidation.js +190 -17
  153. package/backend/dist/utils/pluginValidation.js.map +1 -1
  154. package/backend/dist/utils/pluginVariableValidation.d.ts +7 -0
  155. package/backend/dist/utils/pluginVariableValidation.d.ts.map +1 -1
  156. package/backend/dist/utils/pluginVariableValidation.js +79 -4
  157. package/backend/dist/utils/pluginVariableValidation.js.map +1 -1
  158. package/backend/dist/websocketServer.d.ts.map +1 -1
  159. package/backend/dist/websocketServer.js +16 -3
  160. package/backend/dist/websocketServer.js.map +1 -1
  161. package/backend/package.json +1 -1
  162. package/frontend/dist/assets/index-CKV9Lnj8.js +5 -0
  163. package/frontend/dist/assets/index-Dme2Ozol.css +1 -0
  164. package/frontend/dist/index.html +4 -4
  165. package/frontend/dist/js/{ArtifactContainer-CXfmVx8T.js → ArtifactContainer-B0uVR8Z9.js} +1 -1
  166. package/frontend/dist/js/{ArtifactDemoPage-B1_AuMd2.js → ArtifactDemoPage-DBVPHFxk.js} +1 -1
  167. package/frontend/dist/js/{ArtifactSlideOutPanel-C0eTguNL.js → ArtifactSlideOutPanel-Cg5kAwv9.js} +1 -1
  168. package/frontend/dist/js/ChatPage-CPHlIZqU.js +16 -0
  169. package/frontend/dist/js/{FirstTimeSetup-DOoZUaRs.js → FirstTimeSetup-DksL5arK.js} +1 -1
  170. package/frontend/dist/js/GalleryPage-1mdUGo_N.js +1 -0
  171. package/frontend/dist/js/LibreClawPage-qGr1Jy8p.js +1 -0
  172. package/frontend/dist/js/{ModelManagerModals-DneqFQJC.js → ModelManagerModals-DltRprHU.js} +1 -1
  173. package/frontend/dist/js/ModelSelector-CUnwRmNU.js +1 -0
  174. package/frontend/dist/js/ModelsPage-DMYVg3LT.js +2 -0
  175. package/frontend/dist/js/PersonasPage-Ct-KKW-j.js +13 -0
  176. package/frontend/dist/js/{RichMessageContent-WQLdJ5jq.js → RichMessageContent-CGBAcUES.js} +1 -1
  177. package/frontend/dist/js/SettingsModal-BVj3gFCS.js +1 -0
  178. package/frontend/dist/js/UserManagementPage-D3tLGtTu.js +1 -0
  179. package/frontend/dist/js/WorkPage-DhTjy-kf.js +2 -0
  180. package/frontend/dist/js/api-YWoSobKv.js +19 -0
  181. package/frontend/dist/js/{ar-CCVY0-7X.js → ar-KceI0PfT.js} +1 -1
  182. package/frontend/dist/js/{bn-CoU5CSph.js → bn-DsKo2dzS.js} +1 -1
  183. package/frontend/dist/js/{cs-CR6pjIbG.js → cs-6UKhKj0A.js} +1 -1
  184. package/frontend/dist/js/{da-CkyYITRz.js → da-CJgZ3YuL.js} +1 -1
  185. package/frontend/dist/js/{de-DwNANzB-.js → de-BZAmOuCT.js} +1 -1
  186. package/frontend/dist/js/{es-TxygiaVQ.js → es-CD4u7no9.js} +1 -1
  187. package/frontend/dist/js/{estree-DPzMpjFm.js → estree-PDLT672k.js} +1 -1
  188. package/frontend/dist/js/{fr-DvEqVpZr.js → fr-Cou2jOS3.js} +1 -1
  189. package/frontend/dist/js/{hi-CyrWkyh7.js → hi-ZxkbNCyB.js} +1 -1
  190. package/frontend/dist/js/{html-R50jmBYq.js → html-CkhF9wDQ.js} +1 -1
  191. package/frontend/dist/js/{id-PJjIGHGM.js → id-DT9CqaWQ.js} +1 -1
  192. package/frontend/dist/js/{is-ChXcN1tI.js → is-BWSbAIoq.js} +1 -1
  193. package/frontend/dist/js/{it-DsgM8-MJ.js → it-CdNoTXm3.js} +1 -1
  194. package/frontend/dist/js/{ja-ivtt80N2.js → ja-CIuQg0xv.js} +1 -1
  195. package/frontend/dist/js/{ko-B8l_Ice1.js → ko-BWRUjDOH.js} +1 -1
  196. package/frontend/dist/js/{markdown-C08x2H-K.js → markdown-B1iNUbse.js} +1 -1
  197. package/frontend/dist/js/{ms-DoUBI6Qi.js → ms-gJstIj2a.js} +1 -1
  198. package/frontend/dist/js/{nl-DsX5Hj8m.js → nl-CydjVNUw.js} +1 -1
  199. package/frontend/dist/js/{pl-Db_NKL4x.js → pl-DPN9mAa5.js} +1 -1
  200. package/frontend/dist/js/{pt-CpzBhb5J.js → pt-MRXGqIjQ.js} +1 -1
  201. package/frontend/dist/js/{ru-DCXvJyXJ.js → ru-CvQ9GkJF.js} +1 -1
  202. package/frontend/dist/js/{standalone-DpMZUvjd.js → standalone-CCd6rkf-.js} +1 -1
  203. package/frontend/dist/js/{sv-DcVoybc7.js → sv-BxS2fvhU.js} +1 -1
  204. package/frontend/dist/js/{th-D0G7HyQn.js → th-DMxyn6e4.js} +1 -1
  205. package/frontend/dist/js/{tr-CMDOh4Aq.js → tr-BTPldUaA.js} +1 -1
  206. package/frontend/dist/js/{typescript-B6y_hxBm.js → typescript-DKJ1uM5L.js} +1 -1
  207. package/frontend/dist/js/ui-vendor-CAFEDSaO.js +177 -0
  208. package/frontend/dist/js/{uk-D2MMa6LP.js → uk-BGxZOKgp.js} +1 -1
  209. package/frontend/dist/js/useBaseQuery-MuQj7vA5.js +1 -0
  210. package/frontend/dist/js/useQuery-CYGPkxWR.js +1 -0
  211. package/frontend/dist/js/{vi-B8uHT8yh.js → vi-82JkwFsQ.js} +1 -1
  212. package/frontend/dist/js/{zh-CNqa_15K.js → zh-Bxxfwci3.js} +1 -1
  213. package/package.json +3 -2
  214. package/plugins/github.json +1 -2
  215. package/plugins/huggingface.json +30 -30
  216. package/plugins/mlx-lm.json +55 -0
  217. package/plugins/openai.json +53 -22
  218. package/frontend/dist/assets/index-Bv-twCuY.css +0 -1
  219. package/frontend/dist/assets/index-uCHBH_PM.js +0 -5
  220. package/frontend/dist/js/ChatPage-1bK0gDBy.js +0 -16
  221. package/frontend/dist/js/GalleryPage-6WwyH3kt.js +0 -1
  222. package/frontend/dist/js/LibreClawPage-ChHVfPLb.js +0 -1
  223. package/frontend/dist/js/ModelsPage-Dw6wdiMt.js +0 -2
  224. package/frontend/dist/js/PersonasPage-BxdI2e1-.js +0 -13
  225. package/frontend/dist/js/SettingsModal-C-cK6TNb.js +0 -1
  226. package/frontend/dist/js/UserManagementPage-DqjNXPkf.js +0 -1
  227. package/frontend/dist/js/WorkPage-MGR3odo6.js +0 -2
  228. package/frontend/dist/js/api-DD5qhhLf.js +0 -19
  229. package/frontend/dist/js/ui-vendor-DUqvZsfs.js +0 -177
  230. package/frontend/dist/js/useBaseQuery-Duml3DYp.js +0 -1
  231. package/frontend/dist/js/useQuery-C0lZwNoc.js +0 -1
@@ -16,23 +16,78 @@
16
16
  */
17
17
  import fs from 'fs';
18
18
  import path from 'path';
19
+ import { createHash } from 'crypto';
19
20
  import sanitize from 'sanitize-filename';
20
21
  import axios from 'axios';
21
22
  import pluginCredentialsService from './pluginCredentialsService.js';
22
23
  import pluginVariablesService from './pluginVariablesService.js';
24
+ import pluginActivationService from './pluginActivationService.js';
23
25
  import { PluginCapabilityRegistryService } from './pluginCapabilityRegistryService.js';
24
26
  import { PluginEmbeddingService } from './pluginEmbeddingService.js';
25
27
  import { PluginImageGenerationService } from './pluginImageGenerationService.js';
26
28
  import { PluginTTSService } from './pluginTTSService.js';
27
29
  import { applyPluginDefinitionPolicy, buildPluginChatPayload, convertProviderResponse, getOpenAICompatibleSamplingParameters, resolvePluginChatParameters, toOpenAICompatibleMessages, } from '../utils/pluginChatAdapter.js';
28
- import { streamAnthropicResponse, streamOpenAICompatibleResponse, } from '../utils/pluginStreamAdapter.js';
29
- import { applyModelEndpointTemplate, assertSafePluginEndpoint, buildPluginAuthHeaders, buildPluginModelDiscoveryHeaders, resolvePluginEndpoint, resolvePluginModelsEndpoint, validatePluginEndpointOverride, validatePluginModel, } from '../utils/pluginValidation.js';
30
+ import { streamAnthropicResponse, streamOpenAICompatibleResponse, streamOpenAIResponsesResponse, } from '../utils/pluginStreamAdapter.js';
31
+ import { applyModelEndpointTemplate, assertSafePluginEndpoint, buildPluginAuthHeaders, buildPluginModelDiscoveryHeaders, pluginRequiresApiKey, resolvePluginApiConfig, resolvePluginEndpoint, resolvePluginModelsEndpoint, validatePluginModel, } from '../utils/pluginValidation.js';
30
32
  import { createLogger } from '../utils/logger.js';
31
33
  import { resolveBundledPluginsDir } from '../utils/packagePaths.js';
34
+ import { getDatabaseSafe } from '../db.js';
35
+ import { createOpenAIResponsesStateScope, createPluginCredentialFingerprint, OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY, } from '../utils/openAIResponsesAdapter.js';
36
+ import { getPluginConnectionVariableNames } from '../utils/pluginConnectionVariables.js';
37
+ import { getPluginDefinitionFingerprint, matchesBundledPluginTrustAnchor, } from '../utils/pluginDefinitionTrust.js';
32
38
  const logger = createLogger('plugins');
33
- class PluginService {
39
+ function getPluginRoutingAuthProjection(plugin) {
40
+ const capabilityEndpoints = Object.entries((plugin.capabilities || {}))
41
+ .sort(([left], [right]) => left.localeCompare(right))
42
+ .map(([name, value]) => {
43
+ const capability = value && typeof value === 'object'
44
+ ? value
45
+ : {};
46
+ const config = capability.config && typeof capability.config === 'object'
47
+ ? capability.config
48
+ : {};
49
+ return {
50
+ name,
51
+ endpoint: capability.endpoint ?? null,
52
+ endpoint_variable: config.endpoint_variable ?? capability.endpoint_variable ?? null,
53
+ };
54
+ });
55
+ const definitions = new Map((plugin.variables || []).map(definition => [definition.name, definition]));
56
+ const connectionVariables = Array.from(getPluginConnectionVariableNames(plugin), name => {
57
+ const definition = definitions.get(name);
58
+ if (!definition)
59
+ return null;
60
+ return {
61
+ name: definition.name,
62
+ type: definition.type,
63
+ label: definition.label,
64
+ description: definition.description ?? null,
65
+ default: definition.default ?? null,
66
+ required: definition.required ?? null,
67
+ sensitive: definition.sensitive ?? null,
68
+ options: definition.options ?? null,
69
+ min: definition.min ?? null,
70
+ max: definition.max ?? null,
71
+ };
72
+ });
73
+ return JSON.stringify({
74
+ endpoint: plugin.endpoint,
75
+ base_url: plugin.base_url ?? null,
76
+ api_path: plugin.api_path ?? null,
77
+ api_mode: plugin.api_mode ?? null,
78
+ auth: {
79
+ header: plugin.auth.header,
80
+ prefix: plugin.auth.prefix ?? '',
81
+ key_env: plugin.auth.key_env,
82
+ },
83
+ capabilityEndpoints,
84
+ connectionVariables,
85
+ });
86
+ }
87
+ export class PluginService {
34
88
  constructor() {
35
- this.activePluginIds = new Set();
89
+ this.discoveredModelsCache = new Map();
90
+ this.bundledRoutingProjectionCache = new Map();
36
91
  this.bundledPluginsDir = resolveBundledPluginsDir(import.meta.url);
37
92
  this.legacyPluginsDir = path.join(process.cwd(), 'plugins');
38
93
  this.pluginsDir =
@@ -42,67 +97,280 @@ class PluginService {
42
97
  : this.legacyPluginsDir);
43
98
  this.pluginReadDirs = Array.from(new Set([this.bundledPluginsDir, this.legacyPluginsDir, this.pluginsDir]));
44
99
  this.ensurePluginsDirectory();
45
- this.loadActivePlugins();
100
+ pluginActivationService.migrateLegacyStatus([this.pluginsDir, this.legacyPluginsDir], pluginId => this.canMigrateLegacyActivation(pluginId));
46
101
  this.embeddingService = new PluginEmbeddingService({
47
- getAllPlugins: () => this.getAllPlugins(),
102
+ getAllPlugins: userId => this.getActivePlugins(userId),
48
103
  getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
49
104
  getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
50
105
  validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
51
106
  });
52
107
  this.ttsService = new PluginTTSService({
53
- getAllPlugins: () => this.getAllPlugins(),
54
- getPlugin: id => this.getPlugin(id),
108
+ getAllPlugins: userId => this.getActivePlugins(userId),
109
+ getPlugin: (id, userId) => this.getPlugin(id, userId),
55
110
  getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
56
111
  getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
57
112
  validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
58
113
  });
59
114
  this.imageGenerationService = new PluginImageGenerationService({
60
- getAllPlugins: () => this.getAllPlugins(),
61
- getPlugin: id => this.getPlugin(id),
115
+ getAllPlugins: userId => this.getActivePlugins(userId),
116
+ getPlugin: (id, userId) => this.getPlugin(id, userId),
62
117
  getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
63
118
  getPluginVariables: (plugin, userId) => this.getPluginVariables(plugin, userId),
64
119
  validateEndpointUrl: endpoint => this.validateEndpointUrl(endpoint),
65
120
  });
66
121
  this.capabilityRegistryService = new PluginCapabilityRegistryService({
67
- getAllPlugins: () => this.getAllPlugins(),
122
+ getAllPlugins: userId => this.getActivePlugins(userId),
68
123
  getApiKey: (plugin, userId) => this.getApiKey(plugin, userId),
69
124
  });
70
125
  }
126
+ discoveredModelsCacheKey(pluginId, userId) {
127
+ return `${userId}:${pluginId}`;
128
+ }
129
+ getDiscoveredModels(pluginId, userId) {
130
+ const effectiveUserId = userId || 'default';
131
+ const cacheKey = this.discoveredModelsCacheKey(pluginId, effectiveUserId);
132
+ if (this.discoveredModelsCache.has(cacheKey)) {
133
+ return this.discoveredModelsCache.get(cacheKey) || undefined;
134
+ }
135
+ const db = getDatabaseSafe();
136
+ if (!db) {
137
+ this.discoveredModelsCache.set(cacheKey, null);
138
+ return undefined;
139
+ }
140
+ try {
141
+ const row = db
142
+ .prepare('SELECT models_json FROM plugin_discovered_models WHERE user_id = ? AND plugin_id = ?')
143
+ .get(effectiveUserId, pluginId);
144
+ if (!row) {
145
+ this.discoveredModelsCache.set(cacheKey, null);
146
+ return undefined;
147
+ }
148
+ const parsed = JSON.parse(row.models_json);
149
+ const models = Array.isArray(parsed)
150
+ ? Array.from(new Set(parsed.filter((model) => typeof model === 'string' && model.length > 0)))
151
+ : [];
152
+ if (models.length === 0) {
153
+ this.discoveredModelsCache.set(cacheKey, null);
154
+ return undefined;
155
+ }
156
+ this.discoveredModelsCache.set(cacheKey, models);
157
+ return models;
158
+ }
159
+ catch (error) {
160
+ logger.warn('Failed to read discovered models for plugin %s:', pluginId, error);
161
+ this.discoveredModelsCache.set(cacheKey, null);
162
+ return undefined;
163
+ }
164
+ }
165
+ storeDiscoveredModels(pluginId, models, userId) {
166
+ const effectiveUserId = userId || 'default';
167
+ const uniqueModels = Array.from(new Set(models));
168
+ const cacheKey = this.discoveredModelsCacheKey(pluginId, effectiveUserId);
169
+ this.discoveredModelsCache.set(cacheKey, uniqueModels);
170
+ const db = getDatabaseSafe();
171
+ if (!db)
172
+ return;
173
+ try {
174
+ db.prepare(`INSERT INTO plugin_discovered_models
175
+ (user_id, plugin_id, models_json, updated_at)
176
+ VALUES (?, ?, ?, ?)
177
+ ON CONFLICT(user_id, plugin_id) DO UPDATE SET
178
+ models_json = excluded.models_json,
179
+ updated_at = excluded.updated_at`).run(effectiveUserId, pluginId, JSON.stringify(uniqueModels), Date.now());
180
+ }
181
+ catch (error) {
182
+ logger.warn('Failed to persist discovered models for plugin %s:', pluginId, error);
183
+ }
184
+ }
185
+ clearDiscoveredModels(pluginId, userId) {
186
+ const db = getDatabaseSafe();
187
+ if (userId) {
188
+ this.discoveredModelsCache.set(this.discoveredModelsCacheKey(pluginId, userId), null);
189
+ if (!db)
190
+ return;
191
+ try {
192
+ db.prepare('DELETE FROM plugin_discovered_models WHERE user_id = ? AND plugin_id = ?').run(userId, pluginId);
193
+ }
194
+ catch (error) {
195
+ logger.warn('Failed to clear discovered models for plugin %s:', pluginId, error);
196
+ }
197
+ return;
198
+ }
199
+ for (const key of this.discoveredModelsCache.keys()) {
200
+ if (key.endsWith(`:${pluginId}`)) {
201
+ this.discoveredModelsCache.delete(key);
202
+ }
203
+ }
204
+ if (!db)
205
+ return;
206
+ try {
207
+ db.prepare('DELETE FROM plugin_discovered_models WHERE plugin_id = ?').run(pluginId);
208
+ }
209
+ catch (error) {
210
+ logger.warn('Failed to clear discovered models for plugin %s:', pluginId, error);
211
+ }
212
+ }
213
+ applyDiscoveredModels(plugin, userId) {
214
+ if (!this.canUseStoredConnectionOverrides(userId) &&
215
+ pluginVariablesService.hasStoredConnectionOverride(plugin.id, userId, plugin.variables, getPluginConnectionVariableNames(plugin))) {
216
+ return plugin;
217
+ }
218
+ const models = this.getDiscoveredModels(plugin.id, userId);
219
+ return models ? { ...plugin, model_map: [...models] } : plugin;
220
+ }
71
221
  ensurePluginsDirectory() {
72
222
  if (!fs.existsSync(this.pluginsDir)) {
73
223
  fs.mkdirSync(this.pluginsDir, { recursive: true });
74
224
  }
75
225
  }
76
- loadActivePlugins() {
77
- const statusDirs = Array.from(new Set([this.pluginsDir, this.legacyPluginsDir]));
78
- for (const statusDir of statusDirs) {
79
- const statusFile = path.join(statusDir, '.status.json');
80
- if (!fs.existsSync(statusFile)) {
81
- continue;
82
- }
83
- try {
84
- const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
85
- if (Array.isArray(status.activePlugins)) {
86
- this.activePluginIds = new Set(status.activePlugins);
87
- }
88
- else if (status.activePlugin) {
89
- // Legacy support for single active plugin
90
- this.activePluginIds = new Set([status.activePlugin]);
91
- }
92
- return;
226
+ bundledPluginPath(id) {
227
+ return path.resolve(this.bundledPluginsDir, `${sanitize(id)}.json`);
228
+ }
229
+ isAnchoredBundledDefinition(plugin, filePath) {
230
+ return (path.resolve(filePath) === this.bundledPluginPath(plugin.id) &&
231
+ matchesBundledPluginTrustAnchor(plugin));
232
+ }
233
+ isPluginDefinitionApproved(plugin, filePath) {
234
+ if (this.isAnchoredBundledDefinition(plugin, filePath)) {
235
+ return true;
236
+ }
237
+ const db = getDatabaseSafe();
238
+ if (!db)
239
+ return false;
240
+ try {
241
+ const row = db
242
+ .prepare(`SELECT definition_fingerprint, source_path
243
+ FROM plugin_definition_approvals
244
+ WHERE plugin_id = ?`)
245
+ .get(plugin.id);
246
+ return (row?.definition_fingerprint ===
247
+ getPluginDefinitionFingerprint(plugin) &&
248
+ row.source_path === path.resolve(filePath));
249
+ }
250
+ catch (error) {
251
+ logger.warn('Failed to inspect definition approval for plugin %s:', plugin.id, error);
252
+ return false;
253
+ }
254
+ }
255
+ approvePluginDefinition(plugin, filePath, userId) {
256
+ if (!this.canUseStoredConnectionOverrides(userId)) {
257
+ throw new Error('Administrator approval is required');
258
+ }
259
+ const db = getDatabaseSafe();
260
+ if (!db) {
261
+ throw new Error('Database not available for plugin approval');
262
+ }
263
+ db.prepare(`INSERT INTO plugin_definition_approvals
264
+ (plugin_id, definition_fingerprint, source_path,
265
+ approved_by_user_id, approved_at)
266
+ VALUES (?, ?, ?, ?, ?)
267
+ ON CONFLICT(plugin_id) DO UPDATE SET
268
+ definition_fingerprint = excluded.definition_fingerprint,
269
+ source_path = excluded.source_path,
270
+ approved_by_user_id = excluded.approved_by_user_id,
271
+ approved_at = excluded.approved_at`).run(plugin.id, getPluginDefinitionFingerprint(plugin), path.resolve(filePath), userId, Date.now());
272
+ }
273
+ removePluginDefinitionApproval(pluginId) {
274
+ const db = getDatabaseSafe();
275
+ if (!db)
276
+ return;
277
+ db.prepare('DELETE FROM plugin_definition_approvals WHERE plugin_id = ?').run(pluginId);
278
+ }
279
+ revokePluginDefinitionConsent(pluginId) {
280
+ const db = getDatabaseSafe();
281
+ if (!db) {
282
+ throw new Error('Database not available for plugin approval');
283
+ }
284
+ db.transaction(() => {
285
+ db.prepare('DELETE FROM plugin_activations WHERE plugin_id = ?').run(pluginId);
286
+ db.prepare('DELETE FROM plugin_definition_approvals WHERE plugin_id = ?').run(pluginId);
287
+ })();
288
+ }
289
+ canMigrateLegacyActivation(pluginId) {
290
+ const effectivePath = this.resolveEffectivePluginFilePath(pluginId);
291
+ if (!effectivePath)
292
+ return false;
293
+ try {
294
+ const parsedPlugin = JSON.parse(fs.readFileSync(effectivePath, 'utf8'));
295
+ return (this.validatePlugin(parsedPlugin) &&
296
+ parsedPlugin.id === pluginId &&
297
+ this.isAnchoredBundledDefinition(parsedPlugin, effectivePath));
298
+ }
299
+ catch {
300
+ return false;
301
+ }
302
+ }
303
+ resolveEffectivePluginFilePath(id) {
304
+ const sanitizedId = sanitize(id);
305
+ if (!sanitizedId || sanitizedId !== id)
306
+ return null;
307
+ for (const pluginsDir of [...this.pluginReadDirs].reverse()) {
308
+ const resolvedDirectory = path.resolve(pluginsDir);
309
+ const candidate = path.resolve(pluginsDir, `${sanitizedId}.json`);
310
+ if (candidate.startsWith(resolvedDirectory) && fs.existsSync(candidate)) {
311
+ return candidate;
93
312
  }
94
- catch (error) {
95
- logger.error('Failed to load plugin status:', error);
313
+ }
314
+ return null;
315
+ }
316
+ getBundledRoutingProjection(id) {
317
+ if (this.bundledRoutingProjectionCache.has(id)) {
318
+ return this.bundledRoutingProjectionCache.get(id) ?? null;
319
+ }
320
+ const sanitizedId = sanitize(id);
321
+ if (!sanitizedId || sanitizedId !== id)
322
+ return null;
323
+ const bundledDirectory = path.resolve(this.bundledPluginsDir);
324
+ const bundledPath = this.bundledPluginPath(sanitizedId);
325
+ if (!bundledPath.startsWith(bundledDirectory) ||
326
+ !fs.existsSync(bundledPath)) {
327
+ this.bundledRoutingProjectionCache.set(id, null);
328
+ return null;
329
+ }
330
+ try {
331
+ const parsedPlugin = JSON.parse(fs.readFileSync(bundledPath, 'utf8'));
332
+ if (!this.validatePlugin(parsedPlugin) ||
333
+ parsedPlugin.id !== sanitizedId ||
334
+ !matchesBundledPluginTrustAnchor(parsedPlugin)) {
335
+ this.bundledRoutingProjectionCache.set(id, null);
336
+ return null;
96
337
  }
338
+ const projection = getPluginRoutingAuthProjection(applyPluginDefinitionPolicy(parsedPlugin));
339
+ this.bundledRoutingProjectionCache.set(id, projection);
340
+ return projection;
341
+ }
342
+ catch (error) {
343
+ logger.warn('Failed to inspect bundled routing for plugin %s:', id, error);
344
+ this.bundledRoutingProjectionCache.set(id, null);
345
+ return null;
97
346
  }
98
347
  }
99
- saveActivePlugins() {
100
- const statusFile = path.join(this.pluginsDir, '.status.json');
101
- const status = {
102
- activePlugins: Array.from(this.activePluginIds),
103
- lastUpdated: new Date().toISOString(),
104
- };
105
- fs.writeFileSync(statusFile, JSON.stringify(status, null, 2));
348
+ usesTrustedBundledRouting(plugin) {
349
+ const effectivePath = this.resolveEffectivePluginFilePath(plugin.id);
350
+ const bundledPath = this.bundledPluginPath(plugin.id);
351
+ if (!effectivePath || effectivePath !== bundledPath)
352
+ return false;
353
+ const bundledProjection = this.getBundledRoutingProjection(plugin.id);
354
+ return (bundledProjection !== null &&
355
+ getPluginRoutingAuthProjection(plugin) === bundledProjection);
356
+ }
357
+ isPluginActive(pluginId, userId) {
358
+ return pluginActivationService.getActivePluginIds(userId).has(pluginId);
359
+ }
360
+ canUseStoredConnectionOverrides(userId) {
361
+ const db = getDatabaseSafe();
362
+ if (!db)
363
+ return false;
364
+ try {
365
+ const row = db
366
+ .prepare('SELECT role FROM users WHERE id = ?')
367
+ .get(userId || 'default');
368
+ return row?.role === 'admin';
369
+ }
370
+ catch (error) {
371
+ logger.warn('Failed to resolve plugin routing permission:', error);
372
+ return false;
373
+ }
106
374
  }
107
375
  /**
108
376
  * Get API key for a plugin from database (per-user) or environment variable (fallback)
@@ -111,7 +379,57 @@ class PluginService {
111
379
  * @returns The API key or null if not found
112
380
  */
113
381
  getApiKey(plugin, userId) {
114
- return pluginCredentialsService.getApiKey(plugin.id, plugin.auth.key_env, userId);
382
+ const hasHonoredConnectionOverride = this.canUseStoredConnectionOverrides(userId) &&
383
+ pluginVariablesService.hasStoredConnectionOverride(plugin.id, userId, plugin.variables, getPluginConnectionVariableNames(plugin));
384
+ const usesTrustedBundledRouting = this.usesTrustedBundledRouting(plugin);
385
+ const allowTrustedFallback = usesTrustedBundledRouting && !hasHonoredConnectionOverride;
386
+ return pluginCredentialsService.getApiKey(plugin.id, plugin.auth.key_env, userId, {
387
+ allowEnvironmentFallback: allowTrustedFallback,
388
+ expectedRoutingAuthFingerprint: this.getCredentialRoutingAuthFingerprint(plugin, userId),
389
+ allowLegacyUnboundCredential: allowTrustedFallback,
390
+ });
391
+ }
392
+ /**
393
+ * Bind a user credential to the exact routing/authentication contract in
394
+ * effect when they save it. Generation controls are intentionally excluded.
395
+ */
396
+ getCredentialRoutingAuthFingerprint(plugin, userId) {
397
+ const variables = this.getPluginVariables(plugin, userId);
398
+ const effectiveConnectionValues = Array.from(getPluginConnectionVariableNames(plugin), name => {
399
+ const definition = plugin.variables?.find(candidate => candidate.name === name);
400
+ if (!definition)
401
+ return null;
402
+ return {
403
+ name,
404
+ value: variables[name] ?? definition.default ?? '',
405
+ };
406
+ });
407
+ const effectivePath = this.resolveEffectivePluginFilePath(plugin.id);
408
+ let effectiveDefinitionFingerprint = getPluginDefinitionFingerprint(plugin);
409
+ if (effectivePath) {
410
+ try {
411
+ const effectiveDefinition = JSON.parse(fs.readFileSync(effectivePath, 'utf8'));
412
+ if (this.validatePlugin(effectiveDefinition) &&
413
+ effectiveDefinition.id === plugin.id) {
414
+ effectiveDefinitionFingerprint =
415
+ getPluginDefinitionFingerprint(effectiveDefinition);
416
+ }
417
+ }
418
+ catch {
419
+ // Keep the in-memory fingerprint. A missing/invalid source is already
420
+ // excluded from normal plugin loading and cannot gain trust here.
421
+ }
422
+ }
423
+ const fingerprintInput = JSON.stringify({
424
+ plugin_id: plugin.id,
425
+ plugin_type: plugin.type,
426
+ trusted_bundled_source: this.usesTrustedBundledRouting(plugin),
427
+ effective_source_path: effectivePath ? path.resolve(effectivePath) : null,
428
+ effective_definition_fingerprint: effectiveDefinitionFingerprint,
429
+ routing_auth_projection: getPluginRoutingAuthProjection(plugin),
430
+ effective_connection_values: effectiveConnectionValues,
431
+ });
432
+ return createHash('sha256').update(fingerprintInput).digest('hex');
115
433
  }
116
434
  /**
117
435
  * Get resolved variable values for a plugin (decrypted, typed).
@@ -120,34 +438,58 @@ class PluginService {
120
438
  if (!plugin.variables || plugin.variables.length === 0) {
121
439
  return {};
122
440
  }
123
- return pluginVariablesService.getResolvedVariables(plugin.id, plugin.variables, userId);
441
+ const variables = pluginVariablesService.getResolvedVariables(plugin.id, plugin.variables, userId);
442
+ if (this.canUseStoredConnectionOverrides(userId))
443
+ return variables;
444
+ return Object.fromEntries(Object.entries(variables).map(([name, value]) => {
445
+ if (!getPluginConnectionVariableNames(plugin).has(name)) {
446
+ return [name, value];
447
+ }
448
+ const definition = plugin.variables?.find(candidate => candidate.name === name);
449
+ return [name, definition?.default ?? ''];
450
+ }));
124
451
  }
125
452
  /**
126
- * Validate an endpoint URL for safety (SSRF protection).
127
- * Returns the URL string if valid, or null if invalid.
453
+ * Validate an endpoint override as an absolute HTTP or HTTPS URL.
454
+ * Returns the URL string if valid and throws for an invalid explicit value.
128
455
  */
129
456
  validateEndpointUrl(endpoint) {
130
- return validatePluginEndpointOverride(endpoint);
457
+ return resolvePluginEndpoint('', endpoint);
458
+ }
459
+ /**
460
+ * Resolve and validate routing before credential lookup. The credential
461
+ * service's custom-route policy is integrated separately; keeping this
462
+ * boundary ordered prevents a rejected override from ever selecting an
463
+ * environment credential.
464
+ */
465
+ resolveOperationEndpoint(plugin, userId) {
466
+ return resolvePluginApiConfig(plugin, this.getPluginVariables(plugin, userId)).endpoint;
131
467
  }
132
468
  /**
133
- * Attempt to auto-discover available models from a plugin's base endpoint.
469
+ * Attempt to auto-discover available models from a plugin's full API endpoint.
134
470
  * Resolves the provider's model-list endpoint and updates the plugin's model_map.
135
471
  * Falls back silently to the existing model_map if the endpoint is unavailable.
136
472
  */
137
473
  async discoverModels(pluginId, userId) {
138
- const plugin = this.getPlugin(pluginId);
474
+ const plugin = this.getPlugin(pluginId, userId);
139
475
  if (!plugin)
140
476
  return [];
141
477
  const pluginVars = this.getPluginVariables(plugin, userId);
142
- const endpointOverride = pluginVars.endpoint;
143
- const effectiveEndpoint = (endpointOverride && this.validateEndpointUrl(endpointOverride)) ||
144
- plugin.endpoint;
478
+ const { endpoint: effectiveEndpoint } = resolvePluginApiConfig(plugin, pluginVars);
479
+ const modelsEndpoint = resolvePluginModelsEndpoint(effectiveEndpoint, typeof pluginVars.models_endpoint === 'string'
480
+ ? pluginVars.models_endpoint
481
+ : undefined);
482
+ assertSafePluginEndpoint(modelsEndpoint, 'model discovery endpoint');
145
483
  const apiKey = this.getApiKey(plugin, userId);
484
+ if (pluginRequiresApiKey(plugin) && !apiKey) {
485
+ return plugin.model_map;
486
+ }
146
487
  const headers = buildPluginModelDiscoveryHeaders(plugin, apiKey);
147
488
  try {
148
- const response = await axios.get(resolvePluginModelsEndpoint(effectiveEndpoint), {
489
+ const response = await axios.get(modelsEndpoint, {
149
490
  headers,
150
491
  timeout: 5000,
492
+ maxRedirects: 0,
151
493
  });
152
494
  if (response.data?.data && Array.isArray(response.data.data)) {
153
495
  const models = response.data.data
@@ -155,21 +497,7 @@ class PluginService {
155
497
  .filter((id) => typeof id === 'string');
156
498
  if (models.length > 0) {
157
499
  logger.debug('[Plugin] Auto-discovered %d models for %s:', models.length, pluginId, models);
158
- // Update the plugin file with discovered models
159
- plugin.model_map = models;
160
- const safeId = pluginId.replace(/[^a-zA-Z0-9_.-]/g, '');
161
- if (safeId !== pluginId)
162
- throw new Error('Invalid plugin ID');
163
- const filePath = path.resolve(this.pluginsDir, `${safeId}.json`);
164
- if (!filePath.startsWith(path.resolve(this.pluginsDir))) {
165
- throw new Error('Path traversal detected');
166
- }
167
- const pluginData = fs.existsSync(filePath)
168
- ? JSON.parse(fs.readFileSync(filePath, 'utf8'))
169
- : plugin;
170
- pluginData.model_map = models;
171
- pluginData.updated_at = Date.now();
172
- fs.writeFileSync(filePath, JSON.stringify(pluginData, null, 2));
500
+ this.storeDiscoveredModels(pluginId, models, userId);
173
501
  return models;
174
502
  }
175
503
  }
@@ -180,8 +508,9 @@ class PluginService {
180
508
  return plugin.model_map;
181
509
  }
182
510
  // List all installed plugins
183
- getAllPlugins() {
511
+ getAllPlugins(userId) {
184
512
  const plugins = new Map();
513
+ const activePluginIds = pluginActivationService.getActivePluginIds(userId);
185
514
  for (const pluginsDir of this.pluginReadDirs) {
186
515
  if (!fs.existsSync(pluginsDir)) {
187
516
  continue;
@@ -190,18 +519,36 @@ class PluginService {
190
519
  const files = fs.readdirSync(pluginsDir);
191
520
  for (const file of files) {
192
521
  if (file.endsWith('.json') && !file.startsWith('.')) {
522
+ const filenameId = path.basename(file, '.json');
193
523
  try {
194
524
  const filePath = path.join(pluginsDir, file);
195
525
  const content = fs.readFileSync(filePath, 'utf8');
196
526
  const parsedPlugin = JSON.parse(content);
197
527
  // Validate plugin structure
198
- if (this.validatePlugin(parsedPlugin)) {
528
+ if (this.validatePlugin(parsedPlugin) &&
529
+ parsedPlugin.id === filenameId) {
530
+ if (!this.isPluginDefinitionApproved(parsedPlugin, filePath)) {
531
+ // A later writable definition shadows an earlier bundled
532
+ // definition even while quarantined.
533
+ plugins.delete(parsedPlugin.id);
534
+ continue;
535
+ }
199
536
  const plugin = applyPluginDefinitionPolicy(parsedPlugin);
200
- plugin.active = this.activePluginIds.has(plugin.id);
201
- plugins.set(plugin.id, plugin);
537
+ plugin.active = activePluginIds.has(plugin.id);
538
+ plugins.set(plugin.id, this.applyDiscoveredModels(plugin, userId));
539
+ }
540
+ else if (parsedPlugin.id !== filenameId) {
541
+ plugins.delete(filenameId);
542
+ logger.warn('Ignoring plugin %s because its declared ID does not match its filename', file);
543
+ }
544
+ else {
545
+ plugins.delete(filenameId);
202
546
  }
203
547
  }
204
548
  catch (error) {
549
+ // Invalid JSON in an effective same-ID writable file must fail
550
+ // closed instead of revealing an earlier bundled definition.
551
+ plugins.delete(filenameId);
205
552
  logger.error(`Failed to load plugin ${file}:`, error);
206
553
  }
207
554
  }
@@ -214,39 +561,40 @@ class PluginService {
214
561
  return Array.from(plugins.values());
215
562
  }
216
563
  // Get a specific plugin by ID
217
- getPlugin(id) {
564
+ getPlugin(id, userId) {
218
565
  // Sanitize the ID to prevent path traversal
219
566
  const sanitizedId = sanitize(id);
220
567
  if (!sanitizedId || sanitizedId !== id) {
221
568
  logger.error('Invalid plugin ID provided:', id);
222
569
  return null;
223
570
  }
224
- for (const pluginsDir of [...this.pluginReadDirs].reverse()) {
225
- const filePath = path.resolve(pluginsDir, `${sanitizedId}.json`);
226
- if (!filePath.startsWith(path.resolve(pluginsDir)) ||
227
- !fs.existsSync(filePath)) {
228
- continue;
229
- }
230
- try {
231
- const content = fs.readFileSync(filePath, 'utf8');
232
- const parsedPlugin = JSON.parse(content);
233
- if (this.validatePlugin(parsedPlugin)) {
234
- const plugin = applyPluginDefinitionPolicy(parsedPlugin);
235
- plugin.active = this.activePluginIds.has(plugin.id);
236
- return plugin;
237
- }
238
- }
239
- catch (error) {
240
- logger.error('Failed to load plugin %s:', sanitizedId, error);
571
+ const filePath = this.resolveEffectivePluginFilePath(sanitizedId);
572
+ if (!filePath)
573
+ return null;
574
+ try {
575
+ const content = fs.readFileSync(filePath, 'utf8');
576
+ const parsedPlugin = JSON.parse(content);
577
+ if (this.validatePlugin(parsedPlugin) &&
578
+ parsedPlugin.id === sanitizedId &&
579
+ this.isPluginDefinitionApproved(parsedPlugin, filePath)) {
580
+ const plugin = applyPluginDefinitionPolicy(parsedPlugin);
581
+ plugin.active = this.isPluginActive(plugin.id, userId);
582
+ return this.applyDiscoveredModels(plugin, userId);
241
583
  }
242
584
  }
585
+ catch (error) {
586
+ logger.error('Failed to load plugin %s:', sanitizedId, error);
587
+ }
243
588
  return null;
244
589
  }
245
590
  // Install or update a plugin
246
- installPlugin(pluginData) {
591
+ installPlugin(pluginData, approvedByUserId) {
247
592
  if (!this.validatePlugin(pluginData)) {
248
593
  throw new Error('Invalid plugin structure');
249
594
  }
595
+ if (!this.canUseStoredConnectionOverrides(approvedByUserId)) {
596
+ throw new Error('Administrator approval is required');
597
+ }
250
598
  const now = Date.now();
251
599
  const plugin = {
252
600
  ...pluginData,
@@ -261,7 +609,25 @@ class PluginService {
261
609
  if (!filePath.startsWith(path.resolve(this.pluginsDir))) {
262
610
  throw new Error('Path traversal detected');
263
611
  }
264
- fs.writeFileSync(filePath, JSON.stringify(plugin, null, 2));
612
+ // Revoke definition approval and every user's activation before replacing
613
+ // bytes on disk. Any crash or write failure therefore leaves the provider
614
+ // quarantined and inactive.
615
+ this.revokePluginDefinitionConsent(plugin.id);
616
+ this.clearDiscoveredModels(plugin.id);
617
+ const temporaryPath = path.resolve(this.pluginsDir, `.${safeId}.${process.pid}.${Date.now()}.tmp`);
618
+ try {
619
+ fs.writeFileSync(temporaryPath, JSON.stringify(plugin, null, 2), {
620
+ flag: 'wx',
621
+ });
622
+ fs.renameSync(temporaryPath, filePath);
623
+ }
624
+ catch (error) {
625
+ if (fs.existsSync(temporaryPath)) {
626
+ fs.unlinkSync(temporaryPath);
627
+ }
628
+ throw error;
629
+ }
630
+ this.approvePluginDefinition(plugin, filePath, approvedByUserId);
265
631
  return plugin;
266
632
  }
267
633
  // Delete a plugin
@@ -278,7 +644,8 @@ class PluginService {
278
644
  logger.error('Plugin ID failed sanitization:', id);
279
645
  return false;
280
646
  }
281
- const writablePluginDirs = Array.from(new Set([this.pluginsDir, this.legacyPluginsDir]));
647
+ const bundledDirectory = path.resolve(this.bundledPluginsDir);
648
+ const writablePluginDirs = Array.from(new Set([this.pluginsDir, this.legacyPluginsDir])).filter(pluginsDir => path.resolve(pluginsDir) !== bundledDirectory);
282
649
  const filePath = writablePluginDirs
283
650
  .map(pluginsDir => path.resolve(pluginsDir, `${sanitizedId}.json`))
284
651
  .find(candidate => fs.existsSync(candidate));
@@ -288,13 +655,12 @@ class PluginService {
288
655
  }
289
656
  try {
290
657
  fs.unlinkSync(filePath);
291
- // If this was an active plugin, deactivate it
292
- if (this.activePluginIds.has(id)) {
293
- this.activePluginIds.delete(id);
294
- this.saveActivePlugins();
295
- }
658
+ pluginActivationService.deletePlugin(id);
659
+ this.removePluginDefinitionApproval(id);
296
660
  // Clean up stored variables
297
661
  pluginVariablesService.deletePluginVariables(id);
662
+ pluginCredentialsService.deleteAllPluginCredentials(id);
663
+ this.clearDiscoveredModels(id);
298
664
  return true;
299
665
  }
300
666
  catch (error) {
@@ -303,39 +669,54 @@ class PluginService {
303
669
  }
304
670
  }
305
671
  // Activate a plugin
306
- activatePlugin(id) {
307
- const plugin = this.getPlugin(id);
672
+ async activatePlugin(id, userId) {
673
+ const plugin = this.getPlugin(id, userId);
308
674
  if (!plugin) {
309
675
  throw new Error('Plugin not found');
310
676
  }
311
- this.activePluginIds.add(id);
312
- this.saveActivePlugins();
313
- // Trigger model discovery in background (non-blocking)
314
- this.discoverModels(id).catch(() => { });
677
+ if (!pluginActivationService.activate(id, userId)) {
678
+ throw new Error('Failed to persist plugin activation');
679
+ }
680
+ // Wait for discovery so the activation response and the UI's first reload
681
+ // observe the same user-scoped model catalog.
682
+ await this.discoverModels(id, userId).catch(() => { });
315
683
  return true;
316
684
  }
317
685
  // Deactivate a specific plugin
318
- deactivatePlugin(id) {
319
- if (id) {
320
- this.activePluginIds.delete(id);
321
- }
322
- else {
323
- // Legacy: deactivate all plugins
324
- this.activePluginIds.clear();
325
- }
326
- this.saveActivePlugins();
327
- return true;
686
+ deactivatePlugin(id, userId) {
687
+ // The legacy no-ID route now deactivates all plugins only for this user.
688
+ return pluginActivationService.deactivate(id, userId);
328
689
  }
329
690
  // Get the active plugin for a specific model
330
- getActivePluginForModel(model, userId) {
691
+ getActivePluginForModel(model, userId, pluginId) {
692
+ if (pluginId) {
693
+ const plugin = this.getPlugin(pluginId, userId);
694
+ if (!plugin) {
695
+ throw new Error(`Plugin not found: ${pluginId}`);
696
+ }
697
+ if (!this.isPluginActive(pluginId, userId)) {
698
+ throw new Error(`Plugin is not active: ${pluginId}`);
699
+ }
700
+ if (!plugin.model_map.includes(model)) {
701
+ throw new Error(`Model ${model} is not supported by plugin ${pluginId}`);
702
+ }
703
+ this.resolveOperationEndpoint(plugin, userId);
704
+ const apiKey = this.getApiKey(plugin, userId);
705
+ if (pluginRequiresApiKey(plugin) && !apiKey) {
706
+ throw new Error(`API key not found for plugin ${pluginId} (save a provider credential in Settings)`);
707
+ }
708
+ return plugin;
709
+ }
331
710
  // Only route through plugins the user explicitly activated.
332
- const activePlugins = this.getActivePlugins();
711
+ const activePlugins = this.getActivePlugins(userId);
333
712
  // Find the active plugin that supports this model
334
713
  for (const plugin of activePlugins) {
335
714
  if (plugin.model_map.includes(model)) {
336
- // Check if we have the required API key (from DB or env)
715
+ // Local OpenAI-compatible servers can explicitly opt out of auth by
716
+ // leaving both auth fields empty.
717
+ this.resolveOperationEndpoint(plugin, userId);
337
718
  const apiKey = this.getApiKey(plugin, userId);
338
- if (!apiKey) {
719
+ if (pluginRequiresApiKey(plugin) && !apiKey) {
339
720
  continue;
340
721
  }
341
722
  return plugin;
@@ -344,52 +725,57 @@ class PluginService {
344
725
  return null;
345
726
  }
346
727
  // Get all currently active plugins
347
- getActivePlugins() {
348
- const allPlugins = this.getAllPlugins();
349
- const activePlugins = allPlugins.filter(plugin => this.activePluginIds.has(plugin.id));
728
+ getActivePlugins(userId) {
729
+ const allPlugins = this.getAllPlugins(userId);
730
+ const activePlugins = allPlugins.filter(plugin => plugin.active);
350
731
  return activePlugins;
351
732
  }
352
733
  // Legacy method for backward compatibility - returns first active plugin
353
- getActivePlugin() {
354
- const activePlugins = this.getActivePlugins();
734
+ getActivePlugin(userId) {
735
+ const activePlugins = this.getActivePlugins(userId);
355
736
  return activePlugins.length > 0 ? activePlugins[0] : null;
356
737
  }
357
738
  // Get plugin status
358
- getPluginStatus() {
359
- const plugins = this.getAllPlugins();
739
+ getPluginStatus(userId) {
740
+ const plugins = this.getAllPlugins(userId);
360
741
  return plugins.map(plugin => ({
361
742
  id: plugin.id,
362
743
  active: plugin.active || false,
363
- available: true, // Could be enhanced to check endpoint availability
744
+ available: !pluginRequiresApiKey(plugin) ||
745
+ this.getApiKey(plugin, userId) !== null,
364
746
  }));
365
747
  }
366
748
  // Execute a chat request through the active plugin
367
- async executePluginRequest(model, messages, options = {}, userId) {
749
+ async executePluginRequest(model, messages, options = {}, userId, pluginId) {
368
750
  validatePluginModel(model);
369
- const activePlugin = this.getActivePluginForModel(model, userId);
751
+ const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
370
752
  if (!activePlugin) {
371
753
  throw new Error(`No active plugin found for model: ${model}`);
372
754
  }
373
755
  if (!activePlugin.model_map.includes(model)) {
374
756
  throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
375
757
  }
758
+ const pluginVars = this.getPluginVariables(activePlugin, userId);
759
+ const { apiMode, endpoint: effectiveEndpoint } = resolvePluginApiConfig(activePlugin, pluginVars);
760
+ const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
761
+ assertSafePluginEndpoint(processedEndpoint, 'endpoint URL constructed');
376
762
  const apiKey = this.getApiKey(activePlugin, userId);
377
- if (!apiKey) {
378
- throw new Error(`API key not found for plugin ${activePlugin.id} (set via Settings or ${activePlugin.auth.key_env} env var)`);
763
+ if (pluginRequiresApiKey(activePlugin) && !apiKey) {
764
+ throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
379
765
  }
380
- const pluginVars = this.getPluginVariables(activePlugin, userId);
381
- const effectiveEndpoint = resolvePluginEndpoint(activePlugin.endpoint, pluginVars.endpoint);
766
+ const providerStateScope = apiMode === 'responses'
767
+ ? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
768
+ : undefined;
382
769
  const headers = buildPluginAuthHeaders(activePlugin, apiKey);
383
- const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars);
770
+ const { payload, headers: payloadHeaders } = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, false, apiMode, providerStateScope);
384
771
  Object.assign(headers, payloadHeaders);
385
- const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
386
- assertSafePluginEndpoint(processedEndpoint, 'endpoint URL constructed');
387
772
  try {
388
773
  const response = await axios.post(processedEndpoint, payload, {
389
774
  headers,
390
775
  timeout: 60000, // 60 second timeout
776
+ maxRedirects: 0,
391
777
  });
392
- return convertProviderResponse(activePlugin, response.data, model);
778
+ return convertProviderResponse(activePlugin, response.data, model, apiMode, providerStateScope);
393
779
  }
394
780
  catch (error) {
395
781
  logger.error(`Plugin request failed for ${activePlugin.id}:`, error);
@@ -411,27 +797,32 @@ class PluginService {
411
797
  * Returns an async generator that yields SSE delta content strings.
412
798
  * Also collects tool_calls from the stream.
413
799
  */
414
- async *executePluginStreamRequest(model, messages, options = {}, userId) {
800
+ async *executePluginStreamRequest(model, messages, options = {}, userId, pluginId) {
415
801
  validatePluginModel(model);
416
- const activePlugin = this.getActivePluginForModel(model, userId);
802
+ const activePlugin = this.getActivePluginForModel(model, userId, pluginId);
417
803
  if (!activePlugin) {
418
804
  throw new Error(`No active plugin found for model: ${model}`);
419
805
  }
420
806
  if (!activePlugin.model_map.includes(model)) {
421
807
  throw new Error(`Model ${model} is not supported by plugin ${activePlugin.id}`);
422
808
  }
809
+ const pluginVars = this.getPluginVariables(activePlugin, userId);
810
+ const { apiMode, endpoint: effectiveEndpoint } = resolvePluginApiConfig(activePlugin, pluginVars);
811
+ const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
812
+ assertSafePluginEndpoint(processedEndpoint);
423
813
  const apiKey = this.getApiKey(activePlugin, userId);
424
- if (!apiKey) {
425
- throw new Error(`API key not found for plugin ${activePlugin.id} (set via Settings or ${activePlugin.auth.key_env} env var)`);
814
+ if (pluginRequiresApiKey(activePlugin) && !apiKey) {
815
+ throw new Error(`API key not found for plugin ${activePlugin.id} (save a provider credential in Settings)`);
426
816
  }
427
- const pluginVars = this.getPluginVariables(activePlugin, userId);
428
- const effectiveEndpoint = resolvePluginEndpoint(activePlugin.endpoint, pluginVars.endpoint);
817
+ const providerStateScope = apiMode === 'responses'
818
+ ? createOpenAIResponsesStateScope(activePlugin.id, model, processedEndpoint, createPluginCredentialFingerprint(apiKey))
819
+ : undefined;
429
820
  const headers = buildPluginAuthHeaders(activePlugin, apiKey);
430
821
  let payload;
431
- if (activePlugin.id === 'anthropic') {
432
- const anthropicRequest = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, true);
433
- payload = anthropicRequest.payload;
434
- Object.assign(headers, anthropicRequest.headers);
822
+ if (activePlugin.id === 'anthropic' || apiMode === 'responses') {
823
+ const pluginRequest = buildPluginChatPayload(activePlugin, model, messages, options, pluginVars, true, apiMode, providerStateScope);
824
+ payload = pluginRequest.payload;
825
+ Object.assign(headers, pluginRequest.headers);
435
826
  }
436
827
  else {
437
828
  const params = resolvePluginChatParameters(options, pluginVars);
@@ -444,54 +835,152 @@ class PluginService {
444
835
  stream: true,
445
836
  };
446
837
  }
447
- const processedEndpoint = applyModelEndpointTemplate(effectiveEndpoint, model);
448
- assertSafePluginEndpoint(processedEndpoint);
449
838
  const response = await fetch(processedEndpoint, {
450
839
  method: 'POST',
451
840
  headers,
452
841
  body: JSON.stringify(payload),
842
+ redirect: 'error',
453
843
  });
454
844
  if (activePlugin.id === 'anthropic') {
455
845
  yield* streamAnthropicResponse(response);
456
846
  }
847
+ else if (apiMode === 'responses') {
848
+ const contentType = response.headers.get('content-type') || '';
849
+ if (!contentType.includes('text/event-stream')) {
850
+ if (!response.ok) {
851
+ const errorText = await response.text();
852
+ throw new Error(`Plugin API error: ${response.status} - ${errorText.slice(0, 200)}`);
853
+ }
854
+ const normalized = convertProviderResponse(activePlugin, (await response.json()), model, apiMode, providerStateScope);
855
+ const choice = normalized.choices[0];
856
+ const message = choice?.message;
857
+ if (typeof message?.content === 'string' && message.content) {
858
+ yield { type: 'content', content: message.content };
859
+ }
860
+ if (typeof message?.reasoning_content === 'string' &&
861
+ message.reasoning_content) {
862
+ yield {
863
+ type: 'reasoning',
864
+ content: message.reasoning_content,
865
+ };
866
+ }
867
+ for (const call of message?.tool_calls || []) {
868
+ yield {
869
+ type: 'tool_call',
870
+ toolCall: {
871
+ id: call.id,
872
+ name: call.function.name,
873
+ arguments: call.function.arguments,
874
+ ...(call.providerMetadata
875
+ ? { providerMetadata: call.providerMetadata }
876
+ : {}),
877
+ },
878
+ };
879
+ }
880
+ if (normalized.usage) {
881
+ yield {
882
+ type: 'usage',
883
+ usage: {
884
+ promptTokens: normalized.usage.prompt_tokens,
885
+ completionTokens: normalized.usage.completion_tokens,
886
+ totalTokens: normalized.usage.total_tokens,
887
+ },
888
+ };
889
+ }
890
+ const incompleteReason = typeof normalized.providerMetadata?.[OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY] === 'string'
891
+ ? normalized.providerMetadata[OPENAI_RESPONSES_INCOMPLETE_REASON_METADATA_KEY]
892
+ : undefined;
893
+ yield {
894
+ type: 'done',
895
+ ...(incompleteReason
896
+ ? { doneReason: `incomplete:${incompleteReason}` }
897
+ : {}),
898
+ ...(normalized.providerMetadata
899
+ ? { providerMetadata: normalized.providerMetadata }
900
+ : {}),
901
+ };
902
+ }
903
+ else {
904
+ yield* streamOpenAIResponsesResponse(response, providerStateScope);
905
+ }
906
+ }
457
907
  else {
458
908
  yield* streamOpenAICompatibleResponse(response);
459
909
  }
460
910
  }
461
911
  // Validate plugin structure
462
912
  validatePlugin(plugin) {
463
- return (typeof plugin === 'object' &&
464
- plugin !== null &&
465
- typeof plugin.id === 'string' &&
466
- typeof plugin.name === 'string' &&
467
- typeof plugin.type === 'string' &&
468
- typeof plugin.endpoint === 'string' &&
469
- typeof plugin.auth === 'object' &&
470
- plugin.auth !== null &&
471
- typeof plugin.auth.header === 'string' &&
472
- typeof plugin.auth.key_env === 'string' &&
473
- (plugin.auth
474
- .prefix === undefined ||
475
- typeof plugin.auth.prefix === 'string') &&
476
- Array.isArray(plugin.model_map) &&
477
- plugin.model_map.length > 0);
913
+ const pluginRecord = typeof plugin === 'object' && plugin !== null
914
+ ? plugin
915
+ : null;
916
+ const variables = pluginRecord?.variables;
917
+ const variableNames = variables === undefined
918
+ ? []
919
+ : Array.isArray(variables)
920
+ ? variables.map(variable => typeof variable === 'object' &&
921
+ variable !== null &&
922
+ typeof variable.name === 'string'
923
+ ? variable.name
924
+ : null)
925
+ : [null];
926
+ const hasUniqueVariableNames = !variableNames.includes(null) &&
927
+ new Set(variableNames).size === variableNames.length;
928
+ return (pluginRecord !== null &&
929
+ hasUniqueVariableNames &&
930
+ typeof pluginRecord.id === 'string' &&
931
+ typeof pluginRecord.name === 'string' &&
932
+ typeof pluginRecord.type === 'string' &&
933
+ typeof pluginRecord.endpoint === 'string' &&
934
+ (pluginRecord.api_mode === undefined ||
935
+ pluginRecord.api_mode === 'chat_completions' ||
936
+ pluginRecord.api_mode === 'responses') &&
937
+ (pluginRecord.base_url === undefined ||
938
+ typeof pluginRecord.base_url === 'string') &&
939
+ (pluginRecord.api_path === undefined ||
940
+ typeof pluginRecord.api_path === 'string') &&
941
+ typeof pluginRecord.auth === 'object' &&
942
+ pluginRecord.auth !== null &&
943
+ typeof pluginRecord.auth.header ===
944
+ 'string' &&
945
+ typeof pluginRecord.auth.key_env ===
946
+ 'string' &&
947
+ (pluginRecord.auth.prefix === undefined ||
948
+ typeof pluginRecord.auth.prefix ===
949
+ 'string') &&
950
+ Array.isArray(pluginRecord.model_map) &&
951
+ pluginRecord.model_map.length > 0);
478
952
  }
479
953
  // Export plugin to JSON
480
- exportPlugin(id) {
481
- return this.getPlugin(id);
954
+ exportPlugin(id, userId) {
955
+ return this.getPlugin(id, userId);
482
956
  }
483
957
  // Import plugin from JSON data
484
- importPlugin(pluginData) {
958
+ importPlugin(pluginData, approvedByUserId) {
485
959
  // Validate and clean the plugin data
486
960
  if (!this.validatePlugin(pluginData)) {
487
961
  throw new Error('Invalid plugin data');
488
962
  }
489
963
  // Check if plugin already exists
490
- const existingPlugin = this.getPlugin(pluginData.id);
491
- if (existingPlugin) {
492
- throw new Error(`Plugin with ID ${pluginData.id} already exists`);
964
+ const existingPluginPath = this.resolveEffectivePluginFilePath(pluginData.id);
965
+ if (existingPluginPath) {
966
+ try {
967
+ const existingPlugin = JSON.parse(fs.readFileSync(existingPluginPath, 'utf8'));
968
+ if (this.validatePlugin(existingPlugin) &&
969
+ existingPlugin.id === pluginData.id &&
970
+ this.isPluginDefinitionApproved(existingPlugin, existingPluginPath)) {
971
+ throw new Error(`Plugin with ID ${pluginData.id} already exists`);
972
+ }
973
+ }
974
+ catch (error) {
975
+ if (error instanceof Error &&
976
+ error.message.includes('already exists')) {
977
+ throw error;
978
+ }
979
+ // Invalid and unapproved legacy definitions may be replaced by this
980
+ // authenticated administrator import.
981
+ }
493
982
  }
494
- return this.installPlugin(pluginData);
983
+ return this.installPlugin(pluginData, approvedByUserId);
495
984
  }
496
985
  // ============================================
497
986
  // Capability Methods
@@ -514,20 +1003,20 @@ class PluginService {
514
1003
  async executeTTSRequest(model, input, options = {}) {
515
1004
  return this.ttsService.executeTTSRequest(model, input, options);
516
1005
  }
517
- getTTSConfig(pluginId) {
518
- return this.ttsService.getTTSConfig(pluginId);
1006
+ getTTSConfig(pluginId, userId) {
1007
+ return this.ttsService.getTTSConfig(pluginId, userId);
519
1008
  }
520
- getPluginForImageGen(model) {
521
- return this.imageGenerationService.getPluginForImageGen(model);
1009
+ getPluginForImageGen(model, pluginId, userId) {
1010
+ return this.imageGenerationService.getPluginForImageGen(model, pluginId, userId);
522
1011
  }
523
- getAvailableImageGenModels() {
524
- return this.imageGenerationService.getAvailableImageGenModels();
1012
+ getAvailableImageGenModels(userId) {
1013
+ return this.imageGenerationService.getAvailableImageGenModels(userId);
525
1014
  }
526
- async executeImageGenRequest(model, prompt, options = {}) {
1015
+ async executeImageGenRequest(model, prompt, options) {
527
1016
  return this.imageGenerationService.executeImageGenRequest(model, prompt, options);
528
1017
  }
529
- getImageGenConfig(pluginId) {
530
- return this.imageGenerationService.getImageGenConfig(pluginId);
1018
+ getImageGenConfig(pluginId, userId) {
1019
+ return this.imageGenerationService.getImageGenConfig(pluginId, userId);
531
1020
  }
532
1021
  getPluginsByCapability(capabilityType, userId) {
533
1022
  return this.capabilityRegistryService.getPluginsByCapability(capabilityType, userId);