1- import { CODEX_API_KEY_ENV_VAR , isCodexAuthRequest , OPENAI_API_KEY_ENV_VAR } from "./CodexAuthMethod" ;
1+ import { CODEX_API_KEY_ENV_VAR , GatewayAuthMethod , isCodexAuthRequest , OPENAI_API_KEY_ENV_VAR } from "./CodexAuthMethod" ;
22import type { EmbeddedResourceResource } from "@agentclientprotocol/sdk" ;
33import * as acp from "@agentclientprotocol/sdk" ;
44import { type McpServer , RequestError } from "@agentclientprotocol/sdk" ;
@@ -41,6 +41,21 @@ import type {
4141import packageJson from "../package.json" ;
4242import type { AuthenticationStatusResponse } from "./AcpExtensions" ;
4343
44+ /**
45+ * Well-known provider id for the client-configurable custom LLM gateway.
46+ * This is the only provider exposed through the ACP `providers/*` methods and
47+ * the `gateway` auth method; it maps to a Codex `model_providers` entry.
48+ */
49+ export const CUSTOM_GATEWAY_PROVIDER_ID = "custom-gateway" ;
50+
51+ /**
52+ * ACP `LlmProtocol` values Codex can route through the custom gateway, mapped to
53+ * the Codex `wire_api`. Codex only supports the OpenAI Responses wire API here.
54+ */
55+ const SUPPORTED_GATEWAY_PROTOCOLS : Record < acp . LlmProtocol , WireApi > = {
56+ openai : "responses" ,
57+ } ;
58+
4459/**
4560 * API for accessing the Codex App Server using ACP requests.
4661 * Converts ACP requests into corresponding app-server operations.
@@ -82,7 +97,7 @@ export class CodexAcpClient {
8297 if ( ! isCodexAuthRequest ( authRequest ) ) {
8398 throw RequestError . invalidRequest ( ) ;
8499 }
85-
100+ this . gatewayConfig = null ;
86101 switch ( authRequest . methodId ) {
87102 case "api-key" : {
88103 const apiKey = authRequest . _meta ?. [ "api-key" ] ?. apiKey ?? this . readApiKeyFromEnv ( ) ;
@@ -91,15 +106,13 @@ export class CodexAcpClient {
91106 case "chat-gpt" : {
92107 const accountResponse = await this . codexClient . accountRead ( { refreshToken : true } ) ;
93108 if ( accountResponse . account ?. type === "chatgpt" ) {
94- this . gatewayConfig = null ;
95109 return true ;
96110 }
97111 const loginCompletedPromise = this . awaitNextLoginCompleted ( ) ;
98112 const loginResponse = await this . codexClient . accountLogin ( { type : "chatgpt" } ) ;
99113 if ( loginResponse . type == "chatgpt" ) {
100114 await open ( loginResponse . authUrl ) ;
101115 }
102- this . gatewayConfig = null ;
103116 const result = await loginCompletedPromise ;
104117 return result . success ;
105118 }
@@ -109,33 +122,15 @@ export class CodexAcpClient {
109122 const gatewaySettings = authRequest . _meta [ "gateway" ] ;
110123 if ( ! gatewaySettings ) throw RequestError . invalidRequest ( ) ;
111124
112- const baseUrl = gatewaySettings . baseUrl ;
113- const providerName = typeof gatewaySettings . providerName === "string" && gatewaySettings . providerName . trim ( ) . length > 0
114- ? gatewaySettings . providerName
115- : "User-provided gateway" ;
116- const headers : Record < string , string > = {
117- "X-Client-Feature-ID" : "codex" ,
118- ...gatewaySettings . headers
119- } ;
120-
121- this . gatewayConfig = {
122- modelProvider : "custom-gateway" ,
123- config : {
124- name : providerName ,
125- base_url : baseUrl ,
126- http_headers : headers ,
127- wire_api : "responses"
128- }
129- } ;
125+ this . applyGatewayConfig ( {
126+ baseUrl : gatewaySettings . baseUrl ,
127+ apiType : GatewayAuthMethod . _meta . gateway . protocol ,
128+ headers : gatewaySettings . headers ,
129+ providerName : gatewaySettings . providerName ,
130+ } ) ;
130131
131- // Early return: model provider information will be sent to Codex later during the session creation
132132 return true ;
133-
134133 }
135-
136- // Reset the gateway config to null if another authentication method was used
137- this . gatewayConfig = null ;
138- return false ;
139134 }
140135
141136 private async authenticateWithApiKey ( apiKey : string ) : Promise < Boolean > {
@@ -144,7 +139,6 @@ export class CodexAcpClient {
144139 type : "apiKey" ,
145140 apiKey,
146141 } ) ;
147- this . gatewayConfig = null ;
148142 const result = await loginCompletedPromise ;
149143 return result . success ;
150144 }
@@ -223,8 +217,96 @@ export class CodexAcpClient {
223217 return response . requiresOpenaiAuth && ! response . account ;
224218 }
225219
226- hasGatewayAuth ( ) : boolean {
227- return this . gatewayConfig !== null ;
220+ /**
221+ * Validates and stores custom gateway routing. Shared by the `gateway` auth
222+ * method and the ACP `providers/set` method. Throws `invalid_params` for an
223+ * unsupported protocol or a malformed base URL.
224+ */
225+ private applyGatewayConfig ( params : {
226+ baseUrl : string ;
227+ headers ?: Record < string , string > | undefined ;
228+ providerName ?: string | undefined ;
229+ apiType : acp . LlmProtocol ;
230+ } ) : void {
231+ const apiType = params . apiType ;
232+ const wireApi = SUPPORTED_GATEWAY_PROTOCOLS [ apiType ] ;
233+ if ( ! wireApi ) {
234+ throw RequestError . invalidParams (
235+ { apiType} ,
236+ `Unsupported provider apiType "${ apiType } "; supported: ${ Object . keys ( SUPPORTED_GATEWAY_PROTOCOLS ) . join ( ", " ) } ` ,
237+ ) ;
238+ }
239+ if ( typeof params . baseUrl !== "string" || params . baseUrl . trim ( ) . length === 0 ) {
240+ throw RequestError . invalidParams ( undefined , "baseUrl must be a non-empty string" ) ;
241+ }
242+ const providerName = typeof params . providerName === "string" && params . providerName . trim ( ) . length > 0
243+ ? params . providerName
244+ : "User-provided gateway" ;
245+ const headers : Record < string , string > = {
246+ "X-Client-Feature-ID" : "codex" ,
247+ ...params . headers ,
248+ } ;
249+
250+ this . gatewayConfig = {
251+ modelProvider : CUSTOM_GATEWAY_PROVIDER_ID ,
252+ config : {
253+ name : providerName ,
254+ base_url : params . baseUrl ,
255+ http_headers : headers ,
256+ wire_api : wireApi ,
257+ } ,
258+ } ;
259+ }
260+
261+ /**
262+ * `providers/list`: returns the single client-configurable custom gateway
263+ * provider. `current` carries only non-secret routing (never headers), and is
264+ * `null` when the provider is not configured/disabled.
265+ */
266+ listProviders ( ) : acp . ProviderInfo [ ] {
267+ const gatewayConfig = this . gatewayConfig ;
268+ const current : acp . ProviderCurrentConfig | null = gatewayConfig
269+ ? {
270+ apiType : gatewayApiTypeFromConfig ( gatewayConfig ) ,
271+ baseUrl : gatewayConfig . config . base_url ,
272+ }
273+ : null ;
274+ return [
275+ {
276+ providerId : CUSTOM_GATEWAY_PROVIDER_ID ,
277+ supported : Object . keys ( SUPPORTED_GATEWAY_PROTOCOLS ) ,
278+ required : false ,
279+ current,
280+ } ,
281+ ] ;
282+ }
283+
284+ /**
285+ * `providers/set`: replaces the full configuration for the custom gateway
286+ * provider. Rejects unknown provider ids with `invalid_params`.
287+ */
288+ setProvider ( request : acp . SetProviderRequest ) : void {
289+ if ( request . providerId !== CUSTOM_GATEWAY_PROVIDER_ID ) {
290+ throw RequestError . invalidParams (
291+ { providerId : request . providerId } ,
292+ `Unknown providerId "${ request . providerId } "; only "${ CUSTOM_GATEWAY_PROVIDER_ID } " is configurable` ,
293+ ) ;
294+ }
295+ this . applyGatewayConfig ( {
296+ apiType : request . apiType ,
297+ baseUrl : request . baseUrl ,
298+ headers : request . headers ,
299+ } ) ;
300+ }
301+
302+ /**
303+ * `providers/disable`: disables the custom gateway provider. Disabling an
304+ * unknown provider id is idempotent success (RFD behavior §7).
305+ */
306+ disableProvider ( request : acp . DisableProviderRequest ) : void {
307+ if ( request . providerId === CUSTOM_GATEWAY_PROVIDER_ID ) {
308+ this . gatewayConfig = null ;
309+ }
228310 }
229311
230312 async getAccount ( ) : Promise < GetAccountResponse > {
@@ -732,7 +814,7 @@ export class CodexAcpClient {
732814 const [ allProviders , archivedAllProviders , customGateway ] = await Promise . all ( [
733815 this . codexClient . threadList ( { } ) ,
734816 this . codexClient . threadList ( { archived : true } ) ,
735- this . codexClient . threadList ( { modelProviders : [ "custom-gateway" ] } ) ,
817+ this . codexClient . threadList ( { modelProviders : [ CUSTOM_GATEWAY_PROVIDER_ID ] } ) ,
736818 ] ) ;
737819
738820 return {
@@ -837,13 +919,15 @@ function shouldDeduplicateMcpConflicts(): boolean {
837919 return ! disabledByEnv ;
838920}
839921
922+ type WireApi = "responses" ;
923+
840924interface GatewayConfig {
841925 modelProvider : string ;
842926 config : {
843927 name : string ,
844928 base_url : string ,
845929 http_headers : Record < string , string > ,
846- wire_api : "responses"
930+ wire_api : WireApi
847931 }
848932}
849933
@@ -936,6 +1020,12 @@ function isJsonObject(value: JsonValue | undefined): value is JsonObject {
9361020 return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
9371021}
9381022
1023+ function gatewayApiTypeFromConfig ( gatewayConfig : GatewayConfig ) : acp . LlmProtocol {
1024+ const wireApi = gatewayConfig . config . wire_api ;
1025+ const match = Object . entries ( SUPPORTED_GATEWAY_PROTOCOLS ) . find ( ( [ , wire ] ) => wire === wireApi ) ;
1026+ return match ?. [ 0 ] ?? "openai" ;
1027+ }
1028+
9391029function mergeGatewayConfig ( config : JsonObject , gatewayConfig : GatewayConfig | null ) : JsonObject {
9401030 if ( gatewayConfig !== null ) {
9411031 const newConfig = { ...config } ;
0 commit comments