@@ -3,26 +3,61 @@ import type {MessageConnection} from "vscode-jsonrpc/node";
33import { CodexClient } from "./CodexClient" ;
44import { CodexEventHandler } from "./CodexEventHandler" ;
55import 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
710export 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
1348export 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 }
0 commit comments