@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,80 @@
1
+ import type { Context, Tool } from "@earendil-works/pi-ai";
2
+
3
+ type TranscriptTextBlock = {
4
+ readonly type: "text";
5
+ readonly text: string;
6
+ };
7
+
8
+ type TranscriptSystemMessage = {
9
+ readonly role: "system";
10
+ readonly content: string | readonly TranscriptTextBlock[];
11
+ readonly sections?: Readonly<Record<string, string | null>>;
12
+ readonly toolsAdded?: readonly Tool[];
13
+ readonly toolsRemoved?: readonly { readonly name: string }[];
14
+ };
15
+
16
+ function isTranscriptSystemMessage(
17
+ message: unknown,
18
+ ): message is TranscriptSystemMessage {
19
+ return (
20
+ typeof message === "object" &&
21
+ message !== null &&
22
+ "role" in message &&
23
+ message.role === "system"
24
+ );
25
+ }
26
+
27
+ function transcriptContentText(
28
+ content: TranscriptSystemMessage["content"],
29
+ ): string {
30
+ if (typeof content === "string") return content;
31
+ return content
32
+ .filter(
33
+ (block): block is TranscriptTextBlock =>
34
+ block.type === "text" && typeof block.text === "string",
35
+ )
36
+ .map((block) => block.text)
37
+ .join("\n");
38
+ }
39
+
40
+ /**
41
+ * Replays Pi 0.87 provider-facing transcript declarations into the Pi 0.84
42
+ * Context shape consumed by the exact-pinned OAuth stream and local adaptive
43
+ * derivative. Legacy contexts contain no system messages and pass through by
44
+ * identity.
45
+ */
46
+ export function toPinnedAnthropicContext(context: Context): Context {
47
+ const transcriptMessages = context.messages as readonly unknown[];
48
+ if (!transcriptMessages.some(isTranscriptSystemMessage)) return context;
49
+
50
+ const promptContent: string[] = [];
51
+ const promptSections = new Map<string, string>();
52
+ const tools = new Map<string, Tool>();
53
+ const messages: Context["messages"] = [];
54
+
55
+ for (const message of transcriptMessages) {
56
+ if (!isTranscriptSystemMessage(message)) {
57
+ messages.push(message as Context["messages"][number]);
58
+ continue;
59
+ }
60
+
61
+ const content = transcriptContentText(message.content);
62
+ if (content.length > 0) promptContent.push(content);
63
+
64
+ for (const [name, value] of Object.entries(message.sections ?? {})) {
65
+ if (value === null) promptSections.delete(name);
66
+ else promptSections.set(name, value);
67
+ }
68
+
69
+ for (const tool of message.toolsRemoved ?? []) tools.delete(tool.name);
70
+ for (const tool of message.toolsAdded ?? []) tools.set(tool.name, tool);
71
+ }
72
+
73
+ const systemPrompt = [...promptContent, ...promptSections.values()]
74
+ .filter((part) => part.length > 0)
75
+ .join("\n\n");
76
+ const pinnedContext: Context = { messages };
77
+ if (systemPrompt.length > 0) pinnedContext.systemPrompt = systemPrompt;
78
+ if (tools.size > 0) pinnedContext.tools = [...tools.values()];
79
+ return pinnedContext;
80
+ }
@@ -0,0 +1,579 @@
1
+ import { vendorForFamily, type Vendor } from "./vendor.js";
2
+
3
+ const MANAGED_AUTHORS = ["anthropic", "openai", "google"] as const;
4
+ const MAX_MODEL_COUNT = 4_096;
5
+ const MAX_MODEL_ID_LENGTH = 200;
6
+ const MAX_TIER_COUNT = 16;
7
+ export const MAX_METHOD_LIMITS_LENGTH = 400;
8
+
9
+ export interface ApiModelRate {
10
+ readonly sourceModelId: string;
11
+ readonly inputPerToken: number;
12
+ readonly outputPerToken: number;
13
+ readonly cacheReadPerToken?: number;
14
+ readonly cacheWritePerToken?: number;
15
+ readonly cacheWrite1hPerToken?: number;
16
+ }
17
+
18
+ export interface ApiRateSnapshot {
19
+ readonly schemaVersion: 1;
20
+ readonly source: "openrouter";
21
+ readonly fetchedAtMs: number;
22
+ readonly rates: Readonly<Record<string, ApiModelRate>>;
23
+ }
24
+
25
+ /**
26
+ * Per-token USD rates for one matched cost tier or a model's base rates,
27
+ * exactly as Pi's own `calculateCost` reads `model.cost` / `model.cost.tiers`.
28
+ * Values are USD per 1,000,000 tokens.
29
+ */
30
+ export interface PiModelCostRates {
31
+ readonly input: number;
32
+ readonly output: number;
33
+ readonly cacheRead: number;
34
+ readonly cacheWrite: number;
35
+ }
36
+
37
+ /** A request-wide pricing tier: applies when observed input tokens exceed `inputTokensAbove`. */
38
+ export interface PiModelCostTier extends PiModelCostRates {
39
+ readonly inputTokensAbove: number;
40
+ }
41
+
42
+ /** One installed-catalog model's base rates plus any higher-volume tiers. */
43
+ export interface PiModelCost extends PiModelCostRates {
44
+ readonly sourceModelId: string;
45
+ readonly tiers?: readonly PiModelCostTier[];
46
+ }
47
+
48
+ /**
49
+ * A caller-supplied snapshot of Pi's installed model catalog cost data.
50
+ * `catalogVersion` is an opaque identity (for example the pinned pi-ai
51
+ * package version) recorded verbatim as immutable pricing-method provenance;
52
+ * this module never fetches or infers it.
53
+ */
54
+ export interface PiCatalogSnapshot {
55
+ readonly schemaVersion: 1;
56
+ readonly source: "pi-installed-catalog";
57
+ readonly catalogVersion: string;
58
+ readonly capturedAtMs: number;
59
+ readonly costsBySourceModelId: Readonly<Record<string, PiModelCost>>;
60
+ }
61
+
62
+ /**
63
+ * Immutable provenance for one API-equivalent price: which source produced
64
+ * it, which tier (if any) applied, and the bounded, human-readable limits of
65
+ * that method. Persisted verbatim on priced digest rows and never recomputed.
66
+ */
67
+ export interface ApiEquivalentMethod {
68
+ readonly source: "pi-installed-catalog" | "openrouter" | "legacy-day-aggregate";
69
+ readonly catalogVersion: string;
70
+ readonly tierInputTokensAbove: number | undefined;
71
+ readonly limits: string;
72
+ }
73
+
74
+ export interface EstimateTokens {
75
+ readonly input: number;
76
+ readonly output: number;
77
+ readonly cacheRead: number;
78
+ readonly cacheWrite: number;
79
+ readonly cacheWrite1h?: number;
80
+ }
81
+
82
+ export type ApiEquivalentEstimate =
83
+ | {
84
+ readonly status: "priced";
85
+ readonly totalUsd: number;
86
+ readonly rateAsOfMs: number;
87
+ readonly sourceModelId: string;
88
+ readonly components: {
89
+ readonly inputUsd: number;
90
+ readonly outputUsd: number;
91
+ readonly cacheReadUsd: number;
92
+ readonly cacheWriteUsd: number;
93
+ };
94
+ }
95
+ | { readonly status: "unpriced"; readonly reason: string };
96
+
97
+ function isRecord(value: unknown): value is Record<string, unknown> {
98
+ return typeof value === "object" && value !== null && !Array.isArray(value);
99
+ }
100
+
101
+ function parsePrice(value: unknown): number | undefined {
102
+ if (typeof value !== "string" || value.trim().length === 0) return undefined;
103
+ const parsed = Number(value);
104
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
105
+ }
106
+
107
+ function managedModelId(value: unknown): value is string {
108
+ if (
109
+ typeof value !== "string" ||
110
+ value.length === 0 ||
111
+ value.length > MAX_MODEL_ID_LENGTH
112
+ ) {
113
+ return false;
114
+ }
115
+ return MANAGED_AUTHORS.some((author) => value.startsWith(`${author}/`));
116
+ }
117
+
118
+ function parseRate(value: unknown, sourceModelId: string): ApiModelRate | undefined {
119
+ if (!isRecord(value)) return undefined;
120
+ const input = parsePrice(value.prompt);
121
+ const output = parsePrice(value.completion);
122
+ if (input === undefined || output === undefined) return undefined;
123
+ const discount = value.discount === undefined ? 0 : value.discount;
124
+ if (
125
+ typeof discount !== "number" ||
126
+ !Number.isFinite(discount) ||
127
+ discount < 0 ||
128
+ discount > 1
129
+ ) {
130
+ return undefined;
131
+ }
132
+ const multiplier = 1 - discount;
133
+ const optionalPrice = (field: string): number | undefined => {
134
+ const raw = value[field];
135
+ if (raw === undefined) return undefined;
136
+ const parsed = parsePrice(raw);
137
+ return parsed === undefined ? Number.NaN : parsed * multiplier;
138
+ };
139
+ const cacheRead = optionalPrice("input_cache_read");
140
+ const cacheWrite = optionalPrice("input_cache_write");
141
+ const cacheWrite1h = optionalPrice("input_cache_write_1h");
142
+ if ([cacheRead, cacheWrite, cacheWrite1h].some(Number.isNaN)) return undefined;
143
+ return Object.freeze({
144
+ sourceModelId,
145
+ inputPerToken: input * multiplier,
146
+ outputPerToken: output * multiplier,
147
+ ...(cacheRead === undefined ? {} : { cacheReadPerToken: cacheRead }),
148
+ ...(cacheWrite === undefined ? {} : { cacheWritePerToken: cacheWrite }),
149
+ ...(cacheWrite1h === undefined
150
+ ? {}
151
+ : { cacheWrite1hPerToken: cacheWrite1h }),
152
+ });
153
+ }
154
+
155
+ /** Parse and bound the public OpenRouter model catalogue into a small rate map. */
156
+ export function parseOpenRouterRateSnapshot(
157
+ payload: unknown,
158
+ fetchedAtMs: number,
159
+ ): ApiRateSnapshot {
160
+ if (!Number.isFinite(fetchedAtMs) || fetchedAtMs < 0) {
161
+ throw new RangeError("fetchedAtMs must be a finite non-negative timestamp.");
162
+ }
163
+ if (!isRecord(payload) || !Array.isArray(payload.data)) {
164
+ throw new TypeError("OpenRouter model response must contain a data array.");
165
+ }
166
+ if (payload.data.length > MAX_MODEL_COUNT) {
167
+ throw new RangeError("OpenRouter model response exceeds the model limit.");
168
+ }
169
+
170
+ const rates: Record<string, ApiModelRate> = {};
171
+ for (const candidate of payload.data) {
172
+ if (!isRecord(candidate) || !managedModelId(candidate.id)) continue;
173
+ const rate = parseRate(candidate.pricing, candidate.id);
174
+ if (rate === undefined) continue;
175
+ if (rates[candidate.id] === undefined) rates[candidate.id] = rate;
176
+ if (
177
+ managedModelId(candidate.canonical_slug) &&
178
+ rates[candidate.canonical_slug] === undefined
179
+ ) {
180
+ rates[candidate.canonical_slug] = rate;
181
+ }
182
+ }
183
+ return Object.freeze({
184
+ schemaVersion: 1,
185
+ source: "openrouter",
186
+ fetchedAtMs,
187
+ rates: Object.freeze(rates),
188
+ });
189
+ }
190
+
191
+ function parseStoredRate(value: unknown): ApiModelRate | undefined {
192
+ if (!isRecord(value) || !managedModelId(value.sourceModelId)) return undefined;
193
+ const required = [value.inputPerToken, value.outputPerToken];
194
+ const optional = [
195
+ value.cacheReadPerToken,
196
+ value.cacheWritePerToken,
197
+ value.cacheWrite1hPerToken,
198
+ ].filter((entry) => entry !== undefined);
199
+ if (
200
+ ![...required, ...optional].every(
201
+ (entry) =>
202
+ typeof entry === "number" && Number.isFinite(entry) && entry >= 0,
203
+ )
204
+ ) {
205
+ return undefined;
206
+ }
207
+ return Object.freeze({
208
+ sourceModelId: value.sourceModelId,
209
+ inputPerToken: value.inputPerToken as number,
210
+ outputPerToken: value.outputPerToken as number,
211
+ ...(value.cacheReadPerToken === undefined
212
+ ? {}
213
+ : { cacheReadPerToken: value.cacheReadPerToken as number }),
214
+ ...(value.cacheWritePerToken === undefined
215
+ ? {}
216
+ : { cacheWritePerToken: value.cacheWritePerToken as number }),
217
+ ...(value.cacheWrite1hPerToken === undefined
218
+ ? {}
219
+ : { cacheWrite1hPerToken: value.cacheWrite1hPerToken as number }),
220
+ });
221
+ }
222
+
223
+ /** Re-validate a persisted snapshot before any cached rate is trusted. */
224
+ export function parseStoredApiRateSnapshot(value: unknown): ApiRateSnapshot {
225
+ if (
226
+ !isRecord(value) ||
227
+ value.schemaVersion !== 1 ||
228
+ value.source !== "openrouter" ||
229
+ typeof value.fetchedAtMs !== "number" ||
230
+ !Number.isFinite(value.fetchedAtMs) ||
231
+ value.fetchedAtMs < 0 ||
232
+ !isRecord(value.rates)
233
+ ) {
234
+ throw new TypeError("Stored API rate snapshot is invalid.");
235
+ }
236
+ const entries = Object.entries(value.rates);
237
+ if (entries.length > MAX_MODEL_COUNT * 2) {
238
+ throw new RangeError("Stored API rate snapshot exceeds the model limit.");
239
+ }
240
+ const rates: Record<string, ApiModelRate> = {};
241
+ for (const [modelId, candidate] of entries) {
242
+ if (!managedModelId(modelId)) {
243
+ throw new TypeError("Stored API rate snapshot has an invalid model id.");
244
+ }
245
+ const rate = parseStoredRate(candidate);
246
+ if (rate === undefined) {
247
+ throw new TypeError("Stored API rate snapshot has an invalid rate.");
248
+ }
249
+ rates[modelId] = rate;
250
+ }
251
+ return Object.freeze({
252
+ schemaVersion: 1,
253
+ source: "openrouter",
254
+ fetchedAtMs: value.fetchedAtMs,
255
+ rates: Object.freeze(rates),
256
+ });
257
+ }
258
+
259
+ function accountAuthor(canonicalAccountId: string): Vendor | undefined {
260
+ const family = canonicalAccountId.startsWith("anthropic")
261
+ ? "anthropic"
262
+ : canonicalAccountId.startsWith("openai-codex")
263
+ ? "openai-codex"
264
+ : canonicalAccountId.startsWith("google-antigravity")
265
+ ? "google-antigravity"
266
+ : undefined;
267
+ if (family === undefined) return undefined;
268
+ if (canonicalAccountId !== family) {
269
+ const prefix = `${family}-account-`;
270
+ if (!canonicalAccountId.startsWith(prefix)) return undefined;
271
+ const suffix = canonicalAccountId.slice(prefix.length);
272
+ if (!/^[1-9]\d*$/.test(suffix) || Number(suffix) < 2) return undefined;
273
+ }
274
+ return vendorForFamily(family);
275
+ }
276
+
277
+ /** Resolve the exact managed-catalog key for a request: never borrow a neighbouring model's price. */
278
+ function resolveSourceModelId(
279
+ canonicalAccountId: string,
280
+ requestedModel: string,
281
+ ): string | undefined {
282
+ const author = accountAuthor(canonicalAccountId);
283
+ if (
284
+ author === undefined ||
285
+ requestedModel.length === 0 ||
286
+ requestedModel.length > MAX_MODEL_ID_LENGTH
287
+ ) {
288
+ return undefined;
289
+ }
290
+ const sourceId = requestedModel.includes("/")
291
+ ? requestedModel
292
+ : `${author}/${requestedModel}`;
293
+ if (!sourceId.startsWith(`${author}/`)) return undefined;
294
+ return sourceId;
295
+ }
296
+
297
+ /** Exact only: never borrow a neighbouring model's price. */
298
+ export function lookupApiRate(
299
+ snapshot: ApiRateSnapshot,
300
+ canonicalAccountId: string,
301
+ requestedModel: string,
302
+ ): ApiModelRate | undefined {
303
+ const sourceId = resolveSourceModelId(canonicalAccountId, requestedModel);
304
+ return sourceId === undefined ? undefined : snapshot.rates[sourceId];
305
+ }
306
+
307
+ /**
308
+ * Exact only: look up one model's cost in Pi's installed catalog snapshot.
309
+ * This is the authoritative tier metadata source; `lookupApiRate` above only
310
+ * extends coverage for models this catalog lacks.
311
+ */
312
+ export function lookupPiCatalogCost(
313
+ snapshot: PiCatalogSnapshot,
314
+ canonicalAccountId: string,
315
+ requestedModel: string,
316
+ ): PiModelCost | undefined {
317
+ const sourceId = resolveSourceModelId(canonicalAccountId, requestedModel);
318
+ return sourceId === undefined
319
+ ? undefined
320
+ : snapshot.costsBySourceModelId[sourceId];
321
+ }
322
+
323
+ function validTokenCount(value: number): boolean {
324
+ return Number.isFinite(value) && value >= 0 && Number.isSafeInteger(value);
325
+ }
326
+
327
+ /** Price one retained token tuple without consulting or replacing retained cost. */
328
+ export function estimateApiEquivalentCost(
329
+ tokens: EstimateTokens,
330
+ rate: ApiModelRate,
331
+ rateAsOfMs: number,
332
+ ): ApiEquivalentEstimate {
333
+ const longWrite = tokens.cacheWrite1h ?? 0;
334
+ if (
335
+ ![
336
+ tokens.input,
337
+ tokens.output,
338
+ tokens.cacheRead,
339
+ tokens.cacheWrite,
340
+ longWrite,
341
+ ].every(validTokenCount) ||
342
+ longWrite > tokens.cacheWrite ||
343
+ !Number.isFinite(rateAsOfMs)
344
+ ) {
345
+ return { status: "unpriced", reason: "invalid-input" };
346
+ }
347
+ const shortWrite = tokens.cacheWrite - longWrite;
348
+ if (tokens.cacheRead > 0 && rate.cacheReadPerToken === undefined) {
349
+ return { status: "unpriced", reason: "missing-cache-read-rate" };
350
+ }
351
+ if (shortWrite > 0 && rate.cacheWritePerToken === undefined) {
352
+ return { status: "unpriced", reason: "missing-cache-write-rate" };
353
+ }
354
+ if (longWrite > 0 && rate.cacheWrite1hPerToken === undefined) {
355
+ return { status: "unpriced", reason: "missing-cache-write-1h-rate" };
356
+ }
357
+
358
+ const components = {
359
+ inputUsd: tokens.input * rate.inputPerToken,
360
+ outputUsd: tokens.output * rate.outputPerToken,
361
+ cacheReadUsd: tokens.cacheRead * (rate.cacheReadPerToken ?? 0),
362
+ cacheWriteUsd:
363
+ shortWrite * (rate.cacheWritePerToken ?? 0) +
364
+ longWrite * (rate.cacheWrite1hPerToken ?? 0),
365
+ };
366
+ const totalUsd =
367
+ components.inputUsd +
368
+ components.outputUsd +
369
+ components.cacheReadUsd +
370
+ components.cacheWriteUsd;
371
+ if (!Number.isFinite(totalUsd)) {
372
+ return { status: "unpriced", reason: "estimate-overflow" };
373
+ }
374
+ return {
375
+ status: "priced",
376
+ totalUsd,
377
+ rateAsOfMs,
378
+ sourceModelId: rate.sourceModelId,
379
+ components,
380
+ };
381
+ }
382
+
383
+ /** Bounded, human-readable limits disclosed on every Pi-installed-catalog-priced digest row. */
384
+ export const PI_CATALOG_METHOD_LIMITS =
385
+ "Priced under this response's own token-volume tier from Pi's installed model catalog (model.cost.tiers matched by inputTokensAbove), independent of Pi's retained cost estimate.";
386
+
387
+ /** Bounded, human-readable limits disclosed when OpenRouter extends coverage for a model the installed catalog lacks. */
388
+ export const OPENROUTER_METHOD_LIMITS =
389
+ "Priced from the cached OpenRouter public rate snapshot because this model is absent from Pi's installed catalog; OpenRouter publishes one flat rate with no per-volume tiering.";
390
+
391
+ /**
392
+ * Select the tier whose rates apply to one response, exactly mirroring
393
+ * Pi's own `calculateCost`: the highest `inputTokensAbove` threshold that
394
+ * `input + cacheRead + cacheWrite` strictly exceeds. No matching tier keeps
395
+ * the model's base rates and an undefined tier boundary.
396
+ */
397
+ export function selectPiCostTier(
398
+ cost: PiModelCost,
399
+ tokens: EstimateTokens,
400
+ ): {
401
+ readonly rates: PiModelCostRates;
402
+ readonly tierInputTokensAbove: number | undefined;
403
+ } {
404
+ const inputTokens = tokens.input + tokens.cacheRead + tokens.cacheWrite;
405
+ let rates: PiModelCostRates = cost;
406
+ let matchedThreshold = -1;
407
+ let tierInputTokensAbove: number | undefined;
408
+ for (const tier of cost.tiers ?? []) {
409
+ if (
410
+ inputTokens > tier.inputTokensAbove &&
411
+ tier.inputTokensAbove > matchedThreshold
412
+ ) {
413
+ rates = tier;
414
+ matchedThreshold = tier.inputTokensAbove;
415
+ tierInputTokensAbove = tier.inputTokensAbove;
416
+ }
417
+ }
418
+ return { rates, tierInputTokensAbove };
419
+ }
420
+
421
+ export type PiCatalogApiEquivalentEstimate =
422
+ | {
423
+ readonly status: "priced";
424
+ readonly totalUsd: number;
425
+ readonly rateAsOfMs: number;
426
+ readonly sourceModelId: string;
427
+ readonly tierInputTokensAbove: number | undefined;
428
+ readonly components: {
429
+ readonly inputUsd: number;
430
+ readonly outputUsd: number;
431
+ readonly cacheReadUsd: number;
432
+ readonly cacheWriteUsd: number;
433
+ };
434
+ }
435
+ | { readonly status: "unpriced"; readonly reason: string };
436
+
437
+ /**
438
+ * Price one retained token tuple against Pi's installed catalog, exactly
439
+ * matching Pi's own `calculateCost` formula (including the 2x-base-input
440
+ * one-hour cache write surcharge). Never consults or replaces retained cost.
441
+ */
442
+ export function estimatePiCatalogCost(
443
+ tokens: EstimateTokens,
444
+ cost: PiModelCost,
445
+ rateAsOfMs: number,
446
+ ): PiCatalogApiEquivalentEstimate {
447
+ const longWrite = tokens.cacheWrite1h ?? 0;
448
+ if (
449
+ ![
450
+ tokens.input,
451
+ tokens.output,
452
+ tokens.cacheRead,
453
+ tokens.cacheWrite,
454
+ longWrite,
455
+ ].every(validTokenCount) ||
456
+ longWrite > tokens.cacheWrite ||
457
+ !Number.isFinite(rateAsOfMs)
458
+ ) {
459
+ return { status: "unpriced", reason: "invalid-input" };
460
+ }
461
+ const { rates, tierInputTokensAbove } = selectPiCostTier(cost, tokens);
462
+ const shortWrite = tokens.cacheWrite - longWrite;
463
+ const components = {
464
+ inputUsd: (tokens.input * rates.input) / 1_000_000,
465
+ outputUsd: (tokens.output * rates.output) / 1_000_000,
466
+ cacheReadUsd: (tokens.cacheRead * rates.cacheRead) / 1_000_000,
467
+ cacheWriteUsd:
468
+ (rates.cacheWrite * shortWrite + rates.input * 2 * longWrite) / 1_000_000,
469
+ };
470
+ const totalUsd =
471
+ components.inputUsd +
472
+ components.outputUsd +
473
+ components.cacheReadUsd +
474
+ components.cacheWriteUsd;
475
+ if (!Number.isFinite(totalUsd)) {
476
+ return { status: "unpriced", reason: "estimate-overflow" };
477
+ }
478
+ return {
479
+ status: "priced",
480
+ totalUsd,
481
+ rateAsOfMs,
482
+ sourceModelId: cost.sourceModelId,
483
+ tierInputTokensAbove,
484
+ components,
485
+ };
486
+ }
487
+
488
+ function finiteNonNegativeNumber(value: unknown): value is number {
489
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
490
+ }
491
+
492
+ function parsePiModelCostRates(value: unknown): PiModelCostRates | undefined {
493
+ if (
494
+ !isRecord(value) ||
495
+ ![value.input, value.output, value.cacheRead, value.cacheWrite].every(
496
+ finiteNonNegativeNumber,
497
+ )
498
+ ) {
499
+ return undefined;
500
+ }
501
+ return {
502
+ input: value.input as number,
503
+ output: value.output as number,
504
+ cacheRead: value.cacheRead as number,
505
+ cacheWrite: value.cacheWrite as number,
506
+ };
507
+ }
508
+
509
+ function parsePiModelCostTier(value: unknown): PiModelCostTier | undefined {
510
+ const rates = parsePiModelCostRates(value);
511
+ if (rates === undefined || !isRecord(value)) return undefined;
512
+ if (!finiteNonNegativeNumber(value.inputTokensAbove)) return undefined;
513
+ return Object.freeze({ ...rates, inputTokensAbove: value.inputTokensAbove });
514
+ }
515
+
516
+ function parsePiModelCost(
517
+ value: unknown,
518
+ sourceModelId: string,
519
+ ): PiModelCost | undefined {
520
+ const rates = parsePiModelCostRates(value);
521
+ if (rates === undefined || !isRecord(value)) return undefined;
522
+ if (value.tiers === undefined) {
523
+ return Object.freeze({ sourceModelId, ...rates });
524
+ }
525
+ if (!Array.isArray(value.tiers) || value.tiers.length > MAX_TIER_COUNT) {
526
+ return undefined;
527
+ }
528
+ const tiers: PiModelCostTier[] = [];
529
+ for (const candidate of value.tiers) {
530
+ const tier = parsePiModelCostTier(candidate);
531
+ if (tier === undefined) return undefined;
532
+ tiers.push(tier);
533
+ }
534
+ return Object.freeze({
535
+ sourceModelId,
536
+ ...rates,
537
+ tiers: Object.freeze(tiers),
538
+ });
539
+ }
540
+
541
+ /** Re-validate a caller-supplied installed-catalog snapshot before any cost is trusted. */
542
+ export function parsePiCatalogSnapshot(value: unknown): PiCatalogSnapshot {
543
+ if (
544
+ !isRecord(value) ||
545
+ value.schemaVersion !== 1 ||
546
+ value.source !== "pi-installed-catalog" ||
547
+ typeof value.catalogVersion !== "string" ||
548
+ value.catalogVersion.length === 0 ||
549
+ value.catalogVersion.length > MAX_MODEL_ID_LENGTH ||
550
+ typeof value.capturedAtMs !== "number" ||
551
+ !Number.isFinite(value.capturedAtMs) ||
552
+ value.capturedAtMs < 0 ||
553
+ !isRecord(value.costsBySourceModelId)
554
+ ) {
555
+ throw new TypeError("Pi installed-catalog snapshot is invalid.");
556
+ }
557
+ const entries = Object.entries(value.costsBySourceModelId);
558
+ if (entries.length > MAX_MODEL_COUNT) {
559
+ throw new RangeError("Pi installed-catalog snapshot exceeds the model limit.");
560
+ }
561
+ const costsBySourceModelId: Record<string, PiModelCost> = {};
562
+ for (const [modelId, candidate] of entries) {
563
+ if (!managedModelId(modelId)) {
564
+ throw new TypeError("Pi installed-catalog snapshot has an invalid model id.");
565
+ }
566
+ const cost = parsePiModelCost(candidate, modelId);
567
+ if (cost === undefined) {
568
+ throw new TypeError("Pi installed-catalog snapshot has an invalid model cost.");
569
+ }
570
+ costsBySourceModelId[modelId] = cost;
571
+ }
572
+ return Object.freeze({
573
+ schemaVersion: 1,
574
+ source: "pi-installed-catalog",
575
+ catalogVersion: value.catalogVersion,
576
+ capturedAtMs: value.capturedAtMs,
577
+ costsBySourceModelId: Object.freeze(costsBySourceModelId),
578
+ });
579
+ }