pi-keyrouter 0.2.3 → 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.
Files changed (2) hide show
  1. package/index.ts +23 -9
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -190,9 +190,27 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
190
190
  await activate(ctx);
191
191
  });
192
192
 
193
- pi.on("after_provider_response", async (event, ctx) => {
193
+ pi.on("message_end", async (event, ctx) => {
194
194
  if (!config) return;
195
- if (event.status !== 429 && event.status !== 401 && event.status !== 403) return;
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
196
214
 
197
215
  // Determine provider from current model
198
216
  const model = ctx.model;
@@ -201,22 +219,18 @@ export default function keyRouterExtension(pi: ExtensionAPI): void {
201
219
  const rt = runtimes.get(providerName);
202
220
  if (!rt) return; // not a managed provider
203
221
 
204
- const reason: "rate-limited" | "unauthorized" =
205
- event.status === 429 ? "rate-limited" : "unauthorized";
206
-
207
222
  const authStorage = ctx.modelRegistry.authStorage;
208
- const rotated = rotate(providerName, reason, event.status, (key) => {
223
+ const rotated = rotate(providerName, reason, status, (key) => {
209
224
  authStorage.setRuntimeApiKey(providerName, key);
210
225
  });
211
226
 
212
227
  if (!rotated) {
213
- // All keys exhausted — clear runtime override so pi falls back to auth.json
214
- // (which has the user's original key). pi will surface the real error.
228
+ // All keys exhausted — let pi surface the real error.
215
229
  if (notify) {
216
230
  const failed = rt.keys.filter((k) => k.failures > 0).map((k) => k.name);
217
231
  notify(
218
232
  `🔑 keyrouter: ${providerName} — all keys exhausted (${failed.join(", ")}). ` +
219
- `Letting pi surface the original HTTP ${event.status}.`,
233
+ `Letting pi surface the original error.`,
220
234
  "error",
221
235
  );
222
236
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-keyrouter",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
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",