mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 02:16:43 +00:00
fix(cliproxy): parse Codex additional_rate_limits for Spark quota
The Codex wham/usage API moved GPT-5.3 Codex Spark quota out of code_review_rate_limit (now null) into a new additional_rate_limits[] array. Parse the new field and surface it as Spark windows in the quota view. Adds explicit category/cadence/featureLabel metadata on CodexQuotaWindow so display logic does not depend on label sniffing. The legacy label-sniffing path is preserved as a fallback so cached windows from older versions still render correctly.
This commit is contained in:
@@ -39,8 +39,10 @@ interface CodexUsageResponse {
|
|||||||
planType?: string;
|
planType?: string;
|
||||||
rate_limit?: CodexRateLimitWindow;
|
rate_limit?: CodexRateLimitWindow;
|
||||||
rateLimit?: CodexRateLimitWindow;
|
rateLimit?: CodexRateLimitWindow;
|
||||||
code_review_rate_limit?: CodexRateLimitWindow;
|
code_review_rate_limit?: CodexRateLimitWindow | null;
|
||||||
codeReviewRateLimit?: CodexRateLimitWindow;
|
codeReviewRateLimit?: CodexRateLimitWindow | null;
|
||||||
|
additional_rate_limits?: CodexAdditionalRateLimit[] | null;
|
||||||
|
additionalRateLimits?: CodexAdditionalRateLimit[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rate limit window from API */
|
/** Rate limit window from API */
|
||||||
@@ -51,6 +53,19 @@ interface CodexRateLimitWindow {
|
|||||||
secondaryWindow?: CodexWindowData;
|
secondaryWindow?: CodexWindowData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional rate limit entry from API (introduced for features like GPT-5.3 Codex Spark).
|
||||||
|
* Each entry surfaces its own primary/secondary windows under a feature-specific limit name.
|
||||||
|
*/
|
||||||
|
interface CodexAdditionalRateLimit {
|
||||||
|
limit_name?: string;
|
||||||
|
limitName?: string;
|
||||||
|
metered_feature?: string;
|
||||||
|
meteredFeature?: string;
|
||||||
|
rate_limit?: CodexRateLimitWindow;
|
||||||
|
rateLimit?: CodexRateLimitWindow;
|
||||||
|
}
|
||||||
|
|
||||||
/** Individual window data */
|
/** Individual window data */
|
||||||
interface CodexWindowData {
|
interface CodexWindowData {
|
||||||
used_percent?: number;
|
used_percent?: number;
|
||||||
@@ -92,7 +107,11 @@ function getCodexWindowKind(label: string): CodexWindowKind {
|
|||||||
|
|
||||||
function getUnknownCodexWindowLabels(windows: CodexQuotaWindow[]): string[] {
|
function getUnknownCodexWindowLabels(windows: CodexQuotaWindow[]): string[] {
|
||||||
const unknownLabels = windows
|
const unknownLabels = windows
|
||||||
.filter((window) => getCodexWindowKind(window.label) === 'unknown')
|
.filter((window) => {
|
||||||
|
// Windows with explicit category metadata are always classified.
|
||||||
|
if (window.category) return false;
|
||||||
|
return getCodexWindowKind(window.label) === 'unknown';
|
||||||
|
})
|
||||||
.map((window) => window.label)
|
.map((window) => window.label)
|
||||||
.filter((label): label is string => typeof label === 'string' && label.trim().length > 0);
|
.filter((label): label is string => typeof label === 'string' && label.trim().length > 0);
|
||||||
return Array.from(new Set(unknownLabels));
|
return Array.from(new Set(unknownLabels));
|
||||||
@@ -106,7 +125,8 @@ function shouldLogCodexWindowWarnings(verbose: boolean): boolean {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build explicit 5h + weekly usage summary from raw Codex windows.
|
* Build explicit 5h + weekly usage summary from raw Codex windows.
|
||||||
* Falls back to shortest/longest reset windows if API labels change.
|
* Prefers explicit `category`/`cadence` metadata when present.
|
||||||
|
* Falls back to label sniffing for legacy cached windows.
|
||||||
*/
|
*/
|
||||||
export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCoreUsageSummary {
|
export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCoreUsageSummary {
|
||||||
if (!windows || windows.length === 0) {
|
if (!windows || windows.length === 0) {
|
||||||
@@ -117,20 +137,36 @@ export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCo
|
|||||||
let weeklyWindow: CodexQuotaWindow | null = null;
|
let weeklyWindow: CodexQuotaWindow | null = null;
|
||||||
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
|
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
|
||||||
|
|
||||||
for (const window of windows) {
|
// Determine if any window carries category metadata. If so, prefer category-based
|
||||||
const kind = getCodexWindowKind(window.label);
|
// selection so 'additional' windows (e.g. Spark) do not pollute the main usage summary.
|
||||||
if (kind === 'usage-5h') {
|
const hasCategoryMetadata = windows.some((window) => Boolean(window.category));
|
||||||
if (!fiveHourWindow) fiveHourWindow = window;
|
|
||||||
nonCodeReviewWindows.push(window);
|
if (hasCategoryMetadata) {
|
||||||
continue;
|
for (const window of windows) {
|
||||||
|
if (window.category === 'usage') {
|
||||||
|
if (window.cadence === '5h' && !fiveHourWindow) fiveHourWindow = window;
|
||||||
|
else if (window.cadence === 'weekly' && !weeklyWindow) weeklyWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
}
|
||||||
|
// 'code-review' and 'additional' windows are intentionally excluded from the main
|
||||||
|
// usage summary — they represent feature-specific quotas, not core usage.
|
||||||
}
|
}
|
||||||
if (kind === 'usage-weekly') {
|
} else {
|
||||||
if (!weeklyWindow) weeklyWindow = window;
|
for (const window of windows) {
|
||||||
nonCodeReviewWindows.push(window);
|
const kind = getCodexWindowKind(window.label);
|
||||||
continue;
|
if (kind === 'usage-5h') {
|
||||||
}
|
if (!fiveHourWindow) fiveHourWindow = window;
|
||||||
if (kind === 'unknown') {
|
nonCodeReviewWindows.push(window);
|
||||||
nonCodeReviewWindows.push(window);
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'usage-weekly') {
|
||||||
|
if (!weeklyWindow) weeklyWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'unknown') {
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,9 +306,18 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[]
|
|||||||
// Get rate limit object (handles both cases)
|
// Get rate limit object (handles both cases)
|
||||||
const rateLimit = payload.rate_limit || payload.rateLimit;
|
const rateLimit = payload.rate_limit || payload.rateLimit;
|
||||||
const codeReviewRateLimit = payload.code_review_rate_limit || payload.codeReviewRateLimit;
|
const codeReviewRateLimit = payload.code_review_rate_limit || payload.codeReviewRateLimit;
|
||||||
|
const additionalRateLimits = payload.additional_rate_limits || payload.additionalRateLimits;
|
||||||
|
|
||||||
// Helper to extract window data
|
// Helper to extract window data
|
||||||
const addWindow = (label: string, windowData: CodexWindowData | undefined): void => {
|
const addWindow = (
|
||||||
|
label: string,
|
||||||
|
windowData: CodexWindowData | undefined,
|
||||||
|
meta: {
|
||||||
|
category: NonNullable<CodexQuotaWindow['category']>;
|
||||||
|
cadence: NonNullable<CodexQuotaWindow['cadence']>;
|
||||||
|
featureLabel?: string;
|
||||||
|
}
|
||||||
|
): void => {
|
||||||
if (!windowData) return;
|
if (!windowData) return;
|
||||||
|
|
||||||
// Clamp usedPercent to [0, 100] range
|
// Clamp usedPercent to [0, 100] range
|
||||||
@@ -287,33 +332,68 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[]
|
|||||||
resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
|
resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
windows.push({
|
const window: CodexQuotaWindow = {
|
||||||
label,
|
label,
|
||||||
usedPercent,
|
usedPercent,
|
||||||
remainingPercent: Math.max(0, 100 - usedPercent),
|
remainingPercent: Math.max(0, 100 - usedPercent),
|
||||||
resetAfterSeconds,
|
resetAfterSeconds,
|
||||||
resetAt,
|
resetAt,
|
||||||
});
|
category: meta.category,
|
||||||
|
cadence: meta.cadence,
|
||||||
|
};
|
||||||
|
if (meta.featureLabel) {
|
||||||
|
window.featureLabel = meta.featureLabel;
|
||||||
|
}
|
||||||
|
windows.push(window);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add main rate limit windows
|
// Add main rate limit windows
|
||||||
if (rateLimit) {
|
if (rateLimit) {
|
||||||
addWindow('Primary', rateLimit.primary_window || rateLimit.primaryWindow);
|
addWindow('Primary', rateLimit.primary_window || rateLimit.primaryWindow, {
|
||||||
addWindow('Secondary', rateLimit.secondary_window || rateLimit.secondaryWindow);
|
category: 'usage',
|
||||||
|
cadence: '5h',
|
||||||
|
});
|
||||||
|
addWindow('Secondary', rateLimit.secondary_window || rateLimit.secondaryWindow, {
|
||||||
|
category: 'usage',
|
||||||
|
cadence: 'weekly',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add code review rate limit windows
|
// Add code review rate limit windows
|
||||||
if (codeReviewRateLimit) {
|
if (codeReviewRateLimit) {
|
||||||
addWindow(
|
addWindow(
|
||||||
'Code Review (Primary)',
|
'Code Review (Primary)',
|
||||||
codeReviewRateLimit.primary_window || codeReviewRateLimit.primaryWindow
|
codeReviewRateLimit.primary_window || codeReviewRateLimit.primaryWindow,
|
||||||
|
{ category: 'code-review', cadence: '5h', featureLabel: 'Code Review' }
|
||||||
);
|
);
|
||||||
addWindow(
|
addWindow(
|
||||||
'Code Review (Secondary)',
|
'Code Review (Secondary)',
|
||||||
codeReviewRateLimit.secondary_window || codeReviewRateLimit.secondaryWindow
|
codeReviewRateLimit.secondary_window || codeReviewRateLimit.secondaryWindow,
|
||||||
|
{ category: 'code-review', cadence: 'weekly', featureLabel: 'Code Review' }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add additional rate limit windows (e.g. GPT-5.3 Codex Spark)
|
||||||
|
if (Array.isArray(additionalRateLimits)) {
|
||||||
|
for (const entry of additionalRateLimits) {
|
||||||
|
if (!entry) continue;
|
||||||
|
const entryRateLimit = entry.rate_limit || entry.rateLimit;
|
||||||
|
if (!entryRateLimit) continue;
|
||||||
|
|
||||||
|
const featureLabel = entry.limit_name || entry.limitName || 'Additional';
|
||||||
|
addWindow(
|
||||||
|
`${featureLabel} (Primary)`,
|
||||||
|
entryRateLimit.primary_window || entryRateLimit.primaryWindow,
|
||||||
|
{ category: 'additional', cadence: '5h', featureLabel }
|
||||||
|
);
|
||||||
|
addWindow(
|
||||||
|
`${featureLabel} (Secondary)`,
|
||||||
|
entryRateLimit.secondary_window || entryRateLimit.secondaryWindow,
|
||||||
|
{ category: 'additional', cadence: 'weekly', featureLabel }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return windows;
|
return windows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ export interface QuotaErrorMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex quota window (primary, secondary, code review)
|
* Codex quota window (primary, secondary, code review, additional)
|
||||||
*/
|
*/
|
||||||
export interface CodexQuotaWindow {
|
export interface CodexQuotaWindow {
|
||||||
/** Window label: "Primary", "Secondary", "Code Review (Primary)", "Code Review (Secondary)" */
|
/** Window label: "Primary", "Secondary", "Code Review (Primary)", "Code Review (Secondary)", or "<feature> (Primary|Secondary)" */
|
||||||
label: string;
|
label: string;
|
||||||
/** Percentage used (0-100) */
|
/** Percentage used (0-100) */
|
||||||
usedPercent: number;
|
usedPercent: number;
|
||||||
@@ -42,6 +42,18 @@ export interface CodexQuotaWindow {
|
|||||||
resetAfterSeconds: number | null;
|
resetAfterSeconds: number | null;
|
||||||
/** ISO timestamp when quota resets, null if unknown */
|
/** ISO timestamp when quota resets, null if unknown */
|
||||||
resetAt: string | null;
|
resetAt: string | null;
|
||||||
|
/**
|
||||||
|
* Window category indicating the bucket this window belongs to.
|
||||||
|
* Optional for back-compat with cached data emitted before this field existed.
|
||||||
|
* - 'usage' -> standard rate_limit usage windows
|
||||||
|
* - 'code-review' -> code_review_rate_limit windows
|
||||||
|
* - 'additional' -> additional_rate_limits[] windows (e.g. GPT-5.3 Codex Spark)
|
||||||
|
*/
|
||||||
|
category?: 'usage' | 'code-review' | 'additional';
|
||||||
|
/** Cadence of the window: '5h' = primary, 'weekly' = secondary. Optional for legacy data. */
|
||||||
|
cadence?: '5h' | 'weekly';
|
||||||
|
/** Raw upstream label (e.g. 'GPT-5.3-Codex-Spark', 'Code Review'); absent for plain usage windows. */
|
||||||
|
featureLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Core Codex usage window (5h/weekly) extracted from raw windows */
|
/** Core Codex usage window (5h/weekly) extracted from raw windows */
|
||||||
|
|||||||
@@ -238,7 +238,10 @@ function getCodexWindowKind(label: string): CodexWindowKind {
|
|||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
type CodexWindowSummary = Pick<CodexQuotaResult['windows'][number], 'label' | 'resetAfterSeconds'>;
|
type CodexWindowSummary = Pick<
|
||||||
|
CodexQuotaResult['windows'][number],
|
||||||
|
'label' | 'resetAfterSeconds' | 'category' | 'cadence' | 'featureLabel'
|
||||||
|
>;
|
||||||
|
|
||||||
function inferCodeReviewCadence(
|
function inferCodeReviewCadence(
|
||||||
window: CodexWindowSummary,
|
window: CodexWindowSummary,
|
||||||
@@ -272,12 +275,46 @@ function inferCodeReviewCadence(
|
|||||||
return diffToWeekly <= diffTo5h ? 'weekly' : '5h';
|
return diffToWeekly <= diffTo5h ? 'weekly' : '5h';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip a leading "GPT-X.Y-Codex-" prefix from a feature label and turn the
|
||||||
|
* remainder into a Codex-prefixed display name. Other labels pass through unchanged.
|
||||||
|
*/
|
||||||
|
function prettifyCodexFeatureLabel(featureLabel: string): string {
|
||||||
|
const trimmed = featureLabel.trim();
|
||||||
|
if (!trimmed) return 'Additional';
|
||||||
|
const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, '');
|
||||||
|
if (stripped !== trimmed && stripped.length > 0) {
|
||||||
|
return `Codex ${stripped}`;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
function getCodexWindowDisplayLabel(
|
function getCodexWindowDisplayLabel(
|
||||||
window: CodexWindowSummary,
|
window: CodexWindowSummary,
|
||||||
allWindows: CodexWindowSummary[] = []
|
allWindows: CodexWindowSummary[] = []
|
||||||
): string {
|
): string {
|
||||||
const context = allWindows.length > 0 ? allWindows : [window];
|
const context = allWindows.length > 0 ? allWindows : [window];
|
||||||
|
|
||||||
|
// Prefer explicit category metadata when present (post-2026-04 windows).
|
||||||
|
if (window.category === 'usage') {
|
||||||
|
if (window.cadence === '5h') return '5h usage limit';
|
||||||
|
if (window.cadence === 'weekly') return 'Weekly usage limit';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.category === 'additional') {
|
||||||
|
const pretty = prettifyCodexFeatureLabel(window.featureLabel || 'Additional');
|
||||||
|
if (window.cadence === '5h') return `${pretty} (5h)`;
|
||||||
|
if (window.cadence === 'weekly') return `${pretty} (weekly)`;
|
||||||
|
return pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.category === 'code-review') {
|
||||||
|
if (window.cadence === '5h') return 'Code review (5h)';
|
||||||
|
if (window.cadence === 'weekly') return 'Code review (weekly)';
|
||||||
|
return 'Code review';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy fallback: classify via label sniffing for cached windows without metadata.
|
||||||
switch (getCodexWindowKind(window.label)) {
|
switch (getCodexWindowKind(window.label)) {
|
||||||
case 'usage-5h':
|
case 'usage-5h':
|
||||||
return '5h usage limit';
|
return '5h usage limit';
|
||||||
@@ -304,20 +341,35 @@ function getCodexCoreUsageWindows(windows: CodexQuotaResult['windows']): {
|
|||||||
let weeklyWindow: CodexQuotaResult['windows'][number] | null = null;
|
let weeklyWindow: CodexQuotaResult['windows'][number] | null = null;
|
||||||
const nonCodeReviewWindows: CodexQuotaResult['windows'] = [];
|
const nonCodeReviewWindows: CodexQuotaResult['windows'] = [];
|
||||||
|
|
||||||
for (const window of windows) {
|
// Prefer explicit category metadata when present so 'additional' windows
|
||||||
const kind = getCodexWindowKind(window.label);
|
// (e.g. GPT-5.3 Codex Spark) do not displace core usage windows in the summary.
|
||||||
if (kind === 'usage-5h') {
|
const hasCategoryMetadata = windows.some((window) => Boolean(window.category));
|
||||||
if (!fiveHourWindow) fiveHourWindow = window;
|
|
||||||
nonCodeReviewWindows.push(window);
|
if (hasCategoryMetadata) {
|
||||||
continue;
|
for (const window of windows) {
|
||||||
|
if (window.category === 'usage') {
|
||||||
|
if (window.cadence === '5h' && !fiveHourWindow) fiveHourWindow = window;
|
||||||
|
else if (window.cadence === 'weekly' && !weeklyWindow) weeklyWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
}
|
||||||
|
// 'code-review' and 'additional' are excluded from the core usage summary.
|
||||||
}
|
}
|
||||||
if (kind === 'usage-weekly') {
|
} else {
|
||||||
if (!weeklyWindow) weeklyWindow = window;
|
for (const window of windows) {
|
||||||
nonCodeReviewWindows.push(window);
|
const kind = getCodexWindowKind(window.label);
|
||||||
continue;
|
if (kind === 'usage-5h') {
|
||||||
}
|
if (!fiveHourWindow) fiveHourWindow = window;
|
||||||
if (kind === 'unknown') {
|
nonCodeReviewWindows.push(window);
|
||||||
nonCodeReviewWindows.push(window);
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'usage-weekly') {
|
||||||
|
if (!weeklyWindow) weeklyWindow = window;
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kind === 'unknown') {
|
||||||
|
nonCodeReviewWindows.push(window);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -246,6 +246,122 @@ describe('Codex Quota Fetcher', () => {
|
|||||||
expect(windows[0].usedPercent).toBe(0);
|
expect(windows[0].usedPercent).toBe(0);
|
||||||
expect(windows[0].remainingPercent).toBe(100);
|
expect(windows[0].remainingPercent).toBe(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should attach category and cadence metadata to standard usage windows', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 5, reset_after_seconds: 18000 },
|
||||||
|
secondary_window: { used_percent: 25, reset_after_seconds: 604800 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(2);
|
||||||
|
expect(windows[0].category).toBe('usage');
|
||||||
|
expect(windows[0].cadence).toBe('5h');
|
||||||
|
expect(windows[0].featureLabel).toBeUndefined();
|
||||||
|
expect(windows[1].category).toBe('usage');
|
||||||
|
expect(windows[1].cadence).toBe('weekly');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark code review windows with code-review category and Code Review feature label', () => {
|
||||||
|
const response = {
|
||||||
|
code_review_rate_limit: {
|
||||||
|
primary_window: { used_percent: 12, reset_after_seconds: 1800 },
|
||||||
|
secondary_window: { used_percent: 60, reset_after_seconds: 604800 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(2);
|
||||||
|
expect(windows[0].category).toBe('code-review');
|
||||||
|
expect(windows[0].cadence).toBe('5h');
|
||||||
|
expect(windows[0].featureLabel).toBe('Code Review');
|
||||||
|
expect(windows[1].category).toBe('code-review');
|
||||||
|
expect(windows[1].cadence).toBe('weekly');
|
||||||
|
expect(windows[1].featureLabel).toBe('Code Review');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse additional_rate_limits entries (e.g. GPT-5.3 Codex Spark)', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 0, reset_after_seconds: 18000 },
|
||||||
|
secondary_window: { used_percent: 1, reset_after_seconds: 254493 },
|
||||||
|
},
|
||||||
|
code_review_rate_limit: null,
|
||||||
|
additional_rate_limits: [
|
||||||
|
{
|
||||||
|
limit_name: 'GPT-5.3-Codex-Spark',
|
||||||
|
metered_feature: 'codex_bengalfox',
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 0, reset_after_seconds: 18000 },
|
||||||
|
secondary_window: { used_percent: 1, reset_after_seconds: 254493 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
// 2 standard usage windows + 2 additional windows.
|
||||||
|
expect(windows).toHaveLength(4);
|
||||||
|
|
||||||
|
const additionalWindows = windows.filter((w) => w.category === 'additional');
|
||||||
|
expect(additionalWindows).toHaveLength(2);
|
||||||
|
|
||||||
|
const sparkPrimary = additionalWindows.find((w) => w.cadence === '5h');
|
||||||
|
const sparkSecondary = additionalWindows.find((w) => w.cadence === 'weekly');
|
||||||
|
|
||||||
|
expect(sparkPrimary).toBeDefined();
|
||||||
|
expect(sparkPrimary?.featureLabel).toBe('GPT-5.3-Codex-Spark');
|
||||||
|
expect(sparkPrimary?.label).toBe('GPT-5.3-Codex-Spark (Primary)');
|
||||||
|
expect(sparkPrimary?.usedPercent).toBe(0);
|
||||||
|
expect(sparkPrimary?.remainingPercent).toBe(100);
|
||||||
|
|
||||||
|
expect(sparkSecondary).toBeDefined();
|
||||||
|
expect(sparkSecondary?.featureLabel).toBe('GPT-5.3-Codex-Spark');
|
||||||
|
expect(sparkSecondary?.label).toBe('GPT-5.3-Codex-Spark (Secondary)');
|
||||||
|
expect(sparkSecondary?.usedPercent).toBe(1);
|
||||||
|
expect(sparkSecondary?.remainingPercent).toBe(99);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle additional_rate_limits set to null without breaking', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 10, reset_after_seconds: 3600 },
|
||||||
|
},
|
||||||
|
additional_rate_limits: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].category).toBe('usage');
|
||||||
|
expect(windows.find((w) => w.category === 'additional')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept camelCase additionalRateLimits and rateLimit fields', () => {
|
||||||
|
const response = {
|
||||||
|
additionalRateLimits: [
|
||||||
|
{
|
||||||
|
limitName: 'Custom-Feature',
|
||||||
|
rateLimit: {
|
||||||
|
primaryWindow: { usedPercent: 50, resetAfterSeconds: 3600 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].category).toBe('additional');
|
||||||
|
expect(windows[0].cadence).toBe('5h');
|
||||||
|
expect(windows[0].featureLabel).toBe('Custom-Feature');
|
||||||
|
expect(windows[0].usedPercent).toBe(50);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildCodexCoreUsageSummary', () => {
|
describe('buildCodexCoreUsageSummary', () => {
|
||||||
@@ -311,6 +427,36 @@ describe('Codex Quota Fetcher', () => {
|
|||||||
expect(summary.fiveHour).toBeNull();
|
expect(summary.fiveHour).toBeNull();
|
||||||
expect(summary.weekly).toBeNull();
|
expect(summary.weekly).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('excludes additional and code-review windows from the core usage summary', () => {
|
||||||
|
const windows = buildCodexQuotaWindows({
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 35, reset_after_seconds: 18000 },
|
||||||
|
secondary_window: { used_percent: 60, reset_after_seconds: 604800 },
|
||||||
|
},
|
||||||
|
code_review_rate_limit: {
|
||||||
|
primary_window: { used_percent: 70, reset_after_seconds: 1800 },
|
||||||
|
},
|
||||||
|
additional_rate_limits: [
|
||||||
|
{
|
||||||
|
limit_name: 'GPT-5.3-Codex-Spark',
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 90, reset_after_seconds: 100 },
|
||||||
|
secondary_window: { used_percent: 95, reset_after_seconds: 7200 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = buildCodexCoreUsageSummary(windows);
|
||||||
|
|
||||||
|
// Should pick the 'usage' windows, NOT the Spark windows even though they
|
||||||
|
// have shorter reset cadences.
|
||||||
|
expect(summary.fiveHour?.label).toBe('Primary');
|
||||||
|
expect(summary.fiveHour?.resetAfterSeconds).toBe(18000);
|
||||||
|
expect(summary.weekly?.label).toBe('Secondary');
|
||||||
|
expect(summary.weekly?.resetAfterSeconds).toBe(604800);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getUnknownCodexWindowLabels', () => {
|
describe('getUnknownCodexWindowLabels', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user