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:
@@ -254,11 +254,16 @@ export interface CustomModelOverride {
|
||||
export interface CustomProviderSettings {
|
||||
/** Normalized base URL (see normalizeBaseUrl). Empty/absent = not configured. */
|
||||
baseUrl?: string;
|
||||
/** Models discovered from the endpoint (refreshed on connect). */
|
||||
/** Models this endpoint offers: advertised ids merged with the typed
|
||||
* manual ones (rewritten on connect; a failed probe keeps the same
|
||||
* endpoint's last snapshot). */
|
||||
models?: CustomModel[];
|
||||
/** User pins keyed by model id — pure user intent, never rewritten by
|
||||
* discovery, pruned to the endpoint's live ids on connect. */
|
||||
* discovery, pruned on connect to the ids that connect offers. */
|
||||
overrides?: Record<string, CustomModelOverride>;
|
||||
/** Model ids the user typed by hand, for an endpoint that publishes no
|
||||
* usable /models list. Kept apart from the snapshot so a reconnect can
|
||||
* re-merge them after discovery refreshes. */
|
||||
manualModels?: string[];
|
||||
}
|
||||
|
||||
|
||||
@@ -101,8 +101,12 @@ export function syncCustomProvider(cp: CustomProviderSettings | undefined): void
|
||||
);
|
||||
}
|
||||
|
||||
/** Outcome of a model-discovery probe. */
|
||||
export type DiscoverResult = { ok: true; models: CustomModel[] } | { ok: false; error: string };
|
||||
/** Outcome of a model-discovery probe. `status` is the endpoint's HTTP status
|
||||
* when it answered at all, so callers can tell a rejected key from a missing
|
||||
* model list. */
|
||||
export type DiscoverResult =
|
||||
| { ok: true; models: CustomModel[] }
|
||||
| { ok: false; error: string; status?: number };
|
||||
|
||||
/** Probe GET {baseUrl}/models for model ids. Scheme validated (http/https
|
||||
* only); keyless endpoints answer without a bearer. Non-2xx or unparseable
|
||||
@@ -118,7 +122,7 @@ export async function discoverModels(baseUrl: string, apiKey?: string): Promise<
|
||||
headers: key ? { authorization: `Bearer ${key}` } : {},
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) return { ok: false, error: `Endpoint returned ${res.status} ${res.statusText}` };
|
||||
if (!res.ok) return { ok: false, error: `Endpoint returned ${res.status} ${res.statusText}`, status: res.status };
|
||||
const body = (await res.json().catch(() => null)) as unknown;
|
||||
const list = extractModels(body);
|
||||
if (list === null) return { ok: false, error: "Endpoint response was not a model list" };
|
||||
@@ -131,6 +135,9 @@ export async function discoverModels(baseUrl: string, apiKey?: string): Promise<
|
||||
}
|
||||
}
|
||||
|
||||
/** The connect's model list: what the endpoint advertises, plus every typed id
|
||||
* it does not advertise (at default limits, so the pins can correct them).
|
||||
* Discovered metadata always wins over a typed id of the same name. */
|
||||
export function mergeManualModels(discovered: readonly CustomModel[], manualIds: readonly string[]): CustomModel[] {
|
||||
const byId = new Map(discovered.map((m) => [m.id, m]));
|
||||
for (const id of manualIds) if (!byId.has(id)) byId.set(id, { id });
|
||||
|
||||
Reference in New Issue
Block a user