diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index e0b349c689..f8ed688993 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -469,7 +469,8 @@ "src/sse/services/auth.ts": 3450, "tests/unit/account-fallback-service.test.ts": 2453, "tests/unit/provider-validation-specialty.test.ts": 4656, - "open-sse/services/autoCombo/virtualFactory.ts": 1219 + "open-sse/services/autoCombo/virtualFactory.ts": 1219, + "open-sse/services/combo/roundRobinCombo.ts": 1205 }, "_rebaseline_base_2026_08_10_proxyfetch": "Base-red fix (green-prs sweep, issue #9985): open-sse/utils/proxyFetch.ts 1207 > cap 1000 — new proxied-TLS fetch helper introduced by the Fal reference-image work. Owner-authorized quick rebaseline to green; structural slim tracked for v3.9.0.", "_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).", @@ -656,5 +657,6 @@ "_rebaseline_2026_09_05_12671_combos_usage_guide_external_store": "combos/page.tsx 5018 -> 5066: #12671 replaces the effect-based localStorage read with useSyncExternalStore; the +48 lines are the store helpers (subscribe/getSnapshot/getServerSnapshot/emit) hoisted to module scope, which is the sanctioned shape and what let the react-hooks/set-state-in-effect suppression be dropped.", "_rebaseline_2026_09_07_chatcore_nonstreaming_regression_fixes": "Own growth: open-sse/handlers/chatCore.ts 5984->6021 (+37). Two of my own PRs on top of #12867: #12963 pins the ok variant of the non-streaming leg result in its own binding (the discriminated-union narrowing was lost across the tool-loop reassignment, 13 TS2339 under tsconfig.typecheck-api.json), and #12990 restores four behaviours the same refactor dropped — abort classification through isLocalStreamLifecycleError, the omitted synthetic clientResponse, the claudePromptCacheLogMeta rebuild on the leg path, and the lazy fail-closed fence identity. Irreducible at the existing chokepoints: each edit sits where chatCore already owns the decision, and the helpers themselves (nonStreamingProviderLeg.ts, serverOwnedToolLoopWire.ts) are under cap. Covered by tests/unit/chatcore-translation-paths.test.ts (72/74; the 2 open are issue #13043).", "_rebaseline_2026_09_07_virtualfactory_crosses_the_new_file_cap": "open-sse/services/autoCombo/virtualFactory.ts crosses the 1200 new-file cap for the first time (1187 on the pre-wave tip, 1219 after the wave). Growth is spread across the routing/free-tier wave, not one extractable block: #12794 feeds observed breaker state and model quality into snapshot scoring instead of neutral constants, #12792 adds the reliability factor the snapshot path was still ignoring and the pooled-latency bootstrap, and #12744 tightens the free-model predicate the factory consumes, and #12795 records which filter stage emptied an auto/* pool. FROZEN RATHER THAN SPLIT, deliberately, and this is debt: two cohesive extraction candidates are ready when someone owns the move — computeSnapshotWeights (~85 lines) and the credential-eligibility group hasUsableOAuthToken/hasProviderSpecificSessionData/isKeylessEligibleConnection/hasUsableConnectionCredential (~70 lines). Either alone clears 1200 from here. Splitting three contributors' just-merged work mid-batch was the larger risk.", - "_rebaseline_2026_09_07_streaming_wave": "Stacked growth from the SSE/streaming wave. open-sse/handlers/chatCore.ts 6021->6026 (+5): #12854 seeds the in-memory pending continuation state synchronously, before saveCallLogOperation's first await, closing the window where resolvePreviousResponseState finds nothing because the artifact write has not landed yet. open-sse/utils/stream.ts 3080->3098 (+18): #12828 emits the trailing usage-estimate chunk on the translate flush (#12151 had only covered passthrough, so translate-mode clients never saw token counts) and #12718 stops rebuilding a truncated summary from the collector's cap-dropped event array. Irreducible at the existing chokepoints — both are the flush/finalization points themselves. Covered by the continuation-store, translate-usage and collector-truncation suites." + "_rebaseline_2026_09_07_streaming_wave": "Stacked growth from the SSE/streaming wave. open-sse/handlers/chatCore.ts 6021->6026 (+5): #12854 seeds the in-memory pending continuation state synchronously, before saveCallLogOperation's first await, closing the window where resolvePreviousResponseState finds nothing because the artifact write has not landed yet. open-sse/utils/stream.ts 3080->3098 (+18): #12828 emits the trailing usage-estimate chunk on the translate flush (#12151 had only covered passthrough, so translate-mode clients never saw token counts) and #12718 stops rebuilding a truncated summary from the collector's cap-dropped event array. Irreducible at the existing chokepoints — both are the flush/finalization points themselves. Covered by the continuation-store, translate-usage and collector-truncation suites.", + "_rebaseline_2026_09_07_roundrobin_crosses_new_file_cap": "open-sse/services/combo/roundRobinCombo.ts 1198->1205, crossing the 1200 new-file cap. #12884 wires the quota-skip diagnostics into the round-robin attempt path so an ALL_TARGETS_SKIPPED 503 names which windows were exhausted instead of returning an opaque skip. The file was already at 1198 when #12811 lifted it out of combo.ts, so seven lines cross it; the diagnostics themselves live in quotaSkipDiagnostics.ts, under cap. Frozen rather than split: the natural next extraction is the attempt-loop body, which #12746/#12811 just moved and should settle before being cut again." } diff --git a/open-sse/services/codexUsageQuotas.ts b/open-sse/services/codexUsageQuotas.ts index e730e9279d..2bbe50d03e 100644 --- a/open-sse/services/codexUsageQuotas.ts +++ b/open-sse/services/codexUsageQuotas.ts @@ -4,6 +4,7 @@ import { CODEX_SPARK_QUOTA_WEEKLY, isCodexSparkLimitDescriptor, } from "../config/codexQuotaScopes.ts"; +import { inferWindowFamilyLabel } from "./quotaWindowLabel.ts"; type JsonRecord = Record; @@ -110,6 +111,7 @@ function buildPercentageQuota(window: JsonRecord, displayName?: string): CodexUs // duration instead of assuming primary=session / secondary=weekly by position. const WEEKLY_MIN_WINDOW_SECONDS = 6 * 24 * 3600; // >= ~6d const SESSION_MAX_WINDOW_SECONDS = 6 * 3600; // <= ~6h +const MONTHLY_MIN_WINDOW_SECONDS = 20 * 24 * 3600; // >= ~20d (ChatGPT 30d plans) /** * A never-started window: `used_percent === 0` and the reset still spans the @@ -133,12 +135,15 @@ function isLatentWindow(window: JsonRecord): boolean { * e.g. a 7-day `primary_window` is labeled "Weekly" rather than "Session". * Returns undefined for durations that don't clearly map to either bucket. */ -function windowDurationLabel(window: JsonRecord): "Session" | "Weekly" | undefined { +function windowDurationLabel(window: JsonRecord): "Session" | "Weekly" | "Monthly" | undefined { const limitWindow = toNumber( getFieldValue(window, "limit_window_seconds", "limitWindowSeconds"), 0 ); if (limitWindow <= 0) return undefined; + const inferred = inferWindowFamilyLabel(limitWindow); + if (inferred === "Monthly" || inferred === "Weekly" || inferred === "Session") return inferred; + if (limitWindow >= MONTHLY_MIN_WINDOW_SECONDS) return "Monthly"; if (limitWindow >= WEEKLY_MIN_WINDOW_SECONDS) return "Weekly"; if (limitWindow <= SESSION_MAX_WINDOW_SECONDS) return "Session"; return undefined; @@ -280,7 +285,7 @@ export function buildCodexUsageQuotas(dataValue: unknown): { const primaryLabel = windowDurationLabel(primaryWindow); quotas.session = buildPercentageQuota( primaryWindow, - primaryLabel === "Weekly" ? primaryLabel : undefined + primaryLabel === "Weekly" || primaryLabel === "Monthly" ? primaryLabel : undefined ); } diff --git a/open-sse/services/combo/comboAttemptLoop.ts b/open-sse/services/combo/comboAttemptLoop.ts index f1785454ae..acdcd24da5 100644 --- a/open-sse/services/combo/comboAttemptLoop.ts +++ b/open-sse/services/combo/comboAttemptLoop.ts @@ -14,6 +14,7 @@ import type { ComboDiagnostics } from "../../utils/error.ts"; import { COMBO_FAILURE_THRESHOLD, recordComboFailure } from "./failureTracker.ts"; import { buildNoUpstreamResponseDiagnostics, buildRecoveryHint } from "./pinRecovery.ts"; import { formatExhaustedConnectionKey } from "./comboDiagFormat.ts"; +import { collectQuotaWindowExclusions, formatQuotaSkipMessage } from "./quotaSkipDiagnostics.ts"; import { recordComboRequest } from "../comboMetrics.ts"; import { notifyWebhookEvent } from "../../../src/lib/webhookDispatcher.ts"; import { parseModel } from "../model.ts"; @@ -125,6 +126,9 @@ export async function dispatchWithCooldownRetry(opts: { excluded: [ ...[...state.exhaustedProviders].map((p) => ({ provider: p, reason: "exhausted" })), ...[...state.exhaustedConnections].map((c) => formatExhaustedConnectionKey(String(c))), + ...(terminalReason === "all_targets_skipped" + ? collectQuotaWindowExclusions(state.orderedTargets) + : []), ], attemptOrder: state.comboAttemptOrder, terminalReason, @@ -408,10 +412,15 @@ export async function dispatchWithCooldownRetry(opts: { latencyMs, fallbackCount: state.fallbackCount, }); + const quotaSkip = formatQuotaSkipMessage( + collectQuotaWindowExclusions(state.orderedTargets) + ); return withQuotaExhaustionClassification( errorResponseWithComboDiagnostics( 503, - "Service temporarily unavailable: all targets were skipped by pre-dispatch filters", + quotaSkip + ? `Service temporarily unavailable: all targets were skipped by pre-dispatch filters (${quotaSkip})` + : "Service temporarily unavailable: all targets were skipped by pre-dispatch filters", buildComboDiag("all_targets_skipped"), { code: "ALL_TARGETS_SKIPPED", type: "service_unavailable" } ), diff --git a/open-sse/services/combo/quotaSkipDiagnostics.ts b/open-sse/services/combo/quotaSkipDiagnostics.ts new file mode 100644 index 0000000000..9c0dbe655c --- /dev/null +++ b/open-sse/services/combo/quotaSkipDiagnostics.ts @@ -0,0 +1,75 @@ +/** + * Redacted-safe quota-window skip rows for ALL_TARGETS_SKIPPED 503 bodies. + * Connection ids are prefix-only (8 chars). Labels match AUTH / usage API. + */ + +import { getQuotaCache } from "@/domain/quotaCache"; +import { formatQuotaWindowLabel } from "../quotaWindowLabel.ts"; + +export type QuotaSkipTarget = { + provider?: string; + model?: string; + modelStr?: string; + // `string | null`, not `string | undefined`: ResolvedComboTarget carries null for an + // unpinned target, and this module only reads the field. Line 29 already narrows with + // `typeof === "string"`, so null costs nothing here (TS2345 under typecheck:core). + connectionId?: string | null; +}; + +export type QuotaSkipExclusion = { + provider: string; + model?: string; + reason: string; +}; + +const EXHAUSTED_USED_PERCENT = 99; + +export function collectQuotaWindowExclusions(targets: QuotaSkipTarget[]): QuotaSkipExclusion[] { + const seen = new Set(); + const out: QuotaSkipExclusion[] = []; + + for (const target of targets) { + const connectionId = typeof target.connectionId === "string" ? target.connectionId : ""; + if (!connectionId) continue; + const entry = getQuotaCache(connectionId); + if (!entry?.quotas) continue; + + const provider = + typeof target.provider === "string" && target.provider ? target.provider : "unknown"; + const model = + (typeof target.modelStr === "string" && target.modelStr) || + (typeof target.model === "string" && target.model) || + undefined; + const connPrefix = connectionId.slice(0, 8); + + for (const [key, quota] of Object.entries(entry.quotas)) { + const remaining = quota.remainingPercentage; + if (typeof remaining !== "number" || !Number.isFinite(remaining)) continue; + const used = Math.max(0, Math.min(100, 100 - remaining)); + if (used < EXHAUSTED_USED_PERCENT) continue; + + const label = formatQuotaWindowLabel({ + key, + displayName: quota.displayName, + windowSeconds: quota.windowSeconds, + }); + const reason = `quota:${label} ${Math.round(used)}% conn:${connPrefix}`; + const dedupe = `${provider}|${model ?? ""}|${reason}`; + if (seen.has(dedupe)) continue; + seen.add(dedupe); + out.push({ + provider, + ...(model ? { model } : {}), + reason, + }); + } + } + + return out; +} + +export function formatQuotaSkipMessage(exclusions: QuotaSkipExclusion[]): string | null { + if (exclusions.length === 0) return null; + const windows = [...new Set(exclusions.map((row) => row.reason.replace(/^quota:/, "")))]; + return windows.slice(0, 4).join("; "); +} diff --git a/open-sse/services/combo/roundRobinCombo.ts b/open-sse/services/combo/roundRobinCombo.ts index 1e73eb2521..8547784a30 100644 --- a/open-sse/services/combo/roundRobinCombo.ts +++ b/open-sse/services/combo/roundRobinCombo.ts @@ -15,6 +15,7 @@ import { } from "../../utils/error.ts"; import { buildRecoveryHint } from "./pinRecovery.ts"; import { formatExhaustedConnectionKey } from "./comboDiagFormat.ts"; +import { collectQuotaWindowExclusions, formatQuotaSkipMessage } from "./quotaSkipDiagnostics.ts"; import { recordComboRequest } from "../comboMetrics.ts"; import { expandComboSystemPromptIfPresent, @@ -1152,16 +1153,21 @@ export async function handleRoundRobinCombo({ if (!lastStatus) { if (recordedAttempts === 0) { - return new Response( - JSON.stringify({ - error: { - message: - "Service temporarily unavailable: all targets were skipped by pre-dispatch filters", - type: "service_unavailable", - code: "ALL_TARGETS_SKIPPED", - }, - }), - { status: 503, headers: { "Content-Type": "application/json" } } + const quotaExcluded = collectQuotaWindowExclusions(filteredTargets); + const quotaSkip = formatQuotaSkipMessage(quotaExcluded); + return errorResponseWithComboDiagnostics( + 503, + quotaSkip + ? `Service temporarily unavailable: all targets were skipped by pre-dispatch filters (${quotaSkip})` + : "Service temporarily unavailable: all targets were skipped by pre-dispatch filters", + { + poolSize: filteredTargets.length, + attempted: 0, + excluded: quotaExcluded, + attemptOrder: [], + terminalReason: "all_targets_skipped", + }, + { code: "ALL_TARGETS_SKIPPED", type: "service_unavailable" } ); } return new Response( diff --git a/open-sse/services/quotaWindowLabel.ts b/open-sse/services/quotaWindowLabel.ts new file mode 100644 index 0000000000..821d4c7fea --- /dev/null +++ b/open-sse/services/quotaWindowLabel.ts @@ -0,0 +1,73 @@ +/** + * Shared duration-aware quota window labels for AUTH preflight and /api/usage. + * Routing keys (`session`, `weekly`) stay position-based; only the display + * string follows the real window length so a 30-day primary window is not + * reported as "session (5h)". + */ + +const SESSION_MAX_SECONDS = 6 * 3600; +const WEEKLY_MAX_SECONDS = 8 * 24 * 3600; +const MONTHLY_MAX_SECONDS = 40 * 24 * 3600; +const GENERIC_FAMILY = new Set(["session", "weekly", "monthly"]); + +export type QuotaWindowLabelInput = { + key: string; + displayName?: string | null; + windowSeconds?: number | null; +}; + +function toPositiveSeconds(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value) && value > 0) return value; + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value); + if (Number.isFinite(parsed) && parsed > 0) return parsed; + } + return null; +} + +export function formatWindowDuration(windowSeconds: number): string { + if (windowSeconds % 86400 === 0) return `${windowSeconds / 86400}d`; + if (windowSeconds % 3600 === 0) return `${windowSeconds / 3600}h`; + return `${Math.round(windowSeconds)}s`; +} + +export function inferWindowFamilyLabel(windowSeconds: number): string | undefined { + if (windowSeconds <= SESSION_MAX_SECONDS) return "Session"; + if (windowSeconds <= WEEKLY_MAX_SECONDS) return "Weekly"; + if (windowSeconds <= MONTHLY_MAX_SECONDS) return "Monthly"; + return undefined; +} + +function normalizeFamily(value: string): string { + return value.trim().toLowerCase(); +} + +/** + * Prefer a duration-aware label. When usage already stamped a generic family + * (`Weekly`) on a 30-day window, replace it with Monthly so AUTH and the + * usage API agree on the reset window. + */ +export function formatQuotaWindowLabel(input: QuotaWindowLabelInput): string { + const seconds = toPositiveSeconds(input.windowSeconds); + const duration = seconds != null ? formatWindowDuration(seconds) : null; + const inferred = seconds != null ? inferWindowFamilyLabel(seconds) : undefined; + const named = typeof input.displayName === "string" ? input.displayName.trim() : ""; + const key = typeof input.key === "string" && input.key.trim() ? input.key.trim() : "quota"; + + let base = named || inferred || key; + if (inferred && named && GENERIC_FAMILY.has(normalizeFamily(named)) && inferred !== named) { + base = inferred; + } + + if (duration && !base.toLowerCase().includes(duration.toLowerCase())) { + return `${base} (${duration})`; + } + return base; +} + +export function formatQuotaUsageReason( + input: QuotaWindowLabelInput, + usedPercentage: number +): string { + return `${formatQuotaWindowLabel(input)} usage ${Math.round(usedPercentage)}%`; +} diff --git a/src/domain/quotaCache.ts b/src/domain/quotaCache.ts index bb3045bccc..a3bc93e49f 100644 --- a/src/domain/quotaCache.ts +++ b/src/domain/quotaCache.ts @@ -53,6 +53,8 @@ interface QuotaInfo { // percentage; `false` means "unknown", so callers must not treat the // defaulted-to-0 `remainingPercentage` as genuine exhaustion. fractionReported?: boolean; + displayName?: string; + windowSeconds?: number | null; } interface QuotaCacheEntry { @@ -70,6 +72,8 @@ interface QuotaWindowStatus { usedPercentage: number; resetAt: string | null; reachedThreshold: boolean; + displayName?: string; + windowSeconds?: number | null; } export interface QuotaWindowObservation { @@ -250,6 +254,12 @@ function normalizeQuotas(rawQuotas: Record): Record = {}; for (const [key, q] of Object.entries(rawQuotas)) { if (q && typeof q === "object") { + const windowSeconds = + typeof q.windowSeconds === "number" && Number.isFinite(q.windowSeconds) + ? q.windowSeconds + : typeof q.window_seconds === "number" && Number.isFinite(q.window_seconds) + ? q.window_seconds + : null; result[key] = { remainingPercentage: safePercentage(q.remainingPercentage) ?? @@ -258,6 +268,10 @@ function normalizeQuotas(rawQuotas: Record): Record= thresholdPercent, + ...(window.displayName ? { displayName: window.displayName } : {}), + ...(window.windowSeconds != null ? { windowSeconds: window.windowSeconds } : {}), }; } diff --git a/src/sse/services/auth.ts b/src/sse/services/auth.ts index 43470c04d1..79fc31dfec 100644 --- a/src/sse/services/auth.ts +++ b/src/sse/services/auth.ts @@ -52,7 +52,10 @@ import { getQuotaScopeLabelForProvider, isAntigravityQuotaProvider, } from "@omniroute/open-sse/services/antigravityQuotaFamily.ts"; -import { rehydrateAntigravityFamilyLocksForConnections, persistAntigravityFamilyCooldownIfQuota } from "@omniroute/open-sse/services/antigravityFamilyCooldown.ts"; +import { + rehydrateAntigravityFamilyLocksForConnections, + persistAntigravityFamilyCooldownIfQuota, +} from "@omniroute/open-sse/services/antigravityFamilyCooldown.ts"; import { markQuotaPreflightAccountUnavailable } from "./quotaPreflightUnavailable.ts"; import { getCreditsMode } from "@omniroute/open-sse/services/antigravityCredits.ts"; import { preferAntigravityConnectionsWithStoredProject } from "@omniroute/open-sse/services/antigravityProjectPersistence.ts"; @@ -112,6 +115,7 @@ import { toCodexBaseQuotaWindowName, toCodexScopedQuotaWindowName, } from "@omniroute/open-sse/config/codexQuotaScopes.ts"; +import { formatQuotaUsageReason } from "@omniroute/open-sse/services/quotaWindowLabel.ts"; import { getCodexChildCooldown, isCodexChildUnavailable, @@ -380,7 +384,16 @@ export function evaluateQuotaLimitPolicy( policy.thresholdPercent ); if (!status?.reachedThreshold) continue; - reasons.push(`${effectiveWindowName} usage ${Math.round(status.usedPercentage)}%`); + reasons.push( + formatQuotaUsageReason( + { + key: effectiveWindowName, + displayName: status.displayName, + windowSeconds: status.windowSeconds, + }, + status.usedPercentage + ) + ); resetCandidates.push(status.resetAt); } @@ -2428,7 +2441,7 @@ export function isAgentrouterConnectionQuotaScope( } async function resolveDailyResetForProvider( - provider: string | null, + provider: string | null ): Promise<{ timezone?: unknown; hour?: unknown } | null> { if (!provider) return null; try { @@ -2666,7 +2679,7 @@ export async function markAccountUnavailable( effectiveProviderProfile, null, null, - await resolveDailyResetForProvider(provider), + await resolveDailyResetForProvider(provider) ); // T-PROBE: probe-origin failures (model test-all) must never remove the @@ -2920,7 +2933,13 @@ export async function markAccountUnavailable( "AUTH", `Model-only lockout for ${provider}:${model} — ${status} ${reason} ${Math.ceil(lockout.cooldownMs / 1000)}s (failureCount=${lockout.failureCount}, connection stays active)` ); - persistAntigravityFamilyCooldownIfQuota({ provider, connectionId, model, cooldownMs: lockout.cooldownMs, reason }); + persistAntigravityFamilyCooldownIfQuota({ + provider, + connectionId, + model, + cooldownMs: lockout.cooldownMs, + reason, + }); return { shouldFallback: true, cooldownMs: lockout.cooldownMs }; } const result = fallbackResult; diff --git a/tests/unit/quota-window-label.test.ts b/tests/unit/quota-window-label.test.ts new file mode 100644 index 0000000000..83b7b8e740 --- /dev/null +++ b/tests/unit/quota-window-label.test.ts @@ -0,0 +1,101 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + formatQuotaUsageReason, + formatQuotaWindowLabel, +} from "../../open-sse/services/quotaWindowLabel.ts"; +import { collectQuotaWindowExclusions } from "../../open-sse/services/combo/quotaSkipDiagnostics.ts"; +import { __clearForTests, setQuotaCache } from "../../src/domain/quotaCache.ts"; +import { evaluateQuotaLimitPolicy } from "../../src/sse/services/auth.ts"; +import { buildCodexUsageQuotas } from "../../open-sse/services/codexUsageQuotas.ts"; + +test("formatQuotaWindowLabel keeps the routing key when no duration metadata exists", () => { + assert.equal(formatQuotaWindowLabel({ key: "session" }), "session"); + assert.equal(formatQuotaUsageReason({ key: "session" }, 100), "session usage 100%"); +}); + +test("formatQuotaWindowLabel prefers Monthly for a 30-day window labeled Weekly", () => { + assert.equal( + formatQuotaWindowLabel({ + key: "session", + displayName: "Weekly", + windowSeconds: 2_592_000, + }), + "Monthly (30d)" + ); +}); + +test("formatQuotaWindowLabel keeps a genuine 5h session label", () => { + assert.equal( + formatQuotaWindowLabel({ + key: "session", + displayName: "Session", + windowSeconds: 18_000, + }), + "Session (5h)" + ); +}); + +test("buildCodexUsageQuotas labels a 30-day primary window Monthly, not Weekly", () => { + const { quotas } = buildCodexUsageQuotas({ + rate_limit: { + primary_window: { + used_percent: 100, + limit_window_seconds: 2_592_000, + reset_after_seconds: 100_000, + }, + }, + }); + assert.ok(quotas.session); + assert.equal(quotas.session.displayName, "Monthly"); + assert.equal(quotas.session.windowSeconds, 2_592_000); +}); + +test("AUTH reasons match usage duration labels for a 30-day Codex session window", () => { + __clearForTests(); + const connectionId = "quota-label-conn-30d"; + setQuotaCache(connectionId, "codex", { + session: { + remainingPercentage: 0, + resetAt: new Date(Date.now() + 86_400_000).toISOString(), + displayName: "Weekly", + windowSeconds: 2_592_000, + }, + }); + + const evaluation = evaluateQuotaLimitPolicy("codex", { + id: connectionId, + providerSpecificData: { + limitPolicy: { enabled: true, thresholdPercent: 99, windows: ["session"] }, + codexLimitPolicy: { use5h: true, useWeekly: false }, + }, + } as never); + + assert.equal(evaluation.blocked, true); + assert.deepEqual(evaluation.reasons, ["Monthly (30d) usage 100%"]); +}); + +test("collectQuotaWindowExclusions lists redacted-safe quota windows for 503 bodies", () => { + __clearForTests(); + const connectionId = "abcd1234-ffff-4000-8000-000000000001"; + setQuotaCache(connectionId, "codex", { + session: { + remainingPercentage: 0, + resetAt: new Date(Date.now() + 86_400_000).toISOString(), + displayName: "Weekly", + windowSeconds: 2_592_000, + }, + }); + + const rows = collectQuotaWindowExclusions([ + { provider: "codex", modelStr: "gpt-5.6-sol", connectionId }, + ]); + + assert.equal(rows.length, 1); + assert.equal(rows[0].provider, "codex"); + assert.equal(rows[0].model, "gpt-5.6-sol"); + assert.match(rows[0].reason, /quota:Monthly \(30d\) 100%/); + assert.match(rows[0].reason, /conn:abcd1234/); + assert.equal(rows[0].reason.includes(connectionId), false); +});