fix(opencode): classify provider billing/quota failures instead of masking them#1466
fix(opencode): classify provider billing/quota failures instead of masking them#1466Astro-Han wants to merge 1 commit into
Conversation
…sking them
A DeepSeek account out of balance returns 402 `{"error":{"message":
"Insufficient Balance",...}}`, but it surfaced to users as "Connection lost.
Please check whether the last operation completed before resending." The
first defect is in classification: 402 fell through to `unknown` and the
nested provider message was dropped, so the real reason never reached the UI.
Provider error classification (provider/error.ts):
- Map 402 -> quota_exhausted (account can no longer pay = same user action as
a depleted quota: top up or switch model; no new payment_required kind).
- Add strong/weak BILLING_PATTERNS so billing failures providers report under
inconsistent statuses still classify as quota_exhausted. Strong patterns
(insufficient balance / out of credits / payment required / arrears / 余额不足
/ 欠费) match unconditionally; weak patterns (quota exceeded / billing issue)
only on a billing-shaped status {400,402,403} with no rate-limit signal. 429
is excluded: it is Too Many Requests, so "quota exceeded" wording (e.g.
Google's per-minute request quota) stays a retryable rate_limit.
- Exclude opencode FreeUsageLimitError from billing: it must stay a retry-time
free_quota_exhausted concept (countdown card), never the terminal
quota_exhausted kind which would stop classifyRetry before the free-quota
branch.
- Fix message(): prefer the nested {error:{message}} string so DeepSeek's
"Insufficient Balance" is surfaced instead of dumping the raw body (body.error
is an object and short-circuited the old `||` chain).
- Unify provider code extraction (error.code / code / error.type / Google-style
status) behind extractProviderCode.
- parseStreamError: classify known auth (authentication_error / invalid_api_key
/ permission_denied) and rate-limit (rate_limit_exceeded / too_many_requests
/ rate_limited) codes, and strong-billing messages.
- PR1c middle path: a typed ({type:"error"} + error object) provider error body
with an unhandled code now becomes a structured APIError(kind="unknown")
preserving code/responseBody for the frontend, instead of an opaque
UnknownError. A bare {code} body is NOT upgraded (indistinguishable from a
Node runtime error like EACCES) and stays UnknownError. Retryability is read
from the code only (never free-text message), so a terminal error whose
message mentions "unavailable" is not wrongly retried; a transient-looking
code (exhausted/unavailable/rate limit) stays retryable, and a typed
FreeUsageLimitError stays retryable so classifyRetry routes it to
free_quota_exhausted. Untyped nested envelopes still stay UnknownError.
resource_exhausted retry semantics are intentionally left unchanged.
Tests (message-v2.test.ts, retry.test.ts): 402 -> quota_exhausted; DeepSeek
"Insufficient Balance" at 402 and 400 -> quota_exhausted; FreeUsageLimitError
429 stays rate_limit; 429 "quota exceeded" stays rate_limit; stream
auth/rate/billing codes; typed-unknown -> APIError(kind=unknown) with retry
verdict from code; "unavailable" in message stays non-retryable; bare Node
EACCES error stays UnknownError; typed FreeUsageLimitError stream routes to
free_quota_exhausted; untyped envelope stays UnknownError.
Refs #1105 #1123
Claude-Session: https://claude.ai/code/session_015bW9JQSkuB156gkNQdxCzi
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthrough
ChangesProvider error classification improvements
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. 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. Comment |
Summary
Fixes the first defect behind the "Connection lost. Please check whether the last operation completed before resending." misreport: provider billing/quota failures (DeepSeek 402 "Insufficient Balance" and friends) were classified as
unknown, and the nested provider message was dropped — so the real reason never reached the UI. This PR is classification-only (packages/opencode/src/provider/error.ts); the run-incident halt overwrite that finishes masking the message is PR2.Changes in
provider/error.ts:quota_exhausted— account can no longer pay = same user action as a depleted quota (top up or switch model); no newpayment_requiredkind.BILLING_PATTERNSso billing failures reported under inconsistent statuses still classify asquota_exhausted. Strong patterns (insufficient balance/out of credits/payment required/arrears/余额不足/欠费) match unconditionally; weak patterns (quota exceeded/billing issue) only on a billing-shaped status{400,402,403}with no rate-limit signal. 429 is excluded — it is Too Many Requests, so "quota exceeded" wording (e.g. Google's per-minute request quota) stays a retryablerate_limit.FreeUsageLimitErrorfrom billing: it must stay a retry-timefree_quota_exhaustedconcept (countdown card), never the terminalquota_exhaustedkind, which would stopclassifyRetrybefore the free-quota branch.message()to prefer the nested{error:{message}}string so DeepSeek's "Insufficient Balance" surfaces instead of dumping the raw body (body.erroris an object and short-circuited the old||chain).error.code/code/error.type/ Google-stylestatus) behindextractProviderCode.parseStreamErrornow classifies known auth (authentication_error/invalid_api_key/permission_denied) and rate-limit (rate_limit_exceeded/too_many_requests/rate_limited) codes, plus strong-billing messages.{type:"error"}+ error object) provider error body with an unhandled code now becomes a structuredAPIError(kind="unknown")preservingcode/responseBodyfor the frontend, instead of an opaqueUnknownError. A bare{code}body is not upgraded (indistinguishable from a Node runtime error likeEACCES) and staysUnknownError. Retryability is read from the code only (never the free-text message), so a terminal error whose message merely mentions "unavailable" is not wrongly retried; a transient code (resource_exhausted/unavailable/overloaded/rate-limit) stays retryable, and a typedFreeUsageLimitErrorstays retryable soclassifyRetryroutes it tofree_quota_exhausted.resource_exhaustedretry semantics are intentionally left unchanged.Why
A Windows user on DeepSeek (out of balance) saw repeated "Connection lost" messages. Root cause is a 3-layer defect; the classification layer is fixed here: 402 fell through
apiCallErrorKindtounknown, andmessage()dropped the nested "Insufficient Balance" string. The goal is to classify all common provider error types correctly and pass the real reason through, not just patch 402.Related Issue
Refs #1123 (classify-then-render umbrella), #1105 (classification spine).
Human Review Status
Pending
Review Focus
FreeUsageLimitErrormust stayfree_quota_exhausted;resource_exhaustedmust stay retryable; a terminalquota_exhaustedcode must not be retried.APIError; bare staysUnknownError).Risk Notes
Behavior/contract change (intentional, covered by tests): typed provider error bodies with unhandled codes now serialize as
APIError(kind="unknown")instead ofUnknownError; two existing guard tests were updated to the new contract, with the meaningful guarantee (such errors stay non-retryable) preserved. No data migration. No UI in this PR (Risk Notes "Screenshots" item N/A — backend classification only).How To Verify
Key regressions pinned: 402 →
quota_exhausted; DeepSeek "Insufficient Balance" at 402 and 400 →quota_exhausted;FreeUsageLimitError429 staysrate_limitand routes tofree_quota_exhausted; 429 "quota exceeded" staysrate_limit; typed terminalquota_exhaustedcode not retried; "unavailable" in message stays non-retryable; bare NodeEACCESerror staysUnknownError; untyped envelope staysUnknownError.Screenshots or Recordings
N/A — backend error-classification change, no UI.
Checklist
bug,enhancement,task,documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.app,ui,platform,harness,ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.P0,P1,P2,P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.Pending,Approved by @<reviewer>, orNot required: <reason>(default isPending; "not required" is restricted to bot-authored low-risk PRs).https://claude.ai/code/session_015bW9JQSkuB156gkNQdxCzi
Summary by CodeRabbit
Bug Fixes
Tests