pi-keyrouter 0.2.2 → 0.3.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/index.ts +44 -24
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -49,16 +49,22 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
49
49
|
let notify: ((text: string, level: "info" | "warning" | "error") => void) | undefined;
|
|
50
50
|
let activationNotified = false;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Get-or-create the runtime for a provider. Keyed by the RESOLVED
|
|
54
|
+
* name (the authStorage id, e.g. "zai"), but populated from the
|
|
55
|
+
* provider config passed in directly (avoids name-mismatch bugs).
|
|
56
|
+
*/
|
|
57
|
+
function ensureRuntime(
|
|
58
|
+
resolvedName: string,
|
|
59
|
+
providerCfg: { keys: ReadonlyArray<{ name: string; value: string }> },
|
|
60
|
+
): ProviderRuntime {
|
|
61
|
+
let rt = runtimes.get(resolvedName);
|
|
54
62
|
if (rt) return rt;
|
|
55
|
-
const providerCfg = cfg.providers.find((p) => p.name === providerName);
|
|
56
|
-
if (!providerCfg) return undefined;
|
|
57
63
|
rt = {
|
|
58
64
|
keys: initKeyStates(providerCfg.keys),
|
|
59
65
|
currentIndex: -1,
|
|
60
66
|
};
|
|
61
|
-
runtimes.set(
|
|
67
|
+
runtimes.set(resolvedName, rt);
|
|
62
68
|
return rt;
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -82,15 +88,15 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
82
88
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
83
89
|
let newlyBootstrapped = 0;
|
|
84
90
|
for (const p of config.providers) {
|
|
85
|
-
const
|
|
91
|
+
const resolvedName = resolveProviderName(p.name);
|
|
86
92
|
// Skip providers we've already bootstrapped
|
|
87
|
-
if (runtimes.has(
|
|
88
|
-
if (bootstrap(
|
|
89
|
-
const rt = runtimes.get(
|
|
93
|
+
if (runtimes.has(resolvedName)) continue;
|
|
94
|
+
if (bootstrap(resolvedName, p)) {
|
|
95
|
+
const rt = runtimes.get(resolvedName);
|
|
90
96
|
if (rt && rt.currentIndex >= 0) {
|
|
91
97
|
const key = rt.keys[rt.currentIndex];
|
|
92
98
|
if (key) {
|
|
93
|
-
authStorage.setRuntimeApiKey(
|
|
99
|
+
authStorage.setRuntimeApiKey(resolvedName, key.value);
|
|
94
100
|
newlyBootstrapped++;
|
|
95
101
|
}
|
|
96
102
|
}
|
|
@@ -107,11 +113,11 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/** Set the initial key for a provider on first use. */
|
|
110
|
-
function bootstrap(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
function bootstrap(
|
|
117
|
+
resolvedName: string,
|
|
118
|
+
providerCfg: { keys: ReadonlyArray<{ name: string; value: string }> },
|
|
119
|
+
): boolean {
|
|
120
|
+
const rt = ensureRuntime(resolvedName, providerCfg);
|
|
115
121
|
if (rt.currentIndex >= 0) return true; // already bootstrapped
|
|
116
122
|
const idx = pickNextKey(rt.keys, 0, Date.now());
|
|
117
123
|
if (idx < 0) return false;
|
|
@@ -184,9 +190,27 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
184
190
|
await activate(ctx);
|
|
185
191
|
});
|
|
186
192
|
|
|
187
|
-
pi.on("
|
|
193
|
+
pi.on("message_end", async (event, ctx) => {
|
|
188
194
|
if (!config) return;
|
|
189
|
-
|
|
195
|
+
const msg = event.message;
|
|
196
|
+
// Only intercept assistant error messages
|
|
197
|
+
if (msg.role !== "assistant" || msg.stopReason !== "error") return;
|
|
198
|
+
const errMsg = msg.errorMessage ?? "";
|
|
199
|
+
if (!errMsg) return;
|
|
200
|
+
|
|
201
|
+
// Detect error type from the message string.
|
|
202
|
+
// pi's error messages look like: "429 Usage limit reached..."
|
|
203
|
+
// or "401 Unauthorized" / "403 Forbidden".
|
|
204
|
+
let reason: "rate-limited" | "unauthorized" | null = null;
|
|
205
|
+
let status = 0;
|
|
206
|
+
if (/\b429\b|rate.?limit|too many requests/i.test(errMsg)) {
|
|
207
|
+
reason = "rate-limited";
|
|
208
|
+
status = 429;
|
|
209
|
+
} else if (/\b40[13]\b|unauthorized|forbidden/i.test(errMsg)) {
|
|
210
|
+
reason = "unauthorized";
|
|
211
|
+
status = errMsg.includes("401") ? 401 : 403;
|
|
212
|
+
}
|
|
213
|
+
if (!reason) return; // not a rotatable error
|
|
190
214
|
|
|
191
215
|
// Determine provider from current model
|
|
192
216
|
const model = ctx.model;
|
|
@@ -195,22 +219,18 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
|
|
|
195
219
|
const rt = runtimes.get(providerName);
|
|
196
220
|
if (!rt) return; // not a managed provider
|
|
197
221
|
|
|
198
|
-
const reason: "rate-limited" | "unauthorized" =
|
|
199
|
-
event.status === 429 ? "rate-limited" : "unauthorized";
|
|
200
|
-
|
|
201
222
|
const authStorage = ctx.modelRegistry.authStorage;
|
|
202
|
-
const rotated = rotate(providerName, reason,
|
|
223
|
+
const rotated = rotate(providerName, reason, status, (key) => {
|
|
203
224
|
authStorage.setRuntimeApiKey(providerName, key);
|
|
204
225
|
});
|
|
205
226
|
|
|
206
227
|
if (!rotated) {
|
|
207
|
-
// All keys exhausted —
|
|
208
|
-
// (which has the user's original key). pi will surface the real error.
|
|
228
|
+
// All keys exhausted — let pi surface the real error.
|
|
209
229
|
if (notify) {
|
|
210
230
|
const failed = rt.keys.filter((k) => k.failures > 0).map((k) => k.name);
|
|
211
231
|
notify(
|
|
212
232
|
`🔑 keyrouter: ${providerName} — all keys exhausted (${failed.join(", ")}). ` +
|
|
213
|
-
`Letting pi surface the original
|
|
233
|
+
`Letting pi surface the original error.`,
|
|
214
234
|
"error",
|
|
215
235
|
);
|
|
216
236
|
}
|
package/package.json
CHANGED