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 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, not retried indefinitely.
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
 
@@ -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.0",
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.86.1",
54
- "@earendil-works/pi-ai": "^0.86.1",
55
- "@earendil-works/pi-coding-agent": "^0.86.1",
56
- "@earendil-works/pi-tui": "^0.86.1",
57
- "@types/node": "^26.0.0",
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"