feat(llm): Add OpenAI Codex (ChatGPT subscription) as LLM provider#1461
feat(llm): Add OpenAI Codex (ChatGPT subscription) as LLM provider#1461ilblackdragon merged 7 commits intostagingfrom
Conversation
Add OpenAiCodex as a new LLM backend variant with config for auth endpoint, API base URL, client ID, and session persistence path. The session manager implements OpenAI's device code auth flow (headless-friendly, no browser required on the server) with automatic token refresh, following the same persistence pattern as the existing NEAR AI session manager. Closes #742 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Native Responses API client for chatgpt.com/backend-api/codex/responses, the endpoint that works with ChatGPT subscription tokens. Handles SSE streaming, text completions, and tool call round-trips. Token-refreshing decorator wraps the provider to pre-emptively refresh OAuth tokens before API calls and retry once on auth failures. Reports zero cost since billing is through subscription. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…zard Connect the new provider to the LLM factory, add openai_codex to the CLI --backend flag, and add it as an option in the onboarding wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review fixes for the OpenAI Codex provider PR: - Remove dead `generate_pkce()` code (device flow gets PKCE from server) - Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec - Inline codex dispatch into `build_provider_chain()` (single async function, no separate `assemble_provider_chain()` helper — matches main's pattern) - Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)` - Propagate HTTP client builder error instead of silent fallback - Redact device code response body from debug log - Change `set_model()` in TokenRefreshingProvider to delegate to inner - Replace hardcoded `/tmp/` test path with `tempfile::tempdir()` - Accept `request_timeout_secs` from config instead of hardcoded 300s - Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern) - Reuse `normalize_schema_strict()` for Codex tool definitions - Add warning log for dropped image attachments - Add doc comments on `list_models()` and `include` field - Add `OPENAI_CODEX_API_URL` to `.env.example` - Fix codex error message in `create_llm_provider()` for clarity - Revert unrelated `.worktrees` addition to `.gitignore` - Update `src/llm/CLAUDE.md` with Codex provider docs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # src/app.rs # src/llm/CLAUDE.md # src/llm/mod.rs # src/setup/wizard.rs
#744) Security: - Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and OPENAI_CODEX_API_URL, matching the pattern used by all other base URL configs (regression test for #1103 included) Correctness: - Add missing cache_write_multiplier() and cache_read_discount() trait delegation in TokenRefreshingProvider - Cap device-code polling backoff at 60s to prevent unbounded interval growth on repeated 429 responses - Default expires_in to 3600s when server returns 0, preventing immediately-expired sessions - Fix pre-existing SseEvent::JobResult missing fallback_deliverable field in job_monitor.rs tests Cleanup: - Extract duplicated make_test_jwt() and test_codex_config() into shared codex_test_helpers module Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary of ChangesHello, 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 introduces comprehensive support for OpenAI Codex, enabling users with ChatGPT subscriptions to seamlessly integrate and utilize this LLM within the application. The changes encompass a new provider implementation, a robust OAuth authentication flow with token management, and critical security enhancements. This significantly expands the range of available LLM backends and improves the user experience for a segment of OpenAI subscribers. Highlights
Using Gemini Code AssistThe 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
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 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. Footnotes
|
There was a problem hiding this comment.
Pull request overview
Adds OpenAI Codex (ChatGPT subscription) as a first-class LLM backend in IronClaw, including OAuth device-code authentication, a Responses API client, and provider wiring so users can run without an OpenAI API key.
Changes:
- Introduces
openai_codexbackend with OAuth session persistence + token refresh, and a Responses API provider implementation. - Wires Codex into setup wizard + CLI (
ironclaw login --openai-codex) and into the LLM provider factory chain. - Extends config/env/docs and fixes/updates related tests & CLI snapshots.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/setup/wizard.rs |
Adds OpenAI Codex to provider selection and implements device-code login setup flow. |
src/main.rs |
Adds login command handling that triggers Codex device-code authentication. |
src/llm/token_refreshing.rs |
New decorator provider that pre-refreshes tokens and retries on auth failures. |
src/llm/rig_adapter.rs |
Exposes normalize_schema_strict for reuse by Codex tool schema conversion. |
src/llm/openai_codex_session.rs |
New OAuth session manager with persistence, refresh, and device-code flow. |
src/llm/openai_codex_provider.rs |
New Responses API client + SSE event parsing + tool-call extraction. |
src/llm/models.rs |
Ensures LlmConfig builder includes openai_codex field. |
src/llm/mod.rs |
Exports Codex modules and adds a dedicated factory path in build_provider_chain(). |
src/llm/config.rs |
Introduces OpenAiCodexConfig and adds it into LlmConfig. |
src/llm/codex_test_helpers.rs |
Shared helpers for Codex unit tests (JWT + config builders). |
src/llm/CLAUDE.md |
Documents Codex provider behavior, auth flow, and env vars. |
src/config/mod.rs |
Re-exports OpenAiCodexConfig; minor comment tweak. |
src/config/llm.rs |
Resolves openai_codex backend, including SSRF validation + tests. |
src/cli/snapshots/ironclaw__cli__tests__long_help_output.snap |
Updates CLI snapshot to include new login command. |
src/cli/snapshots/ironclaw__cli__tests__long_help_output_without_import.snap |
Updates CLI snapshot to include new login command. |
src/cli/snapshots/ironclaw__cli__tests__help_output.snap |
Updates CLI snapshot to include new login command. |
src/cli/snapshots/ironclaw__cli__tests__help_output_without_import.snap |
Updates CLI snapshot to include new login command. |
src/cli/mod.rs |
Adds Login { openai_codex: bool } Clap subcommand definition. |
src/app.rs |
Allows openai_codex backend to bypass registry-provider requirement checks. |
src/agent/job_monitor.rs |
Fixes test struct construction by adding fallback_deliverable. |
.env.example |
Documents openai_codex backend and relevant OPENAI_CODEX_* env vars. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let config = Config::from_env() | ||
| .await | ||
| .map_err(|e| anyhow::anyhow!("{}", e))?; | ||
| let codex_config = config.llm.openai_codex.unwrap_or_default(); | ||
| let mgr = ironclaw::llm::OpenAiCodexSessionManager::new(codex_config) | ||
| .map_err(|e| anyhow::anyhow!("{}", e))?; |
There was a problem hiding this comment.
ironclaw login --openai-codex loads Config::from_env() but then falls back to OpenAiCodexConfig::default() when config.llm.openai_codex is None. Since LlmConfig::resolve() only populates openai_codex when LLM_BACKEND/settings are set to Codex, this path ignores OPENAI_CODEX_* env overrides (auth/api URL, session path, refresh margin, model). Consider resolving OpenAiCodexConfig directly from env in this command (or temporarily treating the backend as openai_codex) so login uses the same validated config as runtime.
There was a problem hiding this comment.
Fixed in 3bb817b. The login command now resolves OPENAI_CODEX_* env overrides directly when config.llm.openai_codex is None, so env vars like OPENAI_CODEX_AUTH_URL are honoured regardless of LLM_BACKEND setting.
| return Ok(()); | ||
| } | ||
| if current == "openai_codex" { | ||
| return self.setup_openai_codex().await; |
There was a problem hiding this comment.
When the current provider is openai_codex and the user answers “Keep current provider?”, the wizard calls setup_openai_codex() which triggers a device-code login flow again. That’s a surprising side effect for a “keep” path and may force unnecessary re-auth. Consider mirroring the Bedrock behavior (just keep and return), or doing a non-interactive ensure_authenticated() check and only prompting for login if the session is missing/expired.
| return self.setup_openai_codex().await; | |
| // Keeping the existing OpenAI Codex config — avoid re-running the | |
| // full setup flow (device-code login) on a no-op "keep" choice. | |
| print_info("Keeping existing OpenAI Codex configuration."); | |
| return Ok(()); |
There was a problem hiding this comment.
Fixed in 3bb817b. The "Keep current provider?" path now mirrors Bedrock: prints "Keeping existing OpenAI Codex configuration." and returns without re-triggering device code login.
| // Read the full body and parse SSE events | ||
| let body_bytes = response | ||
| .bytes() | ||
| .await | ||
| .map_err(|e| LlmError::RequestFailed { | ||
| provider: "openai_codex".to_string(), | ||
| reason: format!("Failed to read response body: {e}"), | ||
| })?; | ||
|
|
||
| let body_text = String::from_utf8_lossy(&body_bytes); | ||
| parse_sse_response(&body_text) |
There was a problem hiding this comment.
send_request() advertises SSE streaming (Accept: text/event-stream, stream: true) but then calls response.bytes().await and parses after the full body is buffered. This defeats streaming (slower time-to-first-token) and risks unbounded memory use for long outputs. Consider parsing the response incrementally from bytes_stream() (similar to CodexChatGptProvider::parse_sse_response_stream) and building the output as events arrive.
There was a problem hiding this comment.
Acknowledged — this is a known limitation noted during the takeover review. Switching to incremental SSE parsing requires a significant refactor (using bytes_stream() + line-buffered parsing). Filed as a follow-up, not addressing in this PR.
| } else { | ||
| create_llm_provider(config, session.clone()).await? | ||
| }; | ||
| tracing::info!("LLM provider initialized: {}", llm.model_name()); |
There was a problem hiding this comment.
build_provider_chain() now logs provider initialization at info level. This message includes the model name and will fire on every startup; if the rest of the codebase uses debug for this kind of internal wiring message, consider keeping it at debug (or adding structured fields and a more specific target) to avoid noisy logs in production.
| tracing::info!("LLM provider initialized: {}", llm.model_name()); | |
| tracing::debug!("LLM provider initialized: {}", llm.model_name()); |
There was a problem hiding this comment.
Fixed in 3bb817b. Reverted to tracing::debug! to match the rest of the codebase's internal wiring log level.
There was a problem hiding this comment.
Code Review
This pull request introduces OpenAI Codex as a new LLM provider, enabling ChatGPT Plus/Pro subscribers to use IronClaw without a separate API key. The changes include implementing OAuth device code login, a native Responses API client with SSE parsing and tool call round-trips, and a token-refreshing decorator. Key updates also involve integrating SSRF validation for the new endpoints, adding missing trait delegations, capping device-code polling backoff, and fixing a pre-existing SseEvent::JobResult field. The overall implementation is robust, with good test coverage for the new features and security considerations. The addition of openai_codex_test_helpers is a good practice for shared test utilities.
| /// Authenticate with OpenAI Codex (ChatGPT subscription) | ||
| #[arg(long)] | ||
| openai_codex: bool, |
There was a problem hiding this comment.
There was a problem hiding this comment.
Acknowledged as a follow-up. The boolean flag pattern works for now and the current implementation is from the original PR. Will revisit if/when more OAuth providers are added.
| if let Ok(dt) = chrono::DateTime::parse_from_rfc2822(v.trim()) { | ||
| let now = chrono::Utc::now(); | ||
| let delta = dt.signed_duration_since(now); | ||
| return Some(std::time::Duration::from_secs( | ||
| delta.num_seconds().max(0) as u64 | ||
| )); | ||
| } | ||
| None |
There was a problem hiding this comment.
The chrono::DateTime::parse_from_rfc2822 function is used here, but RFC 2822 is typically for email dates. Given that expires_at is an ISO-8601 timestamp in UserCodeResponse, it might be more appropriate to use chrono::DateTime::parse_from_rfc3339 for consistency and accuracy when parsing Retry-After headers, as RFC 3339 is a profile of ISO 8601. If the header can indeed be RFC 2822, then this is fine, but it's worth double-checking the expected format from the OpenAI API.
There was a problem hiding this comment.
Not a bug. The Retry-After header uses HTTP-date format (RFC 7231 §7.1.3), which is IMF-fixdate — a profile of RFC 2822. Using parse_from_rfc2822 is correct here. The expires_at in UserCodeResponse (ISO 8601) is a different field parsed elsewhere with parse_from_rfc3339.
| let args_str = if tc.arguments.is_string() { | ||
| tc.arguments.as_str().unwrap_or("{}").to_string() | ||
| } else { | ||
| tc.arguments.to_string() | ||
| }; |
There was a problem hiding this comment.
The tc.arguments field is converted to a string using to_string() if it's not already a string. This might lead to issues if the arguments are complex JSON objects that need to be passed as-is, rather than as a string representation. The serde_json::Value type should ideally be passed directly or serialized in a way that preserves its structure, especially for tool calls where the arguments are expected to be a JSON object.
There was a problem hiding this comment.
This is correct behavior. The Responses API function_call item expects arguments as a JSON string (not a nested object). When tc.arguments is already a serde_json::Value::String, we extract it; when it's an object, we serialize to string. This matches the API spec.
| let arguments: serde_json::Value = | ||
| serde_json::from_str(&state.arguments).unwrap_or_else(|_| { | ||
| serde_json::Value::String(state.arguments.clone()) | ||
| }); |
There was a problem hiding this comment.
The fallback logic for serde_json::from_str(&state.arguments).unwrap_or_else(|_| serde_json::Value::String(state.arguments.clone())) could potentially mask parsing errors. If state.arguments is expected to be a JSON string, a parsing failure should probably be treated as an error rather than silently converting it to a string literal. This could hide malformed tool call arguments.
References
- This comment aligns with the principle of avoiding silent failures when handling critical data. Using
unwrap_or_elseto fallback to a string literal instead of failing on a parsing error forstate.argumentscan mask malformed input, similar to how#[serde(default)]can hide missing critical data.
There was a problem hiding this comment.
This is intentionally defensive. The arguments are accumulated from SSE stream chunks. If the final concatenated string isn't valid JSON (e.g., truncated stream), wrapping it as a string value lets the tool handler see the raw text rather than aborting the entire response. The agent loop will handle the malformed arguments gracefully.
| + chrono::Duration::seconds(if token_resp.expires_in > 0 { | ||
| token_resp.expires_in | ||
| } else { | ||
| 3600 | ||
| } as i64), |
There was a problem hiding this comment.
The expires_in field from the TokenResponse is used to calculate expires_at. If expires_in is 0, it defaults to 3600 seconds. While this prevents immediately expired sessions, an expires_in of 0 might indicate an issue with the token or the server's response. It might be safer to log a warning or treat expires_in == 0 as an error condition, or at least use a more conservative default, to avoid silently extending potentially invalid token lifetimes.
References
- Defaulting
expires_into 3600 seconds when it's 0 for aTokenResponsefield can lead to silent failures by extending the lifetime of a potentially invalid token. This aligns with the rule to avoid default values for critical fields where missing or unexpected data should result in an explicit failure.
There was a problem hiding this comment.
Good point. Added a tracing::warn! when expires_in=0 before defaulting to 3600s, in 3bb817b. This makes the fallback visible in logs for debugging without failing hard (since in practice this usually means the server omitted the field rather than indicating an error).
| + chrono::Duration::seconds(if token_resp.expires_in > 0 { | ||
| token_resp.expires_in | ||
| } else { | ||
| 3600 | ||
| } as i64), |
There was a problem hiding this comment.
Similar to the previous comment, the expires_in field from the TokenResponse is defaulted to 3600 seconds if it's 0. This behavior should be consistent and potentially re-evaluated. If the server explicitly sends expires_in: 0, it might imply immediate expiration or an invalid token, which should be handled more explicitly.
References
- This comment reiterates the concern from the previous comment regarding the
expires_infield. Explicitly handlingexpires_in: 0as a potential error or immediate expiration, rather than defaulting, prevents silent failures and aligns with the rule's guidance on critical fields in deserialized data.
There was a problem hiding this comment.
Same as above — added warning log in 3bb817b for both occurrences (device code login and token refresh paths).
| if let Err(e) = self.update_inner_token().await { | ||
| tracing::warn!( | ||
| "Pre-emptive token update failed: {e}, will retry on auth failure" | ||
| ); | ||
| } |
There was a problem hiding this comment.
The tracing::warn! message here indicates that a pre-emptive token update failed and that the system will "retry on auth failure". This implies that the ensure_fresh_token function might not be fully effective in preventing AuthFailed errors from reaching the main complete or complete_with_tools methods. It might be clearer to explicitly state that the token update is part of a best-effort pre-check and that the retry mechanism in the complete methods will handle the actual AuthFailed error.
There was a problem hiding this comment.
The message accurately describes the behavior — ensure_fresh_token is explicitly documented as a best-effort pre-check (see doc comment on line 45-48). The retry-on-auth-failure path in complete()/complete_with_tools() handles the actual recovery. No change needed.
| tracing::warn!( | ||
| "Pre-emptive token refresh failed: {e}, will retry on auth failure" | ||
| ); |
There was a problem hiding this comment.
The warning message for a failed pre-emptive token refresh is informative. However, if refresh_tokens() fails, it might be useful to include the specific error e in the log message to provide more context for debugging, rather than just stating that it failed and will retry on auth failure.
References
- This comment suggests including the specific error
ein the log message for a failed token refresh. This aligns with the rule's emphasis on capturing debugging information in error logs to provide more context for failure analysis.
There was a problem hiding this comment.
The error IS included in the format string: "Pre-emptive token refresh failed: {e}, will retry on auth failure" — {e} expands to the full error. No change needed.
| let config = Config::from_env() | ||
| .await | ||
| .map_err(|e| anyhow::anyhow!("{}", e))?; | ||
| let codex_config = config.llm.openai_codex.unwrap_or_default(); |
There was a problem hiding this comment.
Using unwrap_or_default() on config.llm.openai_codex here means that if openai_codex is None (i.e., the backend is not configured as openai_codex), it will still proceed with a default OpenAiCodexConfig. This might lead to unexpected behavior if the user intended to use a different backend but accidentally triggered the login --openai-codex command. It might be more robust to return an error or a more explicit message if the openai_codex config is not present when attempting to log in.
References
- This comment advises against using
unwrap_or_default()in application logic when aNonevalue forconfig.llm.openai_codexcould lead to unexpected behavior or silently incorrect logic. This mirrors the rule's guidance to avoidunwrap_or()in tests for similar reasons, preferring explicit failure or error handling over silent fallbacks.
There was a problem hiding this comment.
Fixed in 3bb817b. The login command now resolves OPENAI_CODEX_* env overrides via std::env::var() when config.llm.openai_codex is None, rather than silently using defaults.
- Login command now resolves OPENAI_CODEX_* env overrides even when LLM_BACKEND isn't set to openai_codex (Copilot review) - Setup wizard "Keep current provider?" for codex no longer re-triggers device code login — mirrors Bedrock's keep-and-return pattern (Copilot) - Revert provider init log from info back to debug (Copilot) - Add warning log when token expires_in=0, before defaulting to 3600s (Gemini review) Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code reviewNo issues found. |
Code reviewFound 5 issues:
|
Code review (Architecture & Design Issues)Found 8 additional issues:
|
Code review (Security & Safety Issues)Found 4 security-related issues:
Positive Security Findings: |
|
Code review (Bug Scan Results): NO OBVIOUS BUGS DETECTED. Comprehensive diff-only scan found no crashes, data corruption, or critical logic errors. Final summary: 17 total issues found across 4 categories (2 CRITICAL, 4 HIGH, 4 MEDIUM, 3 LOW). Recommend addressing CRITICAL items before merge. |
|
This was reviewed in #744 - rebased and re-reviewed |
…1461) * feat(llm): add OpenAI Codex backend config and OAuth session manager Add OpenAiCodex as a new LLM backend variant with config for auth endpoint, API base URL, client ID, and session persistence path. The session manager implements OpenAI's device code auth flow (headless-friendly, no browser required on the server) with automatic token refresh, following the same persistence pattern as the existing NEAR AI session manager. Closes #742 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): add Responses API client and token-refreshing decorator Native Responses API client for chatgpt.com/backend-api/codex/responses, the endpoint that works with ChatGPT subscription tokens. Handles SSE streaming, text completions, and tool call round-trips. Token-refreshing decorator wraps the provider to pre-emptively refresh OAuth tokens before API calls and retry once on auth failures. Reports zero cost since billing is through subscription. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): wire OpenAI Codex into provider factory, CLI, and setup wizard Connect the new provider to the LLM factory, add openai_codex to the CLI --backend flag, and add it as an option in the onboarding wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(llm): address PR #744 review feedback (20 items) Review fixes for the OpenAI Codex provider PR: - Remove dead `generate_pkce()` code (device flow gets PKCE from server) - Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec - Inline codex dispatch into `build_provider_chain()` (single async function, no separate `assemble_provider_chain()` helper — matches main's pattern) - Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)` - Propagate HTTP client builder error instead of silent fallback - Redact device code response body from debug log - Change `set_model()` in TokenRefreshingProvider to delegate to inner - Replace hardcoded `/tmp/` test path with `tempfile::tempdir()` - Accept `request_timeout_secs` from config instead of hardcoded 300s - Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern) - Reuse `normalize_schema_strict()` for Codex tool definitions - Add warning log for dropped image attachments - Add doc comments on `list_models()` and `include` field - Add `OPENAI_CODEX_API_URL` to `.env.example` - Fix codex error message in `create_llm_provider()` for clarity - Revert unrelated `.worktrees` addition to `.gitignore` - Update `src/llm/CLAUDE.md` with Codex provider docs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback and harden OpenAI Codex provider (takeover #744) Security: - Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and OPENAI_CODEX_API_URL, matching the pattern used by all other base URL configs (regression test for #1103 included) Correctness: - Add missing cache_write_multiplier() and cache_read_discount() trait delegation in TokenRefreshingProvider - Cap device-code polling backoff at 60s to prevent unbounded interval growth on repeated 429 responses - Default expires_in to 3600s when server returns 0, preventing immediately-expired sessions - Fix pre-existing SseEvent::JobResult missing fallback_deliverable field in job_monitor.rs tests Cleanup: - Extract duplicated make_test_jwt() and test_codex_config() into shared codex_test_helpers module Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback on OpenAI Codex provider (#1461) - Login command now resolves OPENAI_CODEX_* env overrides even when LLM_BACKEND isn't set to openai_codex (Copilot review) - Setup wizard "Keep current provider?" for codex no longer re-triggers device code login — mirrors Bedrock's keep-and-return pattern (Copilot) - Revert provider init log from info back to debug (Copilot) - Add warning log when token expires_in=0, before defaulting to 3600s (Gemini review) Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Sanjeev Suresh <Sanjeev-S@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…1461) * feat(llm): add OpenAI Codex backend config and OAuth session manager Add OpenAiCodex as a new LLM backend variant with config for auth endpoint, API base URL, client ID, and session persistence path. The session manager implements OpenAI's device code auth flow (headless-friendly, no browser required on the server) with automatic token refresh, following the same persistence pattern as the existing NEAR AI session manager. Closes #742 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): add Responses API client and token-refreshing decorator Native Responses API client for chatgpt.com/backend-api/codex/responses, the endpoint that works with ChatGPT subscription tokens. Handles SSE streaming, text completions, and tool call round-trips. Token-refreshing decorator wraps the provider to pre-emptively refresh OAuth tokens before API calls and retry once on auth failures. Reports zero cost since billing is through subscription. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): wire OpenAI Codex into provider factory, CLI, and setup wizard Connect the new provider to the LLM factory, add openai_codex to the CLI --backend flag, and add it as an option in the onboarding wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(llm): address PR #744 review feedback (20 items) Review fixes for the OpenAI Codex provider PR: - Remove dead `generate_pkce()` code (device flow gets PKCE from server) - Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec - Inline codex dispatch into `build_provider_chain()` (single async function, no separate `assemble_provider_chain()` helper — matches main's pattern) - Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)` - Propagate HTTP client builder error instead of silent fallback - Redact device code response body from debug log - Change `set_model()` in TokenRefreshingProvider to delegate to inner - Replace hardcoded `/tmp/` test path with `tempfile::tempdir()` - Accept `request_timeout_secs` from config instead of hardcoded 300s - Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern) - Reuse `normalize_schema_strict()` for Codex tool definitions - Add warning log for dropped image attachments - Add doc comments on `list_models()` and `include` field - Add `OPENAI_CODEX_API_URL` to `.env.example` - Fix codex error message in `create_llm_provider()` for clarity - Revert unrelated `.worktrees` addition to `.gitignore` - Update `src/llm/CLAUDE.md` with Codex provider docs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback and harden OpenAI Codex provider (takeover #744) Security: - Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and OPENAI_CODEX_API_URL, matching the pattern used by all other base URL configs (regression test for #1103 included) Correctness: - Add missing cache_write_multiplier() and cache_read_discount() trait delegation in TokenRefreshingProvider - Cap device-code polling backoff at 60s to prevent unbounded interval growth on repeated 429 responses - Default expires_in to 3600s when server returns 0, preventing immediately-expired sessions - Fix pre-existing SseEvent::JobResult missing fallback_deliverable field in job_monitor.rs tests Cleanup: - Extract duplicated make_test_jwt() and test_codex_config() into shared codex_test_helpers module Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback on OpenAI Codex provider (#1461) - Login command now resolves OPENAI_CODEX_* env overrides even when LLM_BACKEND isn't set to openai_codex (Copilot review) - Setup wizard "Keep current provider?" for codex no longer re-triggers device code login — mirrors Bedrock's keep-and-return pattern (Copilot) - Revert provider init log from info back to debug (Copilot) - Add warning log when token expires_in=0, before defaulting to 3600s (Gemini review) Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Sanjeev Suresh <Sanjeev-S@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…earai#1461) * feat(llm): add OpenAI Codex backend config and OAuth session manager Add OpenAiCodex as a new LLM backend variant with config for auth endpoint, API base URL, client ID, and session persistence path. The session manager implements OpenAI's device code auth flow (headless-friendly, no browser required on the server) with automatic token refresh, following the same persistence pattern as the existing NEAR AI session manager. Closes nearai#742 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): add Responses API client and token-refreshing decorator Native Responses API client for chatgpt.com/backend-api/codex/responses, the endpoint that works with ChatGPT subscription tokens. Handles SSE streaming, text completions, and tool call round-trips. Token-refreshing decorator wraps the provider to pre-emptively refresh OAuth tokens before API calls and retry once on auth failures. Reports zero cost since billing is through subscription. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): wire OpenAI Codex into provider factory, CLI, and setup wizard Connect the new provider to the LLM factory, add openai_codex to the CLI --backend flag, and add it as an option in the onboarding wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(llm): address PR nearai#744 review feedback (20 items) Review fixes for the OpenAI Codex provider PR: - Remove dead `generate_pkce()` code (device flow gets PKCE from server) - Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec - Inline codex dispatch into `build_provider_chain()` (single async function, no separate `assemble_provider_chain()` helper — matches main's pattern) - Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)` - Propagate HTTP client builder error instead of silent fallback - Redact device code response body from debug log - Change `set_model()` in TokenRefreshingProvider to delegate to inner - Replace hardcoded `/tmp/` test path with `tempfile::tempdir()` - Accept `request_timeout_secs` from config instead of hardcoded 300s - Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern) - Reuse `normalize_schema_strict()` for Codex tool definitions - Add warning log for dropped image attachments - Add doc comments on `list_models()` and `include` field - Add `OPENAI_CODEX_API_URL` to `.env.example` - Fix codex error message in `create_llm_provider()` for clarity - Revert unrelated `.worktrees` addition to `.gitignore` - Update `src/llm/CLAUDE.md` with Codex provider docs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback and harden OpenAI Codex provider (takeover nearai#744) Security: - Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and OPENAI_CODEX_API_URL, matching the pattern used by all other base URL configs (regression test for nearai#1103 included) Correctness: - Add missing cache_write_multiplier() and cache_read_discount() trait delegation in TokenRefreshingProvider - Cap device-code polling backoff at 60s to prevent unbounded interval growth on repeated 429 responses - Default expires_in to 3600s when server returns 0, preventing immediately-expired sessions - Fix pre-existing SseEvent::JobResult missing fallback_deliverable field in job_monitor.rs tests Cleanup: - Extract duplicated make_test_jwt() and test_codex_config() into shared codex_test_helpers module Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback on OpenAI Codex provider (nearai#1461) - Login command now resolves OPENAI_CODEX_* env overrides even when LLM_BACKEND isn't set to openai_codex (Copilot review) - Setup wizard "Keep current provider?" for codex no longer re-triggers device code login — mirrors Bedrock's keep-and-return pattern (Copilot) - Revert provider init log from info back to debug (Copilot) - Add warning log when token expires_in=0, before defaulting to 3600s (Gemini review) Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Sanjeev Suresh <Sanjeev-S@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
…earai#1461) * feat(llm): add OpenAI Codex backend config and OAuth session manager Add OpenAiCodex as a new LLM backend variant with config for auth endpoint, API base URL, client ID, and session persistence path. The session manager implements OpenAI's device code auth flow (headless-friendly, no browser required on the server) with automatic token refresh, following the same persistence pattern as the existing NEAR AI session manager. Closes nearai#742 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): add Responses API client and token-refreshing decorator Native Responses API client for chatgpt.com/backend-api/codex/responses, the endpoint that works with ChatGPT subscription tokens. Handles SSE streaming, text completions, and tool call round-trips. Token-refreshing decorator wraps the provider to pre-emptively refresh OAuth tokens before API calls and retry once on auth failures. Reports zero cost since billing is through subscription. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(llm): wire OpenAI Codex into provider factory, CLI, and setup wizard Connect the new provider to the LLM factory, add openai_codex to the CLI --backend flag, and add it as an option in the onboarding wizard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(llm): address PR nearai#744 review feedback (20 items) Review fixes for the OpenAI Codex provider PR: - Remove dead `generate_pkce()` code (device flow gets PKCE from server) - Fix `refresh_tokens()` to use `.form()` instead of `.json()` per OAuth spec - Inline codex dispatch into `build_provider_chain()` (single async function, no separate `assemble_provider_chain()` helper — matches main's pattern) - Remove Clone from `OpenAiCodexSession`, restrict fields to `pub(crate)` - Propagate HTTP client builder error instead of silent fallback - Redact device code response body from debug log - Change `set_model()` in TokenRefreshingProvider to delegate to inner - Replace hardcoded `/tmp/` test path with `tempfile::tempdir()` - Accept `request_timeout_secs` from config instead of hardcoded 300s - Parse `Retry-After` header on 429 responses (matches nearai_chat.rs pattern) - Reuse `normalize_schema_strict()` for Codex tool definitions - Add warning log for dropped image attachments - Add doc comments on `list_models()` and `include` field - Add `OPENAI_CODEX_API_URL` to `.env.example` - Fix codex error message in `create_llm_provider()` for clarity - Revert unrelated `.worktrees` addition to `.gitignore` - Update `src/llm/CLAUDE.md` with Codex provider docs [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address review feedback and harden OpenAI Codex provider (takeover nearai#744) Security: - Add SSRF validation (validate_base_url) on OPENAI_CODEX_AUTH_URL and OPENAI_CODEX_API_URL, matching the pattern used by all other base URL configs (regression test for nearai#1103 included) Correctness: - Add missing cache_write_multiplier() and cache_read_discount() trait delegation in TokenRefreshingProvider - Cap device-code polling backoff at 60s to prevent unbounded interval growth on repeated 429 responses - Default expires_in to 3600s when server returns 0, preventing immediately-expired sessions - Fix pre-existing SseEvent::JobResult missing fallback_deliverable field in job_monitor.rs tests Cleanup: - Extract duplicated make_test_jwt() and test_codex_config() into shared codex_test_helpers module Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review feedback on OpenAI Codex provider (nearai#1461) - Login command now resolves OPENAI_CODEX_* env overrides even when LLM_BACKEND isn't set to openai_codex (Copilot review) - Setup wizard "Keep current provider?" for codex no longer re-triggers device code login — mirrors Bedrock's keep-and-return pattern (Copilot) - Revert provider init log from info back to debug (Copilot) - Add warning log when token expires_in=0, before defaulting to 3600s (Gemini review) Co-Authored-By: Sanjeev-S <Sanjeev-S@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Sanjeev Suresh <Sanjeev-S@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Continuation of #744 by @Sanjeev-S.
Adds
openai_codexas a new LLM backend so ChatGPT Plus/Pro subscribers can use IronClaw without a separate API key. Includes OAuth device code login, native Responses API client with SSE parsing and tool call round-trips, and a token-refreshing LlmProvider decorator.Changes from original
staging(resolved 4 conflicts:app.rs,CLAUDE.md,mod.rs,wizard.rs)OPENAI_CODEX_AUTH_URLandOPENAI_CODEX_API_URLviavalidate_base_url()— every other base URL was validated except these two ([HIGH] SSRF risk via configurable embedding base URL without validation #1103)cache_write_multiplier()andcache_read_discount()trait delegation inTokenRefreshingProviderexpires_into 3600s when server returns 0, preventing immediately-expired sessionsmake_test_jwt()andtest_codex_config()into sharedcodex_test_helpersmoduleSseEvent::JobResultmissingfallback_deliverablefield injob_monitor.rstestsOriginal PR
#744 — feat(llm): Add OpenAI Codex (ChatGPT subscription) as LLM provider
Review comments addressed
All review comments from @zmanian and @ilblackdragon were already resolved by the original author in commit bd588e0. The remaining blocker was merge staleness (9 days behind staging) and the SSRF validation gap identified during takeover review.
Test plan
cargo fmt— cleancargo clippy --all --benches --tests --examples --all-features— zero warningsCo-Authored-By: Sanjeev-S Sanjeev-S@users.noreply.github.com
Generated with Claude Code