Add config value type converters for numbers and booleans

3f0393612558acb0f1783f0cf28cfaa6c09b494f

Cohee <18619528+Cohee1207@users.noreply.github.com>

18 files changed, +83 -48Showing whitespace changes
server.js+15 -15
@@ -261,26 +261,26 @@ app.use(responseTime());
261261
262262
263263/** @type {number} */
264264const server_port = cliArguments.port ?? getConfigValue('port', DEFAULT_PORT, 'number');
265265/** @type {boolean} */
266266const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTORUN, 'boolean')) && !cliArguments.ssl;
267267/** @type {boolean} */
268268const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN, 'boolean');
269269/** @type {string} */
270270const listenAddressIPv6 = cliArguments.listenAddressIPv6 ?? getConfigValue('listenAddress.ipv6', DEFAULT_LISTEN_ADDRESS_IPV6);
271271/** @type {string} */
272272const listenAddressIPv4 = cliArguments.listenAddressIPv4 ?? getConfigValue('listenAddress.ipv4', DEFAULT_LISTEN_ADDRESS_IPV4);
273273/** @type {boolean} */
274274const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY, 'boolean');
275275const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST, 'boolean');
276276/** @type {string} */
277277globalThis.DATA_ROOT = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data');
278278/** @type {boolean} */
279279const disableCsrf = cliArguments.disableCsrf ?? getConfigValue('disableCsrfProtection', DEFAULT_CSRF_DISABLED, 'boolean');
280280const basicAuthMode = cliArguments.basicAuthMode ?? getConfigValue('basicAuthMode', DEFAULT_BASIC_AUTH, 'boolean');
281281const perUserBasicAuth = getConfigValue('perUserBasicAuth', DEFAULT_PER_USER_BASIC_AUTH, 'boolean');
282282/** @type {boolean} */
283283const enableAccounts = getConfigValue('enableUserAccounts', DEFAULT_ACCOUNTS, 'boolean');
284284
285285const uploadsPath = path.join(globalThis.DATA_ROOT, UPLOADS_DIRECTORY);
286286
@@ -293,15 +293,15 @@ let enableIPv4 = stringToBool(cliArguments.enableIPv4) ?? getConfigValue('protoc
293293/** @type {string} */
294294const autorunHostname = cliArguments.autorunHostname ?? getConfigValue('autorunHostname', DEFAULT_AUTORUN_HOSTNAME);
295295/** @type {number} */
296296const autorunPortOverride = cliArguments.autorunPortOverride ?? getConfigValue('autorunPortOverride', DEFAULT_AUTORUN_PORT, 'number');
297297
298298/** @type {boolean} */
299299const dnsPreferIPv6 = cliArguments.dnsPreferIPv6 ?? getConfigValue('dnsPreferIPv6', DEFAULT_PREFER_IPV6, 'boolean');
300300
301301/** @type {boolean} */
302302const avoidLocalhost = cliArguments.avoidLocalhost ?? getConfigValue('avoidLocalhost', DEFAULT_AVOID_LOCALHOST, 'boolean');
303303
304304const proxyEnabled = cliArguments.requestProxyEnabled ?? getConfigValue('requestProxy.enabled', DEFAULT_PROXY_ENABLED, 'boolean');
305305const proxyUrl = cliArguments.requestProxyUrl ?? getConfigValue('requestProxy.url', DEFAULT_PROXY_URL);
306306const proxyBypass = cliArguments.requestProxyBypass ?? getConfigValue('requestProxy.bypass', DEFAULT_PROXY_BYPASS);
307307
@@ -395,7 +395,7 @@ if (enableCorsProxy) {
395395
396396function getSessionCookieAge() {
397397 // Defaults to "no expiration" if not set
398398 const configValue = getConfigValue('sessionTimeout', -1, 'number');
399399
400400 // Convert to milliseconds
401401 if (configValue > 0) {
@@ -924,7 +924,7 @@ function setWindowTitle(title) {
924924function logSecurityAlert(message) {
925925 if (basicAuthMode || enableWhitelist) return; // safe!
926926 console.error(color.red(message));
927927 if (getConfigValue('securityOverride', false, 'boolean')) {
928928 console.warn(color.red('Security has been overridden. If it\'s not a trusted network, change the settings.'));
929929 return;
930930 }
src/endpoints/backends/chat-completions.js+4 -4
@@ -106,8 +106,8 @@ async function sendClaudeRequest(request, response) {
106106 const apiUrl = new URL(request.body.reverse_proxy || API_CLAUDE).toString();
107107 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.CLAUDE);
108108 const divider = '-'.repeat(process.stdout.columns);
109109 const enableSystemPromptCache = getConfigValue('claude.enableSystemPromptCache', false, 'boolean') && request.body.model.startsWith('claude-3');
110110 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
111111 // Disabled if not an integer or negative, or if the model doesn't support it
112112 if (!Number.isInteger(cachingAtDepth) || cachingAtDepth < 0 || !request.body.model.startsWith('claude-3')) {
113113 cachingAtDepth = -1;
@@ -969,7 +969,7 @@ router.post('/generate', jsonParser, function (request, response) {
969969 bodyParams.logprobs = true;
970970 }
971971
972972 if (getConfigValue('openai.randomizeUserId', false, 'boolean')) {
973973 bodyParams['user'] = uuidv4();
974974 }
975975 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.OPENROUTER) {
@@ -1008,7 +1008,7 @@ router.post('/generate', jsonParser, function (request, response) {
10081008 bodyParams['include_reasoning'] = true;
10091009 }
10101010
10111011 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number');
10121012 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
10131013 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);
10141014 }
src/endpoints/backends/text-completions.js+2 -2
@@ -372,8 +372,8 @@ router.post('/generate', jsonParser, async function (request, response) {
372372 }
373373
374374 if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) {
375375 const keepAlive = getConfigValue('ollama.keepAlive', -1, 'number');
376376 const numBatch = getConfigValue('ollama.batchSize', -1, 'number');
377377 if (numBatch > 0) {
378378 request.body['num_batch'] = numBatch;
379379 }
src/endpoints/characters.js+1 -1
@@ -24,7 +24,7 @@ import { importRisuSprites } from './sprites.js';
2424const defaultAvatarPath = './public/img/ai4.png';
2525
2626// KV-store for parsed character data
2727const cacheCapacity = Number(getConfigValue('cardsCacheCapacity', 100), 'number'); // MB
2828// With 100 MB limit it would take roughly 3000 characters to reach this limit
2929const characterDataCache = new MemoryLimitedMap(1024 * 1024 * cacheCapacity);
3030// Some Android devices require tighter memory management
src/endpoints/chats.js+3 -3
@@ -19,9 +19,9 @@ import {
1919 formatBytes,
2020} from '../util.js';
2121
2222const isBackupEnabled = !!getConfigValue('backups.chat.enabled', true, 'boolean');
2323const maxTotalChatBackups = Number(getConfigValue('backups.chat.maxTotalBackups', -1), 'number');
2424const throttleInterval = Number(getConfigValue('backups.chat.throttleInterval', 10_000), 'number');
2525
2626/**
2727 * Saves a chat to the backups directory.
src/endpoints/content-manager.js+1 -1
@@ -165,7 +165,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
165165 */
166166export async function checkForNewContent(directoriesList, forceCategories = []) {
167167 try {
168168 const contentCheckSkip = getConfigValue('skipContentCheck', false, 'boolean');
169169 if (contentCheckSkip && forceCategories?.length === 0) {
170170 return;
171171 }
src/endpoints/secrets.js+2 -2
@@ -183,7 +183,7 @@ router.post('/read', jsonParser, (request, response) => {
183183});
184184
185185router.post('/view', jsonParser, async (request, response) => {
186186 const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean');
187187
188188 if (!allowKeysExposure) {
189189 console.error('secrets.json could not be viewed unless the value of allowKeysExposure in config.yaml is set to true');
@@ -205,7 +205,7 @@ router.post('/view', jsonParser, async (request, response) => {
205205});
206206
207207router.post('/find', jsonParser, (request, response) => {
208208 const allowKeysExposure = getConfigValue('allowKeysExposure', false, 'boolean');
209209 const key = request.body.key;
210210
211211 if (!allowKeysExposure && !EXPORTABLE_KEYS.includes(key)) {
src/endpoints/settings.js+3 -3
@@ -11,9 +11,9 @@ import { jsonParser } from '../express-common.js';
1111import { getAllUserHandles, getUserDirectories } from '../users.js';
1212import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
1313
1414const ENABLE_EXTENSIONS = !!getConfigValue('extensions.enabled', true, 'boolean');
1515const ENABLE_EXTENSIONS_AUTO_UPDATE = !!getConfigValue('extensions.autoUpdate', true, 'boolean');
1616const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
1717
1818// 10 minutes
1919const AUTOSAVE_INTERVAL = 10 * 60 * 1000;
src/endpoints/thumbnails.js+2 -2
@@ -12,8 +12,8 @@ import { getAllUserHandles, getUserDirectories } from '../users.js';
1212import { getConfigValue } from '../util.js';
1313import { jsonParser } from '../express-common.js';
1414
1515const thumbnailsEnabled = !!getConfigValue('thumbnails.enabled', true, 'boolean');
1616const quality = Math.min(100, Math.max(1, parseInt(getConfigValue('thumbnails.quality', 95, 'number'))));
1717const pngFormat = String(getConfigValue('thumbnails.format', 'jpg')).toLowerCase().trim() === 'png';
1818
1919/** @type {Record<string, number[]>} */
src/endpoints/tokenizers.js+1 -1
@@ -56,7 +56,7 @@ export const TEXT_COMPLETION_MODELS = [
5656];
5757
5858const CHARS_PER_TOKEN = 3.35;
5959const IS_DOWNLOAD_ALLOWED = getConfigValue('enableDownloadableTokenizers', true, 'boolean');
6060
6161/**
6262 * Gets a path to the tokenizer model. Downloads the model if it's a URL.
src/endpoints/users-public.js+1 -1
@@ -7,7 +7,7 @@ import { jsonParser, getIpFromRequest } from '../express-common.js';
77import { color, Cache, getConfigValue } from '../util.js';
88import { KEY_PREFIX, getUserAvatar, toKey, getPasswordHash, getPasswordSalt } from '../users.js';
99
1010const DISCREET_LOGIN = getConfigValue('enableDiscreetLogin', false, 'boolean');
1111const MFA_CACHE = new Cache(5 * 60 * 1000);
1212
1313export const router = express.Router();
src/middleware/basicAuth.js+2 -2
@@ -7,8 +7,8 @@ import storage from 'node-persist';
77import { getAllUserHandles, toKey, getPasswordHash } from '../users.js';
88import { getConfig, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean');
1111const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
1212
1313const basicAuthMiddleware = async function (request, response, callback) {
1414 const unauthorizedWebpage = safeReadFileSync('./public/error/unauthorized.html') ?? '';
src/middleware/whitelist.js+1 -1
@@ -8,7 +8,7 @@ import { getIpFromRequest } from '../express-common.js';
88import { color, getConfigValue, safeReadFileSync } from '../util.js';
99
1010const whitelistPath = path.join(process.cwd(), './whitelist.txt');
1111const enableForwardedWhitelist = getConfigValue('enableForwardedWhitelist', false, 'boolean');
1212let whitelist = getConfigValue('whitelist', []);
1313let knownIPs = new Set();
1414
src/plugin-loader.js+2 -2
@@ -7,8 +7,8 @@ import { default as git } from 'simple-git';
77import { sync as commandExistsSync } from 'command-exists';
88import { getConfigValue, color } from './util.js';
99
1010const enableServerPlugins = !!getConfigValue('enableServerPlugins', false, 'boolean');
1111const enableServerPluginsAutoUpdate = !!getConfigValue('enableServerPluginsAutoUpdate', true, 'boolean');
1212
1313/**
1414 * Map of loaded plugins.
src/prompt-converters.js+1 -1
@@ -572,7 +572,7 @@ export function convertMistralMessages(messages, names) {
572572 }
573573
574574 // Make the last assistant message a prefill
575575 const prefixEnabled = getConfigValue('mistral.enablePrefix', false, 'boolean');
576576 const lastMsg = messages[messages.length - 1];
577577 if (prefixEnabled && messages.length > 0 && lastMsg?.role === 'assistant') {
578578 lastMsg.prefix = true;
src/transformers.js+1 -1
@@ -132,7 +132,7 @@ export async function getPipeline(task, forceModel = '') {
132132
133133 const cacheDir = path.join(globalThis.DATA_ROOT, '_cache');
134134 const model = forceModel || getModelForTask(task);
135135 const localOnly = !getConfigValue('extensions.models.autoDownload', true, 'boolean');
136136 console.log('Initializing transformers.js pipeline for task', task, 'with model', model);
137137 const instance = await pipeline(task, model, { cache_dir: cacheDir, quantized: tasks[task].quantized ?? true, local_files_only: localOnly });
138138 tasks[task].pipeline = instance;
src/users.js+3 -3
@@ -21,9 +21,9 @@ import { getContentOfType } from './endpoints/content-manager.js';
2121
2222export const KEY_PREFIX = 'user:';
2323const AVATAR_PREFIX = 'avatar:';
2424const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false, 'boolean');
2525const AUTHELIA_AUTH = getConfigValue('autheliaAuth', false, 'boolean');
2626const PER_USER_BASIC_AUTH = getConfigValue('perUserBasicAuth', false, 'boolean');
2727const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64');
2828
2929/**
src/util.js+38 -3
@@ -54,13 +54,16 @@ export function getConfig() {
5454 }
5555}
5656
57+
5758/**
5859 * Returns the value for the given key from the config object.
5960 * @param {string} key - Key to get from the config object
6061 * @param {any} defaultValue - Default value to return if the key is not found
62+ * @param {'number'|'boolean'|null} typeConverter - Type to convert the value to
6163 * @returns {any} Value for the given key
6264 */
6365export function getConfigValue(key, defaultValue = null, typeConverter = null) {
66+ function _getValue() {
6467 const envKey = keyToEnv(key);
6568 if (envKey in process.env) {
6669 return process.env[envKey];
@@ -69,6 +72,17 @@ export function getConfigValue(key, defaultValue = null) {
6972 return _.get(config, key, defaultValue);
7073 }
7174
75+ const value = _getValue();
76+ switch (typeConverter) {
77+ case 'number':
78+ return Number(value);
79+ case 'boolean':
80+ return toBoolean(value);
81+ default:
82+ return value;
83+ }
84+}
85+
7286/**
7387 * Encodes the Basic Auth header value for the given user and password.
7488 * @param {string} auth username:password
@@ -392,7 +406,7 @@ export function generateTimestamp() {
392406 * @param {number?} limit Maximum number of backups to keep. If null, the limit is determined by the `backups.common.numberOfBackups` config value.
393407 */
394408export function removeOldBackups(directory, prefix, limit = null) {
395409 const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number'));
396410
397411 let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix));
398412 if (files.length > MAX_BACKUPS) {
@@ -745,6 +759,27 @@ export async function canResolve(name, useIPv6 = true, useIPv4 = true) {
745759 }
746760}
747761
762+/**
763+ * Converts various JavaScript primitives to boolean values.
764+ * Handles special case for "true"/"false" strings (case-insensitive)
765+ *
766+ * @param {any} value - The value to convert to boolean
767+ * @returns {boolean} - The boolean representation of the value
768+ */
769+export function toBoolean(value) {
770+ // Handle string values case-insensitively
771+ if (typeof value === 'string') {
772+ // Trim and convert to lowercase for case-insensitive comparison
773+ const trimmedLower = value.trim().toLowerCase();
774+
775+ // Handle explicit "true"/"false" strings
776+ if (trimmedLower === 'true') return true;
777+ if (trimmedLower === 'false') return false;
778+ }
779+
780+ // Handle all other JavaScript values based on their "truthiness"
781+ return Boolean(value);
782+}
748783
749784/**
750785 * converts string to boolean accepts 'true' or 'false' else it returns the string put in
@@ -761,7 +796,7 @@ export function stringToBool(str) {
761796 * Setup the minimum log level
762797 */
763798export function setupLogLevel() {
764799 const logLevel = getConfigValue('minLogLevel', LOG_LEVELS.DEBUG, 'number');
765800
766801 globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => {};
767802 globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => {};