@centerforagenticai/pi-multi-account 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +29 -0
  3. package/README.md +999 -0
  4. package/config/models/pi-multi-account.v1.json +32 -0
  5. package/config/subscription-plans.v1.json +122 -0
  6. package/package.json +76 -0
  7. package/packages/pi-anthropic-oauth/LICENSE +21 -0
  8. package/packages/pi-anthropic-oauth/package.json +54 -0
  9. package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
  10. package/packages/pi-anthropic-oauth/src/context.ts +116 -0
  11. package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
  12. package/packages/pi-anthropic-oauth/src/index.ts +37 -0
  13. package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
  14. package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
  15. package/packages/pi-antigravity/LICENSE +21 -0
  16. package/packages/pi-antigravity/package.json +77 -0
  17. package/packages/pi-antigravity/src/auth/index.ts +14 -0
  18. package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
  19. package/packages/pi-antigravity/src/client/client.ts +561 -0
  20. package/packages/pi-antigravity/src/client/index.ts +1 -0
  21. package/packages/pi-antigravity/src/context.ts +110 -0
  22. package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
  23. package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
  24. package/packages/pi-antigravity/src/image/image.ts +336 -0
  25. package/packages/pi-antigravity/src/image/index.ts +1 -0
  26. package/packages/pi-antigravity/src/index.ts +280 -0
  27. package/packages/pi-antigravity/src/models/discovery.ts +154 -0
  28. package/packages/pi-antigravity/src/models/grouping.ts +424 -0
  29. package/packages/pi-antigravity/src/models/index.ts +3 -0
  30. package/packages/pi-antigravity/src/models/models.ts +500 -0
  31. package/packages/pi-antigravity/src/stream/index.ts +1 -0
  32. package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
  33. package/packages/pi-antigravity/src/types/enums.ts +42 -0
  34. package/packages/pi-antigravity/src/types/index.ts +2 -0
  35. package/packages/pi-antigravity/src/types/types.ts +292 -0
  36. package/packages/pi-antigravity/src/usage/index.ts +1 -0
  37. package/packages/pi-antigravity/src/usage/usage.ts +416 -0
  38. package/packages/pi-antigravity/src/utils/http.ts +91 -0
  39. package/packages/pi-antigravity/src/utils/index.ts +3 -0
  40. package/packages/pi-antigravity/src/utils/security.ts +73 -0
  41. package/packages/pi-antigravity/src/utils/util.ts +132 -0
  42. package/scripts/multi-account.mjs +44 -0
  43. package/src/account-labels.ts +223 -0
  44. package/src/account-plan-assignment.ts +340 -0
  45. package/src/account-rate-history.ts +372 -0
  46. package/src/anthropic-adaptive-stream.ts +531 -0
  47. package/src/anthropic-alias-stream.ts +140 -0
  48. package/src/anthropic-context-compat.ts +80 -0
  49. package/src/api-pricing.ts +579 -0
  50. package/src/bounded-file-lines.ts +97 -0
  51. package/src/catalog-rebinding.ts +177 -0
  52. package/src/catalog-registration-probe.ts +111 -0
  53. package/src/codex-adapter.ts +345 -0
  54. package/src/codex-model-defaults.ts +785 -0
  55. package/src/command-completions.ts +404 -0
  56. package/src/commands.ts +2000 -0
  57. package/src/compaction.ts +14 -0
  58. package/src/config.ts +1317 -0
  59. package/src/continuation.ts +569 -0
  60. package/src/cooldowns.ts +110 -0
  61. package/src/cost-digest-store.ts +332 -0
  62. package/src/cost-digest.ts +1044 -0
  63. package/src/cost-history.ts +251 -0
  64. package/src/cost-period-closer.ts +160 -0
  65. package/src/cost-report-json.ts +318 -0
  66. package/src/cost-report-reader.ts +368 -0
  67. package/src/cost-report-render.ts +207 -0
  68. package/src/cost-report.ts +1104 -0
  69. package/src/coverage-attestation.ts +397 -0
  70. package/src/credential-lifecycle.ts +169 -0
  71. package/src/credential-refresh.ts +248 -0
  72. package/src/declaration-notice-marker.ts +238 -0
  73. package/src/diagnostic-store.ts +276 -0
  74. package/src/diagnostics.ts +309 -0
  75. package/src/discovery.ts +471 -0
  76. package/src/duration.ts +13 -0
  77. package/src/error-classification.ts +256 -0
  78. package/src/fuzzy.ts +15 -0
  79. package/src/group-policy.ts +81 -0
  80. package/src/history-store.ts +897 -0
  81. package/src/index.ts +5572 -0
  82. package/src/lifecycle.ts +378 -0
  83. package/src/logical-dispatch.ts +279 -0
  84. package/src/logical-model-selector.ts +254 -0
  85. package/src/logical-model-switcher.ts +430 -0
  86. package/src/logical-provider-attribution.ts +544 -0
  87. package/src/logical-provider.ts +1237 -0
  88. package/src/logical-route-indicator.ts +215 -0
  89. package/src/machine-lease.ts +445 -0
  90. package/src/model-support.ts +66 -0
  91. package/src/models-declaration.ts +1091 -0
  92. package/src/openai-adapter.ts +117 -0
  93. package/src/openrouter-budget.ts +304 -0
  94. package/src/openrouter-fallback.ts +146 -0
  95. package/src/period-boundaries.ts +376 -0
  96. package/src/pi-anthropic-oauth.d.ts +6 -0
  97. package/src/preflight.ts +253 -0
  98. package/src/pricing-cache.ts +235 -0
  99. package/src/project-identity.ts +100 -0
  100. package/src/provider-registration.ts +942 -0
  101. package/src/rate-formula.ts +163 -0
  102. package/src/recovery-engine.ts +853 -0
  103. package/src/recovery-output.ts +837 -0
  104. package/src/recovery-plan.ts +239 -0
  105. package/src/report-range.ts +203 -0
  106. package/src/route-resolver.ts +789 -0
  107. package/src/routing-config-transaction.ts +232 -0
  108. package/src/routing.ts +1163 -0
  109. package/src/runtime-state.ts +630 -0
  110. package/src/session-account-groups.ts +284 -0
  111. package/src/session-restore.ts +287 -0
  112. package/src/shared-usage.ts +1392 -0
  113. package/src/standalone-cli.ts +720 -0
  114. package/src/status-view.ts +578 -0
  115. package/src/subscription-plan-catalog.ts +346 -0
  116. package/src/tier-model-resolver.ts +46 -0
  117. package/src/upstream-anthropic.ts +315 -0
  118. package/src/upstream-antigravity.ts +327 -0
  119. package/src/usage-fetch.ts +1634 -0
  120. package/src/usage.ts +1026 -0
  121. package/src/vendor.ts +87 -0
  122. package/src/warmer.ts +231 -0
  123. package/src/watchdog.ts +219 -0
  124. package/src/window-history.ts +270 -0
@@ -0,0 +1,256 @@
1
+ import type { CooldownReason, NumericServerHint } from "./runtime-state.js";
2
+
3
+ export const PROVIDER_ERROR_CODES = [
4
+ "rate_limit",
5
+ "quota_exhausted",
6
+ "invalid_api_key",
7
+ "invalid_token",
8
+ "token_expired",
9
+ "token_revoked",
10
+ "oauth_refresh_rejected",
11
+ "oauth_service_unavailable",
12
+ "insufficient_permissions",
13
+ "model_not_found",
14
+ "unsupported_api_version",
15
+ "invalid_request",
16
+ ] as const;
17
+ export type ProviderErrorCode = (typeof PROVIDER_ERROR_CODES)[number];
18
+
19
+ export const TRANSPORT_FAILURE_KINDS = [
20
+ "connection-timeout",
21
+ "dns-failure",
22
+ "socket-reset",
23
+ "incomplete-response",
24
+ ] as const;
25
+ export type TransportFailureKind = (typeof TRANSPORT_FAILURE_KINDS)[number];
26
+
27
+ /**
28
+ * A deliberately narrow, structured projection of an upstream failure. Raw
29
+ * errors, messages, headers, request/response bodies, and credentials must be
30
+ * sanitized/parsed before crossing this boundary.
31
+ */
32
+ export interface ProviderFailureSignal {
33
+ readonly httpStatus?: number;
34
+ readonly code?: ProviderErrorCode;
35
+ /** Bounded model id from the current request, when the host exposes it. */
36
+ readonly modelId?: string;
37
+ readonly transportKind?: TransportFailureKind;
38
+ readonly retryAfterSeconds?: number;
39
+ readonly resetAtMs?: number;
40
+ }
41
+
42
+ export type FailureCategory =
43
+ | "quota-rate-limit"
44
+ | "terminal-auth"
45
+ | "transient-auth"
46
+ | "permission"
47
+ | "config"
48
+ | "transport"
49
+ | "unknown";
50
+
51
+ export interface FailureClassification {
52
+ readonly category: FailureCategory;
53
+ readonly accountAction:
54
+ | "cooldown-and-route"
55
+ | "invalidate-and-route"
56
+ | "route-without-retry";
57
+ readonly cooldownReason?: CooldownReason;
58
+ readonly serverHint?: NumericServerHint;
59
+ }
60
+
61
+ const RATE_LIMIT_CODES: ReadonlySet<ProviderErrorCode> = new Set([
62
+ "rate_limit",
63
+ "quota_exhausted",
64
+ ]);
65
+ const TERMINAL_AUTH_CODES: ReadonlySet<ProviderErrorCode> = new Set([
66
+ "invalid_api_key",
67
+ "invalid_token",
68
+ "token_expired",
69
+ "token_revoked",
70
+ "oauth_refresh_rejected",
71
+ ]);
72
+ const CONFIG_CODES: ReadonlySet<ProviderErrorCode> = new Set([
73
+ "model_not_found",
74
+ "unsupported_api_version",
75
+ "invalid_request",
76
+ ]);
77
+
78
+ const STRUCTURED_PROVIDER_ERROR_CODES: Readonly<
79
+ Record<string, ProviderErrorCode>
80
+ > = Object.freeze({
81
+ rate_limit_error: "rate_limit",
82
+ overloaded_error: "rate_limit",
83
+ quota_exhausted: "quota_exhausted",
84
+ authentication_error: "invalid_token",
85
+ permission_error: "insufficient_permissions",
86
+ not_found_error: "model_not_found",
87
+ invalid_request_error: "invalid_request",
88
+ });
89
+
90
+ /**
91
+ * Fixed, bounded messages emitted by pi-ai's Codex wrapper when the upstream
92
+ * SSE error has no HTTP response event for extensions to observe. Keep this an
93
+ * exact allowlist: arbitrary provider prose must never become routing state.
94
+ * Sarrius independently treats `usage limit` as exhaustion; the narrower exact
95
+ * projection here preserves that behavior without retaining the raw message.
96
+ */
97
+ const CODEX_QUOTA_EXHAUSTED_MESSAGES: ReadonlySet<string> = new Set([
98
+ "Codex error: The usage limit has been reached",
99
+ "Codex error: The usage limit has been reached.",
100
+ ]);
101
+
102
+ /**
103
+ * Extracts a bounded, allow-listed provider error code from a structured SSE
104
+ * error payload. Anthropic can open a stream with HTTP 200 and later emit
105
+ * `event: error` whose JSON becomes `AssistantMessage.errorMessage`; without
106
+ * this projection a real `rate_limit_error` is misclassified as unknown.
107
+ *
108
+ * JSON objects shaped as `{ type: "error", error: { type } }` are accepted.
109
+ * The exact fixed Codex wrapper message for subscription exhaustion is also
110
+ * projected because that SSE failure exposes no response status to extensions.
111
+ * Raw messages, request ids, details, and credentials never cross the
112
+ * classification boundary.
113
+ *
114
+ * The envelope is NOT always the whole string. Pi prefixes the HTTP status and
115
+ * may wrap the result again after exhausting its retries, so the same upstream
116
+ * rate limit arrives as any of:
117
+ *
118
+ * {"type":"error",...} (bare envelope)
119
+ * 429 {"type":"error",...} (status-prefixed)
120
+ * Retry failed after 3 attempts: 429 {"type":"error",…} (retry-wrapped)
121
+ *
122
+ * Parsing only the bare form silently lost the other two. Observed live on
123
+ * 2026-08-05: four `429 {"type":"error","error":{"type":"rate_limit_error"}}`
124
+ * messages classified as "transient or unknown", so `settleTurn` logged
125
+ * `routing.advisory` and never switched account -- the exact failure this
126
+ * projection exists to prevent, reintroduced by a leading `429 `.
127
+ *
128
+ * The scan therefore starts at the first `{`. Everything before it is Pi's own
129
+ * framing, never provider content.
130
+ */
131
+ export function providerErrorCodeFromMessage(
132
+ errorMessage: unknown,
133
+ ): ProviderErrorCode | undefined {
134
+ if (typeof errorMessage !== "string" || errorMessage.length === 0) {
135
+ return undefined;
136
+ }
137
+ // Provider error envelopes are small. Refuse oversized/unbounded diagnostic
138
+ // strings rather than parsing arbitrary assistant output.
139
+ if (errorMessage.length > 4_096) return undefined;
140
+
141
+ if (CODEX_QUOTA_EXHAUSTED_MESSAGES.has(errorMessage.trim())) {
142
+ return "quota_exhausted";
143
+ }
144
+
145
+ const envelopeStart = errorMessage.indexOf("{");
146
+ if (envelopeStart < 0) return undefined;
147
+ let parsed: unknown;
148
+ try {
149
+ parsed = JSON.parse(errorMessage.slice(envelopeStart));
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ if (typeof parsed !== "object" || parsed === null) return undefined;
154
+ const envelope = parsed as Record<string, unknown>;
155
+ if (envelope.type !== "error") return undefined;
156
+ if (typeof envelope.error !== "object" || envelope.error === null) {
157
+ return undefined;
158
+ }
159
+ const providerType = (envelope.error as Record<string, unknown>).type;
160
+ if (typeof providerType !== "string") return undefined;
161
+ return STRUCTURED_PROVIDER_ERROR_CODES[providerType];
162
+ }
163
+
164
+ function numericServerHint(
165
+ signal: ProviderFailureSignal,
166
+ ): NumericServerHint | undefined {
167
+ const retryAfterSeconds =
168
+ typeof signal.retryAfterSeconds === "number" &&
169
+ Number.isSafeInteger(signal.retryAfterSeconds) &&
170
+ signal.retryAfterSeconds >= 0
171
+ ? signal.retryAfterSeconds
172
+ : undefined;
173
+ const resetAtMs =
174
+ typeof signal.resetAtMs === "number" &&
175
+ Number.isFinite(signal.resetAtMs) &&
176
+ signal.resetAtMs >= 0
177
+ ? signal.resetAtMs
178
+ : undefined;
179
+ if (retryAfterSeconds === undefined && resetAtMs === undefined)
180
+ return undefined;
181
+ return {
182
+ ...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),
183
+ ...(resetAtMs === undefined ? {} : { resetAtMs }),
184
+ };
185
+ }
186
+
187
+ function hasCode(
188
+ signal: ProviderFailureSignal,
189
+ codes: ReadonlySet<ProviderErrorCode>,
190
+ ): boolean {
191
+ return signal.code !== undefined && codes.has(signal.code);
192
+ }
193
+
194
+ function isRateLimited(signal: ProviderFailureSignal): boolean {
195
+ return signal.httpStatus === 429 || hasCode(signal, RATE_LIMIT_CODES);
196
+ }
197
+
198
+ function isTerminalAuth(signal: ProviderFailureSignal): boolean {
199
+ return signal.httpStatus === 401 || hasCode(signal, TERMINAL_AUTH_CODES);
200
+ }
201
+
202
+ function isPermissionFailure(signal: ProviderFailureSignal): boolean {
203
+ return (
204
+ signal.code === "insufficient_permissions" || signal.httpStatus === 403
205
+ );
206
+ }
207
+
208
+ function isTransportFailure(signal: ProviderFailureSignal): boolean {
209
+ if (signal.transportKind !== undefined) return true;
210
+ return signal.httpStatus !== undefined && signal.httpStatus >= 500;
211
+ }
212
+
213
+ function cooldown(
214
+ category: FailureCategory,
215
+ cooldownReason: CooldownReason,
216
+ signal: ProviderFailureSignal,
217
+ ): FailureClassification {
218
+ const serverHint = numericServerHint(signal);
219
+ return {
220
+ category,
221
+ accountAction: "cooldown-and-route",
222
+ cooldownReason,
223
+ ...(serverHint === undefined ? {} : { serverHint }),
224
+ };
225
+ }
226
+
227
+ /** Classifies from structured status/code/type only; error.message is never inspected. */
228
+ export function classifyFailure(
229
+ signal: ProviderFailureSignal,
230
+ ): FailureClassification {
231
+ if (isRateLimited(signal)) {
232
+ return cooldown("quota-rate-limit", "rate-limit", signal);
233
+ }
234
+
235
+ if (isTerminalAuth(signal)) {
236
+ return { category: "terminal-auth", accountAction: "invalidate-and-route" };
237
+ }
238
+
239
+ if (signal.code === "oauth_service_unavailable") {
240
+ return cooldown("transient-auth", "auth-transient", signal);
241
+ }
242
+
243
+ if (isPermissionFailure(signal)) {
244
+ return cooldown("permission", "permission", signal);
245
+ }
246
+
247
+ if (hasCode(signal, CONFIG_CODES)) {
248
+ return { category: "config", accountAction: "route-without-retry" };
249
+ }
250
+
251
+ if (isTransportFailure(signal)) {
252
+ return cooldown("transport", "transport", signal);
253
+ }
254
+
255
+ return cooldown("unknown", "unknown", signal);
256
+ }
package/src/fuzzy.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Thin project-owned re-export of the host `fuzzyMatch` matcher.
3
+ *
4
+ * The completion policy imports `fuzzyMatch` through this module rather than
5
+ * directly from `@earendil-works/pi-tui` for one reason: the pi-tui namespace
6
+ * export is non-configurable and its module namespace is frozen, so a test
7
+ * cannot spy the matcher at the pi-tui boundary. Routing the single logical
8
+ * haystack call through this project module lets `M-COMP-FUZZY-ID` inspect the
9
+ * exact matcher argument — the only way to prove the standalone bare-id
10
+ * haystack field survives, because a bare id is always a subsequence of both
11
+ * prefixed fields and so cannot be distinguished by a match/no-match query.
12
+ *
13
+ * This module adds no behavior; it re-exports the host matcher unchanged.
14
+ */
15
+ export { fuzzyMatch } from "@earendil-works/pi-tui";
@@ -0,0 +1,81 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { MultiAccountConfig } from "./config.js";
4
+
5
+ export type AccountGroupResolutionSource =
6
+ | "session-override"
7
+ | "cwd-default"
8
+ | "global-default"
9
+ | "unrestricted";
10
+
11
+ export type EffectiveAccountGroupResolution =
12
+ | {
13
+ readonly groupId: string;
14
+ readonly source: Exclude<AccountGroupResolutionSource, "unrestricted">;
15
+ }
16
+ | {
17
+ readonly source: "unrestricted";
18
+ };
19
+
20
+ export type AccountGroupPolicyConfig = Pick<
21
+ MultiAccountConfig,
22
+ "accountGroupCwdDefaults" | "defaultAccountGroup"
23
+ >;
24
+
25
+ /**
26
+ * Canonicalize only the supplied directory. This deliberately does not inspect
27
+ * Git metadata or call projectKeyForCwd/resolveProjectRoot, because linked
28
+ * worktrees that share a git-common-dir remain distinct exact directories.
29
+ */
30
+ export function canonicalizeAccountGroupCwd(cwd: string): string {
31
+ const absolute = resolve(cwd);
32
+ try {
33
+ return realpathSync.native(absolute);
34
+ } catch {
35
+ return absolute;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Resolve one session's effective group in strict precedence order. Both the live
41
+ * cwd and each operator-authored absolute cwd key are realpath-canonicalized,
42
+ * then compared byte-for-byte. Subtrees and linked worktrees therefore never
43
+ * inherit a parent or shared-repository default.
44
+ */
45
+ export function resolveEffectiveAccountGroup(
46
+ config: AccountGroupPolicyConfig,
47
+ cwd: string,
48
+ sessionOverride?: string,
49
+ ): EffectiveAccountGroupResolution {
50
+ if (sessionOverride !== undefined) {
51
+ return Object.freeze({
52
+ groupId: sessionOverride,
53
+ source: "session-override" as const,
54
+ });
55
+ }
56
+
57
+ const canonicalCwd = canonicalizeAccountGroupCwd(cwd);
58
+ let cwdDefault: string | undefined;
59
+ for (const [configuredCwd, groupId] of Object.entries(
60
+ config.accountGroupCwdDefaults ?? {},
61
+ )) {
62
+ if (canonicalizeAccountGroupCwd(configuredCwd) !== canonicalCwd) continue;
63
+ if (cwdDefault !== undefined && cwdDefault !== groupId) {
64
+ throw new Error(
65
+ "Conflicting account-group defaults canonicalize to the same exact cwd.",
66
+ );
67
+ }
68
+ cwdDefault = groupId;
69
+ }
70
+ if (cwdDefault !== undefined) {
71
+ return Object.freeze({ groupId: cwdDefault, source: "cwd-default" as const });
72
+ }
73
+
74
+ if (config.defaultAccountGroup !== undefined) {
75
+ return Object.freeze({
76
+ groupId: config.defaultAccountGroup,
77
+ source: "global-default" as const,
78
+ });
79
+ }
80
+ return Object.freeze({ source: "unrestricted" as const });
81
+ }