@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.
- package/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CostReport,
|
|
3
|
+
CostReportApiEquivalent,
|
|
4
|
+
CostReportPeriod,
|
|
5
|
+
SubsidizationPoint,
|
|
6
|
+
SubsidizationSeries,
|
|
7
|
+
} from "./cost-report.js";
|
|
8
|
+
import type { PeriodType } from "./period-boundaries.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Bump on any breaking field change to {@link CostReportJson}. A consumer
|
|
12
|
+
* (the standalone `cost --format json` command, or any other JSON reader of
|
|
13
|
+
* this pure projection) reads this before trusting the document's shape.
|
|
14
|
+
*/
|
|
15
|
+
export const COST_REPORT_JSON_SCHEMA_VERSION = 1;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The requested-scope summary carried at the top of {@link CostReportJson},
|
|
19
|
+
* mirroring the text renderer's leading block: the saved Pi model-price
|
|
20
|
+
* estimate and the independently calculated API-equivalent estimate are
|
|
21
|
+
* always both present and separately labeled, even when their amounts match.
|
|
22
|
+
* `note` explains that relationship in prose for a human reading raw JSON;
|
|
23
|
+
* neither `money` field is ever combined into one spend total, and neither is
|
|
24
|
+
* an actual provider bill, payment, refund, tax record, or bank charge -- see
|
|
25
|
+
* {@link CostReportJson.limitations}.
|
|
26
|
+
*/
|
|
27
|
+
export interface CostReportJsonSummary {
|
|
28
|
+
readonly scope: {
|
|
29
|
+
readonly periodType: PeriodType;
|
|
30
|
+
readonly startMs: number;
|
|
31
|
+
readonly endMs: number;
|
|
32
|
+
readonly throughMs: number;
|
|
33
|
+
readonly completed: boolean;
|
|
34
|
+
readonly coverage: CostReportPeriod["coverage"];
|
|
35
|
+
};
|
|
36
|
+
readonly money: {
|
|
37
|
+
/** Pi's own saved per-response cost for the requested scope, unchanged from `current.retainedCostUsd`. */
|
|
38
|
+
readonly piModelPriceEstimateUsd: number;
|
|
39
|
+
/** The independently calculated, tier-aware estimate for the requested scope, unchanged from `current.apiEquivalent`. */
|
|
40
|
+
readonly apiEquivalentEstimate: CostReportApiEquivalent;
|
|
41
|
+
};
|
|
42
|
+
readonly note: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** When this document was generated, and which pricing snapshot instants its API-equivalent estimates were computed against. */
|
|
46
|
+
export interface CostReportJsonProvenance {
|
|
47
|
+
readonly generatedAtMs: number;
|
|
48
|
+
readonly generatedAt: string;
|
|
49
|
+
readonly pricingSnapshotAtMs: readonly number[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The `"priced"` member of {@link SubsidizationPoint}, isolated so {@link CostReportJsonSubsidizationPoint} can override just its `coverage` field. */
|
|
53
|
+
type PricedSubsidizationPoint = Extract<SubsidizationPoint, { readonly status: "priced" }>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The legacy subsidization point as serialized for `--format json`.
|
|
57
|
+
* Identical to {@link SubsidizationPoint} -- plus an explicit
|
|
58
|
+
* `comparisonSuppressed: false` -- when `status` is not `"priced"` or
|
|
59
|
+
* `coverage` is `"complete"`. When `status` is `"priced"` and `coverage` is
|
|
60
|
+
* `"partial"` or `"unknown"`, `ratio` and `deltaFromPrevious` are withheld:
|
|
61
|
+
* the underlying retained data or its completeness classification cannot
|
|
62
|
+
* support a definitive comparison for that window, so `comparisonSuppressed`
|
|
63
|
+
* is `true` and `suppressedReason` names why. `apiEquivalentEstimateUsd` and
|
|
64
|
+
* `apportionedSubscriptionUsd` -- the two figures a shown ratio would have
|
|
65
|
+
* divided -- are always still present either way. This withholds only the
|
|
66
|
+
* ratio itself, never a value {@link SubsidizationPoint} did not already
|
|
67
|
+
* carry: `buildCostReport` and `SubsidizationPoint` itself are unchanged.
|
|
68
|
+
*/
|
|
69
|
+
export type CostReportJsonSubsidizationPoint =
|
|
70
|
+
| ((
|
|
71
|
+
| Extract<SubsidizationPoint, { readonly status: "gap" }>
|
|
72
|
+
| Extract<SubsidizationPoint, { readonly status: "unpriced" }>
|
|
73
|
+
| (Omit<PricedSubsidizationPoint, "coverage"> & { readonly coverage: "complete" })
|
|
74
|
+
) & { readonly comparisonSuppressed: false })
|
|
75
|
+
| {
|
|
76
|
+
readonly periodStartMs: number;
|
|
77
|
+
readonly periodEndMs: number;
|
|
78
|
+
readonly status: "priced";
|
|
79
|
+
readonly coverage: "partial" | "unknown";
|
|
80
|
+
readonly apiEquivalentEstimateUsd: number;
|
|
81
|
+
readonly apportionedSubscriptionUsd: number;
|
|
82
|
+
readonly comparisonSuppressed: true;
|
|
83
|
+
readonly suppressedReason: string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** {@link SubsidizationSeries}, with every point run through {@link CostReportJsonSubsidizationPoint}'s ratio-suppression rule. */
|
|
87
|
+
export interface CostReportJsonSubsidizationSeries {
|
|
88
|
+
readonly canonicalAccountId: string;
|
|
89
|
+
readonly monthlySubscriptionUsd: number;
|
|
90
|
+
readonly observedPeriods: number;
|
|
91
|
+
readonly completed: readonly CostReportJsonSubsidizationPoint[];
|
|
92
|
+
readonly current?: CostReportJsonSubsidizationPoint;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One versioned, documented JSON value for the pure `CostReport` projection.
|
|
97
|
+
* Every money, coverage, scope, and comparison field is a direct, unrenamed
|
|
98
|
+
* view of {@link CostReport} -- `current`, `completed`, `accountValue`, and
|
|
99
|
+
* `legacyUnattributed` are the exact same typed values `buildCostReport`
|
|
100
|
+
* produces, reorganized under one schema-versioned wrapper plus a
|
|
101
|
+
* requested-scope `summary` and a `limitations` list. `subsidization` is the
|
|
102
|
+
* one exception: it is the same points with a bare ratio withheld under
|
|
103
|
+
* incomplete coverage (see {@link CostReportJsonSubsidizationPoint}), a
|
|
104
|
+
* serialization decision, not a second computed value. Otherwise this is a
|
|
105
|
+
* serialization and documentation surface, never a second computed report
|
|
106
|
+
* type: it introduces no value this module did not already receive from
|
|
107
|
+
* `CostReport`.
|
|
108
|
+
*/
|
|
109
|
+
export interface CostReportJson {
|
|
110
|
+
readonly schemaVersion: typeof COST_REPORT_JSON_SCHEMA_VERSION;
|
|
111
|
+
readonly provenance: CostReportJsonProvenance;
|
|
112
|
+
readonly summary: CostReportJsonSummary;
|
|
113
|
+
readonly current: CostReportPeriod;
|
|
114
|
+
readonly completed: CostReport["completed"];
|
|
115
|
+
readonly accountValue: NonNullable<CostReport["accountValue"]>;
|
|
116
|
+
readonly subsidization: readonly CostReportJsonSubsidizationSeries[];
|
|
117
|
+
readonly legacyUnattributed: CostReport["legacyUnattributed"];
|
|
118
|
+
readonly limitations: readonly string[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reuses the spec's own approved billing-claim boundary language verbatim so
|
|
123
|
+
* the JSON document and the product description never drift: neither
|
|
124
|
+
* estimate is an actual provider charge, the recorded rate and allocated
|
|
125
|
+
* account cost are not an invoice/payment/refund/tax record/bank charge, and
|
|
126
|
+
* comparisons never claim a real subsidy, saving, or provider revenue.
|
|
127
|
+
*/
|
|
128
|
+
const BASE_LIMITATION =
|
|
129
|
+
"Neither the Pi model-price estimate nor the API-equivalent estimate is an actual provider charge. The recorded account rate and allocated account cost are not an invoice, payment, refund, tax record, or bank charge. Differences, ratios, and value multiples compare report values only and do not claim an actual subsidy, saving, provider revenue, or private-contract economics.";
|
|
130
|
+
|
|
131
|
+
function sortedUniqueNumbers(values: readonly number[]): readonly number[] {
|
|
132
|
+
return [...new Set(values)].sort((left, right) => left - right);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function pricingSnapshotAtMs(report: CostReport): readonly number[] {
|
|
136
|
+
return sortedUniqueNumbers([
|
|
137
|
+
...report.current.apiEquivalent.rateAsOfMs,
|
|
138
|
+
...report.completed.flatMap((period) => period.apiEquivalent.rateAsOfMs),
|
|
139
|
+
]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function summaryNote(current: CostReportPeriod): string {
|
|
143
|
+
const lead =
|
|
144
|
+
"The saved Pi model-price estimate and the independently calculated API-equivalent estimate are reported separately below.";
|
|
145
|
+
if (current.apiEquivalent.status !== "priced") {
|
|
146
|
+
return `${lead} The API-equivalent estimate is currently unpriced for this scope.`;
|
|
147
|
+
}
|
|
148
|
+
return current.apiEquivalent.estimatedUsd === current.retainedCostUsd
|
|
149
|
+
? `${lead} They happen to match for this window; they remain two independently produced figures.`
|
|
150
|
+
: lead;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Serializes one legacy subsidization point, withholding `ratio` and
|
|
155
|
+
* `deltaFromPrevious` when `status` is `"priced"` and `coverage` is not
|
|
156
|
+
* `"complete"`. See {@link CostReportJsonSubsidizationPoint}.
|
|
157
|
+
*
|
|
158
|
+
* `deltaFromPrevious` (cost-report.ts's `withConsecutiveDeltas`) is a
|
|
159
|
+
* difference of two ratios, so even when this point's own `coverage` is
|
|
160
|
+
* `"complete"` the delta is only as reliable as `previous` -- the
|
|
161
|
+
* immediately preceding point in the same `completed` array, never an
|
|
162
|
+
* earlier complete point reached by bridging a gap. When `previous` is
|
|
163
|
+
* missing or not itself a `"priced"`/`"complete"` point, `deltaFromPrevious`
|
|
164
|
+
* is omitted from the output rather than spread through and labeled
|
|
165
|
+
* `comparisonSuppressed: false`; `ratio` and every other field of this
|
|
166
|
+
* point remain valid and present.
|
|
167
|
+
*/
|
|
168
|
+
function toJsonSubsidizationPoint(
|
|
169
|
+
point: SubsidizationPoint,
|
|
170
|
+
previous?: SubsidizationPoint,
|
|
171
|
+
): CostReportJsonSubsidizationPoint {
|
|
172
|
+
if (point.status === "gap") {
|
|
173
|
+
return { ...point, status: point.status, comparisonSuppressed: false };
|
|
174
|
+
}
|
|
175
|
+
if (point.status === "unpriced") {
|
|
176
|
+
return { ...point, status: point.status, comparisonSuppressed: false };
|
|
177
|
+
}
|
|
178
|
+
if (point.coverage === "complete") {
|
|
179
|
+
const deltaBasisComplete =
|
|
180
|
+
previous !== undefined && previous.status === "priced" && previous.coverage === "complete";
|
|
181
|
+
if (deltaBasisComplete || point.deltaFromPrevious === undefined) {
|
|
182
|
+
return { ...point, coverage: point.coverage, comparisonSuppressed: false };
|
|
183
|
+
}
|
|
184
|
+
const { deltaFromPrevious: _omittedDelta, ...pointWithoutDelta } = point;
|
|
185
|
+
return { ...pointWithoutDelta, coverage: point.coverage, comparisonSuppressed: false };
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
periodStartMs: point.periodStartMs,
|
|
189
|
+
periodEndMs: point.periodEndMs,
|
|
190
|
+
status: "priced",
|
|
191
|
+
coverage: point.coverage,
|
|
192
|
+
apiEquivalentEstimateUsd: point.apiEquivalentEstimateUsd,
|
|
193
|
+
apportionedSubscriptionUsd: point.apportionedSubscriptionUsd,
|
|
194
|
+
comparisonSuppressed: true,
|
|
195
|
+
suppressedReason: `legacy-subsidization-${point.coverage}-coverage`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toJsonSubsidizationSeries(
|
|
200
|
+
series: SubsidizationSeries,
|
|
201
|
+
): CostReportJsonSubsidizationSeries {
|
|
202
|
+
return {
|
|
203
|
+
canonicalAccountId: series.canonicalAccountId,
|
|
204
|
+
monthlySubscriptionUsd: series.monthlySubscriptionUsd,
|
|
205
|
+
observedPeriods: series.observedPeriods,
|
|
206
|
+
completed: series.completed.map((point, index) =>
|
|
207
|
+
toJsonSubsidizationPoint(point, series.completed[index - 1]),
|
|
208
|
+
),
|
|
209
|
+
...(series.current === undefined
|
|
210
|
+
? {}
|
|
211
|
+
: { current: toJsonSubsidizationPoint(series.current) }),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One line disclosing that the legacy subsidization ratio is withheld for one or more periods whose coverage is not complete -- never left to a header disclaimer alone. */
|
|
216
|
+
function subsidizationSuppressionLimitations(report: CostReport): readonly string[] {
|
|
217
|
+
const hasSuppressedPoint = report.subsidization.some((series) =>
|
|
218
|
+
[series.current, ...series.completed].some(
|
|
219
|
+
(point) => point !== undefined && point.status === "priced" && point.coverage !== "complete",
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
return hasSuppressedPoint
|
|
223
|
+
? [
|
|
224
|
+
"Legacy subsidization comparison is withheld for one or more periods whose coverage is not complete; their apportioned subscription and API-equivalent estimate remain reported without a ratio.",
|
|
225
|
+
]
|
|
226
|
+
: [];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** One line per account-level limitation already disclosed by `CostReport` itself: a suppressed or conditional comparison, or unknown allocation coverage. */
|
|
230
|
+
function accountValueLimitations(report: CostReport): readonly string[] {
|
|
231
|
+
const limitations: string[] = [];
|
|
232
|
+
for (const entry of report.accountValue ?? []) {
|
|
233
|
+
if (entry.comparison.status === "suppressed") {
|
|
234
|
+
limitations.push(
|
|
235
|
+
`${entry.canonicalAccountId}: comparison suppressed (${entry.comparison.reason}).`,
|
|
236
|
+
);
|
|
237
|
+
} else if (entry.comparison.status === "conditional-lower-bound") {
|
|
238
|
+
limitations.push(
|
|
239
|
+
`${entry.canonicalAccountId}: comparison is a conditional lower bound (${entry.comparison.premise}).`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
if (entry.allocationHasUnknownCoverage) {
|
|
243
|
+
limitations.push(
|
|
244
|
+
`${entry.canonicalAccountId}: allocated account cost has unknown coverage for part of this window.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return limitations;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function buildLimitations(report: CostReport): readonly string[] {
|
|
252
|
+
const limitations: string[] = [BASE_LIMITATION];
|
|
253
|
+
if (report.current.coverage !== "complete") {
|
|
254
|
+
limitations.push(
|
|
255
|
+
`Requested-scope coverage is "${report.current.coverage}": part or all of the requested window has no immutable retained data.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
limitations.push(...accountValueLimitations(report));
|
|
259
|
+
if (report.legacyUnattributed.length > 0) {
|
|
260
|
+
limitations.push(
|
|
261
|
+
"Legacy unattributed retained cost exists for responses saved without a project key.",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (report.subsidization.length > 0) {
|
|
265
|
+
limitations.push(
|
|
266
|
+
"Subsidization figures use the legacy flat monthly-subscription value, superseded by recorded account rate for new comparisons.",
|
|
267
|
+
);
|
|
268
|
+
limitations.push(...subsidizationSuppressionLimitations(report));
|
|
269
|
+
}
|
|
270
|
+
return limitations;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Builds the documented {@link CostReportJson} value for `report`. Pure: no
|
|
275
|
+
* I/O, no clock read, no mutation, and no field not already present on
|
|
276
|
+
* `report` itself.
|
|
277
|
+
*/
|
|
278
|
+
export function buildCostReportJson(report: CostReport): CostReportJson {
|
|
279
|
+
const current = report.current;
|
|
280
|
+
return {
|
|
281
|
+
schemaVersion: COST_REPORT_JSON_SCHEMA_VERSION,
|
|
282
|
+
provenance: {
|
|
283
|
+
generatedAtMs: report.generatedAtMs,
|
|
284
|
+
generatedAt: new Date(report.generatedAtMs).toISOString(),
|
|
285
|
+
pricingSnapshotAtMs: pricingSnapshotAtMs(report),
|
|
286
|
+
},
|
|
287
|
+
summary: {
|
|
288
|
+
scope: {
|
|
289
|
+
periodType: report.periodType,
|
|
290
|
+
startMs: current.periodStartMs,
|
|
291
|
+
endMs: current.periodEndMs,
|
|
292
|
+
throughMs: current.throughMs,
|
|
293
|
+
completed: current.completed,
|
|
294
|
+
coverage: current.coverage,
|
|
295
|
+
},
|
|
296
|
+
money: {
|
|
297
|
+
piModelPriceEstimateUsd: current.retainedCostUsd,
|
|
298
|
+
apiEquivalentEstimate: current.apiEquivalent,
|
|
299
|
+
},
|
|
300
|
+
note: summaryNote(current),
|
|
301
|
+
},
|
|
302
|
+
current,
|
|
303
|
+
completed: report.completed,
|
|
304
|
+
accountValue: report.accountValue ?? [],
|
|
305
|
+
subsidization: report.subsidization.map(toJsonSubsidizationSeries),
|
|
306
|
+
legacyUnattributed: report.legacyUnattributed,
|
|
307
|
+
limitations: buildLimitations(report),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Renders `report` as one versioned JSON document with no surrounding prose,
|
|
313
|
+
* for the standalone report's `--format json` output (wired by a later
|
|
314
|
+
* node).
|
|
315
|
+
*/
|
|
316
|
+
export function renderCostReportJson(report: CostReport): string {
|
|
317
|
+
return JSON.stringify(buildCostReportJson(report), null, 2);
|
|
318
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type { AccountRateRecord } from "./account-rate-history.js";
|
|
2
|
+
import type { PiCatalogSnapshot } from "./api-pricing.js";
|
|
3
|
+
import { isAllowedFamily, type MultiAccountConfig } from "./config.js";
|
|
4
|
+
import {
|
|
5
|
+
classifyProviderId,
|
|
6
|
+
isProviderSlotWithinAccountLimit,
|
|
7
|
+
} from "./discovery.js";
|
|
8
|
+
import {
|
|
9
|
+
defaultCostDigestPaths,
|
|
10
|
+
CostDigestStore,
|
|
11
|
+
} from "./cost-digest-store.js";
|
|
12
|
+
import {
|
|
13
|
+
summarizeCostHistoryForReport,
|
|
14
|
+
type CostDigestRow,
|
|
15
|
+
} from "./cost-digest.js";
|
|
16
|
+
import { buildCostReport, type CostReport } from "./cost-report.js";
|
|
17
|
+
import { iterateHistory } from "./history-store.js";
|
|
18
|
+
import { getPeriodBounds, PERIOD_TYPES, type PeriodType } from "./period-boundaries.js";
|
|
19
|
+
import {
|
|
20
|
+
defaultPricingCachePaths,
|
|
21
|
+
OpenRouterPricingCache,
|
|
22
|
+
} from "./pricing-cache.js";
|
|
23
|
+
import {
|
|
24
|
+
resolveCustomRange,
|
|
25
|
+
type GranularitySegment,
|
|
26
|
+
type RetainedGranularity,
|
|
27
|
+
} from "./report-range.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A request for the six existing calendar periods (a bare {@link PeriodType},
|
|
31
|
+
* always UTC and byte-identical to the existing slash/tool compatibility
|
|
32
|
+
* path), the same six periods with an explicit report timezone (the
|
|
33
|
+
* `"period"` kind), an exact custom range described by raw `--from`/`--to`-
|
|
34
|
+
* shaped bounds (see {@link resolveCustomRange}), or all available retained
|
|
35
|
+
* history. A `"period"` request dispatches directly to
|
|
36
|
+
* {@link buildCostReport}'s existing `periodType`/`timeZone` bounds path --
|
|
37
|
+
* never through `"custom"` -- so a non-UTC named period keeps its correct
|
|
38
|
+
* zoned calendar bounds and "current unfinished period" semantics (a
|
|
39
|
+
* populated `completed` series, a still-open `current` period) instead of
|
|
40
|
+
* collapsing to a single exact-bounds custom window. The reader resolves a
|
|
41
|
+
* `"custom"` request's raw bounds and satisfiability against the real
|
|
42
|
+
* retained store itself, exactly as {@link resolveCustomRange} and
|
|
43
|
+
* {@link assertRangeSatisfiable} are documented to be used.
|
|
44
|
+
*/
|
|
45
|
+
export type CostReportRangeRequest =
|
|
46
|
+
| PeriodType
|
|
47
|
+
| {
|
|
48
|
+
readonly kind: "period";
|
|
49
|
+
readonly periodType: PeriodType;
|
|
50
|
+
readonly timeZone?: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "custom";
|
|
54
|
+
readonly fromRaw: string;
|
|
55
|
+
readonly toRaw: string;
|
|
56
|
+
readonly timeZone?: string;
|
|
57
|
+
}
|
|
58
|
+
| { readonly kind: "all-history" };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Thrown when a `"custom"` {@link CostReportRangeRequest} fails syntax
|
|
62
|
+
* validation or the retained store cannot satisfy its requested precision.
|
|
63
|
+
* `exitCode` mirrors {@link ReportRangeResult}'s own stable exit codes (2 for
|
|
64
|
+
* invalid syntax, 3 for unsatisfiable precision) so a later CLI layer can map
|
|
65
|
+
* this exception onto its own process exit without re-deriving the reason.
|
|
66
|
+
*/
|
|
67
|
+
export class CostReportRangeRequestError extends Error {
|
|
68
|
+
readonly exitCode: 2 | 3;
|
|
69
|
+
constructor(exitCode: 2 | 3, reason: string) {
|
|
70
|
+
super(reason);
|
|
71
|
+
this.name = "CostReportRangeRequestError";
|
|
72
|
+
this.exitCode = exitCode;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type CostReportReader = (request: CostReportRangeRequest) => CostReport;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Projects a live `monthlySubscriptionUsd` map to the subset within the current
|
|
80
|
+
* validated account limit.
|
|
81
|
+
*
|
|
82
|
+
* Each key is classified through `classifyProviderId()` and checked against
|
|
83
|
+
* `isProviderSlotWithinAccountLimit(slot, accountLimit)` before its price is
|
|
84
|
+
* copied. A key that does not classify or is above the current limit is dropped,
|
|
85
|
+
* so an in-range-for-maximum key above the current configured limit, or a
|
|
86
|
+
* hostile in-memory slot-33 key that bypassed config parsing, cannot activate a
|
|
87
|
+
* subscription-value series in the report. The result is frozen and retains no
|
|
88
|
+
* reference to the source object.
|
|
89
|
+
*/
|
|
90
|
+
export function projectMonthlySubscriptionUsdForReport(
|
|
91
|
+
monthlySubscriptionUsd: Readonly<Record<string, number>>,
|
|
92
|
+
accountLimit: number,
|
|
93
|
+
): Readonly<Record<string, number>> {
|
|
94
|
+
const projected: Record<string, number> = {};
|
|
95
|
+
for (const [providerId, monthlyCost] of Object.entries(monthlySubscriptionUsd)) {
|
|
96
|
+
const slot = classifyProviderId({ providerId, credentialType: "unknown" });
|
|
97
|
+
if (
|
|
98
|
+
slot === null ||
|
|
99
|
+
!isProviderSlotWithinAccountLimit(slot, accountLimit) ||
|
|
100
|
+
// Subscription-only report sink: an owning-vendor-api `openai` cost key
|
|
101
|
+
// must not enter the subscription-subsidization report, mirroring the
|
|
102
|
+
// parseMonthlySubscriptionUsd config guard.
|
|
103
|
+
!isAllowedFamily(slot.family)
|
|
104
|
+
) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
projected[providerId] = monthlyCost;
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze(projected);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Projects a live `accountRateHistory` map to the subset within the current
|
|
114
|
+
* validated account limit, mirroring {@link projectMonthlySubscriptionUsdForReport}.
|
|
115
|
+
* Each key is classified and checked against the same account-limit and
|
|
116
|
+
* allowed-family guards before its records are copied, so a hostile
|
|
117
|
+
* in-memory slot-33 key that bypassed config parsing cannot inject an
|
|
118
|
+
* out-of-range account rate into the report. The result is frozen and
|
|
119
|
+
* retains no reference to the source object.
|
|
120
|
+
*/
|
|
121
|
+
export function projectAccountRateHistoryForReport(
|
|
122
|
+
accountRateHistory: Readonly<Record<string, readonly AccountRateRecord[]>>,
|
|
123
|
+
accountLimit: number,
|
|
124
|
+
): Readonly<Record<string, readonly AccountRateRecord[]>> {
|
|
125
|
+
const projected: Record<string, readonly AccountRateRecord[]> = {};
|
|
126
|
+
for (const [providerId, records] of Object.entries(accountRateHistory)) {
|
|
127
|
+
const slot = classifyProviderId({ providerId, credentialType: "unknown" });
|
|
128
|
+
if (
|
|
129
|
+
slot === null ||
|
|
130
|
+
!isProviderSlotWithinAccountLimit(slot, accountLimit) ||
|
|
131
|
+
!isAllowedFamily(slot.family)
|
|
132
|
+
) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
projected[providerId] = records;
|
|
136
|
+
}
|
|
137
|
+
return Object.freeze(projected);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const PERIOD_TYPE_RANK: ReadonlyMap<PeriodType, number> = new Map(
|
|
141
|
+
PERIOD_TYPES.map((periodType, index) => [periodType, index]),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Describes which granularity is actually retained across `[startMs, endMs)`,
|
|
146
|
+
* derived from real, already-closed digest rows plus the still-open current
|
|
147
|
+
* day's live per-response detail -- never from a fabricated or assumed
|
|
148
|
+
* grain. A stretch covered by no digest row and outside the still-open
|
|
149
|
+
* current day is a genuine coverage gap and is simply absent from the
|
|
150
|
+
* result: {@link assertRangeSatisfiable} in `report-range.ts` then correctly
|
|
151
|
+
* refuses to let a caller slice inside it, rather than this function
|
|
152
|
+
* guessing a granularity for time nothing was ever retained at.
|
|
153
|
+
*
|
|
154
|
+
* A closed digest row is retained forever once written (the digest store
|
|
155
|
+
* never deletes a child once a parent rollup exists), so an old day that was
|
|
156
|
+
* closed while its underlying raw per-response records were still fresh
|
|
157
|
+
* keeps reporting "day" granularity even long after those raw records
|
|
158
|
+
* expired from the separate, bounded-retention raw history log -- this
|
|
159
|
+
* function only reads the immutable digest rows and the live window, never
|
|
160
|
+
* the raw log's own retention state.
|
|
161
|
+
*/
|
|
162
|
+
export function deriveRetainedGranularitySegments(input: {
|
|
163
|
+
readonly digestRows: readonly CostDigestRow[];
|
|
164
|
+
/** The start of the still-open current day, or `undefined` when no live window applies. */
|
|
165
|
+
readonly liveRawFromMs: number | undefined;
|
|
166
|
+
readonly nowMs: number;
|
|
167
|
+
}): readonly GranularitySegment[] {
|
|
168
|
+
const boundaries = new Set<number>();
|
|
169
|
+
for (const row of input.digestRows) {
|
|
170
|
+
boundaries.add(row.periodStartMs);
|
|
171
|
+
boundaries.add(row.periodEndMs);
|
|
172
|
+
}
|
|
173
|
+
if (input.liveRawFromMs !== undefined) {
|
|
174
|
+
boundaries.add(input.liveRawFromMs);
|
|
175
|
+
boundaries.add(input.nowMs);
|
|
176
|
+
}
|
|
177
|
+
const sorted = [...boundaries].sort((left, right) => left - right);
|
|
178
|
+
const raw: Array<{
|
|
179
|
+
readonly startMs: number;
|
|
180
|
+
readonly endMs: number;
|
|
181
|
+
readonly granularity: RetainedGranularity;
|
|
182
|
+
}> = [];
|
|
183
|
+
for (let index = 0; index < sorted.length - 1; index += 1) {
|
|
184
|
+
const segStart = sorted[index]!;
|
|
185
|
+
const segEnd = sorted[index + 1]!;
|
|
186
|
+
if (segEnd <= segStart) continue;
|
|
187
|
+
if (
|
|
188
|
+
input.liveRawFromMs !== undefined &&
|
|
189
|
+
segStart >= input.liveRawFromMs &&
|
|
190
|
+
segEnd <= input.nowMs
|
|
191
|
+
) {
|
|
192
|
+
raw.push({ startMs: segStart, endMs: segEnd, granularity: "raw" });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
let finestRank: number | undefined;
|
|
196
|
+
for (const row of input.digestRows) {
|
|
197
|
+
if (row.periodStartMs > segStart || row.periodEndMs < segEnd) continue;
|
|
198
|
+
const rank = PERIOD_TYPE_RANK.get(row.periodType);
|
|
199
|
+
if (rank !== undefined && (finestRank === undefined || rank < finestRank)) {
|
|
200
|
+
finestRank = rank;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (finestRank !== undefined) {
|
|
204
|
+
raw.push({
|
|
205
|
+
startMs: segStart,
|
|
206
|
+
endMs: segEnd,
|
|
207
|
+
granularity: PERIOD_TYPES[finestRank]!,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const merged: GranularitySegment[] = [];
|
|
212
|
+
for (const segment of raw) {
|
|
213
|
+
const previous = merged[merged.length - 1];
|
|
214
|
+
if (
|
|
215
|
+
previous !== undefined &&
|
|
216
|
+
previous.granularity === segment.granularity &&
|
|
217
|
+
previous.endMs === segment.startMs
|
|
218
|
+
) {
|
|
219
|
+
merged[merged.length - 1] = { ...previous, endMs: segment.endMs };
|
|
220
|
+
} else {
|
|
221
|
+
merged.push(segment);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return merged;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type RetainedHistoryCoverageReader = () => readonly GranularitySegment[];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Bind {@link deriveRetainedGranularitySegments} to the current Pi agent
|
|
231
|
+
* directory's real, immutable digest store -- the "all available history"
|
|
232
|
+
* read path. Performs no network request and writes nothing.
|
|
233
|
+
*/
|
|
234
|
+
export function createDefaultRetainedHistoryCoverageReader(options: {
|
|
235
|
+
readonly now?: () => number;
|
|
236
|
+
}): RetainedHistoryCoverageReader {
|
|
237
|
+
const digestStore = new CostDigestStore({ path: defaultCostDigestPaths().path });
|
|
238
|
+
const now = options.now ?? Date.now;
|
|
239
|
+
return () => {
|
|
240
|
+
const nowMs = now();
|
|
241
|
+
const today = getPeriodBounds(nowMs, "day");
|
|
242
|
+
return deriveRetainedGranularitySegments({
|
|
243
|
+
digestRows: digestStore.read(),
|
|
244
|
+
liveRawFromMs: today.startMs,
|
|
245
|
+
nowMs,
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Bind the read-only cost-report surfaces to the current Pi agent directory.
|
|
252
|
+
* The returned reader performs no network request and writes nothing; the
|
|
253
|
+
* machine-leased period closer is invoked separately before this reader.
|
|
254
|
+
*/
|
|
255
|
+
export function createDefaultCostReportReader(options: {
|
|
256
|
+
readonly config: () => Pick<
|
|
257
|
+
MultiAccountConfig,
|
|
258
|
+
"accountLimit" | "projectLabels" | "monthlySubscriptionUsd" | "accountRateHistory"
|
|
259
|
+
>;
|
|
260
|
+
readonly now?: () => number;
|
|
261
|
+
/**
|
|
262
|
+
* Supplies Pi's installed model catalog cost data for per-response
|
|
263
|
+
* tier-aware pricing (see `api-pricing.ts`). This adapter never fetches or
|
|
264
|
+
* derives that snapshot itself; when omitted, the read path prices
|
|
265
|
+
* responses only from the retained OpenRouter rate snapshot, exactly as it
|
|
266
|
+
* did before per-response tier pricing existed.
|
|
267
|
+
*/
|
|
268
|
+
readonly piCatalog?: () => PiCatalogSnapshot | undefined;
|
|
269
|
+
}): CostReportReader {
|
|
270
|
+
const digestStore = new CostDigestStore({ path: defaultCostDigestPaths().path });
|
|
271
|
+
const pricingCache = new OpenRouterPricingCache({
|
|
272
|
+
...defaultPricingCachePaths(),
|
|
273
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
274
|
+
});
|
|
275
|
+
const now = options.now ?? Date.now;
|
|
276
|
+
// The same "all available history" read path a caller can bind directly
|
|
277
|
+
// (see `createDefaultRetainedHistoryCoverageReader`'s own doc comment):
|
|
278
|
+
// reused here so a "custom" request's satisfiability is checked against
|
|
279
|
+
// the real retained store, never a caller-asserted or absent set of
|
|
280
|
+
// segments.
|
|
281
|
+
const coverageReader = createDefaultRetainedHistoryCoverageReader(
|
|
282
|
+
options.now === undefined ? {} : { now: options.now },
|
|
283
|
+
);
|
|
284
|
+
return (request) => {
|
|
285
|
+
const nowMs = now();
|
|
286
|
+
const pricing = pricingCache.readStatus();
|
|
287
|
+
const piCatalog = options.piCatalog?.();
|
|
288
|
+
const history = summarizeCostHistoryForReport(
|
|
289
|
+
iterateHistory("cost-delta"),
|
|
290
|
+
nowMs,
|
|
291
|
+
pricing,
|
|
292
|
+
piCatalog,
|
|
293
|
+
);
|
|
294
|
+
const config = options.config();
|
|
295
|
+
const baseInput = {
|
|
296
|
+
digestRows: digestStore.read(),
|
|
297
|
+
// Only today's still-open window has per-response raw detail left to
|
|
298
|
+
// filter by; a resolved sub-day-precise range clipped inside today
|
|
299
|
+
// (see `provisionalCurrentDayRows` in `cost-report.ts`) rebuilds its
|
|
300
|
+
// provisional row from these instead of slicing the pre-aggregated
|
|
301
|
+
// `currentDayDigestRows` below, which has no per-observation
|
|
302
|
+
// timestamps left once summed.
|
|
303
|
+
observations: history.currentDayObservations,
|
|
304
|
+
gaps: history.currentDayGaps,
|
|
305
|
+
currentDayDigestRows: history.currentDayRows,
|
|
306
|
+
...(history.earliestAttributedObservedAtMs === undefined
|
|
307
|
+
? {}
|
|
308
|
+
: {
|
|
309
|
+
earliestRawObservationAtMs:
|
|
310
|
+
history.earliestAttributedObservedAtMs,
|
|
311
|
+
}),
|
|
312
|
+
rawAccountIds: history.accountIds,
|
|
313
|
+
groupedLegacyUnattributed: history.legacyUnattributed,
|
|
314
|
+
pricing,
|
|
315
|
+
nowMs,
|
|
316
|
+
projectLabels: config.projectLabels,
|
|
317
|
+
monthlySubscriptionUsd: projectMonthlySubscriptionUsdForReport(
|
|
318
|
+
config.monthlySubscriptionUsd,
|
|
319
|
+
config.accountLimit,
|
|
320
|
+
),
|
|
321
|
+
accountRateHistory: projectAccountRateHistoryForReport(
|
|
322
|
+
config.accountRateHistory ?? {},
|
|
323
|
+
config.accountLimit,
|
|
324
|
+
),
|
|
325
|
+
} as const;
|
|
326
|
+
|
|
327
|
+
if (typeof request === "string") {
|
|
328
|
+
return buildCostReport({ ...baseInput, periodType: request });
|
|
329
|
+
}
|
|
330
|
+
if (request.kind === "period") {
|
|
331
|
+
return buildCostReport({
|
|
332
|
+
...baseInput,
|
|
333
|
+
periodType: request.periodType,
|
|
334
|
+
...(request.timeZone === undefined ? {} : { timeZone: request.timeZone }),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// `periodType` below is a nominal placeholder for both branches: neither
|
|
338
|
+
// `buildCostReport`'s `"custom"` nor `"all-history"` range mode reads it
|
|
339
|
+
// for bounds (see `BuildCostReportInput.range`); it only keeps
|
|
340
|
+
// `CostReport.periodType`'s existing required shape until a later node
|
|
341
|
+
// gives a range-mode report its own dedicated label.
|
|
342
|
+
if (request.kind === "custom") {
|
|
343
|
+
const resolved = resolveCustomRange({
|
|
344
|
+
fromRaw: request.fromRaw,
|
|
345
|
+
toRaw: request.toRaw,
|
|
346
|
+
...(request.timeZone === undefined ? {} : { timeZone: request.timeZone }),
|
|
347
|
+
retainedGranularity: coverageReader(),
|
|
348
|
+
});
|
|
349
|
+
if (resolved.status !== "resolved") {
|
|
350
|
+
throw new CostReportRangeRequestError(resolved.exitCode, resolved.reason);
|
|
351
|
+
}
|
|
352
|
+
return buildCostReport({
|
|
353
|
+
...baseInput,
|
|
354
|
+
periodType: "day",
|
|
355
|
+
range: {
|
|
356
|
+
kind: "custom",
|
|
357
|
+
startMs: resolved.startMs,
|
|
358
|
+
endMs: resolved.endMs,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
return buildCostReport({
|
|
363
|
+
...baseInput,
|
|
364
|
+
periodType: "day",
|
|
365
|
+
range: { kind: "all-history" },
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
}
|