pi-multi-account 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/CHANGELOG.md +22 -0
- package/README.md +5 -1
- package/index.ts +201 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,27 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.0] - 2026-06-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Anthropic (Claude Pro/Max) OAuth now works out of the box.** OAuth login is
|
|
13
|
+
enabled on the base `anthropic` provider and on every `anthropic-account-*`
|
|
14
|
+
alias, and outgoing Anthropic OAuth requests are shaped (billing header +
|
|
15
|
+
system-prompt normalization) directly by this package. A separate
|
|
16
|
+
`pi-anthropic-auth` install is no longer required.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Request shaping is idempotent and only touches OAuth-marked Anthropic requests,
|
|
21
|
+
so it coexists safely with `pi-anthropic-auth` if both are installed, and leaves
|
|
22
|
+
API-key Anthropic and OpenAI Codex / Qwen requests untouched.
|
|
23
|
+
|
|
24
|
+
### Credits
|
|
25
|
+
|
|
26
|
+
- Anthropic OAuth request-shaping logic vendored from
|
|
27
|
+
[`gotgenes/pi-anthropic-auth`](https://github.com/gotgenes/pi-anthropic-auth) (MIT).
|
|
28
|
+
|
|
8
29
|
## [1.1.0] - 2026-06-10
|
|
9
30
|
|
|
10
31
|
### Fixed
|
|
@@ -55,5 +76,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
55
76
|
- Plaintext-free credential handling (SHA-256 fingerprints only); `0600`
|
|
56
77
|
config/state files.
|
|
57
78
|
|
|
79
|
+
[1.2.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.2.0
|
|
58
80
|
[1.1.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.1.0
|
|
59
81
|
[1.0.0]: https://github.com/Sarrius/pi-multi-account/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -21,7 +21,11 @@ pi install npm:pi-multi-account
|
|
|
21
21
|
|
|
22
22
|
Restart Pi or run `/reload` after installation.
|
|
23
23
|
|
|
24
|
-
> **Anthropic
|
|
24
|
+
> **Anthropic (Claude Pro/Max) works out of the box.** OAuth login and request
|
|
25
|
+
> shaping for the base `anthropic` provider and every `anthropic-account-*` alias
|
|
26
|
+
> are built in — no separate `pi-anthropic-auth` install is required. If you
|
|
27
|
+
> already have `pi-anthropic-auth`, the two coexist safely (the shaping is
|
|
28
|
+
> idempotent). OpenAI Codex / ChatGPT and Qwen accounts work as well.
|
|
25
29
|
|
|
26
30
|
### Recommended setting
|
|
27
31
|
|
package/index.ts
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
* transparently switches to the next available account/model, optionally
|
|
16
16
|
* queuing a safe continuation prompt.
|
|
17
17
|
*
|
|
18
|
-
* Anthropic OAuth
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Anthropic OAuth (Claude Pro/Max) works out of the box: this package enables
|
|
19
|
+
* OAuth login on the base `anthropic` provider and on every `anthropic-account-*`
|
|
20
|
+
* alias, and shapes the outgoing requests itself (billing header + system-prompt
|
|
21
|
+
* normalization, vendored from gotgenes/pi-anthropic-auth, MIT). No separate
|
|
22
|
+
* pi-anthropic-auth install is needed; if you have one, both coexist (idempotent).
|
|
21
23
|
*
|
|
22
24
|
* Config: ~/.pi/agent/provider-failover.json
|
|
23
25
|
* State: ~/.pi/agent/provider-failover-state.json
|
|
@@ -455,7 +457,7 @@ function codexModelDef(id: string) {
|
|
|
455
457
|
}
|
|
456
458
|
|
|
457
459
|
function registerAnthropicSlot(pi: ExtensionAPI, id: string) {
|
|
458
|
-
if (id === ANTHROPIC_BASE) return; // base provider
|
|
460
|
+
if (id === ANTHROPIC_BASE) return; // base provider: oauth + shaping registered in piMultiAccount()
|
|
459
461
|
const models = DEFAULT_ANTHROPIC_MODELS.map((m) => anthropicModelDef(m, id));
|
|
460
462
|
pi.registerProvider(id, {
|
|
461
463
|
name: `Claude Pro/Max (${id})`,
|
|
@@ -601,6 +603,193 @@ function getAssistantErrorText(messages: any[]) {
|
|
|
601
603
|
return "";
|
|
602
604
|
}
|
|
603
605
|
|
|
606
|
+
// ===========================================================================
|
|
607
|
+
// Anthropic OAuth request shaping (vendored)
|
|
608
|
+
//
|
|
609
|
+
// Makes Claude Pro/Max (OAuth) accounts work out of the box — no separate
|
|
610
|
+
// pi-anthropic-auth install required. Ported from gotgenes/pi-anthropic-auth
|
|
611
|
+
// (MIT). The logic is idempotent: if pi-anthropic-auth is ALSO installed, both
|
|
612
|
+
// before_provider_request hooks run, but the second sees the request already
|
|
613
|
+
// shaped (billing header present, Pi preamble already replaced) and no-ops.
|
|
614
|
+
//
|
|
615
|
+
// CLAUDE_CODE_VERSION must track the current Claude Code release; if it drifts
|
|
616
|
+
// too far Anthropic may reject or miscount OAuth requests. Check `claude
|
|
617
|
+
// --version` or https://github.com/anthropics/claude-code.
|
|
618
|
+
// ===========================================================================
|
|
619
|
+
|
|
620
|
+
const PI_DEFAULT_PROMPT_PREFIX = "You are an expert coding assistant operating inside pi, a coding agent harness.";
|
|
621
|
+
const PI_DEFAULT_PROMPT_TERMINATOR =
|
|
622
|
+
"- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)";
|
|
623
|
+
const MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX = "You are an expert coding assistant.";
|
|
624
|
+
const MINIMAL_ANTHROPIC_OAUTH_PROMPT = [
|
|
625
|
+
MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX,
|
|
626
|
+
"Be concise and helpful.",
|
|
627
|
+
"Use the available tools to answer the user's request.",
|
|
628
|
+
"Show file paths clearly when working with files.",
|
|
629
|
+
].join("\n");
|
|
630
|
+
const CLAUDE_CODE_IDENTITY_PREFIX = "You are Claude Code, Anthropic's official CLI";
|
|
631
|
+
const CLAUDE_CODE_VERSION = "2.1.150";
|
|
632
|
+
const BILLING_HEADER_SALT = "59cf53e54c78";
|
|
633
|
+
const BILLING_HEADER_POSITIONS = [4, 7, 20] as const;
|
|
634
|
+
const CLAUDE_CODE_ENTRYPOINT = "sdk-cli";
|
|
635
|
+
const PARAGRAPH_REMOVAL_ANCHORS: readonly string[] = [
|
|
636
|
+
"operating inside pi, a coding agent harness",
|
|
637
|
+
"In addition to the tools above",
|
|
638
|
+
"Pi documentation (read only when the user asks about pi itself",
|
|
639
|
+
];
|
|
640
|
+
const TEXT_REPLACEMENTS: readonly { match: string; replacement: string }[] = [
|
|
641
|
+
{
|
|
642
|
+
match: "Here is some useful information about the environment you are running in:",
|
|
643
|
+
replacement: "Environment context you are running in:",
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
|
|
647
|
+
type ShapeTextBlock = { type: "text"; text: string; [key: string]: unknown };
|
|
648
|
+
type ShapeMessageBlock = { type?: string; text?: string; [key: string]: unknown };
|
|
649
|
+
type ShapeMessageParam = { role?: string; content?: string | ShapeMessageBlock[]; [key: string]: unknown };
|
|
650
|
+
type ShapeAnthropicPayload = { model?: unknown; messages?: unknown; system?: unknown; stream?: unknown; [key: string]: unknown };
|
|
651
|
+
|
|
652
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
653
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isAnthropicMessagesPayload(payload: unknown): payload is ShapeAnthropicPayload {
|
|
657
|
+
return isRecord(payload) && typeof payload.model === "string" && Array.isArray(payload.messages) && typeof payload.stream === "boolean";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function hasOAuthAnthropicSystemMarker(block: unknown): boolean {
|
|
661
|
+
if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") return false;
|
|
662
|
+
return (
|
|
663
|
+
block.text.includes(CLAUDE_CODE_IDENTITY_PREFIX) ||
|
|
664
|
+
block.text.includes("x-anthropic-billing-header:") ||
|
|
665
|
+
block.text.startsWith(MINIMAL_ANTHROPIC_OAUTH_PROMPT_PREFIX)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Only requests that Pi already marked as OAuth (Claude Code identity block, or
|
|
670
|
+
// already-shaped) are touched — API-key Anthropic requests pass through untouched.
|
|
671
|
+
function isOAuthAnthropicPayload(payload: ShapeAnthropicPayload): boolean {
|
|
672
|
+
if (!Array.isArray(payload.system)) return false;
|
|
673
|
+
return payload.system.some(hasOAuthAnthropicSystemMarker);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function getFirstUserText(messages: ShapeMessageParam[]): string {
|
|
677
|
+
const firstUserMessage = messages.find((message) => message.role === "user");
|
|
678
|
+
if (!firstUserMessage) return "";
|
|
679
|
+
if (typeof firstUserMessage.content === "string") return firstUserMessage.content;
|
|
680
|
+
if (!Array.isArray(firstUserMessage.content)) return "";
|
|
681
|
+
const firstTextBlock = firstUserMessage.content.find((block) => block.type === "text" && typeof block.text === "string");
|
|
682
|
+
return typeof firstTextBlock?.text === "string" ? firstTextBlock.text : "";
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildBillingHeaderValue(messages: ShapeMessageParam[]): string | undefined {
|
|
686
|
+
const messageText = getFirstUserText(messages);
|
|
687
|
+
if (!messageText) return undefined;
|
|
688
|
+
const cch = createHash("sha256").update(messageText).digest("hex").slice(0, 5);
|
|
689
|
+
const sampledCharacters = BILLING_HEADER_POSITIONS.map((index) => messageText[index] || "0").join("");
|
|
690
|
+
const suffix = createHash("sha256")
|
|
691
|
+
.update(`${BILLING_HEADER_SALT}${sampledCharacters}${CLAUDE_CODE_VERSION}`)
|
|
692
|
+
.digest("hex")
|
|
693
|
+
.slice(0, 3);
|
|
694
|
+
return ["x-anthropic-billing-header:", `cc_version=${CLAUDE_CODE_VERSION}.${suffix};`, `cc_entrypoint=${CLAUDE_CODE_ENTRYPOINT};`, `cch=${cch};`].join(" ");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function normalizeSystemBlock(block: unknown): ShapeTextBlock {
|
|
698
|
+
if (typeof block === "string") return { type: "text", text: block };
|
|
699
|
+
if (isRecord(block) && typeof block.text === "string") return { ...block, type: "text", text: block.text };
|
|
700
|
+
return { type: "text", text: "" };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function prependBillingHeader(system: unknown, messages: ShapeMessageParam[]): unknown {
|
|
704
|
+
const billingHeader = buildBillingHeaderValue(messages);
|
|
705
|
+
if (!billingHeader) return system;
|
|
706
|
+
const systemBlocks = Array.isArray(system) ? system.map(normalizeSystemBlock) : system == null ? [] : [normalizeSystemBlock(system)];
|
|
707
|
+
// Idempotent: don't add a second billing header (e.g. pi-anthropic-auth also ran).
|
|
708
|
+
if (systemBlocks.some((block) => block.text.includes("x-anthropic-billing-header:"))) return systemBlocks;
|
|
709
|
+
const billingBlock: ShapeTextBlock = { type: "text", text: billingHeader };
|
|
710
|
+
return [billingBlock, ...systemBlocks];
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// The Anthropic API rejects assistant turns where non-tool_use blocks follow a
|
|
714
|
+
// tool_use block; Pi's serializer can produce that, so split into two turns.
|
|
715
|
+
function splitAssistantToolUseTrailingContent(messages: ShapeMessageParam[]): ShapeMessageParam[] {
|
|
716
|
+
return messages.flatMap((message) => {
|
|
717
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) return [message];
|
|
718
|
+
const firstToolUseIndex = message.content.findIndex((block) => block.type === "tool_use");
|
|
719
|
+
if (firstToolUseIndex === -1) return [message];
|
|
720
|
+
const trailingBlocks = message.content.slice(firstToolUseIndex);
|
|
721
|
+
if (!trailingBlocks.some((block) => block.type !== "tool_use")) return [message];
|
|
722
|
+
const nonToolUseBlocks = message.content.filter((block) => block.type !== "tool_use");
|
|
723
|
+
const toolUseBlocks = message.content.filter((block) => block.type === "tool_use");
|
|
724
|
+
return [
|
|
725
|
+
{ ...message, content: nonToolUseBlocks },
|
|
726
|
+
{ ...message, content: toolUseBlocks },
|
|
727
|
+
];
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function sanitizeSystemText(text: string): string {
|
|
732
|
+
const paragraphs = text.split(/\n\n+/);
|
|
733
|
+
const filtered = paragraphs.filter((paragraph) => !PARAGRAPH_REMOVAL_ANCHORS.some((anchor) => paragraph.includes(anchor)));
|
|
734
|
+
let result = filtered.join("\n\n");
|
|
735
|
+
for (const rule of TEXT_REPLACEMENTS) result = result.replaceAll(rule.match, rule.replacement);
|
|
736
|
+
return result.trim();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function findProjectContextStart(systemPrompt: string): number {
|
|
740
|
+
return systemPrompt.indexOf("\n\n# Project Context\n\n");
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function shapeAnthropicOAuthSystemPrompt(systemPrompt: string): string {
|
|
744
|
+
const prefixIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_PREFIX);
|
|
745
|
+
if (prefixIdx === -1) return systemPrompt;
|
|
746
|
+
const terminatorIdx = systemPrompt.indexOf(PI_DEFAULT_PROMPT_TERMINATOR, prefixIdx);
|
|
747
|
+
if (terminatorIdx !== -1) {
|
|
748
|
+
const terminatorEnd = terminatorIdx + PI_DEFAULT_PROMPT_TERMINATOR.length;
|
|
749
|
+
const preamble = systemPrompt.slice(prefixIdx, terminatorEnd);
|
|
750
|
+
const sanitized = sanitizeSystemText(preamble);
|
|
751
|
+
const shapedPreamble = sanitized ? `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}\n\n${sanitized}` : MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
752
|
+
return systemPrompt.slice(0, prefixIdx) + shapedPreamble + systemPrompt.slice(terminatorEnd);
|
|
753
|
+
}
|
|
754
|
+
// Pi reworded its preamble terminator → fall back to slicing from project context.
|
|
755
|
+
const projectContextStart = findProjectContextStart(systemPrompt);
|
|
756
|
+
if (projectContextStart === -1) return MINIMAL_ANTHROPIC_OAUTH_PROMPT;
|
|
757
|
+
return `${MINIMAL_ANTHROPIC_OAUTH_PROMPT}${systemPrompt.slice(projectContextStart)}`;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function shapeSystemBlocks(blocks: ShapeTextBlock[]): ShapeTextBlock[] {
|
|
761
|
+
return blocks.map((block) => {
|
|
762
|
+
if (block.type !== "text" || !block.text.includes(PI_DEFAULT_PROMPT_PREFIX)) return block;
|
|
763
|
+
return { ...block, text: shapeAnthropicOAuthSystemPrompt(block.text) };
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/** before_provider_request shaper: makes Claude Pro/Max OAuth requests acceptable. */
|
|
768
|
+
function shapeAnthropicOAuthPayload(payload: unknown): unknown {
|
|
769
|
+
if (!isAnthropicMessagesPayload(payload)) return payload;
|
|
770
|
+
const messages = payload.messages as ShapeMessageParam[];
|
|
771
|
+
if (!isOAuthAnthropicPayload(payload)) return payload; // API-key / non-OAuth → untouched
|
|
772
|
+
const normalizedMessages = splitAssistantToolUseTrailingContent(messages);
|
|
773
|
+
const shapedSystem = Array.isArray(payload.system) ? shapeSystemBlocks(payload.system as ShapeTextBlock[]) : payload.system;
|
|
774
|
+
const finalSystem = prependBillingHeader(shapedSystem, normalizedMessages);
|
|
775
|
+
return { ...payload, messages: normalizedMessages, system: finalSystem };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** OAuth override enabling Claude Pro/Max login on a provider (base or alias). */
|
|
779
|
+
const anthropicOAuthOverride = {
|
|
780
|
+
name: "Anthropic (Claude Pro/Max)",
|
|
781
|
+
login: (callbacks: any) => loginAnthropic(callbacks),
|
|
782
|
+
async refreshToken(credentials: any) {
|
|
783
|
+
const refreshed = await refreshAnthropicToken(credentials.refresh);
|
|
784
|
+
return {
|
|
785
|
+
...credentials,
|
|
786
|
+
...refreshed,
|
|
787
|
+
refresh: typeof refreshed.refresh === "string" && refreshed.refresh.trim().length > 0 ? refreshed.refresh : credentials.refresh,
|
|
788
|
+
};
|
|
789
|
+
},
|
|
790
|
+
getApiKey: (credentials: any) => credentials.access,
|
|
791
|
+
};
|
|
792
|
+
|
|
604
793
|
// ===========================================================================
|
|
605
794
|
// Extension entry point
|
|
606
795
|
// ===========================================================================
|
|
@@ -1186,6 +1375,14 @@ export default function piMultiAccount(pi: ExtensionAPI) {
|
|
|
1186
1375
|
pi.registerCommand(name, { description: "Manage automatic multi-account failover & rotation", handler: handleCommand });
|
|
1187
1376
|
}
|
|
1188
1377
|
|
|
1378
|
+
// ----- Anthropic OAuth out of the box -----------------------------------
|
|
1379
|
+
// Enable Claude Pro/Max OAuth login on the base `anthropic` provider and shape
|
|
1380
|
+
// every Anthropic OAuth request so subscription tokens are accepted — without
|
|
1381
|
+
// requiring a separate pi-anthropic-auth install. Idempotent, so it coexists
|
|
1382
|
+
// safely if pi-anthropic-auth is also present.
|
|
1383
|
+
pi.registerProvider("anthropic", { oauth: anthropicOAuthOverride } as any);
|
|
1384
|
+
pi.on("before_provider_request", (event: any) => shapeAnthropicOAuthPayload(event.payload));
|
|
1385
|
+
|
|
1189
1386
|
// ----- lifecycle hooks --------------------------------------------------
|
|
1190
1387
|
|
|
1191
1388
|
refreshDiscovery(true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-multi-account",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Automatic multi-account failover & rotation for Pi Agent across Anthropic (Claude), OpenAI/ChatGPT Codex, and Qwen/Alibaba. Auto-discovers authenticated accounts, grows the rotation on login, and drops accounts on logout, expiry, or quota/rate-limit errors.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|