pi-keyrouter 0.3.0 → 0.3.1
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/index.ts +11 -14
- package/notification.ts +99 -0
- package/package.json +2 -1
package/index.ts
CHANGED
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
// # create ~/.pi/keyrouter.json with your provider keys
|
|
28
28
|
// /reload
|
|
29
29
|
|
|
30
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
30
|
+
import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { loadConfig, configPath } from "./config.ts";
|
|
32
|
+
import { notifyRotation } from "./notification.ts";
|
|
32
33
|
import {
|
|
33
34
|
initKeyStates,
|
|
34
35
|
isAvailable,
|
|
@@ -46,7 +47,7 @@ interface ProviderRuntime {
|
|
|
46
47
|
export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
47
48
|
let config: KeyRouterConfig | undefined;
|
|
48
49
|
const runtimes = new Map<string, ProviderRuntime>();
|
|
49
|
-
let
|
|
50
|
+
let uiCtx: ExtensionUIContext | undefined;
|
|
50
51
|
let activationNotified = false;
|
|
51
52
|
|
|
52
53
|
/**
|
|
@@ -75,7 +76,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
75
76
|
*/
|
|
76
77
|
async function activate(ctx: {
|
|
77
78
|
cwd: string;
|
|
78
|
-
ui:
|
|
79
|
+
ui: ExtensionUIContext;
|
|
79
80
|
modelRegistry: { authStorage: { setRuntimeApiKey: (p: string, k: string) => void } };
|
|
80
81
|
}): Promise<void> {
|
|
81
82
|
// Load config once (reload clears it)
|
|
@@ -83,7 +84,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
83
84
|
config = loadConfig(ctx.cwd);
|
|
84
85
|
}
|
|
85
86
|
if (config.providers.length === 0) return;
|
|
86
|
-
|
|
87
|
+
uiCtx = ctx.ui;
|
|
87
88
|
|
|
88
89
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
89
90
|
let newlyBootstrapped = 0;
|
|
@@ -160,8 +161,8 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
160
161
|
setKey(nextKey.value);
|
|
161
162
|
rt.currentIndex = nextIdx;
|
|
162
163
|
|
|
163
|
-
// Notify
|
|
164
|
-
if (
|
|
164
|
+
// Notify with yellow box widget (falls back to plain notify)
|
|
165
|
+
if (currentKey && uiCtx) {
|
|
165
166
|
const event: RotationEvent = {
|
|
166
167
|
provider: providerName,
|
|
167
168
|
fromKey: currentKey.name,
|
|
@@ -170,11 +171,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
170
171
|
status,
|
|
171
172
|
attempt: rt.keys.reduce((a, k) => a + k.failures, 0),
|
|
172
173
|
};
|
|
173
|
-
|
|
174
|
-
`🔑 keyrouter: ${event.provider} — ${event.fromKey} → ${event.toKey} ` +
|
|
175
|
-
`(HTTP ${event.status}, ${event.reason})`,
|
|
176
|
-
"warning",
|
|
177
|
-
);
|
|
174
|
+
notifyRotation(uiCtx, event);
|
|
178
175
|
}
|
|
179
176
|
return true;
|
|
180
177
|
}
|
|
@@ -226,9 +223,9 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
226
223
|
|
|
227
224
|
if (!rotated) {
|
|
228
225
|
// All keys exhausted — let pi surface the real error.
|
|
229
|
-
if (
|
|
226
|
+
if (uiCtx) {
|
|
230
227
|
const failed = rt.keys.filter((k) => k.failures > 0).map((k) => k.name);
|
|
231
|
-
notify(
|
|
228
|
+
uiCtx.notify(
|
|
232
229
|
`🔑 keyrouter: ${providerName} — all keys exhausted (${failed.join(", ")}). ` +
|
|
233
230
|
`Letting pi surface the original error.`,
|
|
234
231
|
"error",
|
|
@@ -240,7 +237,7 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
240
237
|
pi.on("session_shutdown", () => {
|
|
241
238
|
runtimes.clear();
|
|
242
239
|
config = undefined;
|
|
243
|
-
|
|
240
|
+
uiCtx = undefined;
|
|
244
241
|
activationNotified = false;
|
|
245
242
|
});
|
|
246
243
|
|
package/notification.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// notification.ts — yellow Box widget for key rotation events
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Uses the same pattern as pi-soly's notification.ts:
|
|
6
|
+
// ui.setWidget(key, (tui, theme) => Component, { placement })
|
|
7
|
+
//
|
|
8
|
+
// Why yellow? Rotations are not errors (the request will succeed on the next
|
|
9
|
+
// key), so red toolErrorBg would be misleading. They're warnings, so we use
|
|
10
|
+
// yellow (\x1b[43m) which isn't in pi's ThemeBg palette but is universally
|
|
11
|
+
// supported across terminals (8-color ANSI, always available).
|
|
12
|
+
//
|
|
13
|
+
// Reset codes:
|
|
14
|
+
// \x1b[49m — reset background
|
|
15
|
+
// \x1b[39m — reset foreground (text color)
|
|
16
|
+
|
|
17
|
+
import { Box, Spacer, Text } from "@earendil-works/pi-tui";
|
|
18
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { RotationEvent } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
const WIDGET_KEY = "keyrouter-rotation";
|
|
22
|
+
const AUTO_CLEAR_MS = 8000;
|
|
23
|
+
|
|
24
|
+
// Standard ANSI yellow background (works in all terminals — 8-color minimum).
|
|
25
|
+
// Truecolor would be nicer but detection is terminal-specific; 43m is safe.
|
|
26
|
+
const YELLOW_BG = "\x1b[43m";
|
|
27
|
+
const BLACK_FG = "\x1b[30m";
|
|
28
|
+
const RESET_BG = "\x1b[49m";
|
|
29
|
+
const RESET_FG = "\x1b[39m";
|
|
30
|
+
const BOLD = "\x1b[1m";
|
|
31
|
+
|
|
32
|
+
/** Wrap text in a yellow box (background + black text for contrast). */
|
|
33
|
+
function yellowBg(text: string): string {
|
|
34
|
+
return `${YELLOW_BG}${BLACK_FG}${text}${RESET_BG}${RESET_FG}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build the rotation widget. */
|
|
38
|
+
function buildRotationBox(event: RotationEvent): Box {
|
|
39
|
+
const box = new Box(1, 0, (t) => yellowBg(t));
|
|
40
|
+
|
|
41
|
+
// Title: 🔑 keyrouter: provider — fromKey → toKey
|
|
42
|
+
const title = `${BOLD}🔑 keyrouter: ${event.provider} — ${event.fromKey} → ${event.toKey}${BOLD === "\x1b[1m" ? "\x1b[22m" : ""}`;
|
|
43
|
+
box.addChild(new Text(title, 1, 0));
|
|
44
|
+
box.addChild(new Spacer(1));
|
|
45
|
+
|
|
46
|
+
// Body: reason + status
|
|
47
|
+
const reasonText =
|
|
48
|
+
event.reason === "rate-limited"
|
|
49
|
+
? `Rate-limited (HTTP ${event.status}) — rotated to next key`
|
|
50
|
+
: `Unauthorized (HTTP ${event.status}) — skipping bad key`;
|
|
51
|
+
box.addChild(new Text(reasonText, 1, 0));
|
|
52
|
+
|
|
53
|
+
// Retry hint
|
|
54
|
+
box.addChild(new Text(`pi will retry with ${event.toKey}…`, 1, 0));
|
|
55
|
+
|
|
56
|
+
return box;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Show a yellow box widget for a key rotation event.
|
|
61
|
+
* Auto-clears after 8s.
|
|
62
|
+
*/
|
|
63
|
+
export function notifyRotation(ui: ExtensionUIContext, event: RotationEvent): void {
|
|
64
|
+
try {
|
|
65
|
+
ui.setWidget(
|
|
66
|
+
WIDGET_KEY,
|
|
67
|
+
() => buildRotationBox(event),
|
|
68
|
+
{ placement: "aboveEditor" },
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
// setWidget may fail if UI not available (print mode) — fall back to notify
|
|
72
|
+
try {
|
|
73
|
+
ui.notify(
|
|
74
|
+
`🔑 keyrouter: ${event.provider} — ${event.fromKey} → ${event.toKey} (HTTP ${event.status}, ${event.reason})`,
|
|
75
|
+
"warning",
|
|
76
|
+
);
|
|
77
|
+
} catch {
|
|
78
|
+
// no UI at all — silent
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Auto-clear after 8 seconds
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
try {
|
|
85
|
+
ui.setWidget(WIDGET_KEY, undefined);
|
|
86
|
+
} catch {
|
|
87
|
+
// session may have ended — ignore
|
|
88
|
+
}
|
|
89
|
+
}, AUTO_CLEAR_MS);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Clear the rotation widget manually. */
|
|
93
|
+
export function clearRotationWidget(ui: ExtensionUIContext): void {
|
|
94
|
+
try {
|
|
95
|
+
ui.setWidget(WIDGET_KEY, undefined);
|
|
96
|
+
} catch {
|
|
97
|
+
// ignore
|
|
98
|
+
}
|
|
99
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-keyrouter",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "API key rotation for pi-coding-agent. Multiple keys per provider, automatic 429/401 fallback, max-retries guard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"index.ts",
|
|
24
24
|
"config.ts",
|
|
25
25
|
"rotation.ts",
|
|
26
|
+
"notification.ts",
|
|
26
27
|
"types.ts"
|
|
27
28
|
],
|
|
28
29
|
"keywords": [
|