diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index f26fece246..c0aec009a0 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -104,6 +104,43 @@ it.layer(NodeServices.layer)("server settings", (it) => { }).pipe(Effect.provide(makeServerSettingsLayer())), ); + it.effect("preserves model when switching providers via textGenerationModelSelection", () => + Effect.gen(function* () { + const serverSettings = yield* ServerSettingsService; + + // Start with Claude text generation selection + yield* serverSettings.updateSettings({ + textGenerationModelSelection: { + provider: "claudeAgent", + model: "claude-sonnet-4-6", + options: { + effort: "high", + }, + }, + }); + + // Switch to Codex — the stale Claude "effort" in options must not + // cause the update to lose the selected model. + const next = yield* serverSettings.updateSettings({ + textGenerationModelSelection: { + provider: "codex", + model: "gpt-5.4", + options: { + reasoningEffort: "high", + }, + }, + }); + + assert.deepEqual(next.textGenerationModelSelection, { + provider: "codex", + model: "gpt-5.4", + options: { + reasoningEffort: "high", + }, + }); + }).pipe(Effect.provide(makeServerSettingsLayer())), + ); + it.effect("trims provider path settings when updates are applied", () => Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index f638e7fdfa..d5df84724e 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -27,6 +27,7 @@ import { FileSystem, Layer, Path, + Equal, PubSub, Ref, Schema, @@ -130,6 +131,9 @@ function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings }; } +// Values under these keys are compared as a whole — never stripped field-by-field. +const ATOMIC_SETTINGS_KEYS: ReadonlySet = new Set(["textGenerationModelSelection"]); + function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { if (Array.isArray(current) || Array.isArray(defaults)) { return JSON.stringify(current) === JSON.stringify(defaults) ? undefined : current; @@ -146,9 +150,15 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow const next: Record = {}; for (const key of Object.keys(currentRecord)) { - const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); - if (stripped !== undefined) { - next[key] = stripped; + if (ATOMIC_SETTINGS_KEYS.has(key)) { + if (!Equal.equals(currentRecord[key], defaultsRecord[key])) { + next[key] = currentRecord[key]; + } + } else { + const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]); + if (stripped !== undefined) { + next[key] = stripped; + } } }