import fs from "node:fs/promises"; import path from "node:path"; import { type ClawdbotConfig, loadConfig } from "../config/config.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; import { ensureAuthProfileStore, listProfilesForProvider, } from "./auth-profiles.js"; import { resolveEnvApiKey } from "./model-auth.js"; type ModelsConfig = NonNullable; type ProviderConfig = NonNullable[string]; const DEFAULT_MODE: NonNullable = "merge"; const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1"; const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000; const MINIMAX_DEFAULT_MAX_TOKENS = 8192; // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. const MINIMAX_API_COST = { input: 15, output: 60, cacheRead: 2, cacheWrite: 10, }; function isRecord(value: unknown): value is Record { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function normalizeGoogleModelId(id: string): string { if (id === "gemini-3-pro") return "gemini-3-pro-preview"; if (id === "gemini-3-flash") return "gemini-3-flash-preview"; return id; } function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig { let mutated = false; const models = provider.models.map((model) => { const nextId = normalizeGoogleModelId(model.id); if (nextId === model.id) return model; mutated = true; return { ...model, id: nextId }; }); return mutated ? { ...provider, models } : provider; } function normalizeProviders( providers: ModelsConfig["providers"], ): ModelsConfig["providers"] { if (!providers) return providers; let mutated = false; const next: Record = {}; for (const [key, provider] of Object.entries(providers)) { const normalized = key === "google" ? normalizeGoogleProvider(provider) : provider; if (normalized !== provider) mutated = true; next[key] = normalized; } return mutated ? next : providers; } async function readJson(pathname: string): Promise { try { const raw = await fs.readFile(pathname, "utf8"); return JSON.parse(raw) as unknown; } catch { return null; } } function buildMinimaxApiProvider(): ProviderConfig { return { baseUrl: MINIMAX_API_BASE_URL, api: "anthropic-messages", models: [ { id: MINIMAX_DEFAULT_MODEL_ID, name: "MiniMax M2.1", reasoning: false, input: ["text"], cost: MINIMAX_API_COST, contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, }, ], }; } function resolveImplicitProviders(params: { cfg: ClawdbotConfig; agentDir: string; }): ModelsConfig["providers"] { const providers: Record = {}; const minimaxEnv = resolveEnvApiKey("minimax"); const authStore = ensureAuthProfileStore(params.agentDir); const hasMinimaxProfile = listProfilesForProvider(authStore, "minimax").length > 0; if (minimaxEnv || hasMinimaxProfile) { providers.minimax = buildMinimaxApiProvider(); } return providers; } export async function ensureClawdbotModelsJson( config?: ClawdbotConfig, agentDirOverride?: string, ): Promise<{ agentDir: string; wrote: boolean }> { const cfg = config ?? loadConfig(); const agentDir = agentDirOverride?.trim() ? agentDirOverride.trim() : resolveClawdbotAgentDir(); const configuredProviders = cfg.models?.providers ?? {}; const implicitProviders = resolveImplicitProviders({ cfg, agentDir }); const providers = { ...implicitProviders, ...configuredProviders }; if (Object.keys(providers).length === 0) { return { agentDir, wrote: false }; } const mode = cfg.models?.mode ?? DEFAULT_MODE; const targetPath = path.join(agentDir, "models.json"); let mergedProviders = providers; let existingRaw = ""; if (mode === "merge") { const existing = await readJson(targetPath); if (isRecord(existing) && isRecord(existing.providers)) { const existingProviders = existing.providers as Record< string, NonNullable[string] >; mergedProviders = { ...existingProviders, ...providers }; } } const normalizedProviders = normalizeProviders(mergedProviders); const next = `${JSON.stringify({ providers: normalizedProviders }, null, 2)}\n`; try { existingRaw = await fs.readFile(targetPath, "utf8"); } catch { existingRaw = ""; } if (existingRaw === next) { return { agentDir, wrote: false }; } await fs.mkdir(agentDir, { recursive: true, mode: 0o700 }); await fs.writeFile(targetPath, next, { mode: 0o600 }); return { agentDir, wrote: true }; }