@@ -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 < string , GatewayConfig [ "config" ] [ "wire_api" ] > = {
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.
@@ -109,24 +124,11 @@ export class CodexAcpClient {
109124 const gatewaySettings = authRequest . _meta [ "gateway" ] ;
110125 if ( ! gatewaySettings ) throw RequestError . invalidRequest ( ) ;
111126
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- } ;
127+ this . applyGatewayConfig ( {
128+ baseUrl : gatewaySettings . baseUrl ,
129+ headers : gatewaySettings . headers ,
130+ providerName : gatewaySettings . providerName ,
131+ } ) ;
130132
131133 // Early return: model provider information will be sent to Codex later during the session creation
132134 return true ;
@@ -227,6 +229,98 @@ export class CodexAcpClient {
227229 return this . gatewayConfig !== null ;
228230 }
229231
232+ /**
233+ * Validates and stores custom gateway routing. Shared by the `gateway` auth
234+ * method and the ACP `providers/set` method. Throws `invalid_params` for an
235+ * unsupported protocol or a malformed base URL.
236+ */
237+ private applyGatewayConfig ( params : {
238+ baseUrl : string ;
239+ headers ?: Record < string , string > | undefined ;
240+ providerName ?: string | undefined ;
241+ apiType ?: acp . LlmProtocol | undefined ;
242+ } ) : void {
243+ const apiType = params . apiType ?? "openai" ;
244+ const wireApi = SUPPORTED_GATEWAY_PROTOCOLS [ apiType ] ;
245+ if ( ! wireApi ) {
246+ throw RequestError . invalidParams (
247+ { apiType} ,
248+ `Unsupported provider apiType "${ apiType } "; supported: ${ Object . keys ( SUPPORTED_GATEWAY_PROTOCOLS ) . join ( ", " ) } ` ,
249+ ) ;
250+ }
251+ if ( typeof params . baseUrl !== "string" || params . baseUrl . trim ( ) . length === 0 ) {
252+ throw RequestError . invalidParams ( undefined , "baseUrl must be a non-empty string" ) ;
253+ }
254+ const providerName = typeof params . providerName === "string" && params . providerName . trim ( ) . length > 0
255+ ? params . providerName
256+ : "User-provided gateway" ;
257+ const headers : Record < string , string > = {
258+ "X-Client-Feature-ID" : "codex" ,
259+ ...params . headers ,
260+ } ;
261+
262+ this . gatewayConfig = {
263+ modelProvider : CUSTOM_GATEWAY_PROVIDER_ID ,
264+ config : {
265+ name : providerName ,
266+ base_url : params . baseUrl ,
267+ http_headers : headers ,
268+ wire_api : wireApi ,
269+ } ,
270+ } ;
271+ }
272+
273+ /**
274+ * `providers/list`: returns the single client-configurable custom gateway
275+ * provider. `current` carries only non-secret routing (never headers), and is
276+ * `null` when the provider is not configured/disabled.
277+ */
278+ listProviders ( ) : acp . ProviderInfo [ ] {
279+ const gatewayConfig = this . gatewayConfig ;
280+ const current : acp . ProviderCurrentConfig | null = gatewayConfig
281+ ? {
282+ apiType : gatewayApiTypeFromConfig ( gatewayConfig ) ,
283+ baseUrl : gatewayConfig . config . base_url ,
284+ }
285+ : null ;
286+ return [
287+ {
288+ providerId : CUSTOM_GATEWAY_PROVIDER_ID ,
289+ supported : Object . keys ( SUPPORTED_GATEWAY_PROTOCOLS ) ,
290+ required : false ,
291+ current,
292+ } ,
293+ ] ;
294+ }
295+
296+ /**
297+ * `providers/set`: replaces the full configuration for the custom gateway
298+ * provider. Rejects unknown provider ids with `invalid_params`.
299+ */
300+ setProvider ( request : acp . SetProviderRequest ) : void {
301+ if ( request . providerId !== CUSTOM_GATEWAY_PROVIDER_ID ) {
302+ throw RequestError . invalidParams (
303+ { providerId : request . providerId } ,
304+ `Unknown providerId "${ request . providerId } "; only "${ CUSTOM_GATEWAY_PROVIDER_ID } " is configurable` ,
305+ ) ;
306+ }
307+ this . applyGatewayConfig ( {
308+ apiType : request . apiType ,
309+ baseUrl : request . baseUrl ,
310+ headers : request . headers ,
311+ } ) ;
312+ }
313+
314+ /**
315+ * `providers/disable`: disables the custom gateway provider. Disabling an
316+ * unknown provider id is idempotent success (RFD behavior §7).
317+ */
318+ disableProvider ( request : acp . DisableProviderRequest ) : void {
319+ if ( request . providerId === CUSTOM_GATEWAY_PROVIDER_ID ) {
320+ this . gatewayConfig = null ;
321+ }
322+ }
323+
230324 async getAccount ( ) : Promise < GetAccountResponse > {
231325 return this . codexClient . accountRead ( { refreshToken : false } ) ;
232326 }
@@ -732,7 +826,7 @@ export class CodexAcpClient {
732826 const [ allProviders , archivedAllProviders , customGateway ] = await Promise . all ( [
733827 this . codexClient . threadList ( { } ) ,
734828 this . codexClient . threadList ( { archived : true } ) ,
735- this . codexClient . threadList ( { modelProviders : [ "custom-gateway" ] } ) ,
829+ this . codexClient . threadList ( { modelProviders : [ CUSTOM_GATEWAY_PROVIDER_ID ] } ) ,
736830 ] ) ;
737831
738832 return {
@@ -936,6 +1030,12 @@ function isJsonObject(value: JsonValue | undefined): value is JsonObject {
9361030 return value !== null && typeof value === "object" && ! Array . isArray ( value ) ;
9371031}
9381032
1033+ function gatewayApiTypeFromConfig ( gatewayConfig : GatewayConfig ) : acp . LlmProtocol {
1034+ const wireApi = gatewayConfig . config . wire_api ;
1035+ const match = Object . entries ( SUPPORTED_GATEWAY_PROTOCOLS ) . find ( ( [ , wire ] ) => wire === wireApi ) ;
1036+ return match ?. [ 0 ] ?? "openai" ;
1037+ }
1038+
9391039function mergeGatewayConfig ( config : JsonObject , gatewayConfig : GatewayConfig | null ) : JsonObject {
9401040 if ( gatewayConfig !== null ) {
9411041 const newConfig = { ...config } ;
0 commit comments