mirror of
https://github.com/luckyyzh/pi-agent-integrated.git
synced 2026-10-03 02:59:35 +00:00
122 lines
4.3 KiB
TypeScript
122 lines
4.3 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "fs";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { completeSimple, type AssistantMessage } from "@earendil-works/pi-ai/compat";
|
|
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
import { hasJsonContentType, isApiRequestAllowed } from "@/lib/request-security";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const TEST_TIMEOUT_MS = 20_000;
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function errorMessage(error: unknown): string {
|
|
return error instanceof Error ? error.message : String(error);
|
|
}
|
|
|
|
function getAssistantText(message: AssistantMessage): string {
|
|
return message.content
|
|
.filter((block) => block.type === "text")
|
|
.map((block) => block.text)
|
|
.join("");
|
|
}
|
|
|
|
export async function POST(req: Request) {
|
|
if (!isApiRequestAllowed(req)) {
|
|
return NextResponse.json({ ok: false, error: "Untrusted API request" }, { status: 403 });
|
|
}
|
|
if (!hasJsonContentType(req)) {
|
|
return NextResponse.json(
|
|
{ ok: false, error: "Content-Type must be application/json" },
|
|
{ status: 415 },
|
|
);
|
|
}
|
|
|
|
let tempDir: string | undefined;
|
|
|
|
try {
|
|
const body = await req.json() as { providerName?: unknown; provider?: unknown; model?: unknown };
|
|
const providerName = typeof body.providerName === "string" ? body.providerName.trim() : "";
|
|
if (!providerName) return NextResponse.json({ ok: false, error: "providerName is required" }, { status: 400 });
|
|
if (!isRecord(body.provider)) return NextResponse.json({ ok: false, error: "provider is required" }, { status: 400 });
|
|
if (!isRecord(body.model)) return NextResponse.json({ ok: false, error: "model is required" }, { status: 400 });
|
|
|
|
const modelId = typeof body.model.id === "string" ? body.model.id.trim() : "";
|
|
if (!modelId) return NextResponse.json({ ok: false, error: "Model ID is required" }, { status: 400 });
|
|
|
|
tempDir = mkdtempSync(join(tmpdir(), "pi-web-model-test-"));
|
|
const modelsPath = join(tempDir, "models.json");
|
|
writeFileSync(modelsPath, JSON.stringify({
|
|
providers: {
|
|
[providerName]: {
|
|
...body.provider,
|
|
models: [{ ...body.model, id: modelId }],
|
|
},
|
|
},
|
|
}, null, 2), "utf8");
|
|
|
|
const modelRuntime = await ModelRuntime.create({ modelsPath });
|
|
const loadError = modelRuntime.getError();
|
|
if (loadError) return NextResponse.json({ ok: false, error: loadError });
|
|
|
|
const model = modelRuntime.getModel(providerName, modelId);
|
|
if (!model) return NextResponse.json({ ok: false, error: `Model not found: ${providerName}/${modelId}` });
|
|
|
|
const resolved = await modelRuntime.getAuth(model);
|
|
if (!resolved?.auth.apiKey) {
|
|
return NextResponse.json({ ok: false, error: `No API key found for "${providerName}"` });
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), TEST_TIMEOUT_MS);
|
|
let status: number | undefined;
|
|
const startedAt = Date.now();
|
|
|
|
try {
|
|
const message = await completeSimple(model, {
|
|
messages: [{
|
|
role: "user",
|
|
content: "Reply with OK only.",
|
|
timestamp: Date.now(),
|
|
}],
|
|
}, {
|
|
apiKey: resolved.auth.apiKey,
|
|
headers: resolved.auth.headers,
|
|
maxTokens: 16,
|
|
timeoutMs: TEST_TIMEOUT_MS,
|
|
maxRetries: 0,
|
|
cacheRetention: "none",
|
|
signal: controller.signal,
|
|
onResponse: (response) => { status = response.status; },
|
|
});
|
|
|
|
const latencyMs = Date.now() - startedAt;
|
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
return NextResponse.json({
|
|
ok: false,
|
|
error: message.errorMessage ?? (controller.signal.aborted ? "Test timed out" : "Model returned an error"),
|
|
latencyMs,
|
|
status,
|
|
});
|
|
}
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
latencyMs,
|
|
status,
|
|
responseText: getAssistantText(message).slice(0, 300),
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
} catch (error) {
|
|
return NextResponse.json({ ok: false, error: errorMessage(error) }, { status: 500 });
|
|
} finally {
|
|
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
}
|