Skip to content

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

Description

@yangyaofei

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

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