Skip to content

feat(providers): add OpenAI Codex provider with Responses API support#128

Draft
b3nw wants to merge 9 commits intoMirrowel:devfrom
b3nw:feature/codex-all
Draft

feat(providers): add OpenAI Codex provider with Responses API support#128
b3nw wants to merge 9 commits intoMirrowel:devfrom
b3nw:feature/codex-all

Conversation

@b3nw
Copy link
Copy Markdown
Contributor

@b3nw b3nw commented Feb 15, 2026

Add full Codex provider implementation with OAuth-based authentication, Responses API integration, and quota tracking.

New files:

  • codex_provider.py: Codex provider with Responses API streaming/non-streaming
  • openai_oauth_base.py: Reusable OAuth base for OpenAI-style authentication
  • codex_quota_tracker.py: Header-based quota tracking with tier detection
  • codex_prompt.txt: System prompt template for Codex interactions

Integration points:

  • Register codex in credential_manager, credential_tool, provider_factory
  • Inject usage manager references into providers that support it
  • Skip limitless quota groups (rotation-only) in TUI summary/detail views

Important

Adds OpenAI Codex provider with OAuth authentication, Responses API support, and quota tracking.

  • Behavior:
    • Adds CodexProvider in codex_provider.py for OpenAI Codex models with OAuth authentication and Responses API.
    • Supports streaming and non-streaming responses, reasoning output, and tool calling.
    • Integrates quota tracking via CodexQuotaTracker.
  • Authentication:
    • Implements OAuth2 with PKCE in openai_oauth_base.py for token management.
    • Supports token refresh and API key exchange.
  • Quota Management:
    • Adds codex_quota_tracker.py for tracking rate limits and credits.
    • Updates quota from API headers and stores baselines in UsageManager.
  • Integration:
    • Registers Codex in provider_factory.py and updates credential management.
    • Modifies quota_viewer.py to skip limitless quota groups in views.

This description was created by Ellipsis for 993793c. You can customize this summary. It will automatically update as commits are pushed.

@b3nw
Copy link
Copy Markdown
Contributor Author

b3nw commented Feb 15, 2026

@mirrobot-agent gimme a review ya lazy bot

@mirrobot-agent
Copy link
Copy Markdown
Contributor

@b3nw, I'm on it! I'll dive into the Codex provider implementation and give you a thorough review. 🕵️‍♂️

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Overall Assessment

The OpenAI Codex provider implementation is exceptionally thorough and well-integrated into the existing rotator architecture. The move to the Responses API allows for advanced features like reasoning output and streaming tool calls, which are critical for agentic workflows. The OAuth implementation with PKCE is robust and handles headless environments elegantly.

Architectural Feedback

  • Separation of Concerns: The use of mixins (OpenAIOAuthBase, CodexQuotaTracker) keeps the main CodexProvider class focused on request/response translation.
  • Resilience: The token refresh logic and re-auth coordinator handle credential expiration gracefully.
  • Quota Tracking: Using virtual models (_5h_window, _weekly_window) is a clever way to expose Codex's unique rate limit windows to the TUI.

Key Suggestions

  • Error Handling: Refine the generic exceptions in the streaming loop to include more context (status codes, etc.).
  • Fragile Parsing: Move away from regex-based retry-after extraction if the API provides structured alternatives.
  • Backend API usage: Double-check the stability/longevity of the chatgpt.com/backend-api endpoints as they appear to be internal.

Questions for the Author

  • GPT-5 Models: Are the gpt-5 model names intended as placeholders or are they targeting a specific internal preview?
  • System Message Conversion: Is the conversion of system messages to user messages a strict requirement of the Responses API, or a strategy to bypass the fixed instructions field?

This review was generated by an AI assistant.

error_message = error.get("message", "Response failed")

if error_message:
raise Exception(f"Codex response failed: {error_message}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider using a more specific exception type here, or including the HTTP status code if available, to help with debugging and error handling downstream.


# Try to extract from message
import re
match = re.search(r"try again in (\d+)s", message)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Parsing rate limit retry times from strings can be fragile. If the API provides a Retry-After header or a structured field in the JSON error response, those would be safer to use.

# - If base contains /backend-api: use /wham/usage (ChatGptApi style)
# - Otherwise: use /api/codex/usage (CodexApi style)
# Since we use chatgpt.com/backend-api, we need /wham/usage
CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like a internal backend API endpoint. Ensure that the proxy is intended to hit this directly and that there are no public-facing alternatives that might be more stable.

if item.get("type") == "function_call":
call_id = item.get("call_id") or item.get("id", "")
name = item.get("name", "")
arguments = item.get("arguments", "")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

User-provided system messages are converted to user messages here. While this ensures compatibility with the Responses API's strict instruction field, it might affect models that rely heavily on the system role for personality/constraints. It's worth confirming if this is the intended behavior for all Codex-style models.

@mirrobot-agent
Copy link
Copy Markdown
Contributor

@b3nw, I've finished the review! Overall, it's a very solid implementation. I've left some detailed feedback and a few questions in the formal review. Let me know if you want me to look into anything else! 🚀

@Mirrowel Mirrowel added enhancement New feature or request Agent Monitored Monitored for AI Agent to review PR's and commits Priority labels Feb 15, 2026
@mirrobot-agent
Copy link
Copy Markdown
Contributor

I'm starting my review of the OpenAI Codex provider integration. I see some new retry logic for handling garbled tool calls in the Responses API - checking the implementation details now!

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Codex Provider & Responses API

The implementation of the OpenAI Codex provider is very thorough and the integration with the Responses API is well-handled. The latest additions introducing retry logic for garbled tool calls are a welcome step toward production-level reliability.

Assessment of New Changes:

  • Retry Logic: The introduction of _stream_with_retry and _non_stream_with_retry correctly addresses the intermittent "ChatGPT internal format" reversion issue.
  • Marker Detection: The use of the GARBLED_TOOL_CALL_MARKER is a practical heuristic for detecting when the model has entered an invalid state.

Key Suggestions:

  • Streaming Duplication (High Priority): As noted in the inline comment, retrying a stream after yielding chunks will cause duplication of the initial content (like reasoning tokens) in the client's view. This should be addressed by buffering the start of the stream or implementing a "reset" signal for the client if possible.
  • Fragmented Marker Detection: The current check might miss markers split across two network packets/chunks.
  • Cleanup: There is a minor unreachable code path in the non-streaming retry wrapper.

Note on Previous Feedback:
The previous suggestions regarding specific exception types (line 1186), fragile regex-based retry extraction (line 1242), and the stability of internal backend API endpoints still apply and should be considered for future hardening.

This review was generated by an AI assistant.

Comment on lines +713 to +760
for attempt in range(GARBLED_TOOL_CALL_MAX_RETRIES):
garbled_detected = False

try:
async for chunk in self._stream_response(
client, headers, payload, model, reasoning_compat, credential_path
):
# Inspect content deltas for garbled tool call marker
if hasattr(chunk, "choices") and chunk.choices:
choice = chunk.choices[0]
delta = getattr(choice, "delta", None)
if delta:
content = getattr(delta, "content", None) or ""
if GARBLED_TOOL_CALL_MARKER in content:
garbled_detected = True
lib_logger.warning(
f"[Codex] Garbled tool call detected in stream for {model}, "
f"attempt {attempt + 1}/{GARBLED_TOOL_CALL_MAX_RETRIES}. "
f"Content snippet: {content[:100]!r}"
)
break # Stop consuming this stream

yield chunk

if not garbled_detected:
return # Stream completed successfully

except Exception:
if garbled_detected:
# Exception during stream teardown after garble detected - continue to retry
pass
else:
raise # Non-garble exception - propagate

# Garbled stream detected - retry if we have attempts left
if attempt < GARBLED_TOOL_CALL_MAX_RETRIES - 1:
lib_logger.info(
f"[Codex] Retrying request for {model} after garbled tool call "
f"(attempt {attempt + 2}/{GARBLED_TOOL_CALL_MAX_RETRIES})"
)
await asyncio.sleep(GARBLED_TOOL_CALL_RETRY_DELAY)
else:
lib_logger.error(
f"[Codex] Garbled tool call persisted after {GARBLED_TOOL_CALL_MAX_RETRIES} "
f"attempts for {model}. Giving up."
)
# Final attempt already yielded whatever chunks it had
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There is a significant duplication risk in the streaming retry logic. Since yield chunk occurs inside the same loop that detects the garbled marker, any chunks yielded before the marker is found (e.g., reasoning tokens or early valid text) will be sent to the client. When the request is retried, the client will receive those same chunks again from the new stream, leading to duplicated content in the UI.

Consider buffering the early chunks or only retrying if no content has been yielded yet. If retrying mid-stream is unavoidable, the client should be notified to clear the previous partial response.

delta = getattr(choice, "delta", None)
if delta:
content = getattr(delta, "content", None) or ""
if GARBLED_TOOL_CALL_MARKER in content:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checking for GARBLED_TOOL_CALL_MARKER in individual chunks may fail if the marker (+#+#) is split across two consecutive chunks (e.g., +# followed by +#).

Consider maintaining a small rolling buffer of the last few characters to ensure the marker is detected even when fragmented.

return response

# Should not reach here, but return the last response as fallback
return response
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line is unreachable because the return response at line 805 is inside the loop and covers the final iteration fallback.

@b3nw b3nw force-pushed the feature/codex-all branch from 41085fe to f3739c2 Compare February 15, 2026 16:07
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Anthropic & Cursor Integration

The PR continues to expand with robust implementations for Anthropic (OAuth + API key) and Cursor providers. The new Log Viewer TUI is also a great addition for observability.

Assessment of New Changes:

  • Anthropic Provider: The dual routing (OAuth/API key) is well-handled. The interleaved thinking support is correctly implemented.
  • Cursor Provider: Solid integration with the sidecar and clever usage of a virtual model for quota tracking.
  • Log Viewer: A very functional addition. The lazy loading of metadata and prompts is a good choice for performance.
  • Request Transforms: Centralized transforms for Kimi and GLM-5/4 help with model-specific quirks.

Note on Previous Feedback:
The critical issues raised in the previous review for src/rotator_library/providers/codex_provider.py (specifically the Streaming Duplication risk on line 760 and the Fragmented Marker detection on line 726) still apply as the file has not been modified in the latest commits. These should be addressed to ensure reliability in production scenarios.

This review was generated by an AI assistant.

ANTHROPIC_MESSAGES_ENDPOINT = f"{ANTHROPIC_API_BASE}/v1/messages"

# Required headers for OAuth requests
ANTHROPIC_BETA_HEADER = "oauth-2025-04-20,interleaved-thinking-2025-05-14"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The date 2025-05-14 in the ANTHROPIC_BETA_HEADER seems very far in the future. Is this a placeholder or a requirement for a specific private beta?

ANTHROPIC_USER_AGENT = "claude-cli/2.1.2 (external, cli)"

# Tool name prefix for OAuth path
TOOL_PREFIX = "mcp_"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why is the TOOL_PREFIX set to mcp_? If this refers to "Model Context Protocol", it might be worth adding a comment explaining the choice, as it might affect client expectations for tool naming.

from rich.syntax import Syntax


def _get_logs_dir() -> Path:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This function re-implements logic already available in src/rotator_library/utils/paths.py. It is better to import get_logs_dir from there to ensure consistency and avoid duplication.

@b3nw b3nw force-pushed the feature/codex-all branch from f3739c2 to 8b094e3 Compare February 15, 2026 18:39
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Codex Garble Handling Refinements

The latest updates to the Codex provider significantly harden the garbled tool call detection and retry logic. The move to a buffer-then-flush strategy for streaming is a particularly robust fix for the duplication risks previously identified.

Assessment of New Changes:

  • Robust Detection: Expanding GARBLED_TOOL_CALL_MARKERS and implementing accumulated_text tracking ensures that garbled output is caught even when markers are split across chunks.
  • Streaming Reliability: The buffer-then-flush approach in _stream_with_retry is a great solution. While it adds latency to the initial chunk, it guarantees that no "corrupted" or duplicated content reaches the client.
  • Diagnostics: The new logging for response.completed will be very helpful for tracking down why the model sometimes reverts to ChatML-style tool calls.

Note on Previous Feedback:

  • Resolved: The high-priority issue regarding Streaming Duplication and the concern about Fragmented Markers have been successfully addressed.
  • Still Applies: The minor unreachable code path in _non_stream_with_retry (line 863) still exists. Additionally, the previous suggestions regarding specific exception types (line 1255 of codex_provider.py in the full file) and regex-based retry extraction in the quota tracker remain relevant for future hardening.

This review was generated by an AI assistant.

return response

# Should not reach here, but return the last response as fallback
return response
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line remains unreachable because the return response at line 860 covers all exit paths from the loop. While minor, removing it would clean up the implementation.

Comment on lines +728 to +743
Uses a buffer-then-flush approach: all chunks are collected first,
then checked for the garbled marker. Only if the stream is clean
are chunks yielded to the caller. This allows true retry since
no chunks have been sent to the HTTP client yet.

Detection is done both per-chunk (for early abort) AND on the
accumulated text after stream completion (to catch markers that
are split across multiple SSE chunks).
"""
for attempt in range(GARBLED_TOOL_CALL_MAX_RETRIES):
garbled_detected = False
buffered_chunks: list = []
accumulated_text = "" # Track all text content across chunks

try:
async for chunk in self._stream_response(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The buffer-then-flush strategy effectively addresses the streaming duplication risk identified in the previous review. Note that this does mean real-time streaming is disabled for the client as they must wait for the entire response to be generated before any chunks are yielded. Given the intermittent nature of the Responses API garbling, this seems like the correct trade-off for reliability.

buffered_chunks.append(chunk)

# Post-stream check: inspect accumulated text for markers split across chunks
if not garbled_detected and _is_garbled_tool_call(accumulated_text):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using accumulated_text for a post-stream check is a great addition to catch garble markers that might be split across network packets. This addresses the fragmented marker concern from the previous review.

@b3nw b3nw force-pushed the feature/codex-all branch from 8b094e3 to 64c0a37 Compare February 15, 2026 18:40
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review

This update focuses on cleaning up diagnostic logging within the Codex provider's streaming logic.

Assessment of New Changes:

  • Log Cleanup: The removal of the verbose lib_logger.info block in the response.completed handler is a sensible refinement now that the Responses API integration is stable.

Overall Status:
The implementation remains robust. The core logic for handling garbled tool calls and the buffer-then-flush streaming strategy are well-executed.

Note on Previous Feedback:
The following items from previous reviews remain unaddressed:

  • codex_provider.py: The unreachable code path at line 863 and the use of generic exceptions at lines 1123 and 1242.
  • codex_quota_tracker.py: The recommendation to favor structured retry-after data over regex parsing where the API allows.
  • anthropic_provider.py: The questions regarding the future-dated beta header (2025-05-14) and the choice of the mcp_ tool prefix.
  • log_viewer.py: The suggestion to use the centralized get_logs_dir utility for path consistency.

Great work on the cleanup!

This review was generated by an AI assistant.

@b3nw b3nw force-pushed the feature/codex-all branch from 64c0a37 to 73b27bf Compare February 15, 2026 22:04
@b3nw
Copy link
Copy Markdown
Contributor Author

b3nw commented Feb 15, 2026

@mirrobot-agent major cleanup and fixed a stall issue around instructions.

Repository owner deleted a comment from mirrobot-agent bot Feb 16, 2026
Repository owner deleted a comment from b3nw Feb 16, 2026
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Codex Implementation Refined

The latest changes significantly harden the OpenAI Codex provider. The refactoring of instruction handling and the introduction of a robust streaming retry strategy address the most critical stability concerns raised in previous reviews.

Assessment of New Changes:

  • Retry Logic: The buffer-then-flush approach in _stream_with_retry successfully eliminates the risk of yielding duplicated or garbled content to the client.
  • Instruction Handling: Utilizing the instructions field of the Responses API directly (with proper fallbacks) is a much cleaner approach than converting system messages to user messages and appears to have resolved the stall issues.
  • Error Handling: Replacing generic Exception calls with StreamedAPIError allows the rotator library to correctly classify and handle model-generated errors.
  • Cleanup: Unreachable code paths have been removed, and the overall provider logic is more concise.

Key Suggestions:

  • Regex Parsing (Minor): The parse_quota_error method still relies on regex for retry-after extraction. While functional, favoring headers (if the Responses API eventually provides them) would be more robust.
  • Scope Clarification: I noticed that the Anthropic and Cursor providers, along with the Log Viewer, were deleted in this update. I assume this is a deliberate move to narrow the PR scope to Codex only, but please confirm if these were intended to be removed.

Overall Status:
The Codex implementation is now very stable and follows project conventions well. The addition of the bot management infrastructure also looks like a great step forward for the repository's maintenance.

This review was generated by an AI assistant.

accumulated text after stream completion (to catch markers that
are split across multiple SSE chunks).
"""
for attempt in range(GARBLED_TOOL_CALL_MAX_RETRIES):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The buffer-then-flush strategy in _stream_with_retry is a robust solution to the streaming duplication risk. It ensures that no garbled content is yielded to the client before detection is complete.

# The Codex Responses API requires the 'instructions' field — it's non-optional.
# Always include it; fall back to the Codex system instruction if nothing else.
if not instructions:
instructions = CODEX_SYSTEM_INSTRUCTION
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The refactored instructions handling properly utilizes the Responses API field and includes a fallback to CODEX_SYSTEM_INSTRUCTION. This should effectively address the stall issues previously identified.

error = evt.get("response", {}).get("error", {})
error_msg = error.get("message", "Response failed")
lib_logger.error(f"Codex response failed: {error_msg}")
raise StreamedAPIError(f"Codex response failed: {error_msg}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for replacing generic exceptions with StreamedAPIError. This allows for more granular error classification in the rotator.


# Try to extract from message
import re
match = re.search(r"try again in (\d+)s", message)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This regex-based extraction still exists. While acceptable if the API doesn't provide structured retry data, consider using headers if available in the future.

@b3nw b3nw force-pushed the feature/codex-all branch from b5e5d63 to aaa59f3 Compare February 26, 2026 03:30
Replace hardcoded BASE_MODELS and REASONING_MODEL_EFFORTS with dynamic
fetching from the Codex GitHub repo's models.json at runtime.

- Fetch models from raw.githubusercontent.com/openai/codex on first access
- Cache for 1 hour (configurable via CODEX_MODELS_CACHE_TTL env var)
- Fall back to stale cache on fetch failure, then to hardcoded defaults
- Dynamic _allowed_efforts_for_model() with prefix matching for aliases
- Adds gpt-5.4, gpt-5-codex-mini, gpt-oss-120b, gpt-oss-20b automatically
- URL configurable via CODEX_MODELS_JSON_URL env var
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 6, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 09edb5fd-f43b-43d7-8021-7cba4bce9e6c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ 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.

Dynamically discovered models from GitHub models.json (like gpt-5.4)
were not in the static AVAILABLE_MODELS list, causing
get_model_quota_group() to return None. This made the executor's quota
display show '?/?' instead of actual values.

Changes:
- Override get_model_quota_group() to return 'codex-global' for any
  model, since all Codex models share the same per-account rate limits
  (same pattern as FirmwareProvider)
- Push primary window quota data to 'codex-global' group in addition
  to '5h-limit' so the executor's _get_quota_display() can find limits
  when looking up the model's quota group
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Codex Provider Cleanup

Assessment of New Changes:
The latest changes focus on cleanup and minor refinements to the Codex provider implementation. The addition of get_model_quota_group properly integrates dynamic model discovery with the quota tracking system.

Key Observations:

  • Quota Group Integration: The new get_model_quota_group method ensures that dynamically discovered Codex models (from GitHub's models.json) are properly tracked under the 'codex-global' quota group.
  • Workflow Cleanup: Several legacy workflow files and prompts have been removed, streamlining the repository.
  • Build Output Simplification: The release notes format has been simplified for better readability.

Overall Status:
The implementation remains stable. The quota group method is a sensible addition for supporting dynamic model discovery.

Note on Previous Feedback:
All critical issues from previous reviews (streaming duplication, fragmented marker detection, instruction handling) have been successfully addressed. The codebase is in good shape.

This review was generated by an AI assistant.

"""This provider uses custom logic (Responses API instead of litellm)."""
return True

def get_model_quota_group(self, model: str) -> Optional[str]:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This get_model_quota_group method returns a hardcoded string for all models. Consider adding a comment explaining why this is safe (e.g., "All Codex models share the same per-account rate limits"), or add validation if this method might be called with non-Codex models.

- Add build_env_lines() and export_credential_to_env() to OpenAIOAuthBase
  with Codex-specific fields (api_key, account_id, id_token)
- Add build_env_lines() and export_credential_to_env() to AnthropicOAuthBase
  with proper claudeAiOauth nested format handling
- Add delete_credential() to OpenAIOAuthBase for parity with other providers
- Add export_codex_to_env() and export_anthropic_to_env() functions to
  credential_tool.py
- Update export_credentials_submenu() to include Codex and Anthropic in
  individual, bulk, and combine export options
- Update combine_all_credentials() to include codex and anthropic providers
- Update credential_manager.py to discover Codex API_KEY-only credentials
  (CODEX_N_API_KEY without requiring REFRESH_TOKEN)
- Fix anthropic list_credentials() to properly parse claudeAiOauth format
mirrobot-agent[bot]
mirrobot-agent bot previously approved these changes Mar 21, 2026
Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Credential Export & Management Enhancements

Assessment of New Changes:
The latest commit adds comprehensive credential export functionality for Codex and Anthropic OAuth providers, along with improved credential detection logic. The changes enable users to export OAuth credentials to .env format and combine multiple credentials, which is a valuable addition for deployment scenarios.

Key Observations:

  • Credential Detection: The credential_manager.py changes properly handle Codex's unique ability to work with API_KEY-only credentials (exchanged from OAuth tokens)
  • Export Functions: The new export_codex_to_env() and export_anthropic_to_env() functions follow the established pattern from other providers
  • Base Class Methods: The build_env_lines() and export_credential_to_env() methods are well-implemented and handle both flat and nested credential formats
  • Consistency: The implementation maintains consistency with existing OAuth providers (Gemini CLI, Qwen Code, iFlow, Antigravity)

Overall Status:
Solid implementation that brings Codex and Anthropic credential management in line with other OAuth providers. The code is clean and follows project conventions.

Minor Suggestions:

  • Adding brief clarifying comments about Codex's unique API_KEY-only credential support would help future maintainers (see inline comments)

This review was generated by an AI assistant.

if refresh_key in self.env_vars and self.env_vars[refresh_key]:
found_indices.add(index)

# For Codex provider, also check for API_KEY-only credentials
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Consider expanding this comment slightly to explain why Codex is unique in supporting API_KEY-only credentials. A note like 'Codex exchanges OAuth tokens for persistent API keys, unlike other OAuth providers' would help future maintainers understand this is Codex-specific behavior.

# Use "0" as the index for legacy single credential
found_indices.add("0")

# For Codex, also accept legacy API_KEY-only format
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same suggestion: adding a brief comment explaining that this legacy API_KEY-only format is specific to Codex's token exchange mechanism would clarify why other OAuth providers don't have this fallback.

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Overall Assessment

This is a focused, high-impact fix that addresses a real-world scenario: the same email authenticating to multiple OpenAI workspaces/organizations. The addition of account_id matching prevents credential file collisions and the improved logging makes the credential lifecycle much clearer.

Architectural Feedback

The approach is sound and fits well within the existing OAuth credential management pattern. The change is minimal and targeted, which reduces the risk of introducing regressions.

Key Suggestions

  • Edge Case Handling: Consider the scenario where account_id might be missing from some existing credential files. The current strict matching could lead to unexpected behavior if one file has account_id and another doesn't (both for the same email).
  • Code Duplication: The account_id extraction appears in multiple places. Consolidating this would improve maintainability.

Questions for the Author

  • Missing account_id: What should happen when the OAuth response doesn't include an account_id? Should the system fall back to email-only matching, or treat it as a separate account?

This review was generated by an AI assistant.

Comment on lines +1004 to +1046
def _find_existing_credential_by_email(
self,
email: str,
base_dir: Optional[Path] = None,
account_id: Optional[str] = None,
) -> Optional[Path]:
"""Find an existing credential file for the given email and account.

When ``account_id`` is provided the match requires **both** the email
and the account_id to be equal. This prevents credentials for the same
email on different OpenAI workspaces / organisations from colliding.
"""
if base_dir is None:
base_dir = self._get_oauth_base_dir()

prefix = self._get_provider_file_prefix()
pattern = str(base_dir / f"{prefix}_oauth_*.json")

for cred_file in glob(pattern):
try:
with open(cred_file, "r") as f:
creds = json.load(f)
metadata = creds.get("_proxy_metadata", {})
existing_email = metadata.get("email")
if existing_email != email:
continue

# If an account_id was supplied, require it to match as well
if account_id is not None:
existing_account_id = (
creds.get("account_id")
or metadata.get("account_id")
)
if existing_account_id != account_id:
continue

return Path(cred_file)
except Exception:
continue

return None

def _get_next_credential_number(self, base_dir: Optional[Path] = None) -> int:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The addition of account_id matching is a solid fix for preventing credential collisions across workspaces. However, consider what happens when account_id is None for one credential file but present for another with the same email - the current logic would treat them as different credentials. Is this the intended behavior, or should there be a fallback to match by email only when account_id is unavailable?

Comment on lines +1097 to +1130
success=False, error="Could not retrieve email from OAuth response"
)

# Extract account_id so we can scope the duplicate check to the
# specific workspace / organisation. Without this, the same email
# authenticating to two different workspaces would overwrite the
# first credential file.
account_id = new_creds.get("account_id") or new_creds.get(
"_proxy_metadata", {}
).get("account_id")

existing_path = self._find_existing_credential_by_email(
email, base_dir, account_id=account_id
)
is_update = existing_path is not None

if is_update:
file_path = existing_path
lib_logger.info(
f"Updating existing credential at '{Path(file_path).name}' "
f"for {email} (account {account_id})"
)
else:
file_path = self._build_credential_path(base_dir)
lib_logger.info(
f"Creating new credential at '{Path(file_path).name}' "
f"for {email} (account {account_id})"
)

await self._save_credentials(str(file_path), new_creds)

return CredentialSetupResult(
success=True,
file_path=str(file_path),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The logging improvements are helpful, but the account_id extraction logic is duplicated. Consider extracting this to a helper method or computing it once before the conditional branch to avoid redundancy.

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Workspace & Account Display Enhancements

Assessment of New Changes:
The additions to display workspace title, plan type, and account ID in the credential setup flow are valuable improvements for user transparency. The enhanced organization extraction logic from JWT claims is well-implemented and handles the common case of finding the default organization correctly.

Key Suggestions:

  • Edge Case Handling: The organization fallback logic could be more defensive against malformed JWT claims (see inline comment).

Overall Status:
Solid incremental improvements that enhance the user experience when managing multiple OpenAI workspaces. The changes follow existing patterns and integrate cleanly with the Codex credential flow.

Note on Previous Feedback:
All critical issues from previous reviews have been addressed. This PR is in good shape.

This review was generated by an AI assistant.

Comment on lines +840 to +856
api_key = None
if org_id and project_id:
try:
api_key = await self._exchange_for_api_key(
client, token_data.get("id_token", "")
)
new_creds["api_key"] = api_key
except Exception as e:
lib_logger.warning(f"API key exchange failed: {e}")

new_creds["_proxy_metadata"] = {
"email": email,
"account_id": account_id,
"org_id": org_id,
"project_id": project_id,
"plan_type": plan_type,
"workspace_title": workspace_title,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The logic for finding the default organization is good, but consider adding a safeguard for edge cases. If organizations exists but none have is_default=True and the first item is not a dict, line 855 could raise an IndexError or TypeError.

Suggested change
api_key = None
if org_id and project_id:
try:
api_key = await self._exchange_for_api_key(
client, token_data.get("id_token", "")
)
new_creds["api_key"] = api_key
except Exception as e:
lib_logger.warning(f"API key exchange failed: {e}")
new_creds["_proxy_metadata"] = {
"email": email,
"account_id": account_id,
"org_id": org_id,
"project_id": project_id,
"plan_type": plan_type,
"workspace_title": workspace_title,
if not workspace_title and organizations and isinstance(organizations[0], dict):
workspace_title = organizations[0].get("title", "")

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Workspace & Plan Display Enhancement

Assessment of New Changes:

  • TUI Enhancement: The addition of workspace, plan, and account ID columns to the Codex credential display is a valuable UX improvement. Users can now quickly identify which workspace/organization a credential belongs to.
  • Metadata Completeness: Including plan_type and workspace_title in the credential metadata ensures these fields are available for display and future features.
  • Display Truncation: The account ID truncation logic (showing first 8 chars + "...") is a sensible choice for readability.

Overall Status:
This is a solid incremental improvement that enhances transparency in the credential management flow. The implementation follows existing patterns from other providers (gemini_cli, antigravity).

Note on Previous Feedback:

  • Resolved: The workspace/plan display functionality has been successfully implemented.
  • Still Applies: The minor safeguard suggestion for the organization fallback logic (line 836) remains unaddressed. While the edge case is unlikely (malformed JWT claims), adding the defensive check would improve robustness.

The PR continues to be in good shape and the Codex implementation is stable.

This review was generated by an AI assistant.

break
if not workspace_title and isinstance(organizations[0], dict):
workspace_title = organizations[0].get("title", "")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The previous suggestion about adding a safeguard for edge cases still applies here. If organizations exists but the first item is not a dict, this could raise a TypeError.

Suggested change
if not workspace_title and organizations and isinstance(organizations[0], dict):
workspace_title = organizations[0].get("title", "")

@b3nw b3nw force-pushed the feature/codex-all branch from 7528879 to 609d9dd Compare March 22, 2026 16:19
@mirrobot-agent
Copy link
Copy Markdown
Contributor

I'm starting my follow-up review of the Codex credential display enhancements. Checking the TUI changes now!

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Codex Credential Display Enhancement

Assessment of New Changes:
The addition of workspace, plan, and account ID columns to the Codex credential detail view is a solid UX improvement. Users can now quickly distinguish between credentials from different OpenAI workspaces/organizations, which is especially valuable given the multi-tenant nature of Codex access.

Key Observations:

  • Implementation Quality: The changes follow the established pattern from other OAuth providers (gemini_cli, antigravity)
  • Backward Compatibility: The use of .get() with fallbacks handles older credential files correctly
  • Display Truncation: Account ID truncation (8 chars + "...") maintains readability

Minor Suggestions:

  • Add brief comments noting backward compatibility for future maintainers (see inline comments)

Overall Status:
This is a clean, focused improvement that enhances transparency in credential management. The implementation is sound and follows project conventions.

Note on Previous Feedback:
All critical issues from previous reviews have been addressed. The one remaining minor suggestion from the last review (organization fallback safeguard in openai_oauth_base.py:836) is unrelated to these changes and can be addressed separately.

This review was generated by an AI assistant.

table.add_column("Plan", style="magenta")
table.add_column("Account ID", style="dim")
# Add type column for iFlow (OAuth vs Cookie)
elif provider_name == "iflow":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The workspace/plan/account columns are added here for Codex. Consider adding a comment noting backward compatibility - older credential files created before these fields were added will show '-' which is fine, but future maintainers might wonder why.

if project and len(project) > 25:
project = project[:22] + "..."
table.add_row(str(i), file_name, email, tier, project or "-")
elif provider_name == "codex":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here - the .get() fallbacks handle missing fields correctly. A brief comment like # Backward compatible: older credentials may lack workspace/plan fields would help future maintainers.

Suggested change
elif provider_name == "codex":
elif provider_name == "codex":
# Backward compatible: older credentials may lack workspace/plan fields
workspace = cred.get("workspace_title", "-") or "-"

@mirrobot-agent
Copy link
Copy Markdown
Contributor

I'm starting my follow-up review of the Qwen Code provider model additions. Let me check what's changed!

Copy link
Copy Markdown
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Follow-up Review: Qwen Code Model Addition

Assessment of New Changes:

  • Model Addition: The addition of coder-model (Qwen 3.5 Plus) is a simple, low-risk change that expands available model options.
  • Documentation: The expanded inline comment helpfully clarifies what each model represents.

Overall Status:
This is a minor, focused change that follows existing patterns. The implementation is sound.

This review was generated by an AI assistant.

@b3nw b3nw force-pushed the feature/codex-all branch from 36612fd to 245e56e Compare March 22, 2026 16:24
@mirrobot-agent
Copy link
Copy Markdown
Contributor

Minor suggestion for (line 30):

Adding to the hardcoded list is straightforward, but consider adding a comment clarifying that this is an alias for Qwen 3.5 Plus, since the naming convention differs from the other models (not format).

@mirrobot-agent
Copy link
Copy Markdown
Contributor

Minor suggestion for src/rotator_library/providers/qwen_code_provider.py (line 30):

Adding coder-model to the hardcoded list is straightforward, but consider adding a comment clarifying that this is an alias for Qwen 3.5 Plus, since the naming convention differs from the other models (not qwen3-* format).

…fferent workspaces

The _find_existing_credential_by_email() method was matching credentials
solely by email address. When the same email authenticates to two different
OpenAI workspaces/accounts (each with a different account_id), the second
authentication would overwrite the first credential file instead of
creating a new one.

Changes:
- _find_existing_credential_by_email() now accepts an optional account_id
  parameter. When provided, both email AND account_id must match for a
  credential to be considered a duplicate.
- setup_credential() extracts the account_id from the new OAuth response
  and passes it to the duplicate check.
- Extract workspace/organization title from JWT claims and persist in
  credential metadata.
- Display workspace title, plan type, and account_id in the credential
  setup success panel and credential listing tables.
@b3nw b3nw force-pushed the feature/codex-all branch from 245e56e to 727d0f4 Compare March 22, 2026 23:56
@VooDisss
Copy link
Copy Markdown

@b3nw will this get merge, or you're all out of mana by now?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Agent Monitored Monitored for AI Agent to review PR's and commits enhancement New feature or request Priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants