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 f70ecd6d76..23b4ee6684 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 @@ -563,6 +563,39 @@ describe("Codex app-server provider controls", () => { await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "priority" }); }); + test("a live service_tier: fast is returned as the model's Fast tier", async () => { + const { session, appServer } = await createConnectedSession(); + + await expect(session.applyOptions?.({ service_tier: "fast" })).resolves.toEqual({ + service_tier: "priority", + }); + + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "priority" }); + }); + + test("switching to a model without the speed tier says Normal and keeps it so on the way back", async () => { + const models = [ + ...TEST_SPEED_MODELS, + { id: "plain", isDefault: false, defaultReasoningEffort: "medium", serviceTiers: [] }, + ]; + const { session, appServer } = await createConnectedSession( + { model: "gpt-5.4", providerOptions: { service_tier: "priority" } }, + { models }, + ); + const events: AgentStreamEvent[] = []; + session.subscribe((event) => events.push(event)); + + await session.setModel?.("plain"); + await session.setModel?.("gpt-5.4"); + + expect(events.filter((event) => event.type === "options_changed")).toEqual([ + { type: "options_changed", provider: "codex", patch: { service_tier: "default" } }, + ]); + await session.startTurn("hello"); + await expect(appServer.waitForTurnStart()).resolves.toMatchObject({ serviceTier: "default" }); + }); + describe("a pre-v0.10 fast_mode: true option", () => { function createLegacyFastHarness(configDefaults: Record = {}) { const appServer = createFakeCodexAppServer({ 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 16ae88914c..5085be66de 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 @@ -4665,6 +4665,26 @@ export class CodexAppServerAgentSession implements AgentSession { this.config.model = modelId ?? undefined; this.refreshResolvedCollaborationMode(); this.cachedRuntimeInfo = null; + this.dropUnavailableServiceTier(); + } + + /** + * A speed the new model does not offer turns into Normal and is reported, so choosing a model + * that does offer it again does not bring the paid tier back. It is reported as Normal rather + * than removed so a configured default cannot fill the old speed back in. + */ + private dropUnavailableServiceTier(): void { + const requested = this.requestedServiceTier; + if (requested === null || this.speedModels.length === 0) return; + if (this.isServiceTierAvailable(requested)) return; + this.requestedServiceTier = "default"; + const { fast_mode: legacyFast, ...running } = this.providerOptions; + this.providerOptions = { ...running, service_tier: "default" }; + this.emitEvent({ + type: "options_changed", + provider: CODEX_PROVIDER, + patch: { service_tier: "default", ...(legacyFast === undefined ? {} : { fast_mode: null }) }, + }); } async setThinkingOption(thinkingOptionId: string | null): Promise { @@ -4707,6 +4727,15 @@ export class CodexAppServerAgentSession implements AgentSession { if ("fast_mode" in options && !("service_tier" in options) && options.fast_mode !== null) { return tier === null ? {} : { service_tier: tier, fast_mode: null }; } + // The catalog may name the tier differently from the request (`fast` is the Fast tier's id). + if ( + "service_tier" in options && + tier !== null && + requestedTier !== null && + tier !== requestedTier + ) { + return { service_tier: tier }; + } return {}; }