@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,251 @@
|
|
|
1
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { isProjectKey, projectKeyForCwd } from "./project-identity.js";
|
|
4
|
+
import { appendHistory, type HistoryLockExhaustion, type HistoryRecordEnvelope } from "./history-store.js";
|
|
5
|
+
import { isCanonicalManagedProviderId } from "./runtime-state.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Cost record status.
|
|
9
|
+
*
|
|
10
|
+
* REQ-COST-INCOMPLETE: mutually exclusive status assigned in priority order.
|
|
11
|
+
* Priority: complete > unpriced > partial
|
|
12
|
+
*/
|
|
13
|
+
export type CostStatus = "complete" | "unpriced" | "partial";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Cost record payload appended to cost-history.ndjson.
|
|
17
|
+
*
|
|
18
|
+
* REQ-COST-RECORD: immutable snapshot of one observed response's cost and usage.
|
|
19
|
+
* REQ-OBSERVER-BOUNDED: observer id is bounded, non-identifying, process-scoped.
|
|
20
|
+
*/
|
|
21
|
+
export interface CostRecordPayload {
|
|
22
|
+
readonly canonicalAccountId: string;
|
|
23
|
+
readonly projectKey: string;
|
|
24
|
+
readonly requestedModel: string;
|
|
25
|
+
readonly responseModel: string | undefined;
|
|
26
|
+
readonly observerId: string;
|
|
27
|
+
readonly status: CostStatus;
|
|
28
|
+
readonly schemaProvenance: string;
|
|
29
|
+
readonly buildProvenance: string;
|
|
30
|
+
readonly tokens: {
|
|
31
|
+
readonly input: number;
|
|
32
|
+
readonly output: number;
|
|
33
|
+
readonly cacheRead: number;
|
|
34
|
+
readonly cacheWrite: number;
|
|
35
|
+
readonly cacheWrite1h: number | undefined;
|
|
36
|
+
readonly total: number;
|
|
37
|
+
};
|
|
38
|
+
readonly cost: {
|
|
39
|
+
readonly input: number;
|
|
40
|
+
readonly output: number;
|
|
41
|
+
readonly cacheRead: number;
|
|
42
|
+
readonly cacheWrite: number;
|
|
43
|
+
readonly total: number;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bounded, non-identifying observer id.
|
|
49
|
+
*
|
|
50
|
+
* REQ-OBSERVER-BOUNDED: process-scoped uuid, max 36 bytes. Contains no hostname,
|
|
51
|
+
* username, path, or human-identifying token.
|
|
52
|
+
*/
|
|
53
|
+
let processObserverId: string | undefined;
|
|
54
|
+
function getObserverId(): string {
|
|
55
|
+
if (!processObserverId) {
|
|
56
|
+
processObserverId = randomUUID();
|
|
57
|
+
}
|
|
58
|
+
return processObserverId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Classify cost status.
|
|
63
|
+
*
|
|
64
|
+
* REQ-COST-INCOMPLETE: mutually exclusive status assigned in priority order.
|
|
65
|
+
* Priority: complete > unpriced > partial.
|
|
66
|
+
*
|
|
67
|
+
* Discriminator is TERMINATION, not the cost value. Do NOT branch on
|
|
68
|
+
* `cost.total === 0` (or any other cost-value predicate) as a signal of
|
|
69
|
+
* completeness. A genuinely-free successful response (e.g. a fully-cache-read
|
|
70
|
+
* turn) with every cost component equal to 0 is a REAL, correctly-priced
|
|
71
|
+
* observation and MUST classify `complete`; excluding it would understate
|
|
72
|
+
* usage in the REQ-COST-INCOMPLETE principal aggregate. The
|
|
73
|
+
* "initialized zero-cost object" carve-out for `unpriced` applies to
|
|
74
|
+
* INTERRUPTED responses, which by definition do not carry a terminal
|
|
75
|
+
* stopReason and therefore fall past the `complete` branch on stopReason
|
|
76
|
+
* alone — zero-ness of the payload is not the signal, the abnormal
|
|
77
|
+
* termination is.
|
|
78
|
+
*
|
|
79
|
+
* 1. `complete` — stopReason is `stop`, `length`, or `toolUse` AND every
|
|
80
|
+
* required token and cost field is finite and non-negative. Zero-valued
|
|
81
|
+
* but finite/non-negative cost components ARE valid; a free turn
|
|
82
|
+
* classifies here.
|
|
83
|
+
* 2. else `unpriced` — abnormal termination with no trustworthy computed
|
|
84
|
+
* cost: an interrupted stream whose payload is only pi's untouched
|
|
85
|
+
* zero-cost object, OR any response whose cost fields are missing,
|
|
86
|
+
* non-finite, or negative.
|
|
87
|
+
* 3. else `partial` — `error`/`aborted` response, or a stream interrupted
|
|
88
|
+
* before a successful terminal message, that retains trustworthy
|
|
89
|
+
* computed cost or usage data.
|
|
90
|
+
*
|
|
91
|
+
* Known residual gap (spec REQ-COST-RECORD, accepted): a synthesised
|
|
92
|
+
* terminal stopReason attached upstream to a genuinely-interrupted stream
|
|
93
|
+
* will classify `complete` under this rule. The spec accepts that side of
|
|
94
|
+
* the trade because stopReason is the completeness signal the emitter
|
|
95
|
+
* provides; do not try to fix it here.
|
|
96
|
+
*/
|
|
97
|
+
export function classifyCostStatus(message: AssistantMessage): CostStatus {
|
|
98
|
+
const { stopReason, usage } = message;
|
|
99
|
+
|
|
100
|
+
const hasValidTokens =
|
|
101
|
+
Number.isFinite(usage.input) &&
|
|
102
|
+
usage.input >= 0 &&
|
|
103
|
+
Number.isFinite(usage.output) &&
|
|
104
|
+
usage.output >= 0 &&
|
|
105
|
+
Number.isFinite(usage.cacheRead) &&
|
|
106
|
+
usage.cacheRead >= 0 &&
|
|
107
|
+
Number.isFinite(usage.cacheWrite) &&
|
|
108
|
+
usage.cacheWrite >= 0 &&
|
|
109
|
+
Number.isFinite(usage.totalTokens) &&
|
|
110
|
+
usage.totalTokens >= 0;
|
|
111
|
+
|
|
112
|
+
const hasValidCost =
|
|
113
|
+
Number.isFinite(usage.cost.input) &&
|
|
114
|
+
usage.cost.input >= 0 &&
|
|
115
|
+
Number.isFinite(usage.cost.output) &&
|
|
116
|
+
usage.cost.output >= 0 &&
|
|
117
|
+
Number.isFinite(usage.cost.cacheRead) &&
|
|
118
|
+
usage.cost.cacheRead >= 0 &&
|
|
119
|
+
Number.isFinite(usage.cost.cacheWrite) &&
|
|
120
|
+
usage.cost.cacheWrite >= 0 &&
|
|
121
|
+
Number.isFinite(usage.cost.total) &&
|
|
122
|
+
usage.cost.total >= 0;
|
|
123
|
+
|
|
124
|
+
const isTerminalStop =
|
|
125
|
+
stopReason === "stop" ||
|
|
126
|
+
stopReason === "length" ||
|
|
127
|
+
stopReason === "toolUse";
|
|
128
|
+
|
|
129
|
+
// Priority 1: complete — terminal stopReason with all fields finite
|
|
130
|
+
// and non-negative. Zero is finite and non-negative: a fully cache-read
|
|
131
|
+
// turn that legitimately cost $0.00 classifies here.
|
|
132
|
+
if (isTerminalStop && hasValidTokens && hasValidCost) {
|
|
133
|
+
return "complete";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Priority 2: unpriced — no trustworthy computed cost. Two disjoint
|
|
137
|
+
// cases: (a) cost fields are outright invalid (non-finite or negative);
|
|
138
|
+
// (b) abnormal termination whose payload has no cost or usage signal
|
|
139
|
+
// at all (the untouched zero-cost object from an interrupted stream,
|
|
140
|
+
// with no tokens observed either).
|
|
141
|
+
const hasNoUsageSignal =
|
|
142
|
+
usage.input === 0 &&
|
|
143
|
+
usage.output === 0 &&
|
|
144
|
+
usage.cacheRead === 0 &&
|
|
145
|
+
usage.cacheWrite === 0 &&
|
|
146
|
+
usage.totalTokens === 0 &&
|
|
147
|
+
usage.cost.input === 0 &&
|
|
148
|
+
usage.cost.output === 0 &&
|
|
149
|
+
usage.cost.cacheRead === 0 &&
|
|
150
|
+
usage.cost.cacheWrite === 0 &&
|
|
151
|
+
usage.cost.total === 0;
|
|
152
|
+
|
|
153
|
+
if (!hasValidCost || hasNoUsageSignal) {
|
|
154
|
+
return "unpriced";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Priority 3: partial — abnormal termination retaining trustworthy
|
|
158
|
+
// cost or usage data.
|
|
159
|
+
return "partial";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Append a cost record to cost-history.ndjson.
|
|
164
|
+
*
|
|
165
|
+
* REQ-COST-RECORD: one immutable record per observed response.
|
|
166
|
+
* REQ-COST-NO-REPRICING: retains pi's computed cost verbatim, never recomputes.
|
|
167
|
+
*
|
|
168
|
+
* Fail-soft: returns false on append failure, caller should log and continue.
|
|
169
|
+
* NEVER throws into the host.
|
|
170
|
+
*/
|
|
171
|
+
export async function appendCostRecord(
|
|
172
|
+
message: AssistantMessage,
|
|
173
|
+
canonicalAccountId: string,
|
|
174
|
+
options?: {
|
|
175
|
+
readonly costHistoryPath?: string;
|
|
176
|
+
readonly lockPath?: string;
|
|
177
|
+
readonly expectedBase?: string;
|
|
178
|
+
readonly now?: () => number;
|
|
179
|
+
readonly projectKey?: string;
|
|
180
|
+
readonly retryBudget?: {
|
|
181
|
+
readonly maxRetries?: number;
|
|
182
|
+
readonly initialDelayMs?: number;
|
|
183
|
+
readonly maxDelayMs?: number;
|
|
184
|
+
};
|
|
185
|
+
readonly onLockExhausted?: (details: HistoryLockExhaustion) => void;
|
|
186
|
+
},
|
|
187
|
+
): Promise<boolean> {
|
|
188
|
+
try {
|
|
189
|
+
const rawStatus = classifyCostStatus(message);
|
|
190
|
+
// REQ-COST-NO-CONFLATION: Antigravity has no owning-vendor-api tier
|
|
191
|
+
// (`providerTypeFor` always returns "subscription" for `google-antigravity`),
|
|
192
|
+
// so the model `cost` rates registered from the reviewed upstream catalog
|
|
193
|
+
// (`src/upstream-antigravity.ts`, e.g. Google's own `geminiFlashCost`-style
|
|
194
|
+
// figures) are never a genuine per-token charge pi actually billed -- they
|
|
195
|
+
// are the same public-rate-style estimate this repository already keeps
|
|
196
|
+
// distinct from retained cost for every other family via `api-pricing.ts`.
|
|
197
|
+
// Whatever `message.usage.cost` computed from that catalog rate must never
|
|
198
|
+
// be recorded as a retained subscription-provider cost fact. Forcing
|
|
199
|
+
// "unpriced" here is what makes `cost-digest.ts#parseObservation` floor
|
|
200
|
+
// `retainedCostUsd` to 0 for this record downstream, while the tokens
|
|
201
|
+
// still enter the ledger so usage is not understated.
|
|
202
|
+
const status: CostStatus = isCanonicalManagedProviderId(
|
|
203
|
+
canonicalAccountId,
|
|
204
|
+
"google-antigravity",
|
|
205
|
+
)
|
|
206
|
+
? "unpriced"
|
|
207
|
+
: rawStatus;
|
|
208
|
+
const projectKey = options?.projectKey ?? projectKeyForCwd();
|
|
209
|
+
if (!isProjectKey(projectKey)) return false;
|
|
210
|
+
|
|
211
|
+
const payload: CostRecordPayload = {
|
|
212
|
+
canonicalAccountId,
|
|
213
|
+
projectKey,
|
|
214
|
+
requestedModel: message.model,
|
|
215
|
+
responseModel: message.responseModel,
|
|
216
|
+
observerId: getObserverId(),
|
|
217
|
+
status,
|
|
218
|
+
schemaProvenance: "pi-multi-account@0.0.1",
|
|
219
|
+
buildProvenance: `node@${process.version}`,
|
|
220
|
+
tokens: {
|
|
221
|
+
input: message.usage.input,
|
|
222
|
+
output: message.usage.output,
|
|
223
|
+
cacheRead: message.usage.cacheRead,
|
|
224
|
+
cacheWrite: message.usage.cacheWrite,
|
|
225
|
+
cacheWrite1h: message.usage.cacheWrite1h,
|
|
226
|
+
total: message.usage.totalTokens,
|
|
227
|
+
},
|
|
228
|
+
cost: {
|
|
229
|
+
input: message.usage.cost.input,
|
|
230
|
+
output: message.usage.cost.output,
|
|
231
|
+
cacheRead: message.usage.cost.cacheRead,
|
|
232
|
+
cacheWrite: message.usage.cost.cacheWrite,
|
|
233
|
+
total: message.usage.cost.total,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const envelope: HistoryRecordEnvelope = {
|
|
238
|
+
schemaVersion: 1,
|
|
239
|
+
recordType: "cost-delta",
|
|
240
|
+
stableId: randomUUID(),
|
|
241
|
+
observedAtMs: message.timestamp,
|
|
242
|
+
recordedAtMs: Date.now(),
|
|
243
|
+
payload,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return await appendHistory(envelope, options);
|
|
247
|
+
} catch {
|
|
248
|
+
// Fail-soft: never throw into the host
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
planClosedCostDigestsFromHistory,
|
|
3
|
+
type CostDigestRow,
|
|
4
|
+
} from "./cost-digest.js";
|
|
5
|
+
import {
|
|
6
|
+
CostDigestStore,
|
|
7
|
+
defaultCostDigestPaths,
|
|
8
|
+
} from "./cost-digest-store.js";
|
|
9
|
+
import { iterateHistory, type HistoryLogRecord } from "./history-store.js";
|
|
10
|
+
import {
|
|
11
|
+
acquireMachineLease,
|
|
12
|
+
type MachineLeaseHandle,
|
|
13
|
+
} from "./machine-lease.js";
|
|
14
|
+
import { getPeriodBounds } from "./period-boundaries.js";
|
|
15
|
+
import {
|
|
16
|
+
defaultPricingCachePaths,
|
|
17
|
+
OpenRouterPricingCache,
|
|
18
|
+
type PricingCacheResult,
|
|
19
|
+
} from "./pricing-cache.js";
|
|
20
|
+
|
|
21
|
+
export const COST_PERIOD_CLOSER_LEASE_TTL_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
export interface CostPeriodCloserStore {
|
|
24
|
+
read(): readonly CostDigestRow[];
|
|
25
|
+
appendImmutable(
|
|
26
|
+
rows: readonly CostDigestRow[],
|
|
27
|
+
lease: MachineLeaseHandle,
|
|
28
|
+
): number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CostPeriodCloseResult =
|
|
32
|
+
| { readonly status: "closed" | "no-op"; readonly appendedRows: number }
|
|
33
|
+
| { readonly status: "lease-held"; readonly appendedRows: 0 }
|
|
34
|
+
| { readonly status: "failed"; readonly appendedRows: number }
|
|
35
|
+
| {
|
|
36
|
+
readonly status: "skipped";
|
|
37
|
+
readonly reason: "already-attempted-this-day";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface CostPeriodCloserOptions {
|
|
41
|
+
readonly store: CostPeriodCloserStore;
|
|
42
|
+
readonly pricingCache: {
|
|
43
|
+
refreshIfNeeded(): Promise<PricingCacheResult>;
|
|
44
|
+
};
|
|
45
|
+
readonly readCostHistory: () => Iterable<HistoryLogRecord>;
|
|
46
|
+
readonly acquireLease: () => MachineLeaseHandle | undefined;
|
|
47
|
+
readonly now: () => number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function validTimestamp(value: number): boolean {
|
|
51
|
+
return Number.isFinite(value) && value >= 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class CostPeriodCloser {
|
|
55
|
+
readonly #store: CostPeriodCloserStore;
|
|
56
|
+
readonly #pricingCache: CostPeriodCloserOptions["pricingCache"];
|
|
57
|
+
readonly #readCostHistory: CostPeriodCloserOptions["readCostHistory"];
|
|
58
|
+
readonly #acquireLease: CostPeriodCloserOptions["acquireLease"];
|
|
59
|
+
readonly #now: CostPeriodCloserOptions["now"];
|
|
60
|
+
#attemptedDayStartMs: number | undefined;
|
|
61
|
+
|
|
62
|
+
constructor(options: CostPeriodCloserOptions) {
|
|
63
|
+
this.#store = options.store;
|
|
64
|
+
this.#pricingCache = options.pricingCache;
|
|
65
|
+
this.#readCostHistory = options.readCostHistory;
|
|
66
|
+
this.#acquireLease = options.acquireLease;
|
|
67
|
+
this.#now = options.now;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async closeCompletedPeriods(): Promise<CostPeriodCloseResult> {
|
|
71
|
+
let lease: MachineLeaseHandle | undefined;
|
|
72
|
+
let appendedRows = 0;
|
|
73
|
+
try {
|
|
74
|
+
const nowMs = this.#now();
|
|
75
|
+
if (!validTimestamp(nowMs)) return { status: "failed", appendedRows };
|
|
76
|
+
lease = this.#acquireLease();
|
|
77
|
+
if (lease === undefined) {
|
|
78
|
+
return { status: "lease-held", appendedRows: 0 };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pricing = await this.#pricingCache.refreshIfNeeded();
|
|
82
|
+
const existingRows = this.#store.read();
|
|
83
|
+
const plannedRows = planClosedCostDigestsFromHistory({
|
|
84
|
+
readRecords: this.#readCostHistory,
|
|
85
|
+
existingRows,
|
|
86
|
+
pricing,
|
|
87
|
+
nowMs,
|
|
88
|
+
closedAtMs: nowMs,
|
|
89
|
+
});
|
|
90
|
+
if (plannedRows.length === 0) {
|
|
91
|
+
return { status: "no-op", appendedRows: 0 };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
appendedRows = this.#store.appendImmutable(plannedRows, lease);
|
|
95
|
+
const persistedIds = new Set(this.#store.read().map((row) => row.id));
|
|
96
|
+
if (!plannedRows.every((row) => persistedIds.has(row.id))) {
|
|
97
|
+
return { status: "failed", appendedRows };
|
|
98
|
+
}
|
|
99
|
+
return { status: "closed", appendedRows };
|
|
100
|
+
} catch {
|
|
101
|
+
return { status: "failed", appendedRows };
|
|
102
|
+
} finally {
|
|
103
|
+
lease?.release();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async closeBeforeRender(): Promise<CostPeriodCloseResult> {
|
|
108
|
+
return this.closeCompletedPeriods();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async closeAfterObservation(
|
|
112
|
+
observedAtMs: number,
|
|
113
|
+
): Promise<CostPeriodCloseResult> {
|
|
114
|
+
if (!validTimestamp(observedAtMs)) {
|
|
115
|
+
return { status: "failed", appendedRows: 0 };
|
|
116
|
+
}
|
|
117
|
+
const dayStartMs = getPeriodBounds(observedAtMs, "day").startMs;
|
|
118
|
+
if (
|
|
119
|
+
this.#attemptedDayStartMs !== undefined &&
|
|
120
|
+
dayStartMs <= this.#attemptedDayStartMs
|
|
121
|
+
) {
|
|
122
|
+
return {
|
|
123
|
+
status: "skipped",
|
|
124
|
+
reason: "already-attempted-this-day",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
this.#attemptedDayStartMs = dayStartMs;
|
|
128
|
+
const result = await this.closeCompletedPeriods();
|
|
129
|
+
if (result.status === "failed" && this.#attemptedDayStartMs === dayStartMs) {
|
|
130
|
+
this.#attemptedDayStartMs = undefined;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createDefaultCostPeriodCloser(options?: {
|
|
137
|
+
readonly now?: () => number;
|
|
138
|
+
}): CostPeriodCloser {
|
|
139
|
+
const now = options?.now ?? Date.now;
|
|
140
|
+
const digestPaths = defaultCostDigestPaths();
|
|
141
|
+
const pricingPaths = defaultPricingCachePaths();
|
|
142
|
+
return new CostPeriodCloser({
|
|
143
|
+
store: new CostDigestStore({ path: digestPaths.path }),
|
|
144
|
+
pricingCache: new OpenRouterPricingCache({
|
|
145
|
+
cachePath: pricingPaths.cachePath,
|
|
146
|
+
lockPath: pricingPaths.lockPath,
|
|
147
|
+
now,
|
|
148
|
+
}),
|
|
149
|
+
readCostHistory: () => iterateHistory("cost-delta"),
|
|
150
|
+
acquireLease: () =>
|
|
151
|
+
acquireMachineLease({
|
|
152
|
+
lockPath: digestPaths.lockPath,
|
|
153
|
+
ttlMs: COST_PERIOD_CLOSER_LEASE_TTL_MS,
|
|
154
|
+
now,
|
|
155
|
+
// A wedged digest lock would otherwise stop every period closure.
|
|
156
|
+
reclaimMalformed: true,
|
|
157
|
+
}),
|
|
158
|
+
now,
|
|
159
|
+
});
|
|
160
|
+
}
|