import { type ClawdbotConfig, loadConfig } from "../config/config.js"; import { resolveClawdbotAgentDir } from "./agent-paths.js"; import { ensureClawdbotModelsJson } from "./models-config.js"; export type ModelCatalogEntry = { id: string; name: string; provider: string; contextWindow?: number; reasoning?: boolean; }; type DiscoveredModel = { id: string; name?: string; provider: string; contextWindow?: number; reasoning?: boolean; }; type PiSdkModule = typeof import("@mariozechner/pi-coding-agent"); let modelCatalogPromise: Promise | null = null; let hasLoggedModelCatalogError = false; const defaultImportPiSdk = () => import("@mariozechner/pi-coding-agent"); let importPiSdk = defaultImportPiSdk; export function resetModelCatalogCacheForTest() { modelCatalogPromise = null; hasLoggedModelCatalogError = false; importPiSdk = defaultImportPiSdk; } // Test-only escape hatch: allow mocking the dynamic import to simulate transient failures. export function __setModelCatalogImportForTest(loader?: () => Promise) { importPiSdk = loader ?? defaultImportPiSdk; } export async function loadModelCatalog(params?: { config?: ClawdbotConfig; useCache?: boolean; }): Promise { if (params?.useCache === false) { modelCatalogPromise = null; } if (modelCatalogPromise) return modelCatalogPromise; modelCatalogPromise = (async () => { const models: ModelCatalogEntry[] = []; const sortModels = (entries: ModelCatalogEntry[]) => entries.sort((a, b) => { const p = a.provider.localeCompare(b.provider); if (p !== 0) return p; return a.name.localeCompare(b.name); }); try { const cfg = params?.config ?? loadConfig(); await ensureClawdbotModelsJson(cfg); // IMPORTANT: keep the dynamic import *inside* the try/catch. // If this fails once (e.g. during a pnpm install that temporarily swaps node_modules), // we must not poison the cache with a rejected promise (otherwise all channel handlers // will keep failing until restart). const piSdk = await importPiSdk(); const agentDir = resolveClawdbotAgentDir(); const authStorage = piSdk.discoverAuthStorage(agentDir); const registry = piSdk.discoverModels(authStorage, agentDir) as | { getAll: () => Array; } | Array; const entries = Array.isArray(registry) ? registry : registry.getAll(); for (const entry of entries) { const id = String(entry?.id ?? "").trim(); if (!id) continue; const provider = String(entry?.provider ?? "").trim(); if (!provider) continue; const name = String(entry?.name ?? id).trim() || id; const contextWindow = typeof entry?.contextWindow === "number" && entry.contextWindow > 0 ? entry.contextWindow : undefined; const reasoning = typeof entry?.reasoning === "boolean" ? entry.reasoning : undefined; models.push({ id, name, provider, contextWindow, reasoning }); } if (models.length === 0) { // If we found nothing, don't cache this result so we can try again. modelCatalogPromise = null; } return sortModels(models); } catch (error) { if (!hasLoggedModelCatalogError) { hasLoggedModelCatalogError = true; console.warn(`[model-catalog] Failed to load model catalog: ${String(error)}`); } // Don't poison the cache on transient dependency/filesystem issues. modelCatalogPromise = null; if (models.length > 0) { return sortModels(models); } return []; } })(); return modelCatalogPromise; }