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.
Files changed (3) hide show
  1. package/README.md +19 -21
  2. package/package.json +1 -1
  3. 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 `oira666_pi-limits-wait.try_models` to Pi's normal `settings.json`.
30
+ To enable automatic model fallback, add `fallback-models` to a `limits-wait.json` file.
31
31
 
32
- Pi settings can be global or project-local:
32
+ The extension reads these files in order; later files override earlier ones:
33
33
 
34
34
  | Location | Scope |
35
35
  |----------|-------|
36
- | `~/.pi/agent/settings.json` | Global, all projects |
37
- | `.pi/settings.json` | Project-local |
38
-
39
- Project settings override global settings. The extension reads these through Pi's `SettingsManager`, so normal Pi settings paths and `PI_CODING_AGENT_DIR` are respected.
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
- "oira666_pi-limits-wait": {
46
- "try_models": [
47
- {
48
- "provider": "anthropic",
49
- "modelname": "claude-sonnet-4-5",
50
- "reasoning effort": "medium"
51
- },
52
- {
53
- "provider": "openai",
54
- "modelname": "gpt-5.1-codex"
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 `try_models` entry supports:
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 `oira666_pi-limits-wait.try_models`, top to bottom.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-limits-wait",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
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/index.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { SettingsManager, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
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 SETTINGS_KEY = "oira666_pi-limits-wait";
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 { try_models?: unknown }).try_models)
142
- ? (raw as { try_models: unknown[] }).try_models
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 settingsManager = SettingsManager.create(ctx.cwd);
171
- const globalSettings = settingsManager.getGlobalSettings() as Record<string, unknown>;
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) return;
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 warnings: string[] = [];
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
- warnings.push(`missing ${entry.provider}/${entry.modelname}`);
236
+ modelWarnings.push(`missing ${entry.provider}/${entry.modelname}`);
193
237
  continue;
194
238
  }
195
239
  if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
196
- warnings.push(`no auth ${entry.provider}/${entry.modelname}`);
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 ${SETTINGS_KEY}.try_models:\n${lines.join("\n")}`
210
- : `Loaded ${SETTINGS_KEY}.try_models, but no usable fallback models were found.`;
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(warnings.length > 0 ? `${message}\nSkipped: ${warnings.join(", ")}` : message, warnings.length > 0 ? "warning" : "info");
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 ${SETTINGS_KEY}.try_models: ${err instanceof Error ? err.message : String(err)}`, "warning");
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