Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,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,
...(instructions ? {instructions} : {}),
Expand Down Expand Up @@ -798,7 +798,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,
Expand Down Expand Up @@ -883,7 +883,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,
...(instructions ? {instructions} : {}),
Expand Down
5 changes: 3 additions & 2 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import {
FAST_MODE_ON,
modelSupportsFast,
resolveFastServiceTier,
serviceTierEnablesFast,
} from "./FastModeConfig";
import packageJson from "../package.json";
import {isJetBrains2026_1Client} from "./JBUtils";
Expand Down Expand Up @@ -1412,7 +1413,7 @@ export class CodexAcpServer {
cwd: request.cwd,
additionalDirectories: sessionMetadata.additionalDirectories,
mcpServers: requestedMcpServers,
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
fastModeEnabled: serviceTierEnablesFast(sessionMetadata.currentServiceTier),
currentModelSupportsFast: currentModelSupportsFast,
sessionMcpServers: sessionMcpServers,
clientCapabilities: this.capabilities,
Expand Down Expand Up @@ -3645,7 +3646,7 @@ export class CodexAcpServer {
cwd: request.cwd,
additionalDirectories: sessionMetadata.additionalDirectories,
mcpServers: requestedMcpServers,
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
fastModeEnabled: serviceTierEnablesFast(sessionMetadata.currentServiceTier),
currentModelSupportsFast: currentModelSupportsFast,
sessionMcpServers: sessionMcpServers,
clientCapabilities: this.capabilities,
Expand Down
5 changes: 5 additions & 0 deletions src/FastModeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export function modelSupportsFast(model: Model | undefined): boolean {
return model?.additionalSpeedTiers?.includes("fast") ?? false;
}

export function serviceTierEnablesFast(serviceTier: string | null | undefined): boolean {
// Codex returns the canonical "priority" tier even when configured with "fast".
return serviceTier === "fast" || serviceTier === "priority";
}

export function resolveFastServiceTier(fastModeEnabled: boolean, currentModelSupportsFast: boolean): ServiceTier | null {
return fastModeEnabled && currentModelSupportsFast ? "fast" : null;
}
Expand Down
3 changes: 1 addition & 2 deletions src/SessionFork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {RequestError} from "@agentclientprotocol/sdk/unstable";
import type {SessionConfig} from "./CodexAcpClient";
import type {CodexAppServerClient} from "./CodexAppServerClient";
import type {ModeKind} from "./app-server/ModeKind";
import type {ServiceTier} from "./app-server/ServiceTier";
import type {Model} from "./app-server/v2";
import type {SessionMetadata} from "./SessionMetadata";
import type {AcpMcpServer, WithAcpMcpServers} from "./McpServerConfig";
Expand Down Expand Up @@ -68,7 +67,7 @@ export async function forkSession(
models,
collaborationMode: dependencies.getCollaborationMode(response.thread.id),
modelProvider: response.modelProvider,
currentServiceTier: response.serviceTier as ServiceTier ?? null,
currentServiceTier: response.serviceTier ?? null,
additionalDirectories,
skippedMcpServers,
activePermissionProfileId: response.activePermissionProfile?.id ?? null,
Expand Down
3 changes: 1 addition & 2 deletions src/SessionMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {ModeKind} from "./app-server/ModeKind";
import type {ServiceTier} from "./app-server/ServiceTier";
import type {
ApprovalsReviewer,
AskForApproval,
Expand All @@ -17,7 +16,7 @@ export type SessionMetadata = {
models: Model[],
collaborationMode: ModeKind,
modelProvider?: string | null,
currentServiceTier?: ServiceTier | null,
currentServiceTier?: string | null,
threadStatus?: ThreadStatus,
additionalDirectories: string[],
skippedMcpServers?: string[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"currentValue": false,
"type": "boolean",
"configId": "fast-mode",
"name": "Fast mode",
"description": "1.5x speed, increased usage",
"category": "model_config"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"currentValue": true,
"type": "boolean",
"configId": "fast-mode",
"name": "Fast mode",
"description": "1.5x speed, increased usage",
"category": "model_config"
}
22 changes: 17 additions & 5 deletions src/__tests__/CodexACPAgent/fast-mode-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ 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,
) {
Expand Down Expand Up @@ -99,11 +99,23 @@ describe("Fast mode session config", () => {
expect(response.configOptions).toContainEqual(createFastModeConfigOption(false));
});

it("initializes Fast mode as On when the app-server session tier is fast", async () => {
const {response, codexAcpAgent} = await createSession("fast");
it.each([
{serviceTier: "fast", enabled: true},
{serviceTier: "priority", enabled: true},
{serviceTier: "flex", enabled: false},
{serviceTier: null, enabled: false},
{serviceTier: "unknown", enabled: false},
])("preserves Fast mode from the app-server tier $serviceTier through the first prompt", async ({serviceTier, enabled}) => {
const {fixture, response, codexAcpAgent} = await createSession(serviceTier);
const turnStartSpy = mockPromptTurn(fixture, "session-id");

expect(response.configOptions).toContainEqual(createFastModeConfigOption(true));
expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true);
expect(response.configOptions).toContainEqual(createFastModeConfigOption(enabled));

await codexAcpAgent.prompt({sessionId: "session-id", prompt: [{type: "text", text: "test"}]});

expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({
serviceTier: enabled ? "fast" : null,
}));
});

it("omits Fast mode config options for JetBrains 2026.1 IntelliJ clients", async () => {
Expand Down
35 changes: 35 additions & 0 deletions src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {CodexAcpClient} from '../../CodexAcpClient';
import {CodexAppServerClient} from '../../CodexAppServerClient';
import type {PermissionProfileSummary, SkillsListParams, SkillsListResponse, Thread} from '../../app-server/v2';
import {MODE_CONFIG_ID} from '../../AgentMode';
import {FAST_MODE_CONFIG_ID} from '../../FastModeConfig';
import {createTestModel} from '../acp-test-utils';
import {createMockConnections} from './test-utils';
import {checkV2SessionUpdate, expectConformingV2SessionUpdates} from './v2-session-update-guard';
Expand Down Expand Up @@ -115,6 +116,7 @@ async function connectV2Client(options?: {
permissionProfiles?: PermissionProfileSummary[];
activePermissionProfileId?: string;
defaultPermissionProfileId?: string;
serviceTier?: string | null;
}) {
const mocks = createMockConnections();
const codexRequests: Array<{method: string, params: unknown}> = [];
Expand All @@ -136,6 +138,14 @@ async function connectV2Client(options?: {
return {config: {default_permissions: options.defaultPermissionProfileId}, origins: {}, layers: []};
}
const response = codexResponse(method, options?.replayTurns, options?.thread);
if (options?.serviceTier !== undefined) {
if (["thread/start", "thread/resume", "thread/fork"].includes(method)) {
response["serviceTier"] = options.serviceTier;
}
if (method === "model/list") {
response["data"] = [createTestModel({id: "gpt-5", additionalSpeedTiers: ["fast"]})];
}
}
if (options?.activePermissionProfileId && ["thread/start", "thread/resume", "thread/fork"].includes(method)) {
return {...response, activePermissionProfile: {id: options.activePermissionProfileId, extends: null}};
}
Expand Down Expand Up @@ -243,6 +253,31 @@ describe('Session lifecycle over ACP v2', () => {
.toMatchObject({type: "select", currentValue: "permission-profile:team-default"});
});

describe.each([
{operation: "new", replayFrom: undefined},
{operation: "resume", replayFrom: null},
{operation: "resume", replayFrom: {type: "start"}},
{operation: "fork", replayFrom: undefined},
] as const)('Fast mode over v2 $operation with replayFrom=$replayFrom', ({operation, replayFrom}) => {
it.each([
{serviceTier: "fast", enabled: true},
{serviceTier: "priority", enabled: true},
{serviceTier: "flex", enabled: false},
{serviceTier: null, enabled: false},
{serviceTier: "unknown", enabled: false},
])('restores Fast mode from the app-server tier $serviceTier', async ({serviceTier, enabled}) => {
const {connection} = await connectV2Client({serviceTier});
closeClient = () => connection.close();

const response = await connection.agent.request(acpV2.methods.agent.session[operation], {
sessionId, cwd, ...(replayFrom === undefined ? {} : {replayFrom}),
});

await expect(dump(response.configOptions?.find(option => option.configId === FAST_MODE_CONFIG_ID)))
.toMatchFileSnapshot(`data/session-lifecycle-v2-fast-mode-${enabled ? "on" : "off"}.json`);
});
});

it.each([null, {type: "start"}] as const)(
'preserves the default profile on an unprompted v2 resume with replayFrom=%s', async replayFrom => {
const {connection} = await connectV2Client({
Expand Down
Loading