import { stat } from "fs/promises"; import { resolve } from "path"; import { createAgentSessionServices, getAgentDir, type SettingsManager } from "@earendil-works/pi-coding-agent"; import { getSupportedThinkingLevels } from "@earendil-works/pi-ai"; import { loadModelsWithCache, withModelRuntimeError, withSafeModelLoadFailure, type ModelsData, } from "@/lib/models-cache"; import { resolveVisibleModels, selectInitialModelScope } from "@/lib/model-scope"; import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access"; import { projectTrustReloadOptions } from "@/lib/project-trust"; export const dynamic = "force-dynamic"; const modelNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); function compareModelEntries( a: { id: string; name: string; provider: string }, b: { id: string; name: string; provider: string } ): number { return modelNameCollator.compare(a.name || a.id, b.name || b.id) || modelNameCollator.compare(a.provider, b.provider) || modelNameCollator.compare(a.id, b.id); } async function loadModels(cwd: string): Promise { const nameMap = new Map(); let modelList: { id: string; name: string; provider: string }[] = []; let defaultModel: { provider: string; modelId: string } | null = null; const thinkingLevels: Record = {}; const thinkingLevelMaps: Record> = {}; const agentDir = getAgentDir(); // Gate untrusted project extensions: enumerating models still imports and // runs a repository's .pi/extensions factories, so honor project trust here // too (see lib/project-trust.ts, #236). const trustReloadOptions = projectTrustReloadOptions(cwd, agentDir); const services = await createAgentSessionServices({ cwd, agentDir, ...(trustReloadOptions ? { resourceLoaderReloadOptions: trustReloadOptions } : {}), }); const modelError = services.modelRuntime.getError(); const settings: SettingsManager = services.settingsManager; // `enabledModels` supports globs and fuzzy patterns, so resolve it the same // way the CLI does instead of comparing pattern strings literally (#307). const scope = await resolveVisibleModels( services.modelRuntime, settings.getEnabledModels(), ); const { visible, thinkingLevelPins, warnings } = scope; modelList = visible.map((m) => ({ id: m.id, name: m.name, provider: m.provider, })).sort(compareModelEntries); for (const m of visible) { const key = `${m.provider}:${m.id}`; nameMap.set(key, m.name); thinkingLevels[key] = getSupportedThinkingLevels(m); if (m.thinkingLevelMap) thinkingLevelMaps[key] = m.thinkingLevelMap; } const defaultProvider = settings.getDefaultProvider(); const defaultModelId = settings.getDefaultModel(); const initial = selectInitialModelScope(scope, { ...(defaultProvider && defaultModelId ? { defaultModel: { provider: defaultProvider, modelId: defaultModelId } } : {}), }); if (initial.model) { defaultModel = { provider: initial.model.provider, modelId: initial.model.id }; } return withModelRuntimeError( { models: Object.fromEntries(nameMap), modelList, defaultModel, thinkingLevels, thinkingLevelMaps, thinkingLevelPins, ...(warnings.length > 0 ? { modelScopeWarnings: warnings } : {}), }, modelError, ); } const EMPTY_MODELS: ModelsData = { models: {}, modelList: [], defaultModel: null, thinkingLevels: {}, thinkingLevelMaps: {}, thinkingLevelPins: {}, }; export async function GET(req: Request) { const requestedCwd = new URL(req.url).searchParams.get("cwd") || process.cwd(); const cwd = resolve(requestedCwd); let cwdStat; try { cwdStat = await stat(cwd); } catch { return Response.json({ error: `Directory does not exist: ${cwd}` }, { status: 400 }); } if (!cwdStat.isDirectory()) { return Response.json({ error: `Not a directory: ${cwd}` }, { status: 400 }); } const allowedRoots = await getAllowedFileRoots(); if (!isExistingFilePathAllowed(cwd, allowedRoots)) { return Response.json({ error: "Access denied" }, { status: 403 }); } try { return Response.json(await loadModelsWithCache(cwd, () => loadModels(cwd))); } catch { return Response.json(withSafeModelLoadFailure(EMPTY_MODELS)); } }