Skip to content

Release v1.1.0#15

Merged
ShutovKS merged 13 commits into
mainfrom
dev
Jul 14, 2026
Merged

Release v1.1.0#15
ShutovKS merged 13 commits into
mainfrom
dev

Conversation

@ShutovKS

Copy link
Copy Markdown
Owner

Release v1.1.0

13 PRs merged into dev since v1.0.8. Full changelog in CHANGELOG.md.

Added

Changed

Fixed

Tests

🤖 Generated with Claude Code

ShutovKS and others added 13 commits July 14, 2026 02:07
state.failedUntil accumulated one entry per failed model for the lifetime
of a session and was never cleaned up. Expired cooldowns are now pruned on
each session.error, keeping the map bounded.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped fetch errors (e.g. `new TypeError("fetch failed", { cause: {...} })`)
carry the real status code and message on `error.cause`. statusCode and
errorText now walk the cause chain (bounded to depth 5, safe against cycles),
so a 429/503 buried one or more levels deep still triggers fallback.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#4)

The `/try.?again/` pattern matched non-transient messages (e.g. "try again
with a different prompt"). It now requires the transient qualifier
(later/soon/shortly/moment/in Ns).

Adds a `retry_on_patterns` option: user-supplied regex sources (case-
insensitive) appended to the built-in patterns. Invalid sources are skipped
rather than crashing plugin init. isRetryableError takes an optional
patterns argument (defaults preserved for existing callers).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Previously a single transient error switched the session to a fallback
model permanently: chat.message forced currentModel for the rest of the
session and never returned to the user's chosen model.

Now, on chat.message, once the original model's cooldown has elapsed the
plugin restores it (resetting attempts and notifying via toast) instead of
staying on the fallback. Gated by the new `recover_original_model` option
(default true); skipped if the original is in unavailable_models.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
state.attempts was a lifetime counter that only reset on manual switch or
reset, so a few early transient blips could disable fallback for the rest of
a long-running session.

Fallback attempts are now timestamped and counted within a sliding window
(`attempts_window_ms`, default 10 min). Once the window passes, older
attempts no longer count against `max_attempts`. Set `attempts_window_ms: 0`
to restore the previous absolute lifetime limit. status now also reports
`windowAttempts` and `attemptsWindowMs`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
On a transient error the plugin re-prompted the fallback model instantly,
which can hammer a provider that is rate-limiting all of its models.

Adds `backoff_ms` (default 0 = instant, unchanged) and `backoff_max_ms`
(default 30s cap). When set, the plugin waits computeBackoff() = exponential
growth per attempt with equal jitter, clamped to the cap, before the retry.
computeBackoff is exported and unit-tested with an injected RNG.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
When `debug: true`, the plugin writes a single-line `[model-fallback] ...`
trace to stderr for every fallback decision: why a retry fired or was
skipped (disabled, no fallback_models, not retryable, max_attempts reached,
stale error, no available model, in-flight), the backoff delay, the actual
switch, and recovery to the original model. Default off; no output otherwise.

OpenCode surfaces plugin stderr in its log stream, so this is greppable
without exposing a logger to plugins.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The model_fallback_control status now reports `failureCounts` (how many
retryable errors each model hit) and `switches` (last 20 model transitions
with from/to/reason/timestamp; reasons: fallback, unavailable, recovery).

Useful for diagnosing which models are flaky in a long session. Cleared on
reset alongside the rest of the session state.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Documents all unreleased changes from #2-#9 and the 1.0.8 initial release.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
)

- Document all new options (retry_on_patterns, attempts_window_ms,
  backoff_ms, backoff_max_ms, recover_original_model, debug) in the options
  table and config example.
- Add a `unavailable_models` vs `fallback_models` comparison table.
- Update the "What Counts As Retryable" section (narrowed try-again,
  error.cause chain).
- Add a Troubleshooting section covering the common failure modes.
- Update the status tool description to mention failureCounts/switches.
- Link the CHANGELOG.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…at.message (#12)

Add test/e2e-server.test.ts: boots a real headless OpenCode server
(createOpencodeServer) with a fake OpenAI provider, drives it through the
SDK client, and observes plugin decisions via the SSE event stream. Covers
status-path fallback, retry_on_patterns, cascade fallback, and recovery.

The recovery e2e exposed a real bug: OpenCode invokes the chat.message hook
for the plugin's own retry prompt. Since pendingModel was set only after
promptAsync resolved, that echoed hook call was treated as a manual model
switch, overwriting originalModel with the fallback and silently disabling
recover_original_model in the live runtime. Set pendingModel/awaitingModel
before prompting (and roll back if the retry is not accepted).

Also: chat.message now backfills state models via getState when a model is
present (session.created carries no model in the live runtime), and
cascade-model uses 400 (OpenCode self-retries 429/503 before emitting
session.error, masking plugin-driven cascade). New unit test reproduces the
echoed-prompt runtime behavior.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
PR #3 (suppressed comment): errorText only walked `cause` for Error
instances; plain-object wrappers fell through to JSON.stringify, which
serializes a nested Error to {} and drops retryable text. Walk `cause`
explicitly for records too, with the same depth cap.

PR #2 (inline comment): the prune test only proved an expired cooldown made
the model selectable again — pruning itself was unobservable. Expose active
cooldowns as `cooling` in model_fallback_control status and assert the
expired entries are gone.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bump version, finalize CHANGELOG, add @opencode-ai/sdk devDependency
(used by the in-process e2e suite; was resolving only transitively).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 14, 2026 21:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ShutovKS ShutovKS merged commit 3fa4599 into main Jul 14, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants