Skip to content

fix(runtime-fallback): detect prettified quota errors without HTTP status codes (fixes #2747)#2752

Merged
code-yeongyu merged 3 commits intocode-yeongyu:devfrom
MoerAI:fix/quota-error-fallback-detection
Mar 25, 2026
Merged

fix(runtime-fallback): detect prettified quota errors without HTTP status codes (fixes #2747)#2752
code-yeongyu merged 3 commits intocode-yeongyu:devfrom
MoerAI:fix/quota-error-fallback-detection

Conversation

@MoerAI
Copy link
Copy Markdown
Contributor

@MoerAI MoerAI commented Mar 23, 2026

Summary

  • Detect provider quota errors even when OpenCode prettifies them (strips HTTP status codes)
  • Add HTTP 402 (Payment Required) to default retryable status codes
  • Add quota_exceeded error type classification for pattern-based detection

Problem

When providers return quota-exhaustion errors (e.g., "Subscription quota exceeded. You can continue using free models."), OpenCode prettifies the raw HTTP error into a friendly message. This strips the HTTP status code (402 Payment Required), so the runtime-fallback hook's status-code-based detection fails. The hook never triggers the fallback chain, and the session errors out instead of switching to a fallback model.

Fix

Three-layer detection approach that works regardless of error prettification:

  1. Status code: Added HTTP 402 to retry_on_errors default list alongside 429/500/502/503/504
  2. Error type classification: Added "quota_exceeded" to classifyErrorType() that matches quota/billing/subscription patterns by message content. This type is always treated as retryable (like missing_api_key and model_not_found)
  3. Pattern expansion: Added subscription.*quota, billing.*limit, payment.*required, out of credits patterns to RETRYABLE_ERROR_PATTERNS

Changes

File Change
src/hooks/runtime-fallback/constants.ts Added HTTP 402 to retry_on_errors; added 4 new patterns to RETRYABLE_ERROR_PATTERNS
src/hooks/runtime-fallback/error-classifier.ts Added "quota_exceeded" classification in classifyErrorType(); made it always retryable in isRetryableError(); enhanced getErrorMessage() fallback extraction
src/hooks/runtime-fallback/error-classifier.test.ts Added 6 regression tests covering prettified quota errors, billing limits, capacity exhaustion, and HTTP 402

Fixes #2747


Summary by cubic

Detects provider quota/billing errors even when status codes are missing or prettified, so runtime fallback switches to a fallback model instead of failing. Fixes #2747.

  • Bug Fixes
    • Treat HTTP 402 as retryable by default.
    • Add quota_exceeded error type based on message patterns and error.name; always retry on it.
    • Expand RETRYABLE_ERROR_PATTERNS (subscription quota, billing limit, payment required, out of credits, textual "402").
    • Improve error-name message extraction; add tests for prettified and name-only errors; rename a test to reflect the quota_exceeded path.

Written for commit 44fb114. Summary will update on new commits.

@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Mar 23, 2026

I have read the CLA Document and I hereby sign the CLA

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 62d2704009

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +24 to +28
const errorObj2 = error as Record<string, unknown>
const name = errorObj2.name
if (typeof name === "string" && name.length > 0) {
const nameColonMatch = name.match(/:\s*(.+)/)
if (nameColonMatch) return nameColonMatch[1].trim().toLowerCase()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve nested retryable details when parsing colonized names

If a wrapped provider error uses a generic name like AI_APICallError: Request failed and keeps the retryable detail only in nested fields such as cause.message, this new early return prevents the old JSON.stringify(error) fallback from ever seeing that nested text. extractStatusCode() only reads numeric cause.statusCode, so shapes like { cause: { message: 'status code 503' } } now become non-retryable and the runtime-fallback hook will stop switching models for those wrapped 5xx/quota failures.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 3 files

Confidence score: 3/5

  • This lands at moderate merge risk because there is a concrete behavior gap plus a high-confidence test-validity issue, so outcomes around retry classification may be less reliable.
  • The most severe issue is in src/hooks/runtime-fallback/error-classifier.test.ts: the new test can pass via the quota_exceeded path before RETRYABLE_ERROR_PATTERNS is evaluated, creating a false positive for what it claims to validate.
  • In src/hooks/runtime-fallback/error-classifier.ts, quota_exceeded detection ignores errorName, which can miss quota-related custom error classes when message text lacks quota keywords.
  • Pay close attention to src/hooks/runtime-fallback/error-classifier.test.ts, src/hooks/runtime-fallback/error-classifier.ts - verify the test truly exercises pattern-list logic and that quota classification accounts for custom error class names.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/hooks/runtime-fallback/error-classifier.test.ts">

<violation number="1" location="src/hooks/runtime-fallback/error-classifier.test.ts:233">
P2: The new test is a false positive for pattern-list validation because `classifyErrorType` marks the same message as `quota_exceeded`, and `isRetryableError` returns true before `RETRYABLE_ERROR_PATTERNS` is evaluated.</violation>
</file>

<file name="src/hooks/runtime-fallback/error-classifier.ts">

<violation number="1" location="src/hooks/runtime-fallback/error-classifier.ts:123">
P2: `quota_exceeded` classification ignores `errorName`, so quota-related custom error classes can be missed when message text lacks quota keywords.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


test("matches subscription quota pattern in RETRYABLE_ERROR_PATTERNS", () => {
//#given
const error = { message: "Subscription quota exceeded. You can continue using free models." }
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 23, 2026

Choose a reason for hiding this comment

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

P2: The new test is a false positive for pattern-list validation because classifyErrorType marks the same message as quota_exceeded, and isRetryableError returns true before RETRYABLE_ERROR_PATTERNS is evaluated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/hooks/runtime-fallback/error-classifier.test.ts, line 233:

<comment>The new test is a false positive for pattern-list validation because `classifyErrorType` marks the same message as `quota_exceeded`, and `isRetryableError` returns true before `RETRYABLE_ERROR_PATTERNS` is evaluated.</comment>

<file context>
@@ -166,3 +166,76 @@ describe("extractStatusCode", () => {
+
+  test("matches subscription quota pattern in RETRYABLE_ERROR_PATTERNS", () => {
+    //#given
+    const error = { message: "Subscription quota exceeded. You can continue using free models." }
+
+    //#when
</file context>
Fix with Cubic

Comment thread src/hooks/runtime-fallback/error-classifier.ts
@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Mar 23, 2026

Re: issues identified by cubic.

P2 (test false positive): The test validates that isRetryableError() returns true for prettified quota errors. It deliberately tests the end-to-end behavior, not a specific internal path. Whether the match comes via classifyErrorType()quota_exceeded or via RETRYABLE_ERROR_PATTERNS is an implementation detail — the user-facing behavior (fallback triggers on quota errors) is correctly verified.

P2 (errorName not checked in quota classification): Valid enhancement suggestion. Could add errorName-based detection for custom quota error classes (e.g., QuotaExceededError). However, the primary use case (prettified string messages from OpenCode's error processor) is covered by message-based patterns. Happy to add errorName detection in a follow-up if the maintainer wants it.

@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Mar 23, 2026

Fixed both issues from cubic review:

  1. errorName-based quota detection: Added errorName checks for QuotaExceededError, InsufficientQuota, and BillingError class names in classifyErrorType(). Quota errors are now detected by both message patterns AND error class names.

  2. Strengthened test coverage: Added 2 new tests:

    • classifies QuotaExceededError by errorName even without quota keywords in message — validates errorName-based detection
    • matches payment required pattern directly via RETRYABLE_ERROR_PATTERNS — validates the pattern-list path is exercised (not just the classifyErrorType shortcut)

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f16d55ad95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/billing.?(?:hard.?)?limit/i.test(message) ||
/exhausted\s+your\s+capacity/i.test(message) ||
/out\s+of\s+credits?/i.test(message) ||
/payment.?required/i.test(message)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid retrying generic assistant payment errors as quota exhaustion

message-update-handler.ts turns any assistant parts entry with type: "error" into a MessageContentError before it reaches classifyErrorType(). With this new payment required match, a downstream tool/MCP error like “payment required to use premium search” will now be labeled quota_exceeded, so isRetryableError() will switch models even though the failure is not provider- or model-specific and cannot be fixed by fallback. That creates unnecessary retry churn whenever assistant error parts contain billing text from other integrations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/hooks/runtime-fallback/error-classifier.test.ts">

<violation number="1" location="src/hooks/runtime-fallback/error-classifier.test.ts:257">
P3: The test comment/intent contradicts actual behavior: `classifyErrorType` does match `payment required`, so this does not validate the fallback pattern path it claims.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/hooks/runtime-fallback/error-classifier.test.ts Outdated
@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Mar 23, 2026

Re: P3 identified by cubic on latest commit — test comment/intent contradiction.

Acknowledged. The test name says "matches payment required pattern directly via RETRYABLE_ERROR_PATTERNS" but classifyErrorType does match "payment required" first via the quota_exceeded path. The test still verifies the correct end-to-end behavior (the error IS detected as retryable), but the name/comment implies it tests a specific internal path that it doesn't exclusively exercise.

This is a documentation/naming issue, not a behavioral one. Happy to rename the test to something like "detects payment required errors as retryable" to remove the misleading claim about which internal path is exercised.

@code-yeongyu code-yeongyu added the triage:bug-fix PR: Bug fix label Mar 24, 2026
The test name claimed it exercised RETRYABLE_ERROR_PATTERNS directly,
but classifyErrorType actually matches 'payment required' via the
quota_exceeded path first. Rename to 'detects payment required errors
as retryable' to accurately describe end-to-end behavior.
@MoerAI
Copy link
Copy Markdown
Contributor Author

MoerAI commented Mar 25, 2026

Fixed P3 from cubic review: renamed misleading test.

matches payment required pattern directly via RETRYABLE_ERROR_PATTERNS -> detects payment required errors as retryable

The old name and inline comments claimed the test exercised the RETRYABLE_ERROR_PATTERNS fallback path, but classifyErrorType actually catches payment required via the quota_exceeded classification first. The new name accurately describes the end-to-end behavior being tested.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

@code-yeongyu code-yeongyu merged commit abd6247 into code-yeongyu:dev Mar 25, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

triage:bug-fix PR: Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: runtime-fallback not triggered when opencode prettifies quota exceeded error

2 participants