oira666_pi-limits-wait 0.6.1 → 0.6.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 +1 -0
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/index.ts +174 -166
- package/src/models.ts +281 -281
- package/src/retry-errors.ts +263 -263
- package/src/retry-summary.ts +139 -139
- package/src/settings.ts +162 -162
- package/src/status-json.ts +126 -126
- package/src/stream.ts +855 -855
- package/src/telemetry.ts +81 -81
- package/src/ui.ts +490 -490
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Restart Pi or run `/reload` after installing.
|
|
|
16
16
|
|
|
17
17
|
- Targets Pi 0.80.10+ (developed and tested against Pi 0.83.0) and works with model providers that use `ModelRuntime.streamSimple`.
|
|
18
18
|
- Intercepts the current runtime without registering synthetic providers; every retry and fallback re-enters Pi's normal provider/auth/base URL/configured-header composition, including OAuth refresh checks.
|
|
19
|
+
- On Pi 0.86+, stops background cache warming via `cache_warming_decision` while retry interception is enabled, preventing background refreshes from triggering foreground retries or fallbacks. Normal request caching is unchanged. With `PI_LIMITS_WAIT_DISABLE_ALL_WAITING=true`, Pi's cache-warming policy is left untouched. As with any handler for this event, a later extension can override the decision.
|
|
19
20
|
- Reload-safe ownership ensures `/reload` transfers interception cleanly and aborts waits still owned by the previous extension instance. The interception has one process-global active extension owner, matching the interactive Pi CLI's single active session runtime lifecycle; SDK processes running multiple concurrent Pi sessions are not supported.
|
|
20
21
|
- On rate-limit errors (`429`, `rate_limit`, `too many requests`, quota/reset messages), waits and retries in a loop.
|
|
21
22
|
- Remembers provider retry timing exposed through response callbacks or provider errors (`retry-after`, `retry-after-ms`, `retry in ...`, reset messages). With fallback probing enabled, explicit Retry-After deadlines may intentionally be probed early; set the probe interval to `0` to honor those deadlines (unless a wait is manually skipped).
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -43,7 +43,7 @@ export const DEFAULT_LIVELINESS_INTERVAL_MS = 15 * 1_000; // 15 seconds
|
|
|
43
43
|
export const LIVELINESS_JSON_STATUS_KEY = "oira666.pi-limits-wait.json";
|
|
44
44
|
/** Payload schema version for LIVELINESS_JSON_STATUS_KEY. Bump only on a breaking change. */
|
|
45
45
|
export const LIVELINESS_JSON_VERSION = 1;
|
|
46
|
-
export const EXTENSION_VERSION = "0.6.
|
|
46
|
+
export const EXTENSION_VERSION = "0.6.2";
|
|
47
47
|
|
|
48
48
|
export const SETTINGS_FILE_NAME = "limits-wait.json";
|
|
49
49
|
export const FALLBACK_MODELS_KEY = "fallback-models";
|
package/src/index.ts
CHANGED
|
@@ -1,166 +1,174 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { isInternalSyntheticModel } from "./models.js";
|
|
3
|
-
import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
|
|
4
|
-
import { RetrySummaryCoordinator } from "./retry-summary.js";
|
|
5
|
-
import { loadFallbackSettings } from "./settings.js";
|
|
6
|
-
import { allWaitingDisabled, loadUnknownErrorRetrySettings } from "./ui.js";
|
|
7
|
-
import { consumeExpectedModelSelection, state } from "./state.js";
|
|
8
|
-
import { disableModelRuntimeInterception, installModelRuntimeInterception } from "./stream.js";
|
|
9
|
-
|
|
10
|
-
export { sanitiseAnthropicPayloadSystem, sanitiseSystemPrompt } from "./prompt.js";
|
|
11
|
-
export {
|
|
12
|
-
getRetryableError,
|
|
13
|
-
isAuthenticationRefreshError,
|
|
14
|
-
isRateLimitError,
|
|
15
|
-
isServerOverloadedError,
|
|
16
|
-
isTransientNetworkError,
|
|
17
|
-
parseRetryDelayMs,
|
|
18
|
-
rateLimitWaitMs,
|
|
19
|
-
retryAfterHeaderMs,
|
|
20
|
-
} from "./retry-errors.js";
|
|
21
|
-
export {
|
|
22
|
-
__readFallbackSettingsForTests,
|
|
23
|
-
configFileCandidates,
|
|
24
|
-
parseProbeIntervalSeconds,
|
|
25
|
-
readFallbackSettings,
|
|
26
|
-
} from "./settings.js";
|
|
27
|
-
export {
|
|
28
|
-
blockedState,
|
|
29
|
-
candidateOrder,
|
|
30
|
-
earliestCandidateWakeup,
|
|
31
|
-
initialAttempt,
|
|
32
|
-
isModelBlocked,
|
|
33
|
-
nextAttemptCandidate,
|
|
34
|
-
nextAvailableCandidate,
|
|
35
|
-
probeIntervalMs,
|
|
36
|
-
} from "./models.js";
|
|
37
|
-
export {
|
|
38
|
-
__createRetrySummaryCoordinatorForTests,
|
|
39
|
-
LIMITS_WAIT_ENTRY_TYPE,
|
|
40
|
-
RetrySummaryCoordinator,
|
|
41
|
-
} from "./retry-summary.js";
|
|
42
|
-
export {
|
|
43
|
-
LIMITS_WAIT_EVENT_CHANNEL,
|
|
44
|
-
} from "./telemetry.js";
|
|
45
|
-
export {
|
|
46
|
-
CONTROL_FILE_ENV_VAR,
|
|
47
|
-
EXTENSION_VERSION,
|
|
48
|
-
LIVELINESS_JSON_STATUS_KEY,
|
|
49
|
-
LIVELINESS_JSON_VERSION,
|
|
50
|
-
LIVELINESS_STATUS_KEY,
|
|
51
|
-
PROBE_INTERVAL_ENV_VAR,
|
|
52
|
-
STATUS_JSON_ENV_VAR,
|
|
53
|
-
} from "./constants.js";
|
|
54
|
-
export {
|
|
55
|
-
clearStatusJson,
|
|
56
|
-
controlFilePath,
|
|
57
|
-
publishStatusJson,
|
|
58
|
-
statusJsonEnabled,
|
|
59
|
-
statusPayload,
|
|
60
|
-
} from "./status-json.js";
|
|
61
|
-
export type {
|
|
62
|
-
LimitsWaitStatusContext,
|
|
63
|
-
LimitsWaitStatusEvent,
|
|
64
|
-
LimitsWaitStatusPayload,
|
|
65
|
-
} from "./status-json.js";
|
|
66
|
-
export type {
|
|
67
|
-
LimitsWaitEndEventPayload,
|
|
68
|
-
LimitsWaitEventPayload,
|
|
69
|
-
LimitsWaitModel,
|
|
70
|
-
LimitsWaitOutcome,
|
|
71
|
-
LimitsWaitStartEventPayload,
|
|
72
|
-
LimitsWaitWaitEndPayload,
|
|
73
|
-
LimitsWaitWaitEventPayload,
|
|
74
|
-
LimitsWaitWaitOutcome,
|
|
75
|
-
LimitsWaitWaitStartPayload,
|
|
76
|
-
} from "./telemetry.js";
|
|
77
|
-
export type { LimitsWaitSessionEntryData } from "./retry-summary.js";
|
|
78
|
-
export {
|
|
79
|
-
__configureFallbackModelsForTests,
|
|
80
|
-
__setNonRetryableTuningForTests,
|
|
81
|
-
__setProbeIntervalForTests,
|
|
82
|
-
disableModelRuntimeInterception,
|
|
83
|
-
installModelRuntimeInterception,
|
|
84
|
-
streamWithLimitsRetry,
|
|
85
|
-
streamWithRateLimitRetry,
|
|
86
|
-
} from "./stream.js";
|
|
87
|
-
export {
|
|
88
|
-
allWaitingDisabled,
|
|
89
|
-
clearLivelinessStatus,
|
|
90
|
-
configuredProbeIntervalMs,
|
|
91
|
-
freezingEnabled,
|
|
92
|
-
isNonInteractiveHost,
|
|
93
|
-
loadUnknownErrorRetrySettings,
|
|
94
|
-
notifyFinalFailure,
|
|
95
|
-
notifyLiveliness,
|
|
96
|
-
waitForRateLimit,
|
|
97
|
-
waitForRetry,
|
|
98
|
-
} from "./ui.js";
|
|
99
|
-
export type { FallbackModel } from "./types.js";
|
|
100
|
-
|
|
101
|
-
export default function (pi: ExtensionAPI) {
|
|
102
|
-
// Current pi-ai inserts the Claude Code identity as the first system block
|
|
103
|
-
// for Anthropic OAuth. Sanitise only that exact payload shape; authentication,
|
|
104
|
-
// identity insertion, Anthropic headers, and all other request composition
|
|
105
|
-
// remain owned by ModelRuntime.
|
|
106
|
-
pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
|
|
107
|
-
|
|
108
|
-
// Keep prompt sanitisation and the runtime's normal Anthropic header path,
|
|
109
|
-
// but install no retry/error interception or retry-related lifecycle hooks.
|
|
110
|
-
if (allWaitingDisabled()) {
|
|
111
|
-
disableModelRuntimeInterception();
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
state.extensionApi = pi;
|
|
116
|
-
loadUnknownErrorRetrySettings();
|
|
117
|
-
const retrySummaries = new RetrySummaryCoordinator(pi);
|
|
118
|
-
const releaseInterception = installModelRuntimeInterception(() => retrySummaries.createStream());
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
state.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
pi.on("
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
state
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { isInternalSyntheticModel } from "./models.js";
|
|
3
|
+
import { sanitiseAnthropicPayloadSystem } from "./prompt.js";
|
|
4
|
+
import { RetrySummaryCoordinator } from "./retry-summary.js";
|
|
5
|
+
import { loadFallbackSettings } from "./settings.js";
|
|
6
|
+
import { allWaitingDisabled, loadUnknownErrorRetrySettings } from "./ui.js";
|
|
7
|
+
import { consumeExpectedModelSelection, state } from "./state.js";
|
|
8
|
+
import { disableModelRuntimeInterception, installModelRuntimeInterception } from "./stream.js";
|
|
9
|
+
|
|
10
|
+
export { sanitiseAnthropicPayloadSystem, sanitiseSystemPrompt } from "./prompt.js";
|
|
11
|
+
export {
|
|
12
|
+
getRetryableError,
|
|
13
|
+
isAuthenticationRefreshError,
|
|
14
|
+
isRateLimitError,
|
|
15
|
+
isServerOverloadedError,
|
|
16
|
+
isTransientNetworkError,
|
|
17
|
+
parseRetryDelayMs,
|
|
18
|
+
rateLimitWaitMs,
|
|
19
|
+
retryAfterHeaderMs,
|
|
20
|
+
} from "./retry-errors.js";
|
|
21
|
+
export {
|
|
22
|
+
__readFallbackSettingsForTests,
|
|
23
|
+
configFileCandidates,
|
|
24
|
+
parseProbeIntervalSeconds,
|
|
25
|
+
readFallbackSettings,
|
|
26
|
+
} from "./settings.js";
|
|
27
|
+
export {
|
|
28
|
+
blockedState,
|
|
29
|
+
candidateOrder,
|
|
30
|
+
earliestCandidateWakeup,
|
|
31
|
+
initialAttempt,
|
|
32
|
+
isModelBlocked,
|
|
33
|
+
nextAttemptCandidate,
|
|
34
|
+
nextAvailableCandidate,
|
|
35
|
+
probeIntervalMs,
|
|
36
|
+
} from "./models.js";
|
|
37
|
+
export {
|
|
38
|
+
__createRetrySummaryCoordinatorForTests,
|
|
39
|
+
LIMITS_WAIT_ENTRY_TYPE,
|
|
40
|
+
RetrySummaryCoordinator,
|
|
41
|
+
} from "./retry-summary.js";
|
|
42
|
+
export {
|
|
43
|
+
LIMITS_WAIT_EVENT_CHANNEL,
|
|
44
|
+
} from "./telemetry.js";
|
|
45
|
+
export {
|
|
46
|
+
CONTROL_FILE_ENV_VAR,
|
|
47
|
+
EXTENSION_VERSION,
|
|
48
|
+
LIVELINESS_JSON_STATUS_KEY,
|
|
49
|
+
LIVELINESS_JSON_VERSION,
|
|
50
|
+
LIVELINESS_STATUS_KEY,
|
|
51
|
+
PROBE_INTERVAL_ENV_VAR,
|
|
52
|
+
STATUS_JSON_ENV_VAR,
|
|
53
|
+
} from "./constants.js";
|
|
54
|
+
export {
|
|
55
|
+
clearStatusJson,
|
|
56
|
+
controlFilePath,
|
|
57
|
+
publishStatusJson,
|
|
58
|
+
statusJsonEnabled,
|
|
59
|
+
statusPayload,
|
|
60
|
+
} from "./status-json.js";
|
|
61
|
+
export type {
|
|
62
|
+
LimitsWaitStatusContext,
|
|
63
|
+
LimitsWaitStatusEvent,
|
|
64
|
+
LimitsWaitStatusPayload,
|
|
65
|
+
} from "./status-json.js";
|
|
66
|
+
export type {
|
|
67
|
+
LimitsWaitEndEventPayload,
|
|
68
|
+
LimitsWaitEventPayload,
|
|
69
|
+
LimitsWaitModel,
|
|
70
|
+
LimitsWaitOutcome,
|
|
71
|
+
LimitsWaitStartEventPayload,
|
|
72
|
+
LimitsWaitWaitEndPayload,
|
|
73
|
+
LimitsWaitWaitEventPayload,
|
|
74
|
+
LimitsWaitWaitOutcome,
|
|
75
|
+
LimitsWaitWaitStartPayload,
|
|
76
|
+
} from "./telemetry.js";
|
|
77
|
+
export type { LimitsWaitSessionEntryData } from "./retry-summary.js";
|
|
78
|
+
export {
|
|
79
|
+
__configureFallbackModelsForTests,
|
|
80
|
+
__setNonRetryableTuningForTests,
|
|
81
|
+
__setProbeIntervalForTests,
|
|
82
|
+
disableModelRuntimeInterception,
|
|
83
|
+
installModelRuntimeInterception,
|
|
84
|
+
streamWithLimitsRetry,
|
|
85
|
+
streamWithRateLimitRetry,
|
|
86
|
+
} from "./stream.js";
|
|
87
|
+
export {
|
|
88
|
+
allWaitingDisabled,
|
|
89
|
+
clearLivelinessStatus,
|
|
90
|
+
configuredProbeIntervalMs,
|
|
91
|
+
freezingEnabled,
|
|
92
|
+
isNonInteractiveHost,
|
|
93
|
+
loadUnknownErrorRetrySettings,
|
|
94
|
+
notifyFinalFailure,
|
|
95
|
+
notifyLiveliness,
|
|
96
|
+
waitForRateLimit,
|
|
97
|
+
waitForRetry,
|
|
98
|
+
} from "./ui.js";
|
|
99
|
+
export type { FallbackModel } from "./types.js";
|
|
100
|
+
|
|
101
|
+
export default function (pi: ExtensionAPI) {
|
|
102
|
+
// Current pi-ai inserts the Claude Code identity as the first system block
|
|
103
|
+
// for Anthropic OAuth. Sanitise only that exact payload shape; authentication,
|
|
104
|
+
// identity insertion, Anthropic headers, and all other request composition
|
|
105
|
+
// remain owned by ModelRuntime.
|
|
106
|
+
pi.on("before_provider_request", (event) => sanitiseAnthropicPayloadSystem(event.payload));
|
|
107
|
+
|
|
108
|
+
// Keep prompt sanitisation and the runtime's normal Anthropic header path,
|
|
109
|
+
// but install no retry/error interception or retry-related lifecycle hooks.
|
|
110
|
+
if (allWaitingDisabled()) {
|
|
111
|
+
disableModelRuntimeInterception();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state.extensionApi = pi;
|
|
116
|
+
loadUnknownErrorRetrySettings();
|
|
117
|
+
const retrySummaries = new RetrySummaryCoordinator(pi);
|
|
118
|
+
const releaseInterception = installModelRuntimeInterception(() => retrySummaries.createStream());
|
|
119
|
+
|
|
120
|
+
// Pi 0.86+ warms caches through the same runtime as foreground requests.
|
|
121
|
+
// Stop background refreshes before they can enter our retry/fallback logic.
|
|
122
|
+
// Keep the older Pi dependency range: its event registry accepts this hook
|
|
123
|
+
// but never emits it. The narrow overload bridges the pre-0.86 typings.
|
|
124
|
+
(pi as ExtensionAPI & {
|
|
125
|
+
on(event: "cache_warming_decision", handler: () => { action: "stop" }): void;
|
|
126
|
+
}).on("cache_warming_decision", () => ({ action: "stop" }));
|
|
127
|
+
|
|
128
|
+
pi.on("model_select", (event, ctx) => {
|
|
129
|
+
state.sharedCtx = ctx;
|
|
130
|
+
if (consumeExpectedModelSelection(event.model)) return;
|
|
131
|
+
if (isInternalSyntheticModel(event.model)) return;
|
|
132
|
+
state.primaryModel = event.model;
|
|
133
|
+
state.primaryThinkingLevel = pi.getThinkingLevel();
|
|
134
|
+
state.userModelSelectionGeneration++;
|
|
135
|
+
// A user-selected model takes precedence over a pending retry. Wake every
|
|
136
|
+
// wait so its stream can adopt the new primary model immediately.
|
|
137
|
+
for (const skip of [...state.activeWaitSkips]) skip();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
141
|
+
state.sharedCtx = ctx;
|
|
142
|
+
if (!state.primaryModel && ctx.model && !isInternalSyntheticModel(ctx.model)) state.primaryModel = ctx.model;
|
|
143
|
+
state.primaryThinkingLevel ??= pi.getThinkingLevel();
|
|
144
|
+
loadFallbackSettings(ctx);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
148
|
+
state.sharedCtx = ctx;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// turn_end runs after Pi has persisted the assistant turn, so these custom
|
|
152
|
+
// entries remain durable transcript metadata without entering LLM context.
|
|
153
|
+
pi.on("turn_end", () => {
|
|
154
|
+
retrySummaries.flush();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
pi.on("agent_end", () => {
|
|
158
|
+
retrySummaries.finalizeAndFlush();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
pi.on("session_shutdown", () => {
|
|
162
|
+
// An old extension instance must not disable or clear state owned by a
|
|
163
|
+
// newer /reload instance.
|
|
164
|
+
const ownsInterception = releaseInterception();
|
|
165
|
+
retrySummaries.finalizeAndFlush();
|
|
166
|
+
if (!ownsInterception) return;
|
|
167
|
+
state.ambientStatusCleanup?.();
|
|
168
|
+
state.modelStatusCleanup?.();
|
|
169
|
+
state.expectedModelSelections.clear();
|
|
170
|
+
state.activeWaitSkips.clear();
|
|
171
|
+
state.sharedCtx = undefined;
|
|
172
|
+
state.extensionApi = undefined;
|
|
173
|
+
});
|
|
174
|
+
}
|