UNPKG

lynkr

Version:

Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.

1,087 lines (991 loc) 47.6 kB
const path = require("path"); const dotenv = require("dotenv"); dotenv.config(); function trimTrailingSlash(value) { if (typeof value !== "string") return value; return value.replace(/\/$/, ""); } function parseJson(value, fallback = null) { if (typeof value !== "string" || value.trim().length === 0) return fallback; try { return JSON.parse(value); } catch { return fallback; } } function parseList(value, options = {}) { if (typeof value !== "string" || value.trim().length === 0) return []; const separator = options.separator ?? ","; return value .split(separator) .map((item) => item.trim()) .filter(Boolean); } function parseMountList(value) { if (typeof value !== "string" || value.trim().length === 0) return []; return value .split(";") .map((entry) => entry.trim()) .filter(Boolean) .map((entry) => { const parts = entry.split(":"); if (parts.length < 2) return null; const host = parts[0]?.trim(); const container = parts[1]?.trim(); const mode = parts[2]?.trim() || "rw"; if (!host || !container) return null; return { host: path.resolve(host), container, mode, }; }) .filter(Boolean); } function resolveConfigPath(targetPath) { if (typeof targetPath !== "string" || targetPath.trim().length === 0) { return null; } let normalised = targetPath.trim(); if (normalised.startsWith("~")) { const home = process.env.HOME || process.env.USERPROFILE; if (home) { normalised = path.join(home, normalised.slice(1)); } } return path.resolve(normalised); } const SUPPORTED_MODEL_PROVIDERS = new Set(["databricks", "azure-anthropic", "ollama", "openrouter", "azure-openai", "openai", "llamacpp", "lmstudio", "bedrock", "zai", "vertex", "moonshot"]); const rawModelProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase(); // Validate MODEL_PROVIDER early with a clear error message if (!SUPPORTED_MODEL_PROVIDERS.has(rawModelProvider)) { const supportedList = Array.from(SUPPORTED_MODEL_PROVIDERS).sort().join(", "); throw new Error( `Unsupported MODEL_PROVIDER: "${process.env.MODEL_PROVIDER}". ` + `Valid options are: ${supportedList}` ); } const modelProvider = rawModelProvider; const rawBaseUrl = trimTrailingSlash(process.env.DATABRICKS_API_BASE); const apiKey = process.env.DATABRICKS_API_KEY; const azureAnthropicEndpoint = process.env.AZURE_ANTHROPIC_ENDPOINT ?? null; const azureAnthropicApiKey = process.env.AZURE_ANTHROPIC_API_KEY ?? null; const azureAnthropicVersion = process.env.AZURE_ANTHROPIC_VERSION ?? "2023-06-01"; const ollamaEndpoint = process.env.OLLAMA_ENDPOINT ?? "http://localhost:11434"; const ollamaModel = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b"; const ollamaTimeout = Number.parseInt(process.env.OLLAMA_TIMEOUT_MS ?? "120000", 10); const ollamaKeepAlive = process.env.OLLAMA_KEEP_ALIVE ?? undefined; // Accepts: duration strings ("10m", "24h"), numbers (seconds), -1 (permanent), 0 (immediate unload) const ollamaEmbeddingsEndpoint = process.env.OLLAMA_EMBEDDINGS_ENDPOINT ?? `${ollamaEndpoint}/api/embeddings`; const ollamaEmbeddingsModel = process.env.OLLAMA_EMBEDDINGS_MODEL ?? "nomic-embed-text"; // OpenRouter configuration const openRouterApiKey = process.env.OPENROUTER_API_KEY ?? null; const openRouterModel = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini"; const openRouterEmbeddingsModel = process.env.OPENROUTER_EMBEDDINGS_MODEL ?? "openai/text-embedding-ada-002"; const openRouterEndpoint = process.env.OPENROUTER_ENDPOINT ?? "https://openrouter.ai/api/v1/chat/completions"; // Azure OpenAI configuration const azureOpenAIEndpoint = process.env.AZURE_OPENAI_ENDPOINT?.trim() || null; const azureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null; const azureOpenAIDeployment = process.env.AZURE_OPENAI_DEPLOYMENT?.trim() || "gpt-4o"; const azureOpenAIApiVersion = process.env.AZURE_OPENAI_API_VERSION?.trim() || "2024-08-01-preview"; // OpenAI configuration const openAIApiKey = process.env.OPENAI_API_KEY?.trim() || null; const openAIModel = process.env.OPENAI_MODEL?.trim() || "gpt-4o"; const openAIEndpoint = process.env.OPENAI_ENDPOINT?.trim() || "https://api.openai.com/v1/chat/completions"; const openAIOrganization = process.env.OPENAI_ORGANIZATION?.trim() || null; // llama.cpp configuration const llamacppEndpoint = process.env.LLAMACPP_ENDPOINT?.trim() || "http://localhost:8080"; const llamacppModel = process.env.LLAMACPP_MODEL?.trim() || "default"; const llamacppTimeout = Number.parseInt(process.env.LLAMACPP_TIMEOUT_MS ?? "120000", 10); const llamacppApiKey = process.env.LLAMACPP_API_KEY?.trim() || null; const llamacppEmbeddingsEndpoint = process.env.LLAMACPP_EMBEDDINGS_ENDPOINT?.trim() || `${llamacppEndpoint}/embeddings`; // LM Studio configuration const lmstudioEndpoint = process.env.LMSTUDIO_ENDPOINT?.trim() || "http://localhost:1234"; const lmstudioModel = process.env.LMSTUDIO_MODEL?.trim() || "default"; const lmstudioTimeout = Number.parseInt(process.env.LMSTUDIO_TIMEOUT_MS ?? "120000", 10); const lmstudioApiKey = process.env.LMSTUDIO_API_KEY?.trim() || null; // AWS Bedrock configuration const bedrockRegion = process.env.AWS_BEDROCK_REGION?.trim() || process.env.AWS_REGION?.trim() || "us-east-1"; const bedrockApiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null; // Bearer token const bedrockModelId = process.env.AWS_BEDROCK_MODEL_ID?.trim() || "anthropic.claude-3-5-sonnet-20241022-v2:0"; // Z.AI (Zhipu) configuration - Anthropic-compatible API at ~1/7 cost const zaiApiKey = process.env.ZAI_API_KEY?.trim() || null; const zaiEndpoint = process.env.ZAI_ENDPOINT?.trim() || "https://api.z.ai/api/anthropic/v1/messages"; const zaiModel = process.env.ZAI_MODEL?.trim() || "GLM-4.7"; // Moonshot AI (Kimi) configuration - OpenAI-compatible API const moonshotApiKey = process.env.MOONSHOT_API_KEY?.trim() || null; const moonshotEndpoint = process.env.MOONSHOT_ENDPOINT?.trim() || "https://api.moonshot.ai/v1/chat/completions"; const moonshotModel = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview"; // Vertex AI (Google Gemini) configuration const vertexApiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null; const vertexModel = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash"; // Suggestion mode model override // Values: "default" (use MODEL_DEFAULT), "none" (skip LLM call), or a model name const suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim(); // Hot reload configuration const hotReloadEnabled = process.env.HOT_RELOAD_ENABLED !== "false"; // default true const hotReloadDebounceMs = Number.parseInt(process.env.HOT_RELOAD_DEBOUNCE_MS ?? "1000", 10); // Routing configuration const fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; // default true const ollamaMaxToolsForRouting = Number.parseInt( process.env.OLLAMA_MAX_TOOLS_FOR_ROUTING ?? "3", 10 ); const openRouterMaxToolsForRouting = Number.parseInt( process.env.OPENROUTER_MAX_TOOLS_FOR_ROUTING ?? "15", 10 ); const rawFallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase(); // Validate FALLBACK_PROVIDER early with a clear error message if (!SUPPORTED_MODEL_PROVIDERS.has(rawFallbackProvider)) { const supportedList = Array.from(SUPPORTED_MODEL_PROVIDERS).sort().join(", "); throw new Error( `Unsupported FALLBACK_PROVIDER: "${process.env.FALLBACK_PROVIDER}". ` + `Valid options are: ${supportedList}` ); } const fallbackProvider = rawFallbackProvider; // Tool execution mode: server (default), client, or passthrough const toolExecutionMode = (process.env.TOOL_EXECUTION_MODE ?? "server").toLowerCase(); if (!["server", "client", "passthrough"].includes(toolExecutionMode)) { throw new Error( "TOOL_EXECUTION_MODE must be one of: server, client, passthrough (default: server)" ); } // Memory system configuration (Titans-inspired long-term memory) const memoryEnabled = process.env.MEMORY_ENABLED !== "false"; // default true const memoryRetrievalLimit = Number.parseInt(process.env.MEMORY_RETRIEVAL_LIMIT ?? "5", 10); const memorySurpriseThreshold = Number.parseFloat(process.env.MEMORY_SURPRISE_THRESHOLD ?? "0.3"); const memoryMaxAgeDays = Number.parseInt(process.env.MEMORY_MAX_AGE_DAYS ?? "90", 10); const memoryMaxCount = Number.parseInt(process.env.MEMORY_MAX_COUNT ?? "10000", 10); const memoryIncludeGlobal = process.env.MEMORY_INCLUDE_GLOBAL !== "false"; // default true const memoryInjectionFormat = (process.env.MEMORY_INJECTION_FORMAT ?? "system").toLowerCase(); const memoryExtractionEnabled = process.env.MEMORY_EXTRACTION_ENABLED !== "false"; // default true const memoryDecayEnabled = process.env.MEMORY_DECAY_ENABLED !== "false"; // default true const memoryDecayHalfLifeDays = Number.parseInt(process.env.MEMORY_DECAY_HALF_LIFE ?? "30", 10); // Token optimization settings const tokenTrackingEnabled = process.env.TOKEN_TRACKING_ENABLED !== "false"; // default true const toolTruncationEnabled = process.env.TOOL_TRUNCATION_ENABLED !== "false"; // default true const memoryFormat = (process.env.MEMORY_FORMAT ?? "compact").toLowerCase(); const memoryDedupEnabled = process.env.MEMORY_DEDUP_ENABLED !== "false"; // default true const memoryDedupLookback = Number.parseInt(process.env.MEMORY_DEDUP_LOOKBACK ?? "5", 10); const systemPromptMode = (process.env.SYSTEM_PROMPT_MODE ?? "dynamic").toLowerCase(); const toolDescriptions = (process.env.TOOL_DESCRIPTIONS ?? "minimal").toLowerCase(); const historyCompressionEnabled = process.env.HISTORY_COMPRESSION_ENABLED !== "false"; // default true const historyKeepRecentTurns = Number.parseInt(process.env.HISTORY_KEEP_RECENT_TURNS ?? "10", 10); const historySummarizeOlder = process.env.HISTORY_SUMMARIZE_OLDER !== "false"; // default true const tokenBudgetWarning = Number.parseInt(process.env.TOKEN_BUDGET_WARNING ?? "100000", 10); const tokenBudgetMax = Number.parseInt(process.env.TOKEN_BUDGET_MAX ?? "180000", 10); const tokenBudgetEnforcement = process.env.TOKEN_BUDGET_ENFORCEMENT !== "false"; // default true // Caveman terse-output injection (opt-in, off by default) const cavemanEnabled = process.env.CAVEMAN_ENABLED === "true"; const cavemanLevel = (process.env.CAVEMAN_LEVEL ?? "lite").toLowerCase(); // TOON payload compression (opt-in) const toonEnabled = process.env.TOON_ENABLED === "true"; // default false const toonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10); const toonFailOpen = process.env.TOON_FAIL_OPEN !== "false"; // default true const toonLogStats = process.env.TOON_LOG_STATS !== "false"; // default true // Smart tool selection configuration (always enabled) const smartToolSelectionMode = (process.env.SMART_TOOL_SELECTION_MODE ?? "heuristic").toLowerCase(); const smartToolSelectionTokenBudget = Number.parseInt( process.env.SMART_TOOL_SELECTION_TOKEN_BUDGET ?? "2500", 10 ); // Headroom sidecar configuration const headroomEnabled = process.env.HEADROOM_ENABLED === "true"; const headroomEndpoint = process.env.HEADROOM_ENDPOINT?.trim() || "http://localhost:8787"; const headroomTimeoutMs = Number.parseInt(process.env.HEADROOM_TIMEOUT_MS ?? "5000", 10); const headroomMinTokens = Number.parseInt(process.env.HEADROOM_MIN_TOKENS ?? "500", 10); const headroomMode = (process.env.HEADROOM_MODE ?? "optimize").toLowerCase(); // Headroom Docker container configuration const headroomDockerEnabled = process.env.HEADROOM_DOCKER_ENABLED !== "false"; // default true when headroom enabled const headroomDockerImage = process.env.HEADROOM_DOCKER_IMAGE ?? "lynkr/headroom-sidecar:latest"; const headroomDockerContainerName = process.env.HEADROOM_DOCKER_CONTAINER_NAME ?? "lynkr-headroom"; const headroomDockerPort = Number.parseInt(process.env.HEADROOM_DOCKER_PORT ?? "8787", 10); const headroomDockerMemoryLimit = process.env.HEADROOM_DOCKER_MEMORY_LIMIT ?? "512m"; const headroomDockerCpuLimit = process.env.HEADROOM_DOCKER_CPU_LIMIT ?? "1.0"; const headroomDockerRestartPolicy = process.env.HEADROOM_DOCKER_RESTART_POLICY ?? "unless-stopped"; const headroomDockerNetwork = process.env.HEADROOM_DOCKER_NETWORK ?? null; const headroomDockerBuildContext = process.env.HEADROOM_DOCKER_BUILD_CONTEXT ?? "./headroom-sidecar"; const headroomDockerAutoBuild = process.env.HEADROOM_DOCKER_AUTO_BUILD === "true"; // Headroom transform configuration (passed to sidecar) const headroomSmartCrusher = process.env.HEADROOM_SMART_CRUSHER !== "false"; const headroomSmartCrusherMinTokens = Number.parseInt(process.env.HEADROOM_SMART_CRUSHER_MIN_TOKENS ?? "200", 10); const headroomSmartCrusherMaxItems = Number.parseInt(process.env.HEADROOM_SMART_CRUSHER_MAX_ITEMS ?? "15", 10); const headroomToolCrusher = process.env.HEADROOM_TOOL_CRUSHER !== "false"; const headroomCacheAligner = process.env.HEADROOM_CACHE_ALIGNER !== "false"; const headroomRollingWindow = process.env.HEADROOM_ROLLING_WINDOW !== "false"; const headroomKeepTurns = Number.parseInt(process.env.HEADROOM_KEEP_TURNS ?? "3", 10); const headroomCcrEnabled = process.env.HEADROOM_CCR !== "false"; const headroomCcrTtl = Number.parseInt(process.env.HEADROOM_CCR_TTL ?? "300", 10); const headroomLlmlingua = process.env.HEADROOM_LLMLINGUA === "true"; const headroomLlmlinguaDevice = process.env.HEADROOM_LLMLINGUA_DEVICE ?? "auto"; const headroomProvider = process.env.HEADROOM_PROVIDER ?? "anthropic"; const headroomLogLevel = process.env.HEADROOM_LOG_LEVEL ?? "info"; // Only require Databricks credentials if it's the primary provider or used as fallback if (modelProvider === "databricks" && (!rawBaseUrl || !apiKey)) { throw new Error("Set DATABRICKS_API_BASE and DATABRICKS_API_KEY before starting the proxy."); } else if (modelProvider === "ollama" && !fallbackEnabled && (!rawBaseUrl || !apiKey)) { // Relaxed: Allow mock credentials for true Ollama-only mode (fallback disabled) if (!rawBaseUrl) process.env.DATABRICKS_API_BASE = "http://localhost:8080"; if (!apiKey) process.env.DATABRICKS_API_KEY = "mock-key-for-ollama-only"; console.log("[CONFIG] Using mock Databricks credentials (Ollama-only mode with fallback disabled)"); } if (modelProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) { throw new Error( "Set AZURE_ANTHROPIC_ENDPOINT and AZURE_ANTHROPIC_API_KEY before starting the proxy.", ); } if (modelProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) { throw new Error( "Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY before starting the proxy.", ); } if (modelProvider === "openai" && !openAIApiKey) { throw new Error( "Set OPENAI_API_KEY before starting the proxy.", ); } if (modelProvider === "ollama") { try { new URL(ollamaEndpoint); } catch (err) { throw new Error("OLLAMA_ENDPOINT must be a valid URL (default: http://localhost:11434)"); } } if (modelProvider === "llamacpp") { try { new URL(llamacppEndpoint); } catch (err) { throw new Error("LLAMACPP_ENDPOINT must be a valid URL (default: http://localhost:8080)"); } } if (modelProvider === "lmstudio") { try { new URL(lmstudioEndpoint); } catch (err) { throw new Error("LMSTUDIO_ENDPOINT must be a valid URL (default: http://localhost:1234)"); } } // Validate Bedrock credentials when it's the primary provider if (modelProvider === "bedrock" && !bedrockApiKey) { throw new Error( "AWS Bedrock requires AWS_BEDROCK_API_KEY (Bearer token). " + "Generate from AWS Console → Bedrock → API Keys, then set AWS_BEDROCK_API_KEY in your .env file." ); } // Deprecation warning for PREFER_OLLAMA if (process.env.PREFER_OLLAMA) { console.warn('[DEPRECATION] PREFER_OLLAMA is removed. Use TIER_* env vars for routing. See documentation/routing.md'); } // Warn about misconfigured fallback provider (only when tier routing is active, // since that's the only path that triggers provider fallback) const tiersConfigured = !!( process.env.TIER_SIMPLE?.trim() && process.env.TIER_MEDIUM?.trim() && process.env.TIER_COMPLEX?.trim() && process.env.TIER_REASONING?.trim() ); if (fallbackEnabled && tiersConfigured) { const localProviders = ["ollama", "llamacpp", "lmstudio"]; if (localProviders.includes(fallbackProvider)) { throw new Error(`FALLBACK_PROVIDER cannot be '${fallbackProvider}' (local providers should not be fallbacks). Use cloud providers: databricks, azure-anthropic, azure-openai, openrouter, openai, bedrock`); } let fallbackMisconfigured = false; if (fallbackProvider === "databricks" && (!rawBaseUrl || !apiKey)) { fallbackMisconfigured = true; } if (fallbackProvider === "azure-anthropic" && (!azureAnthropicEndpoint || !azureAnthropicApiKey)) { fallbackMisconfigured = true; } if (fallbackProvider === "azure-openai" && (!azureOpenAIEndpoint || !azureOpenAIApiKey)) { fallbackMisconfigured = true; } if (fallbackProvider === "bedrock" && !bedrockApiKey) { fallbackMisconfigured = true; } if (fallbackMisconfigured) { console.warn(`[WARN] FALLBACK_PROVIDER='${fallbackProvider}' is enabled but missing credentials. Fallback will not work until configured.`); } } const endpointPath = process.env.DATABRICKS_ENDPOINT_PATH ?? "/serving-endpoints/databricks-claude-sonnet-4-5/invocations"; const databricksUrl = rawBaseUrl && endpointPath ? `${rawBaseUrl}${endpointPath.startsWith("/") ? "" : "/"}${endpointPath}` : null; const defaultModel = process.env.MODEL_DEFAULT ?? (modelProvider === "azure-anthropic" ? "claude-opus-4-5" : "databricks-claude-sonnet-4-5"); const port = Number.parseInt(process.env.PORT ?? "8080", 10); const sessionDbPath = process.env.SESSION_DB_PATH ?? path.join(process.cwd(), "data", "sessions.db"); const workspaceRoot = path.resolve(process.env.WORKSPACE_ROOT ?? process.cwd()); // Rate limiting configuration const rateLimitEnabled = process.env.RATE_LIMIT_ENABLED !== "false"; // default true const rateLimitWindow = Number.parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? "60000", 10); // 1 minute const rateLimitMax = Number.parseInt(process.env.RATE_LIMIT_MAX ?? "100", 10); // 100 requests per window const rateLimitKeyBy = process.env.RATE_LIMIT_KEY_BY ?? "session"; // "session", "ip", or "both" const defaultWebEndpoint = process.env.WEB_SEARCH_ENDPOINT ?? "http://localhost:8888/search"; let webEndpointHost = null; try { const { hostname } = new URL(defaultWebEndpoint); webEndpointHost = hostname.toLowerCase(); } catch { webEndpointHost = null; } const allowAllWebHosts = process.env.WEB_SEARCH_ALLOW_ALL !== "false"; const configuredAllowedHosts = process.env.WEB_SEARCH_ALLOWED_HOSTS?.split(",") .map((host) => host.trim().toLowerCase()) .filter(Boolean) ?? []; const webAllowedHosts = allowAllWebHosts ? null : new Set([webEndpointHost, "localhost", "127.0.0.1"].filter(Boolean).concat(configuredAllowedHosts)); const webTimeoutMs = Number.parseInt(process.env.WEB_SEARCH_TIMEOUT_MS ?? "10000", 10); const webFetchBodyPreviewMax = Number.parseInt(process.env.WEB_FETCH_BODY_PREVIEW_MAX ?? "10000", 10); const webSearchRetryEnabled = process.env.WEB_SEARCH_RETRY_ENABLED !== "false"; // default true const webSearchMaxRetries = Number.parseInt(process.env.WEB_SEARCH_MAX_RETRIES ?? "2", 10); // TinyFish AI Browser Automation configuration const tinyfishApiKey = process.env.TINYFISH_API_KEY?.trim() || null; const tinyfishEndpoint = process.env.TINYFISH_ENDPOINT?.trim() || "https://agent.tinyfish.ai/v1/automation/run-sse"; const tinyfishBrowserProfile = process.env.TINYFISH_BROWSER_PROFILE?.trim() || "lite"; const tinyfishTimeoutMs = parseInt(process.env.TINYFISH_TIMEOUT_MS ?? "120000", 10); const tinyfishProxyEnabled = process.env.TINYFISH_PROXY_ENABLED === "true"; const tinyfishProxyCountry = process.env.TINYFISH_PROXY_COUNTRY?.trim() || "US"; const policyMaxSteps = process.env.POLICY_MAX_STEPS ? Number.parseInt(process.env.POLICY_MAX_STEPS, 10) : null; // null = no limit const policyMaxToolCalls = process.env.POLICY_MAX_TOOL_CALLS ? Number.parseInt(process.env.POLICY_MAX_TOOL_CALLS, 10) : null; // null = no limit const policyToolLoopThreshold = Number.parseInt(process.env.POLICY_TOOL_LOOP_THRESHOLD ?? "10", 10); const policyDisallowedTools = process.env.POLICY_DISALLOWED_TOOLS?.split(",") .map((tool) => tool.trim()) .filter(Boolean) ?? []; const policyGitAllowPush = process.env.POLICY_GIT_ALLOW_PUSH === "true"; const policyGitAllowPull = process.env.POLICY_GIT_ALLOW_PULL !== "false"; const policyGitAllowCommit = process.env.POLICY_GIT_ALLOW_COMMIT !== "false"; const policyGitTestCommand = process.env.POLICY_GIT_TEST_COMMAND ?? null; const policyGitRequireTests = process.env.POLICY_GIT_REQUIRE_TESTS === "true"; const policyGitCommitRegex = process.env.POLICY_GIT_COMMIT_REGEX ?? null; const policyGitAutoStash = process.env.POLICY_GIT_AUTOSTASH === "true"; const policyFileAllowedPaths = parseList( process.env.POLICY_FILE_ALLOWED_PATHS ?? "", ); const policyFileBlockedPaths = parseList( process.env.POLICY_FILE_BLOCKED_PATHS ?? "/.env,.env,/etc/passwd,/etc/shadow", ); const policySafeCommandsEnabled = process.env.POLICY_SAFE_COMMANDS_ENABLED !== "false"; const policySafeCommandsConfig = parseJson(process.env.POLICY_SAFE_COMMANDS_CONFIG ?? "", null); const sandboxEnabled = process.env.MCP_SANDBOX_ENABLED !== "false"; const sandboxImage = process.env.MCP_SANDBOX_IMAGE ?? null; const sandboxRuntime = process.env.MCP_SANDBOX_RUNTIME ?? "docker"; const sandboxContainerWorkspace = process.env.MCP_SANDBOX_CONTAINER_WORKSPACE ?? "/workspace"; const sandboxMountWorkspace = process.env.MCP_SANDBOX_MOUNT_WORKSPACE !== "false"; const sandboxAllowNetworking = process.env.MCP_SANDBOX_ALLOW_NETWORKING === "true"; const sandboxNetworkMode = sandboxAllowNetworking ? process.env.MCP_SANDBOX_NETWORK_MODE ?? "bridge" : "none"; const sandboxPassthroughEnv = parseList( process.env.MCP_SANDBOX_PASSTHROUGH_ENV ?? "PATH,LANG,LC_ALL,TERM,HOME", ); const sandboxExtraMounts = parseMountList(process.env.MCP_SANDBOX_EXTRA_MOUNTS ?? ""); const sandboxDefaultTimeoutMs = Number.parseInt( process.env.MCP_SANDBOX_TIMEOUT_MS ?? "20000", 10, ); const sandboxUser = process.env.MCP_SANDBOX_USER ?? null; const sandboxEntrypoint = process.env.MCP_SANDBOX_ENTRYPOINT ?? null; const sandboxReuseSessions = process.env.MCP_SANDBOX_REUSE_SESSION !== "false"; const sandboxReadOnlyRoot = process.env.MCP_SANDBOX_READ_ONLY_ROOT === "true"; const sandboxNoNewPrivileges = process.env.MCP_SANDBOX_NO_NEW_PRIVILEGES !== "false"; const sandboxDropCapabilities = parseList( process.env.MCP_SANDBOX_DROP_CAPABILITIES ?? "ALL", ); const sandboxAddCapabilities = parseList( process.env.MCP_SANDBOX_ADD_CAPABILITIES ?? "", ); const sandboxMemoryLimit = process.env.MCP_SANDBOX_MEMORY_LIMIT ?? "512m"; const sandboxCpuLimit = process.env.MCP_SANDBOX_CPU_LIMIT ?? "1.0"; const sandboxPidsLimit = Number.parseInt( process.env.MCP_SANDBOX_PIDS_LIMIT ?? "100", 10, ); const sandboxPermissionMode = (process.env.MCP_SANDBOX_PERMISSION_MODE ?? "auto").toLowerCase(); const sandboxPermissionAllow = parseList(process.env.MCP_SANDBOX_PERMISSION_ALLOW ?? ""); const sandboxPermissionDeny = parseList(process.env.MCP_SANDBOX_PERMISSION_DENY ?? ""); const sandboxManifestPath = resolveConfigPath(process.env.MCP_SERVER_MANIFEST ?? null); let manifestDirList = null; if (process.env.MCP_MANIFEST_DIRS === "") { manifestDirList = []; } else if (process.env.MCP_MANIFEST_DIRS) { manifestDirList = parseList(process.env.MCP_MANIFEST_DIRS); } else { manifestDirList = ["~/.claude/mcp"]; } const sandboxManifestDirs = manifestDirList .map((dir) => resolveConfigPath(dir)) .filter((dir) => typeof dir === "string" && dir.length > 0); const promptCacheEnabled = process.env.PROMPT_CACHE_ENABLED !== "false"; const promptCacheMaxEntriesRaw = Number.parseInt( process.env.PROMPT_CACHE_MAX_ENTRIES ?? "64", 10, ); const promptCacheTtlRaw = Number.parseInt( process.env.PROMPT_CACHE_TTL_MS ?? "300000", 10, ); const testDefaultCommand = process.env.WORKSPACE_TEST_COMMAND ?? null; const testDefaultArgs = parseList(process.env.WORKSPACE_TEST_ARGS ?? ""); const testTimeoutMs = Number.parseInt(process.env.WORKSPACE_TEST_TIMEOUT_MS ?? "600000", 10); const testSandboxMode = (process.env.WORKSPACE_TEST_SANDBOX ?? "auto").toLowerCase(); let testCoverageFiles = parseList( process.env.WORKSPACE_TEST_COVERAGE_FILES ?? "coverage/coverage-summary.json", ); if (testCoverageFiles.length === 0) { testCoverageFiles = []; } const testProfiles = parseJson(process.env.WORKSPACE_TEST_PROFILES ?? "", null); // Agents configuration const agentsEnabled = process.env.AGENTS_ENABLED === "true"; const agentsMaxConcurrent = Number.parseInt(process.env.AGENTS_MAX_CONCURRENT ?? "10", 10); const agentsDefaultModel = process.env.AGENTS_DEFAULT_MODEL ?? "haiku"; const agentsMaxSteps = Number.parseInt(process.env.AGENTS_MAX_STEPS ?? "15", 10); const agentsTimeout = Number.parseInt(process.env.AGENTS_TIMEOUT ?? "120000", 10); // LLM Audit logging configuration const auditEnabled = process.env.LLM_AUDIT_ENABLED === "true"; // default false const auditLogFile = process.env.LLM_AUDIT_LOG_FILE ?? path.join(process.cwd(), "logs", "llm-audit.log"); const auditMaxContentLength = Number.parseInt(process.env.LLM_AUDIT_MAX_CONTENT_LENGTH ?? "5000", 10); // Legacy fallback const auditMaxSystemLength = Number.parseInt(process.env.LLM_AUDIT_MAX_SYSTEM_LENGTH ?? "2000", 10); const auditMaxUserLength = Number.parseInt(process.env.LLM_AUDIT_MAX_USER_LENGTH ?? "3000", 10); const auditMaxResponseLength = Number.parseInt(process.env.LLM_AUDIT_MAX_RESPONSE_LENGTH ?? "3000", 10); const auditMaxFiles = Number.parseInt(process.env.LLM_AUDIT_MAX_FILES ?? "30", 10); const auditMaxSize = process.env.LLM_AUDIT_MAX_SIZE ?? "100M"; const auditAnnotations = process.env.LLM_AUDIT_ANNOTATIONS !== "false"; // default true // LLM Audit deduplication configuration const auditDeduplicationEnabled = process.env.LLM_AUDIT_DEDUP_ENABLED !== "false"; // default true const auditDeduplicationDictPath = process.env.LLM_AUDIT_DEDUP_DICT_PATH ?? path.join(process.cwd(), "logs", "llm-audit-dictionary.jsonl"); const auditDeduplicationMinSize = Number.parseInt(process.env.LLM_AUDIT_DEDUP_MIN_SIZE ?? "500", 10); const auditDeduplicationCacheSize = Number.parseInt(process.env.LLM_AUDIT_DEDUP_CACHE_SIZE ?? "100", 10); const auditDeduplicationSanitize = process.env.LLM_AUDIT_DEDUP_SANITIZE !== "false"; // default true const auditDeduplicationSessionCache = process.env.LLM_AUDIT_DEDUP_SESSION_CACHE !== "false"; // default true // Oversized Error Logging Configuration const oversizedErrorLoggingEnabled = process.env.OVERSIZED_ERROR_LOGGING_ENABLED !== "false"; // default true const oversizedErrorThreshold = Number.parseInt(process.env.OVERSIZED_ERROR_THRESHOLD ?? "200", 10); const oversizedErrorLogDir = process.env.OVERSIZED_ERROR_LOG_DIR ?? path.join(process.cwd(), "logs", "oversized-errors"); const oversizedErrorMaxFiles = Number.parseInt(process.env.OVERSIZED_ERROR_MAX_FILES ?? "100", 10); // Worker Thread Pool Configuration const workerPoolEnabled = process.env.WORKER_POOL_ENABLED !== "false"; // default true const workerPoolSize = Number.parseInt(process.env.WORKER_POOL_SIZE ?? "0", 10); // 0 = auto (CPU cores - 1) const workerTaskTimeoutMs = Number.parseInt(process.env.WORKER_TASK_TIMEOUT_MS ?? "5000", 10); const workerOffloadThresholdBytes = Number.parseInt(process.env.WORKER_OFFLOAD_THRESHOLD_BYTES ?? "10000", 10); var config = { env: process.env.NODE_ENV ?? "production", port: Number.isNaN(port) ? 8080 : port, databricks: { baseUrl: rawBaseUrl, apiKey, endpointPath, url: databricksUrl, }, azureAnthropic: { endpoint: azureAnthropicEndpoint, apiKey: azureAnthropicApiKey, version: azureAnthropicVersion, }, ollama: { endpoint: ollamaEndpoint, model: ollamaModel, timeout: Number.isNaN(ollamaTimeout) ? 120000 : ollamaTimeout, keepAlive: ollamaKeepAlive, embeddingsEndpoint: ollamaEmbeddingsEndpoint, embeddingsModel: ollamaEmbeddingsModel, }, openrouter: { apiKey: openRouterApiKey, model: openRouterModel, embeddingsModel: openRouterEmbeddingsModel, endpoint: openRouterEndpoint, }, azureOpenAI: { endpoint: azureOpenAIEndpoint, apiKey: azureOpenAIApiKey, deployment: azureOpenAIDeployment, apiVersion: azureOpenAIApiVersion }, openai: { apiKey: openAIApiKey, model: openAIModel, endpoint: openAIEndpoint, organization: openAIOrganization, }, llamacpp: { endpoint: llamacppEndpoint, model: llamacppModel, timeout: Number.isNaN(llamacppTimeout) ? 120000 : llamacppTimeout, apiKey: llamacppApiKey, embeddingsEndpoint: llamacppEmbeddingsEndpoint, }, lmstudio: { endpoint: lmstudioEndpoint, model: lmstudioModel, timeout: Number.isNaN(lmstudioTimeout) ? 120000 : lmstudioTimeout, apiKey: lmstudioApiKey, }, bedrock: { region: bedrockRegion, apiKey: bedrockApiKey, modelId: bedrockModelId, }, zai: { apiKey: zaiApiKey, endpoint: zaiEndpoint, model: zaiModel, }, vertex: { apiKey: vertexApiKey, model: vertexModel, }, moonshot: { apiKey: moonshotApiKey, endpoint: moonshotEndpoint, model: moonshotModel, }, codex: { enabled: process.env.CODEX_ENABLED !== "false", binaryPath: process.env.CODEX_BINARY_PATH?.trim() || null, model: process.env.CODEX_MODEL?.trim() || "gpt-5.3-codex", timeout: Number.parseInt(process.env.CODEX_TIMEOUT || "120000", 10) || 120000, }, hotReload: { enabled: hotReloadEnabled, debounceMs: Number.isNaN(hotReloadDebounceMs) ? 1000 : hotReloadDebounceMs, }, modelProvider: { type: modelProvider, defaultModel, suggestionModeModel, fallbackEnabled, ollamaMaxToolsForRouting, openRouterMaxToolsForRouting, fallbackProvider, }, toolExecutionMode, toolResultCompression: { enabled: true, }, caveman: { enabled: cavemanEnabled, level: cavemanLevel, }, server: { jsonLimit: process.env.REQUEST_JSON_LIMIT ?? "1gb", }, rateLimit: { enabled: rateLimitEnabled, windowMs: rateLimitWindow, max: rateLimitMax, keyBy: rateLimitKeyBy, }, logger: { level: process.env.LOG_LEVEL ?? "info", file: { enabled: process.env.LOG_FILE_ENABLED === "true", path: process.env.LOG_FILE_PATH ?? path.join(process.cwd(), "logs", "lynkr.log"), level: process.env.LOG_FILE_LEVEL ?? "debug", // File captures everything frequency: process.env.LOG_FILE_FREQUENCY ?? "daily", // daily | hourly | <milliseconds> maxFiles: parseInt(process.env.LOG_FILE_MAX_FILES ?? "14", 10), }, }, sessionStore: { dbPath: sessionDbPath, }, workspace: { root: workspaceRoot, }, webSearch: { endpoint: defaultWebEndpoint, apiKey: process.env.WEB_SEARCH_API_KEY ?? null, allowedHosts: allowAllWebHosts ? null : Array.from(webAllowedHosts ?? []), allowAllHosts: allowAllWebHosts, enabled: true, timeoutMs: Number.isNaN(webTimeoutMs) ? 10000 : webTimeoutMs, bodyPreviewMax: Number.isNaN(webFetchBodyPreviewMax) ? 10000 : webFetchBodyPreviewMax, retryEnabled: webSearchRetryEnabled, maxRetries: Number.isNaN(webSearchMaxRetries) ? 2 : webSearchMaxRetries, }, tinyfish: { apiKey: tinyfishApiKey, endpoint: tinyfishEndpoint, browserProfile: tinyfishBrowserProfile, timeoutMs: Number.isNaN(tinyfishTimeoutMs) ? 120000 : tinyfishTimeoutMs, proxyEnabled: tinyfishProxyEnabled, proxyCountry: tinyfishProxyCountry, }, policy: { maxStepsPerTurn: policyMaxSteps, // null = no limit maxToolCallsPerTurn: policyMaxToolCalls, // null = no limit maxToolCallsPerRequest: policyMaxToolCalls, // null = no limit (orchestrator uses this name) toolLoopThreshold: Number.isNaN(policyToolLoopThreshold) ? 10 : policyToolLoopThreshold, // Max tool results before force-terminating disallowedTools: policyDisallowedTools, git: { allowPush: policyGitAllowPush, allowPull: policyGitAllowPull, allowCommit: policyGitAllowCommit, testCommand: policyGitTestCommand, requireTests: policyGitRequireTests, commitMessageRegex: policyGitCommitRegex, autoStash: policyGitAutoStash, }, fileAccess: { allowedPaths: policyFileAllowedPaths, blockedPaths: policyFileBlockedPaths, }, safeCommandsEnabled: policySafeCommandsEnabled, safeCommands: policySafeCommandsConfig, }, mcp: { sandbox: { enabled: sandboxEnabled && Boolean(sandboxImage), runtime: sandboxRuntime, image: sandboxImage, containerWorkspace: sandboxContainerWorkspace, mountWorkspace: sandboxMountWorkspace, allowNetworking: sandboxAllowNetworking, networkMode: sandboxNetworkMode, passthroughEnv: sandboxPassthroughEnv, extraMounts: sandboxExtraMounts, defaultTimeoutMs: Number.isNaN(sandboxDefaultTimeoutMs) ? 20000 : sandboxDefaultTimeoutMs, user: sandboxUser, entrypoint: sandboxEntrypoint, reuseSession: sandboxReuseSessions, readOnlyRoot: sandboxReadOnlyRoot, noNewPrivileges: sandboxNoNewPrivileges, dropCapabilities: sandboxDropCapabilities, addCapabilities: sandboxAddCapabilities, memoryLimit: sandboxMemoryLimit, cpuLimit: sandboxCpuLimit, pidsLimit: Number.isNaN(sandboxPidsLimit) ? 100 : sandboxPidsLimit, }, permissions: { mode: ["auto", "require", "deny"].includes(sandboxPermissionMode) ? sandboxPermissionMode : "auto", allow: sandboxPermissionAllow, deny: sandboxPermissionDeny, }, servers: { manifestPath: sandboxManifestPath, manifestDirs: sandboxManifestDirs, }, codeMode: { enabled: process.env.CODE_MODE_ENABLED === 'true', toolListCacheTtl: parseInt(process.env.CODE_MODE_CACHE_TTL, 10) || 60_000, }, }, promptCache: { enabled: promptCacheEnabled, maxEntries: Number.isNaN(promptCacheMaxEntriesRaw) ? 64 : promptCacheMaxEntriesRaw, ttlMs: Number.isNaN(promptCacheTtlRaw) ? 300000 : promptCacheTtlRaw, }, semanticCache: { enabled: process.env.SEMANTIC_CACHE_ENABLED !== 'false', // Disable via env if needed similarityThreshold: parseFloat(process.env.SEMANTIC_CACHE_THRESHOLD || '0.95'), // Higher threshold maxEntries: Number.parseInt(process.env.SEMANTIC_CACHE_MAX_ENTRIES ?? "50", 10), // Reduced from 500 to prevent memory bloat ttlMs: Number.parseInt(process.env.SEMANTIC_CACHE_TTL_MS ?? "300000", 10), // 5 minutes (was 1 hour) }, agents: { enabled: agentsEnabled, maxConcurrent: Number.isNaN(agentsMaxConcurrent) ? 10 : agentsMaxConcurrent, defaultModel: agentsDefaultModel, maxSteps: Number.isNaN(agentsMaxSteps) ? 15 : agentsMaxSteps, timeout: Number.isNaN(agentsTimeout) ? 120000 : agentsTimeout, }, tests: { defaultCommand: testDefaultCommand ? testDefaultCommand.trim() : null, defaultArgs: testDefaultArgs, timeoutMs: Number.isNaN(testTimeoutMs) ? 600000 : testTimeoutMs, sandbox: ["always", "never", "auto"].includes(testSandboxMode) ? testSandboxMode : "auto", coverage: { files: testCoverageFiles, }, profiles: Array.isArray(testProfiles) ? testProfiles : null, }, memory: { enabled: memoryEnabled, retrievalLimit: Number.isNaN(memoryRetrievalLimit) ? 5 : memoryRetrievalLimit, surpriseThreshold: Number.isNaN(memorySurpriseThreshold) ? 0.3 : memorySurpriseThreshold, maxAgeDays: Number.isNaN(memoryMaxAgeDays) ? 90 : memoryMaxAgeDays, maxCount: Number.isNaN(memoryMaxCount) ? 10000 : memoryMaxCount, includeGlobalMemories: memoryIncludeGlobal, injectionFormat: ["system", "assistant_preamble"].includes(memoryInjectionFormat) ? memoryInjectionFormat : "system", format: memoryFormat, dedupEnabled: memoryDedupEnabled, dedupLookback: memoryDedupLookback, extraction: { enabled: memoryExtractionEnabled, }, decay: { enabled: memoryDecayEnabled, halfLifeDays: Number.isNaN(memoryDecayHalfLifeDays) ? 30 : memoryDecayHalfLifeDays, }, }, tokenTracking: { enabled: tokenTrackingEnabled, }, toolTruncation: { enabled: toolTruncationEnabled, }, systemPrompt: { mode: systemPromptMode, toolDescriptions: toolDescriptions, }, historyCompression: { enabled: historyCompressionEnabled, keepRecentTurns: historyKeepRecentTurns, summarizeOlder: historySummarizeOlder, }, tokenBudget: { warning: tokenBudgetWarning, max: tokenBudgetMax, enforcement: tokenBudgetEnforcement, }, toon: { enabled: toonEnabled, minBytes: Number.isNaN(toonMinBytes) ? 4096 : toonMinBytes, failOpen: toonFailOpen, logStats: toonLogStats, }, smartToolSelection: { enabled: true, // HARDCODED - always enabled mode: smartToolSelectionMode, tokenBudget: smartToolSelectionTokenBudget, minimalMode: false, // HARDCODED - disabled }, headroom: { enabled: headroomEnabled, endpoint: headroomEndpoint, timeoutMs: Number.isNaN(headroomTimeoutMs) ? 5000 : headroomTimeoutMs, minTokens: Number.isNaN(headroomMinTokens) ? 500 : headroomMinTokens, mode: headroomMode, docker: { enabled: headroomDockerEnabled, image: headroomDockerImage, containerName: headroomDockerContainerName, port: Number.isNaN(headroomDockerPort) ? 8787 : headroomDockerPort, memoryLimit: headroomDockerMemoryLimit, cpuLimit: headroomDockerCpuLimit, restartPolicy: headroomDockerRestartPolicy, network: headroomDockerNetwork, buildContext: headroomDockerBuildContext, autoBuild: headroomDockerAutoBuild, }, transforms: { smartCrusher: headroomSmartCrusher, smartCrusherMinTokens: Number.isNaN(headroomSmartCrusherMinTokens) ? 200 : headroomSmartCrusherMinTokens, smartCrusherMaxItems: Number.isNaN(headroomSmartCrusherMaxItems) ? 15 : headroomSmartCrusherMaxItems, toolCrusher: headroomToolCrusher, cacheAligner: headroomCacheAligner, rollingWindow: headroomRollingWindow, keepTurns: Number.isNaN(headroomKeepTurns) ? 3 : headroomKeepTurns, }, ccr: { enabled: headroomCcrEnabled, ttlSeconds: Number.isNaN(headroomCcrTtl) ? 300 : headroomCcrTtl, }, llmlingua: { enabled: headroomLlmlingua, device: headroomLlmlinguaDevice, }, provider: headroomProvider, logLevel: headroomLogLevel, }, security: { // Content filtering contentFilterEnabled: process.env.SECURITY_CONTENT_FILTER_ENABLED !== "false", // default true blockOnDetection: process.env.SECURITY_BLOCK_ON_DETECTION !== "false", // default true // Rate limiting rateLimitEnabled: process.env.SECURITY_RATE_LIMIT_ENABLED !== "false", // default true perIpLimit: Number.parseInt(process.env.SECURITY_PER_IP_LIMIT ?? "100", 10), // requests per minute perEndpointLimit: Number.parseInt(process.env.SECURITY_PER_ENDPOINT_LIMIT ?? "1000", 10), // requests per minute // Audit logging auditLogEnabled: process.env.SECURITY_AUDIT_LOG_ENABLED !== "false", // default true auditLogDir: process.env.SECURITY_AUDIT_LOG_DIR ?? path.join(process.cwd(), "logs"), }, audit: { enabled: auditEnabled, logFile: auditLogFile, maxContentLength: { systemPrompt: Number.isNaN(auditMaxSystemLength) ? 2000 : auditMaxSystemLength, userMessages: Number.isNaN(auditMaxUserLength) ? 3000 : auditMaxUserLength, response: Number.isNaN(auditMaxResponseLength) ? 3000 : auditMaxResponseLength, }, annotations: auditAnnotations, rotation: { maxFiles: Number.isNaN(auditMaxFiles) ? 30 : auditMaxFiles, maxSize: auditMaxSize, }, deduplication: { enabled: auditDeduplicationEnabled, dictionaryPath: auditDeduplicationDictPath, minSize: Number.isNaN(auditDeduplicationMinSize) ? 500 : auditDeduplicationMinSize, cacheSize: Number.isNaN(auditDeduplicationCacheSize) ? 100 : auditDeduplicationCacheSize, sanitize: auditDeduplicationSanitize, sessionCache: auditDeduplicationSessionCache, }, }, oversizedErrorLogging: { enabled: oversizedErrorLoggingEnabled, threshold: oversizedErrorThreshold, logDir: oversizedErrorLogDir, maxFiles: oversizedErrorMaxFiles, }, workerPool: { enabled: workerPoolEnabled, size: workerPoolSize || 0, // 0 = auto taskTimeoutMs: Number.isNaN(workerTaskTimeoutMs) ? 5000 : workerTaskTimeoutMs, offloadThresholdBytes: Number.isNaN(workerOffloadThresholdBytes) ? 10000 : workerOffloadThresholdBytes, }, // Intelligent Routing routing: { weightedScoring: true, costOptimization: true, agenticDetection: true, // Embed an interaction block in the response body so the user can // see *why* a particular tier/provider was chosen. visibleInteraction: process.env.LYNKR_VISIBLE_ROUTING === 'true', // Run user-supplied preflight commands before invoking the model. // If all exit 0, short-circuit the request with zero LLM cost. preflightEnabled: process.env.LYNKR_PREFLIGHT_ENABLED === 'true', preflightTimeoutMs: Number(process.env.LYNKR_PREFLIGHT_TIMEOUT_MS) || 120000, }, // Model Tier Configuration (REQUIRED) // Format: TIER_<LEVEL>=provider:model (e.g., TIER_SIMPLE=ollama:llama3.2) modelTiers: { enabled: true, SIMPLE: process.env.TIER_SIMPLE?.trim() || null, MEDIUM: process.env.TIER_MEDIUM?.trim() || null, COMPLEX: process.env.TIER_COMPLEX?.trim() || null, REASONING: process.env.TIER_REASONING?.trim() || null, }, // Cluster mode (multi-core scaling for 50+ concurrent users) cluster: { enabled: process.env.CLUSTER_ENABLED === 'true', workers: process.env.CLUSTER_WORKERS || 'auto', }, // Graphify knowledge graph integration (structural analysis) codeGraph: { enabled: process.env.CODE_GRAPH_ENABLED === 'true', command: process.env.CODE_GRAPH_COMMAND || 'graphify', workspace: process.env.CODE_GRAPH_WORKSPACE || process.cwd(), timeout: parseInt(process.env.CODE_GRAPH_TIMEOUT, 10) || 10000, }, // Large payload optimization (skip cloning media blocks that get discarded) largePayload: { enabled: process.env.LARGE_PAYLOAD_OPTIMIZATION !== 'false', threshold: parseInt(process.env.LARGE_PAYLOAD_THRESHOLD, 10) || 1_048_576, }, // OpenClaw integration openclaw: { enabled: process.env.OPENCLAW_MODE === "true", }, }; /** * Reload configuration from environment * Called by hot reload watcher when .env changes */ function reloadConfig() { // Re-parse .env file dotenv.config({ override: true }); // Update mutable config values (those that can safely change at runtime) // API keys and endpoints config.databricks.apiKey = process.env.DATABRICKS_API_KEY; config.azureAnthropic.apiKey = process.env.AZURE_ANTHROPIC_API_KEY ?? null; config.ollama.model = process.env.OLLAMA_MODEL ?? "qwen2.5-coder:7b"; config.openrouter.apiKey = process.env.OPENROUTER_API_KEY ?? null; config.openrouter.model = process.env.OPENROUTER_MODEL ?? "openai/gpt-4o-mini"; config.azureOpenAI.apiKey = process.env.AZURE_OPENAI_API_KEY?.trim() || null; config.openai.apiKey = process.env.OPENAI_API_KEY?.trim() || null; config.bedrock.apiKey = process.env.AWS_BEDROCK_API_KEY?.trim() || null; config.zai.apiKey = process.env.ZAI_API_KEY?.trim() || null; config.zai.model = process.env.ZAI_MODEL?.trim() || "GLM-4.7"; config.vertex.apiKey = process.env.VERTEX_API_KEY?.trim() || process.env.GOOGLE_API_KEY?.trim() || null; config.vertex.model = process.env.VERTEX_MODEL?.trim() || "gemini-2.0-flash"; config.moonshot.apiKey = process.env.MOONSHOT_API_KEY?.trim() || null; config.moonshot.model = process.env.MOONSHOT_MODEL?.trim() || "kimi-k2-turbo-preview"; // Model provider settings const newProvider = (process.env.MODEL_PROVIDER ?? "databricks").toLowerCase(); if (SUPPORTED_MODEL_PROVIDERS.has(newProvider)) { config.modelProvider.type = newProvider; } config.modelProvider.fallbackEnabled = process.env.FALLBACK_ENABLED !== "false"; config.modelProvider.fallbackProvider = (process.env.FALLBACK_PROVIDER ?? "databricks").toLowerCase(); config.modelProvider.suggestionModeModel = (process.env.SUGGESTION_MODE_MODEL ?? "default").trim(); // TinyFish config reload config.tinyfish.apiKey = process.env.TINYFISH_API_KEY?.trim() || null; config.tinyfish.browserProfile = process.env.TINYFISH_BROWSER_PROFILE?.trim() || "lite"; config.toon.enabled = process.env.TOON_ENABLED === "true"; const newToonMinBytes = Number.parseInt(process.env.TOON_MIN_BYTES ?? "4096", 10); config.toon.minBytes = Number.isNaN(newToonMinBytes) ? 4096 : newToonMinBytes; config.toon.failOpen = process.env.TOON_FAIL_OPEN !== "false"; config.toon.logStats = process.env.TOON_LOG_STATS !== "false"; // Tier routing (critical for fixing model name issues without restart) config.modelTiers.SIMPLE = process.env.TIER_SIMPLE?.trim() || null; config.modelTiers.MEDIUM = process.env.TIER_MEDIUM?.trim() || null; config.modelTiers.COMPLEX = process.env.TIER_COMPLEX?.trim() || null; config.modelTiers.REASONING = process.env.TIER_REASONING?.trim() || null; config.modelTiers.enabled = !!(config.modelTiers.SIMPLE && config.modelTiers.MEDIUM && config.modelTiers.COMPLEX && config.modelTiers.REASONING); // Ollama model config.ollama.endpoint = process.env.OLLAMA_ENDPOINT ?? config.ollama.endpoint; // OpenClaw config.openclaw.enabled = process.env.OPENCLAW_MODE === "true"; // Graphify config.codeGraph.enabled = process.env.CODE_GRAPH_ENABLED === 'true'; // Code Mode config.mcp.codeMode.enabled = process.env.CODE_MODE_ENABLED === 'true'; // Log level config.logger.level = process.env.LOG_LEVEL ?? "info"; // Reset circuit breakers so stale OPEN states don't persist try { const { getCircuitBreakerRegistry } = require('../clients/circuit-breaker'); getCircuitBreakerRegistry().resetAll(); console.log("[CONFIG] Circuit breakers reset"); } catch (e) { // Ignore if not yet initialized } console.log("[CONFIG] Configuration reloaded from environment"); return config; } // Make config mutable for hot reload config.reloadConfig = reloadConfig; /** * Check if any TIER_* value references Ollama (starts with "ollama:") * Used by server.js to decide whether to wait for Ollama at startup. */ config.tiersReferenceOllama = function tiersReferenceOllama() { const tiers = config.modelTiers; if (!tiers?.enabled) return false; return [tiers.SIMPLE, tiers.MEDIUM, tiers.COMPLEX, tiers.REASONING] .some(v => typeof v === 'string' && v.startsWith('ollama:')); }; // Validate TIER_* configuration (warn if missing, don't crash) const missingTiers = []; if (!config.modelTiers.SIMPLE) missingTiers.push('TIER_SIMPLE'); if (!config.modelTiers.MEDIUM) missingTiers.push('TIER_MEDIUM'); if (!config.modelTiers.COMPLEX) missingTiers.push('TIER_COMPLEX'); if (!config.modelTiers.REASONING) missingTiers.push('TIER_REASONING'); if (missingTiers.length > 0) { config.modelTiers.enabled = false; console.warn( `[WARN] Missing tier configuration: ${missingTiers.join(', ')} — tiered routing disabled.\n` + ` Set TIER_<LEVEL>=provider:model to enable (e.g., TIER_SIMPLE=ollama:llama3.2)` ); } module.exports = config;