Skip to content

Commit d8e062f

Browse files
feat: add setSessionModel
1 parent 968d72f commit d8e062f

3 files changed

Lines changed: 132 additions & 6 deletions

File tree

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/CodexACPAgent.ts

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,61 @@ import type {MessageConnection} from "vscode-jsonrpc/node";
33
import {CodexClient} from "./CodexClient";
44
import {CodexEventHandler} from "./CodexEventHandler";
55
import type {JsonValue} from "./app-server/serde_json/JsonValue";
6+
import type {ModelInfo, SessionModelState} from "@agentclientprotocol/sdk";
7+
import type {ReasoningEffort} from "./app-server";
8+
import type {Model, ThreadStartResponse} from "./app-server/v2";
69

710
export interface SessionState {
811
sessionId: string,
912
seenReasoningDeltas: boolean;
1013
pendingPrompt: AbortController | null;
14+
currentModelId: string;
15+
models: Model[];
16+
}
17+
18+
class ModelId {
19+
private constructor(
20+
public readonly model: string,
21+
public readonly effort: string | null // TODO: ThreadStartResponse
22+
) {}
23+
24+
static fromComponents(model: Model, effort: ReasoningEffort): ModelId {
25+
return new ModelId(model.id, effort);
26+
}
27+
28+
static fromThreadResponse(response: ThreadStartResponse): ModelId {
29+
return new ModelId(response.model, response.reasoningEffort);
30+
}
31+
32+
static fromString(modelId: string): ModelId {
33+
const parts = modelId.split("/");
34+
const model = parts[0];
35+
const effort = parts[1] ?? null;
36+
37+
if (!model) {
38+
throw new Error(`Invalid modelId format: ${modelId}`);
39+
}
40+
return new ModelId(model, effort);
41+
}
42+
43+
toString(): string {
44+
return this.effort ? `${this.model}/${this.effort}` : this.model;
45+
}
1146
}
1247

1348
export class CodexACPAgent implements acp.Agent {
1449
private readonly codexClient: CodexClient;
1550
private readonly connection: acp.AgentSideConnection;
16-
private readonly config: JsonObject | null;
51+
private readonly config: JsonObject;
1752
private readonly modelProvider: string | null;
1853

1954
private readonly sessions: Map<string, SessionState>;
2055

21-
constructor(connection: acp.AgentSideConnection, codexConnection: MessageConnection, codexConfig?: JsonObject, modelProvider?: string) {
56+
constructor(connection: acp.AgentSideConnection, codexConnection: MessageConnection, codexConfig: JsonObject, modelProvider?: string) {
2257
this.sessions = new Map();
2358
this.codexClient = new CodexClient(codexConnection);
2459
this.connection = connection;
25-
this.config = codexConfig ?? null;
60+
this.config = codexConfig ?? {};
2661
this.modelProvider = modelProvider ?? null;
2762
}
2863

@@ -59,14 +94,30 @@ export class CodexACPAgent implements acp.Agent {
5994
})
6095

6196
const sessionId = threadStartResponse.thread.id;
97+
const codexModels = await this.fetchAvailableModels();
98+
if (codexModels.length === 0) {
99+
throw new Error("Codex did not return any models");
100+
}
101+
102+
const currentModelId = ModelId.fromThreadResponse(threadStartResponse).toString();
62103
this.sessions.set(sessionId, {
63104
sessionId: sessionId,
64105
seenReasoningDeltas: false,
65106
pendingPrompt: null,
107+
currentModelId: currentModelId,
108+
models: codexModels,
66109
});
67110

111+
const availableModels = this.buildAvailableModels(codexModels);
112+
113+
const sessionModelState: SessionModelState = {
114+
availableModels: availableModels,
115+
currentModelId: currentModelId,
116+
}
117+
68118
return {
69119
sessionId,
120+
models: sessionModelState,
70121
};
71122
}
72123

@@ -84,6 +135,43 @@ export class CodexACPAgent implements acp.Agent {
84135
return {};
85136
}
86137

138+
async setSessionModel(params: acp.SetSessionModelRequest): Promise<acp.SetSessionModelResponse> {
139+
const sessionState = this.sessions.get(params.sessionId);
140+
if (!sessionState) throw new Error(`Session ${params.sessionId} not found`);
141+
142+
const requestedModelId= ModelId.fromString(params.modelId);
143+
const requestedModelName = requestedModelId.model;
144+
const requestedEffort = requestedModelId.effort;
145+
146+
const model = sessionState.models.find(m => m.id === requestedModelName);
147+
if (!model) throw new Error(`Unknown model ${params.modelId}`);
148+
149+
const requestedEffortValue = requestedEffort as ReasoningEffort | undefined;
150+
let reasoningEffort: ReasoningEffort;
151+
if (requestedEffortValue) {
152+
const matchedEffort = model.supportedReasoningEfforts.find(
153+
(option) => option.reasoningEffort === requestedEffortValue
154+
)?.reasoningEffort;
155+
156+
if (!matchedEffort) {
157+
throw new Error(`Unsupported reasoning effort ${requestedEffortValue} for model ${requestedModelName}`);
158+
}
159+
160+
reasoningEffort = matchedEffort;
161+
} else {
162+
reasoningEffort = model.defaultReasoningEffort;
163+
}
164+
165+
166+
await this.codexClient.setModelRequest({
167+
model: model.model,
168+
reasoningEffort,
169+
});
170+
sessionState.currentModelId = ModelId.fromComponents(model, reasoningEffort).toString();
171+
172+
return {};
173+
}
174+
87175
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
88176
const sessionState = this.sessions.get(params.sessionId);
89177
if (!sessionState) {
@@ -143,6 +231,29 @@ export class CodexACPAgent implements acp.Agent {
143231
await this.codexClient.close()
144232
this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
145233
}
234+
235+
private async fetchAvailableModels(): Promise<Model[]> {
236+
const models: Model[] = [];
237+
let cursor: string | null = null;
238+
239+
do {
240+
const response = await this.codexClient.listModels({ cursor, limit: null });
241+
models.push(...response.data);
242+
cursor = response.nextCursor;
243+
} while (cursor);
244+
245+
return models;
246+
}
247+
248+
private buildAvailableModels(models: Model[]): ModelInfo[] {
249+
return models.flatMap((model) =>
250+
model.supportedReasoningEfforts.map((effort) => ({
251+
modelId: ModelId.fromComponents(model, effort.reasoningEffort).toString(),
252+
name: `${model.displayName} (${effort.reasoningEffort})`,
253+
description: `${model.description} ${effort.description}`,
254+
}))
255+
);
256+
}
146257
}
147258

148-
export type JsonObject = { [key in string]?: JsonValue }
259+
export type JsonObject = { [key in string]?: JsonValue }

src/CodexClient.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import type {
55
InitializeParams,
66
InitializeResponse,
77
NewConversationParams,
8-
NewConversationResponse, SendUserMessageParams, SendUserMessageResponse, ServerNotification, TaskCompleteEvent
8+
NewConversationResponse, SendUserMessageParams, SendUserMessageResponse, ServerNotification,
9+
SetDefaultModelParams, SetDefaultModelResponse, TaskCompleteEvent
910
} from "./app-server";
1011
import type {
12+
ModelListParams,
13+
ModelListResponse,
1114
ThreadStartParams,
1215
ThreadStartResponse,
1316
TurnCompletedNotification,
@@ -72,6 +75,14 @@ export class CodexClient {
7275
private async sendRequest<R>(request: CodexRequest): Promise<R> {
7376
return await this.connection.sendRequest(request.method, request.params);
7477
}
78+
79+
async setModelRequest(params: SetDefaultModelParams): Promise<SetDefaultModelResponse> {
80+
return await this.sendRequest({ method: "setDefaultModel", params });
81+
}
82+
83+
async listModels(params: ModelListParams = {cursor: null, limit: null}): Promise<ModelListResponse> {
84+
return await this.sendRequest({ method: "model/list", params });
85+
}
7586
}
7687

77-
type CodexRequest = Omit<ClientRequest, "id">;
88+
type CodexRequest = Omit<ClientRequest, "id">;

0 commit comments

Comments
 (0)