@zgltyq/pi-provider-claude 1.1.0 → 1.2.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/README.md +13 -0
- package/index.ts +81 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,19 @@ fail over between them: when the active account hits its rate / usage cap (429/5
|
|
|
78
78
|
it's put on cooldown and the next turn transparently continues on the other account.
|
|
79
79
|
Subscription billing is preserved (Pi detects the OAuth path from the token shape).
|
|
80
80
|
|
|
81
|
+
**Priority:** array order in `claude-pool.json` IS the priority. `accounts[0]` is the
|
|
82
|
+
primary — failover is non-sticky, so as soon as the primary's cooldown expires the
|
|
83
|
+
pool returns to it automatically. Put your preferred account first.
|
|
84
|
+
|
|
85
|
+
**Detection (v1.2.0):** pi-ai's anthropic transport only emits
|
|
86
|
+
`after_provider_response` for *successful* requests — the Anthropic SDK throws on
|
|
87
|
+
429/529 before that callback runs, so header-based detection alone never fires.
|
|
88
|
+
Since v1.2.0 the primary detector pattern-matches the provider error that lands on
|
|
89
|
+
the assistant message (`stopReason: "error"`) via `message_end`, including Claude's
|
|
90
|
+
`"usage limit reached|<epoch>"` reset timestamps for accurate cooldowns. When a cap
|
|
91
|
+
is hit, the turn errors once, the pool flips (with a UI notification), and you just
|
|
92
|
+
resend your message to continue on the other account.
|
|
93
|
+
|
|
81
94
|
It is **inert** unless `<agentDir>/claude-pool.json` exists with ≥2 accounts and
|
|
82
95
|
`"enabled" !== false`.
|
|
83
96
|
|
package/index.ts
CHANGED
|
@@ -336,7 +336,10 @@ function persistPool(): void {
|
|
|
336
336
|
|
|
337
337
|
function poolSelectActive(): void {
|
|
338
338
|
const now = Date.now();
|
|
339
|
-
|
|
339
|
+
// Priority = array order: always prefer the LOWEST-index account that is
|
|
340
|
+
// not cooling down (accounts[0] is the primary, e.g. "work"). This makes
|
|
341
|
+
// failover non-sticky: as soon as the primary's cooldown expires, we
|
|
342
|
+
// return to it instead of staying on the fallback account.
|
|
340
343
|
let pick = -1;
|
|
341
344
|
for (let i = 0; i < poolAccounts.length; i++) {
|
|
342
345
|
if (poolCooldownUntil[i] <= now) {
|
|
@@ -391,6 +394,10 @@ function poolMarkRateLimited(headers?: Record<string, string>): void {
|
|
|
391
394
|
const n = Number(ra);
|
|
392
395
|
if (Number.isFinite(n)) until = n > 1e9 ? n * 1000 : now + n * 1000; // epoch-seconds vs delta-seconds
|
|
393
396
|
}
|
|
397
|
+
poolMarkRateLimitedUntil(until);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function poolMarkRateLimitedUntil(until: number): void {
|
|
394
401
|
poolCooldownUntil[poolActive] = until;
|
|
395
402
|
poolLog(
|
|
396
403
|
`rate-limited ${poolAccounts[poolActive].label} until ${new Date(until).toISOString()}`,
|
|
@@ -398,6 +405,34 @@ function poolMarkRateLimited(headers?: Record<string, string>): void {
|
|
|
398
405
|
poolSelectActive();
|
|
399
406
|
}
|
|
400
407
|
|
|
408
|
+
// Rate/usage-cap detection from ERROR MESSAGES.
|
|
409
|
+
//
|
|
410
|
+
// pi-ai's anthropic provider only invokes onResponse (→ after_provider_response)
|
|
411
|
+
// for SUCCESSFUL requests — on a 429/529 the Anthropic SDK throws before that
|
|
412
|
+
// callback, so the header-based hook above never sees rate limits in practice.
|
|
413
|
+
// The thrown error instead lands on the assistant message as stopReason:"error"
|
|
414
|
+
// + errorMessage, which flows through message_end. We pattern-match it there.
|
|
415
|
+
const POOL_LIMIT_RE =
|
|
416
|
+
/\b429\b|\b529\b|rate[ _-]?limit|usage[ _-]?limit|overloaded_error|quota/i;
|
|
417
|
+
|
|
418
|
+
// Claude subscription cap messages often carry a reset epoch after a pipe,
|
|
419
|
+
// e.g. "Claude AI usage limit reached|1750000000".
|
|
420
|
+
function poolParseResetEpoch(message: string): number | undefined {
|
|
421
|
+
const m = message.match(/\|(\d{9,13})\b/);
|
|
422
|
+
if (!m) return undefined;
|
|
423
|
+
const n = Number(m[1]);
|
|
424
|
+
return n > 1e12 ? n : n * 1000;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Returns true if the error was a rate/usage cap and the pool reacted.
|
|
428
|
+
function poolHandleErrorMessage(errorMessage: string): boolean {
|
|
429
|
+
if (!POOL_LIMIT_RE.test(errorMessage)) return false;
|
|
430
|
+
const until =
|
|
431
|
+
poolParseResetEpoch(errorMessage) ?? Date.now() + POOL_DEFAULT_COOLDOWN_MS;
|
|
432
|
+
poolMarkRateLimitedUntil(until);
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
|
|
401
436
|
// Read the current single-slot anthropic OAuth creds from auth.json.
|
|
402
437
|
function readAnthropicAuth():
|
|
403
438
|
| { refresh: string; access: string; expires: number }
|
|
@@ -518,7 +553,51 @@ function setupPool(pi: ExtensionAPI): void {
|
|
|
518
553
|
await prep();
|
|
519
554
|
});
|
|
520
555
|
|
|
521
|
-
//
|
|
556
|
+
// PRIMARY rate/usage-cap detector: provider errors surface on the assistant
|
|
557
|
+
// message (stopReason "error"), not via after_provider_response (the SDK
|
|
558
|
+
// throws on 429/529 before pi-ai's onResponse callback runs).
|
|
559
|
+
pi.on("message_end", async (event, ctx) => {
|
|
560
|
+
try {
|
|
561
|
+
const model = ctx.model;
|
|
562
|
+
if (
|
|
563
|
+
!model ||
|
|
564
|
+
model.provider !== "anthropic" ||
|
|
565
|
+
!ctx.modelRegistry.isUsingOAuth(model)
|
|
566
|
+
)
|
|
567
|
+
return undefined;
|
|
568
|
+
const msg = event.message as {
|
|
569
|
+
role?: string;
|
|
570
|
+
stopReason?: string;
|
|
571
|
+
errorMessage?: string;
|
|
572
|
+
};
|
|
573
|
+
if (
|
|
574
|
+
msg.role !== "assistant" ||
|
|
575
|
+
msg.stopReason !== "error" ||
|
|
576
|
+
typeof msg.errorMessage !== "string"
|
|
577
|
+
)
|
|
578
|
+
return undefined;
|
|
579
|
+
const prev = poolAccounts[poolActive]?.label;
|
|
580
|
+
if (!poolHandleErrorMessage(msg.errorMessage)) return undefined;
|
|
581
|
+
const next = poolAccounts[poolActive]?.label;
|
|
582
|
+
if (next !== prev) {
|
|
583
|
+
await poolEnsureFresh(poolActive);
|
|
584
|
+
ctx.ui.notify(
|
|
585
|
+
`Claude account "${prev}" hit its limit → switched to "${next}". Resend your message to continue.`,
|
|
586
|
+
"warning",
|
|
587
|
+
);
|
|
588
|
+
} else {
|
|
589
|
+
ctx.ui.notify(
|
|
590
|
+
`Claude account "${prev}" hit its limit and all pooled accounts are cooling down — retrying on the one that frees up soonest.`,
|
|
591
|
+
"warning",
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
} catch {
|
|
595
|
+
/* detection must never break the message path */
|
|
596
|
+
}
|
|
597
|
+
return undefined;
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// Secondary detector (kept for transports that DO surface response headers).
|
|
522
601
|
pi.on("after_provider_response", (event: unknown) => {
|
|
523
602
|
try {
|
|
524
603
|
const e = event as {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zgltyq/pi-provider-claude",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Claude (Anthropic OAuth/subscription) compatibility layer for pi-coding-agent — keeps ALL extension tools usable under the Claude subscription by mapping unknown flat tool names to mcp__pi__<name> on the wire instead of dropping them, plus optional multi-account failover. Self-contained fork of the tool half of @benvargas/pi-claude-code-use.",
|
|
5
5
|
"author": "Leechael",
|
|
6
6
|
"license": "MIT",
|