Skip to content

Codex API Support: Feature Parity & UI Overhaul#2

Merged
paoloanzn merged 6 commits intopaoloanzn:mainfrom
bubblu2264326:main
Apr 1, 2026
Merged

Codex API Support: Feature Parity & UI Overhaul#2
paoloanzn merged 6 commits intopaoloanzn:mainfrom
bubblu2264326:main

Conversation

@bubblu2264326
Copy link
Copy Markdown
Contributor

@bubblu2264326 bubblu2264326 commented Mar 31, 2026

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)

  • Native Vision Translation: Anthropic base64 image schemas now map precisely to the Codex expected input_image payloads.
  • Strict Payload Mapping: Refactored the internal mapping logic to translate msg.content items precisely into input_text, sidestepping OpenAI's strict v1/responses validation rules (Invalid value: 'text').
  • Tool Logic Fixes: Properly routed tool_result items into top-level function_call_output objects 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 Stripping: Cleanly stripped Anthropic-only cache_control annotations from tool bindings and prompts prior to transmission so the Codex API doesn't reject malformed JSON.

2. Deep UI & Routing Integration

  • Model Cleanups (model.ts): Updated getPublicModelDisplayName and getClaudeAiUserDefaultModelDescription to recognize Codex GPT strings. Models like gpt-5.1-codex-max now beautifully map to Codex 5.1 Max in the CLI visual outputs instead of passing the raw proxy IDs.
  • Default Reroutes: Made getDefaultMainLoopModelSetting aware of isCodexSubscriber(), automatically defaulting to gpt-5.2-codex instead of sonnet46.
  • Billing Visuals (logoV2Utils.ts): Refactored formatModelAndBilling logic to render Codex API Billing proudly inside the terminal header when authenticated.

3. Reasoning & Metrics Support

  • Thinking Animations: codex-fetch-adapter now intentionally intercepts the proprietary response.reasoning.delta SSE frames emitted by codex-max models. It wraps them into Anthropic <thinking> events, ensuring the standard CLI "Thinking..." spinner continues to function flawlessly for OpenAI reasoning.
  • Token Accuracy: Bound logic to track response.completed completion events, fetching usage.input_tokens and output_tokens. These are injected natively into the final message_stop token handler, meaning Codex queries correctly trigger the terminal's Token/Price tracker summary logic.

4. Git Housekeeping

  • Configured .gitignore to securely and durably exclude the openclaw/ gateway directory from staging commits.

Summary by CodeRabbit

  • New Features

    • OpenAI Codex backend support with OAuth login and selectable Codex models for subscribers.
    • UI: Codex-aware default model selection and terminal billing header showing “Codex API Billing”.
    • Console login flow now offers Codex as a login method with token persistence.
  • Documentation

    • Added CLAUDE.md and changes.md documenting build/run guidance and Codex integration.
  • Bug Fixes

    • Improved ripgrep system-binary detection and more robust OAuth organization error handling.
  • Chores

    • Ignored local gateway directory in VCS; added CLI dev fallback for version/build info.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 04c14b4d-786c-41c8-821e-76de4497fad0

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3f07e and 973aa99.

📒 Files selected for processing (9)
  • src/constants/oauth.ts
  • src/services/oauth/client.ts
  • src/utils/auth.ts
  • src/utils/codex-fetch-adapter.ts
  • src/utils/config.ts
  • src/utils/model/configs.ts
  • src/utils/model/model.ts
  • src/utils/model/modelOptions.ts
  • src/utils/model/providers.ts

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Documentation & Gitignore
/.gitignore, CLAUDE.md, changes.md
Added openclaw/ to .gitignore and new docs describing build/run, architecture, and detailed Codex feature notes.
Codex OAuth Constants
src/constants/codex-oauth.ts
New exported Codex OAuth constants (client id, URLs, redirect URI, scopes, JWT claim, provider id).
Codex OAuth Client
src/services/oauth/codex-client.ts
New PKCE OAuth client with auth URL builder, PKCE helpers, local callback server (127.0.0.1:1455), code exchange, refresh, and runCodexOAuthFlow.
Auth Handlers & Token Persistence
src/cli/handlers/auth.ts, src/utils/auth.ts, src/utils/config.ts
Persist/get/clear Codex tokens; add codexOAuth to GlobalConfig; adjust post-OAuth token handling to save Codex tokens when appropriate; note duplicated isCodexSubscriber() definitions.
Console OAuth Flow UI
src/components/ConsoleOAuthFlow.tsx
Adds Codex as login option, loginWithCodex flow, startCodexOAuth wiring, and safer Anthropic org-validation message handling.
Codex Fetch Adapter & Client Integration
src/services/api/codex-fetch-adapter.ts, src/services/api/client.ts
New adapter mapping Anthropic /v1/messages ↔ Codex /v1/responses (model mapping, system→instructions, messages→inputs, images, tools); translates Codex SSE into Anthropic-style events and emits usage on completion; getAnthropicClient gains Codex branch using createCodexFetch.
Utility Codex Adapter (alternate)
src/utils/codex-fetch-adapter.ts
Added helper adapter and fetch wrapper converting internal Message objects to OpenAI chat completions and back, with error/usage mapping.
Model, Validation & Logo/Billing
src/utils/model/*, src/utils/logoV2Utils.ts
Added Codex-aware model configs/options/display names/marketing names, default main-loop model for Codex subscribers, early validation bypass for Codex models, and billing header selection showing “Codex API Billing”.
Model Configs & Options
src/utils/model/configs.ts, src/utils/model/modelOptions.ts
Added OpenAI mappings to model configs, new GPT_* configs (gpt54, gpt53codex, gpt54mini), and Codex-specific model option constructors and selection path.
Providers & OAuth Helpers
src/utils/model/providers.ts, src/constants/oauth.ts, src/services/oauth/client.ts
Added 'openai' provider to APIProvider, OpenAI OAuth scope constants and environment config fields, and a helper to build OpenAI auth URLs with PKCE.
Subscription Verification & Auth Checks
src/hooks/useApiKeyVerification.ts, src/utils/auth.ts
useApiKeyVerification treats Codex subscribers as early-valid; isCodexSubscriber added/modified (duplicate definitions present).
CLI Macro Fallback & Ripgrep Selection
src/entrypoints/cli.tsx, src/utils/ripgrep.ts
Added development fallback initializer for MACRO and widened system rg preference when builtin preference unset.
JSDoc & Minor Docs
src/services/oauth/crypto.ts, src/services/oauth/getOauthProfile.ts
Added JSDoc comments to PKCE helpers and OAuth profile helpers (no behavioral changes).

Sequence Diagrams

sequenceDiagram
    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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped a tunnel into OAuth light,
Swapped codex crumbs for cookies overnight,
I translate messages, spin SSE delight,
Tokens tucked warm — models find their right.
A rabbit cheers: new backends take flight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Codex API Support: Feature Parity & UI Overhaul' directly and accurately summarizes the main change: adding OpenAI Codex backend support with corresponding UI updates.
Docstring Coverage ✅ Passed Docstring coverage is 82.22% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bubblu2264326
Copy link
Copy Markdown
Contributor Author

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.....

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔴 Critical

Strip Anthropic auth headers before building the Codex client.

By the time this branch runs, defaultHeaders may already include Authorization from configureApiKeyHeaders() and any user-supplied ANTHROPIC_CUSTOM_HEADERS. Reusing ...ARGS here 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

📥 Commits

Reviewing files that changed from the base of the PR and between e5b13b3 and 87a3807.

📒 Files selected for processing (18)
  • .gitignore
  • CLAUDE.md
  • changes.md
  • src/cli/handlers/auth.ts
  • src/components/ConsoleOAuthFlow.tsx
  • src/constants/codex-oauth.ts
  • src/entrypoints/cli.tsx
  • src/hooks/useApiKeyVerification.ts
  • src/services/api/client.ts
  • src/services/api/codex-fetch-adapter.ts
  • src/services/oauth/codex-client.ts
  • src/utils/auth.ts
  • src/utils/config.ts
  • src/utils/logoV2Utils.ts
  • src/utils/model/model.ts
  • src/utils/model/validateModel.ts
  • src/utils/ripgrep.ts
  • src/vendor/ripgrep/x64-linux/rg

Comment thread src/cli/handlers/auth.ts
Comment on lines +118 to +127
} 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 ?? ''),
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +267 to +274
// 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);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +275 to +279
// 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +48 to +60
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')
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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 -10

Repository: 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; fi

Repository: 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 -20

Repository: 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.

Comment on lines +221 to +229
const codexBody: Record<string, unknown> = {
model: codexModel,
store: false,
stream: true,
instructions,
input,
tool_choice: 'auto',
parallel_tool_calls: true,
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/utils/auth.ts
Comment on lines +1340 to +1357
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,
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/utils/config.ts
Comment on lines +231 to +241
/**
* 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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment thread src/utils/logoV2Utils.ts
Comment on lines 259 to +263
const billingType = isClaudeAISubscriber()
? getSubscriptionName()
: 'API Usage Billing'
: isCodexSubscriber()
? 'Codex API Billing'
: 'API Usage Billing'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment thread src/utils/model/model.ts
Comment on lines +181 to +183
if (isCodexSubscriber()) {
return 'gpt-5.2-codex'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +44 to +49
// 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 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
src/services/api/codex-fetch-adapter.ts (4)

66-77: ⚠️ Potential issue | 🟠 Major

JWT 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 throw InvalidCharacterError or 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 | 🟠 Major

Hardcoded options override caller's intent.

These issues were raised in previous reviews:

  1. stream: true is hardcoded, forcing SSE even when the SDK makes a non-streaming call.
  2. tool_choice: 'auto' and parallel_tool_calls: true overwrite 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 | 🟠 Major

Stale accountId and missing AbortSignal forwarding.

Two issues persist from previous reviews:

  1. Stale accountId (lines 749, 787): accountId is extracted once from the constructor's accessToken, but the actual request uses currentToken (which may be refreshed). After token refresh, the header will send a mismatched account ID.

  2. AbortSignal not forwarded (line 781): The init?.signal from 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 | 🟡 Minor

Request object body not handled.

If the SDK passes a Request object (where body is in input.body), this code only reads init?.body and 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 _toolCallArgs parameters are unused. If they're not needed for the content_block_stop event, 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_id for function_call_output (line 148) uses block.tool_use_id from the Anthropic request, but the corresponding function_call (line 200) uses block.id. These are the same identifiers in Anthropic's format, so this should work. However, if tool_use_id is missing and the fallback counter is used, the generated call_id won't match any prior function_call, causing Codex to reject the output.

Consider adding a mapping from Anthropic tool_use_id to generated call_id to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 87a3807 and 0c3f07e.

📒 Files selected for processing (4)
  • src/services/api/codex-fetch-adapter.ts
  • src/services/oauth/codex-client.ts
  • src/services/oauth/crypto.ts
  • src/services/oauth/getOauthProfile.ts
✅ Files skipped from review due to trivial changes (2)
  • src/services/oauth/getOauthProfile.ts
  • src/services/oauth/crypto.ts

Comment on lines +148 to +156
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 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.

Comment on lines +293 to +300
}).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: () => {},
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +334 to +349
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +393 to +395
if (value.includes('code=')) {
const params = new URLSearchParams(value)
return { code: params.get('code') ?? undefined }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

paoloanzn and others added 3 commits April 1, 2026 11:53
- 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>
@paoloanzn paoloanzn merged commit 721fc8b into paoloanzn:main Apr 1, 2026
pfchrono pushed a commit to pfchrono/free-code that referenced this pull request Apr 8, 2026
Codex API Support: Feature Parity & UI Overhaul
cmz1111 pushed a commit to cmz1111/free-code that referenced this pull request Apr 10, 2026
Codex API Support: Feature Parity & UI Overhaul
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants