/** * The custom OpenAI-compatible endpoint's connect surface (custom-endpoint * wave). It is ONE provider in the Keys list, connected exactly like the * built-ins are connected by a key — the only extra input is a base URL. * * GET → the resolved non-secret config { baseUrl, models } + whether a key * is present (masked status only; the key is a secret in the auth * store and never enters settings.json). * POST → connect. Body { baseUrl, key? }: validate + normalize the base URL, * PROBE the endpoint's /models (fail fast — a broken URL never * persists), then store the key (empty string clears it, absent * leaves it), persist { baseUrl, models } to the settings record, and * re-register the provider into piModels() with no restart. * * Discovery lives here (not a separate route) because connecting and discovering * are one user action: paste URL + key, get a working provider. */ import { deleteKey, discoverModels, effectiveSettings, keyValue, readSettingsRecord, saveKey, syncCustomProvider, writeSettingsRecord, } from "@/src/server"; import { CUSTOM_PROVIDER, normalizeBaseUrl } from "@/src/core"; export const dynamic = "force-dynamic"; export async function GET() { const cp = effectiveSettings().customProvider; return Response.json({ baseUrl: cp.baseUrl, models: cp.models, keyPresent: keyValue(CUSTOM_PROVIDER.envVar) !== undefined, }); } export async function POST(request: Request) { const body = (await request.json().catch(() => null)) as { baseUrl?: unknown; key?: unknown; models?: unknown; } | null; const base = normalizeBaseUrl(typeof body?.baseUrl === "string" ? body.baseUrl : ""); if (!base) return Response.json({ error: "Base URL must be a valid http(s) URL" }, { status: 400 }); // Two intents on one endpoint, told apart by the presence of `models`: // • `models` present → CAPABILITY UPDATE: persist the user's per-model // image toggles (no re-discovery, no key change) and re-register. // • `models` absent → CONNECT: probe /models, store the discovered set. if (Array.isArray(body?.models)) { const record = readSettingsRecord() ?? { version: 1 }; const next = writeSettingsRecord({ ...record, customProvider: { baseUrl: base, models: body!.models } }); syncCustomProvider(next.customProvider); return Response.json({ ok: true, models: next.customProvider?.models ?? [] }); } // A provided key is trimmed; an empty string means "clear it", an absent // field means "leave whatever is stored". Only a real key rides the probe. const keyProvided = typeof body?.key === "string"; const key = keyProvided ? (body!.key as string).trim() : undefined; // Probe first: a URL that doesn't answer with a model list never persists, // so a typo can't leave a half-configured provider behind. const found = await discoverModels(base, key ? key : undefined); if (!found.ok) return Response.json({ error: found.error }, { status: 502 }); if (keyProvided) { if (key) { const saved = saveKey(CUSTOM_PROVIDER.envVar, key); if (!saved.ok) return Response.json({ error: saved.error }, { status: 400 }); } else { deleteKey(CUSTOM_PROVIDER.envVar); } } const record = readSettingsRecord() ?? { version: 1 }; const next = writeSettingsRecord({ ...record, customProvider: { baseUrl: base, models: found.models } }); syncCustomProvider(next.customProvider); return Response.json({ ok: true, models: next.customProvider?.models ?? [] }); }