const fs = require("fs"); const path = require("path"); const express = require("express"); const { ensureDataDirs, resolveData } = require("./backend/paths"); const { getConfig, saveConfig, getRuntimeState } = require("./backend/config_manager"); const { detectHardware, estimateAllocation, performanceTuningHints } = require("./backend/hardware"); const metrics = require("./backend/metrics"); const { roleOf } = require("./backend/permissions"); const { canUseAssistant } = require("./backend/assistant_permissions"); const { RequestQueue } = require("./backend/queue_manager"); const { ToolRegistry } = require("./backend/tool_router"); const { DownloadManager } = require("./backend/downloader"); const { RuntimeManager, GateRuntimeManager, combinedResourceEstimate } = require("./backend/runtime_manager"); const { AiProvider } = require("./backend/ai_provider"); const { GateProvider } = require("./backend/gate_provider"); const { SafeAnswerCache } = require("./backend/cache"); const { AssistantRequestJobs } = require("./backend/request_jobs"); const { getLatestDiagnostic, createDiagnosticsBundle } = require("./backend/diagnostics"); const { evaluateAssistantAvailability } = require("./backend/assistant_availability"); const { buildVisibilityDiagnostics } = require("./backend/assistant_visibility"); const repoIndexer = require("./backend/repo_indexer"); const { HARD_RULES, normalizeScope } = require("./backend/scope_manager"); const { AiAccessControl } = require("./backend/access_control"); const { AiRateLimiter, mergeLimits } = require("./backend/rate_limits"); const { buildOriginContext, formatPlatformReply, formatPlatformReplyDetails } = require("./backend/commands"); const { AssistantPanelDiagnostics } = require("./backend/assistant_panel_diagnostics"); const { formatAssistantResponse } = require("./backend/response_formatter"); const { FeedbackStore, FEEDBACK_KINDS, FEEDBACK_TAGS, improvementAccess } = require("./backend/feedback"); const { CorrectionStore, PROMOTION_TARGETS } = require("./backend/corrections"); const { EvalStore } = require("./backend/evals"); const { TrainingExporter } = require("./backend/training_export"); const { ToolRepoClient } = require("./backend/tool_repo_client"); const { ToolInstaller } = require("./backend/tool_installer"); const { ToolLoader } = require("./backend/tool_loader"); const { ToolManager } = require("./backend/tool_manager"); const { ToolSettings } = require("./backend/tool_settings"); const { buildAllowedToolsSection } = require("./backend/prompt_builder"); const storage = require("./backend/storage"); const { formatBytes, bytesFromMb, sanityCheckSize } = require("./backend/size_utils"); const PLUGIN_ID = "lumi_ai"; const TOKEN_PRESETS = Object.freeze([ { label: "Tiny (256)", value: 256, description: "Small helper replies and minimal context." }, { label: "Very small (512)", value: 512, description: "Short replies and low memory usage." }, { label: "Small (1024)", value: 1024, description: "Compact answers and lightweight request gates." }, { label: "Short (2048)", value: 2048, description: "Short conversations and normal commands." }, { label: "Medium (4096)", value: 4096, description: "Balanced default for normal assistant use." }, { label: "Large (8192)", value: 8192, description: "Longer conversations and documents." }, { label: "Extended (16384)", value: 16384, description: "Long context when the selected model supports it." }, { label: "Extra extended (32768)", value: 32768, description: "Highest supported local preset for large-context models." } ]); const CONTEXT_OPTIONS = TOKEN_PRESETS; const GATE_CONTEXT_OPTIONS = TOKEN_PRESETS.filter((option) => option.value >= 512 && option.value <= 4096); const modelManifest = require("./models_manifest.json"); const runtimeManifest = require("./runtime_manifest.json"); module.exports = { id: PLUGIN_ID, init({ web, settings, commandRouter, db, plugin }) { ensureDataDirs(); if (!repoIndexer.loadIndex()) { try { repoIndexer.refreshIndex(); } catch (error) { console.warn("Lumi AI repository index initialization failed", error.message); } } let config = getConfig(); const feedbackStore = new FeedbackStore(); const correctionStore = new CorrectionStore({ getConfig: () => config, verifyLink: isVerifiedImprovementLink }); const getModel = (id) => modelManifest.models.find((model) => model.id === id); const downloads = new DownloadManager((entry) => metrics.record(entry)); const accessControl = new AiAccessControl((entry) => metrics.record(entry)); const rateLimiter = new AiRateLimiter(() => config, (entry) => metrics.record(entry)); const panelTemplatePath = path.join(__dirname, "views", "assistant-panel.ejs"); const panelDiagnostics = new AssistantPanelDiagnostics(panelTemplatePath); panelDiagnostics.templateCheck( ["endpoint", "user"], { endpoint: `/plugins/${PLUGIN_ID}`, user: { id: "diagnostic-user" } } ); const queue = new RequestQueue(() => config); const requestJobs = new AssistantRequestJobs(); const tools = new ToolRegistry((entry) => metrics.record(entry)); const toolRepoClient = new ToolRepoClient({ settings }); const toolInstaller = new ToolInstaller({ repoClient: toolRepoClient }); const toolLoader = new ToolLoader({ registry: tools, installer: toolInstaller, settings, lumiAiVersion: require("./plugin.json").version, lumiVersion: require("../../package.json").version }); const toolSettings = new ToolSettings({ installer: toolInstaller }); const toolManager = new ToolManager({ repoClient: toolRepoClient, installer: toolInstaller, loader: toolLoader, settings: toolSettings }); const contextProviders = new Map(); const frontendVisibility = new Map(); const getSafeContext = (role) => [...contextProviders.values()].flatMap((fn) => { try { return normalizeContext(fn({ role })); } catch { return []; } }); const runtime = new RuntimeManager({ getConfig: () => config, getModel, runtimeManifest, onCrash: (message) => metrics.record({ kind: "runtime", status: "failed", runtime_crash: true, message }), onDiagnostic: (entry) => metrics.record(entry) }); const gateRuntime = new GateRuntimeManager({ getConfig: () => config, getModel, onDiagnostic: (entry) => metrics.record(entry) }); const gate = new GateProvider({ getConfig: () => config, runtime: gateRuntime, lookupRepo: (message) => repoIndexer.lookupSupport(message), lookupCorrection: (context) => correctionStore.findPredefined({ message: context.message, role: context.role, origin: context.origin || context.platform, platform: context.platform }), cache: new SafeAnswerCache(() => config), metrics }); let mainStartPromise = null; let gateStartPromise = null; const ensureMainRuntime = async (options = {}) => { const health = await runtime.health(); if (health.healthy) return health; if (!mainStartPromise) { mainStartPromise = runtime.start(options).finally(() => { mainStartPromise = null; }); } return mainStartPromise; }; const ensureGateRuntime = async () => { if (gateRuntime.status().state === "running") return gateRuntime.health(); if (!gateStartPromise) { gateStartPromise = gateRuntime.start().finally(() => { gateStartPromise = null; }); } return gateStartPromise; }; const provider = new AiProvider({ getConfig: () => config, runtime, gate, queue, tools, metrics, getContext: getSafeContext, lookupRepo: (message) => repoIndexer.lookupSupport(message), getRepoContext: (message, role, allowModeratorCodeHelp) => repoIndexer.supportContext(message, repoIndexer.loadIndex(), 8, role, allowModeratorCodeHelp), getCorrections: (context) => correctionStore.context(context), ensureRuntime: ensureMainRuntime }); const evalStore = new EvalStore({ provider }); const trainingExporter = new TrainingExporter({ feedback: feedbackStore, corrections: correctionStore }); const startRuntimes = async (options = {}) => { if (config.enabled) { ensureGateRuntime().catch((error) => { metrics.record({ kind: "gate_runtime", status: "failed", reason_code: "gate_start_failed", message: error.message }); web.emitEvent?.("ai:model_status", { status: "gate_start_failed", message: `Lumi AI gate runtime failed to start: ${error.message}` }, { role: "admin" }); }); } const main = await ensureMainRuntime(options); return { ...main, gate: await gateRuntime.health() }; }; const stopRuntimes = async (options = {}) => { await gateRuntime.stop(); return runtime.stop(options); }; const restartRuntimes = async () => { await gateRuntime.stop(); const main = await runtime.restart(); if (config.enabled) { try { await ensureGateRuntime(); } catch (error) { metrics.record({ kind: "gate_runtime", status: "failed", reason_code: "gate_restart_failed", message: error.message }); web.emitEvent?.("ai:model_status", { status: "gate_restart_failed", message: `Lumi AI gate runtime restart failed: ${error.message}` }, { role: "admin" }); } } return { ...main, gate: await gateRuntime.health() }; }; let gateRecoveryPending = false; const gateMonitor = setInterval(async () => { if ( gateRecoveryPending || !config.enabled || gateRuntime.status().state === "running" ) return; gateRecoveryPending = true; try { await ensureGateRuntime(); } catch (error) { web.emitEvent?.("ai:model_status", { status: "gate_recovery_failed", message: `Lumi AI gate recovery failed: ${error.message}` }, { role: "admin" }); } finally { gateRecoveryPending = false; } }, 30000); gateMonitor.unref?.(); const api = { health: () => runtime.health(), capabilities: () => ({ provider: "local_llama_cpp", enabled: config.enabled, model_id: config.selected_model_id, roles: config.assistant_visibility, tools: tools.list("admin").map((tool) => tool.tool_id) }), metrics_summary: () => metrics.report(), generate: (input) => provider.generate(input), classify: (input) => provider.classify(input), summarize: (input) => provider.summarize(input), route_tool: async ({ message, allowed_tools = [], ...input }) => { const result = await provider.generate({ message, ...input, scope: "route_tool" }); if (result.tool_call && !allowed_tools.includes(result.tool_call.tool)) { return { ...result, success: false, tool_call: null, refusal_reason: "tool_not_allowed" }; } return result; }, registerTool: (definition) => tools.register(definition), unregisterToolOwner: (owner) => tools.unregisterOwner(owner), registerContext: (id, factory) => { if (!id || typeof factory !== "function") throw new Error("Invalid AI context provider."); contextProviders.set(id, factory); }, unregisterContext: (id) => contextProviders.delete(id) }; global.lumiFrameworks = global.lumiFrameworks || {}; global.lumiFrameworks.ai = api; global.lumiFrameworks.lumi_ai = api; const router = web.createRouter(); router.use("/assets", express.static(path.join(__dirname, "public"))); router.get("/", async (req, res) => { if (!req.session.user?.isAdmin) return denied(res); const hardware = detectHardware(modelManifest.models, runtimeManifest); const runtimeTarget = getRuntimeTarget(hardware); const selectedModel = getModel(config.selected_model_id); const runtimeStatus = await runtime.health(); const gateStatus = await gateRuntime.health(); const gpuAllocation = estimateAllocation({ model: selectedModel, contextSize: config.context_size, gpu: hardware.gpu, backend: runtimeStatus.runtime_backend, intentPercent: config.gpu_allocation_intent_percent, managedUsageMb: runtimeStatus.state === "running" ? runtimeStatus.estimated_gpu_memory_mb : 0 }); const assistantAvailability = evaluateAssistantAvailability({ user: req.session.user, config, model: selectedModel, runtimeHealth: runtimeStatus, providerAvailable: Boolean(provider) }); const visibilityDiagnostics = buildVisibilityDiagnostics({ user: req.session.user, config, model: selectedModel, runtimeHealth: runtimeStatus, providerAvailable: Boolean(provider), frontend: frontendVisibility.get(req.session.user.id) }); const usage = storage.storageUsage(modelManifest.models, config.selected_model_id); const resourceEstimate = combinedResourceEstimate({ main: runtimeStatus, gate: gateStatus, hardware }); const recentGeneration = metrics.history(100).find((entry) => entry.kind === "request" && Number(entry.generation_tps) > 0 ); const tuningHints = performanceTuningHints({ model: selectedModel, config, gpu: hardware.gpu, allocation: gpuAllocation, generationTps: Number(recentGeneration?.generation_tps) || 0 }); const metricsPage = metrics.historyPage(req.query.metrics_page, 25); const slowRequestsPage = metrics.slowRequestsPage(req.query.slow_page, 15); const accessPage = paginateRows(accessControl.list(), req.query.access_page, 25); const logPage = storage.listLogsPage(req.query.logs_page, 25); const runtimeFolderSize = usage.categories.runtime; const selectedModelPath = selectedModel ? storage.modelPath(selectedModel) : null; const modelFileSize = selectedModelPath && fs.existsSync(selectedModelPath) ? fs.statSync(selectedModelPath).size : 0; const sizeDiagnostics = [ sanityCheckSize("Runtime folder", runtimeFolderSize, 5 * 1024 ** 3), sanityCheckSize("Runtime archive", runtimeTarget?.size || 0, 2 * 1024 ** 3), sanityCheckSize("Selected model", selectedModel?.size || 0, 100 * 1024 ** 3), sanityCheckSize("Installed model", modelFileSize, 100 * 1024 ** 3), sanityCheckSize("Estimated GPU memory", bytesFromMb(gpuAllocation.estimated_gpu_memory_mb), 100 * 1024 ** 3) ].filter((check) => !check.valid); for (const diagnostic of sizeDiagnostics) console.warn(`Lumi AI size diagnostic: ${diagnostic.message}`); const models = modelManifest.models.map((model) => ({ ...model, downloaded: fs.existsSync(resolveData("models", model.filename)), installed_size: fs.existsSync(resolveData("models", model.filename)) ? fs.statSync(resolveData("models", model.filename)).size : 0, compatible: model.ram_gb * 1024 <= hardware.total_ram_mb && model.size / 1048576 <= hardware.free_disk_mb })); const installedModels = models.filter((model) => model.downloaded); res.render(path.join(__dirname, "views", "settings.ejs"), { title: "Lumi AI", config, models, installedModels, selectedModelInstalled: installedModels.some((model) => model.id === config.selected_model_id), contextOptions: CONTEXT_OPTIONS, tokenPresets: TOKEN_PRESETS, gateContextOptions: GATE_CONTEXT_OPTIONS, runtimeTarget, runtimeManifest, runtimeStatus, gateStatus, resourceEstimate, tuningHints, jobDiagnostics: requestJobs.diagnostics(), gpuAllocation, assistantAvailability, visibilityDiagnostics, panelDiagnostics: panelDiagnostics.snapshot(), activeAiRestrictions: accessPage.entries, accessPage, recentRateLimitDenials: rateLimiter.recentDenials(), hardRules: HARD_RULES, assistantReason: describeAssistantReason(assistantAvailability.reason_code), repoIndexStatus: repoIndexer.indexStatus(), runtimeState: getRuntimeState(), latestDiagnostic: getLatestDiagnostic(), runtimeFolderSize, modelFileSize, storageUsage: usage, sizeDiagnostics, hardware, metrics: metrics.report(), history: metricsPage.entries, metricsPage, slowRequestsPage, logFiles: logPage.entries, logPage, formatBytes, formatDate, formatDuration }); }); router.post("/settings", async (req, res) => { if (!req.session.user?.isAdmin) return denied(res); const model = getModel(req.body.selected_model_id); if (!model) return flash(req, res, "error", "Unknown model."); if (!fs.existsSync(resolveData("models", model.filename))) { return flash(req, res, "error", "Selected model must be installed before it can be saved."); } const contextValues = CONTEXT_OPTIONS.map((option) => option.value); const requestedContext = Number(req.body.context_size); if (!contextValues.includes(requestedContext)) { return flash(req, res, "error", "Choose a supported AI context size."); } const contextSize = requestedContext; const tokenValues = TOKEN_PRESETS.map((option) => option.value); const gateContextValues = GATE_CONTEXT_OPTIONS.map((option) => option.value); const requestedGateContext = Number(req.body.gate_context_size); if (!gateContextValues.includes(requestedGateContext)) { return flash(req, res, "error", "Choose a supported gate context size."); } const presetToken = (field, fallback, label) => { const value = Number(req.body[field]); if (!tokenValues.includes(value)) { throw new Error(`Choose a supported preset for ${label}.`); } return value || fallback; }; const previousConfig = config; let nextConfig; try { nextConfig = { ...config, enabled: req.body.enabled === "on", selected_model_id: model.id, context_size: contextSize, internal_generation_char_budget: boundedInt(req.body.internal_generation_char_budget, 2000, 64000, 16000), threads: boundedInt(req.body.threads, 0, 256, 0), gpu_allocation_intent_percent: boundedInt(req.body.gpu_allocation_intent_percent, 0, 100, 0), concurrency: boundedInt(req.body.concurrency, 1, 8, 1), max_queue_length: boundedInt(req.body.max_queue_length, 1, 100, 8), request_timeout_ms: boundedInt(req.body.hard_generation_timeout_ms, 30000, 3600000, 600000), ui_soft_timeout_ms: boundedInt(req.body.ui_soft_timeout_ms, 5000, 300000, 45000), hard_generation_timeout_ms: boundedInt(req.body.hard_generation_timeout_ms, 30000, 3600000, 600000), max_output_tokens: presetToken("max_output_tokens", 2048, "API/test output tokens"), output_budgets: { navigation_help: presetToken("output_budget_navigation_help", 256, "navigation/help tokens"), simple_answer: presetToken("output_budget_simple_answer", 512, "simple answer tokens"), code_custom_command: presetToken("output_budget_code_custom_command", 1024, "code/custom command tokens"), admin_debug: presetToken("output_budget_admin_debug", 2048, "admin debug tokens"), explicit_long: presetToken("output_budget_explicit_long", 4096, "explicit long-answer tokens") }, batch_size: boundedInt(req.body.batch_size, 32, 4096, 512), ubatch_size: boundedInt(req.body.ubatch_size, 16, 4096, 128), per_user_requests_per_minute: boundedInt(req.body.per_user_requests_per_minute, 1, 120, 6), admin_bypass_rate_limit: req.body.admin_bypass_rate_limit === "on", assistant_enabled: req.body.assistant_enabled === "on", assistant_debug_logging: req.body.assistant_debug_logging === "on", assistant_visibility: { admins: req.body.visibility_admins === "on", mods: req.body.visibility_mods === "on", users: req.body.visibility_users === "on" }, commands: { enabled: req.body.command_enabled === "on", triggers: parseCommandTriggers(req.body.command_triggers), platforms: { discord: req.body.command_platform_discord === "on", twitch: req.body.command_platform_twitch === "on", youtube: req.body.command_platform_youtube === "on", kick: req.body.command_platform_kick === "on", other: req.body.command_platform_other === "on" }, roles: { admins: req.body.command_role_admins === "on", mods: req.body.command_role_mods === "on", users: req.body.command_role_users === "on" }, unavailable_message: cleanText(req.body.command_unavailable_message, 500), denied_message: cleanText(req.body.command_denied_message, 500) }, rate_limits: mergeLimits({ roles: { admin: limitFromBody(req.body, "limit_role_admin", 30, 60), mod: limitFromBody(req.body, "limit_role_mod", 12, 60), user: limitFromBody(req.body, "limit_role_user", 4, 60) }, platforms: { webui: limitFromBody(req.body, "limit_platform_webui", 60, 60), discord: limitFromBody(req.body, "limit_platform_discord", 30, 60), twitch: limitFromBody(req.body, "limit_platform_twitch", 20, 60), youtube: limitFromBody(req.body, "limit_platform_youtube", 20, 60), kick: limitFromBody(req.body, "limit_platform_kick", 20, 60), other: limitFromBody(req.body, "limit_platform_other", 20, 60) }, per_user: limitFromBody(req.body, "limit_user", 2, 30), per_channel: limitFromBody(req.body, "limit_channel", 12, 60), queue_when_limited: req.body.queue_when_limited === "on" }), gate: { ...config.gate, model_id: getModel(req.body.gate_model_id)?.id || config.gate.model_id, context_size: requestedGateContext, threads: boundedInt(req.body.gate_threads, 1, 16, 2), timeout_ms: boundedInt(req.body.gate_timeout_ms, 1000, 5000, 3000), high_confidence_threshold: boundedNumber(req.body.gate_high_confidence_threshold, 0.5, 0.99, 0.88), main_llm_threshold: boundedNumber(req.body.gate_main_llm_threshold, 0.1, 0.95, 0.72), predefined_enabled: req.body.gate_predefined_enabled === "on", cache_ttl_seconds: boundedInt(req.body.gate_cache_ttl_seconds, 30, 604800, 3600), repeat_force_window_seconds: boundedInt(req.body.gate_repeat_force_window_seconds, 0, 3600, 90), similarity_threshold: boundedNumber(req.body.gate_similarity_threshold, 0.5, 1, 0.86), force_prefix: cleanText(req.body.gate_force_prefix, 40) }, support_scope: normalizeScope({ allowed_topics: cleanText(req.body.allowed_topics, 3000), allowed_support_domains: cleanText(req.body.allowed_support_domains, 3000), answer_style: cleanText(req.body.answer_style, 2000), linking_behavior: cleanText(req.body.linking_behavior, 2000), repo_lookup_enabled: req.body.repo_lookup_enabled === "on", allow_deterministic_help_shortcuts: req.body.allow_deterministic_help_shortcuts === "on", allow_moderator_code_help: req.body.allow_moderator_code_help === "on", clarification_behavior: cleanText(req.body.clarification_behavior, 2000), max_answer_length: boundedInt(req.body.maximum_answer_length, 100, 4000, 4000), role_overrides: { admin: cleanText(req.body.scope_admin, 3000), mod: cleanText(req.body.scope_mod, 3000), user: cleanText(req.body.scope_user, 3000) } }), instructions: { out_of_scope_response: cleanText(req.body.out_of_scope_response, 1000), roleplay_intensity: boundedInt(req.body.roleplay_intensity, 0, 10, 0), community_tone: cleanText(req.body.community_tone, 2000), admin_custom: cleanText(req.body.admin_custom, 6000) }, logging: { log_prompts: req.body.log_prompts === "on", log_responses: req.body.log_responses === "on", log_tool_calls: req.body.log_tool_calls === "on", log_metrics: req.body.log_metrics === "on", log_internal_audit: req.body.log_internal_audit === "on" }, improvement: { ...config.improvement, allow_moderators_to_review_responses: req.body.allow_moderators_to_review_responses === "on", trusted_moderator_reviewers: parseIdList(req.body.trusted_moderator_reviewers), corrections_enabled: req.body.corrections_enabled === "on" } }; } catch (error) { return flash(req, res, "error", error.message); } config = saveConfig(nextConfig); registerAssistantCommands({ commandRouter, provider, runtime, getConfig: () => config, accessControl, rateLimiter, metrics, getBaseUrl: () => configuredBaseUrl(settings) }); writeCommandsManifest(plugin?.dir || __dirname, config); const runtimeSettingsChanged = [ "selected_model_id", "context_size", "threads", "batch_size", "ubatch_size", "gpu_allocation_intent_percent" ].some((key) => previousConfig[key] !== config[key]); const gateSettingsChanged = JSON.stringify(previousConfig.gate) !== JSON.stringify(config.gate); const enabledChanged = previousConfig.enabled !== config.enabled; if ((runtimeSettingsChanged || gateSettingsChanged || enabledChanged) && runtime.status().state === "running") { try { await restartRuntimes(); } catch (error) { return flash(req, res, "error", `Settings saved, but runtime restart failed: ${error.message}`); } } else if (!config.enabled && gateRuntime.status().state === "running") { await gateRuntime.stop(); } return flash(req, res, "success", "Lumi AI settings saved."); }); router.post("/download/runtime", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); const wantsJson = req.accepts(["json", "html"]) === "json"; const hardware = detectHardware(modelManifest.models, runtimeManifest); const target = getRuntimeTarget(hardware); if (!target) { if (wantsJson) return res.status(400).json({ error: "No managed llama.cpp runtime is available for this platform." }); return flash(req, res, "error", "No managed llama.cpp runtime is available for this platform."); } try { const job = downloads.start({ id: "runtime", ...target, kind: "runtime", archive: true, runtimeMetadata: { backend: target.backend || "cpu", version: runtimeManifest.version, target: target.filename } }); if (wantsJson) return res.json({ success: true, job }); return flash(req, res, "success", `${String(target.backend || "CPU").toUpperCase()} runtime download started.`); } catch (error) { if (wantsJson) return res.status(400).json({ error: error.message }); return flash(req, res, "error", error.message); } }); router.post("/download/model/:id", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); const wantsJson = req.accepts(["json", "html"]) === "json"; const model = getModel(req.params.id); if (!model) { if (wantsJson) return res.status(404).json({ error: "Unknown model." }); return flash(req, res, "error", "Unknown model."); } if ( (model.id === config.selected_model_id && runtime.status().state === "running") || (model.id === config.gate.model_id && gateRuntime.status().state === "running") ) { if (wantsJson) return res.status(400).json({ error: "Stop the AI runtimes before replacing an active model." }); return flash(req, res, "error", "Stop the AI runtimes before replacing an active model."); } const hardware = detectHardware(modelManifest.models); const incompatible = model.ram_gb * 1024 > hardware.total_ram_mb || model.size / 1048576 > hardware.free_disk_mb; if (incompatible && req.body.override_compatibility !== "on") { if (wantsJson) return res.status(400).json({ error: "This model exceeds detected RAM or free disk. Check override to download anyway." }); return flash(req, res, "error", "This model exceeds detected RAM or free disk. Check override to download anyway."); } try { const job = downloads.start({ id: `model:${model.id}`, url: `https://huggingface.co/${model.repo}/resolve/${model.revision}/${model.filename}`, filename: model.filename, sha256: model.sha256, size: model.size, kind: "model" }); if (wantsJson) return res.json({ success: true, job }); return flash(req, res, "success", `${model.label} download started.`); } catch (error) { if (wantsJson) return res.status(400).json({ error: error.message }); return flash(req, res, "error", error.message); } }); router.get("/api/status", async (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason }); res.json({ runtime: await runtime.health(), gate: await gateRuntime.health(), queue_length: queue.length, enabled: config.enabled, model_id: config.selected_model_id }); }); router.get("/api/downloads", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); res.json(Object.fromEntries(downloads.jobs)); }); router.get("/api/gpu-capacity", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const model = getModel(req.query.model_id || config.selected_model_id); if (!model) return res.status(400).json({ error: "Unknown model." }); const contextSize = boundedInt(req.query.context_size, 512, 131072, config.context_size); const hardware = detectHardware(modelManifest.models, runtimeManifest); const installedBackend = runtime.runtimeMetadata().backend; res.json(estimateAllocation({ model, contextSize, gpu: hardware.gpu, backend: installedBackend, intentPercent: req.query.intent_percent ?? config.gpu_allocation_intent_percent, managedUsageMb: runtime.activeAcceleration?.estimated_gpu_memory_mb || 0 })); }); router.get("/api/assistant-diagnostics", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const availability = evaluateAssistantAvailability({ user: req.session.user, config, model: getModel(config.selected_model_id), runtimeHealth: await runtime.health(), providerAvailable: Boolean(provider) }); res.json({ ...availability, reason: describeAssistantReason(availability.reason_code) }); }); const visibilityDebugHandler = async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const runtimeHealth = await runtime.health(); const diagnostics = buildVisibilityDiagnostics({ user: req.session.user, config, model: getModel(config.selected_model_id), runtimeHealth, providerAvailable: Boolean(provider), frontend: frontendVisibility.get(req.session.user.id) }); res.set("Cache-Control", "no-store"); return res.json({ ...diagnostics, reason: describeAssistantReason(diagnostics.reason_code), frontend_reported: frontendVisibility.has(req.session.user.id), ...panelDiagnostics.snapshot() }); }; const visibilityReportHandler = (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); frontendVisibility.set(req.session.user.id, { assistant_slot_found: Boolean(req.body.assistant_slot_found), frontend_loader_loaded: Boolean(req.body.frontend_loader_loaded), panel_html_returned: Boolean(req.body.panel_html_returned), mount_successful: Boolean(req.body.mount_successful), reported_at: new Date().toISOString() }); panelDiagnostics.frontend(req.body); return res.json({ success: true }); }; router.get("/api/assistant/visibility-debug", visibilityDebugHandler); router.post("/api/assistant/visibility-debug", visibilityReportHandler); if (typeof web.addRoute === "function") { web.addRoute("get", "/api/lumi-ai/assistant/visibility-debug", visibilityDebugHandler); web.addRoute("post", "/api/lumi-ai/assistant/visibility-debug", visibilityReportHandler); } router.post("/repo-index/refresh", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { const index = req.body.source === "public" ? repoIndexer.refreshPublicIndex() : repoIndexer.refreshIndex(); return flash(req, res, "success", `Repository support index refreshed with ${index.routes.length} routes.`); } catch (error) { return flash(req, res, "error", `Repository index refresh failed: ${error.message}`); } }); router.post("/access-control", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { accessControl.set(cleanText(req.body.user_id, 200), { action: req.body.action, timeoutUntil: req.body.timeout_until, reason: cleanText(req.body.reason, 500), silent: req.body.silent === "on", actorId: req.session.user.id }); return flash(req, res, "success", "AI user access updated."); } catch (error) { return flash(req, res, "error", error.message); } }); router.get("/api/users/search", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const query = cleanText(req.query.q, 120); if (query.length < 2) return res.json({ users: [] }); return res.json({ users: searchKnownUsers(db, query) }); }); router.post("/models/:id/delete", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { const result = storage.deleteModel(getModel(req.params.id), { selectedModelId: config.selected_model_id, gateModelId: config.gate.model_id, runtimeRunning: runtime.status().state === "running", gateRuntimeRunning: gateRuntime.status().state === "running", confirmed: req.body.confirm === "yes" }); return flash(req, res, "success", result.deleted ? `Model deleted. Recovered ${formatBytes(result.bytes_recovered)}.` : "Model was not installed."); } catch (error) { return flash(req, res, "error", error.message); } }); router.post("/models/:id/verify", async (req, res) => { if (!req.session.user?.isAdmin) return denied(res); const result = await runtime.verifyModel(req.params.id); return flash(req, res, result.success ? "success" : "error", result.success ? "Model verification passed." : result.message); }); router.post("/storage/cleanup", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { const categories = Array.isArray(req.body.categories) ? req.body.categories : req.body.categories ? [req.body.categories] : []; const activeDownload = [...downloads.jobs.values()].some((job) => !["complete", "error"].includes(job.state)); if (activeDownload && categories.some((category) => ["tmp", "runtime_archives"].includes(category))) { throw new Error("Wait for active downloads before cleaning temporary files or runtime archives."); } const result = storage.cleanupStorage(categories, { models: modelManifest.models, selectedModelId: config.selected_model_id, gateModelId: config.gate.model_id, runtimeRunning: runtime.status().state === "running" || gateRuntime.status().state === "running", activeLogPath: runtime.activeLogPath }); return flash(req, res, "success", `Storage cleanup recovered ${formatBytes(result.recovered_bytes)}.`); } catch (error) { return flash(req, res, "error", error.message); } }); router.get("/logs/:name", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { const log = storage.readLogTail(req.params.name); return res.render(path.join(__dirname, "views", "log-viewer.ejs"), { title: `Lumi AI log - ${log.name}`, log, formatBytes, formatDate }); } catch (error) { return res.status(404).render("error", { title: "Log unavailable", message: error.message }); } }); router.get("/logs/:name/download", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.download(storage.resolveLog(req.params.name)); } catch (error) { return res.status(404).json({ error: error.message }); } }); router.post("/logs/:name/delete", (req, res) => { if (!req.session.user?.isAdmin) return denied(res); try { const result = storage.deleteLog(req.params.name, runtime.activeLogPath); return flash(req, res, "success", `Log deleted. Recovered ${formatBytes(result.bytes_recovered)}.`); } catch (error) { return flash(req, res, "error", error.message); } }); router.post("/runtime/:action", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { const action = req.params.action; if (!["start", "stop", "restart", "self-test", "verify-runtime", "verify-model", "verify-gate-model"].includes(action)) throw new Error("Unknown runtime action."); const result = action === "self-test" ? await runtime.selfTest() : action === "verify-runtime" ? runtime.verifyRuntimeInstallation() : action === "verify-model" ? await runtime.verifyModel() : action === "verify-gate-model" ? await gateRuntime.verifyModel() : action === "stop" ? await stopRuntimes({ manual: true, reason: "admin_stop" }) : action === "restart" ? await restartRuntimes() : await startRuntimes(); if (result?.success === false) return res.status(400).json({ error: result.message, diagnostic: result }); res.json(result); } catch (error) { res.status(400).json({ error: error.message }); } }); router.get("/diagnostics/download", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const file = createDiagnosticsBundle({ config, runtimeState: getRuntimeState(), manifest: { runtime: runtimeManifest, model: getModel(config.selected_model_id), gate_model: getModel(config.gate.model_id) }, metrics: metrics.report() }); return res.download(file); }); router.post("/assistant/message", async (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) { return res.status(403).json({ error: "Lumi AI is unavailable for this account.", reason: permission.reason, permission: permission.debug_details }); } const message = cleanText(req.body.message, 6000); if (!message) return res.status(400).json({ error: "Message is required." }); const originContext = webOriginContext(req); const access = authorizeAiRequest({ userId: req.session.user.id, context: originContext, accessControl, rateLimiter }); if (!access.allowed) return res.status(429).json({ error: access.message, reason: access.reason, retry_after_seconds: access.retry_after_seconds }); const requestUser = { ...req.session.user }; const requestRole = roleOf(requestUser); const requestConfig = config; const requestSessionId = req.sessionID; const history = normalizeConversationHistory(req.body.history); const requestStarted = Date.now(); const job = requestJobs.create({ userId: requestUser.id, metadata: { context_size: requestConfig.context_size, batch_size: requestConfig.batch_size, ubatch_size: requestConfig.ubatch_size, threads: requestConfig.threads }, execute: async (updateStage, signal) => { try { const result = await provider.generate({ message, user: requestUser, sessionId: requestSessionId, originContext, allowDeterministicShortcut: requestConfig.support_scope.allow_deterministic_help_shortcuts, history, signal, onStage: updateStage }); updateStage("formatting", { route: result.route_used, ...(result.diagnostics || {}) }); const delivered = finalizeAssistantResult(result, { role: requestRole, config: requestConfig, baseUrl: originContext.base_url, maxLength: originContext.max_message_length, requestMessage: message }); metrics.record({ kind: "delivery", status: "success", scope: "webui_chat", route_used: result.route_used || "llm", route_class: result.route_class, max_output_tokens_used: result.max_output_tokens_used, role: requestRole, user_id: requestUser.id, internal_generated_length: result.internal_generated_length || String(result.text || "").length, final_reply_length: delivered.original_final_length, original_final_length: delivered.original_final_length, delivered_length: delivered.delivered_length, ...(result.stage_timings || {}) }); return { ...delivered, diagnostics: result.diagnostics || null, feedback_context: { user_message: message, assistant_answer: delivered.text, route_used: result.route_used || "main_llm", role: requestRole, origin: originContext.origin, platform: originContext.platform, model: result.model_id || requestConfig.selected_model_id, timestamp: new Date().toISOString() } }; } catch (error) { metrics.record({ kind: "request", status: "failed", user_id: requestUser.id, role: requestRole, message: error.message, timeout: error.name === "TimeoutError" || error.code === "HARD_GENERATION_TIMEOUT", cancelled: error.code === "REQUEST_CANCELLED", total_ms: Date.now() - requestStarted, duration_ms: Date.now() - requestStarted }); if (error.code === "QUEUE_FULL" && !error.retry_after_seconds) error.retry_after_seconds = 5; throw error; } } }); return res.status(202).json({ job_id: job.id, state: job.state, stage: job.stage, status_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}`, cancel_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}/cancel`, soft_timeout_url: `/plugins/${PLUGIN_ID}/assistant/jobs/${job.id}/soft-timeout`, ui_soft_timeout_ms: requestConfig.ui_soft_timeout_ms }); }); router.post("/assistant/feedback", (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied." }); try { const entry = feedbackStore.capture({ user_message: req.body.user_message, assistant_answer: req.body.assistant_answer, route_used: req.body.route_used, role: roleOf(req.session.user), origin: "webui", platform: "webui", model: req.body.model, timestamp: req.body.timestamp, feedback_tag: req.body.feedback_tag, feedback_kind: req.body.feedback_kind, optional_correction: req.body.optional_correction }, req.session.user); return res.status(201).json({ success: true, id: entry.id }); } catch (error) { return res.status(400).json({ error: error.message }); } }); router.get("/assistant/jobs/:id", (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied." }); const job = requestJobs.get(req.params.id, req.session.user.id); if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." }); return res.json(job); }); router.post("/assistant/jobs/:id/cancel", (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied." }); const job = requestJobs.cancel(req.params.id, req.session.user.id, { admin: Boolean(req.session.user?.isAdmin) }); if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." }); metrics.record({ kind: "request_job", status: "cancelled", job_id: job.id, user_id: req.session.user.id, stage: job.stage, elapsed_ms: job.elapsed_ms }); return res.json(job); }); router.post("/assistant/jobs/:id/soft-timeout", (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied." }); const job = requestJobs.markSoftTimeout(req.params.id, req.session.user.id); if (!job) return res.status(404).json({ error: "Assistant request was not found or expired." }); metrics.record({ kind: "request_job", status: "soft_timeout", job_id: job.id, user_id: req.session.user.id, stage: job.stage, elapsed_ms: job.elapsed_ms, still_running: job.still_running }); return res.json(job); }); router.post("/assistant/test", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const message = cleanText(req.body.message, 6000); if (!message) return res.status(400).json({ error: "Message is required." }); const simulatedRole = ["admin", "mod", "user"].includes(req.body.role) ? req.body.role : "admin"; const simulatedUser = { id: req.session.user.id, username: req.session.user.username, isAdmin: simulatedRole === "admin", isMod: simulatedRole === "mod" }; const allowTools = req.body.allow_tools === true; const testOrigin = ["webui", "discord", "twitch", "youtube", "kick", "other"].includes(req.body.origin) ? req.body.origin : "webui"; try { const result = await provider.test({ message, user: simulatedUser, includeRaw: Boolean(req.body.show_raw_output), allowTools, originContext: diagnosticOriginContext(simulatedUser, testOrigin) }); if (!req.body.show_raw_prompt) delete result.raw_prompt; res.json(result); } catch (error) { res.status(503).json({ error: error.message }); } }); router.post("/assistant/confirm", async (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason }); try { res.json({ success: true, result: await tools.confirm({ id: req.body.id, user: req.session.user, sessionId: req.sessionID }) }); } catch (error) { res.status(400).json({ error: error.message }); } }); router.post("/assistant/cancel", (req, res) => { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).json({ error: "Access denied.", reason: permission.reason }); const cancelled = tools.cancel(req.body.id, req.session.user.id); metrics.record({ kind: "tool", status: cancelled ? "cancelled" : "failed", user_id: req.session.user.id }); res.json({ success: cancelled }); }); router.get("/api/tools", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.json(await toolManager.list({ force: req.query.refresh === "1" })); } catch (error) { return res.status(503).json({ error: error.message }); } }); router.get("/api/tools-diagnostics", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); const role = ["admin", "mod", "user"].includes(req.query.role) ? req.query.role : "admin"; const origin = ["webui", "discord", "twitch", "youtube", "kick", "other"].includes(req.query.origin) ? req.query.origin : "webui"; const simulatedUser = { id: req.session.user.id, username: req.session.user.username, isAdmin: role === "admin", isMod: role === "mod" }; try { const diagnostics = await toolManager.diagnostics({ role, user: simulatedUser, context: diagnosticOriginContext(simulatedUser, origin) }); return res.json({ ...diagnostics, prompt_preview: buildAllowedToolsSection(diagnostics.prompt_tools) }); } catch (error) { return res.status(503).json({ error: error.message }); } }); router.get("/api/tools/:id/readme", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.json(await toolManager.readme(req.params.id)); } catch (error) { return res.status(404).json({ error: error.message }); } }); router.get("/api/tools/:id/settings", (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.json(toolManager.settingsFor(req.params.id)); } catch (error) { return res.status(404).json({ error: error.message }); } }); router.post("/api/tools/:id/settings", express.json({ limit: "64kb" }), async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.json(await toolManager.saveSettings(req.params.id, req.body?.values || {})); } catch (error) { return res.status(400).json({ error: error.message }); } }); router.post("/api/tools/:id/settings/reset", async (req, res) => { if (!req.session.user?.isAdmin) return res.status(403).json({ error: "Access denied." }); try { return res.json(await toolManager.resetSettings(req.params.id)); } catch (error) { return res.status(400).json({ error: error.message }); } }); router.get("/tools/:id/assets/*", (req, res) => { if (!req.session.user?.isAdmin) { const permission = canUseAssistant({ user: req.session.user, config, origin: "webui", platform: "webui", requestedSurface: "webui_chat" }); if (!permission.allowed) return res.status(403).end(); } const file = toolManager.resolveAsset(req.params.id, req.params[0], { allowInstalled: req.session.user?.isAdmin === true }); return file ? res.sendFile(file) : res.status(404).end(); }); router.post("/tools/:id/enable", async (req, res) => { if (!req.session.user?.isAdmin) return toolDenied(req, res); try { const result = await toolManager.enable(req.params.id); return toolActionResponse(req, res, "AI tool enabled.", result); } catch (error) { return toolActionError(req, res, error); } }); router.post("/tools/:id/disable", async (req, res) => { if (!req.session.user?.isAdmin) return toolDenied(req, res); try { const result = await toolManager.disable(req.params.id); return toolActionResponse(req, res, "AI tool disabled.", result); } catch (error) { return toolActionError(req, res, error); } }); router.post("/tools/:id/update", async (req, res) => { if (!req.session.user?.isAdmin) return toolDenied(req, res); try { const result = await toolManager.update(req.params.id); return toolActionResponse(req, res, "AI tool updated.", result); } catch (error) { return toolActionError(req, res, error); } }); router.post("/tools/:id/delete", async (req, res) => { if (!req.session.user?.isAdmin) return toolDenied(req, res); try { const result = await toolManager.delete(req.params.id); return toolActionResponse(req, res, "AI tool deleted.", result); } catch (error) { return toolActionError(req, res, error); } }); router.get("/improvement_center", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.allowed) return deniedImprovement(res); return res.render(path.join(__dirname, "views", "improvement-center.ejs"), { title: "Lumi AI Improvement Center", config, access, feedbackTags: FEEDBACK_TAGS, feedbackKinds: FEEDBACK_KINDS, promotionTargets: PROMOTION_TARGETS, reviews: feedbackStore.list({ page: req.query.review_page, pageSize: 15, status: cleanText(req.query.status, 30) }), corrections: correctionStore.list({ page: req.query.correction_page, pageSize: 15 }), evalCases: evalStore.list({ page: req.query.eval_page, pageSize: 15 }), evalResults: evalStore.results(25), formatDate }); }); router.post("/improvement_center/settings", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_approve) return deniedImprovement(res); config = saveConfig({ ...config, improvement: { ...config.improvement, allow_moderators_to_review_responses: req.body.allow_moderators_to_review_responses === "on", trusted_moderator_reviewers: parseIdList(req.body.trusted_moderator_reviewers), corrections_enabled: req.body.corrections_enabled === "on" } }); ensureSidebarNavItem(settings); return improvementFlash(req, res, "success", "Improvement Center settings saved."); }); router.post("/improvement_center/reviews/:id", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.allowed) return deniedImprovement(res); try { const action = cleanText(req.body.action, 30); if (action === "flag" && access.can_flag) { feedbackStore.setStatus(req.params.id, "flagged", req.session.user, req.body.review_notes); } else if (action === "verify" && access.can_verify) { feedbackStore.verify(req.params.id, req.session.user, req.body.review_notes); } else if (action === "approve" && access.can_approve) { feedbackStore.setStatus(req.params.id, "approved", req.session.user, req.body.review_notes); } else if (action === "reject" && access.can_approve) { feedbackStore.setStatus(req.params.id, "rejected", req.session.user, req.body.review_notes); } else if (action === "edit" && access.can_edit) { feedbackStore.edit(req.params.id, req.body, req.session.user); } else if (action === "export" && access.can_export) { feedbackStore.markExportApproved(req.params.id, req.session.user); } else if (action === "delete" && access.can_delete) { feedbackStore.delete(req.params.id); } else { return deniedImprovement(res); } return improvementFlash(req, res, "success", `Review ${action} completed.`); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); router.post("/improvement_center/reviews/:id/implement", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_implement) return deniedImprovement(res); try { const review = feedbackStore.get(req.params.id); if (!review || review.status !== "approved") throw new Error("Approve the review before implementing it."); const target = PROMOTION_TARGETS.includes(req.body.target) ? req.body.target : "correction"; if (target === "eval_case") { evalStore.add({ prompt: review.user_message, role: req.body.min_role || review.role, origin: req.body.permission_origin || review.origin, expected_behavior: req.body.corrected_answer || review.optional_correction, forbidden_behavior: req.body.forbidden_behavior, expected_link: req.body.expected_link, notes: req.body.notes }, req.session.user); } else if (target === "training_export") { feedbackStore.markExportApproved(review.id, req.session.user); } else { correctionStore.createFromFeedback(review, { ...req.body, target, explicitly_safe: req.body.explicitly_safe === "on", enabled: req.body.enabled === "on" }, req.session.user); } return improvementFlash(req, res, "success", "Approved feedback was promoted. Save Corrections before it becomes active."); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); router.post("/improvement_center/corrections/save", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_implement) return deniedImprovement(res); const result = correctionStore.saveCorrections(req.session.user); return improvementFlash(req, res, "success", `Corrections saved. ${result.active} of ${result.total} are active.`); }); router.post("/improvement_center/corrections/:id", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.allowed) return deniedImprovement(res); try { const action = cleanText(req.body.action, 30); if (action === "verify" && access.can_verify) { correctionStore.verify(req.params.id, req.session.user); } else if (action === "edit" && access.can_edit) { if (req.body.expected_link && !isVerifiedImprovementLink(req.body.expected_link)) { throw new Error("Internal correction links must match a verified Lumi route."); } correctionStore.update(req.params.id, { ...req.body, explicitly_safe: req.body.explicitly_safe === "on", enabled: req.body.enabled === "on" }); } else if (action === "toggle" && access.can_edit) { correctionStore.setEnabled(req.params.id, req.body.enabled === "on"); } else if (action === "delete" && access.can_delete) { correctionStore.delete(req.params.id); } else { return deniedImprovement(res); } return improvementFlash(req, res, "success", `Correction ${action} completed. Save Corrections to activate changes.`); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); router.post("/improvement_center/evals", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_run_evals) return deniedImprovement(res); try { if (req.body.expected_link && !isVerifiedImprovementLink(req.body.expected_link)) { throw new Error("Expected links must match a verified Lumi route."); } evalStore.add(req.body, req.session.user); return improvementFlash(req, res, "success", "Eval case added."); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); router.post("/improvement_center/evals/:id/delete", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_run_evals) return deniedImprovement(res); evalStore.delete(req.params.id); return improvementFlash(req, res, "success", "Eval case deleted."); }); router.post("/improvement_center/evals/run", async (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_run_evals) return deniedImprovement(res); try { const results = await evalStore.runAll({ provider, actor: req.session.user }); return improvementFlash(req, res, "success", `Eval run completed with ${results.length} result(s).`); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); router.post("/improvement_center/exports/:format", (req, res) => { const access = improvementAccess(req.session.user, config); if (!access.can_export) return deniedImprovement(res); try { const output = trainingExporter.export(req.params.format); return res.download(output.file, output.filename); } catch (error) { return improvementFlash(req, res, "error", error.message); } }); web.mount(`/plugins/${PLUGIN_ID}`, router, { label: "Lumi AI", role: "admin", section: "plugins" }); web.addNavItem({ label: "AI Improvement Center", path: `/plugins/${PLUGIN_ID}/improvement_center`, role: "mod", section: "moderation", canAccess: (user) => improvementAccess(user, config).allowed }); let removeAssistantPanel = () => {}; if (typeof web.addAssistantPanel === "function") { removeAssistantPanel = web.addAssistantPanel({ id: PLUGIN_ID, version: require("./plugin.json").version, canAccess: (user) => canUseAssistant({ user, config, origin: "webui", platform: "webui", requestedSurface: "webui_panel" }), getAvailability: async (user) => evaluateAssistantAvailability({ user, config, model: getModel(config.selected_model_id), runtimeHealth: await runtime.health(), providerAvailable: Boolean(provider), origin: "webui", platform: "webui", requestedSurface: "webui_panel" }), view: path.join(__dirname, "views", "assistant-panel.ejs"), requiredLocals: ["endpoint", "user"], onRenderDiagnostic: (report) => { panelDiagnostics.update(report); if (report.panel_html_error) panelDiagnostics.log("render_failure", report); }, stylesheet: `/plugins/${PLUGIN_ID}/assets/assistant.css`, script: `/plugins/${PLUGIN_ID}/assets/assistant.js`, getDebug: () => ({ enabled: Boolean(config.assistant_debug_logging), ...panelDiagnostics.snapshot() }), locals: { endpoint: `/plugins/${PLUGIN_ID}` } }); } else { console.warn("Lumi AI assistant panel hook is unavailable; settings remain accessible."); } ensureSidebarNavItem(settings); registerAssistantCommands({ commandRouter, provider, runtime, getConfig: () => config, accessControl, rateLimiter, metrics, getBaseUrl: () => configuredBaseUrl(settings) }); writeCommandsManifest(plugin?.dir || __dirname, config); setImmediate(() => toolManager.loadEnabled().catch((error) => console.error("Lumi AI tool loader failed", error) )); if (config.enabled) { setImmediate(() => ensureGateRuntime().catch((error) => console.error("Lumi AI gate runtime start failed", error) )); } const state = getRuntimeState(); if (shouldAutoResume(config, state)) { setImmediate(() => startRuntimes({ resume: true }).catch((error) => console.error("Lumi AI runtime resume failed", error))); } return async () => { clearInterval(gateMonitor); removeAssistantPanel(); commandRouter?.clearCommands?.(PLUGIN_ID); await toolManager.stopAll(); await stopRuntimes({ manual: false, reason: "bot_shutdown" }); if (global.lumiFrameworks?.ai === api) delete global.lumiFrameworks.ai; if (global.lumiFrameworks?.lumi_ai === api) delete global.lumiFrameworks.lumi_ai; }; } }; function getRuntimeTarget(hardware = detectHardware(modelManifest.models, runtimeManifest)) { const selection = hardware.runtime_selection; return selection?.target ? { ...selection.target, backend: selection.backend } : null; } function normalizeContext(value) { if (Array.isArray(value)) return value.filter((item) => typeof item === "string"); return typeof value === "string" ? [value] : []; } function boundedInt(value, min, max, fallback) { const number = Number.parseInt(value, 10); return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback; } function boundedNumber(value, min, max, fallback) { const number = Number(value); return Number.isFinite(number) ? Math.min(max, Math.max(min, number)) : fallback; } function cleanText(value, max) { return String(value || "").trim().slice(0, max); } function flash(req, res, type, message) { req.session.flash = { type, message }; return res.redirect(`/plugins/${PLUGIN_ID}`); } function toolActionResponse(req, res, message, result = {}) { if (req.accepts(["json", "html"]) === "json") return res.json({ success: true, message, result }); req.session.flash = { type: "success", message }; return res.redirect(`/plugins/${PLUGIN_ID}`); } function toolActionError(req, res, error) { if (req.accepts(["json", "html"]) === "json") return res.status(400).json({ error: error.message }); req.session.flash = { type: "error", message: error.message }; return res.redirect(`/plugins/${PLUGIN_ID}`); } function toolDenied(req, res) { if (req.accepts(["json", "html"]) === "json") return res.status(403).json({ error: "Access denied." }); return denied(res); } function improvementFlash(req, res, type, message) { req.session.flash = { type, message }; return res.redirect(`/plugins/${PLUGIN_ID}/improvement_center`); } function denied(res) { return res.status(403).render("error", { title: "Access denied", message: "Administrator access is required." }); } function deniedImprovement(res) { return res.status(403).render("error", { title: "Access denied", message: "Improvement Center access is not enabled for this account." }); } function parseIdList(value) { return [...new Set(String(value || "") .split(/[\s,;]+/) .map((entry) => entry.trim()) .filter(Boolean))] .slice(0, 250); } function isVerifiedImprovementLink(value) { const cleaned = cleanText(value, 2000).replace(/^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s+/i, ""); if (!cleaned) return true; let pathname; try { const url = new URL(cleaned, "https://lumi.invalid"); if (!["http:", "https:"].includes(url.protocol)) return false; pathname = url.pathname; } catch { return false; } return repoIndexer.verifiedRoutePaths().includes(pathname); } function formatDuration(ms) { if (!ms) return "0 ms"; return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`; } function formatDate(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? "-" : date.toLocaleString(); } function describeAssistantReason(code) { return ({ anonymous: "User is not logged in.", feature_disabled: "AI Assistant is disabled.", role_forbidden: "The current role is not enabled for the assistant.", model_not_selected: "No model is selected.", model_missing: "The selected model is not installed.", runtime_missing: "The managed runtime is not installed.", runtime_unusable: "The runtime self-test has not passed.", runtime_stopped: "The runtime is stopped.", runtime_starting: "The runtime is starting but not ready.", runtime_error: "The runtime is in an error state.", runtime_unhealthy: "The runtime health check failed.", assistant_disabled: "The sidebar assistant is disabled.", provider_unavailable: "The AI provider is unavailable." })[code] || (code ? `Assistant unavailable: ${code}.` : "Assistant is available."); } function ensureSidebarNavItem(settings) { if (!settings?.getSetting || !settings?.setSetting) return; const raw = settings.getSetting("nav_structure", null); if (!raw) return; let structure = raw; if (typeof structure === "string") { try { structure = JSON.parse(structure); } catch { return; } } if (!structure?.enabled || !Array.isArray(structure.sections)) return; const navId = "plugins_lumi_ai"; const improvementNavId = "plugins_lumi_ai_improvement_center"; for (const section of structure.sections) { if (Array.isArray(section.items)) { section.items = section.items.filter((item) => ![navId, improvementNavId].includes(item)); } } let plugins = structure.sections.find((section) => section.id === "plugins"); if (!plugins) { plugins = { id: "plugins", label: "Plugins", icon: "blocks", items: [] }; structure.sections.push(plugins); } plugins.items = Array.isArray(plugins.items) ? plugins.items : []; plugins.items.push(navId); let moderation = structure.sections.find((section) => section.id === "moderation"); if (!moderation) { moderation = { id: "moderation", label: "Mod", icon: "shield", items: [] }; structure.sections.push(moderation); } moderation.items = Array.isArray(moderation.items) ? moderation.items : []; moderation.items.push(improvementNavId); settings.setSetting("nav_structure", structure); } function shouldAutoResume(config, state) { return Boolean(config.enabled && state.desired_state === "running" && !state.last_manual_stop && !state.last_crashed); } function registerAssistantCommands({ commandRouter, provider, runtime, getConfig, accessControl, rateLimiter, metrics: commandMetrics = metrics, getBaseUrl = () => "" }) { if (!commandRouter) return; const config = getConfig(); if (!config.commands.enabled) { commandRouter.registerCommands(PLUGIN_ID, []); return; } const platforms = Object.entries(config.commands.platforms) .filter(([, enabled]) => enabled) .map(([platform]) => platform); commandRouter.registerCommands(PLUGIN_ID, [{ id: "lumi_ai:assistant", triggers: config.commands.triggers, platforms, handler: async (ctx) => { const current = getConfig(); const originContext = buildOriginContext(ctx, ctx.trigger); originContext.base_url = getBaseUrl(); const started = Date.now(); const user = { id: originContext.user_id, username: originContext.username, isAdmin: originContext.is_admin, isMod: originContext.is_mod }; const permission = canUseAssistant({ user, config: current, origin: originContext.origin, platform: originContext.platform, requestedSurface: "command", roleHint: originContext.role, roleSource: "command_origin" }); if (!permission.allowed) { commandMetrics.record({ kind: "request", status: "refused", scope: "platform_command", route_used: "denial", role: permission.normalized_role, user_id: originContext.user_id, platform: originContext.platform, reason: permission.reason, duration_ms: Date.now() - started }); if (permission.reason === "command_disabled" || permission.reason === "platform_forbidden") return false; await ctx.reply(formatPlatformReply(current.commands.denied_message, [], originContext)); return true; } if (!ctx.argsText.trim()) { await ctx.reply(`Usage: !${ctx.trigger} `); return true; } const access = authorizeAiRequest({ userId: originContext.user_id, context: originContext, accessControl, rateLimiter }); if (!access.allowed) { commandMetrics.record({ kind: "request", status: "refused", scope: "platform_command", route_used: "denial", role: permission.normalized_role, user_id: originContext.user_id, platform: originContext.platform, reason: access.reason, duration_ms: Date.now() - started }); if (!access.silent) { const message = access.reason === "rate_limited" ? `Lumi Assistant is rate limited. Try again in ${access.retry_after_seconds}s.` : current.commands.denied_message; await ctx.reply(formatPlatformReply(message, [], originContext)); } return true; } const health = await runtime.health(); if (!current.enabled || health.state !== "running" || !health.healthy) { commandMetrics.record({ kind: "request", status: "failed", scope: "platform_command", route_used: "unavailable_fallback", role: permission.normalized_role, user_id: originContext.user_id, platform: originContext.platform, duration_ms: Date.now() - started }); await ctx.reply(formatPlatformReply(current.commands.unavailable_message, [], originContext)); return true; } try { const result = await provider.generate({ message: ctx.argsText.trim(), user, sessionId: `command:${originContext.platform}:${originContext.message_id || Date.now()}`, scope: "platform_command", originContext, allowDeterministicShortcut: current.support_scope?.allow_deterministic_help_shortcuts === true }); const delivered = finalizeAssistantResult(result, { role: permission.normalized_role, config: current, baseUrl: originContext.base_url, maxLength: originContext.max_message_length, requestMessage: ctx.argsText.trim() }); const reply = formatPlatformReplyDetails(delivered.text, delivered.links, originContext); await ctx.reply(reply.text); commandMetrics.record({ kind: "command_audit", status: "success", scope: "platform_command", route_used: result.route_used || "llm", role: permission.normalized_role, user_id: originContext.user_id, platform: originContext.platform, duration_ms: Date.now() - started, original_final_length: delivered.original_final_length, final_reply_length: reply.original_final_length, delivered_length: reply.delivered_length, internal_generated_length: result.internal_generated_length || String(result.text || "").length }); } catch (error) { commandMetrics.record({ kind: "request", status: "failed", scope: "platform_command", route_used: "unavailable_fallback", role: permission.normalized_role, user_id: originContext.user_id, platform: originContext.platform, message: error.message, duration_ms: Date.now() - started }); await ctx.reply(formatPlatformReply(current.commands.unavailable_message, [], originContext)); } return true; } }]); } function authorizeAiRequest({ userId, context, accessControl, rateLimiter }) { const access = accessControl.check(userId, context); if (!access.allowed) return access; const rate = rateLimiter.check(context); if (!rate.allowed) { return { ...rate, message: `Lumi Assistant is rate limited. Try again in ${rate.retry_after_seconds}s.` }; } return { allowed: true }; } function webOriginContext(req) { const user = req.session.user; const role = roleOf(user); return { origin: "webui", platform: "webui", channel_id: null, channel_name: null, server_id: null, user_id: user.id, username: user.username, display_name: user.username, role, is_admin: role === "admin", is_mod: role === "mod", message_id: null, reply_mode: "panel", format_capabilities: { markdown: false, html: true }, max_message_length: 8000, base_url: `${req.protocol}://${req.get("host")}`, source_plugin: "lumi_ai", source_command: "webui_assistant", permission_context: { identified_user: true, webui_actions_allowed: true } }; } function diagnosticOriginContext(user, origin = "webui") { const role = roleOf(user); const limits = { webui: 8000, discord: 1900, twitch: 450, youtube: 1800, kick: 450, other: 1000 }; return { origin, platform: origin, channel_id: "diagnostic-channel", channel_name: "Diagnostic channel", server_id: "diagnostic-server", user_id: user.id, username: user.username, display_name: user.username, role, is_admin: role === "admin", is_mod: role === "mod", message_id: "diagnostic-message", reply_mode: origin === "webui" ? "panel" : "same_channel", format_capabilities: { markdown: origin === "discord", html: origin === "webui" }, max_message_length: limits[origin] || limits.other, source_plugin: "lumi_ai", source_command: "admin_diagnostic", permission_context: { identified_user: true, webui_actions_allowed: origin === "webui" } }; } function finalizeAssistantResult(result, { role, config, baseUrl = "", maxLength = null, requestMessage = "" }) { const formatted = formatAssistantResponse({ text: normalizeCustomCommandReply(result.text, requestMessage), links: result.links, baseUrl, verifiedRoutes: repoIndexer.verifiedRoutePaths(), role, allowModeratorCodeHelp: config.support_scope?.allow_moderator_code_help === true, maxLength }); const output = { ...result, ...formatted }; if (role !== "admin") { delete output.source; output.raw_response = null; } return output; } function normalizeCustomCommandReply(text, requestMessage) { const request = String(requestMessage || ""); const output = String(text || ""); if (!/\b(custom|advanced)\s+(?:javascript|js|python|command)|\bcustom command\b/i.test(request)) return output; if (/```(?:javascript|python)\s*\n[\s\S]*?```/i.test(output)) return output; const language = /\bpython\b/i.test(request) || /^\s*def\s+run\s*\(/m.test(output) ? "python" : "javascript"; const codePattern = language === "python" ? /(^|\n)(def\s+run\s*\([\s\S]+)$/m : /(^|\n)((?:async\s+)?function\s+run\s*\([\s\S]+)$/m; const match = output.match(codePattern); if (!match) return output; const start = match.index + match[1].length; const explanation = output.slice(0, start).trim(); const code = output.slice(start).trim(); return `${explanation ? `${explanation}\n\n` : ""}\`\`\`${language}\n${code}\n\`\`\``; } function configuredBaseUrl(settings) { for (const key of ["discord_redirect_uri", "twitch_redirect_uri", "youtube_redirect_uri"]) { const value = settings?.getSetting?.(key, ""); if (!value) continue; try { return new URL(value).origin; } catch {} } return ""; } function paginateRows(rows, pageValue, pageSize = 25) { const size = Math.max(1, Number.parseInt(pageSize, 10) || 25); const pages = Math.max(1, Math.ceil(rows.length / size)); const page = Math.min(pages, Math.max(1, Number.parseInt(pageValue, 10) || 1)); const start = (page - 1) * size; return { entries: rows.slice(start, start + size), page, pages, page_size: size, total: rows.length }; } function normalizeConversationHistory(history) { if (!Array.isArray(history)) return []; return history.slice(-12).map((entry) => ({ role: ["user", "assistant"].includes(entry?.role) ? entry.role : null, content: cleanText(entry?.content, 4000) })).filter((entry) => entry.role && entry.content); } function searchKnownUsers(database, query) { const value = String(query || "").trim().replace(/[%_]/g, ""); if (value.length < 2) return []; const pattern = `%${value}%`; const rows = database.prepare( "SELECT DISTINCT p.id, p.internal_username, i.provider, i.provider_user_id, i.display_name " + "FROM user_profiles p LEFT JOIN user_identities i ON i.user_id = p.id " + "WHERE p.id LIKE ? OR p.internal_username LIKE ? OR i.provider_user_id LIKE ? OR i.display_name LIKE ? " + "ORDER BY p.internal_username LIMIT 30" ).all(pattern, pattern, pattern, pattern); return [...rows.reduce((map, row) => { const user = map.get(row.id) || { id: row.id, username: row.internal_username, identities: [] }; if (row.provider) { user.identities.push({ provider: row.provider, provider_user_id: row.provider_user_id, display_name: row.display_name }); } map.set(row.id, user); return map; }, new Map()).values()]; } function parseCommandTriggers(value) { const triggers = String(value || "assistant,lumi").split(/[,\s]+/) .map((entry) => entry.trim().replace(/^!+/, "").toLowerCase()) .filter((entry) => /^[a-z0-9_-]+$/.test(entry)); return [...new Set(triggers.length ? triggers : ["assistant", "lumi"])]; } function limitFromBody(body, prefix, fallbackRequests, fallbackWindow) { return { requests: boundedInt(body[`${prefix}_requests`], 0, 10000, fallbackRequests), window_seconds: boundedInt(body[`${prefix}_window`], 1, 86400, fallbackWindow) }; } function writeCommandsManifest(pluginDir, config) { const triggers = config.commands.triggers || ["assistant", "lumi"]; const primary = triggers[0] || "assistant"; const manifest = { pluginId: PLUGIN_ID, pluginName: "Lumi AI", platformKeys: { discord: "command_platform_discord", twitch: "command_platform_twitch", youtube: "command_platform_youtube", kick: "command_platform_kick" }, commands: [{ id: "assistant", trigger: primary, usage: `${primary} `, name: "Lumi Assistant", description: "Ask Lumi Assistant a scoped Lumi, bot, community, or WebUI support question.", level: "public", platforms: Object.entries(config.commands.platforms).filter(([, enabled]) => enabled).map(([key]) => key), aliases: triggers.slice(1) }] }; fs.writeFileSync(path.join(pluginDir, "cmds.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); } module.exports.shouldAutoResume = shouldAutoResume; module.exports.registerAssistantCommands = registerAssistantCommands; module.exports.authorizeAiRequest = authorizeAiRequest; module.exports.searchKnownUsers = searchKnownUsers; module.exports.finalizeAssistantResult = finalizeAssistantResult;