Repository navigation
feat(core): typed free-limit error with reset time - #57146
Conversation
f30295d to
3564c3b
Compare
3564c3b to
744d516
Compare
744d516 to
1a5abbf
Compare
There was a problem hiding this comment.
I fed a FreeUsageLimitError 429 (with retry-after) through RequestExecutor.httpFailure → toSessionError on the base and on this branch. The base gives provider.quota with no reset delay. This branch gives provider.free-limit with retryAfterMs: 3600000. Paid, Go and third-party 429s are unchanged. The session-error tests in core and schema pass, and typecheck passes in ai, core, schema and client. I didn't run the TUI; the visible UI changes are in the stacked PRs.
Public API: two optional additions, both backwards compatible.
Session.StructuredError(SessionError.Error, the client'sSessionStructuredError) gainsretryAfterMs(a non-negative integer).- Session errors can now have the new type string
provider.free-limit. @opencode/ai'sQuotaExceededErrorgainsretryAfterMs.
One thing blocks the merge: packages/protocol/openapi.json wasn't regenerated. bun run check:generated in packages/protocol passes on dev but fails here. The spec marks Session.StructuredError as additionalProperties: false, so any client that validates against it would reject the new field. Running bun run generate in packages/protocol adds the retryAfterMs property and fixes it.
Stack (merge in order):
feat(core): typed free-limit error with reset time(this PR, basedev)fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall(base feat(core): typed free-limit error with reset time #57146)feat(tui): show free-limit notice, recall prompt, and skip failure toast(base feat(core): typed free-limit error with reset time #57146)feat(tui): restore animated Go upsell modal on free usage limit(base feat(tui): show free-limit notice, recall prompt, and skip failure toast #57467)Head:
1a5abbfb4a6a51676d84515223bdcb36d7db9d21· Size: +86/−7 (8 files; +22/−4 non-generated source)Related: #50996, #52894
Today
When OpenCode's free tier returns
FreeUsageLimitError(429),packages/ai/src/provider-error.tsclassifies it as a non-retryableQuotaExceededErrorand drops theretry-afterreset delay.packages/core/src/session/to-session-error.tsthen maps everyQuotaExceededtoprovider.quotawith"Rate limit exceeded. Please try again later.", so neither Desktop nor the TUI can distinguish a free-allowance limit from a paid quota error.dev)dev)After
@opencode/aistays completely provider-generic (QuotaExceededErrorforwardsretryAfterMslikeRateLimitErrorandProviderInternalError), whilepackages/core/src/session/to-session-error.tsmapsQuotaExceededErrorwhose body includes"FreeUsageLimitError"into a typedprovider.free-limitsession error withretryAfterMsand a clear fallback message ("Free usage limit reached. Connect a provider or subscribe to OpenCode Go to keep going, or switch models.").Why this layer, and how v1 did it
44b6938b2a^:packages/opencode/src/session/retry.ts:74-86, introduced in feat: add opencode go upsell modal when limits are hit #21583 and refined inbb3f14119b),SessionRetry.retryable(error, provider)matchederror.data.responseBody?.includes("FreeUsageLimitError")with no URL check and emittedaction: { reason: "free_tier_limit" }on retry status. V2 regressed when@opencode/ai(893d0f2726) classifiedFreeUsageLimitErroras non-retryableQuotaExceededError, bypassing retry status and collapsing intoprovider.quota.packages/core/src/models-dev/snapshot.txt,packages/ai/src/providers/opencode.ts:6) hitshttps://opencode.ai/inference/{openai,anthropic,google,systemone}/v1directly (with/zen/v1/*only kept as a proxy inpackages/console/app/src/lib/inference-proxy.ts:3-25). Console's inference service returns HTTP 429 withRetry-Afterand distinct JSON payloads:error.type: "FreeUsageLimitError"ontier === "free",error.type: "GoUsageLimitError"on Go quota, anderror.type: "server_error"on paidRateLimit.Exceeded.to-session-error.tsinstead of URL matching in@opencode/aiorOpencodePlugin:@opencode/aishould not know about OpenCode product tiers or URLs (isZenRoutebroke custom/self-hosted Console URLs configured viaOpencodePluginand missed/zen/go/v1/*). Meanwhile,OpencodePlugin(packages/core/src/plugin/provider/opencode.ts:479-486) intentionally skips registeringhttp.responsehooks when signed out so anonymous free-tier calls avoid request-body cloning and stream re-wrapping inpackages/core/src/session/model-request.ts:321-349. MatchingFreeUsageLimitErroronQuotaExceededError.bodyinto-session-error.ts—just liketokenSharingMessageson line 130 of the same file—keeps@opencode/aigeneric and works for both signed-out and signed-in free tier users across all Console URLs.How
packages/ai/src/schema/errors.ts&packages/ai/src/provider-error.ts: forwardretryAfterMsonQuotaExceededErrorjust likeRateLimitErrorandProviderInternalError.packages/schema/src/session-error.ts,packages/core/src/session/to-session-error.ts, &packages/client/src/*: add optionalretryAfterMstoSessionError.Errorand mapQuotaExceededErrorbodies containing"FreeUsageLimitError"toprovider.free-limit.Proof
packages/schema/test/session-error.test.ts&packages/core/test/session-error.test.ts: verifiestoSessionErrormapsopencodefree-limit 429 (FreeUsageLimitError) toprovider.free-limitwithretryAfterMs, keeps paidopencode429s as retryableprovider.rate-limit, keepsGoUsageLimitError429s as non-retryableprovider.quota, and leaves other providers' 429s untouched.Risk
Low.
provider.free-limitis additive and only emitted when aQuotaExceededErrorbody contains"FreeUsageLimitError"; paid rate limits, Go quota (GoUsageLimitError), and third-party 429s are unchanged.Added by the opencode bot (AI-generated).
Related: #52894, #55816
Public API: adds optional
retryAfterMsto Session.StructuredError and a newprovider.free-limiterror type (backwards compatible)