@intranefr/superbackend 1.5.0 → 1.5.2
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.
- package/.env.example +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- package/src/routes/llmUi.routes.js +0 -26
|
@@ -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
|
|
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
|
|
383
|
-
|
|
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
|
|
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
|
|
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
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
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
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
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.
|
|
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
|
-
|
|
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({
|
|
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
|
};
|