Skip to content

fix(executor): auto-fallback to API key when OAuth capacity exhausted#537

Merged
RyderFreeman4Logos merged 1 commit intomainfrom
fix/gemini-api-key-fallback
Mar 31, 2026
Merged

fix(executor): auto-fallback to API key when OAuth capacity exhausted#537
RyderFreeman4Logos merged 1 commit intomainfrom
fix/gemini-api-key-fallback

Conversation

@RyderFreeman4Logos
Copy link
Copy Markdown
Owner

Summary

  • When gemini-cli ACP/OAuth transport fails with "No capacity available" or 429/quota errors, CSA now automatically retries with the stored API key from [tools.gemini-cli].api_key config
  • First attempt always uses OAuth; API key is injected only on capacity exhaustion
  • Added structured tracing for retry chain observability (auth mode, attempt count, fallback availability)
  • Test reorganization: extracted gemini fallback tests into dedicated file

Closes #533

Test plan

  • csa review with gemini-cli succeeds when OAuth has capacity (no API key injected)
  • csa review with gemini-cli auto-falls-back to API key when OAuth returns "No capacity available"
  • API key values are never logged (only has_fallback_key: bool)
  • Unit tests pass for rate limit detection, phase selection, and fallback injection

🤖 Generated with Claude Code

…y exhausted

When gemini-cli ACP transport fails with "No capacity available" or
similar rate-limit errors, CSA now automatically:
1. Detects the capacity/quota error pattern in the result
2. Reads api_key from [tools.gemini-cli] config
3. Injects GEMINI_API_KEY env var and retries ONCE
4. Logs warning when fallback is used

The first attempt always uses OAuth (no API key injected). Only on
capacity exhaustion is the stored API key used as fallback.

Closes #533

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

Code Review

This pull request updates crate versions and enhances the Gemini retry and fallback logic in the csa-executor crate. The changes introduce comprehensive tracing and warning logs for the three-phase authentication fallback mechanism (OAuth to API Key) in both LegacyTransport and AcpTransport, and refactor associated unit tests into a dedicated file. Feedback suggests refactoring the duplicated retry initialization logic into a shared handler to improve maintainability and consistency across transport methods. Additionally, a test should be updated to use a dynamic temporary directory instead of a hardcoded path to ensure robustness.

Comment on lines 600 to 636
let max_attempts = gemini_max_attempts(extra_env);
let has_fallback_key = extra_env
.is_some_and(|env| env.contains_key(csa_core::gemini::API_KEY_FALLBACK_ENV_KEY));
let auth_mode = gemini_auth_mode(extra_env).unwrap_or("unknown");
tracing::debug!(
max_attempts,
has_fallback_key,
auth_mode,
"gemini-cli ACP retry chain initialized"
);

let mut attempt = 1u8;
loop {
// Build ACP args for this attempt, injecting model override in phase 3.
let mut args = self.acp_args.clone();
if let Some(model) = gemini_retry_model(attempt) {
tracing::info!(attempt, model, "gemini-cli ACP: overriding model for retry");
args.extend(["-m".into(), model.into()]);
}

// Phase 2+: inject API key auth if available, otherwise keep original env.
let api_key_env = if gemini_should_use_api_key(attempt) {
gemini_inject_api_key_fallback(extra_env)
let injected = gemini_inject_api_key_fallback(extra_env);
if injected.is_none() {
tracing::warn!(
attempt,
auth_mode,
has_fallback_key,
"gemini-cli ACP: API key fallback unavailable for retry \
(auth_mode must be 'oauth' and _CSA_API_KEY_FALLBACK must be set); \
retrying with original auth"
);
}
injected
} else {
None
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

There's significant code duplication here for initializing the retry chain and injecting the API key. This logic is nearly identical to the implementation in LegacyTransport::execute_in (lines 241-272).

Additionally, the LegacyTransport::execute method (lines 317-359 in the full file) which also contains retry logic, has not been updated with this new tracing and fallback checking, leading to inconsistent behavior.

To improve maintainability and ensure consistency, consider refactoring this duplicated logic into a shared helper function or a dedicated struct (e.g., GeminiRetryHandler). This would centralize the retry logic and make it easier to apply to all relevant transport methods.

let result = transport
.execute_in(
"test api key fallback",
std::path::Path::new("/tmp"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using a hardcoded path like /tmp can make tests brittle as it might not be available or writable on all systems. The test already creates a temporary directory via setup_fake_gemini_environment on line 53, but it's currently unused (_temp).

Please use the provided temporary directory to make the test more robust. You'll also need to change _temp to temp on line 53.

Suggested change
std::path::Path::new("/tmp"),
temp.path(),

@RyderFreeman4Logos
Copy link
Copy Markdown
Owner Author

Audit: Bot Findings Assessment

Finding 1: Code duplication in retry chain (transport.rs:636) — Dismissed

The retry initialization between LegacyTransport and AcpTransport is structurally similar but operates in fundamentally different execution contexts (stdio process vs JSON-RPC protocol). Refactoring into a shared handler would:

  • Add coupling between transport types that are intentionally independent
  • Require complex generic abstractions for marginal DRY benefit
  • Make future transport-specific retry customization harder

The current approach favors clarity and independence over DRY. Each transport type owns its complete execution lifecycle.

Finding 2: Hardcoded /tmp in test (transport_tests_gemini_fallback.rs:64) — Accepted (pre-existing)

The /tmp usage is a pre-existing pattern from transport_tests_tail.rs, moved without modification during test reorganization. This PR is scoped to observability + fallback logic, not test infrastructure cleanup. A separate issue/PR for test portability improvements is appropriate.

🤖 Arbitrated by Claude Code orchestrator

@RyderFreeman4Logos RyderFreeman4Logos merged commit 1860087 into main Mar 31, 2026
4 of 6 checks passed
RyderFreeman4Logos added a commit that referenced this pull request Apr 3, 2026
…llback

fix(executor): auto-fallback to API key when OAuth capacity exhausted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gemini-cli: no fallback to API key when ACP capacity exhausted

1 participant