Skip to content

Commit 95ca002

Browse files
committed
Improve quota token estimation accuracy
1 parent 2a8ab3a commit 95ca002

3 files changed

Lines changed: 104 additions & 20 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ out/
99
build/installer-sidebar.bmp
1010
Thumbs.db
1111
.DS_Store
12+
.vscode/

scripts/validate-quota-estimate.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ function quotaSpeedMultiplier(model, serviceTier = null) {
8080
const value = String(model || "").toLowerCase();
8181
const tier = String(serviceTier || "").toLowerCase();
8282
const isFast =
83-
/(^|[-_\s])fast($|[-_\s])|high[-_\s]?speed|speedy/.test(value) ||
83+
/(^|[-_\s])fast($|[-_\s])|high[-_\s]?speed|speedy|turbo|accelerated/.test(value) ||
8484
tier === "fast" ||
85-
tier === "priority";
85+
tier === "priority" ||
86+
tier === "turbo";
8687
return isFast ? codexRateCard(model).fastMultiplier : 1;
8788
}
8889

src/main.js

Lines changed: 100 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,9 +1545,10 @@ function quotaSpeedMultiplier(model, serviceTier = null) {
15451545
const value = String(model || "").toLowerCase();
15461546
const tier = String(serviceTier || "").toLowerCase();
15471547
const isFast =
1548-
/(^|[-_\s])fast($|[-_\s])|high[-_\s]?speed|speedy/.test(value) ||
1548+
/(^|[-_\s])fast($|[-_\s])|high[-_\s]?speed|speedy|turbo|accelerated/.test(value) ||
15491549
tier === "fast" ||
1550-
tier === "priority";
1550+
tier === "priority" ||
1551+
tier === "turbo";
15511552
return isFast ? codexRateCard(model).fastMultiplier : 1;
15521553
}
15531554

@@ -1923,6 +1924,7 @@ async function readLatestSqliteRateLimitQuota(options = {}) {
19231924
const effectiveSinceSeconds = Number.isFinite(sinceMs)
19241925
? Math.floor(sinceMs / 1000)
19251926
: Math.floor(Date.now() / 1000) - 14 * 24 * 60 * 60;
1927+
const accountFilter = normalizeAccountFilter(options.accountIdentity);
19261928
if (!(await pathExists(logsDbPath()))) return null;
19271929
let DatabaseSync;
19281930
try {
@@ -1943,7 +1945,7 @@ async function readLatestSqliteRateLimitQuota(options = {}) {
19431945
limit 1000`
19441946
)
19451947
.all(effectiveSinceSeconds);
1946-
for (const row of rows) {
1948+
for (const row of filterRowsByAccount(rows, accountFilter)) {
19471949
const quota = quotaFromCodexRateLimitsMessage(extractCodexLogMessage(row.feedback_log_body), row.ts);
19481950
if (quota) return quota;
19491951
}
@@ -1964,6 +1966,7 @@ async function readLatestUsageLimitQuota(options = {}) {
19641966
const effectiveSinceSeconds = Number.isFinite(sinceMs)
19651967
? Math.floor(sinceMs / 1000)
19661968
: Math.floor(Date.now() / 1000) - 14 * 24 * 60 * 60;
1969+
const accountFilter = normalizeAccountFilter(options.accountIdentity);
19671970
if (!(await pathExists(logsDbPath()))) return null;
19681971
let DatabaseSync;
19691972
try {
@@ -1984,7 +1987,7 @@ async function readLatestUsageLimitQuota(options = {}) {
19841987
limit 1000`
19851988
)
19861989
.all(effectiveSinceSeconds);
1987-
for (const row of rows) {
1990+
for (const row of filterRowsByAccount(rows, accountFilter)) {
19881991
const quota = quotaFromUsageLimitMessage(extractCodexLogMessage(row.feedback_log_body), row.ts);
19891992
if (quota) return quota;
19901993
}
@@ -2144,6 +2147,10 @@ async function parseLatestRateLimitFile(file, sinceMs) {
21442147
}
21452148
if (entry.type !== "event_msg" || entry.payload?.type !== "token_count") continue;
21462149
if (!entry.payload?.rate_limits) continue;
2150+
serviceTier =
2151+
entry.payload?.service_tier ?? entry.payload?.serviceTier ??
2152+
entry.payload?.rate_limits?.service_tier ?? entry.payload?.rate_limits?.serviceTier ??
2153+
serviceTier;
21472154
const timestamp = entry.timestamp ?? new Date(file.mtimeMs).toISOString();
21482155
const eventMs = new Date(timestamp).getTime();
21492156
const info = entry.payload?.info ?? {};
@@ -2221,14 +2228,18 @@ async function parseQuotaEventFile(file) {
22212228
const ms = dateMs(timestamp);
22222229
if (!Number.isFinite(ms)) continue;
22232230
const info = entry.payload?.info ?? {};
2231+
const eventServiceTier =
2232+
entry.payload?.service_tier ?? entry.payload?.serviceTier ??
2233+
entry.payload?.rate_limits?.service_tier ?? entry.payload?.rate_limits?.serviceTier ??
2234+
serviceTier;
22242235
events.push({
22252236
filePath: file.path,
22262237
sessionId: sessionId ?? file.path,
22272238
startedAtMs,
22282239
timestamp,
22292240
ms,
22302241
model,
2231-
serviceTier,
2242+
serviceTier: eventServiceTier,
22322243
tokenUsage: normalizeTokenUsage(info.total_token_usage ?? info.totalTokenUsage),
22332244
rateLimits: entry.payload?.rate_limits ?? null,
22342245
});
@@ -2266,6 +2277,45 @@ function numberField(fields, key) {
22662277
return Number.isFinite(value) ? value : 0;
22672278
}
22682279

2280+
function normalizeAccountFilter(identity) {
2281+
const ids = new Set();
2282+
const emails = new Set();
2283+
for (const value of [identity?.userId, identity?.accountUserId, identity?.chatgptUserId, identity?.subject]) {
2284+
const text = typeof value === "string" ? value.trim().toLowerCase() : "";
2285+
if (text) ids.add(text);
2286+
}
2287+
const email = typeof identity?.email === "string" ? identity.email.trim().toLowerCase() : "";
2288+
if (email) emails.add(email);
2289+
return ids.size || emails.size ? { ids, emails } : null;
2290+
}
2291+
2292+
function responseEventAccountMatch(event, accountFilter) {
2293+
if (!accountFilter) return "match";
2294+
const accountId = typeof event?.accountId === "string" ? event.accountId.trim().toLowerCase() : "";
2295+
const email = typeof event?.email === "string" ? event.email.trim().toLowerCase() : "";
2296+
if (!accountId && !email) return "unknown";
2297+
if (accountId && accountFilter.ids.has(accountId)) return "match";
2298+
if (email && accountFilter.emails.has(email)) return "match";
2299+
return "mismatch";
2300+
}
2301+
2302+
function filterRowsByAccount(rows, accountFilter) {
2303+
if (!accountFilter) return rows;
2304+
const parsed = rows.map((row) => {
2305+
const fields = parseLogKeyValues(row?.feedback_log_body);
2306+
return {
2307+
row,
2308+
accountId: fields["user.account_id"] || null,
2309+
email: fields["user.email"] || null,
2310+
};
2311+
});
2312+
const hasTaggedRows = parsed.some((entry) => entry.accountId || entry.email);
2313+
if (!hasTaggedRows) return rows;
2314+
return parsed
2315+
.filter((entry) => responseEventAccountMatch(entry, accountFilter) === "match")
2316+
.map((entry) => entry.row);
2317+
}
2318+
22692319
function responseCompletedEventFromLogRow(row) {
22702320
const message = extractCodexLogMessage(row?.feedback_log_body);
22712321
if (message?.type === "response.completed" && message.response?.usage) {
@@ -2296,6 +2346,8 @@ function responseCompletedEventFromLogRow(row) {
22962346
serviceTier,
22972347
tokenUsage,
22982348
signature,
2349+
accountId: null,
2350+
email: null,
22992351
};
23002352
}
23012353

@@ -2312,8 +2364,10 @@ function responseCompletedEventFromLogRow(row) {
23122364
const outputTokens = numberField(fields, "output_token_count");
23132365
const cachedInputTokens = numberField(fields, "cached_token_count");
23142366
const reasoningOutputTokens = numberField(fields, "reasoning_token_count");
2367+
const toolTokenCount = numberField(fields, "tool_token_count");
23152368
const totalTokens = Math.max(0, inputTokens + outputTokens);
2316-
if (totalTokens <= 0) return null;
2369+
const eventTotalTokens = totalTokens > 0 ? totalTokens : toolTokenCount;
2370+
if (eventTotalTokens <= 0) return null;
23172371

23182372
const conversationId = fields["conversation.id"] || row?.thread_id || `sqlite-row-${row?.id ?? ms}`;
23192373
const model = fields.slug || fields.model || null;
@@ -2330,7 +2384,7 @@ function responseCompletedEventFromLogRow(row) {
23302384
cachedInputTokens,
23312385
outputTokens,
23322386
reasoningOutputTokens,
2333-
totalTokens,
2387+
totalTokens: eventTotalTokens,
23342388
},
23352389
signature: [
23362390
Math.floor(ms / 1000),
@@ -2340,6 +2394,8 @@ function responseCompletedEventFromLogRow(row) {
23402394
cachedInputTokens,
23412395
reasoningOutputTokens,
23422396
].join("|"),
2397+
accountId: fields["user.account_id"] || null,
2398+
email: fields["user.email"] || null,
23432399
};
23442400
}
23452401

@@ -2354,11 +2410,16 @@ async function readSqliteResponseCompletedEvents(options = {}) {
23542410

23552411
const sinceMs = Number.isFinite(options.sinceMs) ? options.sinceMs : Date.now() - 6 * 60 * 60 * 1000;
23562412
const sinceSeconds = Math.max(0, Math.floor((sinceMs - 5 * 60 * 1000) / 1000));
2413+
const accountFilter = normalizeAccountFilter(options.accountIdentity);
2414+
const accountCacheKey = accountFilter
2415+
? `${Array.from(accountFilter.ids).join(",")}|${Array.from(accountFilter.emails).join(",")}`
2416+
: "all";
23572417
const cacheKey = [
23582418
await fileCachePart(logsDbPath()),
23592419
await fileCachePart(logsDbWalPath()),
23602420
await fileCachePart(logsDbShmPath()),
23612421
sinceSeconds,
2422+
accountCacheKey,
23622423
].join(":");
23632424
if (sqliteResponseEventCache.has(cacheKey)) return sqliteResponseEventCache.get(cacheKey);
23642425

@@ -2382,11 +2443,24 @@ async function readSqliteResponseCompletedEvents(options = {}) {
23822443
)
23832444
.all(sinceSeconds);
23842445

2385-
const seen = new Set();
2386-
const rawEvents = [];
2446+
const parsedEvents = [];
2447+
let hasAccountTaggedEvents = false;
23872448
for (const row of rows) {
23882449
const event = responseCompletedEventFromLogRow(row);
2389-
if (!event || seen.has(event.signature)) continue;
2450+
if (!event) continue;
2451+
if (event.accountId || event.email) hasAccountTaggedEvents = true;
2452+
parsedEvents.push(event);
2453+
}
2454+
2455+
const filteredEvents =
2456+
accountFilter && hasAccountTaggedEvents
2457+
? parsedEvents.filter((event) => responseEventAccountMatch(event, accountFilter) === "match")
2458+
: parsedEvents;
2459+
2460+
const seen = new Set();
2461+
const rawEvents = [];
2462+
for (const event of filteredEvents) {
2463+
if (seen.has(event.signature)) continue;
23902464
seen.add(event.signature);
23912465
rawEvents.push(event);
23922466
}
@@ -2778,7 +2852,10 @@ async function readQuotaEstimate(options = {}) {
27782852

27792853
const sqliteSinceMs =
27802854
effectiveSinceMs && Number.isFinite(effectiveSinceMs) ? Math.min(effectiveSinceMs, earliestBaseMs) : earliestBaseMs;
2781-
const sqliteEvents = await readSqliteResponseCompletedEvents({ sinceMs: sqliteSinceMs });
2855+
const sqliteEvents = await readSqliteResponseCompletedEvents({
2856+
sinceMs: sqliteSinceMs,
2857+
accountIdentity: options.accountIdentity,
2858+
});
27822859
const events = [...sessionEvents, ...sqliteEvents];
27832860
if (!events.length) return quotaEstimateUnavailable("\u672a\u627e\u5230\u672c\u5730 token \u8bb0\u5f55");
27842861
events.sort((a, b) => a.ms - b.ms);
@@ -2855,10 +2932,13 @@ async function readQuotaEstimate(options = {}) {
28552932
sessionDelta?.weightedTokens >= 100
28562933
? buildWindowEstimate(baseQuota.session, sessionEstimateCoefficient, sessionDelta.weightedTokens, calibration.sessionSamples)
28572934
: null;
2858-
// Weekly quota moves slowly and local Codex logs normally include the latest
2859-
// weekly rate_limit snapshot. Estimating between snapshots tends to be more
2860-
// confusing than useful, so keep weekly display pinned to the observed value.
2861-
const weeklyEstimate = null;
2935+
const weeklyEstimateCoefficient = seededWeeklyDelta
2936+
? Math.min(tunedWeeklyCoefficient.coefficient, quotaCoefficientBounds(baseQuota.planType, "weekly").max)
2937+
: tunedWeeklyCoefficient.coefficient;
2938+
const weeklyEstimate =
2939+
weeklyDelta?.weightedTokens >= 500
2940+
? buildWindowEstimate(baseQuota.weekly, weeklyEstimateCoefficient, weeklyDelta.weightedTokens, calibration.weeklySamples)
2941+
: null;
28622942

28632943
if (!sessionEstimate && !weeklyEstimate) return quotaEstimateUnavailable("\u7b49\u5f85\u5386\u53f2\u6821\u51c6\u6837\u672c");
28642944
return {
@@ -3268,7 +3348,7 @@ function updateLearningWindow(existing, sample) {
32683348
Number.isFinite(actualDelta) &&
32693349
actualDelta > 0 &&
32703350
(predictedDelta > actualDelta * 2 || actualDelta > predictedDelta * 1.5);
3271-
const sampleWeight = needsFastCorrection ? 0.55 : 0.2;
3351+
const sampleWeight = needsFastCorrection ? 0.7 : 0.35;
32723352
const coefficient = Number.isFinite(previous) && previous > 0
32733353
? previous * (1 - sampleWeight) + sample.coefficient * sampleWeight
32743354
: sample.coefficient;
@@ -3357,9 +3437,9 @@ async function readBestLocalQuota(scope, files) {
33573437
return newerQuota(
33583438
newerQuota(
33593439
await readLatestLocalQuota({ since: scope.since, files }),
3360-
await readLatestSqliteRateLimitQuota({ since: scope.since })
3440+
await readLatestSqliteRateLimitQuota({ since: scope.since, accountIdentity: scope.account })
33613441
),
3362-
await readLatestUsageLimitQuota({ since: scope.since })
3442+
await readLatestUsageLimitQuota({ since: scope.since, accountIdentity: scope.account })
33633443
);
33643444
}
33653445

@@ -3381,6 +3461,7 @@ async function resolveQuotaWithMode(scope, files, usage = null) {
33813461
files,
33823462
baseQuota,
33833463
calibration: scope.accountQuotaCalibration,
3464+
accountIdentity: scope.account,
33843465
});
33853466
const fallbackQuota = attachQuotaEstimate(resolveQuota(scope, baseQuota), quotaEstimate);
33863467
return {
@@ -3397,6 +3478,7 @@ async function resolveQuotaWithMode(scope, files, usage = null) {
33973478
files,
33983479
baseQuota,
33993480
calibration: scope.accountQuotaCalibration,
3481+
accountIdentity: scope.account,
34003482
});
34013483
const resolvedQuota = attachQuotaEstimate(resolveQuota(scope, baseQuota), quotaEstimate);
34023484
if (latestQuota && scope.accountId) {

0 commit comments

Comments
 (0)