Skip to content

feat(core): typed free-limit error with reset time - #57146

Merged
kitlangton merged 1 commit into
devfrom
free-limit-card
Oct 20, 2026
Merged

kitlangton merged 1 commit into
devfrom
free-limit-card

Conversation

@kitlangton

@kitlangton kitlangton commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Stack (merge in order):

  1. feat(core): typed free-limit error with reset time #57146 — feat(core): typed free-limit error with reset time (this PR, base dev)
  2. fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall #57466 — fix(app): restore free-tier usage limit dialog, timeline line, and prompt recall (base feat(core): typed free-limit error with reset time #57146)
  3. feat(tui): show free-limit notice, recall prompt, and skip failure toast #57467 — feat(tui): show free-limit notice, recall prompt, and skip failure toast (base feat(core): typed free-limit error with reset time #57146)
  4. feat(tui): restore animated Go upsell modal on free usage limit #57471 — 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.ts classifies it as a non-retryable QuotaExceededError and drops the retry-after reset delay. packages/core/src/session/to-session-error.ts then maps every QuotaExceeded to provider.quota with "Rate limit exceeded. Please try again later.", so neither Desktop nor the TUI can distinguish a free-allowance limit from a paid quota error.

Desktop (dev) TUI (dev)
desktop-before-dark tui-before

After

@opencode/ai stays completely provider-generic (QuotaExceededError forwards retryAfterMs like RateLimitError and ProviderInternalError), while packages/core/src/session/to-session-error.ts maps QuotaExceededError whose body includes "FreeUsageLimitError" into a typed provider.free-limit session error with retryAfterMs and 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

  • How v1 did it: In v1 (44b6938b2a^:packages/opencode/src/session/retry.ts:74-86, introduced in feat: add opencode go upsell modal when limits are hit #21583 and refined in bb3f14119b), SessionRetry.retryable(error, provider) matched error.data.responseBody?.includes("FreeUsageLimitError") with no URL check and emitted action: { reason: "free_tier_limit" } on retry status. V2 regressed when @opencode/ai (893d0f2726) classified FreeUsageLimitError as non-retryable QuotaExceededError, bypassing retry status and collapsing into provider.quota.
  • What Console sends today: Current OpenCode (packages/core/src/models-dev/snapshot.txt, packages/ai/src/providers/opencode.ts:6) hits https://opencode.ai/inference/{openai,anthropic,google,systemone}/v1 directly (with /zen/v1/* only kept as a proxy in packages/console/app/src/lib/inference-proxy.ts:3-25). Console's inference service returns HTTP 429 with Retry-After and distinct JSON payloads: error.type: "FreeUsageLimitError" on tier === "free", error.type: "GoUsageLimitError" on Go quota, and error.type: "server_error" on paid RateLimit.Exceeded.
  • Why to-session-error.ts instead of URL matching in @opencode/ai or OpencodePlugin: @opencode/ai should not know about OpenCode product tiers or URLs (isZenRoute broke custom/self-hosted Console URLs configured via OpencodePlugin and missed /zen/go/v1/*). Meanwhile, OpencodePlugin (packages/core/src/plugin/provider/opencode.ts:479-486) intentionally skips registering http.response hooks when signed out so anonymous free-tier calls avoid request-body cloning and stream re-wrapping in packages/core/src/session/model-request.ts:321-349. Matching FreeUsageLimitError on QuotaExceededError.body in to-session-error.ts—just like tokenSharingMessages on line 130 of the same file—keeps @opencode/ai generic 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: forward retryAfterMs on QuotaExceededError just like RateLimitError and ProviderInternalError.
  • packages/schema/src/session-error.ts, packages/core/src/session/to-session-error.ts, & packages/client/src/*: add optional retryAfterMs to SessionError.Error and map QuotaExceededError bodies containing "FreeUsageLimitError" to provider.free-limit.

Proof

  • packages/schema/test/session-error.test.ts & packages/core/test/session-error.test.ts: verifies toSessionError maps opencode free-limit 429 (FreeUsageLimitError) to provider.free-limit with retryAfterMs, keeps paid opencode 429s as retryable provider.rate-limit, keeps GoUsageLimitError 429s as non-retryable provider.quota, and leaves other providers' 429s untouched.

Risk

Low. provider.free-limit is additive and only emitted when a QuotaExceededError body 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 retryAfterMs to Session.StructuredError and a new provider.free-limit error type (backwards compatible)

@kitlangton kitlangton changed the title fix: show next steps when the free usage limit is reached fix: restore free-tier usage limit dialog on v2 Oct 20, 2026
@kitlangton kitlangton changed the title fix: restore free-tier usage limit dialog on v2 feat(core): typed free-limit error with reset time Oct 20, 2026
@kitlangton
kitlangton marked this pull request as ready for review October 20, 2026 22:10
@kitlangton
kitlangton merged commit a9fa57b into dev Oct 20, 2026
35 of 36 checks passed
@kitlangton
kitlangton deleted the free-limit-card branch October 20, 2026 22:10

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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's SessionStructuredError) gains retryAfterMs (a non-negative integer).
  • Session errors can now have the new type string provider.free-limit.
  • @opencode/ai's QuotaExceededError gains retryAfterMs.

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.

Related: #52894, #55816

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant