Files
pi-agent-integrated/pi-web/app/api/models-config/discover/route.ts
T

89 lines
3.6 KiB
TypeScript

import { NextResponse } from "next/server";
import { resolveModelDiscoveryAuth } from "@/lib/model-discovery-auth";
import { buildModelsListUrl, parseDiscoveredModels } from "@/lib/model-discovery";
export const dynamic = "force-dynamic";
const DISCOVERY_TIMEOUT_MS = 20_000;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function hasHeader(headers: Headers, name: string): boolean {
return headers.has(name);
}
function buildHeaders(api: string, apiKey: string | undefined, configured: Record<string, string>): Headers {
const headers = new Headers(configured);
if (!hasHeader(headers, "accept")) headers.set("Accept", "application/json");
if (!apiKey) return headers;
if (api === "anthropic-messages") {
if (!hasHeader(headers, "x-api-key")) headers.set("x-api-key", apiKey);
if (!hasHeader(headers, "anthropic-version")) headers.set("anthropic-version", "2023-06-01");
} else if (api === "google-generative-ai") {
if (!hasHeader(headers, "x-goog-api-key")) headers.set("x-goog-api-key", apiKey);
} else if (!hasHeader(headers, "authorization")) {
headers.set("Authorization", `Bearer ${apiKey}`);
}
return headers;
}
export async function POST(req: Request) {
try {
const body = await req.json() as { providerName?: unknown; provider?: unknown };
const providerName = typeof body.providerName === "string" ? body.providerName.trim() : "";
if (!providerName) return NextResponse.json({ error: "providerName is required" }, { status: 400 });
if (!isRecord(body.provider)) return NextResponse.json({ error: "provider is required" }, { status: 400 });
const baseUrl = typeof body.provider.baseUrl === "string" ? body.provider.baseUrl.trim() : "";
if (!baseUrl) return NextResponse.json({ error: "Base URL is required" }, { status: 400 });
const api = typeof body.provider.api === "string" && body.provider.api
? body.provider.api
: "openai-completions";
let endpoint: URL;
try {
endpoint = buildModelsListUrl(baseUrl, api);
} catch {
return NextResponse.json({ error: "Base URL is invalid" }, { status: 400 });
}
const auth = await resolveModelDiscoveryAuth(providerName, body.provider);
if (typeof body.provider.apiKey === "string" && body.provider.apiKey.trim() && !auth.apiKey) {
return NextResponse.json({ error: `No API key found for "${providerName}"` }, { status: 400 });
}
const response = await fetch(endpoint, {
cache: "no-store",
headers: buildHeaders(api, auth.apiKey, auth.headers),
signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
});
const responseText = await response.text();
if (!response.ok) {
return NextResponse.json({
error: responseText.slice(0, 500) || `Upstream returned HTTP ${response.status}`,
status: response.status,
}, { status: 502 });
}
let payload: unknown;
try {
payload = JSON.parse(responseText);
} catch {
return NextResponse.json({ error: "Upstream model list was not valid JSON" }, { status: 502 });
}
const models = parseDiscoveredModels(payload);
if (models.length === 0) {
return NextResponse.json({ error: "No models found in the upstream response" }, { status: 502 });
}
return NextResponse.json({ models, endpoint: endpoint.toString() });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const status = error instanceof DOMException && error.name === "TimeoutError" ? 504 : 500;
return NextResponse.json({ error: message }, { status });
}
}