oira666_pi-limits-wait 0.4.11 → 0.4.12

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/README.md CHANGED
@@ -1,87 +1,87 @@
1
- # Pi Limits Wait
2
-
3
- [![npm](https://img.shields.io/npm/v/oira666_pi-limits-wait?style=flat-square&logo=npm&logoColor=white&label=npm&color=7c3aed)](https://www.npmjs.com/package/oira666_pi-limits-wait) [![node](https://img.shields.io/badge/node-%3E%3D18-7c3aed?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org)
4
-
5
- Pi extension that keeps requests alive when a provider rate-limits you or returns `server_is_overloaded`.
6
-
7
- ## Install
8
-
9
- ```bash
10
- pi install npm:oira666_pi-limits-wait
11
- ```
12
-
13
- Restart Pi or run `/reload` after installing.
14
-
15
- ## What it does
16
-
17
- - Works with all Pi model providers/APIs that use `streamSimple`.
18
- - On rate-limit errors (`429`, `rate_limit`, `too many requests`, quota/reset messages), waits and retries in a loop.
19
- - Uses provider retry timing when available (`retry-after`, `retry-after-ms`, `retry in ...`, reset messages).
20
- - If no retry timing is available for a rate limit, waits 30 minutes before retrying.
21
- - On `server_is_overloaded`, waits 5 minutes, then retries. If the provider is still overloaded after Pi's normal retries, it waits another 5 minutes and repeats.
22
- - On transient network/transport failures — including undici idle-timeout aborts (`UND_ERR_HEADERS_TIMEOUT` / `UND_ERR_BODY_TIMEOUT`), `fetch failed`, `terminated`, `ECONNRESET`, `ETIMEDOUT`, etc. — treats the error as retryable with a short backoff (default 15s, or the provider's `retry-after`) instead of giving up. This prevents a stalled streaming request from turning into a silent hang.
23
- - For any other (unclassified) non-retryable error, retries the same model a few times (default 3) before falling back / freezing it, so a one-off hiccup does not immediately sideline a model.
24
- - Reports retry/fallback waits in chat notifications without adding persistent TUI status lines.
25
- - Press Enter during a retry wait to skip the wait and retry immediately.
26
- - Optionally falls back to configured models when the current/default model is rate-limited.
27
-
28
- ## Optional fallback models
29
-
30
- By default, if you do not configure fallback models, the extension behaves exactly as before: it waits for the current model's limit to reset and retries.
31
-
32
- To enable automatic model fallback, add `fallback-models` to a `limits-wait.json` file.
33
-
34
- The extension reads these files in order; later files override earlier ones:
35
-
36
- | Location | Scope |
37
- |----------|-------|
38
- | `~/.config/.pi/limits-wait.json` | Global defaults |
39
- | `<Pi agent dir>/limits-wait.json` (for example `~/.pi/agent/limits-wait.json`, or `PI_CODING_AGENT_DIR/limits-wait.json`) | Pi agent directory |
40
- | `.limits-wait.json` | Project root override |
41
- | `.pi/limits-wait.json` | Pi project override |
42
-
43
- Example:
44
-
45
- ```json
46
- {
47
- "fallback-models": [
48
- {
49
- "provider": "anthropic",
50
- "modelname": "claude-sonnet-4-5",
51
- "reasoning effort": "medium"
52
- },
53
- {
54
- "provider": "openai",
55
- "modelname": "gpt-5.1-codex"
56
- }
57
- ]
58
- }
59
- ```
60
-
61
- Each `fallback-models` entry supports:
62
-
63
- - `provider` — required. The Pi provider name, for example `anthropic`, `openai`, `google`, etc.
64
- - `modelname` — required. The model id/name as Pi knows it.
65
- - `reasoning effort` — optional. One of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. If omitted, Pi's current/default reasoning level is used.
66
-
67
- Fallback behavior:
68
-
69
- 1. Pi starts with the normal default or user-selected model.
70
- 2. If that model is rate-limited, the extension tries models in this order:
71
- - the original default/user-selected model;
72
- - then every model from `fallback-models`, top to bottom.
73
- 3. The first model that responds without a rate-limit becomes the active Pi model for the rest of the session/task.
74
- 4. If that model later becomes rate-limited too, the extension starts again from the same ordered list.
75
- 5. Rate-limit reset times are remembered only in memory, so known-limited models are skipped until their countdown expires. This memory is cleared when Pi restarts.
76
-
77
- When the settings are loaded, the extension shows the full usable fallback model list. When models become rate-limited, it reports the wait in chat notifications.
78
-
79
- ## Environment variables
80
-
81
- | Variable | Default | Description |
82
- |----------|---------|-------------|
83
- | `PI_LIMITS_WAIT_FREEZING_ENABLED` | `true` | When a model keeps failing with a non-retryable error, it is normally "frozen" for 1 hour and skipped in favour of other configured models. Set this to `false` (also accepts `0`, `no`, `off`) to disable freezing entirely: the extension will instead try each configured candidate once (after the bounded retries) and then surface the error, never blocking on a long "model-frozen" wait. Useful for non-interactive / subagent runs where no one can press Enter to skip. |
84
-
85
- ## License
86
-
87
- MIT
1
+ # Pi Limits Wait
2
+
3
+ [![npm](https://img.shields.io/npm/v/oira666_pi-limits-wait?style=flat-square&logo=npm&logoColor=white&label=npm&color=7c3aed)](https://www.npmjs.com/package/oira666_pi-limits-wait) [![node](https://img.shields.io/badge/node-%3E%3D18-7c3aed?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org)
4
+
5
+ Pi extension that keeps requests alive when a provider rate-limits you or returns `server_is_overloaded`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:oira666_pi-limits-wait
11
+ ```
12
+
13
+ Restart Pi or run `/reload` after installing.
14
+
15
+ ## What it does
16
+
17
+ - Works with all Pi model providers/APIs that use `streamSimple`.
18
+ - On rate-limit errors (`429`, `rate_limit`, `too many requests`, quota/reset messages), waits and retries in a loop.
19
+ - Uses provider retry timing when available (`retry-after`, `retry-after-ms`, `retry in ...`, reset messages).
20
+ - If no retry timing is available for a rate limit, waits 30 minutes before retrying.
21
+ - On `server_is_overloaded`, waits 5 minutes, then retries. If the provider is still overloaded after Pi's normal retries, it waits another 5 minutes and repeats.
22
+ - On transient network/transport failures — including undici idle-timeout aborts (`UND_ERR_HEADERS_TIMEOUT` / `UND_ERR_BODY_TIMEOUT`), `fetch failed`, `terminated`, `ECONNRESET`, `ETIMEDOUT`, etc. — treats the error as retryable with a short backoff (default 15s, or the provider's `retry-after`) instead of giving up. This prevents a stalled streaming request from turning into a silent hang.
23
+ - For any other (unclassified) non-retryable error, retries the same model a few times (default 3) before falling back / freezing it, so a one-off hiccup does not immediately sideline a model.
24
+ - Reports retry/fallback waits in chat notifications without adding persistent TUI status lines.
25
+ - Press Enter during a retry wait to skip the wait and retry immediately.
26
+ - Optionally falls back to configured models when the current/default model is rate-limited.
27
+
28
+ ## Optional fallback models
29
+
30
+ By default, if you do not configure fallback models, the extension behaves exactly as before: it waits for the current model's limit to reset and retries.
31
+
32
+ To enable automatic model fallback, add `fallback-models` to a `limits-wait.json` file.
33
+
34
+ The extension reads these files in order; later files override earlier ones:
35
+
36
+ | Location | Scope |
37
+ |----------|-------|
38
+ | `~/.config/.pi/limits-wait.json` | Global defaults |
39
+ | `<Pi agent dir>/limits-wait.json` (for example `~/.pi/agent/limits-wait.json`, or `PI_CODING_AGENT_DIR/limits-wait.json`) | Pi agent directory |
40
+ | `.limits-wait.json` | Project root override |
41
+ | `.pi/limits-wait.json` | Pi project override |
42
+
43
+ Example:
44
+
45
+ ```json
46
+ {
47
+ "fallback-models": [
48
+ {
49
+ "provider": "anthropic",
50
+ "modelname": "claude-sonnet-4-5",
51
+ "reasoning effort": "medium"
52
+ },
53
+ {
54
+ "provider": "openai",
55
+ "modelname": "gpt-5.1-codex"
56
+ }
57
+ ]
58
+ }
59
+ ```
60
+
61
+ Each `fallback-models` entry supports:
62
+
63
+ - `provider` — required. The Pi provider name, for example `anthropic`, `openai`, `google`, etc.
64
+ - `modelname` — required. The model id/name as Pi knows it.
65
+ - `reasoning effort` — optional. One of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`. If omitted, Pi's current/default reasoning level is used.
66
+
67
+ Fallback behavior:
68
+
69
+ 1. Pi starts with the normal default or user-selected model.
70
+ 2. If that model is rate-limited, the extension tries models in this order:
71
+ - the original default/user-selected model;
72
+ - then every model from `fallback-models`, top to bottom.
73
+ 3. The first model that responds without a rate-limit becomes the active Pi model for the rest of the session/task.
74
+ 4. If that model later becomes rate-limited too, the extension starts again from the same ordered list.
75
+ 5. Rate-limit reset times are remembered only in memory, so known-limited models are skipped until their countdown expires. This memory is cleared when Pi restarts.
76
+
77
+ When the settings are loaded, the extension shows the full usable fallback model list. When models become rate-limited, it reports the wait in chat notifications.
78
+
79
+ ## Environment variables
80
+
81
+ | Variable | Default | Description |
82
+ |----------|---------|-------------|
83
+ | `PI_LIMITS_WAIT_FREEZING_ENABLED` | `true` | When a model keeps failing with a non-retryable error, it is normally "frozen" for 1 hour and skipped in favour of other configured models. Set this to `false` (also accepts `0`, `no`, `off`) to disable freezing entirely: the extension will instead try each configured candidate once (after the bounded retries) and then surface the error, never blocking on a long "model-frozen" wait. Useful for non-interactive / subagent runs where no one can press Enter to skip. |
84
+
85
+ ## License
86
+
87
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-limits-wait",
3
- "version": "0.4.11",
3
+ "version": "0.4.12",
4
4
  "description": "Extension for PI that allows to automatically wait for limits to reset and retry the request",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/constants.ts CHANGED
@@ -1,23 +1,26 @@
1
- export const CLAUDE_CODE_IDENTITY =
2
- "You are Claude Code, Anthropic's official CLI for Claude.";
3
-
4
- export const PI_REMOVAL_ANCHORS = [
5
- "pi-coding-agent",
6
- "@mariozechner/pi-coding-agent",
7
- "badlogic/pi-mono",
8
- ] as const;
9
-
10
- export const PI_IDENTITY_SENTENCE_PATTERN =
11
- /(?:^|\n)\s*You are pi\b[^.!?\n]*(?:[.!?](?=\s|$)|(?=\n|$))/gi;
12
-
13
- export const DEFAULT_RATE_LIMIT_WAIT_MS = 30 * 60 * 1_000; // 30 minutes
14
- export const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
15
- export const DEFAULT_NETWORK_WAIT_MS = 15 * 1_000; // 15 seconds
16
- export const DEFAULT_NON_RETRYABLE_FREEZE_MS = 10 * 60 * 1_000; // 10 minutes
17
-
18
- export const NON_RETRYABLE_MAX_ATTEMPTS = 3;
19
- export const NON_RETRYABLE_RETRY_DELAY_MS = 5 * 1_000; // 5 seconds
20
-
21
- export const SETTINGS_FILE_NAME = "limits-wait.json";
22
- export const FALLBACK_MODELS_KEY = "fallback-models";
23
- export const FREEZING_ENV_VAR = "PI_LIMITS_WAIT_FREEZING_ENABLED";
1
+ export const CLAUDE_CODE_IDENTITY =
2
+ "You are Claude Code, Anthropic's official CLI for Claude.";
3
+
4
+ export const PI_REMOVAL_ANCHORS = [
5
+ "pi-coding-agent",
6
+ "@mariozechner/pi-coding-agent",
7
+ "badlogic/pi-mono",
8
+ ] as const;
9
+
10
+ export const PI_IDENTITY_SENTENCE_PATTERN =
11
+ /(?:^|\n)\s*You are pi\b[^.!?\n]*(?:[.!?](?=\s|$)|(?=\n|$))/gi;
12
+
13
+ export const CLAUDE_CODE_IDENTITY_PATTERN =
14
+ /(?:^|\n)\s*You are Claude Code, Anthropic's official CLI for Claude\.\s*/gi;
15
+
16
+ export const DEFAULT_RATE_LIMIT_WAIT_MS = 30 * 60 * 1_000; // 30 minutes
17
+ export const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
18
+ export const DEFAULT_NETWORK_WAIT_MS = 15 * 1_000; // 15 seconds
19
+ export const DEFAULT_NON_RETRYABLE_FREEZE_MS = 10 * 60 * 1_000; // 10 minutes
20
+
21
+ export const NON_RETRYABLE_MAX_ATTEMPTS = 3;
22
+ export const NON_RETRYABLE_RETRY_DELAY_MS = 5 * 1_000; // 5 seconds
23
+
24
+ export const SETTINGS_FILE_NAME = "limits-wait.json";
25
+ export const FALLBACK_MODELS_KEY = "fallback-models";
26
+ export const FREEZING_ENV_VAR = "PI_LIMITS_WAIT_FREEZING_ENABLED";
@@ -1,49 +1,49 @@
1
- import { DEFAULT_RATE_LIMIT_WAIT_MS } from "./constants.js";
2
- import { retryAfterHeaderMs } from "./retry-errors.js";
3
- import { state } from "./state.js";
4
- import { formatErrorDetail, showAmbientRetryStatus, clearAmbientRetryStatus } from "./ui.js";
5
-
6
- async function responseErrorMessage(response: Response): Promise<string> {
7
- const retryAfterMs = retryAfterHeaderMs(response.headers);
8
- const retryAfter = retryAfterMs !== undefined ? ` retry-after-ms ${retryAfterMs}` : "";
9
- let body = "";
10
- try {
11
- body = await response.clone().text();
12
- } catch {
13
- // Ignore body read failures; status and retry headers are enough.
14
- }
15
- const detail = body.trim() ? ` ${formatErrorDetail(body)}` : "";
16
- return `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}${retryAfter}${detail}`;
17
- }
18
-
19
- export function installFetchRateLimitObserver(): void {
20
- if (state.restoreFetch || typeof globalThis.fetch !== "function") return;
21
-
22
- const originalFetch = globalThis.fetch.bind(globalThis);
23
- globalThis.fetch = (async (...args: Parameters<typeof fetch>) => {
24
- const response = await originalFetch(...args);
25
-
26
- if (state.activeProviderRequests > 0) {
27
- if (!response.ok) {
28
- const message = await responseErrorMessage(response);
29
- state.lastObservedHttpError = { at: Date.now(), message };
30
- if (response.status === 429) {
31
- const waitMs = retryAfterHeaderMs(response.headers) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
32
- showAmbientRetryStatus("rate-limit", waitMs);
33
- if (state.activeFallbackProviderRequests > 0) {
34
- throw new Error(message);
35
- }
36
- }
37
- } else if (state.ambientStatusCleanup && response.ok) {
38
- clearAmbientRetryStatus();
39
- }
40
- }
41
-
42
- return response;
43
- }) as typeof fetch;
44
-
45
- state.restoreFetch = () => {
46
- globalThis.fetch = originalFetch as typeof fetch;
47
- state.restoreFetch = undefined;
48
- };
49
- }
1
+ import { DEFAULT_RATE_LIMIT_WAIT_MS } from "./constants.js";
2
+ import { retryAfterHeaderMs } from "./retry-errors.js";
3
+ import { state } from "./state.js";
4
+ import { formatErrorDetail, showAmbientRetryStatus, clearAmbientRetryStatus } from "./ui.js";
5
+
6
+ async function responseErrorMessage(response: Response): Promise<string> {
7
+ const retryAfterMs = retryAfterHeaderMs(response.headers);
8
+ const retryAfter = retryAfterMs !== undefined ? ` retry-after-ms ${retryAfterMs}` : "";
9
+ let body = "";
10
+ try {
11
+ body = await response.clone().text();
12
+ } catch {
13
+ // Ignore body read failures; status and retry headers are enough.
14
+ }
15
+ const detail = body.trim() ? ` ${formatErrorDetail(body)}` : "";
16
+ return `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}${retryAfter}${detail}`;
17
+ }
18
+
19
+ export function installFetchRateLimitObserver(): void {
20
+ if (state.restoreFetch || typeof globalThis.fetch !== "function") return;
21
+
22
+ const originalFetch = globalThis.fetch.bind(globalThis);
23
+ globalThis.fetch = (async (...args: Parameters<typeof fetch>) => {
24
+ const response = await originalFetch(...args);
25
+
26
+ if (state.activeProviderRequests > 0) {
27
+ if (!response.ok) {
28
+ const message = await responseErrorMessage(response);
29
+ state.lastObservedHttpError = { at: Date.now(), message };
30
+ if (response.status === 429) {
31
+ const waitMs = retryAfterHeaderMs(response.headers) ?? DEFAULT_RATE_LIMIT_WAIT_MS;
32
+ showAmbientRetryStatus("rate-limit", waitMs);
33
+ if (state.activeFallbackProviderRequests > 0) {
34
+ throw new Error(message);
35
+ }
36
+ }
37
+ } else if (state.ambientStatusCleanup && response.ok) {
38
+ clearAmbientRetryStatus();
39
+ }
40
+ }
41
+
42
+ return response;
43
+ }) as typeof fetch;
44
+
45
+ state.restoreFetch = () => {
46
+ globalThis.fetch = originalFetch as typeof fetch;
47
+ state.restoreFetch = undefined;
48
+ };
49
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { CLAUDE_CODE_IDENTITY } from "./constants.js";
3
2
  import { isInternalSyntheticModel } from "./models.js";
4
- import { isAnthropicOAuthSession, sanitiseSystemPrompt } from "./prompt.js";
5
3
  import { loadFallbackSettings } from "./settings.js";
6
4
  import { state } from "./state.js";
7
5
  import { registerWrappedApi } from "./stream.js";
@@ -44,7 +42,7 @@ export default function (pi: ExtensionAPI) {
44
42
  state.primaryThinkingLevel = pi.getThinkingLevel();
45
43
  });
46
44
 
47
- pi.on("before_agent_start", (event, ctx) => {
45
+ pi.on("before_agent_start", (_event, ctx) => {
48
46
  state.sharedCtx = ctx;
49
47
  if (!state.primaryModel && ctx.model && !isInternalSyntheticModel(ctx.model)) state.primaryModel = ctx.model;
50
48
  state.primaryThinkingLevel ??= pi.getThinkingLevel();
@@ -57,14 +55,9 @@ export default function (pi: ExtensionAPI) {
57
55
  }
58
56
  if (ctx.model && !isInternalSyntheticModel(ctx.model)) registerWrappedApi(pi, ctx.model.api);
59
57
 
60
- // Anthropic subscription/OAuth requests identify as Claude Code, not Pi.
61
- if (!isAnthropicOAuthSession(ctx)) return;
62
-
63
- const sanitised = sanitiseSystemPrompt(event.systemPrompt);
64
- return {
65
- systemPrompt: sanitised
66
- ? `${CLAUDE_CODE_IDENTITY}\n\n${sanitised}`
67
- : CLAUDE_CODE_IDENTITY,
68
- };
58
+ // Anthropic subscription/OAuth identity is applied per provider request in
59
+ // streamWithLimitsRetry(). Doing it there ensures every retry/fallback
60
+ // attempt gets the right prompt for its actual target model, while
61
+ // non-Anthropic providers never inherit the Claude Code identity.
69
62
  });
70
63
  }