Files
2026-07-13 13:39:12 +08:00

86 lines
3.2 KiB
TypeScript

/**
* chatCore Codex quota-persistence builder (Quality Gate v2 / Fase 9 — chatCore god-file
* decomposition, #3501).
*
* Pure core of handleChatCore's persistCodexQuotaState: turns the upstream Codex quota response
* headers into the next `providerSpecificData` payload (the codexQuotaState snapshot, plus — on a
* 429 whose dual-window usage is past the exhaustion threshold — the per-scope cooldown timestamp,
* the exhausted window, and the debug-log message). The handler keeps the impure parts byte-
* identically: the DB write (updateProviderConnection), the preflight-cache invalidation on every
* 429, the credentials mutation, and emitting the returned log line.
*/
import {
parseCodexQuotaHeaders,
getCodexModelScope,
getCodexDualWindowCooldownMs,
} from "../../executors/codex.ts";
export type CodexQuotaPersistence = {
/** The merged providerSpecificData to persist (existing data + codexQuotaState [+ 429 cooldown]). */
nextProviderData: Record<string, unknown>;
/** The CODEX debug-log message to emit when a 429 exhausted a window, else null. */
exhaustionLog: string | null;
};
/**
* Build the providerSpecificData update for a Codex quota response. Returns null when the response
* carries no quota headers (nothing to persist). Pure: a function of the headers, the existing
* provider data, the model used for scope resolution, and the upstream status.
*/
export function buildCodexQuotaPersistence(opts: {
headers: Record<string, string>;
existingProviderData: Record<string, unknown>;
modelForScope: string;
status: number;
}): CodexQuotaPersistence | null {
const { headers, existingProviderData, modelForScope, status } = opts;
const quota = parseCodexQuotaHeaders(headers);
if (!quota) return null;
const scope = getCodexModelScope(modelForScope);
const quotaState = {
usage5h: quota.usage5h,
limit5h: quota.limit5h,
resetAt5h: quota.resetAt5h,
usage7d: quota.usage7d,
limit7d: quota.limit7d,
resetAt7d: quota.resetAt7d,
scope,
updatedAt: new Date().toISOString(),
};
const nextProviderData: Record<string, unknown> = {
...existingProviderData,
codexQuotaState: quotaState,
};
let exhaustionLog: string | null = null;
// T03/T09: on 429, persist exact reset time per scope to avoid global over-blocking.
// Use dual-window cooldown to distinguish short-term and weekly Codex exhaustion.
if (status === 429) {
const { cooldownMs, window: exhaustedWindow } = getCodexDualWindowCooldownMs(quota);
if (cooldownMs > 0) {
const scopeUntil = new Date(Date.now() + cooldownMs).toISOString();
const scopeMapRaw =
existingProviderData &&
typeof existingProviderData === "object" &&
existingProviderData.codexScopeRateLimitedUntil &&
typeof existingProviderData.codexScopeRateLimitedUntil === "object"
? existingProviderData.codexScopeRateLimitedUntil
: {};
nextProviderData.codexScopeRateLimitedUntil = {
...(scopeMapRaw as Record<string, unknown>),
[scope]: scopeUntil,
};
nextProviderData.codexExhaustedWindow = exhaustedWindow;
exhaustionLog = `Quota exhaustion on ${exhaustedWindow} window, cooldown until ${scopeUntil}`;
}
}
return { nextProviderData, exhaustionLog };
}