// apps/api/src/config.ts // Centralized environment-variable parsing. All API keys live ONLY here. // Never exposed to clients, never written to logs (pino redaction in log.ts). import { z } from "zod"; const ConfigSchema = z.object({ port: z.number().int().positive().default(3001), logLevel: z .enum(["fatal", "error", "warn", "info", "debug", "trace"]) .default("info"), // Default analyzer model defaultModel: z.string().default("deepseek-v4-pro"), defaultBaseURL: z .string() .default("https://api.deepseek.com"), defaultApiKey: z.string().default(""), defaultApiMode: z.enum(["chat", "responses"]).default("chat"), // Judge model judgeModel: z.string().default("gpt-5.5"), judgeBaseURL: z .string() .default("https://app-hk.ppapi.ai/v1/chat/completions"), judgeApiKey: z.string().default(""), judgeApiMode: z.enum(["chat", "responses"]).default("chat"), // Rubric generation model (falls back to default if unset) rubricGenModel: z.string().default("gpt-5.5"), rubricGenBaseURL: z .string() .default("https://app-hk.ppapi.ai/v1/chat/completions"), rubricGenApiKey: z.string().default(""), rubricGenApiMode: z.enum(["chat", "responses"]).default("chat"), // Storage sqlitePath: z.string().default("/app/data/tasks.db"), databaseUrl: z.string().default(""), storeBackend: z.enum(["sqlite", "postgres"]).default("sqlite"), uploadDir: z.string().default("/app/data/uploads"), exportDir: z.string().default("/app/data/exports"), // Upload limits maxUploadMB: z.number().int().positive().default(100), maxBatchFiles: z.number().int().positive().default(20), itemMaxAttempts: z.number().int().positive().default(3), globalLlmConcurrency: z.number().int().nonnegative().default(0), // Optional outbound proxy for AI provider calls (replaces legacy proxy.mjs) upstreamProxy: z.string().default(""), }); export type Config = z.infer; function envInt(value: string | undefined, fallback: number): number { if (!value) return fallback; const n = Number(value); return Number.isFinite(n) ? Math.trunc(n) : fallback; } export function loadConfig(): Config { const parsed = ConfigSchema.parse({ port: envInt(process.env.PORT, 3001), logLevel: process.env.LOG_LEVEL, defaultModel: process.env.DEFAULT_MODEL, defaultBaseURL: process.env.DEFAULT_BASE_URL, defaultApiKey: process.env.DEFAULT_API_KEY, defaultApiMode: process.env.DEFAULT_API_MODE, judgeModel: process.env.JUDGE_MODEL, judgeBaseURL: process.env.JUDGE_BASE_URL, judgeApiKey: process.env.JUDGE_API_KEY, judgeApiMode: process.env.JUDGE_API_MODE, rubricGenModel: process.env.RUBRIC_GEN_MODEL, rubricGenBaseURL: process.env.RUBRIC_GEN_BASE_URL, rubricGenApiKey: process.env.RUBRIC_GEN_API_KEY, rubricGenApiMode: process.env.RUBRIC_GEN_API_MODE, sqlitePath: process.env.SQLITE_PATH, databaseUrl: process.env.DATABASE_URL, storeBackend: process.env.STORE_BACKEND, uploadDir: process.env.UPLOAD_DIR, exportDir: process.env.EXPORT_DIR, maxUploadMB: envInt(process.env.MAX_UPLOAD_MB, 100), maxBatchFiles: envInt(process.env.MAX_BATCH_FILES, 20), itemMaxAttempts: envInt(process.env.ITEM_MAX_ATTEMPTS, 3), globalLlmConcurrency: envInt(process.env.GLOBAL_LLM_CONCURRENCY, 0), upstreamProxy: process.env.UPSTREAM_PROXY, }); return parsed; }