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:
- 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
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.tscaps at 5 retries: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:Observed timeline (opencode 1.18.18, from
~/.local/share/opencode/log/opencode.log):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()honorsretry-after/retry-after-msheaders, and the exponential backoff caps atRETRY_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 } } }provider.<id>.retry) would be a nice follow-up, but a global knob solves the immediate problem.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
zhipuai-coding-plan, model glm-5.3