Skip to content

Commit 314768a

Browse files
committed
Merge branch 'main' into goal
2 parents 4461c54 + b719332 commit 314768a

10 files changed

Lines changed: 633 additions & 29 deletions

package-lock.json

Lines changed: 4 additions & 4 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
@@ -61,7 +61,7 @@
6161
"vitest": "^4.0.10"
6262
},
6363
"dependencies": {
64-
"@agentclientprotocol/sdk": "^0.29.0",
64+
"@agentclientprotocol/sdk": "^1.0.0",
6565
"@openai/codex": "^0.142.2",
6666
"diff": "^8.0.3",
6767
"open": "^11.0.0",

src/CodexAcpClient.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ export class CodexAcpClient {
196196
return sessionModelProvider;
197197
}
198198
const settingsModelProvider = await this.codexClient.configRead({includeLayers: false});
199-
return settingsModelProvider.config.model_provider ?? null;
199+
return settingsModelProvider?.config?.model_provider ?? null;
200200
}
201201

202202
async logout(): Promise<void> {
@@ -218,6 +218,10 @@ export class CodexAcpClient {
218218
return response.requiresOpenaiAuth && !response.account;
219219
}
220220

221+
hasGatewayAuth(): boolean {
222+
return this.gatewayConfig !== null;
223+
}
224+
221225
async getAccount(): Promise<GetAccountResponse> {
222226
return this.codexClient.accountRead({refreshToken: false});
223227
}
@@ -229,7 +233,7 @@ export class CodexAcpClient {
229233
const response = await this.codexClient.threadResume({
230234
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
231235
cwd: request.cwd,
232-
modelProvider: this.getResumeModelProvider(),
236+
modelProvider: await this.getResumeModelProvider(),
233237
threadId: request.sessionId,
234238
});
235239
onSubscribed?.();
@@ -239,6 +243,7 @@ export class CodexAcpClient {
239243
sessionId: request.sessionId,
240244
currentModelId: currentModelId,
241245
models: codexModels,
246+
modelProvider: response.modelProvider,
242247
currentServiceTier: response.serviceTier as ServiceTier ?? null,
243248
additionalDirectories,
244249
}
@@ -251,7 +256,7 @@ export class CodexAcpClient {
251256
const response = await this.codexClient.threadResume({
252257
config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers ?? []),
253258
cwd: request.cwd,
254-
modelProvider: this.getResumeModelProvider(),
259+
modelProvider: await this.getResumeModelProvider(),
255260
threadId: request.sessionId,
256261
});
257262
onSubscribed?.();
@@ -265,6 +270,7 @@ export class CodexAcpClient {
265270
sessionId: request.sessionId,
266271
currentModelId: currentModelId,
267272
models: codexModels,
273+
modelProvider: response.modelProvider,
268274
currentServiceTier: response.serviceTier as ServiceTier ?? null,
269275
thread: historyResponse.thread,
270276
additionalDirectories,
@@ -290,6 +296,7 @@ export class CodexAcpClient {
290296
sessionId: response.thread.id,
291297
currentModelId: currentModelId,
292298
models: codexModels,
299+
modelProvider: response.modelProvider,
293300
currentServiceTier: response.serviceTier as ServiceTier ?? null,
294301
additionalDirectories,
295302
};
@@ -414,10 +421,10 @@ export class CodexAcpClient {
414421
return this.gatewayConfig?.modelProvider ?? this.modelProvider;
415422
}
416423

417-
private getResumeModelProvider(): string {
418-
// Passing `null` forces codex to use the persisted provider for resumed session instead of default one
419-
// Explicit fallback to "openai" fixes error `Model provider not found` at least for ChatGPT authentication
420-
return this.getModelProvider() ?? "openai";
424+
private async getResumeModelProvider(): Promise<string> {
425+
// Prefer an explicit/gateway provider, then the provider persisted in Codex config.
426+
// Keep OpenAI as the final fallback for ChatGPT-authenticated sessions without a configured provider.
427+
return (await this.getCurrentModelProvider()) ?? "openai";
421428
}
422429

423430
private async refreshSkills(
@@ -732,6 +739,7 @@ export type SessionMetadata = {
732739
sessionId: string,
733740
currentModelId: string,
734741
models: Model[],
742+
modelProvider?: string | null,
735743
currentServiceTier?: ServiceTier | null,
736744
additionalDirectories: string[],
737745
}

src/CodexAcpServer.ts

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {RequestError, type SessionId, type SessionModeState} from "@agentclientp
33
import {CodexEventHandler} from "./CodexEventHandler";
44
import {CodexApprovalHandler} from "./CodexApprovalHandler";
55
import {CodexElicitationHandler} from "./CodexElicitationHandler";
6-
import {type CodexAuthRequest, getCodexAuthMethods} from "./CodexAuthMethod";
6+
import {type CodexAuthRequest, getCodexAuthMethods, isCodexAuthRequest} from "./CodexAuthMethod";
77
import {CodexAcpClient, type SessionMetadata, type SessionMetadataWithThread} from "./CodexAcpClient";
88
import type {McpStartupResult} from "./CodexAppServerClient";
99
import {ACPSessionConnection, type AcpClientConnection, type UpdateSessionEvent} from "./ACPSessionConnection";
@@ -86,6 +86,8 @@ export interface SessionState {
8686
modelContextWindow: number | null;
8787
rateLimits: RateLimitsMap | null;
8888
account: Account | null;
89+
authConfigured: boolean;
90+
authProvider: string | null;
8991
cwd: string;
9092
additionalDirectories: string[];
9193
fastModeEnabled: boolean;
@@ -95,6 +97,11 @@ export interface SessionState {
9597
currentGoal?: ThreadGoalSnapshot | null;
9698
}
9799

100+
interface ActiveAuthState {
101+
account: Account | null;
102+
authConfigured: boolean;
103+
}
104+
98105
interface PendingMcpStartupSession {
99106
requestedServers: Set<string>;
100107
afterVersion: number;
@@ -164,7 +171,8 @@ export class CodexAcpServer {
164171
this.availableCommands = new CodexCommands(
165172
connection,
166173
codexAcpClient,
167-
(operation) => this.runWithProcessCheck(operation)
174+
(operation) => this.runWithProcessCheck(operation),
175+
() => this.refreshSessionsAuthState(null)
168176
);
169177
}
170178

@@ -255,6 +263,7 @@ export class CodexAcpServer {
255263
async handleError(e: Error){
256264
if (e.message.includes("log out") || e.message.includes("cloud requirements")) {
257265
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
266+
await this.refreshSessionsAuthState(null);
258267
throw RequestError.internalError(`${(e.message)}\n\nYou have been logged out. Please try again.`);
259268
}
260269
}
@@ -354,9 +363,10 @@ export class CodexAcpServer {
354363
}
355364

356365
const {sessionId, currentModelId, models} = sessionMetadata;
357-
let account: Account | null;
366+
const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider();
367+
let authState: ActiveAuthState;
358368
try {
359-
account = await this.getActiveAccount();
369+
authState = await this.getAuthStateForProvider(authProvider);
360370
} catch (err) {
361371
if (resumeSubscribed && requestedSessionGeneration !== null) {
362372
await this.cleanupStaleSessionOpen(sessionId, requestedSessionGeneration);
@@ -383,7 +393,9 @@ export class CodexAcpServer {
383393
totalTokenUsage: null,
384394
modelContextWindow: null,
385395
rateLimits: null,
386-
account: account,
396+
account: authState.account,
397+
authConfigured: authState.authConfigured,
398+
authProvider: authProvider,
387399
cwd: request.cwd,
388400
additionalDirectories: sessionMetadata.additionalDirectories,
389401
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",
@@ -409,12 +421,36 @@ export class CodexAcpServer {
409421
return [sessionId, sessionModelState, sessionModeState];
410422
}
411423

412-
private async getActiveAccount(){
413-
if (this.codexAcpClient.getModelProvider()) {
414-
return null
424+
private async getAuthStateForProvider(authProvider: string | null): Promise<ActiveAuthState> {
425+
if (!this.authProviderUsesOpenAiAccount(authProvider)) {
426+
return {
427+
account: null,
428+
authConfigured: true,
429+
};
415430
}
416431
const accountResponse = await this.runWithProcessCheck(() => this.codexAcpClient.getAccount());
417-
return accountResponse.account;
432+
return {
433+
account: accountResponse.account,
434+
authConfigured: accountResponse.account !== null || !accountResponse.requiresOpenaiAuth,
435+
};
436+
}
437+
438+
private authProviderUsesOpenAiAccount(authProvider: string | null): boolean {
439+
return authProvider === null || authProvider === "openai";
440+
}
441+
442+
private authProvidersMatch(a: string | null, b: string | null): boolean {
443+
if (this.authProviderUsesOpenAiAccount(a) && this.authProviderUsesOpenAiAccount(b)) {
444+
return true;
445+
}
446+
return a === b;
447+
}
448+
449+
private getAuthProviderForAuthenticateRequest(request: acp.AuthenticateRequest): string | null {
450+
if (isCodexAuthRequest(request) && request.methodId === "gateway") {
451+
return "custom-gateway";
452+
}
453+
return null;
418454
}
419455

420456
async loadSession(params: acp.LoadSessionRequest): Promise<LegacyLoadSessionResponse> {
@@ -573,16 +609,32 @@ export class CodexAcpServer {
573609
logger.log("Authenticate request failed");
574610
throw RequestError.invalidParams();
575611
}
612+
await this.refreshSessionsAuthState(this.getAuthProviderForAuthenticateRequest(_params));
576613
logger.log("Authenticate request completed");
577614
return { };
578615
}
579616

580617
async logout(_params: acp.LogoutRequest): Promise<void> {
581618
logger.log("Logout request received");
582619
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
620+
await this.refreshSessionsAuthState(null);
583621
logger.log("Logout request completed");
584622
}
585623

624+
private async refreshSessionsAuthState(authProvider: string | null): Promise<void> {
625+
if (this.sessions.size === 0) return;
626+
627+
const sessionsToRefresh = [...this.sessions.values()]
628+
.filter(sessionState => this.authProvidersMatch(sessionState.authProvider, authProvider));
629+
if (sessionsToRefresh.length === 0) return;
630+
631+
const authState = await this.getAuthStateForProvider(authProvider);
632+
for (const sessionState of sessionsToRefresh) {
633+
sessionState.account = authState.account;
634+
sessionState.authConfigured = authState.authConfigured;
635+
}
636+
}
637+
586638
async setSessionMode(
587639
_params: acp.SetSessionModeRequest,
588640
): Promise<acp.SetSessionModeResponse> {
@@ -806,9 +858,10 @@ export class CodexAcpServer {
806858
}
807859

808860
const {sessionId, currentModelId, models, thread} = sessionMetadata;
809-
let account: Account | null;
861+
const authProvider = sessionMetadata.modelProvider ?? this.codexAcpClient.getModelProvider();
862+
let authState: ActiveAuthState;
810863
try {
811-
account = await this.getActiveAccount();
864+
authState = await this.getAuthStateForProvider(authProvider);
812865
} catch (err) {
813866
if (subscribed) {
814867
await this.cleanupStaleSessionOpen(request.sessionId, requestedSessionGeneration);
@@ -834,7 +887,9 @@ export class CodexAcpServer {
834887
totalTokenUsage: null,
835888
modelContextWindow: null,
836889
rateLimits: null,
837-
account: account,
890+
account: authState.account,
891+
authConfigured: authState.authConfigured,
892+
authProvider: authProvider,
838893
cwd: request.cwd,
839894
additionalDirectories: sessionMetadata.additionalDirectories,
840895
fastModeEnabled: sessionMetadata.currentServiceTier === "fast",

src/CodexCommands.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,24 @@ export type CommandHandleOptions = {
2222
onTurnStarted?: (turnId: string, threadId: string) => void;
2323
};
2424

25+
export type LogoutHandler = () => void | Promise<void>;
26+
2527
export class CodexCommands {
2628
private readonly connection: AcpClientConnection;
2729
private readonly codexAcpClient: CodexAcpClient;
2830
private readonly runWithProcessCheck: <T>(operation: () => Promise<T>) => Promise<T>;
31+
private readonly onLogout: LogoutHandler;
2932

3033
constructor(
3134
connection: AcpClientConnection,
3235
codexAcpClient: CodexAcpClient,
33-
runWithProcessCheck: <T>(operation: () => Promise<T>) => Promise<T>
36+
runWithProcessCheck: <T>(operation: () => Promise<T>) => Promise<T>,
37+
onLogout: LogoutHandler = () => {}
3438
) {
3539
this.connection = connection;
3640
this.codexAcpClient = codexAcpClient;
3741
this.runWithProcessCheck = runWithProcessCheck;
42+
this.onLogout = onLogout;
3843
}
3944

4045
async publish(sessionId: string): Promise<void> {
@@ -207,6 +212,7 @@ export class CodexCommands {
207212
}
208213
case "logout": {
209214
await this.runWithProcessCheck(() => this.codexAcpClient.logout());
215+
await this.onLogout();
210216
const session = new ACPSessionConnection(this.connection, sessionId);
211217
await session.update({
212218
sessionUpdate: "agent_message_chunk",

src/CodexEventHandler.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -619,9 +619,15 @@ export class CodexEventHandler {
619619
}
620620

621621
private async createErrorEvent(params: ErrorNotification): Promise<UpdateSessionEvent> {
622-
const error = params.error.codexErrorInfo
623-
if (error == "unauthorized" || error == "usageLimitExceeded" || this.getHttpStatusCode(error) == 401) {
624-
this.failure = RequestError.authRequired();
622+
const error = params.error.codexErrorInfo;
623+
if (error === "usageLimitExceeded") {
624+
this.failure = RequestError.internalError(
625+
this.createTurnErrorData(params.error),
626+
);
627+
} else if (this.isAuthenticationRequiredError(error)) {
628+
this.failure = this.sessionState.authConfigured
629+
? RequestError.internalError(this.createTurnErrorData(params.error))
630+
: RequestError.authRequired(this.createTurnErrorData(params.error), params.error.message);
625631
}
626632
return {
627633
sessionUpdate: "agent_message_chunk",
@@ -632,6 +638,10 @@ export class CodexEventHandler {
632638
}
633639
}
634640

641+
private isAuthenticationRequiredError(error: CodexErrorInfo | null): boolean {
642+
return error === "unauthorized" || this.getHttpStatusCode(error) === 401;
643+
}
644+
635645
private getHttpStatusCode(error: CodexErrorInfo | null): number | null {
636646
if (error !== null && typeof error === "object") {
637647
if ("httpConnectionFailed" in error) {
@@ -647,6 +657,27 @@ export class CodexEventHandler {
647657
return null;
648658
}
649659

660+
private createTurnErrorData(error: ErrorNotification["error"]): {
661+
message: string;
662+
codexErrorInfo?: CodexErrorInfo;
663+
additionalDetails?: string;
664+
} {
665+
const data: {
666+
message: string;
667+
codexErrorInfo?: CodexErrorInfo;
668+
additionalDetails?: string;
669+
} = {
670+
message: error.additionalDetails ?? error.message,
671+
};
672+
if (error.codexErrorInfo !== null) {
673+
data.codexErrorInfo = error.codexErrorInfo;
674+
}
675+
if (error.additionalDetails !== null) {
676+
data.additionalDetails = error.additionalDetails;
677+
}
678+
return data;
679+
}
680+
650681
private handleTokenUsageUpdated(params: ThreadTokenUsageUpdatedNotification): void {
651682
this.sessionState.lastTokenUsage = toTokenCount(params.tokenUsage.last);
652683
this.sessionState.totalTokenUsage = toTokenCount(params.tokenUsage.total);

0 commit comments

Comments
 (0)