mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
refactor(chatCore): extrai núcleo puro de persistCodexQuotaState (#3501)
Move a construção do payload de persistCodexQuotaState para o novo leaf puro open-sse/handlers/chatCore/codexQuota.ts (buildCodexQuotaPersistence): parseia os headers de quota do Codex no snapshot codexQuotaState, faz passthrough do providerSpecificData existente e, num 429 cuja janela (dual-window) passou do threshold de exaustão, registra o cooldown por escopo (codexScopeRateLimitedUntil) + codexExhaustedWindow, retornando a mensagem de debug-log. O handler mantém as partes impuras byte-idênticas e na mesma ordem: emitir o exhaustionLog retornado, invalidateCodexQuotaCache em todo 429 (connectionId é garantido pelo early-return), depois updateProviderConnection + mutação de credentials. Imports órfãos (parseCodexQuotaHeaders/getCodexModelScope/ getCodexDualWindowCooldownMs) migram para o leaf; isCompactResponsesEndpoint permanece (usado no site de passthrough). chatCore.ts 5055->5019 (shrink -36); baseline file-size ratchetado. complexity 1905=1905 (neutro). Coberto por tests/unit/chatcore-codex-quota.test.ts (5 casos: sem-headers null, snapshot+passthrough, 429 cooldown+janela+log, merge do scope-map, 429 abaixo-do-threshold no-op).
This commit is contained in:
@@ -206,12 +206,8 @@ import {
|
||||
shouldDetectLimit,
|
||||
} from "../services/toolLimitDetector.ts";
|
||||
|
||||
import {
|
||||
parseCodexQuotaHeaders,
|
||||
getCodexModelScope,
|
||||
getCodexDualWindowCooldownMs,
|
||||
isCompactResponsesEndpoint,
|
||||
} from "../executors/codex.ts";
|
||||
import { isCompactResponsesEndpoint } from "../executors/codex.ts";
|
||||
import { buildCodexQuotaPersistence } from "./chatCore/codexQuota.ts";
|
||||
import { invalidateCodexQuotaCache } from "../services/codexQuotaFetcher.ts";
|
||||
import { translateNonStreamingResponse } from "./responseTranslator.ts";
|
||||
import { extractUsageFromResponse } from "./usageExtractor.ts";
|
||||
@@ -826,67 +822,35 @@ export async function handleChatCore({
|
||||
if (provider !== "codex" || !connectionId || !headers) return;
|
||||
|
||||
try {
|
||||
const quota = parseCodexQuotaHeaders(headers);
|
||||
if (!quota) return;
|
||||
|
||||
const existingProviderData =
|
||||
credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object"
|
||||
? credentials.providerSpecificData
|
||||
? (credentials.providerSpecificData as Record<string, unknown>)
|
||||
: {};
|
||||
const scope = getCodexModelScope(model || requestedModel || "");
|
||||
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(),
|
||||
};
|
||||
// Pure payload build extracted to chatCore/codexQuota.ts (#3501). Returns null when the
|
||||
// response carries no quota headers (nothing to persist).
|
||||
const built = buildCodexQuotaPersistence({
|
||||
headers,
|
||||
existingProviderData,
|
||||
modelForScope: model || requestedModel || "",
|
||||
status,
|
||||
});
|
||||
if (!built) return;
|
||||
|
||||
const nextProviderData: Record<string, unknown> = {
|
||||
...existingProviderData,
|
||||
codexQuotaState: quotaState,
|
||||
};
|
||||
if (built.exhaustionLog) {
|
||||
log?.debug?.("CODEX", built.exhaustionLog);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Invalidate the preflight cache for this connection so the next
|
||||
// isModelAvailable check fetches fresh quota data.
|
||||
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;
|
||||
log?.debug?.(
|
||||
"CODEX",
|
||||
`Quota exhaustion on ${exhaustedWindow} window, cooldown until ${scopeUntil}`
|
||||
);
|
||||
}
|
||||
|
||||
// Invalidate the preflight cache for this connection so the next
|
||||
// isModelAvailable check fetches fresh quota data.
|
||||
if (connectionId) {
|
||||
invalidateCodexQuotaCache(connectionId);
|
||||
}
|
||||
invalidateCodexQuotaCache(connectionId);
|
||||
}
|
||||
|
||||
await updateProviderConnection(connectionId, {
|
||||
providerSpecificData: nextProviderData,
|
||||
providerSpecificData: built.nextProviderData,
|
||||
});
|
||||
|
||||
credentials.providerSpecificData = nextProviderData;
|
||||
credentials.providerSpecificData = built.nextProviderData;
|
||||
} catch (err) {
|
||||
const errMessage = err instanceof Error ? err.message : String(err);
|
||||
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
|
||||
|
||||
85
open-sse/handlers/chatCore/codexQuota.ts
Normal file
85
open-sse/handlers/chatCore/codexQuota.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
106
tests/unit/chatcore-codex-quota.test.ts
Normal file
106
tests/unit/chatcore-codex-quota.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
// tests/unit/chatcore-codex-quota.test.ts
|
||||
// Characterization of buildCodexQuotaPersistence — the pure core of handleChatCore's
|
||||
// persistCodexQuotaState, extracted during the chatCore god-file decomposition (#3501). Locks the
|
||||
// shape of the persisted providerSpecificData: the codexQuotaState snapshot, the existing-data
|
||||
// passthrough, and the 429 dual-window exhaustion fields (codexScopeRateLimitedUntil /
|
||||
// codexExhaustedWindow) plus the debug-log message. The handler keeps the DB write, the
|
||||
// preflight-cache invalidation, and the log emission; this function only builds the data.
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildCodexQuotaPersistence } from "../../open-sse/handlers/chatCore/codexQuota.ts";
|
||||
import { getCodexModelScope } from "../../open-sse/executors/codex.ts";
|
||||
|
||||
const MODEL = "gpt-5-codex";
|
||||
const ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
||||
|
||||
function quotaHeaders(over: Record<string, string> = {}) {
|
||||
return {
|
||||
"x-codex-5h-usage": "50",
|
||||
"x-codex-5h-limit": "100",
|
||||
"x-codex-5h-reset-at": "2999-01-01T00:00:00.000Z",
|
||||
"x-codex-7d-usage": "10",
|
||||
"x-codex-7d-limit": "100",
|
||||
"x-codex-7d-reset-at": "2999-01-08T00:00:00.000Z",
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
test("returns null when the response carries no codex quota headers", () => {
|
||||
assert.equal(
|
||||
buildCodexQuotaPersistence({ headers: {}, existingProviderData: {}, modelForScope: MODEL, status: 200 }),
|
||||
null
|
||||
);
|
||||
assert.equal(
|
||||
buildCodexQuotaPersistence({ headers: { "content-type": "application/json" }, existingProviderData: {}, modelForScope: MODEL, status: 200 }),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test("builds codexQuotaState (parsed numbers + scope + updatedAt) and preserves existing provider data", () => {
|
||||
const built = buildCodexQuotaPersistence({
|
||||
headers: quotaHeaders(),
|
||||
existingProviderData: { keepMe: "yes", apiKeyHealth: { primary: {} } },
|
||||
modelForScope: MODEL,
|
||||
status: 200,
|
||||
});
|
||||
assert.ok(built);
|
||||
const qs = built.nextProviderData.codexQuotaState as Record<string, unknown>;
|
||||
assert.equal(qs.usage5h, 50);
|
||||
assert.equal(qs.limit5h, 100);
|
||||
assert.equal(qs.usage7d, 10);
|
||||
assert.equal(qs.limit7d, 100);
|
||||
assert.equal(qs.scope, getCodexModelScope(MODEL));
|
||||
assert.match(String(qs.updatedAt), ISO);
|
||||
// existing keys passed through, not dropped
|
||||
assert.equal(built.nextProviderData.keepMe, "yes");
|
||||
assert.deepEqual(built.nextProviderData.apiKeyHealth, { primary: {} });
|
||||
// non-429 → no exhaustion fields, no log
|
||||
assert.equal(built.exhaustionLog, null);
|
||||
assert.equal(built.nextProviderData.codexScopeRateLimitedUntil, undefined);
|
||||
assert.equal(built.nextProviderData.codexExhaustedWindow, undefined);
|
||||
});
|
||||
|
||||
test("429 with a near-exhausted 5h window records the per-scope cooldown + window + log", () => {
|
||||
const built = buildCodexQuotaPersistence({
|
||||
headers: quotaHeaders({ "x-codex-5h-usage": "100" }), // ratio 1.0 >= 0.95, reset far in the future
|
||||
existingProviderData: {},
|
||||
modelForScope: MODEL,
|
||||
status: 429,
|
||||
});
|
||||
assert.ok(built);
|
||||
assert.equal(built.nextProviderData.codexExhaustedWindow, "5h");
|
||||
const scope = getCodexModelScope(MODEL);
|
||||
const scopeMap = built.nextProviderData.codexScopeRateLimitedUntil as Record<string, string>;
|
||||
assert.ok(scopeMap[scope]?.startsWith("2999-01-01T00:00:00"));
|
||||
assert.match(
|
||||
String(built.exhaustionLog),
|
||||
/^Quota exhaustion on 5h window, cooldown until 2999-01-01T00:00:00/
|
||||
);
|
||||
});
|
||||
|
||||
test("429 merges into an existing codexScopeRateLimitedUntil map without dropping other scopes", () => {
|
||||
const built = buildCodexQuotaPersistence({
|
||||
headers: quotaHeaders({ "x-codex-5h-usage": "100" }),
|
||||
existingProviderData: { codexScopeRateLimitedUntil: { "other-scope": "2999-12-31T00:00:00.000Z" } },
|
||||
modelForScope: MODEL,
|
||||
status: 429,
|
||||
});
|
||||
assert.ok(built);
|
||||
const scopeMap = built.nextProviderData.codexScopeRateLimitedUntil as Record<string, string>;
|
||||
assert.equal(scopeMap["other-scope"], "2999-12-31T00:00:00.000Z");
|
||||
assert.ok(scopeMap[getCodexModelScope(MODEL)]);
|
||||
});
|
||||
|
||||
test("429 below the exhaustion threshold builds the snapshot but no cooldown / no log", () => {
|
||||
const built = buildCodexQuotaPersistence({
|
||||
headers: quotaHeaders({ "x-codex-5h-usage": "1", "x-codex-7d-usage": "1" }), // ratios well under 0.95
|
||||
existingProviderData: {},
|
||||
modelForScope: MODEL,
|
||||
status: 429,
|
||||
});
|
||||
assert.ok(built);
|
||||
assert.ok(built.nextProviderData.codexQuotaState);
|
||||
assert.equal(built.exhaustionLog, null);
|
||||
assert.equal(built.nextProviderData.codexScopeRateLimitedUntil, undefined);
|
||||
assert.equal(built.nextProviderData.codexExhaustedWindow, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user