feat: custom OpenAI-compatible endpoint as a first-class provider (vision + context from /models)
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 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 ?? [] });
|
||||
}
|
||||
@@ -128,6 +128,6 @@ export async function PUT(request: Request, ctx: { params: Promise<{ section: st
|
||||
// Read-modify-write ONE section of the record; everything else rides
|
||||
// along untouched (other sections, unknown keys — sanitize preserves).
|
||||
const next = { ...record, [section]: sectionOut };
|
||||
writeSettingsRecord(next);
|
||||
return Response.json({ settings: next });
|
||||
const saved = writeSettingsRecord(next);
|
||||
return Response.json({ settings: saved });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user