export interface ModelsData { models: Record; modelList: { id: string; name: string; provider: string }[]; defaultModel: { provider: string; modelId: string } | null; thinkingLevels: Record; thinkingLevelMaps: Record>; /** `provider/modelId` → thinking level pinned by an `enabledModels` `:level` suffix. */ thinkingLevelPins: Record; modelError?: string; /** Warnings from resolving the `enabledModels` scope (e.g. a pattern matched nothing). */ modelScopeWarnings?: string[]; } interface ModelsCacheState { entries: Map; inFlight: Map>; generation: number; } declare global { var __piModelsCacheState: ModelsCacheState | undefined; } const MODELS_CACHE_TTL_MS = 60_000; const MAX_MODELS_CACHE_ENTRIES = 32; // Never interpolate the caught error here; SDK errors can contain paths and provider details. const SAFE_MODEL_LOAD_FAILURE_MESSAGE = "Model list is temporarily unavailable. Check your configuration and try again."; function getModelsCacheState(): ModelsCacheState { if (!globalThis.__piModelsCacheState) { globalThis.__piModelsCacheState = { entries: new Map(), inFlight: new Map(), generation: 0, }; } return globalThis.__piModelsCacheState; } export function invalidateModelsCache(): void { const state = getModelsCacheState(); state.generation += 1; state.entries.clear(); state.inFlight.clear(); } export function withModelRuntimeError(data: ModelsData, modelError: string | undefined): ModelsData { return modelError ? { ...data, modelError } : data; } export function withSafeModelLoadFailure(data: ModelsData): ModelsData { return { ...data, modelError: SAFE_MODEL_LOAD_FAILURE_MESSAGE }; } export function loadModelsWithCache(cwd: string, loader: () => Promise): Promise { const state = getModelsCacheState(); const cached = state.entries.get(cwd); if (cached) { if (cached.expiresAt > Date.now()) return Promise.resolve(cached.data); state.entries.delete(cwd); } const existingLoad = state.inFlight.get(cwd); if (existingLoad) return existingLoad; const generation = state.generation; const loadPromise: Promise = Promise.resolve() .then(loader) .then((data) => { if (state.generation === generation && state.inFlight.get(cwd) === loadPromise) { const now = Date.now(); for (const [key, entry] of state.entries) { if (entry.expiresAt <= now) state.entries.delete(key); } while (state.entries.size >= MAX_MODELS_CACHE_ENTRIES) { const oldestKey = state.entries.keys().next().value; if (oldestKey === undefined) break; state.entries.delete(oldestKey); } state.entries.set(cwd, { data, expiresAt: now + MODELS_CACHE_TTL_MS }); } return data; }) .finally(() => { if (state.inFlight.get(cwd) === loadPromise) state.inFlight.delete(cwd); }); state.inFlight.set(cwd, loadPromise); return loadPromise; }