Rate Limit Retry V2
OpenCode 2 plugin that retries the same model with exponential backoff when a provider rate limit is hit.
0
0
20.0
生态多维模型
17 小时前
2026-10-20
快速安装与配置
opencode.json写入当前项目的 opencode.json,只对这个仓库生效。
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-rate-limit-retry-v2@1.0.0"]
}写入 ~/.config/opencode/opencode.json,对所有项目生效。
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-rate-limit-retry-v2@1.0.0"]
}若你要在本地改造这个插件,先装到项目里再从本地路径引用。
shell
pnpm add -D opencode-rate-limit-retry-v2OpenCode 启动时会通过内嵌运行时自动加载 npm 依赖并缓存至本地目录,无需手动在全局环境执行安装。
An OpenCode 2 plugin that retries the same model with exponential backoff and jitter when a provider answers with a rate limit, instead of leaving the turn failed.
This is a port of @bdliyq/opencode-rate-limit-retry to the OpenCode 2 plugin API. The original targets the OpenCode 1 API and does not load on OpenCode 2.
Install
opencode plugin add opencode-rate-limit-retry-v2
Or add it to opencode.json(c) yourself:
{
"$schema": "https://opencode.ai/config.json",
"plugins": ["opencode-rate-limit-retry-v2"]
}
To configure it inline, use the object form:
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
{
"package": "opencode-rate-limit-retry-v2",
"options": { "baseDelayMs": 10000, "maxRetries": 6 }
}
]
}
Restart OpenCode after changing plugins.
What it does
When a model call fails with a rate limit, the plugin waits, then sends a short follow-up prompt to the same session so the assistant continues where it left off. The prompt runs on the session's current model, which is normally the model that hit the limit — the plugin tracks that model and does not retry into a different one (see Model changes).
- Backoff:
baseDelayMs * 2^attempt, capped atmaxDelayMs, with ±jitterFactorrandomisation. Defaults give 5s → 10s → 20s → 40s → 80s. - Bounded: at most
maxRetriesattempts per incident, where an incident is one session on one model. The budget resets on a genuine successful execution, afterresetAfterMsof inactivity, or when the session switches model. - One at a time: a lock file serialises retries per session across every plugin instance and every OpenCode server process, so duplicate events cannot produce duplicate prompts.
- Cancelled by you: switching models, or sending a message during the wait, drops the pending retry instead of queueing a continuation behind your turn.
- Private by default: nothing is written to disk unless you enable the debug log.
Model changes
session.prompt has no model parameter: a retry runs on whatever model the session has when the prompt is admitted. To keep that from becoming a surprise, the plugin:
- records the model when each run starts (the failure event carries none), and again as part of the persisted attempt state;
- treats a different model as a different incident, so an exhausted budget on one model does not block a retry on another;
- re-reads the session's model immediately before prompting, and drops the pending retry if it changed — the switch is a deliberate user choice, and injecting a continuation into the model they just picked would be a duplicate turn;
- cancels the backoff wait as soon as
session.model.selectedfires, instead of waking up minutes later to a retry that no longer applies; - resets the budget on the new model, so it starts with the full
maxRetries; - proceeds with the retry when the model cannot be read (
session.getfailure), because losing a wanted retry is worse than an occasional retry on the current model.
Configuration
Settings come from three places, highest priority first:
optionson the plugin entry inopencode.json(c)(see above)rate-limit-retry.json, first file found:~/.config/opencode/rate-limit-retry.json(on Windows,%USERPROFILE%\.config\opencode\)<workspace>/.opencode/rate-limit-retry.json<workspace>/rate-limit-retry.json
<workspace> is the directory OpenCode reports for the plugin instance, not the
directory the server happened to start in, so a project file is still found when
the server runs from elsewhere.
3. built-in defaults
Every key is optional.
{
"enabled": true,
"maxRetries": 5,
"baseDelayMs": 5000,
"maxDelayMs": 120000,
"jitterFactor": 0.2,
"resetAfterMs": 300000,
"errorPatterns": ["rate limit", "too many requests", "quota exceeded"],
"debug": false,
"logFile": null
}
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
Master switch. Read at load time. |
maxRetries |
5 |
Attempts per incident before backing off. |
baseDelayMs |
5000 |
First wait. |
maxDelayMs |
120000 |
Delay ceiling. |
jitterFactor |
0.2 |
Randomisation as a fraction of the delay (0 disables). |
resetAfterMs |
300000 |
Inactivity after which the attempt budget resets. |
errorPatterns |
see source | Case-insensitive substrings matched against the provider error. |
debug |
false |
Write lifecycle lines to the log file. |
logFile |
~/.config/opencode/rate-limit-retry.log |
Log destination when debug is on. |
Values are validated on load: out-of-range numbers are clamped and bad types fall back to defaults, so a typo cannot produce NaN delays or an unbounded retry loop.
Tuning
- Hitting limits often? Raise
baseDelayMsto10000andmaxRetriesto6. - Slow to recover? Raise
maxDelayMs. - Want to see what matched? Set
"debug": trueand read the log file.
Differences from the original
The port fixes issues that only become visible once the plugin actually runs:
- Duplicate instances. OpenCode can create several plugin instances for one file (Windows file watchers emit multiple change events per write, and instances are not always disposed). The original deduplicated with an in-memory
Set, which is per-instance and therefore useless here; in testing a single write produced six live instances and 72 retry prompts. Retries are now serialised with a lock file. - Attempt counter reset too early. The original reset the counter as soon as a retry prompt was admitted, which says nothing about whether the model call succeeded, so it never reached its own cap. The counter now resets only on a successful execution.
- Cap never held. On reaching
maxRetriesthe original deleted its state, so the next failure started again at zero. The state is now kept, so the cap holds until a success or the inactivity timeout. - Config path on Windows. The original resolved its config directory via
process.env.HOME, which is unset on Windows, sorate-limit-retry.jsonwas never read there. This port usesXDG_CONFIG_HOME ?? ~/.config, matching OpenCode itself. - Log is opt-in. Provider error bodies can contain account-specific text, so the debug log is disabled by default.
- Model switches during backoff. A prompt carries no model, so the original retried into whatever model the session had when the wait ended — often a model the user had just picked, producing an unexpected extra turn, while still counting the attempt against the model that actually failed. The retry is now tied to the failing model, dropped if the session moved on, and the wait is cancelled immediately.
The trigger is also different by necessity: OpenCode 2 has no session.error event, so this uses session.execution.failed, which carries the structured error.
Troubleshooting
Set "debug": true and read the log (default
~/.config/opencode/rate-limit-retry.log). Each decision is one line:
| Line | Meaning |
|---|---|
[RETRY] attempt 2/5 … waiting 10s |
A rate limit matched and the wait started. |
[SKIP] retry cap reached |
maxRetries is spent; a success, a model switch, or resetAfterMs frees it. |
[SKIP] another instance is already retrying |
Normal when OpenCode loads the plugin more than once. |
[SKIP] lock unavailable |
The lock directory cannot be written; retries are disabled until this is fixed. |
[SKIP] user prompt superseded the pending retry |
You sent a message while the plugin was waiting; that is intentional. |
[EVENT] OpenCode scheduled its own retry |
The host is retrying before the turn fails; both policies are active. |
If retries never happen at all, check in this order:
- Is the error text matched by
errorPatterns? A provider that returns a bare 429 with no message does not match; add the phrase it does return. - Is
enabledtrue and the config file the one you edited? Pluginoptionsoverride the file, and the file is read when the plugin loads. - Does the log show
lock unavailable? PointTMPDIRsomewhere writable, or remove a stale lock file left by a crashed process.
Known limitations
- The retry is a follow-up prompt, not a replayed request. It is admitted through
session.prompt, so it appears in the transcript as an ordinary user message, other plugins'prompthooks see it, and the model starts a fresh turn with the full conversation. - Each retry costs a full turn.
maxRetries: 5on a long session is five additional full-price requests. - OpenCode has its own retry policy. The host exposes a
sessionretry hook and emitssession.retry.scheduled; this plugin uses neither. It reacts after a turn has failed, so both policies can be active for the same provider — lowermaxRetriesif that produces more attempts than you want. - Config is read when the plugin loads. Editing
rate-limit-retry.jsonrequires a restart.enabled: falseis the off switch;maxRetriesis clamped to at least 1. - State is per plugin, not per project. Attempt budgets are keyed by session, and a success, a model switch, or
resetAfterMsof inactivity clears the entry.
Requirements
- OpenCode 2.1 or newer (
@opencode/pluginis provided by the host) - Node 20+ at build time; the published ESM output uses explicit
.jsspecifiers, so it imports under both Node and Bun
Development
bun install
bun test
bun run test:types # `bun test` does not typecheck the tests
bun run build
License
MIT. See LICENSE.
Original work Copyright (c) 2026 leoli, from
bdliyq/opencode-rate-limit-retry, also MIT.
同类生态推荐
Retry Forever
opencode-retry-forever
Keeps long-running opencode sessions alive by retrying transient provider failures (503, DNS, connection resets) forever, with no backoff.
Modular Agents
modular-agents
Modular agent support for OpenCode — define agents as folders with multiple prompt files
Model Fallback
@smart-coders-hq/opencode-model-fallback
OpenCode plugin for ordered model fallback chains, preemptive redirect, and automatic rate limit recovery.