import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; import { writePrivateFileAtomicSync } from "./atomic-file"; import { invalidateModelsCache } from "./models-cache"; const MODEL_COST_KEYS = ["input", "output", "cacheRead", "cacheWrite"] as const; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function normalizeModelCost(value: unknown): Record | undefined { if (!isRecord(value)) return undefined; const providedKeys = MODEL_COST_KEYS.filter((key) => value[key] !== undefined); if (providedKeys.length === 0) return undefined; if (providedKeys.some((key) => ( typeof value[key] !== "number" || !Number.isFinite(value[key]) ))) return undefined; return Object.fromEntries([ ...Object.entries(value), ...MODEL_COST_KEYS.map((key) => [key, value[key] ?? 0]), ]); } /** Complete partial cost groups with zero; omit a cost group only when it is empty. */ export function normalizeModelsConfigCosts( data: Record, ): Record { const normalized = structuredClone(data); if (!isRecord(normalized.providers)) return normalized; for (const provider of Object.values(normalized.providers)) { if (!isRecord(provider) || !Array.isArray(provider.models)) continue; for (const model of provider.models) { if (!isRecord(model) || !("cost" in model)) continue; const cost = normalizeModelCost(model.cost); if (cost) model.cost = cost; else delete model.cost; } } return normalized; } function sanitizeModelsConfig(data: Record): Record { if (!isRecord(data.providers)) return data; const providers = Object.fromEntries(Object.entries(data.providers).map(([providerId, provider]) => { if (!isRecord(provider) || !Array.isArray(provider.models)) return [providerId, provider]; const models = provider.models.filter((model) => ( !isRecord(model) || typeof model.id !== "string" || model.id.trim().length > 0 )); return [providerId, { ...provider, models }]; })); return { ...data, providers }; } export function getModelsConfigPath(): string { return join(getAgentDir(), "models.json"); } export function readModelsConfig( modelsPath = getModelsConfigPath(), ): Record { if (!existsSync(modelsPath)) return { providers: {} }; try { return JSON.parse(readFileSync(modelsPath, "utf8")) as Record; } catch { return { providers: {} }; } } export function writeModelsConfig( data: Record, modelsPath = getModelsConfigPath(), ): void { const dir = dirname(modelsPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); const normalized = normalizeModelsConfigCosts(sanitizeModelsConfig(data)); writePrivateFileAtomicSync(modelsPath, JSON.stringify(normalized, null, 2)); invalidateModelsCache(); }