Skip to content

[FEATURE]: Per-Provider Quota-Aware Auto-Retry with Configurable Reset Schedule #43324

Open

Description

@qzzpfssjlf

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

Problem

OpenCode's session retry already parses retry-after headers and recognises FreeUsageLimitError / GoUsageLimitError (see packages/opencode/src/session/retry.ts), but the handling is essentially hardcoded to the OpenCode Go subscription:

  • GoUsageLimitError already extracts metadata.limitName ("5 hour" | "weekly" | "monthly") and retry-after, then surfaces it in the UI as "X hour usage limit reached. It will reset in Y hours." — but the session does not actually wait for that reset; it just retries up to 5 times with exponential backoff.
  • Providers other than OpenCode Go (e.g. users bringing their own keys to fixed-window-quota providers) hit a 429 with no metadata.limitName in the body. The retry logic falls back to exponential backoff, exhausts the 5 attempts, and gives up — exactly when the user is most likely to need an automatic wait-and-resume.

In short: OpenCode already has the display half of quota handling (for Go) but not the wait-until-reset half, and the whole thing is only wired up for Go.

Proposed solution

  1. Generalise the existing quota classification. packages/opencode/src/session/retry.ts already has the primitives (LimitName = "5 hour" | "weekly" | "monthly", retry-after parsing). Expose them through a per-provider config so any provider can declare its own reset schedule.
  2. Read the schedule from provider.<id>.quota, not provider.options.quota. The options block is pass-through to the AI SDK (timeout, chunkTimeout, setCacheKey, baseURL, apiKey, headers); quota / retry are OpenCode-internal behaviour, so they belong next to options rather than inside it.
  3. Actually wait for the next reset instead of giving up after 5 exponential attempts.

Config (extends the existing provider block; field names follow the camelCase convention used by baseURL, chunkTimeout, setCacheKey):

{
  "provider": {
    "<id>": {
      "options": { "...": "..." },
      "quota": {
        "resetTimes": ["00:00", "05:00", "10:00", "15:00", "20:00"],
        "weeklyResetDay": "monday",
        "monthlyResetDay": 1
      },
      "retry": {
        "maxRetriesPerWindow": 5,
        "maxTotalRetries": 50
      }
    }
  }
}

Behaviour when a quota error is hit:

  1. Classify error → 5h / weekly / monthly / transient (reuse the existing LimitName types from retry.ts).
  2. 5h hit → next reset from resetTimes, sleep, resume.
  3. Weekly / monthly hit → surface a clear "X limit reached, resets on …" message; no auto-retry inside the session.
  4. After maxRetriesPerWindow failures inside one window, queue for the next window.
  5. Hard ceiling maxTotalRetries prevents runaway sessions.

Backwards compatibility: providers without a quota block fall back to today's behaviour (exponential backoff, 5 attempts).

Related issues

Activity

  1. github-actions commented on Aug 19, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of or closely related to existing issues. Please check:

    If these don't cover your use case, clarifying how this proposal differs from #43126 in particular would help maintainers evaluate it.

  2. qzzpfssjlf commented on Aug 22, 2026

    @qzzpfssjlf
    Author

    Not a duplicate of #43126 — these are two layers of the same feature and I'd suggest keeping them linked rather than merged.

    Concretely, #43324 only covers the "wait until reset" decision (classified via retry-after + metadata.limitName, then slept on), not the suspend/resume plumbing itself (that's #27511's scope). It also adds a per-provider config schema so any fixed-window provider can declare its own schedule, rather than hardcoding the Go subscription limits into retry.ts as today.

    So: #43126 = UX thread, #27511 = mechanism, #43324 = retry-engine config + reset scheduling. Happy to fold the config/proposal section into #43126 if maintainers prefer a single thread — just want the reset-scheduling logic to land somewhere.

  3. cannockwolf commented on Sep 22, 2026

    @cannockwolf

    Hi I got astra to make me a plugin that is working perfectly atm here's an ai summary of what's created

    I’ve implemented a local version of this behaviour as an OpenCode plugin and have now been using it successfully in normal use.

    The basic flow is:

    usage_limit_reached → read the provider’s reset timestamp → persist the recovery state → wait locally without model calls → resume the same session after reset.

    A few things I’ve found from real-world testing:

    • It works with both 5-hour and weekly quota resets.
    • Recovery state needs to be persistent rather than an in-memory timer.
    • The pending recovery survives OpenCode restarts.
    • It has also survived OpenCode being updated while waiting on two separate occasions.
    • A single-owner/lock mechanism is useful to prevent duplicate recovery submissions after a restart.
    • The model/variant/agent and original session can be preserved when resuming.
    • The waiting process itself doesn’t need to make provider/model requests.
    • I also added a notification when work actually resumes and when it finishes if longer than 2 minutes.

    I’ve been using this in production rather than just testing it synthetically, and automatic recovery has been reliable so far.

    If this would be useful for the core implementation, I’m happy to provide more detai.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions