/** * Configuration loading. * * The adapter accepts config from two sources: * 1. Environment variables (loaded from process.env at module init) * 2. A plain object passed in (e.g. from Paperclip's adapter config UI) * * For env-var resolution, keys named `*_ENV` are treated as env-var name * pointers (e.g. `MINIMAX_API_KEY_ENV=MINIMAX_PROD_KEY` → reads the * MINIMAX_PROD_KEY env var instead). This is how Paperclip's secret * binding works. */ import { z } from "zod"; import type { AdapterConfig } from "./types.js"; export type ResolvedConfig = AdapterConfig; export const configSchema = z.object({ apiKey: z.string().default(""), baseUrl: z.string().url().default("https://api.minimax.chat/v1"), model: z.string().min(1).default("MiniMax-M3"), priceInputPer1M: z.number().nonnegative().default(1.0), priceOutputPer1M: z.number().nonnegative().default(3.0), paperclipApiKey: z.string().default(""), paperclipApiUrl: z.string().url().default("http://localhost:3100"), maxToolCallsPerTurn: z.number().int().positive().default(10), maxTurns: z.number().int().positive().default(8), enableStreaming: z.boolean().default(true), skillsDir: z.string().default(""), approvalGated: z.boolean().default(true), workspacePath: z.string().default(""), }); /** * Resolve a string of the form `name` or `name_ENV=OTHER_NAME` to an * actual env var value. Falls back to the original string if neither * the *key or the *name resolves. */ function resolveEnv(input: string | undefined, fallback = ""): string { if (!input) return fallback; const m = input.match(/^(.*)_ENV=(.*)$/); if (m) { const [, , otherName] = m; const v = process.env[otherName]; return v && v.length > 0 ? v : fallback; } const v = process.env[input]; return v && v.length > 0 ? v : fallback; } function num(input: string | undefined, fallback: number): number { if (!input) return fallback; const n = Number(input); return Number.isFinite(n) ? n : fallback; } function bool(input: string | undefined, fallback: boolean): boolean { if (!input) return fallback; return input === "1" || input.toLowerCase() === "true"; } /** * Build a full AdapterConfig from (a) the env, (b) any explicit * overrides. `overrides` win. */ export function loadConfig(overrides: Partial = {}): AdapterConfig { // Try the explicit env var first, fall back to the friendly name, // then to the Paperclip convention of `*_ENV` pointing at a stored secret. const apiKey = resolveEnv(overrides.apiKey) || resolveEnv(process.env.MINIMAX_API_KEY) || resolveEnv(process.env.MINIMAX_API_KEY_ENV); const paperclipApiKey = resolveEnv(overrides.paperclipApiKey) || process.env.PAPERCLIP_API_KEY || ""; const merged: AdapterConfig = { apiKey, baseUrl: overrides.baseUrl ?? process.env.MINIMAX_BASE_URL ?? "https://api.minimax.chat/v1", model: overrides.model ?? process.env.MINIMAX_MODEL ?? "MiniMax-M3", priceInputPer1M: overrides.priceInputPer1M ?? num(process.env.MINIMAX_PRICE_INPUT_PER_1M, 1.0), priceOutputPer1M: overrides.priceOutputPer1M ?? num(process.env.MINIMAX_PRICE_OUTPUT_PER_1M, 3.0), paperclipApiKey, paperclipApiUrl: overrides.paperclipApiUrl ?? process.env.PAPERCLIP_API_URL ?? "http://localhost:3100", maxToolCallsPerTurn: overrides.maxToolCallsPerTurn ?? num(process.env.MAX_TOOL_CALLS_PER_TURN, 10), maxTurns: overrides.maxTurns ?? num(process.env.MAX_TURNS, 8), enableStreaming: overrides.enableStreaming ?? bool(process.env.ENABLE_STREAMING, true), skillsDir: overrides.skillsDir ?? process.env.SKILLS_DIR ?? "", approvalGated: overrides.approvalGated ?? bool(process.env.MINIMAX_APPROVAL_GATED, true), workspacePath: overrides.workspacePath ?? process.env.MINIMAX_WORKSPACE_PATH ?? "", }; // Validate. Throws ZodError on bad input — we want the loudest possible // failure for misconfiguration since silent degradation in an agent // heartbeat is the worst possible failure mode. return configSchema.parse(merged); }