mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
feat: integrate Pi backend and Pi Web
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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 }) => ({
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user