anomalyco / anomalyco/opencode

Configurable retry policy: expose maxRetries / initialDelay / backoffFactor / maxDelay via config

Open
#43,596 5 comments 10 reactions 1 assignee View on GitHub

@nexxeln is already working on this.

Since Aug 20, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Configurable retry policy: expose maxRetries / initialDelay / backoffFactor / maxDelay via config

Summary

RETRY_MAX_RETRIES = 5 (introduced in #41939 / c789868) is hardcoded. For providers with long quota windows, 5 retries (~68s total) abort the turn while the error is still transient — the quota window simply hasn't reset yet. Please expose the retry constants via config so users can tune or uncap them.

Background

Before c789868 (v1.18.x, 2026-08-12), retryable errors retried indefinitely with exponential backoff. After it, packages/opencode/src/session/retry.ts caps at 5 retries:

export const RETRY_MAX_RETRIES = 5
...
if (meta.attempt > RETRY_MAX_RETRIES) return Cause.done(meta.attempt)

This is a reasonable default. But it breaks a class of providers whose "transient" errors last much longer than 68 seconds.

Real-world case

Zhipu GLM coding plan (zhipuai-coding-plan) enforces a 5-hour usage window. When exhausted, the API returns a 429-class error:

AI_APICallError: 已达到 5 小时的使用上限。您的限额将在 2026-08-19 02:14:39 重置。
("5-hour usage limit reached. Your quota resets at 2026-08-19 02:14:39.")

Observed timeline (opencode 1.18.18, from ~/.local/share/opencode/log/opencode.log):

17:00:02  attempt 1 → 429 limit error
17:00:04  attempt 2 (+2.1s)
17:00:09  attempt 3 (+4.5s)
17:00:17  attempt 4 (+8.7s)
17:00:34  attempt 5 (+17.1s)
17:01:10  attempt 6 (+35.8s)
17:01:10  process error → turn aborted

Six attempts in 68 seconds, then the session aborts. The error itself says the quota resets hours later. Before the cap, opencode kept backing off (2s → 4s → ... → tens of minutes per attempt) and automatically resumed once the window reset — which is exactly the desired behavior for this provider class. Several coding-plan providers (Zhipu, and similar subscription-window plans) share this pattern.

Note the delay machinery already handles this well: delay() honors retry-after/retry-after-ms headers, and the exponential backoff caps at RETRY_MAX_DELAY (max int32). The binding constraint is the attempt count, not the delay.

Proposal

Expose the existing constants via config, defaults unchanged:

{
  "experimental": {
    "retry": {
      "maxRetries": 5,            // default unchanged; -1 or "unlimited" = no cap
      "initialDelayMs": 2000,
      "backoffFactor": 2,         // e.g. 1.35 for a gentler curve
      "jitterFactor": 0.25,
      "maxDelayMs": 2147483647,
      "maxDelayNoHeadersMs": 30000
    }
  }
}
  • Zero behavior change unless configured.
  • A per-provider override (provider.<id>.retry) would be a nice follow-up, but a global knob solves the immediate problem.
  • Alternatively/additionally: treat a retryable error whose stated reset time (or retry-after) exceeds the total backoff budget as "keep waiting" rather than "give up".

Why not just bump the default?

5 is a sensible default for genuinely transient errors (network blips, brief 429 spikes). The problem is heterogeneity: one user's "transient" is 30 seconds, another's is 5 hours. A config knob is the minimal fix that serves both without regressing the #41939 motivation.

Environment

  • opencode 1.18.18 (npm, linux-x64)
  • provider: zhipuai-coding-plan, model glm-5.3
  • log excerpt above

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.