@intranefr/superbackend 1.4.4 → 1.5.1

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 (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. package/views/partials/llm-provider-model-picker.ejs +183 -0
@@ -6,7 +6,8 @@ const { getInferredI18nKeys, getInferredI18nEntries } = require('../services/i18
6
6
  const { createAuditEvent, getBasicAuthActor } = require('../services/audit.service');
7
7
  const { getSettingValue } = require('../services/globalSettings.service');
8
8
 
9
- const OpenAI = require('openai');
9
+ const llmService = require('../services/llm.service');
10
+ const { resolveLlmProviderModel } = require('../services/llmDefaults.service');
10
11
 
11
12
  async function ensureLocaleExists(code, actor) {
12
13
  if (!code) return;
@@ -379,16 +380,8 @@ exports.deleteEntry = async (req, res) => {
379
380
  }
380
381
  };
381
382
 
382
- async function buildOpenRouterClient() {
383
- const apiKey = await getSettingValue('i18n.ai.openrouter.apiKey', await getSettingValue('ai.openrouter.apiKey', null));
384
- if (!apiKey) {
385
- throw new Error('Missing i18n.ai.openrouter.apiKey or ai.openrouter.apiKey');
386
- }
387
-
388
- return new OpenAI({
389
- apiKey,
390
- baseURL: 'https://openrouter.ai/api/v1',
391
- });
383
+ async function getLegacyOpenRouterApiKey() {
384
+ return getSettingValue('i18n.ai.openrouter.apiKey', await getSettingValue('ai.openrouter.apiKey', null));
392
385
  }
393
386
 
394
387
  function buildAiPrompt({ glossary, fromLocale, toLocale, key, fromValue }) {
@@ -446,10 +439,19 @@ exports.aiPreview = async (req, res) => {
446
439
 
447
440
  const fromMap = new Map(fromEntries.map((e) => [e.key, e]));
448
441
 
449
- const aiModel = model || (await getSettingValue('i18n.ai.model', 'google/gemini-2.5-flash-lite'));
442
+ const resolved = await resolveLlmProviderModel({
443
+ systemKey: 'i18n.translate.preview',
444
+ providerKey: req.body?.providerKey,
445
+ model,
446
+ });
447
+
448
+ const aiModel = resolved.model || (await getSettingValue('i18n.ai.model', 'google/gemini-2.5-flash-lite'));
450
449
  const glossary = await getSettingValue('i18n.ai.glossary', '');
451
450
 
452
- const client = await buildOpenRouterClient();
451
+ const legacyApiKey = await getLegacyOpenRouterApiKey();
452
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
453
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
454
+ : {};
453
455
 
454
456
  const results = [];
455
457
  for (const key of targetKeys) {
@@ -459,12 +461,17 @@ exports.aiPreview = async (req, res) => {
459
461
  }
460
462
 
461
463
  const prompt = buildAiPrompt({ glossary, fromLocale, toLocale, key, fromValue: from.value });
462
- const resp = await client.chat.completions.create({
463
- model: aiModel,
464
- messages: [{ role: 'user', content: prompt }],
465
- });
466
-
467
- const translated = resp.choices?.[0]?.message?.content?.trim() || '';
464
+ const resp = await llmService.callAdhoc(
465
+ {
466
+ providerKey: resolved.providerKey,
467
+ model: aiModel,
468
+ messages: [{ role: 'user', content: prompt }],
469
+ promptKeyForAudit: 'i18n.translate.preview',
470
+ },
471
+ runtimeOptions,
472
+ );
473
+
474
+ const translated = String(resp.content || '').trim();
468
475
 
469
476
  results.push({
470
477
  key,
@@ -473,6 +480,7 @@ exports.aiPreview = async (req, res) => {
473
480
  fromValue: from.value,
474
481
  proposedValue: translated,
475
482
  valueFormat: from.valueFormat || 'text',
483
+ providerKey: resolved.providerKey,
476
484
  model: aiModel,
477
485
  });
478
486
  }
@@ -514,7 +522,7 @@ exports.aiApply = async (req, res) => {
514
522
  edited: true,
515
523
  editedAt: new Date(),
516
524
  editedBy: actor.actorId,
517
- lastAiProvider: 'openrouter',
525
+ lastAiProvider: item.providerKey || 'openrouter',
518
526
  lastAiModel: item.model || null,
519
527
  });
520
528
 
@@ -540,7 +548,7 @@ exports.aiApply = async (req, res) => {
540
548
  existing.edited = true;
541
549
  existing.editedAt = new Date();
542
550
  existing.editedBy = actor.actorId;
543
- existing.lastAiProvider = 'openrouter';
551
+ existing.lastAiProvider = item.providerKey || 'openrouter';
544
552
  existing.lastAiModel = item.model || null;
545
553
  await existing.save();
546
554
 
@@ -578,10 +586,19 @@ exports.aiTranslateText = async (req, res) => {
578
586
 
579
587
  await ensureLocaleExists(toLocale, getBasicAuthActor(req));
580
588
 
581
- const aiModel = model || (await getSettingValue('i18n.ai.model', 'google/gemini-2.5-flash-lite'));
589
+ const resolved = await resolveLlmProviderModel({
590
+ systemKey: 'i18n.translate.text',
591
+ providerKey: req.body?.providerKey,
592
+ model,
593
+ });
594
+
595
+ const aiModel = resolved.model || (await getSettingValue('i18n.ai.model', 'google/gemini-2.5-flash-lite'));
582
596
  const glossary = await getSettingValue('i18n.ai.glossary', '');
583
597
 
584
- const client = await buildOpenRouterClient();
598
+ const legacyApiKey = await getLegacyOpenRouterApiKey();
599
+ const runtimeOptions = (resolved.providerKey === 'openrouter' && legacyApiKey)
600
+ ? { apiKey: legacyApiKey, baseUrl: 'https://openrouter.ai/api/v1' }
601
+ : {};
585
602
  const prompt = buildAiPrompt({
586
603
  glossary,
587
604
  fromLocale,
@@ -590,13 +607,18 @@ exports.aiTranslateText = async (req, res) => {
590
607
  fromValue: text,
591
608
  });
592
609
 
593
- const resp = await client.chat.completions.create({
594
- model: aiModel,
595
- messages: [{ role: 'user', content: prompt }],
596
- });
610
+ const resp = await llmService.callAdhoc(
611
+ {
612
+ providerKey: resolved.providerKey,
613
+ model: aiModel,
614
+ messages: [{ role: 'user', content: prompt }],
615
+ promptKeyForAudit: 'i18n.translate.text',
616
+ },
617
+ runtimeOptions,
618
+ );
597
619
 
598
- const translatedText = resp.choices?.[0]?.message?.content?.trim() || '';
599
- res.json({ translatedText, model: aiModel });
620
+ const translatedText = String(resp.content || '').trim();
621
+ res.json({ translatedText, model: aiModel, providerKey: resolved.providerKey });
600
622
  } catch (error) {
601
623
  console.error('Error translating text with AI:', error);
602
624
  res.status(500).json({ error: error.message || 'Failed to translate text' });
@@ -2,10 +2,17 @@ const GlobalSetting = require("../models/GlobalSetting");
2
2
  const AuditEvent = require("../models/AuditEvent");
3
3
  const llmService = require("../services/llm.service");
4
4
  const { encryptString } = require("../utils/encryption");
5
+ const { decryptString } = require("../utils/encryption");
6
+ const axios = require("axios");
5
7
 
6
8
  const PROVIDERS_KEY = "llm.providers";
7
9
  const PROMPTS_KEY = "llm.prompts";
8
10
 
11
+ const DEFAULTS_PROVIDER_KEY = "llm.defaults.providerKey";
12
+ const DEFAULTS_MODEL_KEY = "llm.defaults.model";
13
+ const SYSTEM_DEFAULTS_KEY = "llm.systemDefaults";
14
+ const PROVIDER_MODELS_KEY = "llm.providerModels";
15
+
9
16
  async function getJsonSetting(key, defaultValue) {
10
17
  const doc = await GlobalSetting.findOne({ key }).lean();
11
18
  if (!doc || !doc.value) return defaultValue;
@@ -15,15 +22,53 @@ async function getJsonSetting(key, defaultValue) {
15
22
  } catch (e) {
16
23
  return defaultValue;
17
24
  }
25
+
26
+ }
27
+
28
+ async function getStringSetting(key, defaultValue) {
29
+ const doc = await GlobalSetting.findOne({ key }).lean();
30
+ if (!doc || doc.value === undefined || doc.value === null) return defaultValue;
31
+ return String(doc.value);
32
+ }
33
+
34
+ async function setStringSetting(key, value, description) {
35
+ const stringValue = value === undefined || value === null ? "" : String(value);
36
+ const existing = await GlobalSetting.findOne({ key });
37
+ if (existing) {
38
+ existing.value = stringValue;
39
+ existing.type = "string";
40
+ if (!existing.description && description) {
41
+ existing.description = description;
42
+ }
43
+ await existing.save();
44
+ return existing;
45
+ }
46
+ // Ensure we never create a document with an undefined value
47
+ if (stringValue === undefined) {
48
+ throw new Error(`Cannot save GlobalSetting with undefined value for key: ${key}`);
49
+ }
50
+ const created = new GlobalSetting({
51
+ key,
52
+ value: stringValue,
53
+ type: "string",
54
+ description: description || `LLM setting ${key}`,
55
+ });
56
+ await created.save();
57
+ return created;
18
58
  }
19
59
 
20
60
  async function setJsonSetting(key, value) {
21
61
  const descriptions = {
22
62
  [PROVIDERS_KEY]: "LLM providers configuration (JSON)",
23
63
  [PROMPTS_KEY]: "LLM prompts configuration (JSON)",
64
+ [SYSTEM_DEFAULTS_KEY]: "LLM system defaults (JSON)",
65
+ [PROVIDER_MODELS_KEY]: "LLM provider model suggestions (JSON)",
24
66
  };
25
67
  const description = descriptions[key] || `LLM setting ${key}`;
26
- const stringValue = JSON.stringify(value || {});
68
+ // Ensure we always have a valid object to stringify
69
+ const safeValue = (value === undefined || value === null || value === '') ? {} : value;
70
+ const stringValue = JSON.stringify(safeValue);
71
+
27
72
  const existing = await GlobalSetting.findOne({ key });
28
73
  if (existing) {
29
74
  existing.value = stringValue;
@@ -34,6 +79,10 @@ async function setJsonSetting(key, value) {
34
79
  await existing.save();
35
80
  return existing;
36
81
  }
82
+ // Ensure we never create a document with an empty value
83
+ if (!stringValue) {
84
+ throw new Error(`Cannot save GlobalSetting with empty value for key: ${key}`);
85
+ }
37
86
  const created = new GlobalSetting({
38
87
  key,
39
88
  value: stringValue,
@@ -48,6 +97,10 @@ async function getConfig(req, res) {
48
97
  try {
49
98
  const providers = await getJsonSetting(PROVIDERS_KEY, {});
50
99
  const prompts = await getJsonSetting(PROMPTS_KEY, {});
100
+ const defaultsProviderKey = (await getStringSetting(DEFAULTS_PROVIDER_KEY, "")) || "";
101
+ const defaultsModel = (await getStringSetting(DEFAULTS_MODEL_KEY, "")) || "";
102
+ const systemDefaults = await getJsonSetting(SYSTEM_DEFAULTS_KEY, {});
103
+ const providerModels = await getJsonSetting(PROVIDER_MODELS_KEY, {});
51
104
  const safeProviders = {};
52
105
  if (providers && typeof providers === "object") {
53
106
  for (const [key, value] of Object.entries(providers)) {
@@ -56,7 +109,13 @@ async function getConfig(req, res) {
56
109
  safeProviders[key] = rest;
57
110
  }
58
111
  }
59
- res.json({ providers: safeProviders, prompts });
112
+ res.json({
113
+ providers: safeProviders,
114
+ prompts,
115
+ defaults: { providerKey: defaultsProviderKey, model: defaultsModel },
116
+ systemDefaults,
117
+ providerModels,
118
+ });
60
119
  } catch (error) {
61
120
  console.error("[adminLlm] getConfig error", error);
62
121
  res.status(500).json({ error: "Failed to load LLM configuration" });
@@ -69,6 +128,10 @@ async function saveConfig(req, res) {
69
128
  const providers = body.providers && typeof body.providers === "object" ? body.providers : {};
70
129
  const prompts = body.prompts && typeof body.prompts === "object" ? body.prompts : {};
71
130
 
131
+ const defaults = body.defaults && typeof body.defaults === "object" ? body.defaults : {};
132
+ const systemDefaults = body.systemDefaults && typeof body.systemDefaults === "object" ? body.systemDefaults : {};
133
+ const providerModels = body.providerModels && typeof body.providerModels === "object" ? body.providerModels : {};
134
+
72
135
  const storedProviders = {};
73
136
  for (const [key, value] of Object.entries(providers)) {
74
137
  if (!value || typeof value !== "object") continue;
@@ -102,6 +165,20 @@ async function saveConfig(req, res) {
102
165
  await setJsonSetting(PROVIDERS_KEY, storedProviders);
103
166
  await setJsonSetting(PROMPTS_KEY, prompts);
104
167
 
168
+ await setStringSetting(
169
+ DEFAULTS_PROVIDER_KEY,
170
+ typeof defaults.providerKey === "string" ? defaults.providerKey.trim() : "",
171
+ "LLM global default providerKey",
172
+ );
173
+ await setStringSetting(
174
+ DEFAULTS_MODEL_KEY,
175
+ typeof defaults.model === "string" ? defaults.model.trim() : "",
176
+ "LLM global default model",
177
+ );
178
+
179
+ await setJsonSetting(SYSTEM_DEFAULTS_KEY, systemDefaults);
180
+ await setJsonSetting(PROVIDER_MODELS_KEY, providerModels);
181
+
105
182
  res.json({ success: true });
106
183
  } catch (error) {
107
184
  console.error("[adminLlm] saveConfig error", error);
@@ -109,6 +186,52 @@ async function saveConfig(req, res) {
109
186
  }
110
187
  }
111
188
 
189
+ async function listOpenRouterModels(req, res) {
190
+ try {
191
+ const apiKeyDoc = await GlobalSetting.findOne({ key: "llm.provider.openrouter.apiKey", type: "encrypted" }).lean();
192
+ if (!apiKeyDoc || !apiKeyDoc.value) {
193
+ return res.status(400).json({ error: "Missing OpenRouter API key (llm.provider.openrouter.apiKey)" });
194
+ }
195
+
196
+ let apiKey;
197
+ try {
198
+ const payload = JSON.parse(apiKeyDoc.value);
199
+ apiKey = decryptString(payload);
200
+ } catch (e) {
201
+ return res.status(500).json({ error: "Failed to decrypt OpenRouter API key" });
202
+ }
203
+
204
+ // OpenRouter model list endpoint
205
+ let data;
206
+ try {
207
+ const response = await axios.get("https://openrouter.ai/api/v1/models", {
208
+ headers: {
209
+ Authorization: `Bearer ${apiKey}`,
210
+ },
211
+ timeout: 20000,
212
+ });
213
+ data = response && response.data ? response.data : {};
214
+ } catch (e) {
215
+ const message =
216
+ (e.response && e.response.data && e.response.data.error && e.response.data.error.message)
217
+ ? e.response.data.error.message
218
+ : (e.message || "Failed to fetch OpenRouter models");
219
+ return res.status(502).json({ error: message });
220
+ }
221
+
222
+ const items = Array.isArray(data?.data) ? data.data : [];
223
+ const models = items
224
+ .map((m) => (m && (m.id || m.name) ? String(m.id || m.name) : ""))
225
+ .filter(Boolean)
226
+ .sort();
227
+
228
+ return res.json({ models });
229
+ } catch (error) {
230
+ console.error("[adminLlm] listOpenRouterModels error", error);
231
+ return res.status(500).json({ error: "Failed to list OpenRouter models" });
232
+ }
233
+ }
234
+
112
235
  async function testPrompt(req, res) {
113
236
  try {
114
237
  const promptKey = String(req.params.key || "");
@@ -270,4 +393,5 @@ module.exports = {
270
393
  testPrompt,
271
394
  listAudit,
272
395
  listCosts,
396
+ listOpenRouterModels,
273
397
  };