Skip to content

Commit e512033

Browse files
brettheapclaude
andcommitted
fix(openai-codex-auth): use ChatGPT Codex backend instead of OpenAI API
The OAuth token works with chatgpt.com/backend-api, not api.openai.com. - Change base URL to https://chatgpt.com/backend-api - Use /codex/responses endpoint - Add required headers: OpenAI-Beta, originator, chatgpt-account-id - Extract account ID from JWT token claims 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 08ddff8 commit e512033

3 files changed

Lines changed: 77 additions & 25 deletions

File tree

packages/openai-codex-auth/src/constants.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ export const LOCAL_SERVER = {
1919
} as const
2020

2121
export const CODEX_API = {
22-
baseUrl: "https://api.openai.com/v1",
23-
responsesEndpoint: "/responses",
22+
baseUrl: "https://chatgpt.com/backend-api",
23+
responsesEndpoint: "/codex/responses",
24+
} as const
25+
26+
export const CODEX_HEADERS = {
27+
openAiBeta: "responses=experimental",
28+
originator: "codex_cli_rs",
29+
jwtClaimPath: "https://api.openai.com/auth",
2430
} as const
2531

2632
export const PLUGIN_NAME = "openai-codex-auth"

packages/openai-codex-auth/src/index.ts

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import type { Plugin, AuthHook, AuthOuathResult } from "@opencode-ai/plugin"
99
import { initiateOAuthFlow, refreshAccessToken } from "./auth/auth"
10-
import { PROVIDER_ID, AUTH_LABEL } from "./constants"
11-
import { isTokenExpired } from "./request/fetch-helpers"
10+
import { PROVIDER_ID, AUTH_LABEL, CODEX_API, CODEX_HEADERS } from "./constants"
11+
import { isTokenExpired, extractAccountId } from "./request/fetch-helpers"
1212

1313
/**
1414
* OpenAI Codex Authentication Plugin
@@ -22,7 +22,7 @@ export const OpenAICodexAuthPlugin: Plugin = async (_ctx) => {
2222

2323
/**
2424
* Loader function called when the provider needs authentication
25-
* Handles token refresh and returns SDK options
25+
* Handles token refresh and returns SDK options for Codex backend
2626
*/
2727
loader: async (getAuth, _provider) => {
2828
const auth = await getAuth()
@@ -31,35 +31,47 @@ export const OpenAICodexAuthPlugin: Plugin = async (_ctx) => {
3131
return {}
3232
}
3333

34+
/**
35+
* Build SDK options with Codex backend configuration
36+
*/
37+
const buildOptions = (accessToken: string, refreshedAuth?: object) => {
38+
const accountId = extractAccountId(accessToken)
39+
const headers: Record<string, string> = {
40+
"OpenAI-Beta": CODEX_HEADERS.openAiBeta,
41+
originator: CODEX_HEADERS.originator,
42+
}
43+
if (accountId) {
44+
headers["chatgpt-account-id"] = accountId
45+
}
46+
47+
return {
48+
apiKey: accessToken,
49+
baseURL: CODEX_API.baseUrl,
50+
headers,
51+
...(refreshedAuth ? { _refreshedAuth: refreshedAuth } : {}),
52+
}
53+
}
54+
3455
// Check if token needs refresh
3556
if (isTokenExpired(auth.expires)) {
3657
try {
3758
const tokens = await refreshAccessToken(auth.refresh)
3859
const newExpires = Date.now() + tokens.expires_in * 1000
3960

40-
// Return refreshed credentials
41-
// Note: The opencode system will handle persisting these
42-
return {
43-
apiKey: tokens.access_token,
44-
_refreshedAuth: {
45-
type: "oauth" as const,
46-
access: tokens.access_token,
47-
refresh: tokens.refresh_token,
48-
expires: newExpires,
49-
},
50-
}
61+
return buildOptions(tokens.access_token, {
62+
type: "oauth" as const,
63+
access: tokens.access_token,
64+
refresh: tokens.refresh_token,
65+
expires: newExpires,
66+
})
5167
} catch (error) {
5268
console.error("[openai-codex-auth] Failed to refresh token:", error)
5369
// Return existing token and let the API call fail if it's truly expired
54-
return {
55-
apiKey: auth.access,
56-
}
70+
return buildOptions(auth.access)
5771
}
5872
}
5973

60-
return {
61-
apiKey: auth.access,
62-
}
74+
return buildOptions(auth.access)
6375
},
6476

6577
methods: [

packages/openai-codex-auth/src/request/fetch-helpers.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import { refreshAccessToken } from "../auth/auth"
6-
import { CODEX_API } from "../constants"
6+
import { CODEX_API, CODEX_HEADERS } from "../constants"
77

88
export interface TokenManager {
99
accessToken: string
@@ -51,13 +51,47 @@ export async function getValidAccessToken(manager: TokenManager): Promise<string
5151
}
5252

5353
/**
54-
* Create authorization headers for API requests
54+
* Decode a JWT token and extract claims (without verification)
55+
*/
56+
export function decodeJWT(token: string): Record<string, unknown> {
57+
try {
58+
const parts = token.split(".")
59+
if (parts.length !== 3) return {}
60+
const payload = parts[1]
61+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"))
62+
return JSON.parse(decoded)
63+
} catch {
64+
return {}
65+
}
66+
}
67+
68+
/**
69+
* Extract ChatGPT account ID from JWT token
70+
*/
71+
export function extractAccountId(accessToken: string): string | undefined {
72+
const claims = decodeJWT(accessToken)
73+
const authClaims = claims[CODEX_HEADERS.jwtClaimPath] as Record<string, unknown> | undefined
74+
return authClaims?.["organization_id"] as string | undefined
75+
}
76+
77+
/**
78+
* Create authorization headers for Codex API requests
5579
*/
5680
export function createAuthHeaders(accessToken: string): Record<string, string> {
57-
return {
81+
const headers: Record<string, string> = {
5882
Authorization: `Bearer ${accessToken}`,
5983
"Content-Type": "application/json",
84+
"OpenAI-Beta": CODEX_HEADERS.openAiBeta,
85+
originator: CODEX_HEADERS.originator,
6086
}
87+
88+
// Extract and add account ID from JWT if available
89+
const accountId = extractAccountId(accessToken)
90+
if (accountId) {
91+
headers["chatgpt-account-id"] = accountId
92+
}
93+
94+
return headers
6195
}
6296

6397
/**

0 commit comments

Comments
 (0)