Skip to content

feat: Reuse Codex CLI OAuth tokens for ChatGPT backend LLM calls#693

Open
ZeroTrust01 wants to merge 10 commits intonearai:mainfrom
ZeroTrust01:codex_auth
Open

feat: Reuse Codex CLI OAuth tokens for ChatGPT backend LLM calls#693
ZeroTrust01 wants to merge 10 commits intonearai:mainfrom
ZeroTrust01:codex_auth

Conversation

@ZeroTrust01
Copy link

@ZeroTrust01 ZeroTrust01 commented Mar 7, 2026

Summary

This PR enables IronClaw to reuse OAuth authentication tokens from the Codex CLI's auth.json file, allowing users with an active OpenAI/ChatGPT subscription to use IronClaw without needing a separate API key. When enabled, IronClaw communicates directly with ChatGPT's private Responses API backend (chatgpt.com/backend-api/codex) using the subscriber's JWT token.

Motivation

Currently, IronClaw requires users to configure a separate LLM API key (e.g., from OpenAI Platform, Anthropic, or a third-party proxy). Users who already have a ChatGPT Plus/Pro/Team subscription and use the Codex CLI have valid OAuth tokens stored locally, but cannot leverage them with IronClaw. This PR bridges that gap, making it easy for ChatGPT subscribers to get started with IronClaw without additional API key setup.

Changes

New Files

  • src/codex_auth.rs — Reads and parses Codex CLI's ~/.codex/auth.json, supporting both apiKey and chatgpt authentication modes. Returns CodexCredentials with the token, mode flag, and correct base URL.

  • src/llm/codex_chatgpt.rs — New LlmProvider implementation (CodexChatGptProvider) that speaks the OpenAI Responses API protocol (POST /responses with SSE streaming), which is different from the standard Chat Completions API. Includes:

    • Auto-detection of the correct model from the /models endpoint (e.g., gpt-5.2-codex)
    • Message format translation from IronClaw's ChatMessage format to Responses API input items
    • SSE stream parsing for text deltas, function call arguments, and usage statistics
    • store: false flag required by the ChatGPT backend
    • Empty string stripping from tool call arguments (model-specific quirk)
    • Timeout-wrapped error handling to prevent silent hangs on HTTP errors

Modified Files

  • src/config/llm.rs — Added is_codex_chatgpt field to RegistryProviderConfig. When LLM_USE_CODEX_AUTH=true, Codex credentials are loaded with highest priority (over env vars and secrets store). In ChatGPT mode, the base URL is automatically overridden to the private backend endpoint.

  • src/llm/mod.rs — Added mod codex_chatgpt and dispatch logic in create_registry_provider to route to the new Responses API provider when is_codex_chatgpt is true.

  • src/lib.rs — Registered the codex_auth module.

Usage

# In ~/.ironclaw/.env:
LLM_USE_CODEX_AUTH=true
# Optionally specify a custom auth.json path:
# CODEX_AUTH_PATH=/path/to/auth.json

# Run IronClaw (model is auto-detected, no API key needed):
cargo run --release -- run --cli-only

The feature requires that the user has previously authenticated with the Codex CLI (codex --login), which stores the OAuth token in ~/.codex/auth.json.

Compatibility

  • No breaking changes. The feature is entirely opt-in via LLM_USE_CODEX_AUTH=true.
  • When LLM_USE_CODEX_AUTH=false (default), all existing LLM provider paths remain completely unchanged.
  • The is_codex_chatgpt flag only affects the provider selection when Codex ChatGPT mode credentials are detected; all other provider protocols (OpenAI-compatible, Anthropic, Ollama) are unaffected.
  • Standard OpenAI Platform API keys in auth.json (apiKey mode) are also supported and route through the existing OpenAI-compatible provider.

Testing

  • 8 unit tests added in codex_chatgpt.rs covering:
    • Message format conversion (user, assistant, tool calls, tool results)
    • System message extraction to instructions field
    • SSE response parsing (text deltas, tool call arguments)
    • store: false assertion
    • Empty string value stripping
  • 6 unit tests in codex_auth.rs covering auth.json parsing
  • Manual end-to-end testing confirmed successful LLM responses from the ChatGPT backend with auto-detected gpt-5.2-codex model

Background

This PR builds on the observation made during #384 (feat(setup): Anthropic OAuth onboarding), where Codex OAuth support was initially attempted but then intentionally removed with:

"refactor: remove Codex OAuth onboarding (incompatible with OpenAI API) — Codex CLI OAuth tokens use a different endpoint (chatgpt.com/backend-api/codex) and the Responses API wire format, not api.openai.com with Chat Completions. The tokens lack the model.request scope needed for the platform API, so they can't be used as drop-in OPENAI_API_KEY replacements."

This PR solves that exact problem by implementing a dedicated CodexChatGptProvider that speaks the Responses API protocol natively (POST /responses with SSE streaming), rather than trying to use the Codex OAuth token as a standard OpenAI API key. This approach correctly handles the different wire format and endpoint.

@github-actions github-actions bot added scope: llm LLM integration size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: new First-time contributor labels Mar 7, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances IronClaw's authentication flexibility by allowing it to utilize existing OpenAI Codex CLI OAuth tokens. This means users with an active ChatGPT subscription can now seamlessly integrate IronClaw without the need for additional API key setup, as IronClaw can directly interact with ChatGPT's private backend API. The change introduces new modules for credential management and a dedicated LLM provider for the ChatGPT Responses API, making the system more convenient for a broader user base.

Highlights

  • Codex CLI OAuth Token Reuse: IronClaw can now reuse OAuth authentication tokens from the OpenAI Codex CLI's auth.json file, enabling ChatGPT subscribers to use IronClaw without a separate API key.
  • Direct ChatGPT Backend Integration: A new LLM provider (CodexChatGptProvider) has been added to communicate directly with ChatGPT's private Responses API backend (chatgpt.com/backend-api/codex), bypassing the standard Chat Completions API.
  • Codex Credentials Loading: A new module (src/codex_auth.rs) was introduced to read and parse auth.json, supporting both apiKey and chatgpt authentication modes and determining the correct base URL.
  • Configuration and Opt-in: The feature is entirely opt-in via the LLM_USE_CODEX_AUTH=true environment variable, ensuring no breaking changes to existing LLM provider configurations.
  • Robust SSE Parsing and Tool Handling: The new provider includes advanced SSE stream parsing for text deltas, function call arguments, and usage statistics, along with a mechanism to strip empty string values from tool arguments to prevent validation issues.
Changelog
  • src/codex_auth.rs
    • Added a new module to read and parse Codex CLI's auth.json file.
    • Implemented logic to extract credentials for both apiKey and chatgpt authentication modes.
    • Defined constants for ChatGPT backend and standard OpenAI API URLs.
    • Included utility functions for default path resolution and credential loading.
    • Added unit tests to cover various credential loading scenarios.
  • src/config/llm.rs
    • Added is_codex_chatgpt field to RegistryProviderConfig to flag the use of the new provider.
    • Modified LlmConfig::load to prioritize Codex credentials when LLM_USE_CODEX_AUTH is enabled.
    • Implemented logic to override the base URL to the ChatGPT backend when in ChatGPT mode.
  • src/lib.rs
    • Registered the new codex_auth module.
  • src/llm/codex_chatgpt.rs
    • Added a new LlmProvider implementation (CodexChatGptProvider) for the ChatGPT Responses API.
    • Implemented message format translation from IronClaw's ChatMessage to Responses API input items.
    • Included auto-detection of the default model from the /models endpoint.
    • Developed SSE stream parsing for text deltas, function call arguments, and usage statistics.
    • Ensured store: false is set in requests and added a function to strip empty string values from JSON objects.
    • Added comprehensive unit tests for message conversion, request body building, SSE parsing, and empty string stripping.
  • src/llm/mod.rs
    • Registered the new codex_chatgpt module.
    • Updated create_registry_provider to dispatch to the CodexChatGptProvider based on the is_codex_chatgpt flag.
    • Introduced create_codex_chatgpt_from_registry to handle the instantiation and model auto-detection for the new provider.
Activity
  • No human activity has occurred on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a great new feature to reuse Codex CLI's OAuth tokens, which simplifies setup for many users. The implementation is well-structured, with new modules for authentication and the custom LLM provider. I've found a couple of areas for improvement. There's a high-severity bug in the credential loading logic that could lead to using the wrong LLM provider, and a medium-severity performance issue related to reqwest::Client instantiation. My detailed comments include code suggestions to address these points. Overall, this is a solid contribution.

Comment on lines +101 to +109
if !is_chatgpt {
if let Some(key) = auth.openai_api_key.filter(|k| !k.is_empty()) {
tracing::info!("Loaded API key from Codex auth.json (API key mode)");
return Some(CodexCredentials {
token: key,
is_chatgpt_mode: false,
});
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The current logic has a bug where if auth_mode is "apiKey" but the key is missing, it incorrectly falls through to check for a ChatGPT token. This can lead to returning credentials with is_chatgpt_mode: true, causing the wrong LLM provider to be used.

To fix this, we should explicitly prevent the fallback when auth_mode is set. If auth_mode is "apiKey" and no key is found, the function should return None.

    if !is_chatgpt {
        if let Some(key) = auth.openai_api_key.filter(|k| !k.is_empty()) {
            tracing::info!("Loaded API key from Codex auth.json (API key mode)");
            return Some(CodexCredentials {
                token: key,
                is_chatgpt_mode: false,
            });
        }
        // If auth_mode was explicitly `apiKey`, do not fall back to checking for a token.
        if auth.auth_mode.is_some() {
            return None;
        }
    }

Comment on lines +56 to +58
async fn fetch_default_model(base_url: &str, api_key: &str) -> Option<String> {
let url = format!("{base_url}/models?client_version=0.1.0");
let client = Client::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Creating a new reqwest::Client for a single request is inefficient. The client manages a connection pool and is designed to be reused. This function is called from with_auto_model, which then calls Self::new, creating another client. This results in two clients being created to instantiate one provider.

To improve this, the client should be created once in with_auto_model, passed as an argument to this function, and then moved into the CodexChatGptProvider instance. This will require modifying with_auto_model as well.

Suggested change
async fn fetch_default_model(base_url: &str, api_key: &str) -> Option<String> {
let url = format!("{base_url}/models?client_version=0.1.0");
let client = Client::new();
async fn fetch_default_model(client: &Client, base_url: &str, api_key: &str) -> Option<String> {
let url = format!("{base_url}/models?client_version=0.1.0");
References
  1. While the general rule advises against reqwest::Client reuse for one-time or infrequent operations with negligible benefit and high churn, this specific scenario involves creating two clients for a single provider instantiation, indicating a clear inefficiency where the benefit of reuse is not negligible.

When LLM_USE_CODEX_AUTH=true, IronClaw reads the Codex CLI's auth.json
(default ~/.codex/auth.json) and extracts the API key or OAuth access
token. This lets IronClaw piggyback on a Codex login without
implementing its own OAuth flow.

New env vars:
  - LLM_USE_CODEX_AUTH: enable Codex auth fallback (default: false)
  - CODEX_AUTH_PATH: override path to auth.json
Switch base_url to chatgpt.com/backend-api/codex when auth.json
contains ChatGPT OAuth tokens. The access_token is a JWT that only
works against the private ChatGPT backend, not the public OpenAI API.

Refactored codex_auth.rs to return CodexCredentials (token +
is_chatgpt_mode) instead of just a string key.
When LLM_USE_CODEX_AUTH=true, Codex credentials are now loaded before
checking env vars or the secrets store overlay. Previously the secrets
store key (injected during onboarding) would shadow the Codex token.
- New CodexChatGptProvider speaks the Responses API protocol
- Auto-detects model from /models endpoint (gpt-4o -> gpt-5.2-codex)
- Adds store=false (required by ChatGPT backend)
- Error handling with timeout for HTTP 400 responses
- Message format translation: Chat Completions -> Responses API
- SSE response parsing for text, tool calls, and usage stats
- 7 unit tests for message conversion and SSE parsing
The Responses API sends function_call_arguments.delta events with
item_id (e.g. fc_...) not call_id (e.g. call_...). The parser now
keys pending tool calls by item_id from output_item.added and
tracks call_id separately for result matching.
gpt-5.2-codex fills optional tool parameters with empty strings
(e.g. timestamp: ""), which IronClaw's tool validation rejects.
Strip them before passing to tool execution.
When auth_mode is explicitly 'apiKey' but the key is missing/empty,
do not fall through to check for a ChatGPT access_token. This prevents
returning credentials with is_chatgpt_mode: true and routing to the
wrong LLM provider.
… calls

Create Client once in with_auto_model, pass &Client to
fetch_default_model, and move it into the provider struct.
Eliminates the redundant Client::new() that wasted a connection pool.
The /models endpoint gates newer models behind client_version.
Version 0.1.0 only returns up to gpt-5.2-codex, while 1.0.0+
also returns gpt-5.3-codex and gpt-5.4.
Fetch the full model list from /models endpoint. If LLM_MODEL is set,
validate it against the supported list and warn with available models
if not found. If LLM_MODEL is not set, auto-detect the highest-priority
model. Also bumps client_version to 1.0.0 to unlock gpt-5.3/5.4.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: new First-time contributor risk: low Changes to docs, tests, or low-risk modules scope: llm LLM integration size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant