ui: polish custom endpoint authentication and model controls
This commit is contained in:
@@ -909,7 +909,7 @@ function CustomEndpointEditor({
|
|||||||
{authMode === "apiKey" && keySource === "env" && <p className="text-[11px] text-muted-foreground" data-testid="custom-env-owned">Environment supplies the effective key. Change the environment to replace it.</p>}
|
{authMode === "apiKey" && keySource === "env" && <p className="text-[11px] text-muted-foreground" data-testid="custom-env-owned">Environment supplies the effective key. Change the environment to replace it.</p>}
|
||||||
{authMode === "apiKey" && keyPresent && (changedTarget || keyReuseRequired) && <label className="text-[11px]"><input type="checkbox" data-testid="custom-reuse-key" checked={reuseKey} onChange={(e) => setReuseKey(e.target.checked)} /> Allow reuse of the effective existing key for this URL</label>}
|
{authMode === "apiKey" && keyPresent && (changedTarget || keyReuseRequired) && <label className="text-[11px]"><input type="checkbox" data-testid="custom-reuse-key" checked={reuseKey} onChange={(e) => setReuseKey(e.target.checked)} /> Allow reuse of the effective existing key for this URL</label>}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
{authMode === "apiKey" && <input
|
||||||
type="password"
|
type="password"
|
||||||
value={key}
|
value={key}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -917,12 +917,12 @@ function CustomEndpointEditor({
|
|||||||
setKeyTouched(true);
|
setKeyTouched(true);
|
||||||
if (!e.target.value.trim()) setAuthMode("keyless");
|
if (!e.target.value.trim()) setAuthMode("keyless");
|
||||||
}}
|
}}
|
||||||
placeholder={authMode === "keyless" ? "Keyless — saved/environment keys are not used" : keyPresent ? "Leave blank to keep effective key" : "API key"}
|
placeholder={keyPresent ? "Leave blank to keep effective key" : "API key"}
|
||||||
aria-label="Custom endpoint API key"
|
aria-label="Custom endpoint API key"
|
||||||
data-testid="custom-key-input"
|
data-testid="custom-key-input"
|
||||||
disabled={busy || pinsBusy || authMode === "keyless" || keySource === "env"}
|
disabled={busy || pinsBusy || keySource === "env"}
|
||||||
className="h-7 min-w-0 flex-1 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"
|
className="h-7 min-w-0 flex-1 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"
|
||||||
/>
|
/>}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void save()}
|
onClick={() => void save()}
|
||||||
@@ -958,9 +958,6 @@ function CustomEndpointEditor({
|
|||||||
</div>
|
</div>
|
||||||
{!setup && models.length > 0 && (
|
{!setup && models.length > 0 && (
|
||||||
<div className="flex flex-col gap-1" data-testid="custom-pins">
|
<div className="flex flex-col gap-1" data-testid="custom-pins">
|
||||||
<span className="text-[11px] text-muted-foreground">
|
|
||||||
Budget pins do not resize backend context. Blank uses reported limits, then fallback (8192 context / 4096 output). Custom pricing is unknown.
|
|
||||||
</span>
|
|
||||||
<div className="flex max-h-56 flex-col gap-1 overflow-y-auto pr-1">
|
<div className="flex max-h-56 flex-col gap-1 overflow-y-auto pr-1">
|
||||||
{models.map((m) => {
|
{models.map((m) => {
|
||||||
const d = draftOf(m);
|
const d = draftOf(m);
|
||||||
@@ -995,38 +992,41 @@ function CustomEndpointEditor({
|
|||||||
<select
|
<select
|
||||||
value={d.reasoning}
|
value={d.reasoning}
|
||||||
onChange={(e) => setDraft(m, { reasoning: e.target.value as PinDraft["reasoning"] })}
|
onChange={(e) => setDraft(m, { reasoning: e.target.value as PinDraft["reasoning"] })}
|
||||||
aria-label={`${m.id} reasoning support`}
|
aria-label={`${m.id} reasoning effort support`}
|
||||||
data-testid={pinTestid("custom-pin-reasoning", m.id)}
|
data-testid={pinTestid("custom-pin-reasoning", m.id)}
|
||||||
disabled={pinsBusy}
|
disabled={pinsBusy}
|
||||||
className="h-7 shrink-0 rounded-sm border border-border bg-background px-1 font-ui text-[11px] text-foreground outline-none focus:border-muted-foreground/50"
|
className="h-7 shrink-0 rounded-sm border border-border bg-background px-1 font-ui text-[11px] text-foreground outline-none focus:border-muted-foreground/50"
|
||||||
>
|
>
|
||||||
<option value="">Support: reported</option>
|
<option value="">Reasoning effort: Use reported</option>
|
||||||
<option value="on">Support: yes</option>
|
<option value="on">Reasoning effort: Supported</option>
|
||||||
<option value="off">Support: no</option>
|
<option value="off">Reasoning effort: Not supported</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void savePins()}
|
|
||||||
disabled={pinsBusy || !dirty}
|
|
||||||
data-testid="custom-pins-save"
|
|
||||||
className="shrink-0 text-xs text-foreground underline-offset-2 hover:underline disabled:text-muted-foreground/40 disabled:no-underline"
|
|
||||||
>
|
|
||||||
{pinsBusy ? "Saving…" : "Save limits"}
|
|
||||||
</button>
|
|
||||||
{pinsError && (
|
{pinsError && (
|
||||||
<p role="alert" data-testid="custom-pins-error" className="text-[11px] text-red-500">
|
<p role="alert" data-testid="custom-pins-error" className="text-[11px] text-red-500">
|
||||||
{pinsError}
|
{pinsError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void savePins()}
|
||||||
|
disabled={pinsBusy || !dirty}
|
||||||
|
data-testid="custom-pins-save"
|
||||||
|
className="ml-auto shrink-0 text-xs text-foreground underline-offset-2 hover:underline disabled:text-muted-foreground/40 disabled:no-underline"
|
||||||
|
>
|
||||||
|
{pinsBusy ? "Saving…" : "Save limits"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-[11px] text-muted-foreground">Reasoning support is a capability declaration. The effort picker’s Off may not disable a custom backend’s default thinking.</p>
|
<div className="flex flex-col gap-1 text-[11px] text-muted-foreground" data-testid="custom-help">
|
||||||
|
{!setup && models.length > 0 && <p data-testid="custom-budget-help">Budget pins do not resize backend context. Blank uses reported limits, then fallback (8192 context / 4096 output). Custom pricing is unknown.</p>}
|
||||||
|
<p data-testid="custom-reasoning-help">Reasoning support is a capability declaration. The effort picker’s Off may not disable a custom backend’s default thinking.</p>
|
||||||
|
</div>
|
||||||
{(error || pinsError) && <button type="button" data-testid="custom-reload" onClick={() => void onReload()} className="text-left text-xs underline">Reload configuration</button>}
|
{(error || pinsError) && <button type="button" data-testid="custom-reload" onClick={() => void onReload()} className="text-left text-xs underline">Reload configuration</button>}
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" data-testid="custom-error" className="text-xs text-red-500">
|
<p role="alert" data-testid="custom-error" className="text-xs text-red-500">
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { act } from "react";
|
||||||
|
(globalThis as Record<string, unknown>).React = React;
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { afterEach, test } from "node:test";
|
||||||
|
import { createRoot, type Root } from "react-dom/client";
|
||||||
|
import { parseHTML } from "linkedom";
|
||||||
|
import { KeysSection, CustomEndpointSetup } from "../components/settings/keys-section.tsx";
|
||||||
|
|
||||||
|
const dom = parseHTML("<html><body></body></html>");
|
||||||
|
Object.assign(globalThis, { window: dom.window, document: dom.document, Element: dom.window.Element, HTMLElement: dom.window.HTMLElement, HTMLInputElement: dom.window.HTMLInputElement, CustomEvent: dom.window.CustomEvent, IS_REACT_ACT_ENVIRONMENT: true });
|
||||||
|
const originalFetch = globalThis.fetch;
|
||||||
|
let root: Root | undefined;
|
||||||
|
let host: HTMLElement | undefined;
|
||||||
|
async function mount(setup = false) {
|
||||||
|
globalThis.fetch = async (input, init) => {
|
||||||
|
assert.equal(init?.method ?? "GET", "GET", "polish interactions must not save credentials/settings");
|
||||||
|
if (String(input).startsWith("/api/custom-provider")) return Response.json({ baseUrl: "http://fixture.invalid/v1", authMode: "keyless", keySource: "none", keyPresent: false, keyReuseRequired: false, models: [{ id: "fixture-model" }], overrides: {}, manualModels: [] }, { headers: { etag: '"fixture"' } });
|
||||||
|
return Response.json({ keys: [], providers: [] });
|
||||||
|
};
|
||||||
|
host = document.createElement("div"); document.body.appendChild(host); root = createRoot(host);
|
||||||
|
await act(async () => root!.render(setup ? <CustomEndpointSetup onSaved={() => assert.fail("unexpected save")} /> : <KeysSection />));
|
||||||
|
if (!setup) await click("custom-endpoint-row-btn");
|
||||||
|
}
|
||||||
|
function element(id: string) { const node = document.querySelector<HTMLElement>(`[data-testid="${id}"]`); assert.ok(node, id); return node; }
|
||||||
|
async function click(id: string) { await act(async () => element(id).dispatchEvent(new dom.window.Event("click", { bubbles: true }))); }
|
||||||
|
async function selectMode(value: string) {
|
||||||
|
const select = element("custom-auth-mode");
|
||||||
|
Object.defineProperty(select, "value", { value, configurable: true });
|
||||||
|
await act(async () => select.dispatchEvent(new dom.window.Event("change", { bubbles: true })));
|
||||||
|
}
|
||||||
|
afterEach(async () => { await act(async () => root?.unmount()); host?.remove(); root = undefined; host = undefined; globalThis.fetch = originalFetch; });
|
||||||
|
for (const setup of [false, true]) {
|
||||||
|
test(`${setup ? "Setup" : "Settings"} hides key input in Keyless and reveals it only in API key mode`, async () => {
|
||||||
|
await mount(setup);
|
||||||
|
assert.equal(document.querySelector('[data-testid="custom-key-input"]'), null);
|
||||||
|
await selectMode("apiKey"); assert.equal(element("custom-key-input").getAttribute("type"), "password");
|
||||||
|
await selectMode("keyless"); assert.equal(document.querySelector('[data-testid="custom-key-input"]'), null);
|
||||||
|
assert.ok(element("custom-save"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test("model controls use approved effort labels, right-align Save limits, and group help below", async () => {
|
||||||
|
await mount();
|
||||||
|
const select = element("custom-pin-reasoning-fixture-model");
|
||||||
|
assert.equal(select.getAttribute("aria-label"), "fixture-model reasoning effort support");
|
||||||
|
assert.deepEqual(Array.from(select.children).map(option => option.textContent), ["Reasoning effort: Use reported", "Reasoning effort: Supported", "Reasoning effort: Not supported"]);
|
||||||
|
assert.deepEqual(Array.from(select.children).map(option => option.getAttribute("value")), ["", "on", "off"], "capability values unchanged");
|
||||||
|
const save = element("custom-pins-save"); assert.ok(save.classList.contains("ml-auto")); assert.equal(save.parentElement!.lastElementChild, save);
|
||||||
|
const help = element("custom-help"); assert.equal(element("custom-budget-help").parentElement, help); assert.equal(element("custom-reasoning-help").parentElement, help);
|
||||||
|
const children = Array.from(element("custom-endpoint-editor").children); assert.ok(children.indexOf(help) > children.indexOf(element("custom-pins")));
|
||||||
|
assert.match(help.textContent ?? "", /do not resize backend context/); assert.match(help.textContent ?? "", /Off may not disable/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user