oira666_pi-limits-wait 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/README.md +6 -3
- package/package.json +1 -1
- package/src/constants.ts +7 -2
- package/src/index.ts +3 -1
- package/src/state.ts +5 -3
- package/src/stream.ts +7 -1
- package/src/ui.ts +41 -4
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Restart Pi or run `/reload` after installing.
|
|
|
22
22
|
- If no retry timing is available for a rate limit, waits 30 minutes before retrying.
|
|
23
23
|
- 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.
|
|
24
24
|
- 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.
|
|
25
|
-
- For any other (unclassified)
|
|
25
|
+
- For any other (unclassified) provider error — including an API `Internal server error` — waits and retries the same model. This is enabled by default and uses a configurable retry limit and interval; it does not alter model-limit wait intervals.
|
|
26
26
|
- Reports retry/fallback waits in chat notifications without adding persistent TUI status lines.
|
|
27
27
|
- Press Enter during a retry wait to skip the wait and retry immediately.
|
|
28
28
|
- Optionally falls back to configured models when the current/default model is rate-limited. Every fallback to a different model—even on the same provider—deliberately discards request-owned credentials, headers, environment overrides, and provider-specific options so the target runtime resolves its own configuration.
|
|
@@ -81,13 +81,16 @@ Fallback behavior:
|
|
|
81
81
|
4. If that model later becomes rate-limited too, the extension starts again from the same ordered list.
|
|
82
82
|
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.
|
|
83
83
|
|
|
84
|
-
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.
|
|
84
|
+
The extension rereads these files before every LLM call, so changes made during a long-running task take effect on the next LLM call without restarting Pi or using `/reload`. Tool calls do not trigger a settings read. 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.
|
|
85
85
|
|
|
86
86
|
## Environment variables
|
|
87
87
|
|
|
88
88
|
| Variable | Default | Description |
|
|
89
89
|
|----------|---------|-------------|
|
|
90
|
-
| `
|
|
90
|
+
| `PI_LIMITS_WAIT_DEFAULT_WAITING` | `true` | Whether unclassified/unknown provider errors enter the wait-and-retry cycle. Set to `false` (also accepts `0`, `no`, `off`) to surface the error immediately. Rate-limit, overload, authentication, and network retry behavior is unchanged. |
|
|
91
|
+
| `PI_LIMITS_WAIT_MAX_RETRY` | `999999` | Maximum retries for an unclassified/unknown provider error. The initial failed request does not count as a retry. Must be a non-negative integer. |
|
|
92
|
+
| `PI_LIMITS_WAIT_RETRY_INTERVAL` | `5` | Seconds to wait between unclassified/unknown provider-error retries. Must be a non-negative integer (values above 2147483 are capped to Node's maximum reliable timer delay). This does not affect model-limit wait intervals. |
|
|
93
|
+
| `PI_LIMITS_WAIT_FREEZING_ENABLED` | `true` | When a model keeps failing with a non-retryable error, it is normally "frozen" for 10 minutes 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. |
|
|
91
94
|
|
|
92
95
|
## License
|
|
93
96
|
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -25,9 +25,14 @@ export const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
|
|
|
25
25
|
export const DEFAULT_NETWORK_WAIT_MS = 15 * 1_000; // 15 seconds
|
|
26
26
|
export const DEFAULT_NON_RETRYABLE_FREEZE_MS = 10 * 60 * 1_000; // 10 minutes
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Unknown provider errors are retried separately from rate-limit, overload,
|
|
29
|
+
// authentication, and network failures.
|
|
30
|
+
export const DEFAULT_UNKNOWN_ERROR_MAX_RETRIES = 999_999;
|
|
31
|
+
export const DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS = 5 * 1_000; // 5 seconds
|
|
30
32
|
|
|
31
33
|
export const SETTINGS_FILE_NAME = "limits-wait.json";
|
|
32
34
|
export const FALLBACK_MODELS_KEY = "fallback-models";
|
|
33
35
|
export const FREEZING_ENV_VAR = "PI_LIMITS_WAIT_FREEZING_ENABLED";
|
|
36
|
+
export const DEFAULT_WAITING_ENV_VAR = "PI_LIMITS_WAIT_DEFAULT_WAITING";
|
|
37
|
+
export const MAX_RETRY_ENV_VAR = "PI_LIMITS_WAIT_MAX_RETRY";
|
|
38
|
+
export const RETRY_INTERVAL_ENV_VAR = "PI_LIMITS_WAIT_RETRY_INTERVAL";
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { isInternalSyntheticModel } from "./models.js";
|
|
3
3
|
import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
|
|
4
4
|
import { loadFallbackSettings } from "./settings.js";
|
|
5
|
+
import { loadUnknownErrorRetrySettings } from "./ui.js";
|
|
5
6
|
import { consumeExpectedModelSelection, state } from "./state.js";
|
|
6
7
|
import { installModelRuntimeInterception } from "./stream.js";
|
|
7
8
|
|
|
@@ -28,11 +29,12 @@ export {
|
|
|
28
29
|
streamWithLimitsRetry,
|
|
29
30
|
streamWithRateLimitRetry,
|
|
30
31
|
} from "./stream.js";
|
|
31
|
-
export { freezingEnabled, waitForRateLimit, waitForRetry } from "./ui.js";
|
|
32
|
+
export { freezingEnabled, loadUnknownErrorRetrySettings, waitForRateLimit, waitForRetry } from "./ui.js";
|
|
32
33
|
export type { FallbackModel } from "./types.js";
|
|
33
34
|
|
|
34
35
|
export default function (pi: ExtensionAPI) {
|
|
35
36
|
state.extensionApi = pi;
|
|
37
|
+
loadUnknownErrorRetrySettings();
|
|
36
38
|
const releaseInterception = installModelRuntimeInterception();
|
|
37
39
|
|
|
38
40
|
// Current pi-ai inserts the Claude Code identity as the first system block
|
package/src/state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Api, Model, ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_UNKNOWN_ERROR_MAX_RETRIES, DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS } from "./constants.js";
|
|
4
4
|
import type { FallbackModel, NonRetryableFailureMemory, RateLimitMemory } from "./types.js";
|
|
5
5
|
|
|
6
6
|
export const state = {
|
|
@@ -9,8 +9,10 @@ export const state = {
|
|
|
9
9
|
ambientStatusCleanup: undefined as (() => void) | undefined,
|
|
10
10
|
modelStatusCleanup: undefined as (() => void) | undefined,
|
|
11
11
|
fallbackModels: [] as FallbackModel[],
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
unknownErrorWaitingEnabled: true,
|
|
13
|
+
// Includes the initial request, so the default is one more than max retries.
|
|
14
|
+
nonRetryableMaxAttempts: DEFAULT_UNKNOWN_ERROR_MAX_RETRIES + 1,
|
|
15
|
+
nonRetryableRetryDelayMs: DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS,
|
|
14
16
|
primaryModel: undefined as Model<Api> | undefined,
|
|
15
17
|
primaryThinkingLevel: undefined as ModelThinkingLevel | undefined,
|
|
16
18
|
expectedModelSelections: new Map<string, Set<symbol>>(),
|
package/src/stream.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "./models.js";
|
|
29
29
|
import { withAttemptResponseObserver } from "./response-observer.js";
|
|
30
30
|
import { errorMessageWithCauses, getRetryableError, isAbortMessage, parseRetryDelayMs, retryAfterHeaderMs } from "./retry-errors.js";
|
|
31
|
+
import { loadFallbackSettings } from "./settings.js";
|
|
31
32
|
import { state } from "./state.js";
|
|
32
33
|
import type { FallbackModel, RetryableError, RuntimeStreamSimpleFn } from "./types.js";
|
|
33
34
|
import { clearModelStatus, freezingEnabled, reasonLabel, waitForRetry } from "./ui.js";
|
|
@@ -173,6 +174,11 @@ export function streamWithLimitsRetry(
|
|
|
173
174
|
? AbortSignal.any([options.signal, ownerSignal])
|
|
174
175
|
: options?.signal ?? ownerSignal;
|
|
175
176
|
|
|
177
|
+
// A long-running agent task can make many LLM calls between tool calls.
|
|
178
|
+
// Refresh here—not only in before_agent_start—so project settings edited
|
|
179
|
+
// during that task take effect on the next LLM call without /reload.
|
|
180
|
+
if (state.sharedCtx?.modelRegistry) loadFallbackSettings(state.sharedCtx);
|
|
181
|
+
|
|
176
182
|
void (async () => {
|
|
177
183
|
let committed = false;
|
|
178
184
|
let finished = false;
|
|
@@ -405,7 +411,7 @@ export function streamWithLimitsRetry(
|
|
|
405
411
|
output.end();
|
|
406
412
|
};
|
|
407
413
|
|
|
408
|
-
if (!committed) {
|
|
414
|
+
if (!committed && state.unknownErrorWaitingEnabled) {
|
|
409
415
|
const key = modelKey(attempt.model);
|
|
410
416
|
const attempts = (nonRetryableAttempts.get(key) ?? 0) + 1;
|
|
411
417
|
nonRetryableAttempts.set(key, attempts);
|
package/src/ui.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_UNKNOWN_ERROR_MAX_RETRIES,
|
|
3
|
+
DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS,
|
|
4
|
+
DEFAULT_WAITING_ENV_VAR,
|
|
5
|
+
FREEZING_ENV_VAR,
|
|
6
|
+
MAX_RETRY_ENV_VAR,
|
|
7
|
+
RETRY_INTERVAL_ENV_VAR,
|
|
8
|
+
} from "./constants.js";
|
|
2
9
|
import { state } from "./state.js";
|
|
3
10
|
import type { RetryReason } from "./types.js";
|
|
4
11
|
|
|
@@ -25,13 +32,43 @@ export function reasonLabel(reason: RetryReason): string {
|
|
|
25
32
|
return "Rate limited";
|
|
26
33
|
}
|
|
27
34
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (raw === undefined) return true;
|
|
35
|
+
function envBoolean(raw: string | undefined, defaultValue: boolean): boolean {
|
|
36
|
+
if (raw === undefined) return defaultValue;
|
|
31
37
|
const value = raw.trim().toLowerCase();
|
|
32
38
|
return !(value === "false" || value === "0" || value === "no" || value === "off");
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
function envNonNegativeInteger(raw: string | undefined, defaultValue: number, maximum = Number.MAX_SAFE_INTEGER): number {
|
|
42
|
+
if (raw === undefined || !/^\d+$/.test(raw.trim())) return defaultValue;
|
|
43
|
+
const value = Number(raw);
|
|
44
|
+
return Number.isSafeInteger(value) ? Math.min(value, maximum) : defaultValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function freezingEnabled(): boolean {
|
|
48
|
+
return envBoolean(process.env[FREEZING_ENV_VAR], true);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Load unknown-error retry settings once per extension instance/reload. */
|
|
52
|
+
export function loadUnknownErrorRetrySettings(): void {
|
|
53
|
+
state.unknownErrorWaitingEnabled = envBoolean(process.env[DEFAULT_WAITING_ENV_VAR], true);
|
|
54
|
+
const maxRetries = envNonNegativeInteger(
|
|
55
|
+
process.env[MAX_RETRY_ENV_VAR],
|
|
56
|
+
DEFAULT_UNKNOWN_ERROR_MAX_RETRIES,
|
|
57
|
+
Number.MAX_SAFE_INTEGER - 1,
|
|
58
|
+
);
|
|
59
|
+
// The first failed request is not itself a retry.
|
|
60
|
+
state.nonRetryableMaxAttempts = maxRetries + 1;
|
|
61
|
+
|
|
62
|
+
// PI_LIMITS_WAIT_RETRY_INTERVAL is expressed in seconds; internally waits use ms.
|
|
63
|
+
const intervalSeconds = envNonNegativeInteger(
|
|
64
|
+
process.env[RETRY_INTERVAL_ENV_VAR],
|
|
65
|
+
DEFAULT_UNKNOWN_ERROR_RETRY_INTERVAL_MS / 1_000,
|
|
66
|
+
// Node timers cannot reliably wait longer than this in one timeout.
|
|
67
|
+
Math.floor(2_147_483_647 / 1_000),
|
|
68
|
+
);
|
|
69
|
+
state.nonRetryableRetryDelayMs = intervalSeconds * 1_000;
|
|
70
|
+
}
|
|
71
|
+
|
|
35
72
|
function countdownText(reason: RetryReason, deadline: number, allowSkip: boolean): string {
|
|
36
73
|
const remaining = Math.max(0, deadline - Date.now());
|
|
37
74
|
const totalSecs = Math.ceil(remaining / 1_000);
|