fix: never lose custom endpoint models or pins on a failed probe
A connect that fell back to typed model ids replaced the stored model list
with the typed ids alone, then pruned the user pins against it. A transient
/models failure (503, timeout) therefore deleted every discovered model, its
detected metadata, and its pins, and answered 200.
- DiscoverResult carries the endpoint's HTTP status. A 401/403 fails the
connect even when ids are typed: a rejected key is not a missing model list.
- A failed probe keeps the stored snapshot for the SAME base URL, so the
prune cannot delete pins for a probe that failed. A new base URL starts
clean, so an older endpoint's models never follow the URL.
- Comment corrections: the route contract and the CustomProviderSettings
fields described the behaviour from before the manual-ids change.
- tests/custom-provider-route.test.ts covers the four branches: transient
failure, rejected key, new endpoint with no model list, working probe.
tsc and eslint clean. Suite: 2554 tests, 2 failures, both also failing on
6959482 (render-sidebar date drift, subscription-auth install artifact).
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* The custom endpoint's connect surface (one provider in the Keys list).
|
||||
* GET → non-secret config { baseUrl, models, overrides } + keyPresent (masked).
|
||||
* POST → connect: validate + normalize the base URL, probe /models (fail
|
||||
* fast), store the key, persist { baseUrl, models }, re-sync the
|
||||
* provider into piModels(). Discovery lives here because connecting
|
||||
* and discovering are one user action. Stored user pins survive a
|
||||
* reconnect, pruned to the ids the endpoint still advertises.
|
||||
* GET → non-secret config { baseUrl, models, overrides, manualModels } +
|
||||
* keyPresent (masked).
|
||||
* POST → connect: validate + normalize the base URL, probe /models, store
|
||||
* the key, persist { baseUrl, models, overrides, manualModels },
|
||||
* re-sync the provider into piModels(). Discovery lives here because
|
||||
* connecting and discovering are one user action. A failed probe is
|
||||
* fatal UNLESS the user typed model IDs: an endpoint that serves chat
|
||||
* but no model list is configured from those ids alone. A failed
|
||||
* probe for the endpoint already stored keeps its last snapshot, so
|
||||
* a blip cannot prune the stored pins. A 401/403 always fails: a
|
||||
* rejected key is not a missing model list. Stored user pins survive
|
||||
* a reconnect, pruned to the ids the connect ends up offering.
|
||||
* PATCH → store per-model pins only. No probe: pins must be editable with
|
||||
* the local server switched off (the case they exist for).
|
||||
* DELETE → disconnect: drop the stored config + the stored key and unregister
|
||||
@@ -48,11 +54,25 @@ export async function POST(request: Request) {
|
||||
const key = keyProvided ? (body!.key as string).trim() : undefined;
|
||||
|
||||
const manualModels = parseModelIds(body?.manualModels);
|
||||
const record = readSettingsRecord() ?? { version: 1 };
|
||||
const found = await discoverModels(base, key ? key : undefined);
|
||||
// A rejected key is not a missing model list: never let typed ids turn an
|
||||
// auth failure into a stored "connected" endpoint.
|
||||
if (!found.ok && (found.status === 401 || found.status === 403)) {
|
||||
return Response.json({ error: found.error }, { status: 502 });
|
||||
}
|
||||
if (!found.ok && !manualModels.length) {
|
||||
return Response.json({ error: `${found.error}. If this endpoint has no /models list, add model IDs manually.` }, { status: 502 });
|
||||
}
|
||||
const models = mergeManualModels(found.ok ? found.models : [], manualModels);
|
||||
// A failed probe says nothing about what the endpoint offers: keep the last
|
||||
// snapshot of the SAME endpoint, so a transient failure cannot shrink the
|
||||
// model list and prune its pins. A different base URL starts clean.
|
||||
const discovered = found.ok
|
||||
? found.models
|
||||
: record.customProvider?.baseUrl === base
|
||||
? (record.customProvider?.models ?? [])
|
||||
: [];
|
||||
const models = mergeManualModels(discovered, manualModels);
|
||||
|
||||
if (keyProvided) {
|
||||
if (key) {
|
||||
@@ -63,9 +83,8 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
const record = readSettingsRecord() ?? { version: 1 };
|
||||
// User pins are never rewritten by discovery — only pruned to what the
|
||||
// endpoint still advertises (the single prune law, core/settings.ts).
|
||||
// User pins are never rewritten by discovery — only pruned to the ids this
|
||||
// connect offers (discovered ∪ typed).
|
||||
const overrides = sanitizeCustomModelOverrides(
|
||||
record.customProvider?.overrides,
|
||||
models.map((m) => m.id),
|
||||
|
||||
Reference in New Issue
Block a user