import { existsSync, readFileSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import type { ConfigResult, ModelProfile, RouterConfig, ThinkingLevel } from "./types.js"; export const DEFAULT_CONFIG: RouterConfig = { enabled: true, models: { fast: "openai-codex/gpt-5.6-luna", balanced: "openai-codex/gpt-5.6-terra", advanced: "openai-codex/gpt-5.6-sol", }, defaultThinking: "medium", advancedThreshold: 70, hysteresis: 12, switchPolicy: "stable", debug: false, }; const thinkingLevels: readonly ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; const profiles: readonly ModelProfile[] = ["fast", "balanced", "advanced"]; type Input = Record; function record(value: unknown): Input | undefined { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Input : undefined; } export function parseConfig(value: unknown, env: Readonly> = {}): ConfigResult { const input = record(value); const warnings: string[] = []; const config: RouterConfig = { ...DEFAULT_CONFIG, models: { ...DEFAULT_CONFIG.models }, }; if (value !== undefined && !input) warnings.push("configuration must be an object"); if (!input) return applyEnvironment(config, env, warnings); if (typeof input.enabled === "boolean") config.enabled = input.enabled; else if (input.enabled !== undefined) warnings.push("enabled must be a boolean"); const inputModels = record(input.models); if (input.models !== undefined && !inputModels) warnings.push("models must be an object"); for (const profile of profiles) { const model = inputModels?.[profile]; if (model === undefined) continue; if (typeof model === "string" && model.trim()) config.models[profile] = model.trim(); else warnings.push(`models.${profile} must be a non-empty string`); } if (typeof input.defaultThinking === "string" && thinkingLevels.includes(input.defaultThinking as ThinkingLevel)) { config.defaultThinking = input.defaultThinking as ThinkingLevel; } else if (input.defaultThinking !== undefined) warnings.push("defaultThinking must be a supported thinking level"); for (const field of ["advancedThreshold", "hysteresis"] as const) { const number = input[field]; if (number === undefined) continue; if (typeof number === "number" && Number.isFinite(number) && number >= 1 && number <= 100) config[field] = number; else warnings.push(`${field} must be a number between 1 and 100`); } if (input.switchPolicy === "stable" || input.switchPolicy === "always") config.switchPolicy = input.switchPolicy; else if (input.switchPolicy !== undefined) warnings.push("switchPolicy must be stable or always"); if (typeof input.debug === "boolean") config.debug = input.debug; else if (input.debug !== undefined) warnings.push("debug must be a boolean"); return applyEnvironment(config, env, warnings); } function applyEnvironment(config: RouterConfig, env: Readonly>, warnings: string[]): ConfigResult { for (const profile of profiles) { const value = env[`PI_CODEX_ROUTER_${profile.toUpperCase()}_MODEL`]; if (value?.trim()) config.models[profile] = value.trim(); } return { config, warnings }; } export interface ConfigPaths { global: string; project: string; } export function defaultConfigPaths(cwd: string): ConfigPaths { return { global: join(homedir(), ".pi", "agent", "codex-router.json"), project: join(cwd, ".pi", "codex-router.json"), }; } function readJson(path: string, label: string, warnings: string[]): unknown { if (!existsSync(path)) return undefined; try { return JSON.parse(readFileSync(path, "utf8")); } catch { warnings.push(`${label} configuration could not be read`); return undefined; } } export function loadConfig(cwd: string, paths = defaultConfigPaths(cwd), env: Readonly> = process.env): ConfigResult { const warnings: string[] = []; const global = readJson(paths.global, "global", warnings); const project = readJson(paths.project, "project", warnings); const globalRecord = record(global); const projectRecord = record(project); if (global !== undefined && !globalRecord) warnings.push("global configuration must be an object"); if (project !== undefined && !projectRecord) warnings.push("project configuration must be an object"); const merged = { ...globalRecord, ...projectRecord, models: { ...record(globalRecord?.models), ...record(projectRecord?.models) }, }; const result = parseConfig(merged, env); return { config: result.config, warnings: [...warnings, ...result.warnings] }; }