@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,1044 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
estimateApiEquivalentCost,
|
|
4
|
+
estimatePiCatalogCost,
|
|
5
|
+
lookupApiRate,
|
|
6
|
+
lookupPiCatalogCost,
|
|
7
|
+
OPENROUTER_METHOD_LIMITS,
|
|
8
|
+
PI_CATALOG_METHOD_LIMITS,
|
|
9
|
+
type ApiEquivalentMethod,
|
|
10
|
+
type PiCatalogSnapshot,
|
|
11
|
+
} from "./api-pricing.js";
|
|
12
|
+
import type { HistoryLogRecord } from "./history-store.js";
|
|
13
|
+
import {
|
|
14
|
+
getPeriodBounds,
|
|
15
|
+
type PeriodBounds,
|
|
16
|
+
type PeriodType,
|
|
17
|
+
} from "./period-boundaries.js";
|
|
18
|
+
import { isProjectKey } from "./project-identity.js";
|
|
19
|
+
import type { PricingCacheResult } from "./pricing-cache.js";
|
|
20
|
+
export type { ApiEquivalentMethod, PiCatalogSnapshot } from "./api-pricing.js";
|
|
21
|
+
|
|
22
|
+
const DAY_MS = 86_400_000;
|
|
23
|
+
|
|
24
|
+
export type CostCoverage = "complete" | "partial" | "unknown";
|
|
25
|
+
|
|
26
|
+
export interface CostObservation {
|
|
27
|
+
readonly observedAtMs: number;
|
|
28
|
+
readonly projectKey: string | undefined;
|
|
29
|
+
readonly canonicalAccountId: string;
|
|
30
|
+
readonly requestedModel: string;
|
|
31
|
+
readonly status: CostCoverage;
|
|
32
|
+
readonly tokens: {
|
|
33
|
+
readonly input: number;
|
|
34
|
+
readonly output: number;
|
|
35
|
+
readonly cacheRead: number;
|
|
36
|
+
readonly cacheWrite: number;
|
|
37
|
+
readonly cacheWrite1h: number;
|
|
38
|
+
};
|
|
39
|
+
readonly retainedCostUsd: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CostHistoryGap {
|
|
43
|
+
readonly startMs: number;
|
|
44
|
+
readonly endMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CostDigestRow {
|
|
48
|
+
readonly schemaVersion: 1;
|
|
49
|
+
readonly kind: "cost-period-digest";
|
|
50
|
+
readonly id: string;
|
|
51
|
+
readonly projectKey: string;
|
|
52
|
+
readonly canonicalAccountId: string;
|
|
53
|
+
readonly requestedModel: string;
|
|
54
|
+
readonly periodType: PeriodType;
|
|
55
|
+
readonly periodStartMs: number;
|
|
56
|
+
readonly periodEndMs: number;
|
|
57
|
+
readonly closedAtMs: number;
|
|
58
|
+
readonly source: "raw" | "day-rollup" | "month-rollup";
|
|
59
|
+
readonly childRowCount: number;
|
|
60
|
+
readonly observationCount: number;
|
|
61
|
+
readonly coverage: CostCoverage;
|
|
62
|
+
readonly tokens: CostObservation["tokens"];
|
|
63
|
+
readonly retainedCostUsd: number;
|
|
64
|
+
readonly apiEquivalent:
|
|
65
|
+
| {
|
|
66
|
+
readonly status: "priced";
|
|
67
|
+
readonly estimatedUsd: number;
|
|
68
|
+
readonly rateAsOfMs: readonly number[];
|
|
69
|
+
readonly sourceModelIds: readonly string[];
|
|
70
|
+
readonly methods?: readonly ApiEquivalentMethod[];
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
readonly status: "unpriced";
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
readonly rateAsOfMs: readonly number[];
|
|
76
|
+
readonly methods?: readonly ApiEquivalentMethod[];
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function validTimestamp(value: unknown): value is number {
|
|
85
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function tokenCount(value: unknown): value is number {
|
|
89
|
+
return (
|
|
90
|
+
typeof value === "number" &&
|
|
91
|
+
Number.isSafeInteger(value) &&
|
|
92
|
+
value >= 0
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function boundedString(value: unknown, maxLength: number): value is string {
|
|
97
|
+
return (
|
|
98
|
+
typeof value === "string" &&
|
|
99
|
+
value.length > 0 &&
|
|
100
|
+
value.length <= maxLength &&
|
|
101
|
+
!value.includes("\0")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseObservation(record: HistoryLogRecord): CostObservation | undefined {
|
|
106
|
+
if (record.recordType !== "cost-delta" || !isRecord(record.payload)) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const payload = record.payload;
|
|
110
|
+
if (
|
|
111
|
+
!validTimestamp(record.observedAtMs) ||
|
|
112
|
+
!boundedString(payload.canonicalAccountId, 128) ||
|
|
113
|
+
!boundedString(payload.requestedModel, 200) ||
|
|
114
|
+
!isRecord(payload.tokens) ||
|
|
115
|
+
!isRecord(payload.cost) ||
|
|
116
|
+
!tokenCount(payload.tokens.input) ||
|
|
117
|
+
!tokenCount(payload.tokens.output) ||
|
|
118
|
+
!tokenCount(payload.tokens.cacheRead) ||
|
|
119
|
+
!tokenCount(payload.tokens.cacheWrite) ||
|
|
120
|
+
(payload.tokens.cacheWrite1h !== undefined &&
|
|
121
|
+
!tokenCount(payload.tokens.cacheWrite1h)) ||
|
|
122
|
+
!(["complete", "partial", "unknown", "unpriced"] as const).includes(
|
|
123
|
+
payload.status as CostCoverage | "unpriced",
|
|
124
|
+
) ||
|
|
125
|
+
(payload.status !== "unpriced" &&
|
|
126
|
+
(typeof payload.cost.total !== "number" ||
|
|
127
|
+
!Number.isFinite(payload.cost.total) ||
|
|
128
|
+
payload.cost.total < 0))
|
|
129
|
+
) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const cacheWrite1h = (payload.tokens.cacheWrite1h as number | undefined) ?? 0;
|
|
133
|
+
if (cacheWrite1h > payload.tokens.cacheWrite) return undefined;
|
|
134
|
+
return {
|
|
135
|
+
observedAtMs: record.observedAtMs,
|
|
136
|
+
projectKey:
|
|
137
|
+
typeof payload.projectKey === "string" &&
|
|
138
|
+
isProjectKey(payload.projectKey)
|
|
139
|
+
? payload.projectKey
|
|
140
|
+
: undefined,
|
|
141
|
+
canonicalAccountId: payload.canonicalAccountId,
|
|
142
|
+
requestedModel: payload.requestedModel,
|
|
143
|
+
status:
|
|
144
|
+
payload.status === "unpriced"
|
|
145
|
+
? "unknown"
|
|
146
|
+
: (payload.status as CostCoverage),
|
|
147
|
+
tokens: {
|
|
148
|
+
input: payload.tokens.input,
|
|
149
|
+
output: payload.tokens.output,
|
|
150
|
+
cacheRead: payload.tokens.cacheRead,
|
|
151
|
+
cacheWrite: payload.tokens.cacheWrite,
|
|
152
|
+
cacheWrite1h,
|
|
153
|
+
},
|
|
154
|
+
retainedCostUsd:
|
|
155
|
+
payload.status === "unpriced" ? 0 : (payload.cost.total as number),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function extractCostHistory(records: Iterable<HistoryLogRecord>): {
|
|
160
|
+
readonly observations: readonly CostObservation[];
|
|
161
|
+
readonly gaps: readonly CostHistoryGap[];
|
|
162
|
+
} {
|
|
163
|
+
const observations: CostObservation[] = [];
|
|
164
|
+
const gaps: CostHistoryGap[] = [];
|
|
165
|
+
for (const record of records) {
|
|
166
|
+
if (
|
|
167
|
+
record.recordType === "gap" &&
|
|
168
|
+
validTimestamp(record.gapStartMs) &&
|
|
169
|
+
validTimestamp(record.gapEndMs) &&
|
|
170
|
+
record.gapEndMs > record.gapStartMs
|
|
171
|
+
) {
|
|
172
|
+
gaps.push({ startMs: record.gapStartMs, endMs: record.gapEndMs });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const observation = parseObservation(record);
|
|
176
|
+
if (observation !== undefined) observations.push(observation);
|
|
177
|
+
}
|
|
178
|
+
return { observations, gaps };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addReportCount(left: number, right: number): number {
|
|
182
|
+
const total = left + right;
|
|
183
|
+
if (!Number.isSafeInteger(total) || total < 0) {
|
|
184
|
+
throw new RangeError("Cost report token total exceeds the safe integer range.");
|
|
185
|
+
}
|
|
186
|
+
return total;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function addReportTokens(
|
|
190
|
+
left: CostObservation["tokens"],
|
|
191
|
+
right: CostObservation["tokens"],
|
|
192
|
+
): CostObservation["tokens"] {
|
|
193
|
+
return {
|
|
194
|
+
input: addReportCount(left.input, right.input),
|
|
195
|
+
output: addReportCount(left.output, right.output),
|
|
196
|
+
cacheRead: addReportCount(left.cacheRead, right.cacheRead),
|
|
197
|
+
cacheWrite: addReportCount(left.cacheWrite, right.cacheWrite),
|
|
198
|
+
cacheWrite1h: addReportCount(left.cacheWrite1h, right.cacheWrite1h),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function addReportUsd(left: number, right: number): number {
|
|
203
|
+
const total = left + right;
|
|
204
|
+
if (!Number.isFinite(total) || total < 0) {
|
|
205
|
+
throw new RangeError(
|
|
206
|
+
"Cost report USD total must remain finite and non-negative.",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
return total;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface CostHistoryReportSummary {
|
|
213
|
+
readonly currentDayRows: readonly CostDigestRow[];
|
|
214
|
+
/**
|
|
215
|
+
* Every attributed (project-keyed) raw observation for the still-open
|
|
216
|
+
* current day, in arrival order. Kept alongside the pre-aggregated
|
|
217
|
+
* {@link currentDayRows} so a caller needing a sub-day-precise window
|
|
218
|
+
* inside today (a custom range ending or starting before `nowMs`) can
|
|
219
|
+
* rebuild a correctly clipped provisional row instead of slicing the
|
|
220
|
+
* already-summed aggregate, which has no per-response timestamps left to
|
|
221
|
+
* slice by.
|
|
222
|
+
*/
|
|
223
|
+
readonly currentDayObservations: readonly CostObservation[];
|
|
224
|
+
/** The same current-day gap set folded into {@link currentDayRows}'s coverage, exposed so a caller rebuilding a clipped provisional row from {@link currentDayObservations} preserves the same gap-aware coverage. */
|
|
225
|
+
readonly currentDayGaps: readonly CostHistoryGap[];
|
|
226
|
+
readonly earliestAttributedObservedAtMs: number | undefined;
|
|
227
|
+
readonly accountIds: readonly string[];
|
|
228
|
+
readonly legacyUnattributed: readonly {
|
|
229
|
+
readonly canonicalAccountId: string;
|
|
230
|
+
readonly requestedModel: string;
|
|
231
|
+
readonly observationCount: number;
|
|
232
|
+
readonly tokens: CostObservation["tokens"];
|
|
233
|
+
readonly retainedCostUsd: number;
|
|
234
|
+
}[];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Retain only current-day detail and grouped legacy totals needed by cost rendering. */
|
|
238
|
+
export function summarizeCostHistoryForReport(
|
|
239
|
+
records: Iterable<HistoryLogRecord>,
|
|
240
|
+
nowMs: number,
|
|
241
|
+
pricing: PricingCacheResult,
|
|
242
|
+
piCatalog?: PiCatalogSnapshot,
|
|
243
|
+
): CostHistoryReportSummary {
|
|
244
|
+
if (!validTimestamp(nowMs)) {
|
|
245
|
+
throw new RangeError("Cost report clock must be a finite timestamp.");
|
|
246
|
+
}
|
|
247
|
+
const day = getPeriodBounds(nowMs, "day");
|
|
248
|
+
const currentDayGroups = new Map<string, RawCostGroup>();
|
|
249
|
+
const accountIds = new Set<string>();
|
|
250
|
+
const legacy = new Map<
|
|
251
|
+
string,
|
|
252
|
+
CostHistoryReportSummary["legacyUnattributed"][number]
|
|
253
|
+
>();
|
|
254
|
+
let earliestAttributedObservedAtMs: number | undefined;
|
|
255
|
+
let currentDayGap = false;
|
|
256
|
+
const currentDayObservations: CostObservation[] = [];
|
|
257
|
+
for (const record of records) {
|
|
258
|
+
if (
|
|
259
|
+
record.recordType === "gap" &&
|
|
260
|
+
validTimestamp(record.gapStartMs) &&
|
|
261
|
+
validTimestamp(record.gapEndMs) &&
|
|
262
|
+
record.gapEndMs > record.gapStartMs
|
|
263
|
+
) {
|
|
264
|
+
currentDayGap ||=
|
|
265
|
+
record.gapStartMs < day.endMs && record.gapEndMs > day.startMs;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const observation = parseObservation(record);
|
|
269
|
+
if (observation === undefined) continue;
|
|
270
|
+
accountIds.add(observation.canonicalAccountId);
|
|
271
|
+
if (observation.projectKey === undefined) {
|
|
272
|
+
const key = JSON.stringify([
|
|
273
|
+
observation.canonicalAccountId,
|
|
274
|
+
observation.requestedModel,
|
|
275
|
+
]);
|
|
276
|
+
const previous = legacy.get(key);
|
|
277
|
+
legacy.set(
|
|
278
|
+
key,
|
|
279
|
+
previous === undefined
|
|
280
|
+
? {
|
|
281
|
+
canonicalAccountId: observation.canonicalAccountId,
|
|
282
|
+
requestedModel: observation.requestedModel,
|
|
283
|
+
observationCount: 1,
|
|
284
|
+
tokens: observation.tokens,
|
|
285
|
+
retainedCostUsd: observation.retainedCostUsd,
|
|
286
|
+
}
|
|
287
|
+
: {
|
|
288
|
+
...previous,
|
|
289
|
+
observationCount: addReportCount(previous.observationCount, 1),
|
|
290
|
+
tokens: addReportTokens(previous.tokens, observation.tokens),
|
|
291
|
+
retainedCostUsd: addReportUsd(
|
|
292
|
+
previous.retainedCostUsd,
|
|
293
|
+
observation.retainedCostUsd,
|
|
294
|
+
),
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
earliestAttributedObservedAtMs =
|
|
300
|
+
earliestAttributedObservedAtMs === undefined
|
|
301
|
+
? observation.observedAtMs
|
|
302
|
+
: Math.min(earliestAttributedObservedAtMs, observation.observedAtMs);
|
|
303
|
+
if (
|
|
304
|
+
observation.observedAtMs >= day.startMs &&
|
|
305
|
+
observation.observedAtMs < nowMs
|
|
306
|
+
) {
|
|
307
|
+
addRawObservation(currentDayGroups, observation, day.endMs);
|
|
308
|
+
currentDayObservations.push(observation);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const currentDayGaps: readonly CostHistoryGap[] = currentDayGap
|
|
312
|
+
? [{ startMs: day.startMs, endMs: day.endMs }]
|
|
313
|
+
: [];
|
|
314
|
+
const currentDayRows = planClosedCostDigestGroups({
|
|
315
|
+
groups: currentDayGroups,
|
|
316
|
+
gaps: currentDayGaps,
|
|
317
|
+
existingRows: [],
|
|
318
|
+
pricing,
|
|
319
|
+
...(piCatalog === undefined ? {} : { piCatalog }),
|
|
320
|
+
nowMs: day.endMs,
|
|
321
|
+
closedAtMs: nowMs,
|
|
322
|
+
}).filter(
|
|
323
|
+
(row) => row.periodType === "day" && row.periodStartMs === day.startMs,
|
|
324
|
+
);
|
|
325
|
+
return {
|
|
326
|
+
currentDayRows,
|
|
327
|
+
currentDayObservations,
|
|
328
|
+
currentDayGaps,
|
|
329
|
+
earliestAttributedObservedAtMs,
|
|
330
|
+
accountIds: [...accountIds],
|
|
331
|
+
legacyUnattributed: [...legacy.values()].sort(
|
|
332
|
+
(left, right) =>
|
|
333
|
+
left.canonicalAccountId.localeCompare(right.canonicalAccountId) ||
|
|
334
|
+
left.requestedModel.localeCompare(right.requestedModel),
|
|
335
|
+
),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function identityKey(
|
|
340
|
+
projectKey: string,
|
|
341
|
+
canonicalAccountId: string,
|
|
342
|
+
requestedModel: string,
|
|
343
|
+
periodType: PeriodType,
|
|
344
|
+
periodStartMs: number,
|
|
345
|
+
): string {
|
|
346
|
+
return JSON.stringify([
|
|
347
|
+
projectKey,
|
|
348
|
+
canonicalAccountId,
|
|
349
|
+
requestedModel,
|
|
350
|
+
periodType,
|
|
351
|
+
periodStartMs,
|
|
352
|
+
]);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function digestId(identity: string): string {
|
|
356
|
+
return `cost-digest-${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function costDigestId(
|
|
360
|
+
projectKey: string,
|
|
361
|
+
canonicalAccountId: string,
|
|
362
|
+
requestedModel: string,
|
|
363
|
+
periodType: PeriodType,
|
|
364
|
+
periodStartMs: number,
|
|
365
|
+
): string {
|
|
366
|
+
return digestId(
|
|
367
|
+
identityKey(
|
|
368
|
+
projectKey,
|
|
369
|
+
canonicalAccountId,
|
|
370
|
+
requestedModel,
|
|
371
|
+
periodType,
|
|
372
|
+
periodStartMs,
|
|
373
|
+
),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function emptyTokens(): CostObservation["tokens"] {
|
|
378
|
+
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cacheWrite1h: 0 };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function addTokens(
|
|
382
|
+
target: CostObservation["tokens"],
|
|
383
|
+
value: CostObservation["tokens"],
|
|
384
|
+
): CostObservation["tokens"] {
|
|
385
|
+
return addReportTokens(target, value);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function priceRaw(
|
|
389
|
+
tokens: CostObservation["tokens"],
|
|
390
|
+
canonicalAccountId: string,
|
|
391
|
+
requestedModel: string,
|
|
392
|
+
pricing: PricingCacheResult,
|
|
393
|
+
): CostDigestRow["apiEquivalent"] {
|
|
394
|
+
if (pricing.state !== "fresh") {
|
|
395
|
+
return {
|
|
396
|
+
status: "unpriced",
|
|
397
|
+
reason:
|
|
398
|
+
pricing.state === "stale" ? "pricing-stale" : "pricing-unavailable",
|
|
399
|
+
rateAsOfMs:
|
|
400
|
+
pricing.state === "stale" ? [pricing.snapshot.fetchedAtMs] : [],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const rate = lookupApiRate(
|
|
404
|
+
pricing.snapshot,
|
|
405
|
+
canonicalAccountId,
|
|
406
|
+
requestedModel,
|
|
407
|
+
);
|
|
408
|
+
if (rate === undefined) {
|
|
409
|
+
return {
|
|
410
|
+
status: "unpriced",
|
|
411
|
+
reason: "model-rate-missing",
|
|
412
|
+
rateAsOfMs: [pricing.snapshot.fetchedAtMs],
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const estimate = estimateApiEquivalentCost(
|
|
416
|
+
tokens,
|
|
417
|
+
rate,
|
|
418
|
+
pricing.snapshot.fetchedAtMs,
|
|
419
|
+
);
|
|
420
|
+
return estimate.status === "priced"
|
|
421
|
+
? {
|
|
422
|
+
status: "priced",
|
|
423
|
+
estimatedUsd: estimate.totalUsd,
|
|
424
|
+
rateAsOfMs: [estimate.rateAsOfMs],
|
|
425
|
+
sourceModelIds: [estimate.sourceModelId],
|
|
426
|
+
}
|
|
427
|
+
: {
|
|
428
|
+
status: "unpriced",
|
|
429
|
+
reason: estimate.reason,
|
|
430
|
+
rateAsOfMs: [pricing.snapshot.fetchedAtMs],
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
type PricedObservationResult =
|
|
435
|
+
| {
|
|
436
|
+
readonly status: "priced";
|
|
437
|
+
readonly totalUsd: number;
|
|
438
|
+
readonly rateAsOfMs: number;
|
|
439
|
+
readonly sourceModelId: string;
|
|
440
|
+
readonly method: ApiEquivalentMethod;
|
|
441
|
+
}
|
|
442
|
+
| { readonly status: "unpriced"; readonly reason: string };
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Price one response under its own matched tier, before any grouping.
|
|
446
|
+
* Tries Pi's installed catalog first (the authoritative tier metadata
|
|
447
|
+
* source); OpenRouter extends coverage only for models the catalog lacks.
|
|
448
|
+
*/
|
|
449
|
+
function priceObservationApiEquivalent(
|
|
450
|
+
tokens: CostObservation["tokens"],
|
|
451
|
+
canonicalAccountId: string,
|
|
452
|
+
requestedModel: string,
|
|
453
|
+
piCatalog: PiCatalogSnapshot,
|
|
454
|
+
pricing: PricingCacheResult,
|
|
455
|
+
): PricedObservationResult {
|
|
456
|
+
const catalogCost = lookupPiCatalogCost(
|
|
457
|
+
piCatalog,
|
|
458
|
+
canonicalAccountId,
|
|
459
|
+
requestedModel,
|
|
460
|
+
);
|
|
461
|
+
if (catalogCost !== undefined) {
|
|
462
|
+
const estimate = estimatePiCatalogCost(
|
|
463
|
+
tokens,
|
|
464
|
+
catalogCost,
|
|
465
|
+
piCatalog.capturedAtMs,
|
|
466
|
+
);
|
|
467
|
+
if (estimate.status !== "priced") {
|
|
468
|
+
return { status: "unpriced", reason: estimate.reason };
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
status: "priced",
|
|
472
|
+
totalUsd: estimate.totalUsd,
|
|
473
|
+
rateAsOfMs: estimate.rateAsOfMs,
|
|
474
|
+
sourceModelId: estimate.sourceModelId,
|
|
475
|
+
method: {
|
|
476
|
+
source: "pi-installed-catalog",
|
|
477
|
+
catalogVersion: piCatalog.catalogVersion,
|
|
478
|
+
tierInputTokensAbove: estimate.tierInputTokensAbove,
|
|
479
|
+
limits: PI_CATALOG_METHOD_LIMITS,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
// Unsupported by the installed catalog: fall back to the cached OpenRouter
|
|
484
|
+
// snapshot, which only extends coverage and never overrides the catalog.
|
|
485
|
+
if (pricing.state !== "fresh") {
|
|
486
|
+
return {
|
|
487
|
+
status: "unpriced",
|
|
488
|
+
reason:
|
|
489
|
+
pricing.state === "stale" ? "pricing-stale" : "pricing-unavailable",
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const rate = lookupApiRate(pricing.snapshot, canonicalAccountId, requestedModel);
|
|
493
|
+
if (rate === undefined) {
|
|
494
|
+
return { status: "unpriced", reason: "model-rate-missing" };
|
|
495
|
+
}
|
|
496
|
+
const estimate = estimateApiEquivalentCost(tokens, rate, pricing.snapshot.fetchedAtMs);
|
|
497
|
+
if (estimate.status !== "priced") {
|
|
498
|
+
return { status: "unpriced", reason: estimate.reason };
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
status: "priced",
|
|
502
|
+
totalUsd: estimate.totalUsd,
|
|
503
|
+
rateAsOfMs: estimate.rateAsOfMs,
|
|
504
|
+
sourceModelId: estimate.sourceModelId,
|
|
505
|
+
method: {
|
|
506
|
+
source: "openrouter",
|
|
507
|
+
catalogVersion: `openrouter-snapshot-${pricing.snapshot.fetchedAtMs}`,
|
|
508
|
+
tierInputTokensAbove: undefined,
|
|
509
|
+
limits: OPENROUTER_METHOD_LIMITS,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function methodKey(method: ApiEquivalentMethod): string {
|
|
515
|
+
return JSON.stringify([
|
|
516
|
+
method.source,
|
|
517
|
+
method.catalogVersion,
|
|
518
|
+
method.tierInputTokensAbove ?? null,
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function sortedMethods(
|
|
523
|
+
methods: ReadonlyMap<string, ApiEquivalentMethod>,
|
|
524
|
+
): readonly ApiEquivalentMethod[] {
|
|
525
|
+
return [...methods.values()].sort(
|
|
526
|
+
(left, right) =>
|
|
527
|
+
left.source.localeCompare(right.source) ||
|
|
528
|
+
left.catalogVersion.localeCompare(right.catalogVersion) ||
|
|
529
|
+
(left.tierInputTokensAbove ?? -1) - (right.tierInputTokensAbove ?? -1),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Price every response in a raw day group under its own matched tier before
|
|
535
|
+
* summing into the group's total: REQ-TIER-PER-RESPONSE. A day mixing
|
|
536
|
+
* responses below and above a tier threshold must never be priced as if the
|
|
537
|
+
* whole day's tokens were one request.
|
|
538
|
+
*/
|
|
539
|
+
function priceRawPerResponse(
|
|
540
|
+
group: RawCostGroup,
|
|
541
|
+
piCatalog: PiCatalogSnapshot,
|
|
542
|
+
pricing: PricingCacheResult,
|
|
543
|
+
): CostDigestRow["apiEquivalent"] {
|
|
544
|
+
let estimatedUsd = 0;
|
|
545
|
+
let unpriced = false;
|
|
546
|
+
let reason: string | undefined;
|
|
547
|
+
const rateAsOfMs = new Set<number>();
|
|
548
|
+
const sourceModelIds = new Set<string>();
|
|
549
|
+
const methods = new Map<string, ApiEquivalentMethod>();
|
|
550
|
+
for (const tokens of group.observationTokens) {
|
|
551
|
+
const priced = priceObservationApiEquivalent(
|
|
552
|
+
tokens,
|
|
553
|
+
group.canonicalAccountId,
|
|
554
|
+
group.requestedModel,
|
|
555
|
+
piCatalog,
|
|
556
|
+
pricing,
|
|
557
|
+
);
|
|
558
|
+
if (priced.status === "unpriced") {
|
|
559
|
+
unpriced = true;
|
|
560
|
+
reason ??= priced.reason;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
estimatedUsd = addReportUsd(estimatedUsd, priced.totalUsd);
|
|
564
|
+
rateAsOfMs.add(priced.rateAsOfMs);
|
|
565
|
+
sourceModelIds.add(priced.sourceModelId);
|
|
566
|
+
methods.set(methodKey(priced.method), priced.method);
|
|
567
|
+
}
|
|
568
|
+
if (unpriced) {
|
|
569
|
+
return {
|
|
570
|
+
status: "unpriced",
|
|
571
|
+
reason: reason ?? "response-unpriced",
|
|
572
|
+
rateAsOfMs: sortedUniqueNumbers([...rateAsOfMs]),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
status: "priced",
|
|
577
|
+
estimatedUsd,
|
|
578
|
+
rateAsOfMs: sortedUniqueNumbers([...rateAsOfMs]),
|
|
579
|
+
sourceModelIds: sortedUniqueStrings([...sourceModelIds]),
|
|
580
|
+
methods: sortedMethods(methods),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function sortedUniqueNumbers(values: readonly number[]): readonly number[] {
|
|
585
|
+
return [...new Set(values)].sort((left, right) => left - right);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function sortedUniqueStrings(values: readonly string[]): readonly string[] {
|
|
589
|
+
return [...new Set(values)].sort();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function rollupPrice(
|
|
593
|
+
children: readonly CostDigestRow[],
|
|
594
|
+
): CostDigestRow["apiEquivalent"] {
|
|
595
|
+
const rateAsOfMs = sortedUniqueNumbers(
|
|
596
|
+
children.flatMap((child) => child.apiEquivalent.rateAsOfMs),
|
|
597
|
+
);
|
|
598
|
+
if (children.some((child) => child.apiEquivalent.status === "unpriced")) {
|
|
599
|
+
return { status: "unpriced", reason: "child-unpriced", rateAsOfMs };
|
|
600
|
+
}
|
|
601
|
+
const priced = children.filter(
|
|
602
|
+
(child): child is CostDigestRow & {
|
|
603
|
+
readonly apiEquivalent: Extract<
|
|
604
|
+
CostDigestRow["apiEquivalent"],
|
|
605
|
+
{ readonly status: "priced" }
|
|
606
|
+
>;
|
|
607
|
+
} => child.apiEquivalent.status === "priced",
|
|
608
|
+
);
|
|
609
|
+
return {
|
|
610
|
+
status: "priced",
|
|
611
|
+
estimatedUsd: priced.reduce(
|
|
612
|
+
(total, child) => addReportUsd(total, child.apiEquivalent.estimatedUsd),
|
|
613
|
+
0,
|
|
614
|
+
),
|
|
615
|
+
rateAsOfMs,
|
|
616
|
+
sourceModelIds: sortedUniqueStrings(
|
|
617
|
+
priced.flatMap((child) => child.apiEquivalent.sourceModelIds),
|
|
618
|
+
),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function rollupCoverage(
|
|
623
|
+
children: readonly CostDigestRow[],
|
|
624
|
+
expectedChildren: number,
|
|
625
|
+
): CostCoverage {
|
|
626
|
+
if (
|
|
627
|
+
children.length !== expectedChildren ||
|
|
628
|
+
children.some((child) => child.coverage === "partial")
|
|
629
|
+
) {
|
|
630
|
+
return "partial";
|
|
631
|
+
}
|
|
632
|
+
return children.some((child) => child.coverage === "unknown")
|
|
633
|
+
? "unknown"
|
|
634
|
+
: "complete";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function expectedChildCount(
|
|
638
|
+
periodType: Exclude<PeriodType, "day">,
|
|
639
|
+
bounds: PeriodBounds,
|
|
640
|
+
): number {
|
|
641
|
+
switch (periodType) {
|
|
642
|
+
case "week":
|
|
643
|
+
return 7;
|
|
644
|
+
case "month":
|
|
645
|
+
return Math.round((bounds.endMs - bounds.startMs) / 86_400_000);
|
|
646
|
+
case "quarter":
|
|
647
|
+
return 3;
|
|
648
|
+
case "half-year":
|
|
649
|
+
return 6;
|
|
650
|
+
case "year":
|
|
651
|
+
return 12;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function parentRows(
|
|
656
|
+
children: readonly CostDigestRow[],
|
|
657
|
+
periodType: Exclude<PeriodType, "day">,
|
|
658
|
+
source: "day-rollup" | "month-rollup",
|
|
659
|
+
nowMs: number,
|
|
660
|
+
closedAtMs: number,
|
|
661
|
+
existingIds: Set<string>,
|
|
662
|
+
): CostDigestRow[] {
|
|
663
|
+
const groups = new Map<
|
|
664
|
+
string,
|
|
665
|
+
{ readonly bounds: PeriodBounds; readonly children: CostDigestRow[] }
|
|
666
|
+
>();
|
|
667
|
+
for (const child of children) {
|
|
668
|
+
const bounds = getPeriodBounds(child.periodStartMs, periodType);
|
|
669
|
+
if (bounds.endMs > nowMs) continue;
|
|
670
|
+
const key = identityKey(
|
|
671
|
+
child.projectKey,
|
|
672
|
+
child.canonicalAccountId,
|
|
673
|
+
child.requestedModel,
|
|
674
|
+
periodType,
|
|
675
|
+
bounds.startMs,
|
|
676
|
+
);
|
|
677
|
+
const current = groups.get(key) ?? { bounds, children: [] };
|
|
678
|
+
current.children.push(child);
|
|
679
|
+
groups.set(key, current);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const rows: CostDigestRow[] = [];
|
|
683
|
+
for (const group of groups.values()) {
|
|
684
|
+
const first = group.children[0];
|
|
685
|
+
if (first === undefined) continue;
|
|
686
|
+
const id = costDigestId(
|
|
687
|
+
first.projectKey,
|
|
688
|
+
first.canonicalAccountId,
|
|
689
|
+
first.requestedModel,
|
|
690
|
+
periodType,
|
|
691
|
+
group.bounds.startMs,
|
|
692
|
+
);
|
|
693
|
+
if (existingIds.has(id)) continue;
|
|
694
|
+
const tokens = group.children.reduce(
|
|
695
|
+
(total, child) => addTokens(total, child.tokens),
|
|
696
|
+
emptyTokens(),
|
|
697
|
+
);
|
|
698
|
+
rows.push({
|
|
699
|
+
schemaVersion: 1,
|
|
700
|
+
kind: "cost-period-digest",
|
|
701
|
+
id,
|
|
702
|
+
projectKey: first.projectKey,
|
|
703
|
+
canonicalAccountId: first.canonicalAccountId,
|
|
704
|
+
requestedModel: first.requestedModel,
|
|
705
|
+
periodType,
|
|
706
|
+
periodStartMs: group.bounds.startMs,
|
|
707
|
+
periodEndMs: group.bounds.endMs,
|
|
708
|
+
closedAtMs,
|
|
709
|
+
source,
|
|
710
|
+
childRowCount: group.children.length,
|
|
711
|
+
observationCount: group.children.reduce(
|
|
712
|
+
(total, child) => addReportCount(total, child.observationCount),
|
|
713
|
+
0,
|
|
714
|
+
),
|
|
715
|
+
coverage: rollupCoverage(
|
|
716
|
+
group.children,
|
|
717
|
+
expectedChildCount(periodType, group.bounds),
|
|
718
|
+
),
|
|
719
|
+
tokens,
|
|
720
|
+
retainedCostUsd: group.children.reduce(
|
|
721
|
+
(total, child) => addReportUsd(total, child.retainedCostUsd),
|
|
722
|
+
0,
|
|
723
|
+
),
|
|
724
|
+
apiEquivalent: rollupPrice(group.children),
|
|
725
|
+
});
|
|
726
|
+
existingIds.add(id);
|
|
727
|
+
}
|
|
728
|
+
return rows;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
interface RawCostGroup {
|
|
732
|
+
readonly bounds: PeriodBounds;
|
|
733
|
+
readonly projectKey: string;
|
|
734
|
+
readonly canonicalAccountId: string;
|
|
735
|
+
readonly requestedModel: string;
|
|
736
|
+
tokens: CostObservation["tokens"];
|
|
737
|
+
retainedCostUsd: number;
|
|
738
|
+
observationCount: number;
|
|
739
|
+
coverage: CostCoverage;
|
|
740
|
+
/** Per-response token tuples in arrival order, priced individually before grouping. */
|
|
741
|
+
readonly observationTokens: CostObservation["tokens"][];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function addRawObservation(
|
|
745
|
+
groups: Map<string, RawCostGroup>,
|
|
746
|
+
observation: CostObservation,
|
|
747
|
+
nowMs: number,
|
|
748
|
+
): void {
|
|
749
|
+
if (observation.projectKey === undefined) return;
|
|
750
|
+
const bounds = getPeriodBounds(observation.observedAtMs, "day");
|
|
751
|
+
if (bounds.endMs > nowMs) return;
|
|
752
|
+
const key = identityKey(
|
|
753
|
+
observation.projectKey,
|
|
754
|
+
observation.canonicalAccountId,
|
|
755
|
+
observation.requestedModel,
|
|
756
|
+
"day",
|
|
757
|
+
bounds.startMs,
|
|
758
|
+
);
|
|
759
|
+
const current = groups.get(key);
|
|
760
|
+
if (current === undefined) {
|
|
761
|
+
groups.set(key, {
|
|
762
|
+
bounds,
|
|
763
|
+
projectKey: observation.projectKey,
|
|
764
|
+
canonicalAccountId: observation.canonicalAccountId,
|
|
765
|
+
requestedModel: observation.requestedModel,
|
|
766
|
+
tokens: observation.tokens,
|
|
767
|
+
retainedCostUsd: observation.retainedCostUsd,
|
|
768
|
+
observationCount: 1,
|
|
769
|
+
coverage: observation.status,
|
|
770
|
+
observationTokens: [observation.tokens],
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
current.tokens = addReportTokens(current.tokens, observation.tokens);
|
|
775
|
+
current.retainedCostUsd = addReportUsd(
|
|
776
|
+
current.retainedCostUsd,
|
|
777
|
+
observation.retainedCostUsd,
|
|
778
|
+
);
|
|
779
|
+
current.observationCount = addReportCount(current.observationCount, 1);
|
|
780
|
+
current.observationTokens.push(observation.tokens);
|
|
781
|
+
if (current.coverage !== "partial") {
|
|
782
|
+
current.coverage =
|
|
783
|
+
observation.status === "partial"
|
|
784
|
+
? "partial"
|
|
785
|
+
: current.coverage === "unknown" || observation.status === "unknown"
|
|
786
|
+
? "unknown"
|
|
787
|
+
: "complete";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
interface PlanCostDigestGroupsInput {
|
|
792
|
+
readonly groups: ReadonlyMap<string, RawCostGroup>;
|
|
793
|
+
readonly gaps: readonly CostHistoryGap[];
|
|
794
|
+
readonly existingRows: readonly CostDigestRow[];
|
|
795
|
+
readonly pricing: PricingCacheResult;
|
|
796
|
+
readonly piCatalog?: PiCatalogSnapshot;
|
|
797
|
+
readonly nowMs: number;
|
|
798
|
+
readonly closedAtMs: number;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function planClosedCostDigestGroups(
|
|
802
|
+
input: PlanCostDigestGroupsInput,
|
|
803
|
+
): readonly CostDigestRow[] {
|
|
804
|
+
const existingIds = new Set(input.existingRows.map((row) => row.id));
|
|
805
|
+
const newRows: CostDigestRow[] = [];
|
|
806
|
+
for (const group of input.groups.values()) {
|
|
807
|
+
const id = costDigestId(
|
|
808
|
+
group.projectKey,
|
|
809
|
+
group.canonicalAccountId,
|
|
810
|
+
group.requestedModel,
|
|
811
|
+
"day",
|
|
812
|
+
group.bounds.startMs,
|
|
813
|
+
);
|
|
814
|
+
if (existingIds.has(id)) continue;
|
|
815
|
+
const gapOverlaps = input.gaps.some(
|
|
816
|
+
(gap) =>
|
|
817
|
+
gap.startMs < group.bounds.endMs && gap.endMs > group.bounds.startMs,
|
|
818
|
+
);
|
|
819
|
+
newRows.push({
|
|
820
|
+
schemaVersion: 1,
|
|
821
|
+
kind: "cost-period-digest",
|
|
822
|
+
id,
|
|
823
|
+
projectKey: group.projectKey,
|
|
824
|
+
canonicalAccountId: group.canonicalAccountId,
|
|
825
|
+
requestedModel: group.requestedModel,
|
|
826
|
+
periodType: "day",
|
|
827
|
+
periodStartMs: group.bounds.startMs,
|
|
828
|
+
periodEndMs: group.bounds.endMs,
|
|
829
|
+
closedAtMs: input.closedAtMs,
|
|
830
|
+
source: "raw",
|
|
831
|
+
childRowCount: 0,
|
|
832
|
+
observationCount: group.observationCount,
|
|
833
|
+
coverage: gapOverlaps ? "partial" : group.coverage,
|
|
834
|
+
tokens: group.tokens,
|
|
835
|
+
retainedCostUsd: group.retainedCostUsd,
|
|
836
|
+
apiEquivalent:
|
|
837
|
+
input.piCatalog === undefined
|
|
838
|
+
? priceRaw(
|
|
839
|
+
group.tokens,
|
|
840
|
+
group.canonicalAccountId,
|
|
841
|
+
group.requestedModel,
|
|
842
|
+
input.pricing,
|
|
843
|
+
)
|
|
844
|
+
: priceRawPerResponse(group, input.piCatalog, input.pricing),
|
|
845
|
+
});
|
|
846
|
+
existingIds.add(id);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const allRows = [...input.existingRows, ...newRows];
|
|
850
|
+
const days = allRows.filter((row) => row.periodType === "day");
|
|
851
|
+
const weeks = parentRows(
|
|
852
|
+
days,
|
|
853
|
+
"week",
|
|
854
|
+
"day-rollup",
|
|
855
|
+
input.nowMs,
|
|
856
|
+
input.closedAtMs,
|
|
857
|
+
existingIds,
|
|
858
|
+
);
|
|
859
|
+
const months = parentRows(
|
|
860
|
+
days,
|
|
861
|
+
"month",
|
|
862
|
+
"day-rollup",
|
|
863
|
+
input.nowMs,
|
|
864
|
+
input.closedAtMs,
|
|
865
|
+
existingIds,
|
|
866
|
+
);
|
|
867
|
+
newRows.push(...weeks, ...months);
|
|
868
|
+
const allMonths = [...input.existingRows, ...newRows].filter(
|
|
869
|
+
(row) => row.periodType === "month",
|
|
870
|
+
);
|
|
871
|
+
for (const periodType of ["quarter", "half-year", "year"] as const) {
|
|
872
|
+
newRows.push(
|
|
873
|
+
...parentRows(
|
|
874
|
+
allMonths,
|
|
875
|
+
periodType,
|
|
876
|
+
"month-rollup",
|
|
877
|
+
input.nowMs,
|
|
878
|
+
input.closedAtMs,
|
|
879
|
+
existingIds,
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const order: Record<PeriodType, number> = {
|
|
884
|
+
day: 0,
|
|
885
|
+
week: 1,
|
|
886
|
+
month: 2,
|
|
887
|
+
quarter: 3,
|
|
888
|
+
"half-year": 4,
|
|
889
|
+
year: 5,
|
|
890
|
+
};
|
|
891
|
+
return newRows.sort(
|
|
892
|
+
(left, right) =>
|
|
893
|
+
left.periodEndMs - right.periodEndMs ||
|
|
894
|
+
order[left.periodType] - order[right.periodType] ||
|
|
895
|
+
left.id.localeCompare(right.id),
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function assertDigestClocks(nowMs: number, closedAtMs: number): void {
|
|
900
|
+
if (!validTimestamp(nowMs) || !validTimestamp(closedAtMs)) {
|
|
901
|
+
throw new RangeError("Cost digest clocks must be finite timestamps.");
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
export function planClosedCostDigests(input: {
|
|
906
|
+
readonly observations: readonly CostObservation[];
|
|
907
|
+
readonly gaps: readonly CostHistoryGap[];
|
|
908
|
+
readonly existingRows: readonly CostDigestRow[];
|
|
909
|
+
readonly pricing: PricingCacheResult;
|
|
910
|
+
readonly piCatalog?: PiCatalogSnapshot;
|
|
911
|
+
readonly nowMs: number;
|
|
912
|
+
readonly closedAtMs: number;
|
|
913
|
+
}): readonly CostDigestRow[] {
|
|
914
|
+
assertDigestClocks(input.nowMs, input.closedAtMs);
|
|
915
|
+
const groups = new Map<string, RawCostGroup>();
|
|
916
|
+
for (const observation of input.observations) {
|
|
917
|
+
addRawObservation(groups, observation, input.nowMs);
|
|
918
|
+
}
|
|
919
|
+
return planClosedCostDigestGroups({ ...input, groups });
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
export function planClosedCostDigestsFromHistory(input: {
|
|
923
|
+
readonly readRecords: () => Iterable<HistoryLogRecord>;
|
|
924
|
+
readonly existingRows: readonly CostDigestRow[];
|
|
925
|
+
readonly pricing: PricingCacheResult;
|
|
926
|
+
readonly piCatalog?: PiCatalogSnapshot;
|
|
927
|
+
readonly nowMs: number;
|
|
928
|
+
readonly closedAtMs: number;
|
|
929
|
+
}): readonly CostDigestRow[] {
|
|
930
|
+
assertDigestClocks(input.nowMs, input.closedAtMs);
|
|
931
|
+
const groups = new Map<string, RawCostGroup>();
|
|
932
|
+
for (const record of input.readRecords()) {
|
|
933
|
+
const observation = parseObservation(record);
|
|
934
|
+
if (observation !== undefined) {
|
|
935
|
+
addRawObservation(groups, observation, input.nowMs);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (groups.size > 0) {
|
|
940
|
+
const groupsByDay = new Map<number, RawCostGroup[]>();
|
|
941
|
+
for (const group of groups.values()) {
|
|
942
|
+
const dayGroups = groupsByDay.get(group.bounds.startMs) ?? [];
|
|
943
|
+
dayGroups.push(group);
|
|
944
|
+
groupsByDay.set(group.bounds.startMs, dayGroups);
|
|
945
|
+
}
|
|
946
|
+
const dayStarts = [...groupsByDay.keys()].sort((left, right) => left - right);
|
|
947
|
+
for (const record of input.readRecords()) {
|
|
948
|
+
if (
|
|
949
|
+
record.recordType !== "gap" ||
|
|
950
|
+
!validTimestamp(record.gapStartMs) ||
|
|
951
|
+
!validTimestamp(record.gapEndMs) ||
|
|
952
|
+
record.gapEndMs <= record.gapStartMs
|
|
953
|
+
) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
let low = 0;
|
|
957
|
+
let high = dayStarts.length;
|
|
958
|
+
while (low < high) {
|
|
959
|
+
const middle = Math.floor((low + high) / 2);
|
|
960
|
+
const start = dayStarts[middle];
|
|
961
|
+
if (start === undefined || start + DAY_MS > record.gapStartMs) {
|
|
962
|
+
high = middle;
|
|
963
|
+
} else {
|
|
964
|
+
low = middle + 1;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
for (let index = low; index < dayStarts.length; index += 1) {
|
|
968
|
+
const start = dayStarts[index];
|
|
969
|
+
if (start === undefined || start >= record.gapEndMs) break;
|
|
970
|
+
for (const group of groupsByDay.get(start) ?? []) {
|
|
971
|
+
group.coverage = "partial";
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return planClosedCostDigestGroups({ ...input, groups, gaps: [] });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const PERIOD_COARSENESS: Readonly<Record<PeriodType, number>> = {
|
|
980
|
+
year: 0,
|
|
981
|
+
"half-year": 1,
|
|
982
|
+
quarter: 2,
|
|
983
|
+
month: 3,
|
|
984
|
+
week: 4,
|
|
985
|
+
day: 5,
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
function digestRowIdentityKey(row: CostDigestRow): string {
|
|
989
|
+
return JSON.stringify([row.projectKey, row.canonicalAccountId, row.requestedModel]);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Selects one non-overlapping retained representation per (project, account,
|
|
994
|
+
* model) identity across `[rangeStartMs, rangeEndMs)`, so a report spanning
|
|
995
|
+
* several granularities never double-counts an observation that is present
|
|
996
|
+
* both in a raw/day row and in a coarser rollup that already absorbed it.
|
|
997
|
+
*
|
|
998
|
+
* Only rows fully contained in the requested range are candidates -- a row
|
|
999
|
+
* that only partially overlaps the boundary is excluded rather than sliced,
|
|
1000
|
+
* since neither this function nor its callers may fabricate a sub-period
|
|
1001
|
+
* total a coarser rollup never recorded. Candidates are considered coarsest
|
|
1002
|
+
* first per identity; a candidate is accepted only when its interval does not
|
|
1003
|
+
* overlap an already-accepted interval for that same identity, so finer rows
|
|
1004
|
+
* fill exactly the gaps a coarser rollup left uncovered.
|
|
1005
|
+
*/
|
|
1006
|
+
export function selectNonOverlappingDigestRows(
|
|
1007
|
+
rows: readonly CostDigestRow[],
|
|
1008
|
+
rangeStartMs: number,
|
|
1009
|
+
rangeEndMs: number,
|
|
1010
|
+
): readonly CostDigestRow[] {
|
|
1011
|
+
if (rangeEndMs <= rangeStartMs) return [];
|
|
1012
|
+
const byIdentity = new Map<string, CostDigestRow[]>();
|
|
1013
|
+
for (const row of rows) {
|
|
1014
|
+
if (row.periodStartMs < rangeStartMs || row.periodEndMs > rangeEndMs) continue;
|
|
1015
|
+
const key = digestRowIdentityKey(row);
|
|
1016
|
+
const list = byIdentity.get(key) ?? [];
|
|
1017
|
+
list.push(row);
|
|
1018
|
+
byIdentity.set(key, list);
|
|
1019
|
+
}
|
|
1020
|
+
const selected: CostDigestRow[] = [];
|
|
1021
|
+
for (const candidates of byIdentity.values()) {
|
|
1022
|
+
const sorted = [...candidates].sort(
|
|
1023
|
+
(left, right) =>
|
|
1024
|
+
PERIOD_COARSENESS[left.periodType] - PERIOD_COARSENESS[right.periodType] ||
|
|
1025
|
+
left.periodStartMs - right.periodStartMs,
|
|
1026
|
+
);
|
|
1027
|
+
const covered: Array<{ readonly startMs: number; readonly endMs: number }> = [];
|
|
1028
|
+
for (const row of sorted) {
|
|
1029
|
+
const overlapsCovered = covered.some(
|
|
1030
|
+
(interval) =>
|
|
1031
|
+
row.periodStartMs < interval.endMs && row.periodEndMs > interval.startMs,
|
|
1032
|
+
);
|
|
1033
|
+
if (overlapsCovered) continue;
|
|
1034
|
+
covered.push({ startMs: row.periodStartMs, endMs: row.periodEndMs });
|
|
1035
|
+
selected.push(row);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return selected.sort(
|
|
1039
|
+
(left, right) =>
|
|
1040
|
+
left.periodStartMs - right.periodStartMs ||
|
|
1041
|
+
PERIOD_COARSENESS[left.periodType] - PERIOD_COARSENESS[right.periodType] ||
|
|
1042
|
+
left.id.localeCompare(right.id),
|
|
1043
|
+
);
|
|
1044
|
+
}
|