oira666_pi-limits-wait 0.4.7 → 0.4.8
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 +19 -21
- package/package.json +1 -1
- package/src/index.ts +68 -22
package/README.md
CHANGED
|
@@ -27,38 +27,36 @@ Restart Pi or run `/reload` after installing.
|
|
|
27
27
|
|
|
28
28
|
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.
|
|
29
29
|
|
|
30
|
-
To enable automatic model fallback, add `
|
|
30
|
+
To enable automatic model fallback, add `fallback-models` to a `limits-wait.json` file.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
The extension reads these files in order; later files override earlier ones:
|
|
33
33
|
|
|
34
34
|
| Location | Scope |
|
|
35
35
|
|----------|-------|
|
|
36
|
-
| `~/.pi/
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
| `~/.config/.pi/limits-wait.json` | Global defaults |
|
|
37
|
+
| `<Pi agent dir>/limits-wait.json` (for example `~/.pi/agent/limits-wait.json`, or `PI_CODING_AGENT_DIR/limits-wait.json`) | Pi agent directory |
|
|
38
|
+
| `.limits-wait.json` | Project root override |
|
|
39
|
+
| `.pi/limits-wait.json` | Pi project override |
|
|
40
40
|
|
|
41
41
|
Example:
|
|
42
42
|
|
|
43
43
|
```json
|
|
44
44
|
{
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
]
|
|
57
|
-
}
|
|
45
|
+
"fallback-models": [
|
|
46
|
+
{
|
|
47
|
+
"provider": "anthropic",
|
|
48
|
+
"modelname": "claude-sonnet-4-5",
|
|
49
|
+
"reasoning effort": "medium"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"provider": "openai",
|
|
53
|
+
"modelname": "gpt-5.1-codex"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
58
56
|
}
|
|
59
57
|
```
|
|
60
58
|
|
|
61
|
-
Each `
|
|
59
|
+
Each `fallback-models` entry supports:
|
|
62
60
|
|
|
63
61
|
- `provider` — required. The Pi provider name, for example `anthropic`, `openai`, `google`, etc.
|
|
64
62
|
- `modelname` — required. The model id/name as Pi knows it.
|
|
@@ -69,7 +67,7 @@ Fallback behavior:
|
|
|
69
67
|
1. Pi starts with the normal default or user-selected model.
|
|
70
68
|
2. If that model is rate-limited, the extension tries models in this order:
|
|
71
69
|
- the original default/user-selected model;
|
|
72
|
-
- then every model from `
|
|
70
|
+
- then every model from `fallback-models`, top to bottom.
|
|
73
71
|
3. The first model that responds without a rate-limit becomes the active Pi model for the rest of the session/task.
|
|
74
72
|
4. If that model later becomes rate-limited too, the extension starts again from the same ordered list.
|
|
75
73
|
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.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join, resolve } from "path";
|
|
4
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
5
|
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
|
|
3
6
|
import {
|
|
4
7
|
createAssistantMessageEventStream,
|
|
@@ -29,7 +32,8 @@ const PI_IDENTITY_SENTENCE_PATTERN =
|
|
|
29
32
|
const DEFAULT_RATE_LIMIT_WAIT_MS = 30 * 60 * 1_000; // 30 minutes
|
|
30
33
|
const DEFAULT_OVERLOADED_WAIT_MS = 5 * 60 * 1_000; // 5 minutes
|
|
31
34
|
const DEFAULT_NON_RETRYABLE_FREEZE_MS = 60 * 60 * 1_000; // 1 hour
|
|
32
|
-
const
|
|
35
|
+
const SETTINGS_FILE_NAME = "limits-wait.json";
|
|
36
|
+
const FALLBACK_MODELS_KEY = "fallback-models";
|
|
33
37
|
|
|
34
38
|
/** Key used with ctx.ui.setStatus() for the countdown line. */
|
|
35
39
|
const STATUS_KEY = "limits-wait";
|
|
@@ -138,8 +142,8 @@ function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function parseConfiguredModels(raw: unknown): ConfiguredModel[] {
|
|
141
|
-
const source = raw && typeof raw === "object" && Array.isArray((raw as {
|
|
142
|
-
? (raw as {
|
|
145
|
+
const source = raw && typeof raw === "object" && Array.isArray((raw as { [FALLBACK_MODELS_KEY]?: unknown })[FALLBACK_MODELS_KEY])
|
|
146
|
+
? (raw as { [FALLBACK_MODELS_KEY]: unknown[] })[FALLBACK_MODELS_KEY]
|
|
143
147
|
: [];
|
|
144
148
|
|
|
145
149
|
const models: ConfiguredModel[] = [];
|
|
@@ -163,37 +167,77 @@ function parseConfiguredModels(raw: unknown): ConfiguredModel[] {
|
|
|
163
167
|
return models;
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
function configFileCandidates(cwd: string, agentDir = getAgentDir(), homeDir = homedir()): string[] {
|
|
171
|
+
return [
|
|
172
|
+
join(homeDir, ".config", ".pi", SETTINGS_FILE_NAME),
|
|
173
|
+
join(agentDir, SETTINGS_FILE_NAME),
|
|
174
|
+
resolve(cwd, `.${SETTINGS_FILE_NAME}`),
|
|
175
|
+
resolve(cwd, ".pi", SETTINGS_FILE_NAME),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function mergeConfig(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
|
|
180
|
+
return { ...base, ...override };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readFallbackSettings(cwd: string, agentDir = getAgentDir(), homeDir = homedir()): { config: Record<string, unknown>; loadedPaths: string[]; warnings: string[] } {
|
|
184
|
+
let config: Record<string, unknown> = {};
|
|
185
|
+
const loadedPaths: string[] = [];
|
|
186
|
+
const warnings: string[] = [];
|
|
187
|
+
const seen = new Set<string>();
|
|
188
|
+
|
|
189
|
+
for (const candidate of configFileCandidates(cwd, agentDir, homeDir)) {
|
|
190
|
+
const path = resolve(candidate);
|
|
191
|
+
if (seen.has(path)) continue;
|
|
192
|
+
seen.add(path);
|
|
193
|
+
if (!existsSync(path)) continue;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
|
|
197
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
198
|
+
warnings.push(`${path}: expected a JSON object`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
config = mergeConfig(config, parsed as Record<string, unknown>);
|
|
202
|
+
loadedPaths.push(path);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
warnings.push(`${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { config, loadedPaths, warnings };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function __readFallbackSettingsForTests(cwd: string, agentDir: string, homeDir: string): { config: Record<string, unknown>; loadedPaths: string[]; warnings: string[] } {
|
|
212
|
+
return readFallbackSettings(cwd, agentDir, homeDir);
|
|
213
|
+
}
|
|
214
|
+
|
|
166
215
|
function loadFallbackSettings(ctx: ExtensionContext): void {
|
|
167
216
|
fallbackModels = [];
|
|
168
217
|
|
|
169
218
|
try {
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const projectSettings = settingsManager.getProjectSettings() as Record<string, unknown>;
|
|
173
|
-
const globalConfig = globalSettings[SETTINGS_KEY];
|
|
174
|
-
const projectConfig = projectSettings[SETTINGS_KEY];
|
|
175
|
-
const config = {
|
|
176
|
-
...(globalConfig && typeof globalConfig === "object" && !Array.isArray(globalConfig) ? globalConfig : {}),
|
|
177
|
-
...(projectConfig && typeof projectConfig === "object" && !Array.isArray(projectConfig) ? projectConfig : {}),
|
|
178
|
-
};
|
|
179
|
-
const content = JSON.stringify(config);
|
|
219
|
+
const { config, loadedPaths, warnings } = readFallbackSettings(ctx.cwd);
|
|
220
|
+
const content = JSON.stringify({ config, loadedPaths, warnings });
|
|
180
221
|
const shouldNotify = settingsSignature !== content;
|
|
181
222
|
settingsSignature = content;
|
|
182
223
|
|
|
183
224
|
const configured = parseConfiguredModels(config);
|
|
184
|
-
if (configured.length === 0)
|
|
225
|
+
if (configured.length === 0) {
|
|
226
|
+
if (shouldNotify && warnings.length > 0) ctx.ui.notify(`Could not fully load ${SETTINGS_FILE_NAME}: ${warnings.join(", ")}`, "warning");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
185
229
|
|
|
186
|
-
const
|
|
230
|
+
const modelWarnings: string[] = [];
|
|
187
231
|
const seen = new Set<string>();
|
|
188
232
|
|
|
189
233
|
for (const entry of configured) {
|
|
190
234
|
const model = ctx.modelRegistry.find(entry.provider, entry.modelname);
|
|
191
235
|
if (!model) {
|
|
192
|
-
|
|
236
|
+
modelWarnings.push(`missing ${entry.provider}/${entry.modelname}`);
|
|
193
237
|
continue;
|
|
194
238
|
}
|
|
195
239
|
if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
|
|
196
|
-
|
|
240
|
+
modelWarnings.push(`no auth ${entry.provider}/${entry.modelname}`);
|
|
197
241
|
continue;
|
|
198
242
|
}
|
|
199
243
|
const key = modelKey(model);
|
|
@@ -205,14 +249,16 @@ function loadFallbackSettings(ctx: ExtensionContext): void {
|
|
|
205
249
|
const lines = fallbackModels.map((entry, index) =>
|
|
206
250
|
`${index + 1}. ${formatModel(entry.model)}${entry.reasoningEffort ? ` (${entry.reasoningEffort})` : ""}`,
|
|
207
251
|
);
|
|
252
|
+
const source = loadedPaths.length > 0 ? ` from ${loadedPaths.join(", ")}` : "";
|
|
208
253
|
const message = lines.length > 0
|
|
209
|
-
? `Loaded ${
|
|
210
|
-
: `Loaded ${
|
|
254
|
+
? `Loaded ${SETTINGS_FILE_NAME}.${FALLBACK_MODELS_KEY}${source}:\n${lines.join("\n")}`
|
|
255
|
+
: `Loaded ${SETTINGS_FILE_NAME}.${FALLBACK_MODELS_KEY}${source}, but no usable fallback models were found.`;
|
|
256
|
+
const allWarnings = [...warnings, ...modelWarnings];
|
|
211
257
|
if (shouldNotify) {
|
|
212
|
-
ctx.ui.notify(
|
|
258
|
+
ctx.ui.notify(allWarnings.length > 0 ? `${message}\nSkipped: ${allWarnings.join(", ")}` : message, allWarnings.length > 0 ? "warning" : "info");
|
|
213
259
|
}
|
|
214
260
|
} catch (err) {
|
|
215
|
-
ctx.ui.notify(`Could not load ${
|
|
261
|
+
ctx.ui.notify(`Could not load ${SETTINGS_FILE_NAME}.${FALLBACK_MODELS_KEY}: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
216
262
|
}
|
|
217
263
|
}
|
|
218
264
|
|