Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

@shutovks/opencode-model-fallback

Keep your OpenCode session alive when the model fails — automatic fallback to the next configured model on rate limits, quota exhaustion, overloads, and transient provider errors.

CInpmLicense: MITBun

When the active model errors out with a transient failure, this plugin retries the last user message in the same session using the next model from your fallback list — then recovers back to the original once it's healthy again. No lost context, no manual restart.

Install

bun add -g @shutovks/opencode-model-fallback

Or use any package manager OpenCode can resolve from your plugin config.

Configure

Add the plugin to opencode.json or .opencode/opencode.json:

{
"$schema": "https://opencode.ai/config.json",
"plugin": [
[
"@shutovks/opencode-model-fallback",
{
"enabled": true,
"fallback_models": [
"anthropic/claude-sonnet-4-5",
"openai/gpt-5.1-codex",
"google/gemini-2.5-pro"
],
"unavailable_models": [],
"max_attempts": 3,
"attempts_window_ms": 600000,
"cooldown_ms": 60000,
"backoff_ms": 0,
"recover_original_model": true,
"notify": true,
"debug": false
}
]
]
}

Restart OpenCode after changing config.

Options

OptionDefaultMeaning
enabledtrueDefault behavior for new sessions.
fallback_models[]Ordered fallback model list, each as provider/model.
unavailable_models[]Exact model IDs to skip in the config hook before the first provider call. Useful when OpenCode fails before emitting session.error.
retry_on_errors[429, 500, 502, 503, 504]HTTP status codes that trigger fallback.
retry_on_patterns[]Extra case-insensitive regex sources matched against the error text, appended to the built-in patterns. Invalid regexes are skipped.
max_attempts3Max fallback retries within attempts_window_ms.
attempts_window_ms600000Sliding window (ms) for max_attempts. Older attempts stop counting once outside it. Set 0 for a lifetime cap.
cooldown_ms60000How long to avoid a failed model.
backoff_ms0Base delay for exponential backoff with equal jitter before a retry. 0 = instant.
backoff_max_ms30000Cap for the computed backoff delay.
recover_original_modeltrueReturn to the original model once its cooldown expires instead of staying on the fallback.
notifytrueShow a toast when switching or recovering models.
debugfalseWrite a [model-fallback] … trace to stderr explaining every fallback decision.

Session Control

The plugin registers one tool:

model_fallback_control(action: "enable" | "disable" | "status" | "reset")

Use it from inside OpenCode to control fallback for the current session:

Use model_fallback_control to disable fallback in this session.
ActionEffect
enableEnable fallback for the current session.
disableDisable fallback for the current session.
statusReturn effective state, current model, attempts, windowed attempt count, per-model failure counts, active cooldowns, recent switches, and configured fallbacks.
resetClear session state and return to the config default.

Session overrides live in memory only — they disappear when the session is deleted or OpenCode restarts.

unavailable_models vs fallback_models

They look similar but act at different times:

fallback_modelsunavailable_models
PurposeRetry on these when the current model fails with a transient error.Skip these up front because they are known-bad (deprecated, no credentials, etc.).
WhenAfter a session.error or on recovery.Before any provider call.
TriggerA retryable error at runtime.Static configuration — no error needed.
Example"If claude-opus is rate-limited, try gpt-5.1-codex.""Never select claude-3-opus; OpenCode errors before session.error."

A model can be in both lists, but that's redundant — anything in unavailable_models is already filtered out when the plugin picks the next fallback.

What Counts As Retryable

Fallback triggers on configured status codes and common transient error text:

  • rate limit / too many requests
  • quota exceeded
  • all credentials for model exhausted
  • model unsupported
  • service unavailable / overloaded / temporarily unavailable
  • "try again" with a transient qualifier (later / soon / shortly / in Ns) — a bare "try again" does not match
  • 429, 503, 529 in plain text errors

Status codes and text are also detected through the wrapped error.cause chain, so a 429/503 buried in a fetch error still triggers fallback. Add your own phrases with retry_on_patterns.

Troubleshooting

Fallback never triggers

Turn on debug: true and reproduce. The [model-fallback] … lines in the OpenCode log explain each skip: disabled, no fallback_models, "not retryable" (status/text unmatched), max_attempts reached, or no available model.

Fallback triggers too aggressively

A provider's non-transient message may be matching a pattern. Check debug output, then either narrow retry_on_errors or set retry_on_patterns to only the phrases you want. Note that a bare "try again" no longer matches by default.

Session stuck on a fallback model

Expected only while the original model's cooldown (cooldown_ms) is active. Once it expires the next message recovers to the original model — unless recover_original_model: false or the original is in unavailable_models.

Fallback stops retrying partway through a long session

max_attempts counts within attempts_window_ms (default 10 min). Raise max_attempts, lengthen attempts_window_ms, or set attempts_window_ms: 0 for an absolute lifetime cap.

A known-broken model still gets selected

List it in unavailable_models so the plugin skips it before any provider call, not only after it fails.

Limitations

OpenCode does not let a plugin swap the model inside a failed stream. This plugin starts a new prompt in the same session with the last user message and the fallback model.

It does not restore a half-finished tool-call turn. If the model failed after starting tool calls, the retry is a new model turn from the last user request.

Development

bun install
bun test
bun run typecheck
bun run build

See CHANGELOG.md for release history.

License

MIT


This project is independent and is not built by, endorsed by, or affiliated with the OpenCode team.

Releases

Packages

Used by

Contributors

Languages