From e4d4ba7489e0fa87d28b5da4ab82d9092b6a01d8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 1 Nov 2026 18:49:11 +0100 Subject: [PATCH] fix(server): the legacy Codex Fast alias folds into one speed on apply and in configured defaults A patch with both speed keys left the alias in the stored options, so a reload started Fast again after the speed was removed; the apply now reports the folded alias. A configured fast_mode default now supplies the service_tier default, so removing an agent's own speed falls back to the configured Fast like opening does. --- .../src/server/agent/provider-options.ts | 31 +++++++++++++------ .../server/agent/provider-registry.test.ts | 14 +++++++++ .../codex-app-server-agent.features.test.ts | 11 +++++++ .../agent/providers/codex-app-server-agent.ts | 10 +++--- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/packages/server/src/server/agent/provider-options.ts b/packages/server/src/server/agent/provider-options.ts index 2967e0724f..01a78cda87 100644 --- a/packages/server/src/server/agent/provider-options.ts +++ b/packages/server/src/server/agent/provider-options.ts @@ -100,28 +100,39 @@ export function mergeProviderOptions( } // COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once saved Fast preferences have migrated. -const LEGACY_OPTION_ALIASES: Record> = { - codex: { fast_mode: "service_tier" }, +const LEGACY_OPTION_ALIASES: Record< + string, + Record unknown }> +> = { + codex: { + fast_mode: { canonical: "service_tier", value: (legacy) => (legacy ? "fast" : "default") }, + }, }; /** * The options a provider opens with: `defaults` from config.json under the agent's `own` - * options. An agent's own legacy alias (a saved `fast_mode`) replaces the default of the key it - * stands for, so a configured `service_tier` does not override the agent's own Fast preference. + * options. A legacy alias (a saved or configured `fast_mode`) stands for the key it aliases: + * a configured alias supplies that key's default, and an agent's own alias replaces the default + * of that key, so a configured `service_tier` does not override the agent's own Fast preference. */ export function mergeDefaultsUnderOwn(input: { provider: string; defaults: ProviderOptions | undefined; own: ProviderOptions | undefined; }): ProviderOptions | undefined { - const { defaults, own } = input; const aliases = LEGACY_OPTION_ALIASES[input.provider]; - if (!aliases || !defaults || !own) return mergeProviderOptions(defaults, own); - const kept = { ...defaults }; - for (const [alias, canonical] of Object.entries(aliases)) { - if (own[alias] !== undefined && own[canonical] === undefined) delete kept[canonical]; + if (!aliases || !input.defaults) return mergeProviderOptions(input.defaults, input.own); + const defaults = { ...input.defaults }; + for (const [alias, { canonical, value }] of Object.entries(aliases)) { + const own = input.own ?? {}; + if (own[alias] !== undefined && own[canonical] === undefined) { + delete defaults[canonical]; + } else if (defaults[alias] != null && defaults[canonical] === undefined) { + defaults[canonical] = value(defaults[alias]); + delete defaults[alias]; + } } - return mergeProviderOptions(kept, own); + return mergeProviderOptions(defaults, input.own); } function isPlainObject(value: unknown): value is ProviderOptions { diff --git a/packages/server/src/server/agent/provider-registry.test.ts b/packages/server/src/server/agent/provider-registry.test.ts index b113bbd1b9..0accbd1c66 100644 --- a/packages/server/src/server/agent/provider-registry.test.ts +++ b/packages/server/src/server/agent/provider-registry.test.ts @@ -809,6 +809,20 @@ test("a saved legacy Codex Fast preference wins over a configured service_tier d expect(client.effectiveOptions?.({})).toEqual({ service_tier: "default", plan_mode: false }); }); +test("a configured legacy Codex Fast default supplies the service_tier default", () => { + const registry = buildProviderRegistry(logger, { + providerOverrides: { codex: { options: { fast_mode: true } } }, + }); + const client = registry.codex.createClient(logger); + + // Removing an agent's own speed falls back to the configured Fast. + expect(client.effectiveOptions?.({})).toEqual({ service_tier: "fast" }); + expect(client.effectiveOptions?.({ service_tier: "default" })).toEqual({ + service_tier: "default", + }); + expect(client.effectiveOptions?.({ fast_mode: false })).toEqual({ fast_mode: false }); +}); + test("cursor provider extending acp uses CursorACPAgentClient", () => { const registry = buildProviderRegistry(logger, { providerOverrides: { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts index 9cbc0ccf0e..d6e12f5486 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.features.test.ts @@ -364,6 +364,17 @@ describe("Codex app-server provider controls", () => { } }); + test("a patch carrying both speed keys reports the folded alias, so removing the speed removes it everywhere", async () => { + const { session } = await createConnectedSession(); + try { + await expect( + session.applyOptions?.({ service_tier: "priority", fast_mode: true }), + ).resolves.toEqual({ fast_mode: null }); + } finally { + await session.close(); + } + }); + test("maps legacy Fast commands to the catalog's request id", async () => { const { session, appServer } = await createConnectedSession(); try { diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index c928b27e58..085100b142 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -4738,8 +4738,10 @@ export class CodexAppServerAgentSession implements AgentSession { this.cachedRuntimeInfo = null; // COMPAT(codexFastPreference): added in v0.10.0, remove after 2027-03-29 once clients use service_tier. const tier = this.serviceTier; - if ("fast_mode" in options && !("service_tier" in options) && options.fast_mode !== null) { - return tier === null ? {} : { service_tier: tier, fast_mode: null }; + // `fast_mode` is folded into the speed, so the options no longer hold it. + const folded = options.fast_mode == null ? {} : { fast_mode: null }; + if (options.fast_mode != null && !("service_tier" in options)) { + return tier === null ? folded : { service_tier: tier, ...folded }; } // The catalog may name the tier differently from the request (`fast` is the Fast tier's id). if ( @@ -4748,9 +4750,9 @@ export class CodexAppServerAgentSession implements AgentSession { requestedTier !== null && tier !== requestedTier ) { - return { service_tier: tier }; + return { service_tier: tier, ...folded }; } - return {}; + return folded; } private requestedTierFrom(options: ProviderOptions): string | null {