Repository navigation
Configurable retry policy: expose maxRetries / initialDelay / backoffFactor / maxDelay via config #43596
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
Activity
github-actions commentedon Aug 20, 2026
This issue might be a duplicate of existing issues. Please check:
retryconfig and references the same Zhipu 5-hour quota window scenariomaxRetries/retryBackofffields in provider optionsIf your request is not already covered by those, please consider commenting there with your specific use case (the Zhipu
zhipuai-coding-plan5-hour window + the regression from c789868) rather than tracking it separately.JoaoBerne commentedon Aug 23, 2026
Hitting this from the other end: the transient
network_errorcase on a busy free model rather than a long quota window, but the same binding constraint.Two things that might be worth folding into the proposal.
Which path through
delay()the error takes matters as much as the attempt count. An error with no response headers has its exponential clamped toRETRY_MAX_DELAY_NO_HEADERS, so raisingmaxRetriesalone means every attempt from the 5th onward waits the full 30s. An error that does carry headers but noretry-afteris not clamped by that constant at all (#33728), so attempt 10 waits ~17 min and attempt 15 waits ~9 h. Either waybackoffFactorneeds to be tunable alongsidemaxRetries, otherwise the knob people reach for first is the one that helps least.Sketch of the change, in case it helps show the surface is small:
packages/core/src/v1/config/config.ts: aretrystruct underexperimental, same shape as the neighbouringcompactionandmcp_timeoutentries.packages/opencode/src/session/retry.ts:policy()anddelay()take optional overrides; the current exported constants stay as the defaults, so behaviour is unchanged when the key is absent.packages/opencode/src/session/processor.ts, at theSessionRetry.policycall:Config.Serviceis already in scope there, so it is threading one value through.packages/opencode/test/session/retry.test.tsalready drives the schedule viaSchedule.toStepWithMetadata, so the override cases extend naturally.Happy to open the PR if a maintainer wants to assign it. CONTRIBUTING says config and product surface should get a design review first, so asking rather than sending it unprompted.
As a stopgap I published a patcher for the compiled constants, with the measured before/after schedule: https://github.com/JoaoBerne/opencode-retry-patch. Not a substitute for the config option, and it stops being needed the day this lands.
fsx8 commentedon Aug 26, 2026
I really hope this can get merged at some point because the retry behaviour is totally broken
yangyaofei commentedon Aug 27, 2026
@fsx8 you can try this hacky plugin: https://github.com/yangyaofei/opencode-buraunkan-kobo
KayIter commentedon Sep 10, 2026
We have the inverse use case for the same configuration surface: coding-agent turns can have side effects, so we need
maxRetries: 0to guarantee that the session processor does not implicitly replay a provider request.On OpenCode 1.18.30, setting the provider/AI SDK retry count to zero does not disable
SessionRetry.policy(). With injected retryable header/chunk transport failures we observed multiple primary provider requests. A global or per-providermaxRetriesoption that explicitly accepts zero would cover this case while preserving the current default for other users.We are keeping our integration on unmodified upstream source in the meantime: the outer client reports failure and never automatically replays the Prompt, but it cannot promise a single provider attempt until this is configurable.
JoaoBerne commentedon Sep 23, 2026
@KayIter #44517 covers
maxRetries: 0: the policy stops on the first attempt, so a failed request isn't replayed. There was no test for it, there is now.That's the only replay I can find on the chat path.
llm.tsalready calls the SDK withmaxRetries: 0(only title generation passes 2) andSessionRetry.policy()is the oneEffect.retryaround it in the processor. I haven't gone through the outer session loop, so I'd only promise "no replay of a failed request", not more.Funny that it's the opposite of what I wrote it for: quota windows want unbounded, you want zero. Same knob either way.
feat: make the session retry policy configurable
test: pin that maxRetries 0 means exactly one provider attempt
test: pin that maxRetries 0 disables the session-level replay
thdxr commentedon Oct 11, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊
yangyaofei commentedon Oct 12, 2026
@thdxr nonsense, this bug/limitation still exist in V2, still hardcode max-retry
JoaoBerne commentedon Oct 12, 2026
Re-filed for 2.0 as #54862.