Codex API Support: Feature Parity & UI Overhaul#2
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds OpenAI Codex integration: new Codex OAuth PKCE client and local callback server, persistent Codex tokens and subscriber checks, Codex fetch adapters translating Anthropic requests/responses (including SSE, vision, and tool payloads), UI/login/billing/model updates, and related config/CLI tweaks. Changes
Sequence DiagramssequenceDiagram
participant User as User (CLI)
participant OAuth as Codex OAuth Flow
participant Local as Local Callback Server (127.0.0.1:1455)
participant OpenAI as OpenAI Auth Server
participant Store as Token Store
User->>OAuth: Initiate Codex login
OAuth->>Local: Start callback server
OAuth->>OpenAI: Open browser with auth URL (PKCE)
OpenAI->>Local: Redirect with code & state
Local->>OAuth: Return code
OAuth->>OpenAI: Exchange code for tokens
OpenAI->>OAuth: Return access/refresh tokens
OAuth->>Store: Save CodexTokens (access, refresh, expiresAt, accountId)
OAuth->>User: Emit login success
sequenceDiagram
participant CLI as Client (Anthropic-format)
participant API as getAnthropicClient
participant Adapter as Codex Fetch Adapter
participant Codex as OpenAI Codex API
participant CLIResp as Client Response Handler
CLI->>API: request Anthropic /v1/messages
API->>API: isCodexSubscriber? -> createCodexFetch(accessToken)
API->>Adapter: POST /v1/messages (Anthropic JSON)
Adapter->>Adapter: Translate to Codex /v1/responses payload
Adapter->>Codex: POST /v1/responses (Codex format)
Codex-->>Adapter: SSE stream (response.* events)
Adapter->>Adapter: Map SSE to Anthropic events (message_start, content_block_delta, thinking, tool_use)
Adapter-->>CLIResp: Emit Anthropic-compatible SSE events
Codex->>Adapter: response.completed (usage tokens)
Adapter->>CLIResp: Emit message_stop with usage metrics
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
I added the Open Ai Codex Oauth support .....and want to add support for more other models and providers....idea is simple ....I took inspiration from openclaw repo..... |
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/services/api/client.ts (1)
138-140:⚠️ Potential issue | 🔴 CriticalStrip Anthropic auth headers before building the Codex client.
By the time this branch runs,
defaultHeadersmay already includeAuthorizationfromconfigureApiKeyHeaders()and any user-suppliedANTHROPIC_CUSTOM_HEADERS. Reusing...ARGShere can leak Anthropic credentials to the Codex backend or send conflicting auth on the OpenAI path.Possible fix
- if (!isClaudeAISubscriber()) { + if (!isClaudeAISubscriber() && !isCodexSubscriber()) { await configureApiKeyHeaders(defaultHeaders, getIsNonInteractiveSession()) } … if (isCodexSubscriber()) { const codexTokens = getCodexOAuthTokens() if (codexTokens?.accessToken) { + const codexHeaders = { ...defaultHeaders } + delete codexHeaders.Authorization + const codexFetch = createCodexFetch(codexTokens.accessToken) const clientConfig: ConstructorParameters<typeof Anthropic>[0] = { apiKey: 'codex-placeholder', // SDK requires a key but the fetch adapter handles auth ...ARGS, + defaultHeaders: codexHeaders, fetch: codexFetch as unknown as typeof globalThis.fetch, ...(isDebugToStdErr() && { logger: createStderrLogger() }), }Also applies to: 303-315
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/client.ts` around lines 138 - 140, The code may leak Anthropic credentials when building the Codex/OpenAI client because defaultHeaders (populated via configureApiKeyHeaders and ANTHROPIC_CUSTOM_HEADERS) can contain Authorization or Anthropic-specific headers; before constructing the Codex client/path guarded by isClaudeAISubscriber(), remove any Anthropic auth headers (e.g., Authorization if it came from Anthropic context and any ANTHROPIC_* custom headers) or create a fresh headers object for the Codex/OpenAI path; update the logic around isClaudeAISubscriber(), configureApiKeyHeaders(), and the Codex client construction to sanitize defaultHeaders (strip "Authorization" when Anthropic context or remove keys with "ANTHROPIC_" prefix) so only appropriate OpenAI/Codex auth is sent (apply same fix to the other occurrence noted around lines 303-315).
🧹 Nitpick comments (1)
.gitignore (1)
5-5: Anchor this ignore rule if only the repo-root gateway should be excluded.
openclaw/matches any directory with that name anywhere in the tree. If the intent is just the top-level gateway folder from this PR,/openclaw/is safer.♻️ Minimal diff
-openclaw/ +/openclaw/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.gitignore at line 5, The .gitignore entry "openclaw/" is unanchored and will ignore any directory named openclaw anywhere; change it to "/openclaw/" to anchor it to the repository root so only the top-level gateway directory is excluded. Update the .gitignore line from openclaw/ to /openclaw/ to restrict the ignore to the repo-root folder.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/cli/handlers/auth.ts`:
- Around line 118-127: authStatus() currently derives loggedIn and authMethod
only from Anthropic API key/token sources, so a user who only completed Codex
OAuth (persisted by saveCodexOAuthTokens) is treated as "Not logged in"; update
authStatus() to detect the presence of Codex session tokens (the same config
slot written by saveCodexOAuthTokens) and set loggedIn = true and authMethod =
"codex" when no Anthropic credentials exist but Codex tokens are present,
ensuring all downstream checks use this updated status.
In `@src/components/ConsoleOAuthFlow.tsx`:
- Around line 267-274: The Codex OAuth flow never wires the paste prompt to
Codex-specific handling; update startCodexOAuth to pass an onManualInput
callback into runCodexOAuthFlow (instead of leaving it out) that accepts the
pasted code, calls the Codex token-exchange/verification logic (not
handleSubmitCode which targets the Anthropic oauthService), updates
setOAuthStatus appropriately, and closes the paste prompt via
setShowPastePrompt; ensure the same onManualInput is used by the UI shown in the
'waiting_for_login' state so pasted codes trigger the Codex flow rather than the
Anthropic handler.
- Around line 275-279: The current code calls saveCodexOAuthTokens(codexTokens)
directly which bypasses the shared post-login cleanup in installOAuthTokens;
change this to invoke the shared installer/cleanup flow (call installOAuthTokens
with the Codex tokens or call installOAuthTokens after saving) so the
logout/cache/account bootstrap in installOAuthTokens (the logic around
installOAuthTokens in src/cli/handlers/auth.ts) runs and clears any stale
Anthropic state; import and use installOAuthTokens instead of (or immediately
after) saveCodexOAuthTokens while keeping the existing
logEvent('tengu_oauth_codex_success', {}), setOAuthStatus({ state: 'success' }),
and sendNotification calls.
In `@src/services/api/codex-fetch-adapter.ts`:
- Around line 221-229: The adapter currently ignores the original fetch contract
by always forcing stream: true, reading only init.body, and returning
translateCodexStreamToAnthropic(...); change it to respect the incoming
Request/init: detect if input is a Request and extract/clone its body and
AbortSignal, only set codexBody.stream = true when the incoming
anthropicBody.stream === true (otherwise preserve false), forward the original
AbortSignal into the fetch to Codex, and implement a non-stream translation path
that returns the raw JSON response when stream is not enabled; keep using
translateCodexStreamToAnthropic(...) only when anthropicBody.stream === true and
ensure Request/Response body handling is compatible with both streaming and
non-streaming callers (refer to codexBody, init.body,
translateCodexStreamToAnthropic, and anthropicBody.stream).
- Around line 221-229: The codexBody construction is overwriting
caller-specified tool selection and parallelization by hardcoding tool_choice:
'auto' and parallel_tool_calls: true; update the logic that builds codexBody
(the codexBody object in codex-fetch-adapter.ts) to only set tool_choice and
parallel_tool_calls when those keys are absent on the incoming request (preserve
incoming values if present), e.g., merge or shallow-copy the caller-provided
options into codexBody before defaulting, so callers’ explicit tool selection
and parallel_tool_calls settings are not lost.
- Around line 696-737: The header 'chatgpt-account-id' is computed once from the
constructor accessToken (accountId) but the request may use the refreshed token
(currentToken); update createCodexFetch to recompute the account id from
currentToken (using extractAccountId or equivalent) right before building the
Codex request headers so the 'chatgpt-account-id' matches the actual bearer
token sent (refer to createCodexFetch, getCodexOAuthTokens, currentToken, and
accountId).
- Around line 48-60: The extractAccountId function is decoding the JWT payload
with atob which expects standard base64; update it to decode base64url instead
(handle '-' -> '+', '_' -> '/', add '=' padding to length%4) before JSON.parse;
implement a small helper (e.g., base64UrlDecode) and use it in extractAccountId
(which references JWT_CLAIM_PATH) so the payload is correctly decoded for RFC
7515/7519 base64url-encoded JWT segments.
In `@src/services/oauth/codex-client.ts`:
- Line 286: The server binds to 127.0.0.1 but the redirect URI uses
http://localhost:1455/auth/callback, which can resolve to ::1 on some systems;
change the bind in s.listen to use the same loopback host family as the redirect
URI by replacing '127.0.0.1' with 'localhost' (or bind to both IPv4 and IPv6) so
the OAuth callback reaches the listener; update the s.listen(1455, '127.0.0.1',
...) call accordingly.
In `@src/utils/auth.ts`:
- Around line 1324-1334: The current saveCodexOAuthTokens implementation writes
plaintext accessToken and refreshToken into GlobalConfig (via saveGlobalConfig
and the codexOAuth object); instead, remove secrets from GlobalConfig and
persist only non-secret metadata (e.g., accountId, expiresAt) in GlobalConfig,
and store accessToken/refreshToken in a secure credential store (OS keychain or
encrypted file). Update saveCodexOAuthTokens to call the secure store API (e.g.,
setSecureCredential or a new saveCodexSecrets function) for tokens and write
only accountId/expiresAt into codexOAuth via saveGlobalConfig; also update any
counterpart loader (e.g., getCodexOAuthTokens/getCodexOAuthFromConfig) to read
tokens from the secure store instead of GlobalConfig.
- Around line 1340-1357: getCodexOAuthTokens() currently only checks presence of
fields and returns expired tokens as valid; update it to validate expiresAt is
in the future (e.g., compare stored.expiresAt to Date.now(), handling
string/number Date formats) and return null when expired so callers like
isCodexSubscriber(), useApiKeyVerification(), and getAnthropicClient() stop
using stale bearer tokens and trigger refreshCodexToken(); also apply the same
expiry validation at the other occurrences referenced (around the other
get/usage sites) to ensure refreshCodexToken() is used when tokens are expired.
In `@src/utils/config.ts`:
- Around line 231-241: Update the auth-loss guard in wouldLoseAuthState() to
also detect loss of Codex OAuth tokens by comparing cached
GlobalConfig.codexOAuth with the freshly-read config; specifically, in
wouldLoseAuthState() (which currently checks oauthAccount and
hasCompletedOnboarding) add a lostCodexOauth boolean that becomes true when
globalConfigCache.config.codexOAuth is defined but the incoming fresh.codexOAuth
is undefined, and include this in the final return so
saveGlobalConfig()/saveCurrentProjectConfig() won't overwrite the cache if codex
OAuth creds would be lost.
In `@src/utils/logoV2Utils.ts`:
- Around line 259-263: The billingType selection uses auth-presence checks
(isClaudeAISubscriber(), isCodexSubscriber()) which can mask the actual selected
provider; change billingType to derive its value from the active/selected
provider or model instead of auth flags—replace the ternary using
isClaudeAISubscriber() and isCodexSubscriber() with a lookup based on the
current selected provider/model (e.g., call the function that returns the active
provider or model for the session) and map that provider identifier to 'Claude
Billing' / 'Codex API Billing' / 'API Usage Billing' accordingly so billing text
reflects the chosen backend rather than whichever auth check runs first.
In `@src/utils/model/model.ts`:
- Around line 181-183: The early return that forces 'gpt-5.2-codex' when
isCodexSubscriber() is true must be gated by first-party routing so it doesn't
override third-party provider defaults; update the logic around
isCodexSubscriber() to only return 'gpt-5.2-codex' after verifying the app is
using first-party routing (i.e., no Bedrock/Vertex/Foundry/other 3P provider
envs are set) or move this check to run after the provider-aware default
selection; ensure the condition references isCodexSubscriber() and the
provider-detection checks so a stored Codex credential does not override a
configured third-party provider.
In `@src/utils/model/validateModel.ts`:
- Around line 44-49: The Codex fast path is using the raw trimmed model string
so models with suffixes like "[1m]" or "[2m]" (e.g., "gpt-5.2-codex[1m]") miss
the bypass; before calling isCodexModel(normalizedModel) (and before storing in
validModelCache) strip any trailing "[1m]" or "[2m]" style suffix from
normalizedModel (same normalization logic as in src/utils/model/model.ts) and
use that stripped value for the isCodexModel check and cache key while keeping
the original trimming behavior elsewhere.
---
Outside diff comments:
In `@src/services/api/client.ts`:
- Around line 138-140: The code may leak Anthropic credentials when building the
Codex/OpenAI client because defaultHeaders (populated via configureApiKeyHeaders
and ANTHROPIC_CUSTOM_HEADERS) can contain Authorization or Anthropic-specific
headers; before constructing the Codex client/path guarded by
isClaudeAISubscriber(), remove any Anthropic auth headers (e.g., Authorization
if it came from Anthropic context and any ANTHROPIC_* custom headers) or create
a fresh headers object for the Codex/OpenAI path; update the logic around
isClaudeAISubscriber(), configureApiKeyHeaders(), and the Codex client
construction to sanitize defaultHeaders (strip "Authorization" when Anthropic
context or remove keys with "ANTHROPIC_" prefix) so only appropriate
OpenAI/Codex auth is sent (apply same fix to the other occurrence noted around
lines 303-315).
---
Nitpick comments:
In @.gitignore:
- Line 5: The .gitignore entry "openclaw/" is unanchored and will ignore any
directory named openclaw anywhere; change it to "/openclaw/" to anchor it to the
repository root so only the top-level gateway directory is excluded. Update the
.gitignore line from openclaw/ to /openclaw/ to restrict the ignore to the
repo-root folder.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 71ac345e-bedb-4968-9b43-cea1fb437530
📒 Files selected for processing (18)
.gitignoreCLAUDE.mdchanges.mdsrc/cli/handlers/auth.tssrc/components/ConsoleOAuthFlow.tsxsrc/constants/codex-oauth.tssrc/entrypoints/cli.tsxsrc/hooks/useApiKeyVerification.tssrc/services/api/client.tssrc/services/api/codex-fetch-adapter.tssrc/services/oauth/codex-client.tssrc/utils/auth.tssrc/utils/config.tssrc/utils/logoV2Utils.tssrc/utils/model/model.tssrc/utils/model/validateModel.tssrc/utils/ripgrep.tssrc/vendor/ripgrep/x64-linux/rg
| } else { | ||
| // Third-party provider (e.g. OpenAI Codex) — tokens carry no Anthropic | ||
| // scopes. Skip Anthropic API key creation entirely and store the tokens | ||
| // in their own dedicated config slot. | ||
| saveCodexOAuthTokens({ | ||
| accessToken: tokens.accessToken, | ||
| refreshToken: tokens.refreshToken ?? '', | ||
| expiresAt: tokens.expiresAt ?? Date.now() + 3600_000, | ||
| accountId: (tokens.tokenAccount?.uuid ?? ''), | ||
| }) |
There was a problem hiding this comment.
Expose Codex sessions through authStatus().
This branch persists a successful Codex login, but authStatus() below still computes loggedIn and authMethod only from Anthropic token/API-key sources. A Codex-only user will immediately get “Not logged in” and exit code 1 after a successful login.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/cli/handlers/auth.ts` around lines 118 - 127, authStatus() currently
derives loggedIn and authMethod only from Anthropic API key/token sources, so a
user who only completed Codex OAuth (persisted by saveCodexOAuthTokens) is
treated as "Not logged in"; update authStatus() to detect the presence of Codex
session tokens (the same config slot written by saveCodexOAuthTokens) and set
loggedIn = true and authMethod = "codex" when no Anthropic credentials exist but
Codex tokens are present, ensuring all downstream checks use this updated
status.
| // Codex-specific OAuth flow — completely separate from the Anthropic OAuthService | ||
| const startCodexOAuth = useCallback(async () => { | ||
| try { | ||
| logEvent('tengu_oauth_codex_flow_start', {}); | ||
| const codexTokens = await runCodexOAuthFlow(async (url) => { | ||
| setOAuthStatus({ state: 'waiting_for_login', url }); | ||
| setTimeout(setShowPastePrompt, 3000, true); | ||
| }); |
There was a problem hiding this comment.
Wire the paste prompt into the Codex flow.
runCodexOAuthFlow() has an onManualInput path, but this branch never supplies it. The prompt shown in waiting_for_login still uses handleSubmitCode() at Line 157, which feeds pasted input into the Anthropic oauthService, so Codex login can't recover when the localhost callback or browser-open step fails.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ConsoleOAuthFlow.tsx` around lines 267 - 274, The Codex OAuth
flow never wires the paste prompt to Codex-specific handling; update
startCodexOAuth to pass an onManualInput callback into runCodexOAuthFlow
(instead of leaving it out) that accepts the pasted code, calls the Codex
token-exchange/verification logic (not handleSubmitCode which targets the
Anthropic oauthService), updates setOAuthStatus appropriately, and closes the
paste prompt via setShowPastePrompt; ensure the same onManualInput is used by
the UI shown in the 'waiting_for_login' state so pasted codes trigger the Codex
flow rather than the Anthropic handler.
| // Save directly via saveCodexOAuthTokens (bypasses installOAuthTokens Anthropic path) | ||
| saveCodexOAuthTokens(codexTokens); | ||
| logEvent('tengu_oauth_codex_success', {}); | ||
| setOAuthStatus({ state: 'success' }); | ||
| void sendNotification({ message: 'Codex login successful', notificationType: 'auth_success' }, terminal); |
There was a problem hiding this comment.
Don't bypass the shared post-login cleanup.
Saving Codex tokens directly here skips the logout/cache/account bootstrap performed by installOAuthTokens() in src/cli/handlers/auth.ts:61-131. Switching from Claude to Codex can leave stale Anthropic account info and caches live, so the success UI and later auth checks can reflect the previous session.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/ConsoleOAuthFlow.tsx` around lines 275 - 279, The current code
calls saveCodexOAuthTokens(codexTokens) directly which bypasses the shared
post-login cleanup in installOAuthTokens; change this to invoke the shared
installer/cleanup flow (call installOAuthTokens with the Codex tokens or call
installOAuthTokens after saving) so the logout/cache/account bootstrap in
installOAuthTokens (the logic around installOAuthTokens in
src/cli/handlers/auth.ts) runs and clears any stale Anthropic state; import and
use installOAuthTokens instead of (or immediately after) saveCodexOAuthTokens
while keeping the existing logEvent('tengu_oauth_codex_success', {}),
setOAuthStatus({ state: 'success' }), and sendNotification calls.
| const JWT_CLAIM_PATH = 'https://api.openai.com/auth' | ||
|
|
||
| function extractAccountId(token: string): string { | ||
| try { | ||
| const parts = token.split('.') | ||
| if (parts.length !== 3) throw new Error('Invalid token') | ||
| const payload = JSON.parse(atob(parts[1])) | ||
| const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id | ||
| if (!accountId) throw new Error('No account ID in token') | ||
| return accountId | ||
| } catch { | ||
| throw new Error('Failed to extract account ID from Codex token') | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does JavaScript atob() reliably decode JWT payload segments, or do JWT segments need base64url-to-base64 conversion and padding first?
💡 Result:
No, JavaScript's atob does not reliably decode JWT payload segments directly. JWT segments (header and payload) use Base64url encoding per RFC 7515 and RFC 7519, which differs from standard Base64 used by atob: Base64url replaces + with -, / with , and omits = padding. Direct use of atob on unpadded or character-variant strings throws "InvalidCharacterError" or produces incorrect results, especially with UTF-8 payloads. To decode reliably: 1. Extract the segment: token.split('.')[1] 2. Convert Base64url to Base64: replace(/-/g, '+').replace(/_/g, '/') 3. Add padding: while (s.length % 4) s += '='; 4. Decode: atob(s) 5. Parse: JSON.parse(result) (with UTF-8 handling if needed: decodeURIComponent(escape(atob(s)))) Example function: function decodeJwtSegment(segment) { segment = segment.replace(/-/g, '+').replace(//g, '/'); while (segment.length % 4) segment += '='; return JSON.parse(atob(segment)); } This is confirmed across Stack Overflow (highest-voted answers), Auth0 jwt-decode library docs, and tutorials. Direct atob works only if no -/_ chars or padding needed, which is unreliable.
Citations:
- 1: https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-without-using-a-library
- 2: https://www.rfc-editor.org/rfc/rfc7519
- 3: https://www.rfc-editor.org/rfc/rfc7515
- 4: https://www.edureka.co/community/212508/how-to-decode-jwt-token-in-javascript-without-using-library
- 5: https://github.com/auth0/jwt-decode
- 6: https://zenn.dev/ai_worklab/articles/jwt-decoder-base64url-padding?locale=en
- 7: https://devkit.escalixstudio.com/blog/decode-jwt-token-javascript
- 8: https://blog.jesselangford.dev/post/decrypting-jwts-without-a-library
🏁 Script executed:
# Check the file to understand context
head -20 src/services/api/codex-fetch-adapter.ts
# Check imports to see if Buffer is available
grep -n "import\|require\|Buffer" src/services/api/codex-fetch-adapter.ts | head -20
# Check if there are existing JWT decoding utilities
fd -t f -e ts -e js | xargs grep -l "atob\|Buffer.from\|base64url\|jwt" | grep -v node_modules | head -10Repository: paoloanzn/free-code
Length of output: 1109
🏁 Script executed:
# Read the file around the extractAccountId function
sed -n '40,80p' src/services/api/codex-fetch-adapter.ts
# Check if Buffer is imported in this file
grep -n "Buffer\|import.*base64\|import.*jwt" src/services/api/codex-fetch-adapter.ts | head -20
# Check what platform/environment this code targets (package.json, tsconfig)
if [ -f package.json ]; then echo "=== package.json type ==="; grep -A 2 '"type"' package.json; fi
if [ -f tsconfig.json ]; then echo "=== tsconfig.json target ==="; grep -A 2 '"target"' tsconfig.json; fiRepository: paoloanzn/free-code
Length of output: 1330
🏁 Script executed:
# Search the entire file for Buffer usage and Node.js APIs
rg "Buffer|crypto|fs\.|import.*from.*node" src/services/api/codex-fetch-adapter.ts
# Check what directory this is in to understand the architecture
ls -la src/services/api/
# Check if there are any utility functions for decoding/encoding
rg "decode|encode|base64" src/utils/ | head -20Repository: paoloanzn/free-code
Length of output: 50376
Decode JWT payload segments as base64url, not standard base64.
JWT segments (header and payload) use RFC 7515/7519 base64url encoding, which replaces + with -, / with _, and omits = padding. Direct atob(parts[1]) will throw InvalidCharacterError or produce incorrect results when the payload contains these characters or when padding is missing. This breaks token parsing for valid Codex tokens before the first request.
🩹 Fix
function extractAccountId(token: string): string {
try {
const parts = token.split('.')
if (parts.length !== 3) throw new Error('Invalid token')
- const payload = JSON.parse(atob(parts[1]))
+ const base64 = parts[1]
+ .replace(/-/g, '+')
+ .replace(/_/g, '/')
+ .padEnd(Math.ceil(parts[1].length / 4) * 4, '=')
+ const payload = JSON.parse(atob(base64))
const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id
if (!accountId) throw new Error('No account ID in token')
return accountId🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api/codex-fetch-adapter.ts` around lines 48 - 60, The
extractAccountId function is decoding the JWT payload with atob which expects
standard base64; update it to decode base64url instead (handle '-' -> '+', '_'
-> '/', add '=' padding to length%4) before JSON.parse; implement a small helper
(e.g., base64UrlDecode) and use it in extractAccountId (which references
JWT_CLAIM_PATH) so the payload is correctly decoded for RFC 7515/7519
base64url-encoded JWT segments.
| const codexBody: Record<string, unknown> = { | ||
| model: codexModel, | ||
| store: false, | ||
| stream: true, | ||
| instructions, | ||
| input, | ||
| tool_choice: 'auto', | ||
| parallel_tool_calls: true, | ||
| } |
There was a problem hiding this comment.
Preserve the original fetch contract instead of forcing SSE.
The adapter only reads init.body, hard-codes stream: true, and always returns translateCodexStreamToAnthropic(...). If the SDK passes a Request object or makes a non-streaming messages.create() call, the body can be lost, JSON callers receive SSE, and the original AbortSignal never reaches the Codex request.
Implementation outline
+ const request = input instanceof Request ? input : undefined
let anthropicBody: Record<string, unknown>
try {
- const bodyText =
- init?.body instanceof ReadableStream
- ? await new Response(init.body).text()
- : typeof init?.body === 'string'
- ? init.body
+ const upstreamBody = init?.body ?? request?.body
+ const bodyText =
+ upstreamBody instanceof ReadableStream
+ ? await new Response(upstreamBody).text()
+ : typeof upstreamBody === 'string'
+ ? upstreamBody
: '{}'
@@
- stream: true,
+ stream: anthropicBody.stream === true,
@@
+ signal: init?.signal ?? request?.signal,You'll also need a non-stream translation path when anthropicBody.stream !== true; otherwise JSON callers still get an SSE body.
Also applies to: 702-718, 731-760
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api/codex-fetch-adapter.ts` around lines 221 - 229, The adapter
currently ignores the original fetch contract by always forcing stream: true,
reading only init.body, and returning translateCodexStreamToAnthropic(...);
change it to respect the incoming Request/init: detect if input is a Request and
extract/clone its body and AbortSignal, only set codexBody.stream = true when
the incoming anthropicBody.stream === true (otherwise preserve false), forward
the original AbortSignal into the fetch to Codex, and implement a non-stream
translation path that returns the raw JSON response when stream is not enabled;
keep using translateCodexStreamToAnthropic(...) only when anthropicBody.stream
=== true and ensure Request/Response body handling is compatible with both
streaming and non-streaming callers (refer to codexBody, init.body,
translateCodexStreamToAnthropic, and anthropicBody.stream).
Don't overwrite caller-provided tool selection behavior.
This hard-codes tool_choice: 'auto' and parallel_tool_calls: true, so any upstream request that forces a specific tool or disables parallel tool use loses its semantics when routed through Codex.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/api/codex-fetch-adapter.ts` around lines 221 - 229, The
codexBody construction is overwriting caller-specified tool selection and
parallelization by hardcoding tool_choice: 'auto' and parallel_tool_calls: true;
update the logic that builds codexBody (the codexBody object in
codex-fetch-adapter.ts) to only set tool_choice and parallel_tool_calls when
those keys are absent on the incoming request (preserve incoming values if
present), e.g., merge or shallow-copy the caller-provided options into codexBody
before defaulting, so callers’ explicit tool selection and parallel_tool_calls
settings are not lost.
| export function getCodexOAuthTokens(): CodexTokens | null { | ||
| const cfg = getGlobalConfig() | ||
| const stored = cfg.codexOAuth | ||
| if ( | ||
| !stored?.accessToken || | ||
| !stored.refreshToken || | ||
| !stored.expiresAt || | ||
| !stored.accountId | ||
| ) { | ||
| return null | ||
| } | ||
| return { | ||
| accessToken: stored.accessToken, | ||
| refreshToken: stored.refreshToken, | ||
| expiresAt: stored.expiresAt, | ||
| accountId: stored.accountId, | ||
| } | ||
| } |
There was a problem hiding this comment.
Don't treat expired Codex tokens as valid.
getCodexOAuthTokens() only checks that fields exist. Once expiresAt has passed, isCodexSubscriber(), useApiKeyVerification(), and getAnthropicClient() still consider the session authenticated and keep sending the stale bearer token; the new refreshCodexToken() helper is never used.
Also applies to: 1633-1635
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/auth.ts` around lines 1340 - 1357, getCodexOAuthTokens() currently
only checks presence of fields and returns expired tokens as valid; update it to
validate expiresAt is in the future (e.g., compare stored.expiresAt to
Date.now(), handling string/number Date formats) and return null when expired so
callers like isCodexSubscriber(), useApiKeyVerification(), and
getAnthropicClient() stop using stale bearer tokens and trigger
refreshCodexToken(); also apply the same expiry validation at the other
occurrences referenced (around the other get/usage sites) to ensure
refreshCodexToken() is used when tokens are expired.
| /** | ||
| * OpenAI Codex OAuth tokens, stored separately from Anthropic credentials. | ||
| * These are acquired via the Codex OAuth flow (auth.openai.com) and are used | ||
| * as Bearer tokens against OpenAI's API — they are never sent to Anthropic servers. | ||
| */ | ||
| codexOAuth?: { | ||
| accessToken: string | ||
| refreshToken: string | ||
| expiresAt: number | ||
| accountId: string | ||
| } |
There was a problem hiding this comment.
Extend the auth-loss guard to cover codexOAuth.
The new Codex tokens live in GlobalConfig.codexOAuth, but wouldLoseAuthState() still only protects oauthAccount and onboarding state. In the same corrupted re-read case GH #3117 is guarding against, saveGlobalConfig() and saveCurrentProjectConfig() can still overwrite the cached config with one that has silently dropped Codex credentials.
Minimal follow-up in `wouldLoseAuthState()`
function wouldLoseAuthState(fresh: {
oauthAccount?: unknown
codexOAuth?: unknown
hasCompletedOnboarding?: boolean
}): boolean {
const cached = globalConfigCache.config
if (!cached) return false
const lostOauth =
cached.oauthAccount !== undefined && fresh.oauthAccount === undefined
const lostCodexOauth =
cached.codexOAuth !== undefined && fresh.codexOAuth === undefined
const lostOnboarding =
cached.hasCompletedOnboarding === true &&
fresh.hasCompletedOnboarding !== true
return lostOauth || lostCodexOauth || lostOnboarding
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/config.ts` around lines 231 - 241, Update the auth-loss guard in
wouldLoseAuthState() to also detect loss of Codex OAuth tokens by comparing
cached GlobalConfig.codexOAuth with the freshly-read config; specifically, in
wouldLoseAuthState() (which currently checks oauthAccount and
hasCompletedOnboarding) add a lostCodexOauth boolean that becomes true when
globalConfigCache.config.codexOAuth is defined but the incoming fresh.codexOAuth
is undefined, and include this in the final return so
saveGlobalConfig()/saveCurrentProjectConfig() won't overwrite the cache if codex
OAuth creds would be lost.
| const billingType = isClaudeAISubscriber() | ||
| ? getSubscriptionName() | ||
| : 'API Usage Billing' | ||
| : isCodexSubscriber() | ||
| ? 'Codex API Billing' | ||
| : 'API Usage Billing' |
There was a problem hiding this comment.
Choose billing text from the active backend, not whichever auth check happens first.
isClaudeAISubscriber() masks Codex whenever both credential sets are stored, so a Codex session can still render Claude billing text. With oauthAccount and codexOAuth now persisted independently, this should key off the selected provider/model instead of auth presence alone.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/logoV2Utils.ts` around lines 259 - 263, The billingType selection
uses auth-presence checks (isClaudeAISubscriber(), isCodexSubscriber()) which
can mask the actual selected provider; change billingType to derive its value
from the active/selected provider or model instead of auth flags—replace the
ternary using isClaudeAISubscriber() and isCodexSubscriber() with a lookup based
on the current selected provider/model (e.g., call the function that returns the
active provider or model for the session) and map that provider identifier to
'Claude Billing' / 'Codex API Billing' / 'API Usage Billing' accordingly so
billing text reflects the chosen backend rather than whichever auth check runs
first.
| if (isCodexSubscriber()) { | ||
| return 'gpt-5.2-codex' | ||
| } |
There was a problem hiding this comment.
Keep the Codex default behind first-party routing.
This short-circuit runs before the 3P-provider-aware default logic. If a user has stored Codex credentials but launches with Bedrock/Vertex/Foundry env vars, the session default becomes gpt-5.2-codex and the selected provider gets an unsupported model id.
Possible fix
export function getDefaultMainLoopModelSetting(): ModelName | ModelAlias {
- if (isCodexSubscriber()) {
+ if (getAPIProvider() === 'firstParty' && isCodexSubscriber()) {
return 'gpt-5.2-codex'
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/model/model.ts` around lines 181 - 183, The early return that
forces 'gpt-5.2-codex' when isCodexSubscriber() is true must be gated by
first-party routing so it doesn't override third-party provider defaults; update
the logic around isCodexSubscriber() to only return 'gpt-5.2-codex' after
verifying the app is using first-party routing (i.e., no
Bedrock/Vertex/Foundry/other 3P provider envs are set) or move this check to run
after the provider-aware default selection; ensure the condition references
isCodexSubscriber() and the provider-detection checks so a stored Codex
credential does not override a configured third-party provider.
| // Check if it's a known Codex/OpenAI model (skip Anthropic API validation) | ||
| const { isCodexSubscriber } = await import('../auth.js') | ||
| const { isCodexModel } = await import('../../services/api/codex-fetch-adapter.js') | ||
| if (isCodexSubscriber() && isCodexModel(normalizedModel)) { | ||
| validModelCache.set(normalizedModel, true) | ||
| return { valid: true } |
There was a problem hiding this comment.
Normalize [1m] / [2m] suffixes before the Codex fast path.
src/utils/model/model.ts already strips those suffixes before API calls. This branch checks the raw trimmed string, so a valid value like gpt-5.2-codex[1m] misses the bypass and falls back to Anthropic validation.
🩹 Minimal diff
+ const { normalizeModelStringForAPI } = await import('./model.js')
+ const apiModel = normalizeModelStringForAPI(normalizedModel)
const { isCodexSubscriber } = await import('../auth.js')
const { isCodexModel } = await import('../../services/api/codex-fetch-adapter.js')
- if (isCodexSubscriber() && isCodexModel(normalizedModel)) {
+ if (isCodexSubscriber() && isCodexModel(apiModel)) {
validModelCache.set(normalizedModel, true)
return { valid: true }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/model/validateModel.ts` around lines 44 - 49, The Codex fast path
is using the raw trimmed model string so models with suffixes like "[1m]" or
"[2m]" (e.g., "gpt-5.2-codex[1m]") miss the bypass; before calling
isCodexModel(normalizedModel) (and before storing in validModelCache) strip any
trailing "[1m]" or "[2m]" style suffix from normalizedModel (same normalization
logic as in src/utils/model/model.ts) and use that stripped value for the
isCodexModel check and cache key while keeping the original trimming behavior
elsewhere.
- Add comprehensive docstrings to functions in codex-fetch-adapter.ts - Add documentation for OAuth crypto functions - Document profile fetching functions - Improve overall code documentation quality to meet coverage requirements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (4)
src/services/api/codex-fetch-adapter.ts (4)
66-77:⚠️ Potential issue | 🟠 MajorJWT base64url decoding issue persists.
This issue was raised in a previous review. The code still uses
atob(parts[1])directly, but JWT segments use base64url encoding (RFC 7515/7519), not standard base64. This will throwInvalidCharacterErroror produce incorrect results when the payload contains-,_, or requires padding.🩹 Recommended fix
function extractAccountId(token: string): string { try { const parts = token.split('.') if (parts.length !== 3) throw new Error('Invalid token') - const payload = JSON.parse(atob(parts[1])) + // Convert base64url to base64 and add padding + const base64 = parts[1] + .replace(/-/g, '+') + .replace(/_/g, '/') + .padEnd(Math.ceil(parts[1].length / 4) * 4, '=') + const payload = JSON.parse(atob(base64)) const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id if (!accountId) throw new Error('No account ID in token') return accountId } catch { throw new Error('Failed to extract account ID from Codex token') } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 66 - 77, The extractAccountId function incorrectly uses atob on a JWT segment (base64url) which can fail; update extractAccountId to decode the JWT payload using base64url decoding instead of atob (either implement a small helper that converts '-'->'+', '_'->'/', adds '=' padding then uses atob/Buffer, or use Buffer.from(segment, 'base64url') where available), then JSON.parse the decoded payload and continue to read payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id as before; ensure errors are still caught and rethrown with the existing message.
253-261:⚠️ Potential issue | 🟠 MajorHardcoded options override caller's intent.
These issues were raised in previous reviews:
stream: trueis hardcoded, forcing SSE even when the SDK makes a non-streaming call.tool_choice: 'auto'andparallel_tool_calls: trueoverwrite any explicit values from the caller.🩹 Recommended fix
const codexBody: Record<string, unknown> = { model: codexModel, store: false, - stream: true, + stream: anthropicBody.stream !== false, instructions, input, - tool_choice: 'auto', - parallel_tool_calls: true, + tool_choice: anthropicBody.tool_choice ?? 'auto', + parallel_tool_calls: anthropicBody.parallel_tool_calls ?? true, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 253 - 261, codexBody currently hardcodes stream: true, tool_choice: 'auto', and parallel_tool_calls: true which override caller-supplied options; change codexBody construction to merge caller-provided options (e.g., an options or params argument) into the body and only apply these as defaults when the caller did not supply them—ensure you reference the existing codexBody object and fields (model/codexModel, instructions, input, stream, tool_choice, parallel_tool_calls) and implement a shallow merge so that caller values take precedence over defaults rather than being overwritten.
746-792:⚠️ Potential issue | 🟠 MajorStale
accountIdand missingAbortSignalforwarding.Two issues persist from previous reviews:
Stale accountId (lines 749, 787):
accountIdis extracted once from the constructor'saccessToken, but the actual request usescurrentToken(which may be refreshed). After token refresh, the header will send a mismatched account ID.AbortSignal not forwarded (line 781): The
init?.signalfrom the caller is not passed to the Codex fetch, so abort requests won't cancel the upstream call.🩹 Recommended fix
export function createCodexFetch( accessToken: string, ): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> { - const accountId = extractAccountId(accessToken) return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => { const url = input instanceof Request ? input.url : String(input) @@ // Get current token (may have been refreshed) const tokens = getCodexOAuthTokens() const currentToken = tokens?.accessToken || accessToken + const accountId = extractAccountId(currentToken) @@ // Call Codex API const codexResponse = await globalThis.fetch(CODEX_BASE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream', Authorization: `Bearer ${currentToken}`, 'chatgpt-account-id': accountId, originator: 'pi', 'OpenAI-Beta': 'responses=experimental', }, body: JSON.stringify(codexBody), + signal: init?.signal, })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 746 - 792, The code in createCodexFetch extracts accountId once from the initial accessToken and never updates it when tokens are refreshed, and it also fails to forward the caller's AbortSignal; fix by re-extracting the account id from the up-to-date token right before calling Codex (call extractAccountId(currentToken) and use that for the 'chatgpt-account-id' header instead of the statically captured accountId) and include the caller's signal (init?.signal) in the fetch options (e.g., signal: init?.signal) so upstream requests can be aborted; update the fetch call that posts to CODEX_BASE_URL to use these corrected values (currentToken, new accountId, and forwarded signal).
759-771:⚠️ Potential issue | 🟡 MinorRequest object body not handled.
If the SDK passes a
Requestobject (where body is ininput.body), this code only readsinit?.bodyand would default to{}. Consider extracting body from either source:🩹 Handle Request body
// Parse the Anthropic request body let anthropicBody: Record<string, unknown> try { + const request = input instanceof Request ? input : undefined + const upstreamBody = init?.body ?? request?.body const bodyText = - init?.body instanceof ReadableStream - ? await new Response(init.body).text() - : typeof init?.body === 'string' - ? init.body + upstreamBody instanceof ReadableStream + ? await new Response(upstreamBody).text() + : typeof upstreamBody === 'string' + ? upstreamBody : '{}' anthropicBody = JSON.parse(bodyText)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 759 - 771, The parsing currently only inspects init?.body so if the SDK passed a Request-like object whose body is under input.body you end up with {} — update the anthropicBody extraction to check both locations: if init is a Request or has an input property, read the body from init.input?.body (or init.clone()/input clone for Request) before falling back to init?.body or '{}'; ensure you still handle ReadableStream vs string and preserve the try/catch around JSON.parse so anthropicBody is set correctly.
🧹 Nitpick comments (2)
src/services/api/codex-fetch-adapter.ts (2)
632-648: Consider removing unused parameters.The
_toolCallId,_toolCallName, and_toolCallArgsparameters are unused. If they're not needed for thecontent_block_stopevent, consider simplifying the function signature.♻️ Simplified signature
function closeToolCallBlock( controller: ReadableStreamDefaultController, encoder: TextEncoder, index: number, - _toolCallId: string, - _toolCallName: string, - _toolCallArgs: string, ) {Update call sites accordingly (lines 401, 534, 625).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 632 - 648, The function closeToolCallBlock currently accepts unused parameters _toolCallId, _toolCallName, and _toolCallArgs; remove these three parameters from the closeToolCallBlock signature and its internal parameter list so it becomes closeToolCallBlock(controller: ReadableStreamDefaultController, encoder: TextEncoder, index: number), and update all call sites that invoke closeToolCallBlock in this module to call it with only (controller, encoder, index). Ensure no other code relies on the removed parameters and run tests/type-checking to confirm no remaining references to _toolCallId/_toolCallName/_toolCallArgs exist.
144-166: Tool call ID correlation may break round-trip tool use.The
call_idforfunction_call_output(line 148) usesblock.tool_use_idfrom the Anthropic request, but the correspondingfunction_call(line 200) usesblock.id. These are the same identifiers in Anthropic's format, so this should work. However, iftool_use_idis missing and the fallback counter is used, the generatedcall_idwon't match any priorfunction_call, causing Codex to reject the output.Consider adding a mapping from Anthropic
tool_use_idto generatedcall_idto ensure correlation when IDs are missing:♻️ Suggested approach
function translateMessages( anthropicMessages: AnthropicMessage[], ): Array<Record<string, unknown>> { const codexInput: Array<Record<string, unknown>> = [] - let toolCallCounter = 0 + // Map Anthropic tool_use IDs to generated call_ids for correlation + const toolIdMap = new Map<string, string>() + let toolCallCounter = 0 + + function getOrCreateCallId(anthropicId: string | undefined): string { + if (anthropicId && toolIdMap.has(anthropicId)) { + return toolIdMap.get(anthropicId)! + } + const callId = anthropicId || `call_${toolCallCounter++}` + if (anthropicId) { + toolIdMap.set(anthropicId, callId) + } + return callId + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/api/codex-fetch-adapter.ts` around lines 144 - 166, The tool-result call_id generation can diverge when block.tool_use_id is missing (you currently fallback to `call_${toolCallCounter++}` when building `function_call_output`), breaking correlation with the later `function_call` that uses `block.id`; to fix, introduce a mapping (e.g., toolCallMap) keyed by the Anthropic `block.id` that stores the chosen callId (use `block.tool_use_id` when present else generate `call_${toolCallCounter++}`), set it when creating entries pushed to `codexInput` inside the function that handles tool results, and then consult that same mapping when creating the corresponding `function_call` entries so both sides use the identical call_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/services/oauth/codex-client.ts`:
- Around line 293-300: The current startCodexCallbackServer() swallows bind
errors by resolving a null-only stub, making runCodexOAuthFlow() unable to
distinguish “listener failed to start” from “no code provided”; change
startCodexCallbackServer() so that its 'error' handler rejects the Promise (or
resolves with an explicit error state object) instead of silently returning a
null-only server; propagate that failure through runCodexOAuthFlow() (inspect
the returned value or catch the rejection) and ensure callers like
ConsoleOAuthFlow.tsx handle this case (e.g., trigger the manual input UI or show
a clear "callback server failed to start" message) so the UX doesn't just show
"No authorization code received." Reference symbols: startCodexCallbackServer,
runCodexOAuthFlow, ConsoleOAuthFlow (component handling onManualInput).
- Around line 334-349: The current logic races onManualInput() incorrectly
because it starts immediately and any later rejection is unhandled; change it so
either (A) defer calling onManualInput() until after await
callbackServer.waitForCode() returns no code (i.e., only invoke onManualInput()
when callbackResult?.code is falsy and then call parseCodexCallbackInput), or
(B) make onManualInput accept a cancellable signal (e.g., an AbortSignal or a
cancel function) and call that cancel when callbackServer.waitForCode() resolves
with a code (use callbackServer.cancelWait() or similar), and ensure you
handle/reject the manual promise properly; modify the callers and
parseCodexCallbackInput usage accordingly to avoid dropped rejections.
- Around line 393-395: The query-string branch that handles
value.includes('code=') currently returns the code without validating state;
update that branch to mirror the full-URL branch by reading params.get('state')
and verifying it against the same expected state value/mechanism used in the
callback path (the same check used elsewhere in this module), and only return {
code: ... } when the state matches (otherwise return undefined or an error).
Locate the block using URLSearchParams in the code-path handling
value.includes('code=') and add the params.get('state') check before returning
the code.
- Around line 148-156: The token parsing currently rejects responses missing
refresh_token; update the parsing and types so refresh is optional and refresh
flows reuse the existing refresh token: change TokenSuccessResult (or equivalent
return shape from postToTokenUrl / parseCodexTokenResponse) so refresh?: string,
remove the hard check that requires json.refresh_token in the block that
currently throws when typeof json.expires_in !== 'number' (the block around the
return with access/refresh/expires), and in refreshCodexToken() (and the similar
handling around lines ~198-216) set the resulting refresh value to
json.refresh_token || existingRefreshToken (i.e., fall back to the stored
refresh token when the response omits it) while keeping access and expires
handling unchanged.
---
Duplicate comments:
In `@src/services/api/codex-fetch-adapter.ts`:
- Around line 66-77: The extractAccountId function incorrectly uses atob on a
JWT segment (base64url) which can fail; update extractAccountId to decode the
JWT payload using base64url decoding instead of atob (either implement a small
helper that converts '-'->'+', '_'->'/', adds '=' padding then uses atob/Buffer,
or use Buffer.from(segment, 'base64url') where available), then JSON.parse the
decoded payload and continue to read
payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id as before; ensure errors are still
caught and rethrown with the existing message.
- Around line 253-261: codexBody currently hardcodes stream: true, tool_choice:
'auto', and parallel_tool_calls: true which override caller-supplied options;
change codexBody construction to merge caller-provided options (e.g., an options
or params argument) into the body and only apply these as defaults when the
caller did not supply them—ensure you reference the existing codexBody object
and fields (model/codexModel, instructions, input, stream, tool_choice,
parallel_tool_calls) and implement a shallow merge so that caller values take
precedence over defaults rather than being overwritten.
- Around line 746-792: The code in createCodexFetch extracts accountId once from
the initial accessToken and never updates it when tokens are refreshed, and it
also fails to forward the caller's AbortSignal; fix by re-extracting the account
id from the up-to-date token right before calling Codex (call
extractAccountId(currentToken) and use that for the 'chatgpt-account-id' header
instead of the statically captured accountId) and include the caller's signal
(init?.signal) in the fetch options (e.g., signal: init?.signal) so upstream
requests can be aborted; update the fetch call that posts to CODEX_BASE_URL to
use these corrected values (currentToken, new accountId, and forwarded signal).
- Around line 759-771: The parsing currently only inspects init?.body so if the
SDK passed a Request-like object whose body is under input.body you end up with
{} — update the anthropicBody extraction to check both locations: if init is a
Request or has an input property, read the body from init.input?.body (or
init.clone()/input clone for Request) before falling back to init?.body or '{}';
ensure you still handle ReadableStream vs string and preserve the try/catch
around JSON.parse so anthropicBody is set correctly.
---
Nitpick comments:
In `@src/services/api/codex-fetch-adapter.ts`:
- Around line 632-648: The function closeToolCallBlock currently accepts unused
parameters _toolCallId, _toolCallName, and _toolCallArgs; remove these three
parameters from the closeToolCallBlock signature and its internal parameter list
so it becomes closeToolCallBlock(controller: ReadableStreamDefaultController,
encoder: TextEncoder, index: number), and update all call sites that invoke
closeToolCallBlock in this module to call it with only (controller, encoder,
index). Ensure no other code relies on the removed parameters and run
tests/type-checking to confirm no remaining references to
_toolCallId/_toolCallName/_toolCallArgs exist.
- Around line 144-166: The tool-result call_id generation can diverge when
block.tool_use_id is missing (you currently fallback to
`call_${toolCallCounter++}` when building `function_call_output`), breaking
correlation with the later `function_call` that uses `block.id`; to fix,
introduce a mapping (e.g., toolCallMap) keyed by the Anthropic `block.id` that
stores the chosen callId (use `block.tool_use_id` when present else generate
`call_${toolCallCounter++}`), set it when creating entries pushed to
`codexInput` inside the function that handles tool results, and then consult
that same mapping when creating the corresponding `function_call` entries so
both sides use the identical call_id.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4554b221-5c86-41f9-9bae-e65f0d2407ab
📒 Files selected for processing (4)
src/services/api/codex-fetch-adapter.tssrc/services/oauth/codex-client.tssrc/services/oauth/crypto.tssrc/services/oauth/getOauthProfile.ts
✅ Files skipped from review due to trivial changes (2)
- src/services/oauth/getOauthProfile.ts
- src/services/oauth/crypto.ts
| if (!json.access_token || !json.refresh_token || typeof json.expires_in !== 'number') { | ||
| logError(new Error('[codex-oauth] token response missing required fields')) | ||
| return { type: 'failed' } | ||
| } | ||
| return { | ||
| type: 'success', | ||
| access: json.access_token, | ||
| refresh: json.refresh_token, | ||
| expires: Date.now() + json.expires_in * 1000, |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Does OAuth 2.0 allow a refresh-token response to omit refresh_token? If OpenAI documents its OAuth refresh response, does it guarantee returning a rotated refresh token on every refresh?
💡 Result:
Yes, OAuth 2.0 allows a refresh-token response to omit the refresh_token parameter. No, OpenAI does not document its own OAuth refresh response or guarantee returning a rotated refresh token on every refresh; their OAuth documentation is for Custom GPT Actions where developers implement their own OAuth servers, and those servers may optionally return a new refresh_token.
Citations:
- 1: https://datatracker.ietf.org/doc/html/rfc6749
- 2: https://rfc-editor.org/rfc/rfc6749.html
- 3: https://rfcinfo.com/rfc-6749/refreshAccessToken
- 4: https://rfcinfo.com/rfc-6749/obtainAuthorization/
- 5: https://community.openai.com/t/guide-how-oauth-refresh-tokens-revocation-work-with-gpt-actions/533147
- 6: https://developers.openai.com/api/docs/actions/authentication/
- 7: https://platform.openai.com/docs/actions/authentication
🏁 Script executed:
cat -n src/services/oauth/codex-client.ts | sed -n '140,225p'Repository: paoloanzn/free-code
Length of output: 3052
🏁 Script executed:
cat -n src/services/oauth/codex-client.ts | sed -n '1,140p'Repository: paoloanzn/free-code
Length of output: 5685
Handle optional refresh_token in OAuth responses per RFC 6749.
OpenAI's token endpoint may omit refresh_token on refresh (per OAuth 2.0 spec), but postToTokenUrl() rejects any response lacking it. This forces unnecessary re-login instead of reusing the existing refresh token. Fix by making refresh optional in TokenSuccessResult and falling back to the existing token in refreshCodexToken().
Changes required
type TokenSuccessResult = {
type: 'success'
access: string
- refresh: string
+ refresh?: string
expires: number
}- if (!json.access_token || !json.refresh_token || typeof json.expires_in !== 'number') {
+ if (!json.access_token || typeof json.expires_in !== 'number') {
logError(new Error('[codex-oauth] token response missing required fields'))
return { type: 'failed' }
}
return {
type: 'success',
access: json.access_token,
- refresh: json.refresh_token,
+ refresh: typeof json.refresh_token === 'string' ? json.refresh_token : undefined,
expires: Date.now() + json.expires_in * 1000,
}- if (result.type !== 'success') {
+ if (result.type !== 'success' || !result.refresh) {
throw new Error('Codex token exchange failed. Please try again.')
} return {
accessToken: result.access,
- refreshToken: result.refresh,
+ refreshToken: result.refresh ?? refreshToken,
expiresAt: result.expires,
accountId,
}Also applies to: 198-216
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/oauth/codex-client.ts` around lines 148 - 156, The token parsing
currently rejects responses missing refresh_token; update the parsing and types
so refresh is optional and refresh flows reuse the existing refresh token:
change TokenSuccessResult (or equivalent return shape from postToTokenUrl /
parseCodexTokenResponse) so refresh?: string, remove the hard check that
requires json.refresh_token in the block that currently throws when typeof
json.expires_in !== 'number' (the block around the return with
access/refresh/expires), and in refreshCodexToken() (and the similar handling
around lines ~198-216) set the resulting refresh value to json.refresh_token ||
existingRefreshToken (i.e., fall back to the stored refresh token when the
response omits it) while keeping access and expires handling unchanged.
| }).on('error', () => { | ||
| // Port 1455 is busy — resolve with a server that always returns null | ||
| // so the user falls back to manual paste. | ||
| resolve({ | ||
| waitForCode: async () => null, | ||
| cancelWait: () => {}, | ||
| close: () => {}, | ||
| }) |
There was a problem hiding this comment.
Surface callback-server bind failures explicitly.
When port 1455 is busy, startCodexCallbackServer() resolves a null-only stub, so runCodexOAuthFlow() can't tell “listener never started” from “user never finished login.” With the current caller in src/components/ConsoleOAuthFlow.tsx:271-274 not passing onManualInput, this path always degrades into No authorization code received... after the browser already opened.
🔧 Suggested fix
type LocalServer = {
+ requiresManualInput: boolean
waitForCode: () => Promise<{ code: string } | null>
cancelWait: () => void
close: () => void
} const localServer: LocalServer = {
+ requiresManualInput: false,
waitForCode: () => waitPromise,
cancelWait: () => {
settleWait?.(null)
settleWait = null
},
close: doClose,
} resolve({
+ requiresManualInput: true,
waitForCode: async () => null,
cancelWait: () => {},
close: () => {},
}) try {
+ if (callbackServer.requiresManualInput && !onManualInput) {
+ throw new Error('Port 1455 is already in use. Free the port or provide manual callback input.')
+ }
await onUrlReady(url)
await openBrowser(url)Also applies to: 319-321, 323-357
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/oauth/codex-client.ts` around lines 293 - 300, The current
startCodexCallbackServer() swallows bind errors by resolving a null-only stub,
making runCodexOAuthFlow() unable to distinguish “listener failed to start” from
“no code provided”; change startCodexCallbackServer() so that its 'error'
handler rejects the Promise (or resolves with an explicit error state object)
instead of silently returning a null-only server; propagate that failure through
runCodexOAuthFlow() (inspect the returned value or catch the rejection) and
ensure callers like ConsoleOAuthFlow.tsx handle this case (e.g., trigger the
manual input UI or show a clear "callback server failed to start" message) so
the UX doesn't just show "No authorization code received." Reference symbols:
startCodexCallbackServer, runCodexOAuthFlow, ConsoleOAuthFlow (component
handling onManualInput).
| if (onManualInput) { | ||
| // Race: browser callback vs. manual paste | ||
| const manualPromise = onManualInput().then((input) => { | ||
| callbackServer.cancelWait() | ||
| return input | ||
| }) | ||
|
|
||
| const callbackResult = await callbackServer.waitForCode() | ||
| if (callbackResult?.code) { | ||
| code = callbackResult.code | ||
| } else { | ||
| // Callback didn't arrive — use manual input | ||
| const manualInput = await manualPromise | ||
| const parsed = parseCodexCallbackInput(manualInput, state) | ||
| code = parsed.code | ||
| } |
There was a problem hiding this comment.
If onManualInput is ever used, this branch is not a real race.
onManualInput() starts immediately, but the code still awaits waitForCode() first. That leaves the manual prompt running even when the callback succeeds, and any later rejection from onManualInput() is dropped on the floor.
🔧 Guidance
Either defer onManualInput() until the callback path is impossible, or change its signature so the prompt can be canceled when waitForCode() resolves first.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/oauth/codex-client.ts` around lines 334 - 349, The current logic
races onManualInput() incorrectly because it starts immediately and any later
rejection is unhandled; change it so either (A) defer calling onManualInput()
until after await callbackServer.waitForCode() returns no code (i.e., only
invoke onManualInput() when callbackResult?.code is falsy and then call
parseCodexCallbackInput), or (B) make onManualInput accept a cancellable signal
(e.g., an AbortSignal or a cancel function) and call that cancel when
callbackServer.waitForCode() resolves with a code (use
callbackServer.cancelWait() or similar), and ensure you handle/reject the manual
promise properly; modify the callers and parseCodexCallbackInput usage
accordingly to avoid dropped rejections.
| if (value.includes('code=')) { | ||
| const params = new URLSearchParams(value) | ||
| return { code: params.get('code') ?? undefined } |
There was a problem hiding this comment.
Preserve the state check in the query-string parser too.
The full-URL branch validates state, but the code=... branch accepts the pasted code without checking the accompanying state. If manual paste is wired later, that weakens the same CSRF guard the callback path relies on.
🔧 Suggested fix
if (value.includes('code=')) {
const params = new URLSearchParams(value)
+ const state = params.get('state')
+ if (state && state !== expectedState) {
+ throw new Error('State mismatch in pasted callback')
+ }
return { code: params.get('code') ?? undefined }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/services/oauth/codex-client.ts` around lines 393 - 395, The query-string
branch that handles value.includes('code=') currently returns the code without
validating state; update that branch to mirror the full-URL branch by reading
params.get('state') and verifying it against the same expected state
value/mechanism used in the callback path (the same check used elsewhere in this
module), and only return { code: ... } when the state matches (otherwise return
undefined or an error). Locate the block using URLSearchParams in the code-path
handling value.includes('code=') and add the params.get('state') check before
returning the code.
- Add OpenAI as new API provider with support for gpt-5.4, gpt-5.3-codex, gpt-5.4-mini models - Extend model configuration system to include OpenAI provider - Add isCodexSubscriber function for OpenAI authentication detection - Update model options and descriptions for Codex models - Create codex-fetch-adapter for OpenAI API integration - Add OpenAI OAuth configuration and authentication URLs - Update default model selection logic for Codex users 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
- Add openaiOauthTokens field to GlobalConfig for storing OAuth tokens - Update isCodexSubscriber to check both env vars and OAuth token state - Ensure UI components update automatically after OpenAI OAuth without restart - Fix model menu and header to reflect Codex subscription status dynamically 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The default model string was incorrectly set to gpt-5.2-codex while the description already advertised GPT-5.3 Codex. Also removed a duplicate dead-code isCodexSubscriber() block that was unreachable. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex API Support: Feature Parity & UI Overhaul
Codex API Support: Feature Parity & UI Overhaul
Codex API Support: Feature Parity & UI Overhaul
Summary
This pull request introduces full feature parity and explicit UI support for the OpenAI Codex backend (
chatgpt.com/backend-api/codex/responses). The codebase is now entirely backend-agnostic and smoothly transitions between Anthropic Claude and OpenAI Codex schemas based on current authentication, without losing features like reasoning animations, token billing, or multi-modal visual inputs.Key Changes
1. Codex API Gateway Adapter (
codex-fetch-adapter.ts)base64image schemas now map precisely to the Codex expectedinput_imagepayloads.msg.contentitems precisely intoinput_text, sidestepping OpenAI's strictv1/responsesvalidation rules (Invalid value: 'text').tool_resultitems into top-levelfunction_call_outputobjects to guarantee that local CLI tool executions (File Reads, Bash loops) cleanly feed back into Codex logic without throwing "No tool output found" errors.cache_controlannotations from tool bindings and prompts prior to transmission so the Codex API doesn't reject malformed JSON.2. Deep UI & Routing Integration
model.ts): UpdatedgetPublicModelDisplayNameandgetClaudeAiUserDefaultModelDescriptionto recognize Codex GPT strings. Models likegpt-5.1-codex-maxnow beautifully map toCodex 5.1 Maxin the CLI visual outputs instead of passing the raw proxy IDs.getDefaultMainLoopModelSettingaware ofisCodexSubscriber(), automatically defaulting togpt-5.2-codexinstead ofsonnet46.logoV2Utils.ts): RefactoredformatModelAndBillinglogic to renderCodex API Billingproudly inside the terminal header when authenticated.3. Reasoning & Metrics Support
codex-fetch-adapternow intentionally intercepts the proprietaryresponse.reasoning.deltaSSE frames emitted bycodex-maxmodels. It wraps them into Anthropic<thinking>events, ensuring the standard CLI "Thinking..." spinner continues to function flawlessly for OpenAI reasoning.response.completedcompletion events, fetchingusage.input_tokensandoutput_tokens. These are injected natively into the finalmessage_stoptoken handler, meaning Codex queries correctly trigger the terminal's Token/Price tracker summary logic.4. Git Housekeeping
.gitignoreto securely and durably exclude theopenclaw/gateway directory from staging commits.Summary by CodeRabbit
New Features
Documentation
Bug Fixes
Chores