Skip to content

Commit 51144cc

Browse files
Pigbibicodex
andauthored
fix: degrade review gate on quota exhaustion (#55)
Co-authored-by: Codex <noreply@openai.com>
1 parent d6c92c7 commit 51144cc

4 files changed

Lines changed: 52 additions & 2 deletions

File tree

scripts/codex_audit_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ def _job_dedupe_key(payload: dict[str, Any]) -> str:
690690

691691

692692
def _classify_codex_exec_failure(text: str) -> str:
693-
if any(word in text for word in ("quota", "rate limit", "too many active", "budget")):
693+
if any(word in text for word in ("quota", "rate limit", "too many active", "budget", "usage limit")):
694694
return "quota_or_capacity_failure"
695695
return "unknown_failure"
696696

scripts/run_codex_pr_review.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,9 @@ def run_codex_service_review(prompt: str, timeout_minutes: int, complexity: str
491491
return output.strip()
492492
if status == "failed":
493493
error = str(job_payload.get("error") or "unknown failure")
494-
raise ReviewError(f"Codex service job failed: {error[:600]}")
494+
failure_category = str(job_payload.get("failure_category") or "").strip()
495+
category_suffix = f" [{failure_category}]" if failure_category else ""
496+
raise ReviewError(f"Codex service job failed{category_suffix}: {error[:600]}")
495497
if status not in {"queued", "running"}:
496498
raise ReviewError(f"Unexpected Codex service status: {status!r}")
497499

@@ -509,6 +511,11 @@ def _review_backend_is_unconfigured(exc: ReviewError) -> bool:
509511
return message == NO_REVIEW_BACKEND_CONFIGURED or "oidc repository is not allowed" in normalized
510512

511513

514+
def _review_capacity_is_unavailable(exc: ReviewError) -> bool:
515+
message = str(exc).lower()
516+
return "[quota_or_capacity_failure]" in message or "usage limit" in message
517+
518+
512519
def _api_fallback_enabled() -> bool:
513520
return parse_bool(env_value("CODEX_PR_REVIEW_API_FALLBACK_ENABLED", "true"))
514521

@@ -1260,6 +1267,16 @@ def main() -> int:
12601267
changed_line_count=len(diff.splitlines()),
12611268
)
12621269
except ReviewError as exc:
1270+
if _review_capacity_is_unavailable(exc):
1271+
print(f"::warning::Codex review unavailable due to quota or capacity: {exc}")
1272+
warning_body = (
1273+
"<!-- codex-pr-review -->\n"
1274+
"## 🤖 Codex PR Review\n\n"
1275+
"⚠️ **Review unavailable**: Codex review quota or capacity is unavailable. "
1276+
"No direct paid API fallback was used; required CI checks remain the merge gate.\n"
1277+
)
1278+
upsert_pr_comment(token, repo, pr_number, warning_body)
1279+
return 0
12631280
print(f"::error::Codex review failed: {exc}", file=sys.stderr)
12641281
warning_body = (
12651282
"<!-- codex-pr-review -->\n"

tests/test_codex_audit_service_complexity.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ def test_task_requires_async_review_and_execute(self) -> None:
117117
with self.assertRaisesRegex(ValueError, "Unsupported task='analyze'"):
118118
_service.resolve_adapter("analyze", "")
119119

120+
def test_codex_usage_limit_is_classified_as_capacity_failure(self) -> None:
121+
self.assertEqual(
122+
_service._classify_codex_exec_failure("You have reached your Codex usage limits for code reviews."),
123+
"quota_or_capacity_failure",
124+
)
125+
120126
def test_direct_api_model_for_complexity_reads_provider_specific_env(self) -> None:
121127
with mock.patch.dict(
122128
"os.environ",

tests/test_run_codex_pr_review.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,33 @@ def test_main_blocks_high_risk_on_review_infra_error(self) -> None:
358358
backend.assert_called_once()
359359
self.assertIn("Merge blocked", comment.call_args.args[3])
360360

361+
def test_main_allows_required_ci_to_gate_when_review_quota_is_unavailable(self) -> None:
362+
with tempfile.TemporaryDirectory() as tmpdir:
363+
event_path = self._write_event(tmpdir, ["src/app.py"])
364+
env = {
365+
"GH_TOKEN": "token",
366+
"GITHUB_REPOSITORY": "org/repo",
367+
"GITHUB_EVENT_PATH": event_path,
368+
"GITHUB_EVENT_NAME": "pull_request",
369+
}
370+
with (
371+
patch.dict(os.environ, env, clear=True),
372+
patch("scripts.run_codex_pr_review.fetch_pr_files", return_value=[{"filename": "src/app.py"}]),
373+
patch("scripts.run_codex_pr_review.fetch_pr_diff", return_value="diff --git a/src/app.py b/src/app.py"),
374+
patch("scripts.run_codex_pr_review.load_policy", return_value=run_codex_pr_review._default_policy()),
375+
patch("scripts.run_codex_pr_review.find_existing_review_comment", return_value=(None, "")),
376+
patch(
377+
"scripts.run_codex_pr_review.run_codex_review_with_fallback",
378+
side_effect=ReviewError("Codex service job failed [quota_or_capacity_failure]: usage limits reached"),
379+
),
380+
patch("scripts.run_codex_pr_review.upsert_pr_comment") as comment,
381+
):
382+
self.assertEqual(run_codex_pr_review.main(), 0)
383+
384+
comment.assert_called_once()
385+
self.assertIn("Review unavailable", comment.call_args.args[3])
386+
self.assertNotIn("Merge blocked", comment.call_args.args[3])
387+
361388
def test_main_skips_low_risk_docs_before_calling_review_backend(self) -> None:
362389
with tempfile.TemporaryDirectory() as tmpdir:
363390
event_path = self._write_event(tmpdir, ["docs/guide.md", "tests/test_x.py"])

0 commit comments

Comments
 (0)