From 286a95ba04f8742abcfdcf994d84290817fd63b9 Mon Sep 17 00:00:00 2001 From: Daniel Buckner Date: Sun, 25 Oct 2026 19:22:06 -0700 Subject: [PATCH] feat: disconnect a custom endpoint (DELETE /api/custom-provider + Disconnect in its editor) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connecting was one-way: POST could only replace the base URL, and the only escape was hand-editing settings.json. DELETE now mirrors POST — drop the stored section, delete the reserved key, unregister the provider from piModels() — and the connect editor grows a Disconnect beside Connect, following the stored-key rows' Delete pattern (quiet text, one click, no confirm, only once there is an endpoint to remove). --- app/api/custom-provider/route.ts | 14 ++++++++++++++ components/settings/keys-section.tsx | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/app/api/custom-provider/route.ts b/app/api/custom-provider/route.ts index 5439fb9..05becc5 100644 --- a/app/api/custom-provider/route.ts +++ b/app/api/custom-provider/route.ts @@ -5,6 +5,8 @@ * fast), store the key, persist { baseUrl, models }, re-sync the * provider into piModels(). Discovery lives here because connecting * and discovering are one user action. + * DELETE → disconnect: drop the stored config + the stored key and unregister + * the provider (the mirror image of POST; idempotent). */ import { deleteKey, @@ -56,3 +58,15 @@ export async function POST(request: Request) { syncCustomProvider(next.customProvider); return Response.json({ ok: true, models: next.customProvider?.models ?? [] }); } + +/** Disconnect: forget the endpoint AND its key, then unregister the provider. + * Deleting the section (rather than blanking baseUrl) keeps settings.json the + * same shape as a never-connected install. Idempotent by construction. */ +export async function DELETE() { + const record = readSettingsRecord() ?? { version: 1 }; + const { customProvider: _dropped, ...rest } = record; + const next = writeSettingsRecord(rest); + deleteKey(CUSTOM_PROVIDER.envVar); + syncCustomProvider(next.customProvider); + return Response.json({ ok: true }); +} diff --git a/components/settings/keys-section.tsx b/components/settings/keys-section.tsx index fb8e7f2..8b28ad3 100644 --- a/components/settings/keys-section.tsx +++ b/components/settings/keys-section.tsx @@ -444,6 +444,15 @@ export function KeysSection({ notifySettingsSaved("keys"); } + /** Forget the custom endpoint (config + key server-side), same rhythm as + * deleteKey: fire, re-read, broadcast so the catalog narrows everywhere. */ + async function disconnectCustom(): Promise { + await fetch("/api/custom-provider", { method: "DELETE" }).catch(() => undefined); + setAddingCustom(false); + await reload(); + notifySettingsSaved("keys"); + } + if (loading || statuses === null) { return

Loading keys…

; } @@ -496,6 +505,7 @@ export function KeysSection({ await reload(); notifySettingsSaved("keys"); }} + onDelete={disconnectCustom} onCancel={() => setAddingCustom(false)} /> )} @@ -660,11 +670,13 @@ function CustomEndpointEditor({ keyPresent, onSaved, onCancel, + onDelete, }: { baseUrl0: string; keyPresent: boolean; onSaved: () => void | Promise; onCancel: () => void; + onDelete?: () => void | Promise; }) { const [baseUrl, setBaseUrl] = useState(baseUrl0); const [key, setKey] = useState(""); @@ -742,6 +754,20 @@ function CustomEndpointEditor({ > Cancel + {/* Disconnect rides beside Connect, exactly the stored-key rows' Delete: + quiet text, one click, no confirm — and only once there IS an + endpoint (a fresh add has nothing to delete). */} + {onDelete && baseUrl0.trim() && ( + + )} {needsKey && (