import { z } from 'zod'; const ConfigSchema = z.object({ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), PORT: z.coerce.number().int().positive().default(3000), HOST: z.string().default('0.0.0.0'), DATABASE_URL: z.string().url(), LLAMA_SWAP_URL: z.string().url(), PROJECT_ROOT_WHITELIST: z.string().default('/opt'), BOOTSTRAP_ROOT: z.string().default('/opt/projects'), DEFAULT_MODEL: z.string().default('sam-desktop/qwen3.6-35b-a3b'), LOG_LEVEL: z.string().default('info'), // v1.11.8: SearXNG JSON endpoint for web_search / web_fetch tools. // Defaults to the internal Tailscale Fathom URL (bypasses Authelia). // The public search.indifferentketchup.com URL would 302 to auth and // is unusable from the server context — keep the internal one. SEARXNG_URL: z.string().url().default('http://100.114.205.53:8888'), GITEA_BASE_URL: z.string().url().default('https://git.indifferentketchup.com'), GITEA_USER: z.string().default('indifferentketchup'), GITEA_TOKEN: z.string().optional(), // v1.15.0-mcp-multi: path to the MCP config JSON file. Default /data/mcp.json // (bind-mounted alongside AGENTS.md). File missing = no MCP (opt-in). MCP_CONFIG_PATH: z.string().optional(), // v2.0.5: cheaper model for titles, summaries, labeling. Falls back to // session model (auto_name) or DEFAULT_MODEL when unset. FAST_MODEL: z.string().optional(), TASK_MODEL_URL: z.string().url().optional(), // vDeepSeek: DeepSeek API key for direct API access. When set, models // with IDs starting with 'deepseek-' route through DeepSeek's API instead // of llama-swap. Defaults to empty (DeepSeek routing disabled). DEEPSEEK_API_KEY: z.string().optional(), // Optional base URL override for DeepSeek API. Defaults to api.deepseek.com. DEEPSEEK_BASE_URL: z.string().url().default('https://api.deepseek.com'), // Beta endpoint for experimental features (strict tools, prefix completion, etc.). // Defaults to api.deepseek.com/beta. When set, deepseek calls with tools or // prefix content route through this endpoint. DEEPSEEK_BETA_BASE_URL: z.string().url().default('https://api.deepseek.com/beta'), // Hosted Anthropic Claude. When set, models with provider id "anthropic" // (or bare "claude-*" ids) route through the Anthropic Messages API via // @ai-sdk/anthropic instead of llama-swap. Unset = Claude routing disabled. ANTHROPIC_API_KEY: z.string().optional(), ANTHROPIC_BASE_URL: z.string().url().optional(), // vWhale hooks: path to hooks JSON config file. Missing file = no hooks. HOOKS_CONFIG_PATH: z.string().default('/data/hooks.json'), // vMultiProvider: path to the local providers config JSON file. Missing file // = legacy synthesis from LLAMA_SWAP_URL. LLAMA_PROVIDERS_PATH: z.string().optional(), BOOCONTROL_URL: z.string().url().optional(), }); export type Config = z.infer; let cached: Config | null = null; export function loadConfig(): Config { if (cached) return cached; const parsed = ConfigSchema.safeParse(process.env); if (!parsed.success) { console.error('Invalid environment configuration:'); console.error(parsed.error.flatten().fieldErrors); process.exit(1); } cached = parsed.data; return cached; }