authorgravatar for alex.vostres@gmail.comAlexander Vostres <alex.vostres@gmail.com> 2026-03-29 21:27:20+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-29 11:27:20-07:00
log40e7dbdae9cbb4b5b9cfdb0c8b9672709034b810
treeed4df6068a35810699d8d174b9ae94e750495554
parenta60daa1c764a97e88f2eef63bbac960b79274ed6
signature Signed by PGP key B5690EEEBB952194

Fix for broken Codex model selection for 'Text Generation Model' (#1543)


2 files changed, 50 insertions(+), 3 deletions(-)

apps/server/src/serverSettings.test.ts+37
......@@ -104,6 +104,43 @@ it.layer(NodeServices.layer)("server settings", (it) => {
104104 }).pipe(Effect.provide(makeServerSettingsLayer())),
105105 );
106106
107 it.effect("preserves model when switching providers via textGenerationModelSelection", () =>
108 Effect.gen(function* () {
109 const serverSettings = yield* ServerSettingsService;
110
111 // Start with Claude text generation selection
112 yield* serverSettings.updateSettings({
113 textGenerationModelSelection: {
114 provider: "claudeAgent",
115 model: "claude-sonnet-4-6",
116 options: {
117 effort: "high",
118 },
119 },
120 });
121
122 // Switch to Codex — the stale Claude "effort" in options must not
123 // cause the update to lose the selected model.
124 const next = yield* serverSettings.updateSettings({
125 textGenerationModelSelection: {
126 provider: "codex",
127 model: "gpt-5.4",
128 options: {
129 reasoningEffort: "high",
130 },
131 },
132 });
133
134 assert.deepEqual(next.textGenerationModelSelection, {
135 provider: "codex",
136 model: "gpt-5.4",
137 options: {
138 reasoningEffort: "high",
139 },
140 });
141 }).pipe(Effect.provide(makeServerSettingsLayer())),
142 );
143
107144 it.effect("trims provider path settings when updates are applied", () =>
108145 Effect.gen(function* () {
109146 const serverSettings = yield* ServerSettingsService;
apps/server/src/serverSettings.ts+13-3
......@@ -27,6 +27,7 @@ import {
2727 FileSystem,
2828 Layer,
2929 Path,
30 Equal,
3031 PubSub,
3132 Ref,
3233 Schema,
......@@ -130,6 +131,9 @@ function resolveTextGenerationProvider(settings: ServerSettings): ServerSettings
130131 };
131132}
132133
134// Values under these keys are compared as a whole — never stripped field-by-field.
135const ATOMIC_SETTINGS_KEYS: ReadonlySet<string> = new Set(["textGenerationModelSelection"]);
136
133137function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined {
134138 if (Array.isArray(current) || Array.isArray(defaults)) {
135139 return JSON.stringify(current) === JSON.stringify(defaults) ? undefined : current;
......@@ -146,9 +150,15 @@ function stripDefaultServerSettings(current: unknown, defaults: unknown): unknow
146150 const next: Record<string, unknown> = {};
147151
148152 for (const key of Object.keys(currentRecord)) {
149 const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]);
150 if (stripped !== undefined) {
151 next[key] = stripped;
153 if (ATOMIC_SETTINGS_KEYS.has(key)) {
154 if (!Equal.equals(currentRecord[key], defaultsRecord[key])) {
155 next[key] = currentRecord[key];
156 }
157 } else {
158 const stripped = stripDefaultServerSettings(currentRecord[key], defaultsRecord[key]);
159 if (stripped !== undefined) {
160 next[key] = stripped;
161 }
152162 }
153163 }
154164