fix: harden codex runtime routing and oauth recovery

This commit is contained in:
Cita
2026-03-21 19:07:14 +08:00
parent 6a19d809f0
commit 1d8889001f
18 changed files with 775 additions and 195 deletions
+6 -2
View File
@@ -91,11 +91,15 @@ function parseTokenResponsePayload(payload: unknown): CodexTokenExchangeResult {
throw new Error('codex token exchange response missing required fields');
}
const claims = parseJwtClaims(idToken);
const accountId = asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_account_id);
if (!accountId) {
throw new Error('codex token exchange response missing chatgpt_account_id');
}
return {
accessToken,
refreshToken,
idToken,
accountId: asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_account_id),
accountId,
email: asTrimmedString(claims?.email),
planType: asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_plan_type),
tokenExpiresAt: Date.now() + expiresIn * 1000,
@@ -220,7 +224,7 @@ export const codexOauthProvider: OAuthProviderDefinition = {
};
},
buildProxyHeaders: ({ oauth, downstreamHeaders }) => {
const accountId = getHeaderValue(downstreamHeaders, 'chatgpt-account-id') || oauth.accountId || oauth.accountKey;
const accountId = oauth.accountId || oauth.accountKey;
const originator = getHeaderValue(downstreamHeaders, 'originator') || 'codex_cli_rs';
const headers: Record<string, string> = {
Originator: originator,
+18 -3
View File
@@ -178,7 +178,7 @@ async function findExistingOauthAccount(input: {
if (byKey) return byKey;
}
if (!accountKey && email) {
if (!accountKey && email && input.provider !== 'codex') {
const byEmail = await db.select().from(schema.accounts).where(and(
eq(schema.accounts.oauthProvider, input.provider),
eq(schema.accounts.username, email),
@@ -253,6 +253,7 @@ async function upsertOauthAccount(input: {
account: await db.select().from(schema.accounts).where(eq(schema.accounts.id, existing.id)).get(),
site,
created: false,
previousAccount: existing,
};
}
@@ -270,7 +271,7 @@ async function upsertOauthAccount(input: {
isPinned: false,
sortOrder: await getNextAccountSortOrder(),
}).returning().get();
return { account: created, site, created: true };
return { account: created, site, created: true, previousAccount: null };
}
export function listOauthProviders() {
@@ -365,7 +366,7 @@ export async function handleOauthCallback(input: {
codeVerifier: session.codeVerifier,
projectId: session.projectId,
});
const { account, site, created } = await upsertOauthAccount({
const { account, site, created, previousAccount } = await upsertOauthAccount({
definition,
exchange,
rebindAccountId: session.rebindAccountId,
@@ -379,6 +380,20 @@ export async function handleOauthCallback(input: {
if (refreshResult.status !== 'success') {
if (created) {
await db.delete(schema.accounts).where(eq(schema.accounts.id, account.id)).run();
} else if (previousAccount) {
await db.update(schema.accounts).set({
siteId: previousAccount.siteId,
username: previousAccount.username,
accessToken: previousAccount.accessToken,
apiToken: previousAccount.apiToken,
checkinEnabled: previousAccount.checkinEnabled,
status: previousAccount.status,
oauthProvider: previousAccount.oauthProvider,
oauthAccountKey: previousAccount.oauthAccountKey,
oauthProjectId: previousAccount.oauthProjectId,
extraConfig: previousAccount.extraConfig,
updatedAt: previousAccount.updatedAt,
}).where(eq(schema.accounts.id, previousAccount.id)).run();
}
await rebuildTokenRoutesFromAvailability();
const errorMessage = refreshResult.errorMessage || `${input.provider} model discovery failed`;
@@ -164,4 +164,49 @@ describe('TokenRouter site status guard', () => {
expect(selected).not.toBeNull();
expect(selected?.tokenValue).toBe('oauth-access-token');
});
it('prefers codex oauth access token over a stale legacy api token on oauth-managed rows', async () => {
const site = await db.insert(schema.sites).values({
name: 'codex-site',
url: 'https://chatgpt.com/backend-api/codex',
platform: 'codex',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'codex-user@example.com',
accessToken: 'oauth-access-token',
apiToken: 'stale-direct-token',
status: 'active',
extraConfig: JSON.stringify({
credentialMode: 'session',
oauth: {
provider: 'codex',
accountId: 'chatgpt-account-123',
email: 'codex-user@example.com',
planType: 'plus',
},
}),
}).returning().get();
const route = await db.insert(schema.tokenRoutes).values({
modelPattern: 'gpt-5.2-codex',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: route.id,
accountId: account.id,
tokenId: null,
priority: 0,
weight: 10,
enabled: true,
manualOverride: false,
}).run();
const selected = await new TokenRouter().selectChannel('gpt-5.2-codex');
expect(selected).not.toBeNull();
expect(selected?.tokenValue).toBe('oauth-access-token');
});
});
+5 -4
View File
@@ -2037,14 +2037,15 @@ export class TokenRouter {
return token ? token : null;
}
const fallback = candidate.account.apiToken?.trim();
if (fallback) return fallback;
if (getOauthInfoFromExtraConfig(candidate.account.extraConfig)) {
const accessToken = candidate.account.accessToken?.trim();
return accessToken || null;
if (accessToken) return accessToken;
return null;
}
const fallback = candidate.account.apiToken?.trim();
if (fallback) return fallback;
return null;
}