Skip to content

Commit d2e19a9

Browse files
authored
Merge branch 'main' into feat/structured-file-edit-reporting
2 parents d15e89b + 9d48473 commit d2e19a9

22 files changed

Lines changed: 884 additions & 117 deletions

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
},
6363
"dependencies": {
6464
"@agentclientprotocol/sdk": "^1.2.1",
65-
"@openai/codex": "^0.144.0",
65+
"@openai/codex": "^0.144.1",
6666
"diff": "^9.0.0",
6767
"open": "^11.0.0",
6868
"vscode-jsonrpc": "^9.0.1",

src/CodexAcpClient.ts

Lines changed: 127 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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";
22
import type {EmbeddedResourceResource} from "@agentclientprotocol/sdk";
33
import * as acp from "@agentclientprotocol/sdk";
44
import {type McpServer, RequestError} from "@agentclientprotocol/sdk";
@@ -41,6 +41,21 @@ import type {
4141
import packageJson from "../package.json";
4242
import 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> {
@@ -534,6 +616,10 @@ export class CodexAcpClient {
534616
await this.waitForSessionNotifications(sessionId);
535617
return await elicitationHandler.handleElicitation(params);
536618
},
619+
handleUserInput: async (params) => {
620+
await this.waitForSessionNotifications(sessionId);
621+
return await elicitationHandler.handleUserInput(params);
622+
},
537623
});
538624
}
539625

@@ -732,7 +818,7 @@ export class CodexAcpClient {
732818
const [allProviders, archivedAllProviders, customGateway] = await Promise.all([
733819
this.codexClient.threadList({}),
734820
this.codexClient.threadList({archived: true}),
735-
this.codexClient.threadList({modelProviders: ["custom-gateway"]}),
821+
this.codexClient.threadList({modelProviders: [CUSTOM_GATEWAY_PROVIDER_ID]}),
736822
]);
737823

738824
return {
@@ -837,13 +923,15 @@ function shouldDeduplicateMcpConflicts(): boolean {
837923
return !disabledByEnv;
838924
}
839925

926+
type WireApi = "responses";
927+
840928
interface GatewayConfig {
841929
modelProvider: string;
842930
config: {
843931
name: string,
844932
base_url: string,
845933
http_headers: Record<string, string>,
846-
wire_api: "responses"
934+
wire_api: WireApi
847935
}
848936
}
849937

@@ -936,6 +1024,12 @@ function isJsonObject(value: JsonValue | undefined): value is JsonObject {
9361024
return value !== null && typeof value === "object" && !Array.isArray(value);
9371025
}
9381026

1027+
function gatewayApiTypeFromConfig(gatewayConfig: GatewayConfig): acp.LlmProtocol {
1028+
const wireApi = gatewayConfig.config.wire_api;
1029+
const match = Object.entries(SUPPORTED_GATEWAY_PROTOCOLS).find(([, wire]) => wire === wireApi);
1030+
return match?.[0] ?? "openai";
1031+
}
1032+
9391033
function mergeGatewayConfig(config: JsonObject, gatewayConfig: GatewayConfig | null): JsonObject {
9401034
if (gatewayConfig !== null) {
9411035
const newConfig = {...config};

0 commit comments

Comments
 (0)