From 8f7bc1a72493c57d30f90669929ce23531510a49 Mon Sep 17 00:00:00 2001 From: Aleksey Zelenskiy Date: Sun, 2 Aug 2026 13:36:08 +0200 Subject: [PATCH] fix(server): remove deleted custom models --- .../provider/Layers/ProviderRegistry.test.ts | 47 +++++++++++++++++++ .../src/provider/Layers/ProviderRegistry.ts | 7 +-- .../src/provider/providerStatusCache.test.ts | 45 ++++++++++++++++++ .../src/provider/providerStatusCache.ts | 5 +- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/apps/server/src/provider/Layers/ProviderRegistry.test.ts b/apps/server/src/provider/Layers/ProviderRegistry.test.ts index c78ecb3952a..60220f63325 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.test.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.test.ts @@ -600,6 +600,53 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te ]); }); + it("retains discovered models but never removed custom models", () => { + const discoveredModel = { + slug: "gpt-5.6-sol", + name: "GPT-5.6-Sol", + isCustom: false, + capabilities: null, + } as const; + const previousProvider = { + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + status: "ready", + enabled: true, + installed: true, + auth: { status: "authenticated" }, + checkedAt: "2026-08-02T10:00:00.000Z", + version: "0.146.0", + models: [ + discoveredModel, + { + slug: "removed-custom-model", + name: "removed-custom-model", + isCustom: true, + capabilities: null, + }, + ], + slashCommands: [], + skills: [], + } as const satisfies ServerProvider; + const successfulRefresh = { + ...previousProvider, + checkedAt: "2026-08-02T10:01:00.000Z", + models: [discoveredModel], + } satisfies ServerProvider; + const incompleteRefresh = { + ...previousProvider, + checkedAt: "2026-08-02T10:02:00.000Z", + models: [], + } satisfies ServerProvider; + + assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, successfulRefresh).models, [ + discoveredModel, + ]); + assert.deepStrictEqual(mergeProviderSnapshot(previousProvider, incompleteRefresh).models, [ + discoveredModel, + ]); + }); + it("drops stale OpenCode models missing from a successful refresh", () => { const previousProvider = { instanceId: ProviderInstanceId.make("opencode"), diff --git a/apps/server/src/provider/Layers/ProviderRegistry.ts b/apps/server/src/provider/Layers/ProviderRegistry.ts index 760c8e1c59e..9c4cef7c7a5 100644 --- a/apps/server/src/provider/Layers/ProviderRegistry.ts +++ b/apps/server/src/provider/Layers/ProviderRegistry.ts @@ -101,9 +101,10 @@ const mergeProviderModels = ( nextModels: ReadonlyArray, ): ReadonlyArray => { const shouldRetainMissingModels = shouldRetainMissingProviderModels(provider); + const retainablePreviousModels = previousModels.filter((model) => !model.isCustom); - if (shouldRetainMissingModels && nextModels.length === 0 && previousModels.length > 0) { - return previousModels; + if (shouldRetainMissingModels && nextModels.length === 0 && retainablePreviousModels.length > 0) { + return retainablePreviousModels; } const previousBySlug = new Map(previousModels.map((model) => [model.slug, model] as const)); @@ -119,7 +120,7 @@ const mergeProviderModels = ( }); const nextSlugs = new Set(nextModels.map((model) => model.slug)); return shouldRetainMissingModels - ? [...mergedModels, ...previousModels.filter((model) => !nextSlugs.has(model.slug))] + ? [...mergedModels, ...retainablePreviousModels.filter((model) => !nextSlugs.has(model.slug))] : mergedModels; }; diff --git a/apps/server/src/provider/providerStatusCache.test.ts b/apps/server/src/provider/providerStatusCache.test.ts index 07f67cd7de8..57d5874270a 100644 --- a/apps/server/src/provider/providerStatusCache.test.ts +++ b/apps/server/src/provider/providerStatusCache.test.ts @@ -182,6 +182,51 @@ it.layer(NodeServices.layer)("providerStatusCache", (it) => { ); }); + it("uses current settings as the authority for cached custom models", () => { + const configuredCustomModel = { + slug: "configured-custom-model", + name: "configured-custom-model", + isCustom: true, + capabilities: emptyCapabilities, + } as const; + const cachedCodex = makeProvider(CODEX_DRIVER, { + models: [ + configuredCustomModel, + { + slug: "removed-custom-model", + name: "removed-custom-model", + isCustom: true, + capabilities: emptyCapabilities, + }, + { + slug: "gpt-5-mini", + name: "GPT-5 Mini", + isCustom: false, + capabilities: emptyCapabilities, + }, + ], + }); + const fallbackCodex = makeProvider(CODEX_DRIVER, { + models: [configuredCustomModel], + }); + + assert.deepStrictEqual( + hydrateCachedProvider({ + cachedProvider: cachedCodex, + fallbackProvider: fallbackCodex, + }).models, + [ + configuredCustomModel, + { + slug: "gpt-5-mini", + name: "GPT-5 Mini", + isCustom: false, + capabilities: emptyCapabilities, + }, + ], + ); + }); + it("ignores stale cached enabled state when the provider is now disabled", () => { const cachedCodex = makeProvider(CODEX_DRIVER, { checkedAt: "2026-04-10T12:00:00.000Z", diff --git a/apps/server/src/provider/providerStatusCache.ts b/apps/server/src/provider/providerStatusCache.ts index 2fe0424b4f5..d3020437f6c 100644 --- a/apps/server/src/provider/providerStatusCache.ts +++ b/apps/server/src/provider/providerStatusCache.ts @@ -21,7 +21,10 @@ const mergeProviderModels = ( cachedModels: ReadonlyArray, ): ReadonlyArray => { const fallbackSlugs = new Set(fallbackModels.map((model) => model.slug)); - return [...fallbackModels, ...cachedModels.filter((model) => !fallbackSlugs.has(model.slug))]; + return [ + ...fallbackModels, + ...cachedModels.filter((model) => !model.isCustom && !fallbackSlugs.has(model.slug)), + ]; }; export const orderProviderSnapshots = (