From 99f270962b91c7e7f6ee88373ec80bd827f1558c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Nov 2026 12:59:25 +0100 Subject: [PATCH] Read a remembered Codex fast_mode as its service tier The create-agent preferences convert the legacy key on read, keyed on the built-in codex provider id, so the Codex draft shows the Fast selection the user last used. Tagged COMPAT(featureValues) with the other legacy-option shims. --- .../preferences.test.ts | 35 +++++++++++++++++-- .../create-agent-preferences/preferences.ts | 23 +++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/app/src/create-agent-preferences/preferences.test.ts b/packages/app/src/create-agent-preferences/preferences.test.ts index cdec2ac3c8..543b49bb1c 100644 --- a/packages/app/src/create-agent-preferences/preferences.test.ts +++ b/packages/app/src/create-agent-preferences/preferences.test.ts @@ -80,7 +80,7 @@ describe("create agent preferences", () => { modelId: "gpt-5.5", modeId: "full-access", thinkingOptionId: "high", - options: { fast_mode: true }, + options: { service_tier: "fast" }, }), ); @@ -95,7 +95,7 @@ describe("create agent preferences", () => { model: "gpt-5.5", mode: "full-access", thinkingByModel: { "gpt-5.5": "high" }, - options: { fast_mode: true }, + options: { service_tier: "fast" }, }, }, }); @@ -220,6 +220,37 @@ describe("create agent preferences", () => { }); }); + it("reads a remembered Codex fast_mode as the service tier it meant, so the draft shows Fast", () => { + expect( + parseFormPreferences({ + provider: "codex", + providerPreferences: { + codex: { options: { fast_mode: true, sandbox_mode: "read-only" } }, + claude: { options: { fast_mode: true } }, + }, + }), + ).toEqual({ + provider: "codex", + providerPreferences: { + codex: { options: { service_tier: "fast", sandbox_mode: "read-only" } }, + claude: { options: { fast_mode: true } }, + }, + }); + + expect( + parseFormPreferences({ + providerPreferences: { codex: { featureValues: { fast_mode: false } } }, + }), + ).toEqual({ providerPreferences: { codex: { options: { service_tier: "default" } } } }); + + // A tier already remembered wins over the key it replaced. + expect( + parseFormPreferences({ + providerPreferences: { codex: { options: { fast_mode: true, service_tier: "default" } } }, + }), + ).toEqual({ providerPreferences: { codex: { options: { service_tier: "default" } } } }); + }); + it("keeps options that are not booleans or strings, such as a profile's sandbox settings", () => { const options = { sandbox_workspace_write: { network_access: true, writable_roots: ["/tmp"] }, diff --git a/packages/app/src/create-agent-preferences/preferences.ts b/packages/app/src/create-agent-preferences/preferences.ts index 24dc426729..cd9d012fe7 100644 --- a/packages/app/src/create-agent-preferences/preferences.ts +++ b/packages/app/src/create-agent-preferences/preferences.ts @@ -39,6 +39,24 @@ const providerPreferencesSchema: z.ZodType = z.preprocess( }), ); +// COMPAT(featureValues): added in v0.15.0, remove after 2027-05-01. Codex took `fast_mode` +// before `service_tier`; the daemon converts the option on every agent and profile it touches, +// and this is the one place the app reads a remembered value without the daemon in between. Keyed +// on the built-in provider id: a custom provider extending Codex keeps the old key and the daemon +// still converts it on create. +function convertLegacyCodexOptions( + preferences: Record, +): Record { + const codex = preferences.codex; + if (!codex?.options || !("fast_mode" in codex.options)) return preferences; + const { fast_mode: legacy, ...rest } = codex.options; + const options = + "service_tier" in rest || typeof legacy !== "boolean" + ? rest + : { ...rest, service_tier: legacy ? "fast" : "default" }; + return { ...preferences, codex: { ...codex, options } }; +} + const launchTargetSchema: z.ZodType = z.discriminatedUnion("kind", [ z.strictObject({ kind: z.literal("chat") }), z.strictObject({ kind: z.literal("terminal"), profileId: z.string() }), @@ -46,7 +64,10 @@ const launchTargetSchema: z.ZodType = z.discriminatedUnion("kind", export const FormPreferencesSchema = z.strictObject({ provider: z.string().optional(), - providerPreferences: z.record(z.string(), providerPreferencesSchema).optional(), + providerPreferences: z + .record(z.string(), providerPreferencesSchema) + .transform(convertLegacyCodexOptions) + .optional(), // COMPAT(agentProfileFavoriteMigration): favourites were removed in v0.3.2. // Keep the legacy payload alive until every capable host has had a chance to // import it; ordinary preference writes must not erase it first.