mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
181 lines
4.9 KiB
TypeScript
181 lines
4.9 KiB
TypeScript
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,
|
|
type ModelsData,
|
|
} from "@/lib/models-cache";
|
|
import {
|
|
getAllowedFileRoots,
|
|
isExistingFilePathAllowed,
|
|
} from "@/lib/file-access";
|
|
import { projectTrustReloadOptions } from "@/lib/project-trust";
|
|
import {
|
|
createAppSettingsManager,
|
|
getAppResourceLoaderOptions,
|
|
} from "@/lib/app-runtime";
|
|
|
|
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)
|
|
);
|
|
}
|
|
|
|
const THINKING_SUFFIXES = new Set([
|
|
"off",
|
|
"minimal",
|
|
"low",
|
|
"medium",
|
|
"high",
|
|
"xhigh",
|
|
"max",
|
|
]);
|
|
|
|
function stripThinkingSuffix(modelRef: string): string {
|
|
const trimmed = modelRef.trim();
|
|
const colonIndex = trimmed.lastIndexOf(":");
|
|
if (colonIndex === -1) return trimmed;
|
|
const suffix = trimmed.substring(colonIndex + 1);
|
|
return THINKING_SUFFIXES.has(suffix)
|
|
? trimmed.substring(0, colonIndex)
|
|
: trimmed;
|
|
}
|
|
|
|
function filterByExactEnabledModels<T extends { id: string; provider: string }>(
|
|
available: readonly T[],
|
|
enabledModels: string[] | undefined,
|
|
): readonly T[] {
|
|
if (!enabledModels || enabledModels.length === 0) return available;
|
|
|
|
const refs = new Set(enabledModels.map(stripThinkingSuffix).filter(Boolean));
|
|
const visible = available.filter(
|
|
(m) => refs.has(`${m.provider}/${m.id}`) || refs.has(m.id),
|
|
);
|
|
return visible.length > 0 ? visible : available;
|
|
}
|
|
|
|
async function loadModels(cwd: string): Promise<ModelsData> {
|
|
const nameMap = new Map<string, string>();
|
|
let modelList: { id: string; name: string; provider: string }[] = [];
|
|
let defaultModel: { provider: string; modelId: string } | null = null;
|
|
const thinkingLevels: Record<string, string[]> = {};
|
|
const thinkingLevelMaps: Record<string, Record<string, string | null>> = {};
|
|
|
|
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,
|
|
settingsManager: createAppSettingsManager(cwd, agentDir),
|
|
resourceLoaderOptions: getAppResourceLoaderOptions(),
|
|
...(trustReloadOptions
|
|
? { resourceLoaderReloadOptions: trustReloadOptions }
|
|
: {}),
|
|
});
|
|
const available = await services.modelRuntime.getAvailable();
|
|
const modelError = services.modelRuntime.getError();
|
|
const settings: SettingsManager = services.settingsManager;
|
|
const enabledModels = settings.getEnabledModels();
|
|
const visible = filterByExactEnabledModels(available, enabledModels);
|
|
modelList = visible
|
|
.map(
|
|
(m: {
|
|
id: string;
|
|
name: string;
|
|
provider: string;
|
|
serviceTier?: string;
|
|
}) => ({
|
|
id: m.id,
|
|
name: m.name,
|
|
provider: m.provider,
|
|
serviceTier: m.serviceTier,
|
|
}),
|
|
)
|
|
.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 provider = settings.getDefaultProvider();
|
|
const modelId = settings.getDefaultModel();
|
|
if (
|
|
provider &&
|
|
modelId &&
|
|
visible.some((m) => m.provider === provider && m.id === modelId)
|
|
) {
|
|
defaultModel = { provider, modelId };
|
|
}
|
|
|
|
return withModelRuntimeError(
|
|
{
|
|
models: Object.fromEntries(nameMap),
|
|
modelList,
|
|
defaultModel,
|
|
thinkingLevels,
|
|
thinkingLevelMaps,
|
|
},
|
|
modelError,
|
|
);
|
|
}
|
|
|
|
const EMPTY_MODELS: ModelsData = {
|
|
models: {},
|
|
modelList: [],
|
|
defaultModel: null,
|
|
thinkingLevels: {},
|
|
thinkingLevelMaps: {},
|
|
};
|
|
|
|
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(EMPTY_MODELS);
|
|
}
|
|
}
|