File size: 3,407 Bytes
c4ae742
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// 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<typeof ConfigSchema>;

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;
}