Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,65 @@
# Default: 8085 (same as Gemini CLI, shared)
# ANTIGRAVITY_OAUTH_PORT=8085

# ------------------------------------------------------------------------------
# | [CODEX] OpenAI Codex Provider Configuration |
# ------------------------------------------------------------------------------
#
# Codex provider uses OAuth authentication with OpenAI's ChatGPT backend API.
# Credentials are stored in oauth_creds/ directory as codex_oauth_*.json files.
#

# --- Reasoning Effort ---
# Controls how much "thinking" the model does before responding.
# Higher effort = more thorough reasoning but slower responses.
#
# Available levels (model-dependent):
# - low: Minimal reasoning, fastest responses
# - medium: Balanced (default)
# - high: More thorough reasoning
# - xhigh: Maximum reasoning (gpt-5.2, gpt-5.2-codex, gpt-5.3-codex, gpt-5.1-codex-max only)
#
# Can also be controlled per-request via:
# 1. Model suffix: codex/gpt-5.2:high
# 2. Request param: "reasoning_effort": "high"
#
# CODEX_REASONING_EFFORT=medium

# --- Reasoning Summary ---
# Controls how reasoning is summarized in responses.
# Options: auto, concise, detailed, none
# CODEX_REASONING_SUMMARY=auto

# --- Reasoning Output Format ---
# How reasoning/thinking is presented in responses.
# Options:
# - think-tags: Wrap in <think>...</think> tags (default, matches other providers)
# - raw: Include reasoning as-is
# - none: Don't include reasoning in output
# CODEX_REASONING_COMPAT=think-tags

# --- Identity Override ---
# When true, injects an override that tells the model to prioritize
# user-provided system prompts over the required opencode instructions.
# CODEX_INJECT_IDENTITY_OVERRIDE=true

# --- Instruction Injection ---
# When true, injects the required opencode system instruction.
# Only disable if you know what you're doing (API may reject requests).
# CODEX_INJECT_INSTRUCTION=true

# --- Empty Response Handling ---
# Number of retry attempts when receiving empty responses.
# CODEX_EMPTY_RESPONSE_ATTEMPTS=3

# Delay (seconds) between empty response retries.
# CODEX_EMPTY_RESPONSE_RETRY_DELAY=2

# --- OAuth Configuration ---
# OAuth callback port for Codex interactive authentication.
# Default: 8086
# CODEX_OAUTH_PORT=8086

# ------------------------------------------------------------------------------
# | [ADVANCED] Debugging / Logging |
# ------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
8 changes: 8 additions & 0 deletions src/rotator_library/client/rotating_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ async def initialize_usage_managers(self) -> None:
lib_logger.info(
f"Usage managers initialized: {', '.join(sorted(summaries))}"
)

# Inject usage manager references into providers that support it
# (e.g., CodexProvider via CodexQuotaTracker for header-based quota updates)
for provider, manager in self._usage_managers.items():
instance = self._get_provider_instance(provider)
if instance and hasattr(instance, "set_usage_manager"):
instance.set_usage_manager(manager)

self._usage_initialized = True

async def close(self):
Expand Down
18 changes: 18 additions & 0 deletions src/rotator_library/credential_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ def _discover_env_oauth_credentials(self) -> Dict[str, List[str]]:
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.

# Codex can exchange OAuth tokens for persistent API keys, so
# CODEX_N_API_KEY alone (without a refresh token) is valid
if provider == "codex":
api_key_pattern = re.compile(rf"^{env_prefix}_(\d+)_API_KEY$")
for key in self.env_vars.keys():
match = api_key_pattern.match(key)
if match:
index = match.group(1)
if index not in found_indices and self.env_vars[key]:
found_indices.add(index)

# Check for legacy single credential (PROVIDER_ACCESS_TOKEN pattern)
# Only use this if no numbered credentials exist
if not found_indices:
Expand All @@ -105,6 +117,12 @@ def _discover_env_oauth_credentials(self) -> Dict[str, List[str]]:
# 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.

if not found_indices and provider == "codex":
api_key = f"{env_prefix}_API_KEY"
if api_key in self.env_vars and self.env_vars[api_key]:
found_indices.add("0")

if found_indices:
env_credentials[provider] = found_indices
lib_logger.info(
Expand Down
Loading
Loading