From 54f8f595cac85eee156a784adc4f226624ab876c Mon Sep 17 00:00:00 2001 From: Harry Berg <6897093+harryrberg@users.noreply.github.com> Date: Sun, 1 Nov 2026 08:22:26 +0100 Subject: [PATCH] feat: expose model-advertised Ultrafast session speed --- README.md | 2 + docs/speed-options.md | 33 ++++ src/CodexAcpClient.ts | 9 +- src/CodexAcpServer.ts | 22 ++- src/FastModeConfig.ts | 50 ++++-- src/SessionMetadata.ts | 3 +- .../data/session-config-options-v2-list.json | 2 +- .../data/session-config-options-v2-set.json | 4 +- .../session-config-options-v2-ultrafast.json | 150 ++++++++++++++++++ .../session-config-options-v2-update.json | 2 +- .../CodexACPAgent/fast-mode-config.test.ts | 31 +++- .../session-config-options-v2.test.ts | 11 ++ .../CodexACPAgent/ultrafast-lifecycle.test.ts | 45 ++++++ src/__tests__/FastModeConfig.test.ts | 22 +++ 14 files changed, 355 insertions(+), 31 deletions(-) create mode 100644 docs/speed-options.md create mode 100644 src/__tests__/CodexACPAgent/data/session-config-options-v2-ultrafast.json create mode 100644 src/__tests__/CodexACPAgent/ultrafast-lifecycle.test.ts create mode 100644 src/__tests__/FastModeConfig.test.ts diff --git a/README.md b/README.md index ab5ce5c92..418842adc 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,5 @@ See [AIR extensions](docs/air-extensions.md#async-tasks) for the capability, lif ## License By contributing, you agree that your contributions will be licensed under the Apache 2.0 License. + +See [session speed selection](docs/speed-options.md) for Standard, Fast, and model-advertised Ultrafast controls. diff --git a/docs/speed-options.md b/docs/speed-options.md new file mode 100644 index 000000000..5b66508fd --- /dev/null +++ b/docs/speed-options.md @@ -0,0 +1,33 @@ +# Session speed selection + +The `fast-mode` session config option selects a service tier. Its ID and existing +`off`/`on` values remain compatible with older clients: + +| Display name | Select value | Turn service tier | +| --- | --- | --- | +| Standard | `off` | `null` | +| Fast | `on` | `fast` | +| Ultrafast | `ultrafast` | `ultrafast` | + +Ultrafast is offered only when the selected model advertises it through +`serviceTiers` or the older `additionalSpeedTiers` field. Model names are not +hardcoded. Availability and usage remain subject to Codex account eligibility. + +For models with Ultrafast, the option is a select even if the client supports +boolean options. For other Fast-capable models, boolean-capable clients retain +the existing Fast mode toggle; clients without boolean support see Standard/Fast. +Models supporting neither accelerated tier do not advertise the option. + +Use `session/set_config_option` with `configId: "fast-mode"` and the advertised +value. ACP v2 clients use `type: "id"` for the select, or `type: "boolean"` for +the toggle. Successful changes return updated config options and subsequent +prompts use the selected tier. Invalid or unsupported Ultrafast choices are +rejected. Older boolean and `on`/`off` requests retain their meaning in ACP v1. + +New and restored sessions recognize Ultrafast returned by Codex. When switching +to a model without Ultrafast, an Ultrafast selection becomes Fast if supported, +otherwise Standard; updated options report this fallback. + +This adds no model, reasoning-effort, or speed default. Other externally selected +tiers, such as Flex, are outside this control's scope; preserving them is tracked +separately in #673. diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index e1fadb05c..7ab60c890 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -22,7 +22,6 @@ import type { ReasoningEffort, ServerNotification } from "./app-server"; -import type {ServiceTier} from "./app-server/ServiceTier"; import type {JsonValue} from "./app-server/serde_json/JsonValue"; import {ModelId} from "./ModelId"; import {AgentMode} from "./AgentMode"; @@ -724,7 +723,7 @@ export class CodexAcpClient { threadStatus: response.thread.status, sessionTitle: normalizeSessionTitle(listedSessionTitle(response.thread)), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, additionalDirectories, skippedMcpServers: sessionConfig.skippedMcpServers, ...(instructions ? {instructions} : {}), @@ -804,7 +803,7 @@ export class CodexAcpClient { // Codex sends no thread/settings/updated on a resume. The response holds the mode. collaborationMode: response.collaborationMode ?? this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, thread, history, additionalDirectories, @@ -886,7 +885,7 @@ export class CodexAcpClient { models: codexModels, collaborationMode: this.getCollaborationMode(response.thread.id), modelProvider: response.modelProvider, - currentServiceTier: response.serviceTier as ServiceTier ?? null, + currentServiceTier: response.serviceTier ?? null, additionalDirectories, skippedMcpServers: sessionConfig.skippedMcpServers, ...(instructions ? {instructions} : {}), @@ -1362,7 +1361,7 @@ export class CodexAcpClient { request: acp.PromptRequest, agentMode: AgentMode, modelId: ModelId, - serviceTier: ServiceTier | null, + serviceTier: string | null, disableSummary: boolean, cwd: string, additionalDirectories: string[], diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index b83503524..add3128cc 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -153,6 +153,11 @@ import { FAST_MODE_ON, modelSupportsFast, resolveFastServiceTier, + fastModeFromServiceTier, + normalizeFastMode, + modelSupportsUltrafast, + ULTRAFAST_MODE, + type FastModeSelection, } from "./FastModeConfig"; import packageJson from "../package.json"; import {isJetBrains2026_1Client} from "./JBUtils"; @@ -276,7 +281,7 @@ export interface SessionState extends SessionIndexTitleState { cwd: string; additionalDirectories: string[]; mcpServers?: Array; - fastModeEnabled: boolean; + fastModeEnabled: FastModeSelection; currentModelSupportsFast: boolean; sessionMcpServers?: Array; /** The capability choices of the client for tool call and plan reports. */ @@ -1386,7 +1391,7 @@ export class CodexAcpServer { cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, mcpServers: requestedMcpServers, - fastModeEnabled: sessionMetadata.currentServiceTier === "fast", + fastModeEnabled: normalizeFastMode(fastModeFromServiceTier(sessionMetadata.currentServiceTier), currentModel), currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, clientCapabilities: this.capabilities, @@ -2816,6 +2821,12 @@ export class CodexAcpServer { private applyFastModeChange(sessionState: SessionState, params: acp.SetSessionConfigOptionRequest): void { const value = params.value; + if (value === ULTRAFAST_MODE) { + const model = this.findCurrentModel(sessionState.availableModels, sessionState.currentModelId); + if (!modelSupportsUltrafast(model)) throw RequestError.invalidParams(); + sessionState.fastModeEnabled = ULTRAFAST_MODE; + return; + } if (typeof value === "boolean") { sessionState.fastModeEnabled = value; return; @@ -2892,6 +2903,7 @@ export class CodexAcpServer { sessionState.supportedReasoningEfforts = currentModel?.supportedReasoningEfforts ?? []; sessionState.supportedInputModalities = currentModel?.inputModalities ?? ["text", "image"]; sessionState.currentModelSupportsFast = modelSupportsFast(currentModel); + sessionState.fastModeEnabled = normalizeFastMode(sessionState.fastModeEnabled, currentModel); }); } @@ -2900,6 +2912,7 @@ export class CodexAcpServer { sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts; sessionState.supportedInputModalities = model.inputModalities; sessionState.currentModelSupportsFast = modelSupportsFast(model); + sessionState.fastModeEnabled = normalizeFastMode(sessionState.fastModeEnabled, model); } async unstable_setSessionModel(params: LegacySetSessionModelRequest): Promise { @@ -3267,10 +3280,11 @@ export class CodexAcpServer { ), ); } - if (sessionState.currentModelSupportsFast) { + if (sessionState.currentModelSupportsFast || modelSupportsUltrafast(currentModel)) { configOptions.push(createFastModeConfigOption( sessionState.fastModeEnabled, this.booleanConfigOptionsSupported, + currentModel, )); } return configOptions; @@ -3554,7 +3568,7 @@ export class CodexAcpServer { cwd: request.cwd, additionalDirectories: sessionMetadata.additionalDirectories, mcpServers: requestedMcpServers, - fastModeEnabled: sessionMetadata.currentServiceTier === "fast", + fastModeEnabled: normalizeFastMode(fastModeFromServiceTier(sessionMetadata.currentServiceTier), currentModel), currentModelSupportsFast: currentModelSupportsFast, sessionMcpServers: sessionMcpServers, clientCapabilities: this.capabilities, diff --git a/src/FastModeConfig.ts b/src/FastModeConfig.ts index defab8dc6..5f682f28a 100644 --- a/src/FastModeConfig.ts +++ b/src/FastModeConfig.ts @@ -1,6 +1,5 @@ import type {SessionConfigOption} from "@agentclientprotocol/sdk/unstable"; import type * as acp from "@agentclientprotocol/sdk/unstable"; -import type {ServiceTier} from "./app-server/ServiceTier"; import type {Model} from "./app-server/v2"; export const FAST_MODE_CONFIG_ID = "fast-mode"; @@ -8,13 +7,31 @@ export const FAST_MODE_CATEGORY = "model_config"; export const FAST_MODE_ON = "on"; export const FAST_MODE_OFF = "off"; -const FAST_MODE_DESCRIPTION = "1.5x speed, increased usage"; +export const ULTRAFAST_MODE = "ultrafast"; +export type FastModeSelection = boolean | typeof ULTRAFAST_MODE; + +const FAST_MODE_DESCRIPTION = "Faster responses, increased usage"; + +export function modelSupportsUltrafast(model: Model | undefined): boolean { + return model?.serviceTiers?.some(tier => tier.id === ULTRAFAST_MODE) + || (model?.additionalSpeedTiers?.includes(ULTRAFAST_MODE) ?? false); +} + +export function fastModeFromServiceTier(tier: string | null | undefined): FastModeSelection { + return tier === ULTRAFAST_MODE ? ULTRAFAST_MODE : tier === "fast" || tier === "priority"; +} + +export function normalizeFastMode(selection: FastModeSelection, model: Model | undefined): FastModeSelection { + return selection === ULTRAFAST_MODE && !modelSupportsUltrafast(model) ? modelSupportsFast(model) : selection; +} export function modelSupportsFast(model: Model | undefined): boolean { - return model?.additionalSpeedTiers?.includes("fast") ?? false; + return model?.serviceTiers?.some(tier => tier.id === "fast" || tier.id === "priority") + || (model?.additionalSpeedTiers?.includes("fast") ?? false); } -export function resolveFastServiceTier(fastModeEnabled: boolean, currentModelSupportsFast: boolean): ServiceTier | null { +export function resolveFastServiceTier(fastModeEnabled: FastModeSelection, currentModelSupportsFast: boolean): string | null { + if (fastModeEnabled === ULTRAFAST_MODE) return ULTRAFAST_MODE; return fastModeEnabled && currentModelSupportsFast ? "fast" : null; } @@ -22,36 +39,41 @@ export function clientSupportsBooleanConfigOptions(clientCapabilities?: acp.Clie return clientCapabilities?.session?.configOptions?.boolean != null; } -export function createFastModeConfigOption(fastModeEnabled: boolean, useBooleanConfigOption = false): SessionConfigOption { - if (useBooleanConfigOption) { +export function createFastModeConfigOption(fastModeEnabled: FastModeSelection, useBooleanConfigOption = false, model?: Model): SessionConfigOption { + if (useBooleanConfigOption && !modelSupportsUltrafast(model)) { return { id: FAST_MODE_CONFIG_ID, name: "Fast mode", description: FAST_MODE_DESCRIPTION, category: FAST_MODE_CATEGORY, type: "boolean", - currentValue: fastModeEnabled, + currentValue: fastModeEnabled === true, }; } return { id: FAST_MODE_CONFIG_ID, - name: "Fast mode", - description: FAST_MODE_DESCRIPTION, + name: "Speed", + description: "Service tier for the current model", category: FAST_MODE_CATEGORY, type: "select", - currentValue: fastModeEnabled ? FAST_MODE_ON : FAST_MODE_OFF, + currentValue: fastModeEnabled === ULTRAFAST_MODE ? ULTRAFAST_MODE : fastModeEnabled ? FAST_MODE_ON : FAST_MODE_OFF, options: [ { value: FAST_MODE_OFF, - name: "Off", + name: "Standard", description: "Default speed, normal usage", }, - { + ...(model === undefined || modelSupportsFast(model) ? [{ value: FAST_MODE_ON, - name: "On", + name: "Fast", description: FAST_MODE_DESCRIPTION, - }, + }] : []), + ...(modelSupportsUltrafast(model) ? [{ + value: ULTRAFAST_MODE, + name: "Ultrafast", + description: "Fastest responses, increased usage", + }] : []), ], }; } diff --git a/src/SessionMetadata.ts b/src/SessionMetadata.ts index 553cfd188..223d941a2 100644 --- a/src/SessionMetadata.ts +++ b/src/SessionMetadata.ts @@ -1,5 +1,4 @@ import type {ModeKind} from "./app-server/ModeKind"; -import type {ServiceTier} from "./app-server/ServiceTier"; import type {Model, Thread, ThreadItemEntry, ThreadStatus} from "./app-server/v2"; import type {SessionInstructions} from "./ClientSystemPrompt"; @@ -9,7 +8,7 @@ export type SessionMetadata = { models: Model[], collaborationMode: ModeKind, modelProvider?: string | null, - currentServiceTier?: ServiceTier | null, + currentServiceTier?: string | null, threadStatus?: ThreadStatus, additionalDirectories: string[], skippedMcpServers?: string[], diff --git a/src/__tests__/CodexACPAgent/data/session-config-options-v2-list.json b/src/__tests__/CodexACPAgent/data/session-config-options-v2-list.json index cbe3cbbe0..204377166 100644 --- a/src/__tests__/CodexACPAgent/data/session-config-options-v2-list.json +++ b/src/__tests__/CodexACPAgent/data/session-config-options-v2-list.json @@ -124,7 +124,7 @@ { "configId": "fast-mode", "name": "Fast mode", - "description": "1.5x speed, increased usage", + "description": "Faster responses, increased usage", "category": "model_config", "type": "boolean", "currentValue": false diff --git a/src/__tests__/CodexACPAgent/data/session-config-options-v2-set.json b/src/__tests__/CodexACPAgent/data/session-config-options-v2-set.json index 99684b220..aa4365969 100644 --- a/src/__tests__/CodexACPAgent/data/session-config-options-v2-set.json +++ b/src/__tests__/CodexACPAgent/data/session-config-options-v2-set.json @@ -127,7 +127,7 @@ "type": "boolean", "configId": "fast-mode", "name": "Fast mode", - "description": "1.5x speed, increased usage", + "description": "Faster responses, increased usage", "category": "model_config" } ] @@ -260,7 +260,7 @@ "type": "boolean", "configId": "fast-mode", "name": "Fast mode", - "description": "1.5x speed, increased usage", + "description": "Faster responses, increased usage", "category": "model_config" } ] diff --git a/src/__tests__/CodexACPAgent/data/session-config-options-v2-ultrafast.json b/src/__tests__/CodexACPAgent/data/session-config-options-v2-ultrafast.json new file mode 100644 index 000000000..952e59ad1 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/session-config-options-v2-ultrafast.json @@ -0,0 +1,150 @@ +{ + "configOptions": [ + { + "currentValue": "agent", + "options": [ + { + "value": "read-only", + "name": "Read-only", + "description": "Requires approval to edit files and access the internet.", + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "kind": "standard" + } + } + } + }, + { + "value": "workspace-write", + "name": "Workspace access", + "description": "Edit workspace files; ask before writing outside the workspace or accessing the network.", + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "kind": "standard" + } + } + } + }, + { + "value": "agent", + "name": "Auto review", + "description": "Only ask for actions detected as potentially unsafe", + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "kind": "auto_review" + } + } + } + }, + { + "value": "agent-full-access", + "name": "Full access", + "description": "Unrestricted access to the internet and any file on your computer", + "_meta": { + "jetbrains": { + "air": { + "version": 1, + "kind": "full_access" + } + } + } + } + ], + "type": "select", + "configId": "mode", + "name": "Mode", + "description": "Approval and sandboxing preset for the session", + "category": "mode" + }, + { + "currentValue": "default", + "options": [ + { + "value": "default", + "name": "Default" + }, + { + "value": "plan", + "name": "Plan", + "description": "Plan before making changes" + } + ], + "type": "select", + "configId": "collaboration_mode", + "name": "Collaboration mode", + "description": "How Codex collaborates for subsequent turns", + "category": "collaboration_mode" + }, + { + "currentValue": "fast-model", + "options": [ + { + "value": "fast-model", + "name": "Fast model", + "description": "Frontier" + }, + { + "value": "slow-model", + "name": "Slow model", + "description": "slow-model model" + } + ], + "type": "select", + "configId": "model", + "name": "Model", + "description": "Model Codex uses for the session", + "category": "model" + }, + { + "currentValue": "medium", + "options": [ + { + "value": "low", + "name": "Low", + "description": "Fast" + }, + { + "value": "medium", + "name": "Medium", + "description": "Balanced" + } + ], + "type": "select", + "configId": "reasoning_effort", + "name": "Reasoning effort", + "description": "How much reasoning effort the model should use", + "category": "thought_level" + }, + { + "currentValue": "ultrafast", + "options": [ + { + "value": "off", + "name": "Standard", + "description": "Default speed, normal usage" + }, + { + "value": "on", + "name": "Fast", + "description": "Faster responses, increased usage" + }, + { + "value": "ultrafast", + "name": "Ultrafast", + "description": "Fastest responses, increased usage" + } + ], + "type": "select", + "configId": "fast-mode", + "name": "Speed", + "description": "Service tier for the current model", + "category": "model_config" + } + ] +} diff --git a/src/__tests__/CodexACPAgent/data/session-config-options-v2-update.json b/src/__tests__/CodexACPAgent/data/session-config-options-v2-update.json index 4e2b2244c..b2954167e 100644 --- a/src/__tests__/CodexACPAgent/data/session-config-options-v2-update.json +++ b/src/__tests__/CodexACPAgent/data/session-config-options-v2-update.json @@ -129,7 +129,7 @@ "type": "boolean", "configId": "fast-mode", "name": "Fast mode", - "description": "1.5x speed, increased usage", + "description": "Faster responses, increased usage", "category": "model_config" }, { diff --git a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts index f50944f51..d3c9aead5 100644 --- a/src/__tests__/CodexACPAgent/fast-mode-config.test.ts +++ b/src/__tests__/CodexACPAgent/fast-mode-config.test.ts @@ -24,16 +24,17 @@ describe("Fast mode session config", () => { }; async function createSession( - currentServiceTier: "fast" | "flex" | null = null, + currentServiceTier: string | null = null, clientInfo: acp.Implementation | null = null, clientCapabilities?: acp.ClientCapabilities, + tiers = ["fast"], ) { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); const codexAcpClient = fixture.getCodexAcpClient(); const fastModel = createTestModel({ id: "fast-model", - additionalSpeedTiers: ["fast"], + additionalSpeedTiers: tiers, }); const slowModel = createTestModel({id: "slow-model"}); @@ -286,4 +287,30 @@ describe("Fast mode session config", () => { serviceTier: null, })); }); + it.each([undefined, booleanConfigCapabilities])("offers Ultrafast as a select even for boolean clients", async capabilities => { + const {response, codexAcpAgent, fixture} = await createSession(null, null, capabilities, ["fast", "ultrafast"]); + const option = response.configOptions?.find(option => option.id === FAST_MODE_CONFIG_ID); + expect(option).toMatchObject({name: "Speed", type: "select", currentValue: "off", options: [ + {value: "off", name: "Standard"}, {value: "on", name: "Fast"}, {value: "ultrafast", name: "Ultrafast"}, + ]}); + const turnStartSpy = mockPromptTurn(fixture, "session-id"); + await codexAcpAgent.setSessionConfigOption({sessionId: "session-id", configId: FAST_MODE_CONFIG_ID, value: "ultrafast"}); + await codexAcpAgent.prompt({sessionId: "session-id", prompt: [{type: "text", text: "test"}]}); + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({serviceTier: "ultrafast"})); + }); + + it("preserves an initial Ultrafast tier and clears it on an unsupported model", async () => { + const {response, codexAcpAgent, codexAcpClient, fixture} = await createSession("ultrafast", null, undefined, ["ultrafast"]); + expect(response.configOptions).toContainEqual(expect.objectContaining({id: FAST_MODE_CONFIG_ID, currentValue: "ultrafast"})); + const fastOnly = createTestModel({id: "fast-only", additionalSpeedTiers: ["fast"]}); + vi.spyOn(codexAcpClient, "fetchAvailableModels").mockResolvedValue([fastOnly]); + codexAcpAgent.getSessionState("session-id").availableModels.push(fastOnly); + const result = await codexAcpAgent.setSessionConfigOption({sessionId: "session-id", configId: MODEL_CONFIG_ID, value: "fast-only"}); + expect(result.configOptions).toContainEqual(expect.objectContaining({id: FAST_MODE_CONFIG_ID, currentValue: "on"})); + await expect(codexAcpAgent.setSessionConfigOption({sessionId: "session-id", configId: FAST_MODE_CONFIG_ID, value: "ultrafast"})).rejects.toThrow(); + const turnStartSpy = mockPromptTurn(fixture, "session-id"); + await codexAcpAgent.prompt({sessionId: "session-id", prompt: [{type: "text", text: "test"}]}); + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({serviceTier: "fast"})); + }); + }); diff --git a/src/__tests__/CodexACPAgent/session-config-options-v2.test.ts b/src/__tests__/CodexACPAgent/session-config-options-v2.test.ts index d3607987a..bc3d22fe2 100644 --- a/src/__tests__/CodexACPAgent/session-config-options-v2.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options-v2.test.ts @@ -179,4 +179,15 @@ describe('Session config options over ACP v2', () => { modeId: AgentMode.ReadOnly.id, })).rejects.toMatchObject({code: -32601}); }); + it('selects Ultrafast through the v2 id option', async () => { + const {connection, sessionState} = await connectV2Client(); + closeClient = () => connection.close(); + sessionState.availableModels[0]!.additionalSpeedTiers.push("ultrafast"); + const response = await connection.agent.request(acpV2.methods.agent.session.setConfigOption, { + sessionId: sessionState.sessionId, configId: FAST_MODE_CONFIG_ID, type: "id", value: "ultrafast", + }); + expect(sessionState.fastModeEnabled).toBe("ultrafast"); + await expect(dump(response)).toMatchFileSnapshot('data/session-config-options-v2-ultrafast.json'); + }); + }); diff --git a/src/__tests__/CodexACPAgent/ultrafast-lifecycle.test.ts b/src/__tests__/CodexACPAgent/ultrafast-lifecycle.test.ts new file mode 100644 index 000000000..992e8f0df --- /dev/null +++ b/src/__tests__/CodexACPAgent/ultrafast-lifecycle.test.ts @@ -0,0 +1,45 @@ +import {describe, expect, it, vi} from "vitest"; +import * as acp from "@agentclientprotocol/sdk/unstable"; +import {createCodexMockTestFixture, createTestModel, mockPromptTurn} from "../acp-test-utils"; + +describe.each(["new", "resume", "load", "fork"] as const)("Ultrafast after session/%s", operation => { + it("advertises and sends the restored tier on consecutive prompts", async () => { + const fixture = createCodexMockTestFixture(); + const agent = fixture.getCodexAcpAgent(); + const client = fixture.getCodexAcpClient(); + const appServer = fixture.getCodexAppServerClient(); + const model = createTestModel({id: "ultra-model", additionalSpeedTiers: ["fast", "ultrafast"]}); + const thread = {id: "tier-session", historyMode: "paginated", status: {type: "idle"}, turns: []}; + const response = { + thread, model: model.id, modelProvider: "openai", cwd: "/workspace", + approvalPolicy: "never", sandbox: {type: "dangerFullAccess"}, + reasoningEffort: model.defaultReasoningEffort, serviceTier: "ultrafast", + }; + vi.spyOn(client, "readAuthRequirement").mockResolvedValue({required: false, account: null}); + vi.spyOn(client, "getAccount").mockResolvedValue({account: null, requiresOpenaiAuth: false}); + vi.spyOn(client, "listSkills").mockResolvedValue({data: []}); + appServer.listModels = vi.fn().mockResolvedValue({data: [model], nextCursor: null}); + appServer.threadStart = vi.fn().mockImplementation(async (params: {ephemeral?: boolean | null}) => ({ + ...response, thread: {...thread, id: params.ephemeral ? "title-session" : thread.id}, + })); + appServer.threadResume = vi.fn().mockResolvedValue(response); + appServer.threadFork = vi.fn().mockResolvedValue(response); + const turnStart = mockPromptTurn(fixture, thread.id); + await agent.initialize({protocolVersion: acp.PROTOCOL_VERSION}); + const request = {sessionId: thread.id, cwd: "/workspace", mcpServers: []}; + switch (operation) { + case "new": await agent.newSession({cwd: request.cwd, mcpServers: []}); break; + case "resume": await agent.resumeSession(request); break; + case "load": await agent.loadSession(request); break; + case "fork": await agent.forkSession({...request, sessionId: "source-session"}); break; + } + const config = await agent.setSessionConfigOption({sessionId: thread.id, configId: "reasoning_effort", value: model.defaultReasoningEffort}); + expect(config.configOptions).toContainEqual(expect.objectContaining({id: "fast-mode", currentValue: "ultrafast"})); + for (const text of ["first", "second"]) { + await agent.prompt({sessionId: thread.id, prompt: [{type: "text", text}]}); + } + const turns = turnStart.mock.calls.map(([params]) => params).filter(params => params.threadId === thread.id); + expect(turns).toHaveLength(2); + expect(turns.every(params => params.serviceTier === "ultrafast")).toBe(true); + }); +}); diff --git a/src/__tests__/FastModeConfig.test.ts b/src/__tests__/FastModeConfig.test.ts new file mode 100644 index 000000000..a13642b08 --- /dev/null +++ b/src/__tests__/FastModeConfig.test.ts @@ -0,0 +1,22 @@ +import {describe, expect, it} from "vitest"; +import {createTestModel} from "./acp-test-utils"; +import {createFastModeConfigOption, fastModeFromServiceTier, modelSupportsFast, modelSupportsUltrafast, normalizeFastMode} from "../FastModeConfig"; + +describe("advertised speed tiers", () => { + it.each(["fast", "priority"])("recognizes the %s Fast alias", tier => { + const model = createTestModel({serviceTiers: [{id: tier, name: "Fast", description: ""}]}); + expect(modelSupportsFast(model)).toBe(true); + expect(fastModeFromServiceTier(tier)).toBe(true); + }); + + it("uses catalog capabilities rather than model names", () => { + const model = createTestModel({id: "custom-model", serviceTiers: [{id: "ultrafast", name: "Ultrafast", description: ""}]}); + expect(modelSupportsUltrafast(model)).toBe(true); + expect(createFastModeConfigOption("ultrafast", true, model)).toMatchObject({type: "select", currentValue: "ultrafast"}); + expect(modelSupportsUltrafast(createTestModel({id: "gpt-6-astra"}))).toBe(false); + }); + + it("falls back to Standard when neither accelerated tier is available", () => { + expect(normalizeFastMode("ultrafast", createTestModel())).toBe(false); + }); +});