feat: detect retryable errors through error.cause chain#3
Merged
Conversation
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>
There was a problem hiding this comment.
Pull request overview
This PR improves the model-fallback plugin’s retryability detection by following error.cause chains so wrapped fetch-style errors can still trigger fallback, and adds tests to cover the new behavior.
Changes:
- Refactors status extraction into
numericField/ownStatusCodeand makesstatusCode()recurse througherror.causewith a max depth. - Extends
errorText()to incorporateError.causetext (depth-limited) when the input is anError. - Adds helper tests for wrapped errors, nested cause chains, and circular cause chains.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/plugin.ts |
Adds depth-limited cause traversal for status-code detection and enhances error text aggregation for Error instances. |
test/index.test.ts |
Adds unit tests covering retryability via error.cause, nested causes, and cycle termination. |
Comments suppressed due to low confidence (1)
src/plugin.ts:231
errorTextonly walks thecausechain when the value is anError. For plain objects it falls back toJSON.stringify(error), which drops text ifcauseis anError(JSON.stringify(new Error("x")) -> "{}"). That can miss retryable text buried inerror.causefor non-Error wrappers. Consider handlingRecordvalues by extractingname/message(when present) and recursively appendingcausetext with the same depth cap.
try {
return JSON.stringify(error)
} catch {
return String(error)
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ShutovKS
added a commit
that referenced
this pull request
Jul 14, 2026
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>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Проблема
statusCode/errorTextсмотрели только наerror,error.data,error.response. Обёрнутые fetch-ошибки (new TypeError("fetch failed", { cause: { statusCode: 429 } })) прятали реальный код/текст вerror.cause, и fallback не срабатывал.Решение
Обе функции теперь проходят по цепочке
error.cause(ограничение глубины 5, безопасно к циклам). 429/503, лежащий на один-несколько уровней глубже, снова триггерит fallback. Заодно вынесенnumericField/ownStatusCode— меньше дублирования.Тесты
Добавлены кейсы: статус на
cause, вложенная цепочкаcause.cause, Error с текстовымcause, циклическая ссылка (терминируется). Юнит-тесты 40/40, typecheck чист.🤖 Generated with Claude Code