pi-model-fallback 0.3.7 → 0.4.0
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/CHANGELOG.md +20 -8
- package/README.md +6 -0
- package/extensions/index.ts +48 -10
- package/lib/config.ts +5 -16
- package/lib/error-status.ts +3 -0
- package/lib/internal.ts +31 -0
- package/lib/state.ts +3 -35
- package/lib/storage.ts +3 -14
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,25 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
## [0.
|
|
5
|
+
## [0.4.0] - 2026-09-11
|
|
6
6
|
|
|
7
|
-
###
|
|
7
|
+
### Added
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- `autoRetry` config option (default `true`): when a fallback switch happens after a failed turn, the failed user prompt is automatically re-queued so the turn retries on the fallback model.
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
13
|
+
- Parse leading bare status codes in provider error messages (for example `401: {...}` from OpenCode Go and OpenRouter) so fallback rules fire on real-world failure shapes.
|
|
14
|
+
- Auto-retry when a fallback model is merely preselected instead of skipping the switch path.
|
|
15
|
+
- Parse optional `Error:` prefixes before leading bare status codes.
|
|
16
16
|
|
|
17
17
|
### Changed
|
|
18
18
|
|
|
19
|
-
- Bump
|
|
19
|
+
- Bump Pi dev dependencies to `@earendil-works/*` 0.84.4 and `@types/node` 26.4.0.
|
|
20
|
+
- Add `jiti` as a devDependency so auto-retry integration tests run in CI.
|
|
21
|
+
|
|
22
|
+
## [0.3.7] - 2026-08-22
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
20
25
|
|
|
26
|
+
- Merge the 2026-08-22 managed OSS dependency and maintenance PR batch.
|
|
21
27
|
- Declare `@earendil-works/pi-agent-core` as a peerDependency to align with pi-extension-template.
|
|
22
28
|
- Add Buy Me a Coffee sponsor button to README and native GitHub funding link via `.github/FUNDING.yml`.
|
|
23
29
|
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Warn on completely shadowed fallback rules in config validation, save confirmations, status output, and config details without rejecting warning-bearing configs.
|
|
33
|
+
- Document the missing `git add`/`git commit` steps in `CONTRIBUTING.md` release instructions so maintainers can trigger auto-release after `npm version patch --no-git-tag-version`.
|
|
34
|
+
- Clarify that maintainers should update `CHANGELOG.md` before staging release files.
|
|
35
|
+
|
|
24
36
|
## [0.3.3] - 2026-07-04
|
|
25
37
|
|
|
26
38
|
### Changed
|
|
@@ -74,7 +86,7 @@ This project follows semantic versioning.
|
|
|
74
86
|
- Publish workflow now installs a current npm CLI so npm Trusted Publishing OIDC is supported.
|
|
75
87
|
- CI and publish workflow commands no longer include literal trailing `\\n` text.
|
|
76
88
|
|
|
77
|
-
## [0.1.0] -
|
|
89
|
+
## [0.1.0] - 2026-06-30
|
|
78
90
|
|
|
79
91
|
### Added
|
|
80
92
|
|
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Default config shape:
|
|
|
70
70
|
{
|
|
71
71
|
"version": 1,
|
|
72
72
|
"enabled": true,
|
|
73
|
+
"autoRetry": true,
|
|
73
74
|
"rules": [
|
|
74
75
|
{
|
|
75
76
|
"name": "zai-to-deepseek-flash",
|
|
@@ -92,6 +93,11 @@ Rule fields:
|
|
|
92
93
|
- `cooldownMs`: optional persistent fallback window
|
|
93
94
|
- `fallback`: target model Pi should switch to
|
|
94
95
|
|
|
96
|
+
Top-level fields:
|
|
97
|
+
|
|
98
|
+
- `enabled`: toggle the extension without deleting rules
|
|
99
|
+
- `autoRetry`: when a fallback fires from a failed turn, automatically re-queue the failed user prompt so the turn retries on the fallback model (defaults to `true`; set to `false` to only switch models)
|
|
100
|
+
|
|
95
101
|
Rules use first-match order: the first rule whose provider/model and status match wins. Put specific `matchModels` rules before broad `matchProviders` rules when they should take priority. `model_fallback_config validate`, `save`, `read`, and `status` report warning-only diagnostics when a later rule or model entry is completely shadowed by an earlier rule; `/model-fallback:status` also includes a concise warning summary for the current config. Warning-bearing config remains valid and can still be saved.
|
|
96
102
|
|
|
97
103
|
### Cooldowns
|
package/extensions/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "../lib/config.js";
|
|
18
18
|
import { parseStatusFromErrorMessage } from "../lib/error-status.js";
|
|
19
19
|
import { emptyState, findActiveStateEntry, pruneExpiredState, readState, upsertStateEntry, validateStateShape, writeState, type FallbackState } from "../lib/state.js";
|
|
20
|
+
import { isRecord } from "../lib/internal.ts";
|
|
20
21
|
import { modelFallbackPaths, readConfig, writeConfig, type ModelFallbackPaths } from "../lib/storage.js";
|
|
21
22
|
|
|
22
23
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -106,17 +107,20 @@ export default function modelFallback(pi: ExtensionAPI) {
|
|
|
106
107
|
ctx.ui.notify(`Model fallback preselected: ${originalModelKey} → ${activeFallbackKey} until ${active.until}.`, "warning");
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
async function persistFailure(source: ModelRef, status: number, headers: Record<string, string>, ctx: ExtensionContext): Promise<
|
|
110
|
+
async function persistFailure(source: ModelRef, status: number, headers: Record<string, string>, ctx: ExtensionContext): Promise<boolean> {
|
|
110
111
|
const loaded = config ?? (await loadConfig(ctx));
|
|
111
|
-
if (!loaded) return;
|
|
112
|
+
if (!loaded) return false;
|
|
112
113
|
const match = findFallback(loaded, { provider: source.provider, id: source.model }, status);
|
|
113
|
-
if (!match) return;
|
|
114
|
-
|
|
114
|
+
if (!match) return false;
|
|
115
|
+
// Only skip when the failing model IS the active fallback (its own failure should not
|
|
116
|
+
// re-trigger). A failure on the original model while the fallback is merely preselected
|
|
117
|
+
// must still proceed so auto-retry can run on the fallback.
|
|
118
|
+
if (activeFallbackKey && modelRefKey(source) === activeFallbackKey) return false;
|
|
115
119
|
|
|
116
120
|
const fallbackModel = ctx.modelRegistry.find(match.fallback.provider, match.fallback.model);
|
|
117
121
|
if (!fallbackModel) {
|
|
118
122
|
ctx.ui.notify(`Model fallback missing: ${modelRefKey(match.fallback)}`, "warning");
|
|
119
|
-
return;
|
|
123
|
+
return false;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
const now = new Date();
|
|
@@ -135,7 +139,7 @@ export default function modelFallback(pi: ExtensionAPI) {
|
|
|
135
139
|
const ok = await pi.setModel(fallbackModel);
|
|
136
140
|
if (!ok) {
|
|
137
141
|
ctx.ui.notify(`Model fallback auth unavailable: ${modelRefKey(match.fallback)}`, "warning");
|
|
138
|
-
return;
|
|
142
|
+
return false;
|
|
139
143
|
}
|
|
140
144
|
|
|
141
145
|
originalModelKey = modelRefKey(source);
|
|
@@ -143,6 +147,7 @@ export default function modelFallback(pi: ExtensionAPI) {
|
|
|
143
147
|
lastFallbackReason = `${status} from ${originalModelKey}; persistent until ${until}`;
|
|
144
148
|
updateStatus(ctx);
|
|
145
149
|
ctx.ui.notify(`Model fallback: ${originalModelKey} → ${activeFallbackKey} (${status}). Future sessions preselect fallback until ${until}.`, "warning");
|
|
150
|
+
return true;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
pi.on("agent_start", async (_event, ctx) => {
|
|
@@ -182,7 +187,14 @@ export default function modelFallback(pi: ExtensionAPI) {
|
|
|
182
187
|
const model = typeof message.model === "string" ? message.model : ctx.model?.id;
|
|
183
188
|
if (!provider || !model) return;
|
|
184
189
|
if (activeFallbackKey && `${provider}/${model}` === activeFallbackKey) return;
|
|
185
|
-
await persistFailure({ provider, model }, status, {}, ctx);
|
|
190
|
+
const switched = await persistFailure({ provider, model }, status, {}, ctx);
|
|
191
|
+
if (!switched) return;
|
|
192
|
+
const loaded = config;
|
|
193
|
+
if (!loaded || loaded.autoRetry === false) return;
|
|
194
|
+
const prompt = lastUserPromptText(ctx);
|
|
195
|
+
if (!prompt) return;
|
|
196
|
+
ctx.ui.notify(`Model fallback: retrying failed prompt on ${activeFallbackKey}.`, "info");
|
|
197
|
+
await pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
186
198
|
});
|
|
187
199
|
|
|
188
200
|
pi.registerCommand("model-fallback:status", {
|
|
@@ -266,6 +278,33 @@ export default function modelFallback(pi: ExtensionAPI) {
|
|
|
266
278
|
});
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
function autoRetryEnabled(config: ModelFallbackConfig): boolean {
|
|
282
|
+
return config.autoRetry !== false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function lastUserPromptText(ctx: ExtensionContext): string | undefined {
|
|
286
|
+
const entries = ctx.sessionManager.getEntries() as unknown[];
|
|
287
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
288
|
+
const entry = entries[index];
|
|
289
|
+
if (!isRecord(entry) || !isRecord(entry.message)) continue;
|
|
290
|
+
const message = entry.message;
|
|
291
|
+
if (message.role !== "user") continue;
|
|
292
|
+
return userMessageText(message.content);
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function userMessageText(content: unknown): string | undefined {
|
|
298
|
+
if (typeof content === "string") return content.trim() || undefined;
|
|
299
|
+
if (!Array.isArray(content)) return undefined;
|
|
300
|
+
const parts: string[] = [];
|
|
301
|
+
for (const block of content) {
|
|
302
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
303
|
+
}
|
|
304
|
+
const text = parts.join("\n").trim();
|
|
305
|
+
return text || undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
269
308
|
function shouldUseProjectLocalState(ctx: ExtensionContext): boolean {
|
|
270
309
|
const projectSettingsPath = join(ctx.cwd, ".pi", "settings.json");
|
|
271
310
|
return existsSync(projectSettingsPath) && projectSettingsIncludesThisPackage(projectSettingsPath);
|
|
@@ -307,6 +346,7 @@ function formatStatus(
|
|
|
307
346
|
): string {
|
|
308
347
|
return [
|
|
309
348
|
`Model fallback: ${config.enabled ? "enabled" : "disabled"}`,
|
|
349
|
+
`Auto retry: ${autoRetryEnabled(config) ? "enabled" : "disabled"}`,
|
|
310
350
|
`Config: ${configPath}`,
|
|
311
351
|
`State: ${statePath}`,
|
|
312
352
|
`Rules: ${config.rules.length}`,
|
|
@@ -322,6 +362,7 @@ function formatStatus(
|
|
|
322
362
|
function summarizeConfig(config: ModelFallbackConfig, warnings: RuleWarning[]): string {
|
|
323
363
|
return [
|
|
324
364
|
`enabled: ${config.enabled}`,
|
|
365
|
+
`autoRetry: ${autoRetryEnabled(config)}`,
|
|
325
366
|
...config.rules.map((rule) => `${rule.name ?? "rule"}: ${rule.matchProviders?.join(",") ?? "models"} -> ${modelRefKey(rule.fallback)}`),
|
|
326
367
|
`warnings: ${warningSummary(warnings)}`,
|
|
327
368
|
].join("\n");
|
|
@@ -423,6 +464,3 @@ function errorMessage(error: unknown): string {
|
|
|
423
464
|
return error instanceof Error ? error.message : String(error);
|
|
424
465
|
}
|
|
425
466
|
|
|
426
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
427
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
428
|
-
}
|
package/lib/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isRecord, readModelRef, readNonEmptyString } from "./internal.ts";
|
|
2
3
|
|
|
3
4
|
export const CONFIG_VERSION = 1;
|
|
4
5
|
export const DEFAULT_FALLBACK_STATUSES = [429, 500, 502, 503, 504] as const;
|
|
@@ -21,6 +22,8 @@ export interface FallbackRule {
|
|
|
21
22
|
export interface ModelFallbackConfig {
|
|
22
23
|
version: 1;
|
|
23
24
|
enabled: boolean;
|
|
25
|
+
/** Re-queue the failed user prompt after switching to the fallback model so the turn retries automatically. Defaults to true. */
|
|
26
|
+
autoRetry?: boolean;
|
|
24
27
|
rules: FallbackRule[];
|
|
25
28
|
}
|
|
26
29
|
|
|
@@ -55,6 +58,7 @@ export function defaultConfig(): ModelFallbackConfig {
|
|
|
55
58
|
return {
|
|
56
59
|
version: CONFIG_VERSION,
|
|
57
60
|
enabled: true,
|
|
61
|
+
autoRetry: true,
|
|
58
62
|
rules: [
|
|
59
63
|
{
|
|
60
64
|
name: "zai-to-deepseek-flash",
|
|
@@ -76,6 +80,7 @@ export function validateConfigShape(value: unknown): ModelFallbackConfig {
|
|
|
76
80
|
return {
|
|
77
81
|
version: CONFIG_VERSION,
|
|
78
82
|
enabled,
|
|
83
|
+
autoRetry: typeof value.autoRetry === "boolean" ? value.autoRetry : true,
|
|
79
84
|
rules: value.rules.map((ruleValue, index) => validateRule(ruleValue, index)),
|
|
80
85
|
};
|
|
81
86
|
}
|
|
@@ -281,14 +286,6 @@ function formatModelList(models: ModelRef[]): string {
|
|
|
281
286
|
return models.map(modelRefKey).sort().join(", ");
|
|
282
287
|
}
|
|
283
288
|
|
|
284
|
-
function readModelRef(value: unknown, path: string): ModelRef {
|
|
285
|
-
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
286
|
-
return {
|
|
287
|
-
provider: readNonEmptyString(value.provider, `${path}.provider`),
|
|
288
|
-
model: readNonEmptyString(value.model, `${path}.model`),
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
|
|
292
289
|
function readModelRefArray(value: unknown, path: string): ModelRef[] {
|
|
293
290
|
if (!Array.isArray(value)) throw new Error(`${path} must be an array.`);
|
|
294
291
|
if (value.length === 0) throw new Error(`${path} must not be empty.`);
|
|
@@ -315,11 +312,3 @@ function readPositiveInteger(value: unknown, path: string): number {
|
|
|
315
312
|
return value;
|
|
316
313
|
}
|
|
317
314
|
|
|
318
|
-
function readNonEmptyString(value: unknown, path: string): string {
|
|
319
|
-
if (typeof value !== "string" || value.trim() === "") throw new Error(`${path} must be a non-empty string.`);
|
|
320
|
-
return value.trim();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
324
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
325
|
-
}
|
package/lib/error-status.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export function parseStatusFromErrorMessage(message: string): number | undefined {
|
|
2
2
|
const patterns: RegExp[] = [
|
|
3
|
+
// Leading bare status code, optionally prefixed with "Error:", e.g. "401: {\"type\":\"CreditsError\"...}"
|
|
4
|
+
// or "Error: 401: {\"type\":\"CreditsError\"...}" (OpenCode Go, OpenRouter relays)
|
|
5
|
+
/^\s*(?:error\s*:?\s*)?([1-5][0-9]{2})\s*:/i,
|
|
3
6
|
/\b(?:status|code|http)\s*[:\s]?\s*([1-5][0-9]{2})\b/i,
|
|
4
7
|
/\bHTTP\/\d(?:\.\d)?\s+([1-5][0-9]{2})\b/,
|
|
5
8
|
/\b(?:rate[\s-]?limit(?:ed)?|too many requests)[^0-9]{0,40}([45][0-9]{2})\b/i,
|
package/lib/internal.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
4
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function readNonEmptyString(value: unknown, path: string): string {
|
|
8
|
+
if (typeof value !== "string" || value.trim() === "") throw new Error(`${path} must be a non-empty string.`);
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readModelRef(value: unknown, path: string): { provider: string; model: string } {
|
|
13
|
+
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
14
|
+
return {
|
|
15
|
+
provider: readNonEmptyString(value.provider, `${path}.provider`),
|
|
16
|
+
model: readNonEmptyString(value.model, `${path}.model`),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function readJsonIfExists(path: string): Promise<unknown | undefined> {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (isNodeError(error) && error.code === "ENOENT") return undefined;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
30
|
+
return error instanceof Error && "code" in error;
|
|
31
|
+
}
|
package/lib/state.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { modelRefKey, type ModelRef } from "./config.ts";
|
|
4
|
+
import { isRecord, readJsonIfExists, readModelRef, readNonEmptyString } from "./internal.ts";
|
|
4
5
|
|
|
5
6
|
export const STATE_VERSION = 1;
|
|
6
7
|
|
|
@@ -41,10 +42,6 @@ export function upsertStateEntry(state: FallbackState, entry: FallbackStateEntry
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function modelRefKey(ref: ModelRef): string {
|
|
45
|
-
return `${ref.provider}/${ref.model}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
45
|
export function findActiveStateEntry(state: FallbackState, source: ModelRef, now = new Date()): FallbackStateEntry | undefined {
|
|
49
46
|
const sourceKey = modelRefKey(source);
|
|
50
47
|
return state.entries.find((entry) => modelRefKey(entry.source) === sourceKey && Date.parse(entry.until) > now.getTime());
|
|
@@ -87,32 +84,3 @@ function validateEntry(value: unknown, index: number): FallbackStateEntry {
|
|
|
87
84
|
return entry;
|
|
88
85
|
}
|
|
89
86
|
|
|
90
|
-
function readModelRef(value: unknown, path: string): ModelRef {
|
|
91
|
-
if (!isRecord(value)) throw new Error(`${path} must be an object.`);
|
|
92
|
-
return {
|
|
93
|
-
provider: readNonEmptyString(value.provider, `${path}.provider`),
|
|
94
|
-
model: readNonEmptyString(value.model, `${path}.model`),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function readNonEmptyString(value: unknown, path: string): string {
|
|
99
|
-
if (typeof value !== "string" || value.trim() === "") throw new Error(`${path} must be a non-empty string.`);
|
|
100
|
-
return value.trim();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function readJsonIfExists(path: string): Promise<unknown | undefined> {
|
|
104
|
-
try {
|
|
105
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (isNodeError(error) && error.code === "ENOENT") return undefined;
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
113
|
-
return error instanceof Error && "code" in error;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
117
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
118
|
-
}
|
package/lib/storage.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import { defaultConfig, validateConfigShape, type ModelFallbackConfig } from "./config.
|
|
3
|
+
import { defaultConfig, validateConfigShape, type ModelFallbackConfig } from "./config.ts";
|
|
4
|
+
import { readJsonIfExists } from "./internal.ts";
|
|
4
5
|
|
|
5
6
|
export interface ModelFallbackPaths {
|
|
6
7
|
dir: string;
|
|
@@ -23,15 +24,3 @@ export async function writeConfig(path: string, config: ModelFallbackConfig): Pr
|
|
|
23
24
|
await writeFile(path, `${JSON.stringify(validateConfigShape(config), null, 2)}\n`, "utf8");
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
async function readJsonIfExists(path: string): Promise<unknown | undefined> {
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (isNodeError(error) && error.code === "ENOENT") return undefined;
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
36
|
-
return error instanceof Error && "code" in error;
|
|
37
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-model-fallback",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pi extension that switches to a fallback model after provider failures such as 429 rate limits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,8 +54,9 @@
|
|
|
54
54
|
"@earendil-works/pi-ai": "^0.84.1",
|
|
55
55
|
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
56
56
|
"@earendil-works/pi-tui": "^0.84.1",
|
|
57
|
-
"typebox": "latest",
|
|
58
57
|
"@types/node": "^26.0.0",
|
|
58
|
+
"jiti": "^2.7.0",
|
|
59
|
+
"typebox": "latest",
|
|
59
60
|
"typescript": "^7.0.2"
|
|
60
61
|
}
|
|
61
62
|
}
|