libre-webui 0.2.8 → 0.3.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 (51) hide show
  1. package/README.md +25 -16
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +31 -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 +13 -0
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/routes/imageGen.d.ts +3 -0
  9. package/backend/dist/routes/imageGen.d.ts.map +1 -0
  10. package/backend/dist/routes/imageGen.js +313 -0
  11. package/backend/dist/routes/imageGen.js.map +1 -0
  12. package/backend/dist/routes/plugins.d.ts.map +1 -1
  13. package/backend/dist/routes/plugins.js +122 -0
  14. package/backend/dist/routes/plugins.js.map +1 -1
  15. package/backend/dist/services/galleryService.d.ts +41 -0
  16. package/backend/dist/services/galleryService.d.ts.map +1 -0
  17. package/backend/dist/services/galleryService.js +175 -0
  18. package/backend/dist/services/galleryService.js.map +1 -0
  19. package/backend/dist/services/pluginCredentialsService.d.ts +46 -0
  20. package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -0
  21. package/backend/dist/services/pluginCredentialsService.js +175 -0
  22. package/backend/dist/services/pluginCredentialsService.js.map +1 -0
  23. package/backend/dist/services/pluginService.d.ts +23 -1
  24. package/backend/dist/services/pluginService.d.ts.map +1 -1
  25. package/backend/dist/services/pluginService.js +440 -24
  26. package/backend/dist/services/pluginService.js.map +1 -1
  27. package/backend/dist/types/index.d.ts +39 -0
  28. package/backend/dist/types/index.d.ts.map +1 -1
  29. package/backend/dist/types/index.js.map +1 -1
  30. package/frontend/dist/assets/index-BAlYrgVl.js +26 -0
  31. package/frontend/dist/css/index-CYRSPYSA.css +1 -0
  32. package/frontend/dist/index.html +4 -4
  33. package/frontend/dist/js/ArtifactContainer-DOyRNr2K.js +24 -0
  34. package/frontend/dist/js/{ArtifactDemoPage-Cg6PD9Cv.js → ArtifactDemoPage-DuHnKjTy.js} +1 -1
  35. package/frontend/dist/js/ChatPage-D4AGvUr9.js +281 -0
  36. package/frontend/dist/js/GalleryPage-DPxs-JHF.js +1 -0
  37. package/frontend/dist/js/ModelsPage-CDx8DvZ8.js +2 -0
  38. package/frontend/dist/js/PersonasPage-CHDGv_Lh.js +13 -0
  39. package/frontend/dist/js/UserManagementPage-COb0e5_w.js +1 -0
  40. package/frontend/dist/js/{markdown-vendor-D-79K2xZ.js → markdown-vendor-DRtqGHm3.js} +1 -1
  41. package/frontend/dist/js/ui-vendor-DA07XX1l.js +177 -0
  42. package/package.json +1 -1
  43. package/plugins/comfyui.json +49 -0
  44. package/frontend/dist/assets/index-CE4fvnod.js +0 -3
  45. package/frontend/dist/css/index-B1OjddR-.css +0 -1
  46. package/frontend/dist/js/ArtifactContainer-BoHnMmlC.js +0 -23
  47. package/frontend/dist/js/ChatPage-Bwc5n8Z3.js +0 -281
  48. package/frontend/dist/js/ModelsPage-2HtOH51G.js +0 -2
  49. package/frontend/dist/js/PersonasPage-BESJgtJW.js +0 -13
  50. package/frontend/dist/js/UserManagementPage-DQaE_2Mt.js +0 -1
  51. package/frontend/dist/js/ui-vendor-VxSCY_bv.js +0 -177
@@ -18,6 +18,7 @@ import fs from 'fs';
18
18
  import path from 'path';
19
19
  import sanitize from 'sanitize-filename';
20
20
  import axios from 'axios';
21
+ import pluginCredentialsService from './pluginCredentialsService.js';
21
22
  class PluginService {
22
23
  constructor() {
23
24
  this.activePluginIds = new Set();
@@ -56,6 +57,15 @@ class PluginService {
56
57
  };
57
58
  fs.writeFileSync(statusFile, JSON.stringify(status, null, 2));
58
59
  }
60
+ /**
61
+ * Get API key for a plugin from database (per-user) or environment variable (fallback)
62
+ * @param plugin The plugin to get the API key for
63
+ * @param userId Optional user ID for per-user credentials
64
+ * @returns The API key or null if not found
65
+ */
66
+ getApiKey(plugin, userId) {
67
+ return pluginCredentialsService.getApiKey(plugin.id, plugin.auth.key_env, userId);
68
+ }
59
69
  // List all installed plugins
60
70
  getAllPlugins() {
61
71
  const plugins = [];
@@ -195,10 +205,10 @@ class PluginService {
195
205
  console.log(`[DEBUG] Checking plugin ${plugin.id} with model_map:`, plugin.model_map);
196
206
  if (plugin.model_map.includes(model)) {
197
207
  console.log(`[DEBUG] Found plugin ${plugin.id} for model ${model}`);
198
- // Check if we have the required API key
199
- const apiKey = process.env[plugin.auth.key_env];
208
+ // Check if we have the required API key (from DB or env)
209
+ const apiKey = this.getApiKey(plugin);
200
210
  if (!apiKey) {
201
- console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
211
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
202
212
  continue;
203
213
  }
204
214
  return plugin;
@@ -256,10 +266,10 @@ class PluginService {
256
266
  if (!activePlugin) {
257
267
  throw new Error(`No active plugin found for model: ${model}`);
258
268
  }
259
- // Get API key from environment
260
- const apiKey = process.env[activePlugin.auth.key_env];
269
+ // Get API key from database (per-user) or environment variable (fallback)
270
+ const apiKey = this.getApiKey(activePlugin);
261
271
  if (!apiKey) {
262
- throw new Error(`API key not found in environment variable: ${activePlugin.auth.key_env}`);
272
+ throw new Error(`API key not found for plugin ${activePlugin.id} (set via Settings or ${activePlugin.auth.key_env} env var)`);
263
273
  }
264
274
  // Prepare headers
265
275
  const headers = {
@@ -421,11 +431,12 @@ class PluginService {
421
431
  // Validate the final endpoint URL
422
432
  try {
423
433
  const url = new URL(processedEndpoint);
424
- // Allow HTTP only for localhost/127.0.0.1 (safe for local development)
434
+ // Allow HTTP for localhost and private network IPs (safe for local/LAN development)
425
435
  const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
426
- if (url.protocol !== 'https:' && !isLocalhost) {
436
+ const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(url.hostname);
437
+ if (url.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
427
438
  throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints. ` +
428
- `(HTTP is permitted only for localhost during local development)`);
439
+ `(HTTP is permitted for localhost and private network IPs)`);
429
440
  }
430
441
  }
431
442
  catch (_error) {
@@ -641,10 +652,10 @@ class PluginService {
641
652
  const ttsCapability = plugin.capabilities.tts;
642
653
  if (ttsCapability.model_map.includes(model)) {
643
654
  console.log(`[DEBUG] Found TTS plugin ${plugin.id} for model ${model}`);
644
- // Check if we have the required API key
645
- const apiKey = process.env[plugin.auth.key_env];
655
+ // Check if we have the required API key (from DB or env)
656
+ const apiKey = this.getApiKey(plugin);
646
657
  if (!apiKey) {
647
- console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
658
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
648
659
  continue;
649
660
  }
650
661
  return plugin;
@@ -653,9 +664,9 @@ class PluginService {
653
664
  // Also check primary type for backward compatibility with TTS-only plugins
654
665
  if (plugin.type === 'tts' && plugin.model_map.includes(model)) {
655
666
  console.log(`[DEBUG] Found TTS-type plugin ${plugin.id} for model ${model}`);
656
- const apiKey = process.env[plugin.auth.key_env];
667
+ const apiKey = this.getApiKey(plugin);
657
668
  if (!apiKey) {
658
- console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env} not set`);
669
+ console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
659
670
  continue;
660
671
  }
661
672
  return plugin;
@@ -672,8 +683,8 @@ class PluginService {
672
683
  // Check capabilities-based TTS
673
684
  if (plugin.capabilities?.tts) {
674
685
  const ttsCapability = plugin.capabilities.tts;
675
- // Check if API key is available
676
- const apiKey = process.env[plugin.auth.key_env];
686
+ // Check if API key is available (from DB or env)
687
+ const apiKey = this.getApiKey(plugin);
677
688
  if (apiKey) {
678
689
  for (const model of ttsCapability.model_map) {
679
690
  models.push({
@@ -686,7 +697,7 @@ class PluginService {
686
697
  }
687
698
  // Check primary type for TTS-only plugins
688
699
  if (plugin.type === 'tts') {
689
- const apiKey = process.env[plugin.auth.key_env];
700
+ const apiKey = this.getApiKey(plugin);
690
701
  if (apiKey) {
691
702
  for (const model of plugin.model_map) {
692
703
  models.push({
@@ -718,10 +729,10 @@ class PluginService {
718
729
  if (!plugin) {
719
730
  throw new Error(`No TTS plugin found for model: ${model}`);
720
731
  }
721
- // Get API key from environment
722
- const apiKey = process.env[plugin.auth.key_env];
732
+ // Get API key from database (per-user) or environment variable (fallback)
733
+ const apiKey = this.getApiKey(plugin);
723
734
  if (!apiKey) {
724
- throw new Error(`API key not found in environment variable: ${plugin.auth.key_env}`);
735
+ throw new Error(`API key not found for plugin ${plugin.id} (set via Settings or ${plugin.auth.key_env} env var)`);
725
736
  }
726
737
  // Determine endpoint
727
738
  let endpoint;
@@ -823,8 +834,10 @@ class PluginService {
823
834
  try {
824
835
  const url = new URL(processedEndpoint);
825
836
  const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
826
- if (url.protocol !== 'https:' && !isLocalhost) {
827
- throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints.`);
837
+ const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(url.hostname);
838
+ if (url.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
839
+ throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints. ` +
840
+ `(HTTP is permitted for localhost and private network IPs)`);
828
841
  }
829
842
  }
830
843
  catch (_error) {
@@ -912,6 +925,409 @@ class PluginService {
912
925
  }
913
926
  return null;
914
927
  }
928
+ // ==================== Image Generation Methods ====================
929
+ // Get plugin that supports a specific image generation model
930
+ getPluginForImageGen(model) {
931
+ const allPlugins = this.getAllPlugins();
932
+ for (const plugin of allPlugins) {
933
+ // Check capabilities-based image generation
934
+ if (plugin.capabilities?.image) {
935
+ const imageCapability = plugin.capabilities.image;
936
+ if (imageCapability.model_map.includes(model)) {
937
+ return plugin;
938
+ }
939
+ }
940
+ // Check primary type for image-only plugins
941
+ if (plugin.type === 'image' && plugin.model_map.includes(model)) {
942
+ return plugin;
943
+ }
944
+ }
945
+ console.log(`[DEBUG] No image generation plugin found for model: ${model}`);
946
+ return null;
947
+ }
948
+ // Get all available image generation models from all plugins
949
+ getAvailableImageGenModels() {
950
+ const models = [];
951
+ const allPlugins = this.getAllPlugins();
952
+ for (const plugin of allPlugins) {
953
+ // Check capabilities-based image generation
954
+ if (plugin.capabilities?.image) {
955
+ const imageCapability = plugin.capabilities.image;
956
+ // Check if API key is available (from DB or env) or if no auth is required
957
+ const noAuthRequired = imageCapability.config
958
+ ?.no_auth_required === true;
959
+ const apiKey = this.getApiKey(plugin);
960
+ if (apiKey || noAuthRequired) {
961
+ for (const model of imageCapability.model_map) {
962
+ models.push({
963
+ model,
964
+ plugin: plugin.id,
965
+ config: imageCapability.config,
966
+ });
967
+ }
968
+ }
969
+ }
970
+ // Check primary type for image-only plugins
971
+ if (plugin.type === 'image') {
972
+ const apiKey = this.getApiKey(plugin);
973
+ if (apiKey) {
974
+ for (const model of plugin.model_map) {
975
+ models.push({
976
+ model,
977
+ plugin: plugin.id,
978
+ });
979
+ }
980
+ }
981
+ }
982
+ }
983
+ return models;
984
+ }
985
+ // Execute an image generation request through the appropriate plugin
986
+ async executeImageGenRequest(model, prompt, options = {}) {
987
+ // Validate model parameter
988
+ if (!model || typeof model !== 'string') {
989
+ throw new Error('Invalid model parameter: must be a non-empty string');
990
+ }
991
+ // Sanitize model parameter
992
+ const modelPattern = /^[a-zA-Z0-9\-_:.]+$/;
993
+ if (!modelPattern.test(model)) {
994
+ throw new Error(`Invalid model parameter: ${model} contains invalid characters`);
995
+ }
996
+ // Validate prompt
997
+ if (!prompt || typeof prompt !== 'string') {
998
+ throw new Error('Invalid prompt: must be a non-empty string');
999
+ }
1000
+ const plugin = this.getPluginForImageGen(model);
1001
+ if (!plugin) {
1002
+ throw new Error(`No image generation plugin found for model: ${model}`);
1003
+ }
1004
+ // Determine endpoint and config first (needed for auth check)
1005
+ let endpoint;
1006
+ let imageConfig;
1007
+ if (plugin.capabilities?.image) {
1008
+ endpoint = plugin.capabilities.image.endpoint;
1009
+ imageConfig = plugin.capabilities.image.config;
1010
+ }
1011
+ else {
1012
+ endpoint = plugin.endpoint;
1013
+ }
1014
+ // Get API key from database (per-user) or environment variable (fallback)
1015
+ // Some plugins (like local ComfyUI) don't require auth
1016
+ const noAuthRequired = imageConfig?.no_auth_required ===
1017
+ true;
1018
+ const apiKey = this.getApiKey(plugin);
1019
+ if (!apiKey && !noAuthRequired) {
1020
+ throw new Error(`API key not found for plugin ${plugin.id} (set via Settings or ${plugin.auth.key_env} env var)`);
1021
+ }
1022
+ // Validate prompt length
1023
+ if (imageConfig?.max_prompt_length &&
1024
+ prompt.length > imageConfig.max_prompt_length) {
1025
+ throw new Error(`Prompt exceeds maximum length of ${imageConfig.max_prompt_length} characters`);
1026
+ }
1027
+ // Build headers
1028
+ const headers = {
1029
+ 'Content-Type': 'application/json',
1030
+ };
1031
+ // Only add auth header if API key is available
1032
+ if (apiKey) {
1033
+ if (plugin.auth.prefix) {
1034
+ headers[plugin.auth.header] = `${plugin.auth.prefix}${apiKey}`;
1035
+ }
1036
+ else {
1037
+ headers[plugin.auth.header] = apiKey;
1038
+ }
1039
+ }
1040
+ // Build payload (OpenAI-compatible format)
1041
+ const payload = {
1042
+ model,
1043
+ prompt,
1044
+ size: options.size || imageConfig?.default_size || '1024x1024',
1045
+ quality: options.quality || imageConfig?.default_quality || 'standard',
1046
+ n: options.n || 1,
1047
+ response_format: options.response_format || 'url',
1048
+ };
1049
+ // Add style if supported
1050
+ if (options.style || imageConfig?.default_style) {
1051
+ payload.style = options.style || imageConfig?.default_style;
1052
+ }
1053
+ // Validate the final endpoint URL
1054
+ let baseUrl;
1055
+ try {
1056
+ baseUrl = new URL(endpoint);
1057
+ const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(baseUrl.hostname);
1058
+ const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(baseUrl.hostname);
1059
+ if (baseUrl.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
1060
+ throw new Error(`Insecure endpoint protocol: ${baseUrl.protocol}. Only HTTPS is allowed for remote endpoints.`);
1061
+ }
1062
+ }
1063
+ catch (_error) {
1064
+ throw new Error(`Invalid endpoint URL: ${endpoint}`);
1065
+ }
1066
+ // Check if this is ComfyUI (special handling required)
1067
+ if (plugin.id === 'comfyui' || endpoint.includes('/prompt')) {
1068
+ return this.executeComfyUIRequest(baseUrl, prompt, { ...options, model });
1069
+ }
1070
+ try {
1071
+ const response = await axios.post(endpoint, payload, {
1072
+ headers,
1073
+ timeout: 120000, // 2 minute timeout for image generation
1074
+ });
1075
+ // Handle OpenAI-style response
1076
+ if (response.data?.data) {
1077
+ return {
1078
+ images: response.data.data.map((img) => ({
1079
+ url: img.url,
1080
+ b64_json: img.b64_json,
1081
+ revised_prompt: img.revised_prompt,
1082
+ })),
1083
+ model,
1084
+ };
1085
+ }
1086
+ // Handle direct response format
1087
+ return {
1088
+ images: Array.isArray(response.data) ? response.data : [response.data],
1089
+ model,
1090
+ };
1091
+ }
1092
+ catch (error) {
1093
+ if (axios.isAxiosError(error)) {
1094
+ const message = error.response?.data?.error?.message ||
1095
+ error.response?.data?.message ||
1096
+ error.message;
1097
+ throw new Error(`Image generation failed: ${message}`);
1098
+ }
1099
+ throw error;
1100
+ }
1101
+ }
1102
+ // Execute ComfyUI image generation request (Flux.1 workflow)
1103
+ async executeComfyUIRequest(baseUrl, prompt, options = {}) {
1104
+ const comfyBaseUrl = `${baseUrl.protocol}//${baseUrl.host}`;
1105
+ // Parse size
1106
+ const size = options.size || '1024x1024';
1107
+ const [width, height] = size.split('x').map(Number);
1108
+ // Determine model-specific settings
1109
+ const model = options.model || 'flux1-dev';
1110
+ const modelConfigs = {
1111
+ 'flux1-dev': {
1112
+ unetFile: 'flux1-dev.safetensors',
1113
+ t5File: 't5xxl_fp16.safetensors',
1114
+ steps: { draft: 12, standard: 20, high: 28, ultra: 40 },
1115
+ guidance: 3.5,
1116
+ useCheckpointLoader: false,
1117
+ },
1118
+ 'flux1-dev-fp8': {
1119
+ unetFile: 'flux1-dev-fp8.safetensors',
1120
+ t5File: 't5xxl_fp8_e4m3fn_scaled.safetensors',
1121
+ steps: { draft: 12, standard: 20, high: 28, ultra: 40 },
1122
+ guidance: 3.5,
1123
+ useCheckpointLoader: false,
1124
+ },
1125
+ 'flux1-schnell': {
1126
+ unetFile: 'flux1-schnell.safetensors',
1127
+ t5File: 't5xxl_fp16.safetensors',
1128
+ steps: { draft: 2, standard: 4, high: 6, ultra: 8 },
1129
+ guidance: 0, // Schnell doesn't use guidance
1130
+ useCheckpointLoader: false,
1131
+ },
1132
+ };
1133
+ const config = modelConfigs[model] || modelConfigs['flux1-dev'];
1134
+ const quality = (options.quality ||
1135
+ 'standard');
1136
+ const steps = config.steps[quality] || config.steps.standard;
1137
+ // Create a Flux.1 workflow for ComfyUI
1138
+ // Flux uses UNET loader + dual CLIP + VAE separately
1139
+ const workflow = {
1140
+ '6': {
1141
+ inputs: {
1142
+ text: prompt,
1143
+ clip: ['11', 0],
1144
+ },
1145
+ class_type: 'CLIPTextEncode',
1146
+ _meta: { title: 'CLIP Text Encode (Prompt)' },
1147
+ },
1148
+ '8': {
1149
+ inputs: {
1150
+ samples: ['13', 0],
1151
+ vae: ['10', 0],
1152
+ },
1153
+ class_type: 'VAEDecode',
1154
+ _meta: { title: 'VAE Decode' },
1155
+ },
1156
+ '9': {
1157
+ inputs: {
1158
+ filename_prefix: `LibreWebUI_${model}`,
1159
+ images: ['8', 0],
1160
+ },
1161
+ class_type: 'SaveImage',
1162
+ _meta: { title: 'Save Image' },
1163
+ },
1164
+ '10': {
1165
+ inputs: {
1166
+ vae_name: 'ae.safetensors',
1167
+ },
1168
+ class_type: 'VAELoader',
1169
+ _meta: { title: 'Load VAE' },
1170
+ },
1171
+ '11': {
1172
+ inputs: {
1173
+ clip_name1: 'clip_l.safetensors',
1174
+ clip_name2: config.t5File,
1175
+ type: 'flux',
1176
+ },
1177
+ class_type: 'DualCLIPLoader',
1178
+ _meta: { title: 'DualCLIPLoader' },
1179
+ },
1180
+ '12': {
1181
+ inputs: {
1182
+ unet_name: config.unetFile,
1183
+ weight_dtype: 'default',
1184
+ },
1185
+ class_type: 'UNETLoader',
1186
+ _meta: { title: 'Load Diffusion Model' },
1187
+ },
1188
+ '13': {
1189
+ inputs: {
1190
+ noise: ['25', 0],
1191
+ guider: ['22', 0],
1192
+ sampler: ['16', 0],
1193
+ sigmas: ['17', 0],
1194
+ latent_image: ['27', 0],
1195
+ },
1196
+ class_type: 'SamplerCustomAdvanced',
1197
+ _meta: { title: 'SamplerCustomAdvanced' },
1198
+ },
1199
+ '16': {
1200
+ inputs: {
1201
+ sampler_name: 'euler',
1202
+ },
1203
+ class_type: 'KSamplerSelect',
1204
+ _meta: { title: 'KSamplerSelect' },
1205
+ },
1206
+ '17': {
1207
+ inputs: {
1208
+ scheduler: 'simple',
1209
+ steps: steps,
1210
+ denoise: 1,
1211
+ model: ['12', 0],
1212
+ },
1213
+ class_type: 'BasicScheduler',
1214
+ _meta: { title: 'BasicScheduler' },
1215
+ },
1216
+ '22': {
1217
+ inputs: {
1218
+ model: ['12', 0],
1219
+ conditioning: config.guidance > 0 ? ['26', 0] : ['6', 0],
1220
+ },
1221
+ class_type: 'BasicGuider',
1222
+ _meta: { title: 'BasicGuider' },
1223
+ },
1224
+ '25': {
1225
+ inputs: {
1226
+ noise_seed: Math.floor(Math.random() * 1000000000000000),
1227
+ },
1228
+ class_type: 'RandomNoise',
1229
+ _meta: { title: 'RandomNoise' },
1230
+ },
1231
+ '27': {
1232
+ inputs: {
1233
+ width: width,
1234
+ height: height,
1235
+ batch_size: 1,
1236
+ },
1237
+ class_type: 'EmptySD3LatentImage',
1238
+ _meta: { title: 'EmptySD3LatentImage' },
1239
+ },
1240
+ };
1241
+ // Only add FluxGuidance node if guidance > 0 (not needed for schnell)
1242
+ if (config.guidance > 0) {
1243
+ workflow['26'] = {
1244
+ inputs: {
1245
+ guidance: config.guidance,
1246
+ conditioning: ['6', 0],
1247
+ },
1248
+ class_type: 'FluxGuidance',
1249
+ _meta: { title: 'FluxGuidance' },
1250
+ };
1251
+ }
1252
+ try {
1253
+ // Generate a unique client ID
1254
+ const clientId = `libre-webui-${Date.now()}`;
1255
+ // Submit the workflow
1256
+ const promptResponse = await axios.post(`${comfyBaseUrl}/prompt`, {
1257
+ prompt: workflow,
1258
+ client_id: clientId,
1259
+ }, {
1260
+ headers: { 'Content-Type': 'application/json' },
1261
+ timeout: 10000,
1262
+ });
1263
+ const promptId = promptResponse.data.prompt_id;
1264
+ if (!promptId) {
1265
+ throw new Error('Failed to get prompt ID from ComfyUI');
1266
+ }
1267
+ // Poll for completion
1268
+ let completed = false;
1269
+ let attempts = 0;
1270
+ const maxAttempts = 120; // 2 minutes with 1 second intervals
1271
+ while (!completed && attempts < maxAttempts) {
1272
+ await new Promise(resolve => setTimeout(resolve, 1000));
1273
+ attempts++;
1274
+ const historyResponse = await axios.get(`${comfyBaseUrl}/history/${promptId}`, { timeout: 5000 });
1275
+ if (historyResponse.data[promptId]) {
1276
+ const outputs = historyResponse.data[promptId].outputs;
1277
+ if (outputs && Object.keys(outputs).length > 0) {
1278
+ completed = true;
1279
+ // Find the SaveImage output
1280
+ for (const nodeId in outputs) {
1281
+ const nodeOutput = outputs[nodeId];
1282
+ if (nodeOutput.images && nodeOutput.images.length > 0) {
1283
+ const imageInfo = nodeOutput.images[0];
1284
+ // Get the image data
1285
+ const imageUrl = `${comfyBaseUrl}/view?filename=${encodeURIComponent(imageInfo.filename)}&subfolder=${encodeURIComponent(imageInfo.subfolder || '')}&type=${encodeURIComponent(imageInfo.type || 'output')}`;
1286
+ // Fetch image and convert to base64
1287
+ const imageResponse = await axios.get(imageUrl, {
1288
+ responseType: 'arraybuffer',
1289
+ timeout: 30000,
1290
+ });
1291
+ const base64Image = Buffer.from(imageResponse.data).toString('base64');
1292
+ return {
1293
+ images: [
1294
+ {
1295
+ b64_json: base64Image,
1296
+ revised_prompt: prompt,
1297
+ },
1298
+ ],
1299
+ model,
1300
+ };
1301
+ }
1302
+ }
1303
+ }
1304
+ }
1305
+ }
1306
+ if (!completed) {
1307
+ throw new Error('ComfyUI generation timed out');
1308
+ }
1309
+ throw new Error('No image output found from ComfyUI');
1310
+ }
1311
+ catch (error) {
1312
+ if (axios.isAxiosError(error)) {
1313
+ const message = error.response?.data?.error ||
1314
+ error.response?.data?.message ||
1315
+ error.message;
1316
+ throw new Error(`ComfyUI generation failed: ${message}`);
1317
+ }
1318
+ throw error;
1319
+ }
1320
+ }
1321
+ // Get image generation configuration for a specific plugin
1322
+ getImageGenConfig(pluginId) {
1323
+ const plugin = this.getPlugin(pluginId);
1324
+ if (!plugin)
1325
+ return null;
1326
+ if (plugin.capabilities?.image?.config) {
1327
+ return plugin.capabilities.image.config;
1328
+ }
1329
+ return null;
1330
+ }
915
1331
  // Get all plugins that support a specific capability type
916
1332
  getPluginsByCapability(capabilityType) {
917
1333
  const allPlugins = this.getAllPlugins();
@@ -919,7 +1335,7 @@ class PluginService {
919
1335
  for (const plugin of allPlugins) {
920
1336
  // Check if primary type matches
921
1337
  if (plugin.type === capabilityType) {
922
- const apiKey = process.env[plugin.auth.key_env];
1338
+ const apiKey = this.getApiKey(plugin);
923
1339
  if (apiKey) {
924
1340
  result.push(plugin);
925
1341
  }
@@ -947,7 +1363,7 @@ class PluginService {
947
1363
  break;
948
1364
  }
949
1365
  if (hasCapability) {
950
- const apiKey = process.env[plugin.auth.key_env];
1366
+ const apiKey = this.getApiKey(plugin);
951
1367
  if (apiKey) {
952
1368
  result.push(plugin);
953
1369
  }