@kunkun.sh/kuota 0.0.0-stage → 0.1.0
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/README.md +130 -2
- package/bin/kuota.mjs +3 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +1303 -0
- package/dist/dist-CIjerpSd.mjs +576 -0
- package/dist/history-G4o4HVvW.d.mts +36 -0
- package/dist/history-oOXsjdWR.mjs +236 -0
- package/dist/history.d.mts +3 -0
- package/dist/history.mjs +3 -0
- package/dist/index-2k6FkhpT.d.mts +81 -0
- package/dist/index-NHzh2TGn.d.mts +168 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/main-B-hZbKK1.mjs +340 -0
- package/dist/metrics-Bzmrfgn-.mjs +307 -0
- package/dist/metrics-D7oNul_9.d.mts +30 -0
- package/dist/metrics.d.mts +3 -0
- package/dist/metrics.mjs +3 -0
- package/dist/parsers.d.mts +13 -0
- package/dist/parsers.mjs +19 -0
- package/dist/providers.d.mts +2 -0
- package/dist/providers.mjs +1 -0
- package/dist/src-C48Ew_L9.mjs +707 -0
- package/dist/src-y_QKmTyj.mjs +309 -0
- package/dist/web/assets/index-ihoe7C_u.js +27 -0
- package/dist/web/index.html +30 -0
- package/dist/web-server-DkdA5Ixj.mjs +154 -0
- package/package.json +73 -4
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
// ../core/dist/index.mjs
|
|
3
|
+
const ProviderIdSchema = Schema.Literal("codex", "claude", "antigravity");
|
|
4
|
+
const QuotaStatusSchema = Schema.Literal("ok", "partial", "unauthenticated", "unavailable", "unsupported", "error");
|
|
5
|
+
const QuotaWindowSchema = Schema.Struct({
|
|
6
|
+
windowId: Schema.String,
|
|
7
|
+
usedPercent: Schema.NullOr(Schema.Number),
|
|
8
|
+
remainingPercent: Schema.NullOr(Schema.Number),
|
|
9
|
+
resetAt: Schema.NullOr(Schema.String)
|
|
10
|
+
});
|
|
11
|
+
Schema.Struct({
|
|
12
|
+
provider: ProviderIdSchema,
|
|
13
|
+
status: QuotaStatusSchema,
|
|
14
|
+
fetchedAt: Schema.String,
|
|
15
|
+
source: Schema.String,
|
|
16
|
+
windows: Schema.Array(QuotaWindowSchema),
|
|
17
|
+
errorCode: Schema.NullOr(Schema.String)
|
|
18
|
+
});
|
|
19
|
+
function safePercent(value) {
|
|
20
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 100 ? value : null;
|
|
21
|
+
}
|
|
22
|
+
function safeIsoDate(value) {
|
|
23
|
+
if (typeof value !== "string" && typeof value !== "number") return null;
|
|
24
|
+
const date = new Date(value);
|
|
25
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : null;
|
|
26
|
+
}
|
|
27
|
+
function normalizeWindow(input) {
|
|
28
|
+
const usedPercent = safePercent(input.usedPercent);
|
|
29
|
+
return {
|
|
30
|
+
windowId: input.windowId,
|
|
31
|
+
usedPercent,
|
|
32
|
+
remainingPercent: usedPercent === null ? null : Number((100 - usedPercent).toFixed(2)),
|
|
33
|
+
resetAt: safeIsoDate(input.resetAt)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function makeUnavailableSnapshot(provider, errorCode, status = "unavailable") {
|
|
37
|
+
return {
|
|
38
|
+
provider,
|
|
39
|
+
status,
|
|
40
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
41
|
+
source: "local-probe",
|
|
42
|
+
windows: [],
|
|
43
|
+
errorCode
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** Returns null when the provider has not proved a comparable fixed window. */
|
|
47
|
+
function calculatePace(input) {
|
|
48
|
+
if (input.semantics === "rolling" || input.semantics === "unknown" || input.startSource === "unknown" || input.semantics === "first_use" && input.startSource !== "provider" || input.startAt === null || input.resetAt === null || input.usedPercent === null || !Number.isFinite(input.usedPercent) || input.usedPercent < 0 || input.usedPercent > 100) return null;
|
|
49
|
+
const now = input.now.getTime();
|
|
50
|
+
const start = input.startAt.getTime();
|
|
51
|
+
const reset = input.resetAt.getTime();
|
|
52
|
+
if (!Number.isFinite(now) || !Number.isFinite(start) || !Number.isFinite(reset) || reset <= start || now < start) return null;
|
|
53
|
+
const elapsedFraction = Math.min(1, (now - start) / (reset - start));
|
|
54
|
+
const idealUsedPercent = elapsedFraction * 100;
|
|
55
|
+
return {
|
|
56
|
+
elapsedFraction,
|
|
57
|
+
idealUsedPercent,
|
|
58
|
+
idealRemainingPercent: 100 - idealUsedPercent,
|
|
59
|
+
actualUsedPercent: input.usedPercent,
|
|
60
|
+
paceDeltaPoints: input.usedPercent - idealUsedPercent,
|
|
61
|
+
paceRatio: idealUsedPercent === 0 ? null : input.usedPercent / idealUsedPercent,
|
|
62
|
+
remainingHours: Math.max(0, reset - now) / 36e5,
|
|
63
|
+
quality: input.startSource === "provider" ? "exact" : "inferred"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function iso(ms) {
|
|
67
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : (/* @__PURE__ */ new Date(0)).toISOString();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Pure constructor. If `snapshot.fetchedAt` is not a valid timestamp we fall back to the
|
|
71
|
+
* injected `checkedAtMs` as the observation time, so no NaN / Invalid Date is ever emitted.
|
|
72
|
+
*/
|
|
73
|
+
function makeObservation(input) {
|
|
74
|
+
const parsed = Date.parse(input.snapshot.fetchedAt);
|
|
75
|
+
const observedMs = Number.isFinite(parsed) ? parsed : input.checkedAtMs;
|
|
76
|
+
const next = input.nextAttemptAtMs;
|
|
77
|
+
return {
|
|
78
|
+
snapshot: input.snapshot,
|
|
79
|
+
freshness: input.freshness,
|
|
80
|
+
observedAt: iso(observedMs),
|
|
81
|
+
checkedAt: iso(input.checkedAtMs),
|
|
82
|
+
ageMs: Math.max(0, input.nowMs - observedMs),
|
|
83
|
+
nextAttemptAt: next === null || next === void 0 || !Number.isFinite(next) ? null : iso(next),
|
|
84
|
+
latestErrorCode: input.latestErrorCode ?? null
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const K = new Uint32Array([
|
|
88
|
+
1116352408,
|
|
89
|
+
1899447441,
|
|
90
|
+
3049323471,
|
|
91
|
+
3921009573,
|
|
92
|
+
961987163,
|
|
93
|
+
1508970993,
|
|
94
|
+
2453635748,
|
|
95
|
+
2870763221,
|
|
96
|
+
3624381080,
|
|
97
|
+
310598401,
|
|
98
|
+
607225278,
|
|
99
|
+
1426881987,
|
|
100
|
+
1925078388,
|
|
101
|
+
2162078206,
|
|
102
|
+
2614888103,
|
|
103
|
+
3248222580,
|
|
104
|
+
3835390401,
|
|
105
|
+
4022224774,
|
|
106
|
+
264347078,
|
|
107
|
+
604807628,
|
|
108
|
+
770255983,
|
|
109
|
+
1249150122,
|
|
110
|
+
1555081692,
|
|
111
|
+
1996064986,
|
|
112
|
+
2554220882,
|
|
113
|
+
2821834349,
|
|
114
|
+
2952996808,
|
|
115
|
+
3210313671,
|
|
116
|
+
3336571891,
|
|
117
|
+
3584528711,
|
|
118
|
+
113926993,
|
|
119
|
+
338241895,
|
|
120
|
+
666307205,
|
|
121
|
+
773529912,
|
|
122
|
+
1294757372,
|
|
123
|
+
1396182291,
|
|
124
|
+
1695183700,
|
|
125
|
+
1986661051,
|
|
126
|
+
2177026350,
|
|
127
|
+
2456956037,
|
|
128
|
+
2730485921,
|
|
129
|
+
2820302411,
|
|
130
|
+
3259730800,
|
|
131
|
+
3345764771,
|
|
132
|
+
3516065817,
|
|
133
|
+
3600352804,
|
|
134
|
+
4094571909,
|
|
135
|
+
275423344,
|
|
136
|
+
430227734,
|
|
137
|
+
506948616,
|
|
138
|
+
659060556,
|
|
139
|
+
883997877,
|
|
140
|
+
958139571,
|
|
141
|
+
1322822218,
|
|
142
|
+
1537002063,
|
|
143
|
+
1747873779,
|
|
144
|
+
1955562222,
|
|
145
|
+
2024104815,
|
|
146
|
+
2227730452,
|
|
147
|
+
2361852424,
|
|
148
|
+
2428436474,
|
|
149
|
+
2756734187,
|
|
150
|
+
3204031479,
|
|
151
|
+
3329325298
|
|
152
|
+
]);
|
|
153
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
154
|
+
/** Dependency-free SHA-256 (hex). Keeps @kuota/core free of node:crypto so it stays browser-safe. */
|
|
155
|
+
function sha256Hex(text) {
|
|
156
|
+
const data = new TextEncoder().encode(text);
|
|
157
|
+
const bitLength = data.length * 8;
|
|
158
|
+
const padded = new Uint8Array(data.length + 9 + 63 >> 6 << 6);
|
|
159
|
+
padded.set(data);
|
|
160
|
+
padded[data.length] = 128;
|
|
161
|
+
const view = new DataView(padded.buffer);
|
|
162
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 2 ** 32));
|
|
163
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
164
|
+
const h = new Uint32Array([
|
|
165
|
+
1779033703,
|
|
166
|
+
3144134277,
|
|
167
|
+
1013904242,
|
|
168
|
+
2773480762,
|
|
169
|
+
1359893119,
|
|
170
|
+
2600822924,
|
|
171
|
+
528734635,
|
|
172
|
+
1541459225
|
|
173
|
+
]);
|
|
174
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
175
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
176
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
177
|
+
for (let i = 16; i < 64; i++) {
|
|
178
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
179
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
180
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
181
|
+
}
|
|
182
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
183
|
+
for (let i = 0; i < 64; i++) {
|
|
184
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
185
|
+
const ch = e & f ^ ~e & g;
|
|
186
|
+
const t1 = hh + s1 + ch + K[i] + w[i] >>> 0;
|
|
187
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
188
|
+
hh = g;
|
|
189
|
+
g = f;
|
|
190
|
+
f = e;
|
|
191
|
+
e = d + t1 >>> 0;
|
|
192
|
+
d = c;
|
|
193
|
+
c = b;
|
|
194
|
+
b = a;
|
|
195
|
+
a = t1 + t2 >>> 0;
|
|
196
|
+
}
|
|
197
|
+
const next = [
|
|
198
|
+
a,
|
|
199
|
+
b,
|
|
200
|
+
c,
|
|
201
|
+
d,
|
|
202
|
+
e,
|
|
203
|
+
f,
|
|
204
|
+
g,
|
|
205
|
+
hh
|
|
206
|
+
];
|
|
207
|
+
for (let i = 0; i < 8; i++) h[i] = h[i] + next[i] >>> 0;
|
|
208
|
+
}
|
|
209
|
+
return Array.from(h, (x) => x.toString(16).padStart(8, "0")).join("");
|
|
210
|
+
}
|
|
211
|
+
const HOUR = 36e5;
|
|
212
|
+
/**
|
|
213
|
+
* Window cadence heuristics, derived from window ids the adapters emit. Only cadences named in
|
|
214
|
+
* the id (5h / weekly / daily) are trusted; everything else stays `unknown` rather than guessed.
|
|
215
|
+
* Codex `primary` / `secondary` follow the documented 5h / weekly convention of its app-server.
|
|
216
|
+
*/
|
|
217
|
+
function windowProfile(provider, windowId) {
|
|
218
|
+
if (provider === "codex") {
|
|
219
|
+
if (windowId === "primary") return {
|
|
220
|
+
semantics: "fixed",
|
|
221
|
+
durationMs: 5 * HOUR
|
|
222
|
+
};
|
|
223
|
+
if (windowId === "secondary") return {
|
|
224
|
+
semantics: "fixed",
|
|
225
|
+
durationMs: 168 * HOUR
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const id = windowId.toLowerCase();
|
|
229
|
+
const has = (re) => re.test(id);
|
|
230
|
+
if (has(/(^|[.:_-])(5h|five-hour)([.:_-]|$)/)) return {
|
|
231
|
+
semantics: "fixed",
|
|
232
|
+
durationMs: 5 * HOUR
|
|
233
|
+
};
|
|
234
|
+
if (has(/(^|[.:_-])(weekly|7d|seven-day)([.:_-]|$)/)) return {
|
|
235
|
+
semantics: "fixed",
|
|
236
|
+
durationMs: 168 * HOUR
|
|
237
|
+
};
|
|
238
|
+
if (has(/(^|[.:_-])(daily|24h)([.:_-]|$)/)) return {
|
|
239
|
+
semantics: "fixed",
|
|
240
|
+
durationMs: 24 * HOUR
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
semantics: "unknown",
|
|
244
|
+
durationMs: null
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const IsoDate = Schema.String.pipe(Schema.filter((value) => Number.isFinite(Date.parse(value)) || "must be an ISO timestamp"));
|
|
248
|
+
const Percent = Schema.Number.pipe(Schema.filter((value) => Number.isFinite(value) && value >= 0 && value <= 100 || "0..100"));
|
|
249
|
+
const QuotaHistoryEventV1Schema = Schema.Struct({
|
|
250
|
+
schemaVersion: Schema.Literal(1),
|
|
251
|
+
kind: Schema.Literal("quota_observed", "quota_unavailable", "usage_observed"),
|
|
252
|
+
eventId: Schema.String,
|
|
253
|
+
observedAt: IsoDate,
|
|
254
|
+
recordedAt: IsoDate,
|
|
255
|
+
provider: Schema.String,
|
|
256
|
+
accountScope: Schema.NullOr(Schema.String),
|
|
257
|
+
deviceId: Schema.String,
|
|
258
|
+
windowId: Schema.String,
|
|
259
|
+
epochId: Schema.NullOr(Schema.String),
|
|
260
|
+
semantics: Schema.Literal("fixed", "rolling", "first_use", "unknown"),
|
|
261
|
+
startAt: Schema.NullOr(IsoDate),
|
|
262
|
+
resetAt: Schema.NullOr(IsoDate),
|
|
263
|
+
usedPercent: Schema.NullOr(Percent),
|
|
264
|
+
remainingPercent: Schema.NullOr(Percent),
|
|
265
|
+
source: Schema.String,
|
|
266
|
+
state: Schema.Literal("ok", "partial", "stale", "error", "unauthenticated"),
|
|
267
|
+
errorCode: Schema.NullOr(Schema.String),
|
|
268
|
+
usage: Schema.optional(Schema.Struct({
|
|
269
|
+
inputTokens: Schema.optional(Schema.Number),
|
|
270
|
+
outputTokens: Schema.optional(Schema.Number),
|
|
271
|
+
cacheReadTokens: Schema.optional(Schema.Number),
|
|
272
|
+
cacheWriteTokens: Schema.optional(Schema.Number),
|
|
273
|
+
model: Schema.optional(Schema.String),
|
|
274
|
+
estimatedUsd: Schema.optional(Schema.Number)
|
|
275
|
+
}))
|
|
276
|
+
});
|
|
277
|
+
const decode = Schema.decodeUnknownEither(QuotaHistoryEventV1Schema, { onExcessProperty: "error" });
|
|
278
|
+
/** Strict decode: unknown (possibly secret-bearing) fields are rejected, not silently kept. */
|
|
279
|
+
function decodeHistoryEvent(input) {
|
|
280
|
+
return decode(input);
|
|
281
|
+
}
|
|
282
|
+
const validPercent = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 100;
|
|
283
|
+
const minuteIso = (ms) => (/* @__PURE__ */ new Date(Math.round(ms / 6e4) * 6e4)).toISOString();
|
|
284
|
+
function makeId(parts) {
|
|
285
|
+
return sha256Hex(parts.map((part) => part === null ? "\0" : String(part)).join(""));
|
|
286
|
+
}
|
|
287
|
+
/** Pure normalization: one event per measurable window, or one sanitized unavailable event. */
|
|
288
|
+
function historyEventsFromSnapshot(snapshot, options) {
|
|
289
|
+
const observedMs = Date.parse(snapshot.fetchedAt);
|
|
290
|
+
if (!Number.isFinite(observedMs)) return [];
|
|
291
|
+
const observedAt = new Date(observedMs).toISOString();
|
|
292
|
+
const base = {
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
observedAt,
|
|
295
|
+
recordedAt: options.recordedAt,
|
|
296
|
+
provider: snapshot.provider,
|
|
297
|
+
accountScope: options.accountScope,
|
|
298
|
+
deviceId: options.deviceId,
|
|
299
|
+
source: snapshot.source
|
|
300
|
+
};
|
|
301
|
+
const measurable = snapshot.windows.filter((w) => validPercent(w.usedPercent));
|
|
302
|
+
if (!(snapshot.status === "ok" || snapshot.status === "partial") || measurable.length === 0) {
|
|
303
|
+
const state = snapshot.status === "unauthenticated" ? "unauthenticated" : "error";
|
|
304
|
+
const errorCode = snapshot.errorCode ?? snapshot.status.toUpperCase();
|
|
305
|
+
return [{
|
|
306
|
+
...base,
|
|
307
|
+
kind: "quota_unavailable",
|
|
308
|
+
eventId: makeId([
|
|
309
|
+
"quota_unavailable",
|
|
310
|
+
snapshot.provider,
|
|
311
|
+
options.accountScope,
|
|
312
|
+
options.deviceId,
|
|
313
|
+
observedAt,
|
|
314
|
+
state,
|
|
315
|
+
errorCode
|
|
316
|
+
]),
|
|
317
|
+
windowId: "*",
|
|
318
|
+
epochId: null,
|
|
319
|
+
semantics: "unknown",
|
|
320
|
+
startAt: null,
|
|
321
|
+
resetAt: null,
|
|
322
|
+
usedPercent: null,
|
|
323
|
+
remainingPercent: null,
|
|
324
|
+
state,
|
|
325
|
+
errorCode
|
|
326
|
+
}];
|
|
327
|
+
}
|
|
328
|
+
const state = options.stale ? "stale" : snapshot.status === "partial" ? "partial" : "ok";
|
|
329
|
+
return measurable.map((window) => {
|
|
330
|
+
const profile = windowProfile(snapshot.provider, window.windowId);
|
|
331
|
+
const semantics = options.semanticsForWindow?.(window.windowId) ?? profile.semantics;
|
|
332
|
+
const resetMs = window.resetAt === null ? NaN : Date.parse(window.resetAt);
|
|
333
|
+
const hasReset = Number.isFinite(resetMs);
|
|
334
|
+
const epochId = hasReset ? minuteIso(resetMs) : null;
|
|
335
|
+
const providedStart = options.startForWindow?.(window.windowId) ?? null;
|
|
336
|
+
const startAt = providedStart !== null && Number.isFinite(Date.parse(providedStart)) ? new Date(providedStart).toISOString() : hasReset && profile.durationMs !== null ? new Date(resetMs - profile.durationMs).toISOString() : null;
|
|
337
|
+
const usedPercent = window.usedPercent;
|
|
338
|
+
const remaining = validPercent(window.remainingPercent) ? window.remainingPercent : Math.max(0, Math.min(100, 100 - usedPercent));
|
|
339
|
+
return {
|
|
340
|
+
...base,
|
|
341
|
+
kind: "quota_observed",
|
|
342
|
+
eventId: makeId([
|
|
343
|
+
"quota_observed",
|
|
344
|
+
snapshot.provider,
|
|
345
|
+
options.accountScope,
|
|
346
|
+
options.deviceId,
|
|
347
|
+
window.windowId,
|
|
348
|
+
epochId,
|
|
349
|
+
observedAt,
|
|
350
|
+
usedPercent,
|
|
351
|
+
state
|
|
352
|
+
]),
|
|
353
|
+
windowId: window.windowId,
|
|
354
|
+
epochId,
|
|
355
|
+
semantics,
|
|
356
|
+
startAt,
|
|
357
|
+
resetAt: hasReset ? new Date(resetMs).toISOString() : null,
|
|
358
|
+
usedPercent,
|
|
359
|
+
remainingPercent: remaining,
|
|
360
|
+
state,
|
|
361
|
+
errorCode: snapshot.status === "partial" ? snapshot.errorCode : null
|
|
362
|
+
};
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const H = 36e5;
|
|
366
|
+
const EPOCH_TOLERANCE_MS = 6e5;
|
|
367
|
+
const DECREASE_TOLERANCE = .5;
|
|
368
|
+
const GAP_CAP_HOURS = 6;
|
|
369
|
+
const RECENT_HOURS = 24;
|
|
370
|
+
const STALE_LATEST_MS = 6 * H;
|
|
371
|
+
const SLOPE_EPSILON = 1e-6;
|
|
372
|
+
const LEVELS = [
|
|
373
|
+
"low",
|
|
374
|
+
"medium",
|
|
375
|
+
"high"
|
|
376
|
+
];
|
|
377
|
+
const ms = (iso) => iso === null ? NaN : Date.parse(iso);
|
|
378
|
+
const boundary = (e) => {
|
|
379
|
+
const reset = ms(e.resetAt);
|
|
380
|
+
return Number.isFinite(reset) ? reset : ms(e.epochId);
|
|
381
|
+
};
|
|
382
|
+
function isMeasurable(e) {
|
|
383
|
+
return e.kind === "quota_observed" && (e.state === "ok" || e.state === "partial" || e.state === "stale") && e.usedPercent !== null && Number.isFinite(e.usedPercent) && Number.isFinite(ms(e.observedAt));
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Reset/epoch detection. A new segment starts when the provider-reported reset boundary moves
|
|
387
|
+
* (beyond jitter tolerance) or when used% drops (credit / unannounced reset). Fixed windows only
|
|
388
|
+
* ever compare points inside one segment.
|
|
389
|
+
*/
|
|
390
|
+
function detectEpochs(events) {
|
|
391
|
+
const sorted = events.filter(isMeasurable).map((e, i) => ({
|
|
392
|
+
e,
|
|
393
|
+
i
|
|
394
|
+
})).sort((a, b) => ms(a.e.observedAt) - ms(b.e.observedAt) || a.i - b.i).map(({ e }) => e);
|
|
395
|
+
const segments = [];
|
|
396
|
+
let lastTime = NaN;
|
|
397
|
+
for (const event of sorted) {
|
|
398
|
+
const time = ms(event.observedAt);
|
|
399
|
+
if (time === lastTime) continue;
|
|
400
|
+
lastTime = time;
|
|
401
|
+
const current = segments[segments.length - 1];
|
|
402
|
+
const prev = current?.events[current.events.length - 1];
|
|
403
|
+
if (!current || !prev) {
|
|
404
|
+
segments.push({
|
|
405
|
+
events: [event],
|
|
406
|
+
reason: null
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const a = boundary(prev);
|
|
411
|
+
const b = boundary(event);
|
|
412
|
+
if (Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) > EPOCH_TOLERANCE_MS) segments.push({
|
|
413
|
+
events: [event],
|
|
414
|
+
reason: "EPOCH_RESET_DETECTED"
|
|
415
|
+
});
|
|
416
|
+
else if (event.usedPercent < prev.usedPercent - DECREASE_TOLERANCE) segments.push({
|
|
417
|
+
events: [event],
|
|
418
|
+
reason: "USAGE_DECREASE_SPLIT"
|
|
419
|
+
});
|
|
420
|
+
else current.events.push(event);
|
|
421
|
+
}
|
|
422
|
+
return segments;
|
|
423
|
+
}
|
|
424
|
+
function weightedMedian(items) {
|
|
425
|
+
const sorted = [...items].sort((a, b) => a.value - b.value);
|
|
426
|
+
const total = sorted.reduce((sum, item) => sum + item.weight, 0);
|
|
427
|
+
let acc = 0;
|
|
428
|
+
for (const item of sorted) {
|
|
429
|
+
acc += item.weight;
|
|
430
|
+
if (acc >= total / 2) return item.value;
|
|
431
|
+
}
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
const unsupported = (reasons) => ({
|
|
435
|
+
slopePercentPerHour: null,
|
|
436
|
+
exhaustedAt: null,
|
|
437
|
+
projectedUsedAtResetPercent: null,
|
|
438
|
+
sampleCount: 0,
|
|
439
|
+
observedSpanHours: 0,
|
|
440
|
+
quality: "unsupported",
|
|
441
|
+
reasons
|
|
442
|
+
});
|
|
443
|
+
function forecastQuota(input) {
|
|
444
|
+
const segments = detectEpochs(input.observations);
|
|
445
|
+
const last = segments[segments.length - 1];
|
|
446
|
+
if (!last) return {
|
|
447
|
+
...unsupported([]),
|
|
448
|
+
quality: "insufficient",
|
|
449
|
+
reasons: ["NO_OBSERVATIONS"]
|
|
450
|
+
};
|
|
451
|
+
const latest = last.events[last.events.length - 1];
|
|
452
|
+
if (latest.semantics === "rolling") return unsupported(["ROLLING_WINDOW_UNMODELED"]);
|
|
453
|
+
if (latest.semantics === "unknown") return unsupported(["WINDOW_SEMANTICS_UNKNOWN"]);
|
|
454
|
+
const reasons = [];
|
|
455
|
+
if (last.reason) reasons.push(last.reason);
|
|
456
|
+
let samples = last.events;
|
|
457
|
+
const latestMs = ms(latest.observedAt);
|
|
458
|
+
if (input.mode === "recent-activity") samples = samples.filter((e) => ms(e.observedAt) >= latestMs - RECENT_HOURS * H);
|
|
459
|
+
const first = samples[0];
|
|
460
|
+
const spanHours = (latestMs - ms(first.observedAt)) / H;
|
|
461
|
+
const used = latest.usedPercent;
|
|
462
|
+
const base = {
|
|
463
|
+
sampleCount: samples.length,
|
|
464
|
+
observedSpanHours: spanHours
|
|
465
|
+
};
|
|
466
|
+
if (samples.length < 3 || spanHours < 2) return {
|
|
467
|
+
...base,
|
|
468
|
+
slopePercentPerHour: null,
|
|
469
|
+
exhaustedAt: used >= 100 ? latest.observedAt : null,
|
|
470
|
+
projectedUsedAtResetPercent: null,
|
|
471
|
+
quality: "insufficient",
|
|
472
|
+
reasons: [
|
|
473
|
+
...reasons,
|
|
474
|
+
"INSUFFICIENT_DATA",
|
|
475
|
+
...used >= 100 ? ["ALREADY_EXHAUSTED"] : []
|
|
476
|
+
]
|
|
477
|
+
};
|
|
478
|
+
const intervals = [];
|
|
479
|
+
let changes = 0;
|
|
480
|
+
for (let i = 1; i < samples.length; i++) {
|
|
481
|
+
const dt = (ms(samples[i].observedAt) - ms(samples[i - 1].observedAt)) / H;
|
|
482
|
+
const du = samples[i].usedPercent - samples[i - 1].usedPercent;
|
|
483
|
+
if (dt <= 0 || du < 0) continue;
|
|
484
|
+
if (du > 0) changes++;
|
|
485
|
+
intervals.push({
|
|
486
|
+
value: du / dt,
|
|
487
|
+
weight: Math.min(dt, GAP_CAP_HOURS)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
const slope = input.mode === "all-window" ? (used - first.usedPercent) / spanHours : weightedMedian(intervals);
|
|
491
|
+
const positive = intervals.filter((i) => i.value > 0).map((i) => i.value);
|
|
492
|
+
const mean = positive.reduce((a, b) => a + b, 0) / (positive.length || 1);
|
|
493
|
+
const variance = positive.reduce((a, b) => a + (b - mean) ** 2, 0) / (positive.length || 1);
|
|
494
|
+
const lowVariability = mean > 0 && Math.sqrt(variance) / mean < .5;
|
|
495
|
+
let level = 0;
|
|
496
|
+
if (spanHours >= 24 && used >= 10) level = 1;
|
|
497
|
+
if (spanHours >= 72 && changes >= 20 && lowVariability && used >= 10) level = 2;
|
|
498
|
+
else if (spanHours >= 24 && changes < 6) level = 0;
|
|
499
|
+
if (samples.some((s) => s.state === "stale")) {
|
|
500
|
+
reasons.push("STALE_INPUT");
|
|
501
|
+
level = Math.max(0, level - 1);
|
|
502
|
+
}
|
|
503
|
+
if (input.now.getTime() - latestMs > STALE_LATEST_MS) {
|
|
504
|
+
reasons.push("LATEST_SAMPLE_OLD");
|
|
505
|
+
level = Math.max(0, level - 1);
|
|
506
|
+
}
|
|
507
|
+
const quality = LEVELS[level];
|
|
508
|
+
const resetMs = input.resetAt ? input.resetAt.getTime() : boundary(latest);
|
|
509
|
+
const hasReset = Number.isFinite(resetMs);
|
|
510
|
+
const roundedSlope = Math.max(0, slope);
|
|
511
|
+
let exhaustedAt = null;
|
|
512
|
+
let projected = null;
|
|
513
|
+
if (used >= 100) {
|
|
514
|
+
exhaustedAt = latest.observedAt;
|
|
515
|
+
projected = 100;
|
|
516
|
+
reasons.push("ALREADY_EXHAUSTED");
|
|
517
|
+
} else if (roundedSlope <= SLOPE_EPSILON) {
|
|
518
|
+
reasons.push("STABLE_AT_CURRENT_ACTIVITY");
|
|
519
|
+
projected = used;
|
|
520
|
+
} else {
|
|
521
|
+
const exhaustMs = latestMs + (100 - used) / roundedSlope * H;
|
|
522
|
+
projected = Math.min(100, used + roundedSlope * (hasReset ? Math.max(0, resetMs - latestMs) / H : 0));
|
|
523
|
+
if (!hasReset || exhaustMs <= resetMs) {
|
|
524
|
+
exhaustedAt = new Date(exhaustMs).toISOString();
|
|
525
|
+
reasons.push("EXHAUSTION_BEFORE_RESET");
|
|
526
|
+
} else reasons.push("ON_PACE");
|
|
527
|
+
if (!hasReset) projected = null;
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
...base,
|
|
531
|
+
slopePercentPerHour: roundedSlope,
|
|
532
|
+
exhaustedAt,
|
|
533
|
+
projectedUsedAtResetPercent: projected,
|
|
534
|
+
quality,
|
|
535
|
+
reasons
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
/** Account-safe wrapper: only events sharing the newest event's accountScope are considered. */
|
|
539
|
+
function buildForecastV1(input) {
|
|
540
|
+
const scoped = input.observations.filter((e) => e.provider === input.provider && e.windowId === input.windowId && isMeasurable(e)).sort((a, b) => ms(a.observedAt) - ms(b.observedAt));
|
|
541
|
+
const newest = scoped[scoped.length - 1];
|
|
542
|
+
const events = newest ? scoped.filter((e) => e.accountScope === newest.accountScope) : [];
|
|
543
|
+
const result = forecastQuota({
|
|
544
|
+
now: input.now,
|
|
545
|
+
resetAt: null,
|
|
546
|
+
observations: events,
|
|
547
|
+
mode: input.mode
|
|
548
|
+
});
|
|
549
|
+
const latest = events[events.length - 1];
|
|
550
|
+
const pace = latest ? calculatePace({
|
|
551
|
+
now: input.now,
|
|
552
|
+
startAt: latest.startAt ? new Date(latest.startAt) : null,
|
|
553
|
+
resetAt: latest.resetAt ? new Date(latest.resetAt) : null,
|
|
554
|
+
usedPercent: latest.usedPercent,
|
|
555
|
+
semantics: latest.semantics,
|
|
556
|
+
startSource: latest.startAt ? "inferred" : "unknown"
|
|
557
|
+
}) : null;
|
|
558
|
+
return {
|
|
559
|
+
schemaVersion: 1,
|
|
560
|
+
provider: input.provider,
|
|
561
|
+
windowId: input.windowId,
|
|
562
|
+
computedAt: input.now.toISOString(),
|
|
563
|
+
currentUsedPercent: latest ? latest.usedPercent : null,
|
|
564
|
+
idealUsedPercent: pace ? pace.idealUsedPercent : null,
|
|
565
|
+
observedSpanHours: result.observedSpanHours,
|
|
566
|
+
sampleCount: result.sampleCount,
|
|
567
|
+
slopePercentPerHour: result.slopePercentPerHour,
|
|
568
|
+
exhaustedAt: result.exhaustedAt,
|
|
569
|
+
projectedUsedAtResetPercent: result.projectedUsedAtResetPercent,
|
|
570
|
+
expectedTimeToLimitHours: result.exhaustedAt ? Math.max(0, (Date.parse(result.exhaustedAt) - input.now.getTime()) / H) : null,
|
|
571
|
+
mode: input.mode,
|
|
572
|
+
quality: result.quality,
|
|
573
|
+
reasons: [...result.reasons]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
export { makeObservation as a, windowProfile as c, historyEventsFromSnapshot as i, calculatePace as n, makeUnavailableSnapshot as o, decodeHistoryEvent as r, normalizeWindow as s, buildForecastV1 as t };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { c as QuotaHistoryEventV1 } from "./index-NHzh2TGn.mjs";
|
|
2
|
+
interface ReadOptions {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
provider?: string;
|
|
6
|
+
windowId?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ReadResult {
|
|
9
|
+
events: QuotaHistoryEventV1[];
|
|
10
|
+
/** Lines that were truncated, not JSON, or failed strict schema validation. */
|
|
11
|
+
invalidLines: number;
|
|
12
|
+
}
|
|
13
|
+
interface JournalStore {
|
|
14
|
+
append(events: readonly QuotaHistoryEventV1[]): Promise<{
|
|
15
|
+
written: number;
|
|
16
|
+
duplicates: number;
|
|
17
|
+
}>;
|
|
18
|
+
read(options: ReadOptions): AsyncIterable<QuotaHistoryEventV1>;
|
|
19
|
+
readAll(options: ReadOptions): Promise<ReadResult>;
|
|
20
|
+
/** Deletes whole UTC-day partitions older than `retentionDays`. Returns files removed. */
|
|
21
|
+
applyRetention(now?: Date): Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
interface JournalOptions {
|
|
24
|
+
directory: string;
|
|
25
|
+
/** Rotate to `events-DATE.N.jsonl` beyond this size. Default 16 MiB. */
|
|
26
|
+
maxFileBytes?: number;
|
|
27
|
+
/** Default 400 days. */
|
|
28
|
+
retentionDays?: number;
|
|
29
|
+
/** fsync after each append batch. Default true. */
|
|
30
|
+
fsync?: boolean;
|
|
31
|
+
lockStaleMs?: number;
|
|
32
|
+
lockTimeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
declare function createLocalJsonlJournal(options: JournalOptions): JournalStore;
|
|
35
|
+
declare function defaultHistoryDirectory(env?: NodeJS.ProcessEnv, platform?: string): string;
|
|
36
|
+
export { createLocalJsonlJournal as a, ReadResult as i, JournalStore as n, defaultHistoryDirectory as o, ReadOptions as r, JournalOptions as t };
|