From 6f1d011dcbdb18caecea065d7c106a7b0b136791 Mon Sep 17 00:00:00 2001 From: Sawyer Cross Date: Mon, 27 Jul 2026 10:45:00 -0500 Subject: [PATCH 1/2] fix(web): remove deleted custom models from picker --- apps/web/src/modelSelection.test.ts | 44 +++++++++++++++++++++++++++++ apps/web/src/modelSelection.ts | 37 ++++++++++++++++++------ 2 files changed, 72 insertions(+), 9 deletions(-) diff --git a/apps/web/src/modelSelection.test.ts b/apps/web/src/modelSelection.test.ts index e8fc8a244d02..faf7dd21cbab 100644 --- a/apps/web/src/modelSelection.test.ts +++ b/apps/web/src/modelSelection.test.ts @@ -177,6 +177,50 @@ describe("instance-scoped model selection", () => { ).not.toContain("openai/gpt-5.5"); }); + it("removes a custom model immediately when the provider snapshot is stale", () => { + const baseProvider = provider({ + instanceId: "claudeAgent", + models: ["claude-sonnet-4-6"], + }); + const staleProviders: ServerProvider[] = [ + { + ...baseProvider, + models: [ + ...baseProvider.models, + { + slug: "removed-preview-model", + name: "Removed Preview Model", + isCustom: true, + capabilities: {}, + }, + ], + }, + ]; + const settings: UnifiedSettings = { + ...settingsWithProviderInstances(), + providerInstances: { + ...settingsWithProviderInstances().providerInstances, + [ProviderInstanceId.make("claudeAgent")]: { + driver: ProviderDriverKind.make("claudeAgent"), + config: { customModels: [] }, + }, + }, + }; + const stock = deriveProviderInstanceEntries(staleProviders)[0]!; + + expect(getAppModelOptionsForInstance(settings, stock).map((option) => option.slug)).toEqual([ + "claude-sonnet-4-6", + ]); + expect( + resolveAppModelSelectionForInstance( + ProviderInstanceId.make("claudeAgent"), + settings, + staleProviders, + "removed-preview-model", + ), + ).toBe("claude-sonnet-4-6"); + }); + it("hides server models from the instance option list", () => { const providers = [ provider({ diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index ec089d766cf0..379bb8704aed 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -150,10 +150,16 @@ export function getAppModelOptions( provider: ProviderDriverKind, _selectedModel?: string | null, ): AppModelOption[] { - const options: AppModelOption[] = getProviderModels(providers, provider).map(toAppModelOption); + const liveModels = getProviderModels(providers, provider); + const options: AppModelOption[] = liveModels + .filter((model) => !model.isCustom) + .map(toAppModelOption); + const liveCustomModelsBySlug = new Map( + liveModels.filter((model) => model.isCustom).map((model) => [model.slug, model] as const), + ); const seen = new Set(options.map((option) => option.slug)); const builtInModelSlugs = new Set( - Arr.filterMap(getProviderModels(providers, provider), (model) => + Arr.filterMap(liveModels, (model) => model.isCustom ? Result.failVoid : Result.succeed(model.slug), ), ); @@ -170,11 +176,16 @@ export function getAppModelOptions( } seen.add(slug); - options.push({ - slug, - name: slug, - isCustom: true, - }); + const liveCustomModel = liveCustomModelsBySlug.get(slug); + options.push( + liveCustomModel + ? toAppModelOption(liveCustomModel) + : { + slug, + name: slug, + isCustom: true, + }, + ); } return applyInstanceModelPreferences( @@ -198,7 +209,12 @@ export function getAppModelOptionsForInstance( settings: UnifiedSettings, entry: ProviderInstanceEntry, ): AppModelOption[] { - const options: AppModelOption[] = entry.models.map(toAppModelOption); + const options: AppModelOption[] = entry.models + .filter((model) => !model.isCustom) + .map(toAppModelOption); + const liveCustomModelsBySlug = new Map( + entry.models.filter((model) => model.isCustom).map((model) => [model.slug, model] as const), + ); const seen = new Set(options.map((option) => option.slug)); const builtInModelSlugs = new Set( Arr.filterMap(entry.models, (model) => @@ -213,7 +229,10 @@ export function getAppModelOptionsForInstance( } seen.add(slug); - options.push({ slug, name: slug, isCustom: true }); + const liveCustomModel = liveCustomModelsBySlug.get(slug); + options.push( + liveCustomModel ? toAppModelOption(liveCustomModel) : { slug, name: slug, isCustom: true }, + ); } return applyInstanceModelPreferences( From 05005f4a16c1d68453b286b3a279f5236c9c1537 Mon Sep 17 00:00:00 2001 From: Sawyer Cross Date: Mon, 27 Jul 2026 11:18:04 -0500 Subject: [PATCH 2/2] fix(web): avoid stale custom model fallback --- apps/web/src/modelSelection.test.ts | 36 +++++++++++++++++++++++++++++ apps/web/src/modelSelection.ts | 4 ++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/web/src/modelSelection.test.ts b/apps/web/src/modelSelection.test.ts index faf7dd21cbab..851b37efbc60 100644 --- a/apps/web/src/modelSelection.test.ts +++ b/apps/web/src/modelSelection.test.ts @@ -221,6 +221,42 @@ describe("instance-scoped model selection", () => { ).toBe("claude-sonnet-4-6"); }); + it("does not fall back to a stale custom model when no configured models remain", () => { + const baseProvider = provider({ instanceId: "claudeAgent" }); + const staleProviders: ServerProvider[] = [ + { + ...baseProvider, + models: [ + { + slug: "removed-preview-model", + name: "Removed Preview Model", + isCustom: true, + capabilities: {}, + }, + ], + }, + ]; + const settings: UnifiedSettings = { + ...settingsWithProviderInstances(), + providerInstances: { + ...settingsWithProviderInstances().providerInstances, + [ProviderInstanceId.make("claudeAgent")]: { + driver: ProviderDriverKind.make("claudeAgent"), + config: { customModels: [] }, + }, + }, + }; + + expect( + resolveAppModelSelectionForInstance( + ProviderInstanceId.make("claudeAgent"), + settings, + staleProviders, + "removed-preview-model", + ), + ).toBeNull(); + }); + it("hides server models from the instance option list", () => { const providers = [ provider({ diff --git a/apps/web/src/modelSelection.ts b/apps/web/src/modelSelection.ts index 379bb8704aed..e77c14121ef6 100644 --- a/apps/web/src/modelSelection.ts +++ b/apps/web/src/modelSelection.ts @@ -270,8 +270,8 @@ export function resolveAppModelSelectionForInstance( resolveSelectableModel(entry.driverKind, selectedModel, options) ?? options.find((option) => option.isDefault)?.slug ?? options[0]?.slug ?? - entry.models.find((model) => model.isDefault)?.slug ?? - entry.models[0]?.slug ?? + entry.models.find((model) => model.isDefault && !model.isCustom)?.slug ?? + entry.models.find((model) => !model.isCustom)?.slug ?? null ); }