pi-model-fallback 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +3 -3
- package/docs/usage.md +1 -1
- package/lib/error-status.ts +30 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## [0.5.2] - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Update `@earendil-works/pi-*` development dependencies to `1.0.4`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Recognize a status in parentheses when a provider error wrapper precedes it, for example `omni API error (503): ...`. Previously such messages yielded no status, so rules listing `503` were never evaluated even when configured.
|
|
14
|
+
|
|
15
|
+
## [0.5.1] - 2026-09-30
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Update `@earendil-works/pi-*` dependencies to `0.99.1`.
|
|
20
|
+
|
|
5
21
|
## [0.5.0] - 2026-09-22
|
|
6
22
|
|
|
7
23
|
### Added
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Rule fields:
|
|
|
97
97
|
Top-level fields:
|
|
98
98
|
|
|
99
99
|
- `enabled`: toggle the extension without deleting rules
|
|
100
|
-
- `autoRetry`: when a fallback fires from a failed turn, automatically re-queue the failed user prompt so the turn retries on the fallback model (defaults to `true`; set to `false` to only switch models). The failed prompt is queued once,
|
|
100
|
+
- `autoRetry`: when a fallback fires from a failed turn, automatically re-queue the failed user prompt so the turn retries on the fallback model (defaults to `true`; set to `false` to only switch models). The failed prompt is queued once per fallback transition, so a fallback chain may replay it more than once.
|
|
101
101
|
|
|
102
102
|
Rules use first-match order: the first rule whose provider/model and status match wins. Put specific `matchModels` rules before broad `matchProviders` rules when they should take priority. `model_fallback_config validate`, `save`, `read`, and `status` report warning-only diagnostics when a later rule or model entry is completely shadowed by an earlier rule; `/model-fallback:status` also includes a concise warning summary for the current config. Warning-bearing config remains valid and can still be saved.
|
|
103
103
|
|
|
@@ -123,8 +123,8 @@ If the package is installed project-locally and the current project references i
|
|
|
123
123
|
|
|
124
124
|
- Successful responses do nothing.
|
|
125
125
|
- Matching failures from `after_provider_response` can trigger fallback immediately.
|
|
126
|
-
- Assistant error messages parsed at `turn_end` can also persist fallback state for SDK/provider failures that do not emit the normal response hook. Status extraction looks for HTTP-style tokens (for example `status 429`, `HTTP 503`, or `rate limit`) rather than any bare 3-digit number in the message.
|
|
127
|
-
- With the default `autoRetry: true`, the failed prompt is automatically queued once on the fallback model. Set `autoRetry: false` to switch models without replaying it.
|
|
126
|
+
- Assistant error messages parsed at `turn_end` can also persist fallback state for SDK/provider failures that do not emit the normal response hook. Status extraction looks for HTTP-style tokens (for example `status 429`, `HTTP 503`, or `rate limit`) rather than any bare 3-digit number in the message. A status in parentheses is only recognized when an error wrapper keyword precedes it, as in `omni API error (503): ...`; a parenthesized number with no such wrapper, such as `(503) items processed`, stays unmatched.
|
|
127
|
+
- With the default `autoRetry: true`, the failed prompt is automatically queued once per fallback transition on the fallback model. A fallback chain may therefore replay the prompt more than once. Set `autoRetry: false` to switch models without replaying it.
|
|
128
128
|
|
|
129
129
|
Rules never fall back to the failing model itself; a rule whose fallback target equals the failing model is skipped so the next matching rule (or no rule) applies.
|
|
130
130
|
|
package/docs/usage.md
CHANGED
|
@@ -6,7 +6,7 @@ Install:
|
|
|
6
6
|
pi install npm:pi-model-fallback
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
Default behavior: when a `zai/*` model receives `429`, `500`, `502`, `503`, or `504`, Pi switches to `deepseek/deepseek-v4-flash` for the next prompt. The failed prompt is automatically queued once on the fallback model; set `"autoRetry": false` in `model-fallback/config.json` to switch models without retrying it.
|
|
9
|
+
Default behavior: when a `zai/*` model receives `429`, `500`, `502`, `503`, or `504`, Pi switches to `deepseek/deepseek-v4-flash` for the next prompt. The failed prompt is automatically queued once per fallback transition on the fallback model, so a fallback chain may replay it more than once; set `"autoRetry": false` in `model-fallback/config.json` to switch models without retrying it.
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
12
|
|
package/lib/error-status.ts
CHANGED
|
@@ -15,7 +15,30 @@ export function parseReasonFromErrorMessage(message: string): string | undefined
|
|
|
15
15
|
return undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Failure modes where the transport reports HTTP success but the provider
|
|
19
|
+
// yields nothing usable — no numeric status is present in any of them, so the
|
|
20
|
+
// status parser must synthesize one for the fallback rules to fire:
|
|
21
|
+
// - "Provider returned an empty response" (OpenRouter stealth/pool models)
|
|
22
|
+
// - "Stream ended without finish_reason" (chunk stream truncated mid-flight)
|
|
23
|
+
// - "<provider> response has no body" (Anthropic/Mistral/generic, 200 empty body)
|
|
24
|
+
// - "<provider> stream ended without a terminal event" (SSE stream dies silently)
|
|
25
|
+
const EMPTY_PROVIDER_RESPONSE_PATTERNS: RegExp[] = [
|
|
26
|
+
/provider returned an empty response/i,
|
|
27
|
+
/response (has|with) no body/i,
|
|
28
|
+
/stream ended without a terminal event/i,
|
|
29
|
+
/stream ended without finish_reason/i,
|
|
30
|
+
];
|
|
31
|
+
|
|
18
32
|
export function parseStatusFromErrorMessage(message: string): number | undefined {
|
|
33
|
+
for (const pattern of EMPTY_PROVIDER_RESPONSE_PATTERNS) {
|
|
34
|
+
if (pattern.test(message)) {
|
|
35
|
+
// Synthesize 502 so status-based fallback rules fire instead of the
|
|
36
|
+
// agent silently retrying the same model. 502 is deliberately chosen
|
|
37
|
+
// over a novel code: it lands in the 5xx status lists users already
|
|
38
|
+
// configure and inherits the standard 10-minute cooldown.
|
|
39
|
+
return 502;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
19
42
|
const patterns: RegExp[] = [
|
|
20
43
|
// Leading bare status code, optionally prefixed with "Error:", e.g. "401: {\"type\":\"CreditsError\"...}"
|
|
21
44
|
// or "Error: 401: {\"type\":\"CreditsError\"...}" (OpenCode Go, OpenRouter relays)
|
|
@@ -28,6 +51,13 @@ export function parseStatusFromErrorMessage(message: string): number | undefined
|
|
|
28
51
|
// "429 tokens remaining in context window", which must stay unmatched.
|
|
29
52
|
/^\s*error\s*:?\s*([1-5][0-9]{2})\b/i,
|
|
30
53
|
/\b(?:status|code|http)\s*[:\s]?\s*([1-5][0-9]{2})\b/i,
|
|
54
|
+
// Parenthesized status inside a provider error wrapper, e.g.
|
|
55
|
+
// "omni API error (503): overloaded" or "chat_completion_error (429): ...".
|
|
56
|
+
// Without this the wrapper text sits between "error" and the number, so the
|
|
57
|
+
// anchored "error <status>" pattern cannot see it and the rule set never
|
|
58
|
+
// sees a status at all. The wrapper keyword and the parentheses are both
|
|
59
|
+
// required so prose such as "(503) items processed" stays unmatched.
|
|
60
|
+
/\b\w*(?:error|failure|failed)\s*\(\s*([1-5][0-9]{2})\s*\)/i,
|
|
31
61
|
/\bHTTP\/\d(?:\.\d)?\s+([1-5][0-9]{2})\b/,
|
|
32
62
|
/\b(?:rate[\s-]?limit(?:ed)?|too many requests)[^0-9]{0,40}([45][0-9]{2})\b/i,
|
|
33
63
|
/\b([45][0-9]{2})\s+(?:error|quota|too many requests|service unavailable|bad gateway|gateway timeout|internal server error)\b/i,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-model-fallback",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Pi extension that switches to a fallback model after provider failures such as 429 rate limits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"typebox": "*"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
54
|
-
"@earendil-works/pi-ai": "^0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
56
|
-
"@earendil-works/pi-tui": "^0.
|
|
57
|
-
"@types/node": "^26.
|
|
53
|
+
"@earendil-works/pi-agent-core": "^1.0.4",
|
|
54
|
+
"@earendil-works/pi-ai": "^1.0.4",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "^1.0.4",
|
|
56
|
+
"@earendil-works/pi-tui": "^1.0.4",
|
|
57
|
+
"@types/node": "^26.6.3",
|
|
58
58
|
"jiti": "^2.7.0",
|
|
59
59
|
"typebox": "latest",
|
|
60
60
|
"typescript": "^7.0.2"
|