Skip to content

Commit b065e65

Browse files
authored
feat: expose model config options (#154)
Add session config options for mode, model, and reasoning effort, and route session/set_config_option changes through the same session state used for prompts.
1 parent 9195f8c commit b065e65

6 files changed

Lines changed: 369 additions & 31 deletions

File tree

src/AgentMode.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type {AskForApproval, SandboxMode, SandboxPolicy} from "./app-server/v2";
2-
import type {SessionMode, SessionModeState} from "@agentclientprotocol/sdk";
2+
import type {SessionConfigOption, SessionMode, SessionModeState} from "@agentclientprotocol/sdk";
3+
4+
export const MODE_CONFIG_ID = "mode";
35

46
export class AgentMode {
57
readonly id: string;
@@ -69,6 +71,22 @@ export class AgentMode {
6971
};
7072
}
7173

74+
toConfigOption(): SessionConfigOption {
75+
return {
76+
id: MODE_CONFIG_ID,
77+
name: "Mode",
78+
description: "Approval and sandboxing preset for the session",
79+
category: "mode",
80+
type: "select",
81+
currentValue: this.id,
82+
options: AgentMode.all().map(mode => ({
83+
value: mode.id,
84+
name: mode.name,
85+
description: mode.description,
86+
})),
87+
};
88+
}
89+
7290
static all(): AgentMode[] {
7391
return [AgentMode.ReadOnly, AgentMode.Agent, AgentMode.AgentFullAccess];
7492
}

src/CodexAcpServer.ts

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,14 @@ import type {
1919
} from "./app-server/v2";
2020
import type {RateLimitsMap} from "./RateLimitsMap";
2121
import {ModelId} from "./ModelId";
22-
import {AgentMode} from "./AgentMode";
22+
import {AgentMode, MODE_CONFIG_ID} from "./AgentMode";
23+
import {
24+
createModelConfigOption,
25+
createReasoningEffortConfigOption,
26+
findSupportedEffort,
27+
MODEL_CONFIG_ID,
28+
REASONING_EFFORT_CONFIG_ID,
29+
} from "./ModelConfigOption";
2330
import type {TokenCount} from "./TokenCount";
2431
import {toPromptUsage} from "./TokenCount";
2532
import {CodexCommands} from "./CodexCommands";
@@ -47,6 +54,7 @@ import {isJetBrains2026_1Client} from "./JBUtils";
4754
export interface SessionState {
4855
sessionId: string,
4956
currentModelId: string,
57+
availableModels: Array<Model>,
5058
supportedReasoningEfforts: Array<ReasoningEffortOption>,
5159
supportedInputModalities: Array<InputModality>,
5260
agentMode: AgentMode,
@@ -327,6 +335,7 @@ export class CodexAcpServer implements acp.Agent {
327335
const sessionState: SessionState = {
328336
sessionId: sessionId,
329337
currentModelId: currentModelId,
338+
availableModels: models,
330339
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
331340
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
332341
agentMode: AgentMode.getInitialAgentMode(),
@@ -495,11 +504,7 @@ export class CodexAcpServer implements acp.Agent {
495504
const sessionState = this.sessions.get(_params.sessionId);
496505
if (!sessionState) throw new Error(`Session ${_params.sessionId} not found`);
497506

498-
const newMode = AgentMode.find(_params.modeId);
499-
if (!newMode) {
500-
throw RequestError.invalidParams();
501-
}
502-
sessionState.agentMode = newMode;
507+
this.applyModeChange(sessionState, _params.modeId);
503508
return {};
504509
}
505510

@@ -511,20 +516,75 @@ export class CodexAcpServer implements acp.Agent {
511516
const sessionState = this.sessions.get(params.sessionId);
512517
if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
513518

514-
if (params.configId !== FAST_MODE_CONFIG_ID || ("type" in params && params.type === "boolean")) {
519+
if (typeof params.value !== "string") {
515520
throw RequestError.invalidParams();
516521
}
517-
518-
if (params.value !== FAST_MODE_ON && params.value !== FAST_MODE_OFF) {
519-
throw RequestError.invalidParams();
522+
const value = params.value;
523+
524+
switch (params.configId) {
525+
case FAST_MODE_CONFIG_ID:
526+
this.applyFastModeChange(sessionState, value);
527+
break;
528+
case MODE_CONFIG_ID:
529+
this.applyModeChange(sessionState, value);
530+
break;
531+
case MODEL_CONFIG_ID:
532+
this.applyModelChange(sessionState, value);
533+
break;
534+
case REASONING_EFFORT_CONFIG_ID:
535+
this.applyReasoningEffortChange(sessionState, value);
536+
break;
537+
default:
538+
throw RequestError.invalidParams();
520539
}
521540

522-
sessionState.fastModeEnabled = params.value === FAST_MODE_ON;
523541
return {
524542
configOptions: this.createSessionConfigOptions(sessionState),
525543
};
526544
}
527545

546+
private applyFastModeChange(sessionState: SessionState, value: string): void {
547+
if (value !== FAST_MODE_ON && value !== FAST_MODE_OFF) {
548+
throw RequestError.invalidParams();
549+
}
550+
sessionState.fastModeEnabled = value === FAST_MODE_ON;
551+
}
552+
553+
private applyModeChange(sessionState: SessionState, value: string): void {
554+
const newMode = AgentMode.find(value);
555+
if (!newMode) {
556+
throw RequestError.invalidParams();
557+
}
558+
sessionState.agentMode = newMode;
559+
}
560+
561+
private applyModelChange(sessionState: SessionState, value: string): void {
562+
const model = sessionState.availableModels.find(m => m.id === value);
563+
if (!model) {
564+
throw RequestError.invalidParams();
565+
}
566+
const currentEffort = ModelId.fromString(sessionState.currentModelId).effort;
567+
const effort = findSupportedEffort(model.supportedReasoningEfforts, currentEffort)
568+
?? model.defaultReasoningEffort;
569+
this.applyModelAndEffort(sessionState, model, effort);
570+
}
571+
572+
private applyReasoningEffortChange(sessionState: SessionState, value: string): void {
573+
const effort = findSupportedEffort(sessionState.supportedReasoningEfforts, value);
574+
if (!effort) {
575+
throw RequestError.invalidParams();
576+
}
577+
const {model} = ModelId.fromString(sessionState.currentModelId);
578+
sessionState.currentModelId = ModelId.create(model, effort).toString();
579+
}
580+
581+
private applyModelAndEffort(sessionState: SessionState, model: Model, effort: ReasoningEffort): void {
582+
sessionState.currentModelId = ModelId.fromComponents(model, effort).toString();
583+
sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts;
584+
sessionState.supportedInputModalities = model.inputModalities;
585+
sessionState.currentModelSupportsFast = modelSupportsFast(model);
586+
}
587+
528588
async unstable_setSessionModel(params: acp.SetSessionModelRequest): Promise<acp.SetSessionModelResponse | void> {
529589
logger.log("Set session model requested", {
530590
sessionId: params.sessionId,
@@ -533,40 +593,35 @@ export class CodexAcpServer implements acp.Agent {
533593
const sessionState = this.sessions.get(params.sessionId);
534594
if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
535595

536-
const requestedModelId= ModelId.fromString(params.modelId);
537-
const requestedModelName = requestedModelId.model;
538-
const requestedEffort = requestedModelId.effort;
596+
const {model: requestedModelName, effort: requestedEffort} = ModelId.fromString(params.modelId);
539597

540598
const models = await this.codexAcpClient.fetchAvailableModels();
541599
const model = models.find(m => m.id === requestedModelName);
542600
if (!model) throw new Error(`Unknown model ${params.modelId}`);
543601

544-
const requestedEffortValue = requestedEffort as ReasoningEffort | undefined;
545602
let reasoningEffort: ReasoningEffort;
546-
if (requestedEffortValue) {
547-
const matchedEffort = model.supportedReasoningEfforts.find(
548-
(option) => option.reasoningEffort === requestedEffortValue
549-
)?.reasoningEffort;
550-
603+
if (requestedEffort) {
604+
const matchedEffort = findSupportedEffort(model.supportedReasoningEfforts, requestedEffort);
551605
if (!matchedEffort) {
552-
throw new Error(`Unsupported reasoning effort ${requestedEffortValue} for model ${requestedModelName}`);
606+
throw new Error(`Unsupported reasoning effort ${requestedEffort} for model ${requestedModelName}`);
553607
}
554-
555608
reasoningEffort = matchedEffort;
556609
} else {
557610
reasoningEffort = model.defaultReasoningEffort;
558611
}
559612

560-
sessionState.currentModelId = ModelId.fromComponents(model, reasoningEffort).toString();
561-
sessionState.supportedReasoningEfforts = model.supportedReasoningEfforts;
562-
sessionState.supportedInputModalities = model.inputModalities;
563-
sessionState.currentModelSupportsFast = modelSupportsFast(model);
613+
sessionState.availableModels = models;
614+
this.applyModelAndEffort(sessionState, model, reasoningEffort);
564615

565616
return {};
566617
}
567618

568619
private createSessionConfigOptions(sessionState: SessionState): Array<acp.SessionConfigOption> {
620+
const currentModelId = ModelId.fromString(sessionState.currentModelId);
569621
return [
622+
sessionState.agentMode.toConfigOption(),
623+
createModelConfigOption(sessionState.availableModels, currentModelId.model),
624+
createReasoningEffortConfigOption(sessionState.supportedReasoningEfforts, currentModelId.effort),
570625
createFastModeConfigOption(sessionState.fastModeEnabled),
571626
];
572627
}
@@ -669,6 +724,7 @@ export class CodexAcpServer implements acp.Agent {
669724
const sessionState: SessionState = {
670725
sessionId: sessionId,
671726
currentModelId: currentModelId,
727+
availableModels: models,
672728
supportedReasoningEfforts: currentModel?.supportedReasoningEfforts ?? [],
673729
supportedInputModalities: currentModel?.inputModalities ?? ["text", "image"],
674730
agentMode: AgentMode.getInitialAgentMode(),

src/ModelConfigOption.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type {SessionConfigOption} from "@agentclientprotocol/sdk";
2+
import type {ReasoningEffort} from "./app-server";
3+
import type {Model, ReasoningEffortOption} from "./app-server/v2";
4+
5+
export const MODEL_CONFIG_ID = "model";
6+
export const REASONING_EFFORT_CONFIG_ID = "reasoning_effort";
7+
8+
export function findSupportedEffort(
9+
options: ReadonlyArray<ReasoningEffortOption>,
10+
effort: string | undefined,
11+
): ReasoningEffort | undefined {
12+
if (!effort) return undefined;
13+
return options.find(o => o.reasoningEffort === effort)?.reasoningEffort;
14+
}
15+
16+
export function createModelConfigOption(availableModels: Array<Model>, currentBaseModelId: string): SessionConfigOption {
17+
return {
18+
id: MODEL_CONFIG_ID,
19+
name: "Model",
20+
description: "Model Codex uses for the session",
21+
category: "model",
22+
type: "select",
23+
currentValue: currentBaseModelId,
24+
options: availableModels.map(model => ({
25+
value: model.id,
26+
name: model.displayName,
27+
description: model.description,
28+
})),
29+
};
30+
}
31+
32+
export function createReasoningEffortConfigOption(
33+
supportedReasoningEfforts: Array<ReasoningEffortOption>,
34+
currentEffort: string,
35+
): SessionConfigOption {
36+
return {
37+
id: REASONING_EFFORT_CONFIG_ID,
38+
name: "Reasoning effort",
39+
description: "How much reasoning effort the model should use",
40+
category: "thought_level",
41+
type: "select",
42+
currentValue: currentEffort,
43+
options: supportedReasoningEfforts.map(option => ({
44+
value: option.reasoningEffort,
45+
name: option.reasoningEffort,
46+
description: option.description,
47+
})),
48+
};
49+
}

src/__tests__/CodexACPAgent/fast-mode-config.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ describe("Fast mode session config", () => {
5757
it("returns the Fast mode config option defaulted to Off for new sessions", async () => {
5858
const {response} = await createSession();
5959

60-
expect(response.configOptions).toEqual([createFastModeConfigOption(false)]);
60+
expect(response.configOptions).toContainEqual(createFastModeConfigOption(false));
6161
});
6262

6363
it("initializes Fast mode as On when the app-server session tier is fast", async () => {
6464
const {response, codexAcpAgent} = await createSession("fast");
6565

66-
expect(response.configOptions).toEqual([createFastModeConfigOption(true)]);
66+
expect(response.configOptions).toContainEqual(createFastModeConfigOption(true));
6767
expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true);
6868
});
6969

@@ -124,15 +124,15 @@ describe("Fast mode session config", () => {
124124
configId: FAST_MODE_CONFIG_ID,
125125
value: FAST_MODE_ON,
126126
});
127-
expect(onResponse.configOptions).toEqual([createFastModeConfigOption(true)]);
127+
expect(onResponse.configOptions).toContainEqual(createFastModeConfigOption(true));
128128
expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(true);
129129

130130
const offResponse = await codexAcpAgent.setSessionConfigOption({
131131
sessionId: "session-id",
132132
configId: FAST_MODE_CONFIG_ID,
133133
value: FAST_MODE_OFF,
134134
});
135-
expect(offResponse.configOptions).toEqual([createFastModeConfigOption(false)]);
135+
expect(offResponse.configOptions).toContainEqual(createFastModeConfigOption(false));
136136
expect(codexAcpAgent.getSessionState("session-id").fastModeEnabled).toBe(false);
137137
});
138138

0 commit comments

Comments
 (0)