244 lines
9.2 KiB
JavaScript
244 lines
9.2 KiB
JavaScript
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 };
|