const fs = require("fs"); const { resolveData, ensureDataDirs } = require("./paths"); const { DEFAULT_SCOPE, normalizeScope } = require("./scope_manager"); const { DEFAULT_RATE_LIMITS, mergeLimits } = require("./rate_limits"); const DEFAULT_CONFIG = { enabled: false, selected_model_id: "qwen3-1.7b-q4", context_size: 4096, internal_generation_char_budget: 16000, threads: 0, gpu_allocation_intent_percent: 0, concurrency: 1, max_queue_length: 8, request_timeout_ms: 120000, ui_soft_timeout_ms: 45000, hard_generation_timeout_ms: 600000, max_output_tokens: 2048, output_budgets: { navigation_help: 256, simple_answer: 512, code_custom_command: 896, admin_debug: 1280, explicit_long: 2048 }, batch_size: 512, ubatch_size: 128, per_user_requests_per_minute: 6, admin_bypass_rate_limit: false, assistant_enabled: true, assistant_debug_logging: false, assistant_visibility: { admins: true, mods: false, users: false }, improvement: { allow_moderators_to_review_responses: false, trusted_moderator_reviewers: [], corrections_enabled: true }, gate: { model_id: "smollm2-360m-q8", context_size: 1024, threads: 2, timeout_ms: 3000, high_confidence_threshold: 0.88, main_llm_threshold: 0.72, predefined_enabled: true, cache_ttl_seconds: 3600, repeat_force_window_seconds: 90, similarity_threshold: 0.86, force_prefix: "force ai:" }, commands: { enabled: true, triggers: ["assistant", "lumi"], platforms: { discord: true, twitch: true, youtube: true, kick: false, other: false }, roles: { admins: true, mods: true, users: true }, unavailable_message: "Lumi Assistant is currently unavailable.", denied_message: "Lumi Assistant access is unavailable for your account." }, rate_limits: DEFAULT_RATE_LIMITS, support_scope: DEFAULT_SCOPE, instructions: { out_of_scope_response: "I am sorry, but that is outside my scope.", roleplay_intensity: 0, community_tone: "", admin_custom: "" }, logging: { log_prompts: false, log_responses: false, log_tool_calls: true, log_metrics: true, log_internal_audit: true } }; function readJson(name, fallback) { ensureDataDirs(); const file = resolveData("config", name); if (!fs.existsSync(file)) { writeJson(name, fallback); return structuredClone(fallback); } try { return { ...structuredClone(fallback), ...JSON.parse(fs.readFileSync(file, "utf8")) }; } catch { return structuredClone(fallback); } } function writeJson(name, value) { const file = resolveData("config", name); const tmp = `${file}.tmp`; fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`); fs.renameSync(tmp, file); } function getConfig() { const config = readJson("ai_config.json", DEFAULT_CONFIG); if (config.gpu_workload_percent != null && config.gpu_allocation_intent_percent === DEFAULT_CONFIG.gpu_allocation_intent_percent) { config.gpu_allocation_intent_percent = Math.max(0, Math.min(100, Number(config.gpu_workload_percent) || 0)); } delete config.gpu_workload_percent; config.support_scope = normalizeScope(config.support_scope || { allowed_topics: config.instructions?.allowed_topics, answer_style: config.instructions?.style, max_answer_length: config.instructions?.maximum_answer_length }); config.assistant_visibility = { ...DEFAULT_CONFIG.assistant_visibility, ...(config.assistant_visibility || {}) }; config.improvement = mergeImprovement(config.improvement); config.output_budgets = mergeOutputBudgets(config.output_budgets); config.gate = mergeGate(config.gate); config.instructions = { ...DEFAULT_CONFIG.instructions, ...(config.instructions || {}) }; config.logging = { ...DEFAULT_CONFIG.logging, ...(config.logging || {}) }; config.commands = mergeCommands(config.commands); config.rate_limits = mergeLimits(config.rate_limits); return config; } function saveConfig(value) { const merged = { ...DEFAULT_CONFIG, ...value }; const legacyIntent = value.gpu_workload_percent; merged.gpu_allocation_intent_percent = Math.max( 0, Math.min(100, Number(value.gpu_allocation_intent_percent ?? legacyIntent) || 0) ); merged.internal_generation_char_budget = Math.max( 2000, Math.min(64000, Number(value.internal_generation_char_budget) || DEFAULT_CONFIG.internal_generation_char_budget) ); merged.ui_soft_timeout_ms = boundedNumber( value.ui_soft_timeout_ms, 5000, 300000, DEFAULT_CONFIG.ui_soft_timeout_ms ); merged.hard_generation_timeout_ms = boundedNumber( value.hard_generation_timeout_ms ?? value.request_timeout_ms, 30000, 3600000, DEFAULT_CONFIG.hard_generation_timeout_ms ); merged.max_output_tokens = boundedNumber( value.max_output_tokens, 64, 32768, DEFAULT_CONFIG.max_output_tokens ); merged.output_budgets = mergeOutputBudgets(value.output_budgets); merged.batch_size = boundedNumber(value.batch_size, 32, 4096, DEFAULT_CONFIG.batch_size); merged.ubatch_size = Math.min( merged.batch_size, boundedNumber(value.ubatch_size, 16, 4096, DEFAULT_CONFIG.ubatch_size) ); delete merged.gpu_workload_percent; merged.assistant_visibility = { ...DEFAULT_CONFIG.assistant_visibility, ...(value.assistant_visibility || {}) }; merged.improvement = mergeImprovement(value.improvement); merged.gate = mergeGate(value.gate); merged.support_scope = normalizeScope(value.support_scope); merged.instructions = { ...DEFAULT_CONFIG.instructions, ...(value.instructions || {}) }; merged.logging = { ...DEFAULT_CONFIG.logging, ...(value.logging || {}) }; merged.commands = mergeCommands(value.commands); merged.rate_limits = mergeLimits(value.rate_limits); writeJson("ai_config.json", merged); return merged; } function getRuntimeState() { return readJson("runtime_state.json", { desired_state: "stopped", last_known_state: "stopped", last_stop_reason: "never_started", last_manual_stop: true, last_crashed: false, last_exit_code: null, last_diagnostic_category: null, selected_model_id: null, gpu_allocation_actual_percent: 0, gpu_allocation_max_safe_percent: 0, gpu_allocation_clamped_reason: null, updated_at: new Date().toISOString() }); } function saveRuntimeState(value) { writeJson("runtime_state.json", { ...value, updated_at: new Date().toISOString() }); } function mergeCommands(value = {}) { return { ...DEFAULT_CONFIG.commands, ...value, platforms: { ...DEFAULT_CONFIG.commands.platforms, ...(value.platforms || {}) }, roles: { ...DEFAULT_CONFIG.commands.roles, ...(value.roles || {}) }, triggers: Array.isArray(value.triggers) && value.triggers.length ? value.triggers.map((entry) => String(entry).trim().replace(/^!+/, "").toLowerCase()).filter(Boolean) : [...DEFAULT_CONFIG.commands.triggers] }; } function mergeGate(value = {}) { return { ...DEFAULT_CONFIG.gate, ...value, context_size: Math.max(512, Math.min(4096, Number(value.context_size) || DEFAULT_CONFIG.gate.context_size)), threads: Math.max(1, Math.min(16, Number(value.threads) || DEFAULT_CONFIG.gate.threads)), timeout_ms: boundedGateNumber(value.timeout_ms, 1000, 5000, DEFAULT_CONFIG.gate.timeout_ms), high_confidence_threshold: clampConfidence(value.high_confidence_threshold, DEFAULT_CONFIG.gate.high_confidence_threshold), main_llm_threshold: clampConfidence(value.main_llm_threshold, DEFAULT_CONFIG.gate.main_llm_threshold), cache_ttl_seconds: Math.max(30, Math.min(604800, Number(value.cache_ttl_seconds) || DEFAULT_CONFIG.gate.cache_ttl_seconds)), repeat_force_window_seconds: boundedGateNumber( value.repeat_force_window_seconds, 0, 3600, DEFAULT_CONFIG.gate.repeat_force_window_seconds ), similarity_threshold: clampConfidence(value.similarity_threshold, DEFAULT_CONFIG.gate.similarity_threshold), force_prefix: String(value.force_prefix ?? DEFAULT_CONFIG.gate.force_prefix).trim().slice(0, 40) }; } function mergeOutputBudgets(value = {}) { return Object.fromEntries( Object.entries(DEFAULT_CONFIG.output_budgets).map(([key, fallback]) => [ key, boundedNumber(value?.[key], 64, 32768, fallback) ]) ); } function mergeImprovement(value = {}) { return { ...DEFAULT_CONFIG.improvement, ...value, allow_moderators_to_review_responses: value.allow_moderators_to_review_responses === true, corrections_enabled: value.corrections_enabled !== false, trusted_moderator_reviewers: [...new Set( (Array.isArray(value.trusted_moderator_reviewers) ? value.trusted_moderator_reviewers : []) .map((entry) => String(entry || "").trim()) .filter(Boolean) .slice(0, 100) )] }; } function clampConfidence(value, fallback) { const number = Number(value); return Number.isFinite(number) ? Math.max(0, Math.min(1, number)) : fallback; } function boundedGateNumber(value, min, max, fallback) { const number = Number(value); return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback; } function boundedNumber(value, min, max, fallback) { const number = Number(value); return Number.isFinite(number) ? Math.max(min, Math.min(max, Math.round(number))) : fallback; } module.exports = { DEFAULT_CONFIG, getConfig, saveConfig, getRuntimeState, saveRuntimeState, readJson, writeJson };