@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,97 @@
1
+ import { closeSync, openSync, readSync } from "node:fs";
2
+
3
+ export const DEFAULT_FILE_READ_CHUNK_BYTES = 64 * 1024;
4
+ const MAX_FILE_READ_CHUNK_BYTES = 1024 * 1024;
5
+
6
+ export interface BoundedFileLineReadOptions {
7
+ readonly maxLineBytes: number;
8
+ readonly chunkBytes?: number;
9
+ readonly includeIncompleteFinalLine?: boolean;
10
+ readonly maxBytes?: number;
11
+ readonly onReadChunk?: (bytes: number) => void;
12
+ }
13
+
14
+ export function normalizedFileReadChunkBytes(value: number | undefined): number {
15
+ const chunkBytes = value ?? DEFAULT_FILE_READ_CHUNK_BYTES;
16
+ if (
17
+ !Number.isSafeInteger(chunkBytes) ||
18
+ chunkBytes < 1 ||
19
+ chunkBytes > MAX_FILE_READ_CHUNK_BYTES
20
+ ) {
21
+ throw new RangeError("chunkBytes must be a safe integer from 1 to 1048576.");
22
+ }
23
+ return chunkBytes;
24
+ }
25
+
26
+ /**
27
+ * Iterate complete bounded UTF-8 lines without decoding or retaining the whole file.
28
+ * Lines above maxLineBytes are skipped until their next newline.
29
+ */
30
+ export function* iterateBoundedFileLines(
31
+ path: string,
32
+ options: BoundedFileLineReadOptions,
33
+ ): Generator<string> {
34
+ if (!Number.isSafeInteger(options.maxLineBytes) || options.maxLineBytes < 1) {
35
+ throw new RangeError("maxLineBytes must be a positive safe integer.");
36
+ }
37
+ const chunkBytes = normalizedFileReadChunkBytes(options.chunkBytes);
38
+ if (
39
+ options.maxBytes !== undefined &&
40
+ (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0)
41
+ ) {
42
+ throw new RangeError("maxBytes must be a non-negative safe integer.");
43
+ }
44
+ const includeIncompleteFinalLine = options.includeIncompleteFinalLine ?? true;
45
+ const descriptor = openSync(path, "r");
46
+ const readBuffer = Buffer.allocUnsafe(chunkBytes);
47
+ let fragments: Buffer[] = [];
48
+ let lineBytes = 0;
49
+ let oversized = false;
50
+
51
+ const addSegment = (segment: Buffer): void => {
52
+ if (oversized || segment.length === 0) return;
53
+ if (lineBytes + segment.length > options.maxLineBytes) {
54
+ fragments = [];
55
+ lineBytes = 0;
56
+ oversized = true;
57
+ return;
58
+ }
59
+ fragments.push(Buffer.from(segment));
60
+ lineBytes += segment.length;
61
+ };
62
+ const finishLine = (): string | undefined => {
63
+ const value = oversized
64
+ ? undefined
65
+ : Buffer.concat(fragments, lineBytes).toString("utf8");
66
+ fragments = [];
67
+ lineBytes = 0;
68
+ oversized = false;
69
+ return value;
70
+ };
71
+
72
+ let remainingBytes = options.maxBytes ?? Number.POSITIVE_INFINITY;
73
+ try {
74
+ while (remainingBytes > 0) {
75
+ const requested = Math.min(readBuffer.length, remainingBytes);
76
+ const bytesRead = readSync(descriptor, readBuffer, 0, requested, null);
77
+ if (bytesRead === 0) break;
78
+ remainingBytes -= bytesRead;
79
+ options.onReadChunk?.(bytesRead);
80
+ let segmentStart = 0;
81
+ for (let index = 0; index < bytesRead; index += 1) {
82
+ if (readBuffer[index] !== 0x0a) continue;
83
+ addSegment(readBuffer.subarray(segmentStart, index));
84
+ const line = finishLine();
85
+ if (line !== undefined) yield line;
86
+ segmentStart = index + 1;
87
+ }
88
+ addSegment(readBuffer.subarray(segmentStart, bytesRead));
89
+ }
90
+ if (includeIncompleteFinalLine && (fragments.length > 0 || oversized)) {
91
+ const line = finishLine();
92
+ if (line !== undefined) yield line;
93
+ }
94
+ } finally {
95
+ closeSync(descriptor);
96
+ }
97
+ }
@@ -0,0 +1,177 @@
1
+ import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface LiveCatalogCost extends Record<string, unknown> {
4
+ input: number;
5
+ output: number;
6
+ cacheRead: number;
7
+ cacheWrite: number;
8
+ tiers?: Array<
9
+ Record<string, unknown> & {
10
+ input: number;
11
+ output: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ inputTokensAbove: number;
15
+ }
16
+ >;
17
+ }
18
+
19
+ export interface LiveCatalogModel extends Record<string, unknown> {
20
+ id: string;
21
+ name: string;
22
+ provider: string;
23
+ api: string;
24
+ baseUrl: string;
25
+ reasoning: boolean;
26
+ input: ("text" | "image")[];
27
+ cost: LiveCatalogCost;
28
+ contextWindow: number;
29
+ maxTokens: number;
30
+ }
31
+
32
+ export interface LiveModelRegistrySurface {
33
+ getAll(): readonly unknown[];
34
+ }
35
+
36
+ export interface AliasCatalogRequest {
37
+ registry: unknown;
38
+ baseProvider: string;
39
+ aliasProvider: string;
40
+ displayLabel: string;
41
+ }
42
+
43
+ export class CatalogRebindingCompatibilityError extends Error {
44
+ readonly feature = "pi.model-registry-live-catalog" as const;
45
+ readonly remediation =
46
+ "Use the current public ExtensionContext.modelRegistry.getAll surface; do not import a private registry or hard-code model IDs.";
47
+
48
+ constructor(message: string) {
49
+ super(
50
+ `[pi.model-registry-live-catalog] ${message} Action: ${
51
+ "Use the current public ExtensionContext.modelRegistry.getAll surface; do not import a private registry or hard-code model IDs."
52
+ }`,
53
+ );
54
+ this.name = "CatalogRebindingCompatibilityError";
55
+ }
56
+ }
57
+
58
+ const isRecord = (value: unknown): value is Record<PropertyKey, unknown> =>
59
+ typeof value === "object" && value !== null;
60
+
61
+ function isLiveCatalogModel(value: unknown): value is LiveCatalogModel {
62
+ return (
63
+ isRecord(value) &&
64
+ typeof value.id === "string" &&
65
+ typeof value.name === "string" &&
66
+ typeof value.provider === "string" &&
67
+ typeof value.api === "string" &&
68
+ typeof value.baseUrl === "string" &&
69
+ typeof value.reasoning === "boolean" &&
70
+ Array.isArray(value.input) &&
71
+ isRecord(value.cost) &&
72
+ typeof value.contextWindow === "number" &&
73
+ typeof value.maxTokens === "number"
74
+ );
75
+ }
76
+
77
+ function readLiveCatalog(registry: unknown): readonly unknown[] {
78
+ if (!isRecord(registry) || typeof registry.getAll !== "function") {
79
+ throw new CatalogRebindingCompatibilityError(
80
+ "ExtensionContext.modelRegistry.getAll is unavailable, so the live base catalog cannot be derived.",
81
+ );
82
+ }
83
+
84
+ let catalog: unknown;
85
+ try {
86
+ catalog = Reflect.apply(registry.getAll, registry, []);
87
+ } catch (error) {
88
+ const detail = error instanceof Error ? error.message : String(error);
89
+ throw new CatalogRebindingCompatibilityError(
90
+ `ExtensionContext.modelRegistry.getAll threw: ${detail}`,
91
+ );
92
+ }
93
+
94
+ if (!Array.isArray(catalog)) {
95
+ throw new CatalogRebindingCompatibilityError(
96
+ `ExtensionContext.modelRegistry.getAll returned ${typeof catalog}; expected an array.`,
97
+ );
98
+ }
99
+ return catalog;
100
+ }
101
+
102
+ function cloneModel(model: LiveCatalogModel): LiveCatalogModel {
103
+ try {
104
+ return structuredClone(model) as LiveCatalogModel;
105
+ } catch (error) {
106
+ const detail = error instanceof Error ? error.message : String(error);
107
+ throw new CatalogRebindingCompatibilityError(
108
+ `Live model ${model.provider}/${model.id} could not be losslessly cloned: ${detail}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Reads and deep-clones every model for one live provider. Future public model
115
+ * fields are preserved and no mutable descendant is shared with the registry.
116
+ */
117
+ function cloneLiveProviderCatalog(
118
+ registry: unknown,
119
+ baseProvider: string,
120
+ ): LiveCatalogModel[] {
121
+ const models: LiveCatalogModel[] = [];
122
+ for (const candidate of readLiveCatalog(registry)) {
123
+ if (!isRecord(candidate) || candidate.provider !== baseProvider) continue;
124
+ if (!isLiveCatalogModel(candidate)) {
125
+ const modelId = typeof candidate.id === "string" ? candidate.id : "<unknown>";
126
+ throw new CatalogRebindingCompatibilityError(
127
+ `Live ${baseProvider} model ${modelId} is missing a required public model field.`,
128
+ );
129
+ }
130
+ models.push(cloneModel(candidate));
131
+ }
132
+ return models;
133
+ }
134
+
135
+ /**
136
+ * Converts a lossless live snapshot into provider-registration models. Only the
137
+ * registry-owned provider identity is removed; all current/future model fields
138
+ * and nested metadata remain deeply cloned.
139
+ */
140
+ export function cloneProviderModelCatalog(
141
+ registry: unknown,
142
+ baseProvider: string,
143
+ ): ProviderModelConfig[] {
144
+ return cloneLiveProviderCatalog(registry, baseProvider).map((model) => {
145
+ const providerModel = cloneModel(model);
146
+ Reflect.deleteProperty(providerModel, "provider");
147
+ return providerModel as ProviderModelConfig;
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Derives aliases from the registry snapshot available at call time. The deep
153
+ * clone deliberately occurs before the two intentional identity/display
154
+ * replacements, preserving current and future data fields without shared
155
+ * mutable descendants.
156
+ */
157
+ export function deriveLiveAliasCatalog(
158
+ request: AliasCatalogRequest,
159
+ ): LiveCatalogModel[] {
160
+ const { baseProvider, aliasProvider, displayLabel } = request;
161
+ if (!baseProvider.trim() || !aliasProvider.trim()) {
162
+ throw new CatalogRebindingCompatibilityError(
163
+ "Base and alias provider IDs must both be non-empty.",
164
+ );
165
+ }
166
+ if (baseProvider === aliasProvider) {
167
+ throw new CatalogRebindingCompatibilityError(
168
+ `Alias provider ${aliasProvider} must be distinct from its base provider.`,
169
+ );
170
+ }
171
+
172
+ return cloneLiveProviderCatalog(request.registry, baseProvider).map((alias) => {
173
+ alias.provider = aliasProvider;
174
+ alias.name = `${alias.name} (${displayLabel})`;
175
+ return alias;
176
+ });
177
+ }
@@ -0,0 +1,111 @@
1
+ import type {
2
+ ProviderConfig,
3
+ ProviderModelConfig,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import {
6
+ deriveLiveAliasCatalog,
7
+ type LiveCatalogModel,
8
+ type LiveModelRegistrySurface,
9
+ } from "./catalog-rebinding.js";
10
+
11
+ export interface AliasProviderProbe {
12
+ baseProvider: string;
13
+ aliasProvider: string;
14
+ displayLabel: string;
15
+ isolatedApi: string;
16
+ streamSimple: NonNullable<ProviderConfig["streamSimple"]>;
17
+ }
18
+
19
+ export interface ProviderRegistrationSurface {
20
+ registerProvider(name: string, config: ProviderConfig): void;
21
+ unregisterProvider(name: string): void;
22
+ }
23
+
24
+ export interface CatalogRegistrationProbeInput {
25
+ extensionApi: ProviderRegistrationSurface;
26
+ extensionContext: { modelRegistry: LiveModelRegistrySurface };
27
+ aliases: readonly AliasProviderProbe[];
28
+ }
29
+
30
+ export interface AliasRegistrationRecord {
31
+ baseProvider: string;
32
+ aliasProvider: string;
33
+ isolatedApi: string;
34
+ sourceApis: readonly string[];
35
+ models: readonly LiveCatalogModel[];
36
+ }
37
+
38
+ function asProviderModel(
39
+ model: LiveCatalogModel,
40
+ isolatedApi: string,
41
+ ): ProviderModelConfig {
42
+ const registeredModel = structuredClone(model);
43
+ Reflect.deleteProperty(registeredModel, "provider");
44
+ registeredModel.api = isolatedApi;
45
+ return registeredModel as ProviderModelConfig;
46
+ }
47
+
48
+ /**
49
+ * Investigation-only composition probe. It reads the live public registry on
50
+ * every invocation, then calls the public provider-registration surface. An
51
+ * isolated API ID is used only at registration time so OAuth-specific stream
52
+ * behavior cannot replace a base provider's shared API handler.
53
+ */
54
+ export function registerLiveAliasCatalogs(
55
+ input: CatalogRegistrationProbeInput,
56
+ ): AliasRegistrationRecord[] {
57
+ const seenProviders = new Set<string>();
58
+ const seenApis = new Set<string>();
59
+ const records: AliasRegistrationRecord[] = [];
60
+
61
+ for (const aliasRequest of input.aliases) {
62
+ if (seenProviders.has(aliasRequest.aliasProvider)) {
63
+ throw new Error(`Duplicate alias provider: ${aliasRequest.aliasProvider}`);
64
+ }
65
+ if (seenApis.has(aliasRequest.isolatedApi)) {
66
+ throw new Error(`Duplicate isolated API handler slot: ${aliasRequest.isolatedApi}`);
67
+ }
68
+
69
+ const aliases = deriveLiveAliasCatalog({
70
+ registry: input.extensionContext.modelRegistry,
71
+ baseProvider: aliasRequest.baseProvider,
72
+ aliasProvider: aliasRequest.aliasProvider,
73
+ displayLabel: aliasRequest.displayLabel,
74
+ });
75
+ const sourceApis = [...new Set(aliases.map(({ api }) => api))];
76
+ if (sourceApis.includes(aliasRequest.isolatedApi)) {
77
+ throw new Error(
78
+ `Alias provider ${aliasRequest.aliasProvider} must use an isolated API handler slot, not source API ${aliasRequest.isolatedApi}.`,
79
+ );
80
+ }
81
+
82
+ seenProviders.add(aliasRequest.aliasProvider);
83
+ seenApis.add(aliasRequest.isolatedApi);
84
+
85
+ // Public unregister is required even for an empty new snapshot: Pi treats
86
+ // registerProvider({ models: [] }) as no replacement, which would otherwise
87
+ // leave the previous alias catalog stale.
88
+ input.extensionApi.unregisterProvider(aliasRequest.aliasProvider);
89
+ const firstAlias = aliases[0];
90
+ if (firstAlias) {
91
+ input.extensionApi.registerProvider(aliasRequest.aliasProvider, {
92
+ api: aliasRequest.isolatedApi,
93
+ baseUrl: firstAlias.baseUrl,
94
+ streamSimple: aliasRequest.streamSimple,
95
+ models: aliases.map((model) =>
96
+ asProviderModel(model, aliasRequest.isolatedApi),
97
+ ),
98
+ });
99
+ }
100
+
101
+ records.push({
102
+ baseProvider: aliasRequest.baseProvider,
103
+ aliasProvider: aliasRequest.aliasProvider,
104
+ isolatedApi: aliasRequest.isolatedApi,
105
+ sourceApis,
106
+ models: aliases,
107
+ });
108
+ }
109
+
110
+ return records;
111
+ }