From 5d21cd446c985840afc9b1c9b52024483655ab51 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 11:03:43 +0000 Subject: [PATCH 1/4] test(shared): reproduce #17556 hidden project default model bypassing the global default Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/projectSettings.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/shared/src/projectSettings.test.ts b/packages/shared/src/projectSettings.test.ts index 67aaf2186e7a2..3fe8758d14b75 100644 --- a/packages/shared/src/projectSettings.test.ts +++ b/packages/shared/src/projectSettings.test.ts @@ -119,6 +119,27 @@ describe("resolveProjectSettings", () => { expect(resolved.sources.defaultModelSelection).toBe("environment"); }); + // regression: #17556 a hidden project default silently became the catalog's first model + it.each([ + ["hidden", ["gpt-5.6-terra", "gpt-5.6-sol"], "gpt-6.1-sol", "environment"], + ["visible", ["gpt-5.6-terra"], "gpt-5.6-sol", "project"], + ] as const)( + "resolves a %s project default model against the environment default", + (_visibility, hiddenModels, expectedModel, expectedSource) => { + const codex = ProviderInstanceId.make("codex"); + const settings = applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, { + defaultModelSelection: createModelSelection(codex, "gpt-6.1-sol"), + providerModelPreferences: { [codex]: { hiddenModels, modelOrder: [] } }, + projectSettingsOverrides: { + [projectId]: { defaultModelSelection: createModelSelection(codex, "gpt-5.6-sol") }, + }, + }); + const resolved = resolveProjectSettings(settings, projectId); + expect(resolved.settings.defaultModelSelection?.model).toBe(expectedModel); + expect(resolved.sources.defaultModelSelection).toBe(expectedSource); + }, + ); + it("moves text generation off an instance the project turned off", () => { const codex = ProviderInstanceId.make("codex"); const settings = { From 70805ba7058ed6070ec9ebe3528d7e88b53b5aa6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 22 Oct 2026 11:05:58 +0000 Subject: [PATCH 2/4] fix(shared): a hidden project default model falls back to the global default A project's default model override that names a model hidden for its provider instance was still applied, and clients then resolved it to the catalog's first visible model. The override is now ignored like one whose provider is disabled for the project, so the environment default applies. Co-Authored-By: Claude Opus 5.5 --- packages/shared/src/projectSettings.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/shared/src/projectSettings.ts b/packages/shared/src/projectSettings.ts index b0a636a21bf6b..7ec8f20b64588 100644 --- a/packages/shared/src/projectSettings.ts +++ b/packages/shared/src/projectSettings.ts @@ -154,6 +154,17 @@ function resolveProjectOverrides( ) { continue; } + // Clients resolve a hidden model to the catalog's first visible one, so a + // hidden project default would silently pick a model nobody chose. + if ( + key === "defaultModelSelection" && + value !== null && + settings.providerModelPreferences?.[ + (value as ModelSelection).instanceId + ]?.hiddenModels.includes((value as ModelSelection).model) + ) { + continue; + } effective[key] = value; sources[key] = "project"; } From 3ed86b5757c2c4dfb86ac13f7db2c90b8f3a8e89 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:31:30 +0000 Subject: [PATCH 3/4] test(shared): a hidden project default keeps the stored override AI-assisted; verified with the tests and tools listed above. --- packages/shared/src/projectSettings.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/src/projectSettings.test.ts b/packages/shared/src/projectSettings.test.ts index 2127a047066e7..c7a5a54454a60 100644 --- a/packages/shared/src/projectSettings.test.ts +++ b/packages/shared/src/projectSettings.test.ts @@ -137,6 +137,7 @@ describe("resolveProjectSettings", () => { const resolved = resolveProjectSettings(settings, projectId); expect(resolved.settings.defaultModelSelection?.model).toBe(expectedModel); expect(resolved.sources.defaultModelSelection).toBe(expectedSource); + expect(resolved.overrides.defaultModelSelection?.model).toBe("gpt-5.6-sol"); }, ); From 85e80d4b1398de45ad102470b1467467d0a091cb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 11 Nov 2026 01:46:44 +0000 Subject: [PATCH 4/4] fix(shared): a custom project default model stays selected when listed as hidden Clients never hide custom models, even when their slug is in hiddenModels, so the resolver exempts them too. AI-assisted; verified with the tests and tools listed above. --- packages/shared/src/projectSettings.test.ts | 10 +++++++--- packages/shared/src/projectSettings.ts | 21 +++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/shared/src/projectSettings.test.ts b/packages/shared/src/projectSettings.test.ts index c7a5a54454a60..a5edf854c3aec 100644 --- a/packages/shared/src/projectSettings.test.ts +++ b/packages/shared/src/projectSettings.test.ts @@ -121,14 +121,18 @@ describe("resolveProjectSettings", () => { // regression: #17556 a hidden project default silently became the catalog's first model it.each([ - ["hidden", ["gpt-5.6-terra", "gpt-5.6-sol"], "gpt-6.1-sol", "environment"], - ["visible", ["gpt-5.6-terra"], "gpt-5.6-sol", "project"], + ["hidden", ["gpt-5.6-terra", "gpt-5.6-sol"], [], "gpt-6.1-sol", "environment"], + ["visible", ["gpt-5.6-terra"], [], "gpt-5.6-sol", "project"], + ["custom", ["gpt-5.6-sol"], ["gpt-5.6-sol"], "gpt-5.6-sol", "project"], ] as const)( "resolves a %s project default model against the environment default", - (_visibility, hiddenModels, expectedModel, expectedSource) => { + (_visibility, hiddenModels, customModels, expectedModel, expectedSource) => { const codex = ProviderInstanceId.make("codex"); const settings = applyServerSettingsPatch(DEFAULT_SERVER_SETTINGS, { defaultModelSelection: createModelSelection(codex, "gpt-6.1-sol"), + providerInstances: { + [codex]: { driver: ProviderDriverKind.make("codex"), config: { customModels } }, + }, providerModelPreferences: { [codex]: { hiddenModels, modelOrder: [] } }, projectSettingsOverrides: { [projectId]: { defaultModelSelection: createModelSelection(codex, "gpt-5.6-sol") }, diff --git a/packages/shared/src/projectSettings.ts b/packages/shared/src/projectSettings.ts index acf35f486eeab..afcbf8a8225b4 100644 --- a/packages/shared/src/projectSettings.ts +++ b/packages/shared/src/projectSettings.ts @@ -14,6 +14,7 @@ import { type ThreadEnvMode, type WorktreeCleanupRules, } from "@t3tools/contracts"; +import { readCustomModelEntries } from "./model.ts"; import { normalizeProjectPathForComparison } from "./path.ts"; import { fallbackTextGenerationModelSelection } from "./serverSettings.ts"; @@ -150,14 +151,18 @@ function resolveProjectOverrides( } // Clients resolve a hidden model to the catalog's first visible one, so a // hidden project default would silently pick a model nobody chose. - if ( - key === "defaultModelSelection" && - value !== null && - settings.providerModelPreferences?.[ - (value as ModelSelection).instanceId - ]?.hiddenModels.includes((value as ModelSelection).model) - ) { - continue; + // Custom models stay visible even when listed as hidden, as in the clients. + if (key === "defaultModelSelection" && value !== null) { + const { instanceId, model } = value as ModelSelection; + const config = settings.providerInstances[instanceId]?.config as + | { customModels?: unknown } + | undefined; + if ( + settings.providerModelPreferences?.[instanceId]?.hiddenModels.includes(model) && + !readCustomModelEntries(config?.customModels).some((entry) => entry.slug === model) + ) { + continue; + } } effective[key] = value; sources[key] = "project";