feat: manual model IDs when /models is unavailable

Hosted OpenAI-compatible APIs often serve chat but not GET /models
(Volcengine Ark Agent Plan returns 404), so connect failed. Accept a
typed list of model IDs: merged into discovered models when /models
works, used alone when it does not. The typed list is stored as
customProvider.manualModels so reconnects and pin saves keep it.
This commit is contained in:
iamabhishekt
2026-10-28 16:16:30 -04:00
parent 695948271f
commit b4a13f5c3b
6 changed files with 84 additions and 10 deletions
+17 -1
View File
@@ -59,6 +59,7 @@ interface CustomStatus {
models: CustomModel[];
/** User pins by model id (context / max output / thinking). */
overrides: Record<string, CustomModelOverride>;
manualModels: string[];
keyPresent: boolean;
}
@@ -413,6 +414,7 @@ export function KeysSection({
baseUrl: cp.baseUrl ?? "",
models: cp.models ?? [],
overrides: cp.overrides ?? {},
manualModels: cp.manualModels ?? [],
keyPresent: Boolean(cp.keyPresent),
});
} catch {
@@ -513,6 +515,7 @@ export function KeysSection({
keyPresent={custom?.keyPresent ?? false}
models={custom?.models ?? []}
overrides={custom?.overrides ?? {}}
manualModels0={custom?.manualModels ?? []}
onSaved={async () => {
setAddingCustom(false);
await reload();
@@ -728,6 +731,7 @@ function CustomEndpointEditor({
keyPresent,
models,
overrides,
manualModels0,
onSaved,
onPinsSaved,
onCancel,
@@ -739,6 +743,7 @@ function CustomEndpointEditor({
models: readonly CustomModel[];
/** Their stored user pins. */
overrides: Record<string, CustomModelOverride>;
manualModels0: readonly string[];
onSaved: () => void | Promise<void>;
/** Pins saved — the editor stays open (a form, not a connect). */
onPinsSaved: () => void | Promise<void>;
@@ -748,6 +753,7 @@ function CustomEndpointEditor({
const [baseUrl, setBaseUrl] = useState(baseUrl0);
const [key, setKey] = useState("");
const [keyTouched, setKeyTouched] = useState(false);
const [manualModels, setManualModels] = useState(manualModels0.join(", "));
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
// The limits form: edited drafts over the stored pins, plus the last saved
@@ -797,7 +803,7 @@ function CustomEndpointEditor({
const res = await fetch("/api/custom-provider", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ baseUrl, ...(keyTouched ? { key } : {}) }),
body: JSON.stringify({ baseUrl, manualModels, ...(keyTouched ? { key } : {}) }),
}).catch(() => null);
setBusy(false);
if (!res || !res.ok) {
@@ -824,6 +830,16 @@ function CustomEndpointEditor({
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"
/>
</div>
<input
type="text"
value={manualModels}
onChange={(e) => setManualModels(e.target.value)}
placeholder="Model IDs, comma-separated — optional, needed when the endpoint has no /models list"
aria-label="Custom endpoint model IDs"
data-testid="custom-models-input"
disabled={busy}
className="h-7 min-w-0 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"