feat: finish donor-led custom endpoint integration

This commit is contained in:
amosblomqvist
2026-10-29 04:43:53 +02:00
parent 80662d3d3b
commit b48821309b
33 changed files with 1116 additions and 1117 deletions
+9 -34
View File
@@ -48,7 +48,7 @@ import { notifySettingsSaved, useSettingsSaved } from "@/components/settings-sig
import { useModelCatalog } from "@/components/model-catalog";
import { savedSlotsConfigured } from "@/components/setup-cta";
import { Hint } from "@/components/ui/hint";
import { type ProviderKeyInfo, CUSTOM_PROVIDER_INFO } from "@/components/settings/keys-section";
import { type ProviderKeyInfo, CUSTOM_PROVIDER_INFO, CustomEndpointSetup } from "@/components/settings/keys-section";
import { ProviderMenuList } from "@/components/settings/model-menu";
import { ChatGPTBackgroundAccess } from "@/components/settings/chatgpt-connection";
import { NativeSubscriptionBackgroundAccess, SubscriptionConnections } from "@/components/settings/subscription-connections";
@@ -135,8 +135,7 @@ export function SetupKeysBody({
onSave,
menuAnchor,
isCustom,
baseUrl,
onBaseUrl,
onCustomSaved,
}: {
/** The full provider roster (GET /api/keys providers) — undefined
* while it loads; the picker renders an honest empty state then. */
@@ -157,8 +156,7 @@ export function SetupKeysBody({
/** Chosen provider is the custom endpoint: a base URL field joins the key
* field. All three optional so plain-key callers (and tests) stay unchanged. */
isCustom?: boolean;
baseUrl?: string;
onBaseUrl?: (v: string) => void;
onCustomSaved?: () => void;
}) {
return (
<div className="flex flex-col gap-3 px-6 py-5" data-testid="setup-stage-keys">
@@ -192,20 +190,8 @@ export function SetupKeysBody({
</MenuShell>
)}
</div>
{provider && (
{provider && (isCustom ? <CustomEndpointSetup onSaved={onCustomSaved ?? onSave} /> : (
<div className="flex max-w-96 flex-col gap-2">
{isCustom && (
<input
type="text"
value={baseUrl ?? ""}
onChange={(e) => onBaseUrl?.(e.target.value)}
placeholder="Base URL, e.g. http://localhost:11434/v1"
aria-label="Custom endpoint base URL"
data-testid="setup-custom-baseurl"
disabled={saving}
className="h-7 w-full rounded-sm border border-border bg-background px-1.5 font-mono text-xs text-foreground outline-none placeholder:text-muted-foreground/60 focus:border-muted-foreground/50"
/>
)}
<div className="flex items-center gap-2">
<input
type="text"
@@ -214,7 +200,7 @@ export function SetupKeysBody({
onKeyDown={(e) => {
if (e.key === "Enter" && value.trim()) onSave();
}}
placeholder={isCustom ? "API key — required (any value works for local servers)" : "Paste key…"}
placeholder="Paste key…"
aria-label={`${provider.label} key`}
data-testid="setup-key-input"
disabled={saving}
@@ -228,11 +214,11 @@ export function SetupKeysBody({
data-testid="setup-key-save"
className="shrink-0 rounded-sm border border-border bg-background px-3 py-1.5 font-ui text-xs text-foreground transition-colors hover:border-muted-foreground/50 disabled:cursor-not-allowed disabled:opacity-40"
>
{saving ? "Saving…" : isCustom ? "Connect" : "Save key"}
{saving ? "Saving…" : "Save key"}
</button>
</div>
</div>
)}
))}
{error && (
<p role="alert" data-testid="setup-key-error" className="text-xs text-red-400">
{error}
@@ -286,7 +272,6 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
const [keyProviders, setKeyProviders] = useState<ProviderKeyInfo[] | undefined>(undefined);
const [keyProvider, setKeyProvider] = useState<ProviderKeyInfo | null>(null);
const [keyValue, setKeyValue] = useState("");
const [customBaseUrl, setCustomBaseUrl] = useState("");
const [keySaving, setKeySaving] = useState(false);
const [keyError, setKeyError] = useState<string | null>(null);
const [keyMenuOpen, setKeyMenuOpen] = useState(false);
@@ -398,18 +383,10 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
* 400s keep their user-facing line inline. */
async function saveKey() {
if (!keyProvider || keySaving) return;
const isCustom = keyProvider.id === CUSTOM_PROVIDER.id;
if (!keyValue.trim()) return;
setKeySaving(true);
setKeyError(null);
// Custom endpoint connects through its own route; every other provider is a plain key PUT.
const res = isCustom
? await fetch("/api/custom-provider", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl: customBaseUrl, key: keyValue }),
}).catch(() => null)
: await fetch(`/api/keys/${encodeURIComponent(keyProvider.envVar)}`, {
const res = await fetch(`/api/keys/${encodeURIComponent(keyProvider.envVar)}`, {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ value: keyValue }),
@@ -422,7 +399,6 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
}
setKeyProvider(null);
setKeyValue("");
setCustomBaseUrl("");
notifySettingsSaved("keys");
}
@@ -461,8 +437,7 @@ function OpenSetupDialog({ onClose, onDone }: SetupDialogProps) {
menuOpen={keyMenuOpen}
menuAnchor={keyTriggerRef}
isCustom={keyProvider?.id === CUSTOM_PROVIDER.id}
baseUrl={customBaseUrl}
onBaseUrl={setCustomBaseUrl}
onCustomSaved={() => { setKeyProvider(null); notifySettingsSaved("keys"); }}
onToggleMenu={() => setKeyMenuOpen((v) => !v)}
onPick={(p) => { setKeyProvider(p); setKeyMenuOpen(false); setKeyError(null); }}
onValue={setKeyValue}