mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
Merge pull request #1113 from yousiki/fix/codex-additional-rate-limits
fix(cliproxy): parse Codex additional_rate_limits for Spark quota
This commit is contained in:
@@ -39,8 +39,10 @@ interface CodexUsageResponse {
|
||||
planType?: string;
|
||||
rate_limit?: CodexRateLimitWindow;
|
||||
rateLimit?: CodexRateLimitWindow;
|
||||
code_review_rate_limit?: CodexRateLimitWindow;
|
||||
codeReviewRateLimit?: CodexRateLimitWindow;
|
||||
code_review_rate_limit?: CodexRateLimitWindow | null;
|
||||
codeReviewRateLimit?: CodexRateLimitWindow | null;
|
||||
additional_rate_limits?: CodexAdditionalRateLimit[] | null;
|
||||
additionalRateLimits?: CodexAdditionalRateLimit[] | null;
|
||||
}
|
||||
|
||||
/** Rate limit window from API */
|
||||
@@ -51,6 +53,19 @@ interface CodexRateLimitWindow {
|
||||
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 */
|
||||
interface CodexWindowData {
|
||||
used_percent?: number;
|
||||
@@ -92,7 +107,11 @@ function getCodexWindowKind(label: string): CodexWindowKind {
|
||||
|
||||
function getUnknownCodexWindowLabels(windows: CodexQuotaWindow[]): string[] {
|
||||
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)
|
||||
.filter((label): label is string => typeof label === 'string' && label.trim().length > 0);
|
||||
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.
|
||||
* 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 {
|
||||
if (!windows || windows.length === 0) {
|
||||
@@ -117,20 +137,36 @@ export function buildCodexCoreUsageSummary(windows: CodexQuotaWindow[]): CodexCo
|
||||
let weeklyWindow: CodexQuotaWindow | null = null;
|
||||
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
|
||||
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
if (kind === 'usage-5h') {
|
||||
if (!fiveHourWindow) fiveHourWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
continue;
|
||||
// Determine if any window carries category metadata. If so, prefer category-based
|
||||
// selection so 'additional' windows (e.g. Spark) do not pollute the main usage summary.
|
||||
const hasCategoryMetadata = windows.some((window) => Boolean(window.category));
|
||||
|
||||
if (hasCategoryMetadata) {
|
||||
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') {
|
||||
if (!weeklyWindow) weeklyWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
continue;
|
||||
}
|
||||
if (kind === 'unknown') {
|
||||
nonCodeReviewWindows.push(window);
|
||||
} else {
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
if (kind === 'usage-5h') {
|
||||
if (!fiveHourWindow) fiveHourWindow = 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)
|
||||
const rateLimit = payload.rate_limit || payload.rateLimit;
|
||||
const codeReviewRateLimit = payload.code_review_rate_limit || payload.codeReviewRateLimit;
|
||||
const additionalRateLimits = payload.additional_rate_limits || payload.additionalRateLimits;
|
||||
|
||||
// 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;
|
||||
|
||||
// Clamp usedPercent to [0, 100] range
|
||||
@@ -287,33 +332,68 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[]
|
||||
resetAt = new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
|
||||
}
|
||||
|
||||
windows.push({
|
||||
const window: CodexQuotaWindow = {
|
||||
label,
|
||||
usedPercent,
|
||||
remainingPercent: Math.max(0, 100 - usedPercent),
|
||||
resetAfterSeconds,
|
||||
resetAt,
|
||||
});
|
||||
category: meta.category,
|
||||
cadence: meta.cadence,
|
||||
};
|
||||
if (meta.featureLabel) {
|
||||
window.featureLabel = meta.featureLabel;
|
||||
}
|
||||
windows.push(window);
|
||||
};
|
||||
|
||||
// Add main rate limit windows
|
||||
if (rateLimit) {
|
||||
addWindow('Primary', rateLimit.primary_window || rateLimit.primaryWindow);
|
||||
addWindow('Secondary', rateLimit.secondary_window || rateLimit.secondaryWindow);
|
||||
addWindow('Primary', rateLimit.primary_window || rateLimit.primaryWindow, {
|
||||
category: 'usage',
|
||||
cadence: '5h',
|
||||
});
|
||||
addWindow('Secondary', rateLimit.secondary_window || rateLimit.secondaryWindow, {
|
||||
category: 'usage',
|
||||
cadence: 'weekly',
|
||||
});
|
||||
}
|
||||
|
||||
// Add code review rate limit windows
|
||||
if (codeReviewRateLimit) {
|
||||
addWindow(
|
||||
'Code Review (Primary)',
|
||||
codeReviewRateLimit.primary_window || codeReviewRateLimit.primaryWindow
|
||||
codeReviewRateLimit.primary_window || codeReviewRateLimit.primaryWindow,
|
||||
{ category: 'code-review', cadence: '5h', featureLabel: 'Code Review' }
|
||||
);
|
||||
addWindow(
|
||||
'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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
/** 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;
|
||||
/** Percentage used (0-100) */
|
||||
usedPercent: number;
|
||||
@@ -42,6 +42,18 @@ export interface CodexQuotaWindow {
|
||||
resetAfterSeconds: number | null;
|
||||
/** ISO timestamp when quota resets, null if unknown */
|
||||
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 */
|
||||
|
||||
@@ -238,7 +238,10 @@ function getCodexWindowKind(label: string): CodexWindowKind {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
type CodexWindowSummary = Pick<CodexQuotaResult['windows'][number], 'label' | 'resetAfterSeconds'>;
|
||||
type CodexWindowSummary = Pick<
|
||||
CodexQuotaResult['windows'][number],
|
||||
'label' | 'resetAfterSeconds' | 'category' | 'cadence' | 'featureLabel'
|
||||
>;
|
||||
|
||||
function inferCodeReviewCadence(
|
||||
window: CodexWindowSummary,
|
||||
@@ -272,12 +275,46 @@ function inferCodeReviewCadence(
|
||||
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(
|
||||
window: CodexWindowSummary,
|
||||
allWindows: CodexWindowSummary[] = []
|
||||
): string {
|
||||
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 || window.label || '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)) {
|
||||
case 'usage-5h':
|
||||
return '5h usage limit';
|
||||
@@ -304,20 +341,35 @@ function getCodexCoreUsageWindows(windows: CodexQuotaResult['windows']): {
|
||||
let weeklyWindow: CodexQuotaResult['windows'][number] | null = null;
|
||||
const nonCodeReviewWindows: CodexQuotaResult['windows'] = [];
|
||||
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
if (kind === 'usage-5h') {
|
||||
if (!fiveHourWindow) fiveHourWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
continue;
|
||||
// Prefer explicit category metadata when present so 'additional' windows
|
||||
// (e.g. GPT-5.3 Codex Spark) do not displace core usage windows in the summary.
|
||||
const hasCategoryMetadata = windows.some((window) => Boolean(window.category));
|
||||
|
||||
if (hasCategoryMetadata) {
|
||||
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') {
|
||||
if (!weeklyWindow) weeklyWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
continue;
|
||||
}
|
||||
if (kind === 'unknown') {
|
||||
nonCodeReviewWindows.push(window);
|
||||
} else {
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
if (kind === 'usage-5h') {
|
||||
if (!fiveHourWindow) fiveHourWindow = 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].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', () => {
|
||||
@@ -311,6 +427,36 @@ describe('Codex Quota Fetcher', () => {
|
||||
expect(summary.fiveHour).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', () => {
|
||||
|
||||
@@ -259,11 +259,17 @@ export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentPro
|
||||
|
||||
// Codex provider tooltip
|
||||
if (isCodexQuotaResult(quota)) {
|
||||
const { fiveHourWindow, weeklyWindow, codeReviewWindows, unknownWindows } =
|
||||
const { fiveHourWindow, weeklyWindow, codeReviewWindows, additionalWindows, unknownWindows } =
|
||||
getCodexQuotaBreakdown(quota.windows);
|
||||
const fiveHourResetAt = quota.coreUsage?.fiveHour?.resetAt ?? fiveHourWindow?.resetAt ?? null;
|
||||
const weeklyResetAt = quota.coreUsage?.weekly?.resetAt ?? weeklyWindow?.resetAt ?? null;
|
||||
const orderedWindows = [fiveHourWindow, weeklyWindow, ...codeReviewWindows, ...unknownWindows]
|
||||
const orderedWindows = [
|
||||
fiveHourWindow,
|
||||
weeklyWindow,
|
||||
...codeReviewWindows,
|
||||
...additionalWindows,
|
||||
...unknownWindows,
|
||||
]
|
||||
.filter((w): w is NonNullable<typeof w> => !!w)
|
||||
.filter(
|
||||
(w, index, arr) =>
|
||||
|
||||
@@ -645,7 +645,11 @@ export interface QuotaResult {
|
||||
|
||||
/** Codex rate limit window */
|
||||
export interface CodexQuotaWindow {
|
||||
/** Window label: "Primary", "Secondary", "Code Review (Primary)", "Code Review (Secondary)" */
|
||||
/**
|
||||
* Window label, e.g.: "Primary", "Secondary",
|
||||
* "Code Review (Primary)", "Code Review (Secondary)",
|
||||
* or "GPT-5.3-Codex-Spark (Primary)" for additional rate limits.
|
||||
*/
|
||||
label: string;
|
||||
/** Percentage used (0-100) */
|
||||
usedPercent: number;
|
||||
@@ -655,6 +659,18 @@ export interface CodexQuotaWindow {
|
||||
resetAfterSeconds: number | null;
|
||||
/** ISO timestamp when quota resets, null if unknown */
|
||||
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 feature 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 */
|
||||
|
||||
@@ -1626,6 +1626,9 @@ const resources = {
|
||||
codeReview5h: 'Code review (5h)',
|
||||
codeReviewWeekly: 'Code review (weekly)',
|
||||
codeReview: 'Code review',
|
||||
codexAdditional5h: '{{name}} (5h)',
|
||||
codexAdditionalWeekly: '{{name}} (weekly)',
|
||||
codexAdditional: '{{name}}',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: 'Sponsor this project on GitHub',
|
||||
@@ -4098,6 +4101,9 @@ const resources = {
|
||||
codeReview5h: '代码审查(5 小时)',
|
||||
codeReviewWeekly: '代码审查(每周)',
|
||||
codeReview: '代码审查',
|
||||
codexAdditional5h: '{{name}}(5 小时)',
|
||||
codexAdditionalWeekly: '{{name}}(每周)',
|
||||
codexAdditional: '{{name}}',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: '在 GitHub 上赞助此项目',
|
||||
@@ -6630,6 +6636,9 @@ const resources = {
|
||||
codeReview5h: 'Đánh giá mã (5h)',
|
||||
codeReviewWeekly: 'Đánh giá mã (hàng tuần)',
|
||||
codeReview: 'Đánh giá mã',
|
||||
codexAdditional5h: '{{name}} (5h)',
|
||||
codexAdditionalWeekly: '{{name}} (hàng tuần)',
|
||||
codexAdditional: '{{name}}',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: 'Tài trợ dự án này trên GitHub',
|
||||
@@ -9766,6 +9775,9 @@ const resources = {
|
||||
codeReview5h: 'コードレビュー(5時間)',
|
||||
codeReviewWeekly: 'コードレビュー(週次)',
|
||||
codeReview: 'コードレビュー',
|
||||
codexAdditional5h: '{{name}}(5時間)',
|
||||
codexAdditionalWeekly: '{{name}}(週次)',
|
||||
codexAdditional: '{{name}}',
|
||||
},
|
||||
rawEditorSection: {
|
||||
rawConfig: 'Raw 設定',
|
||||
|
||||
+122
-25
@@ -332,12 +332,33 @@ export type CodexWindowKind =
|
||||
| 'code-review-5h'
|
||||
| 'code-review-weekly'
|
||||
| 'code-review'
|
||||
| 'additional-5h'
|
||||
| 'additional-weekly'
|
||||
| 'unknown';
|
||||
|
||||
/**
|
||||
* Map raw Codex API window labels into semantic buckets.
|
||||
* Map a Codex window into a semantic bucket. Prefers explicit category metadata
|
||||
* (post-2026-04 windows) and falls back to label sniffing for legacy cached data.
|
||||
*/
|
||||
export function getCodexWindowKind(label: string): CodexWindowKind {
|
||||
export function getCodexWindowKind(labelOrWindow: string | CodexWindowSummary): CodexWindowKind {
|
||||
if (typeof labelOrWindow === 'object' && labelOrWindow !== null) {
|
||||
const w = labelOrWindow;
|
||||
if (w.category === 'additional' && w.cadence) {
|
||||
return w.cadence === 'weekly' ? 'additional-weekly' : 'additional-5h';
|
||||
}
|
||||
if (w.category === 'code-review' && w.cadence) {
|
||||
return w.cadence === 'weekly' ? 'code-review-weekly' : 'code-review-5h';
|
||||
}
|
||||
if (w.category === 'usage' && w.cadence) {
|
||||
return w.cadence === 'weekly' ? 'usage-weekly' : 'usage-5h';
|
||||
}
|
||||
// Missing or incomplete category metadata -> fall through to label sniffing.
|
||||
return getCodexWindowKindFromLabel(w.label);
|
||||
}
|
||||
return getCodexWindowKindFromLabel(labelOrWindow);
|
||||
}
|
||||
|
||||
function getCodexWindowKindFromLabel(label: string): CodexWindowKind {
|
||||
const lower = (label || '').toLowerCase();
|
||||
const isCodeReview = lower.includes('code review') || lower.includes('code_review');
|
||||
const isPrimary = lower.includes('primary');
|
||||
@@ -354,7 +375,29 @@ export function getCodexWindowKind(label: string): CodexWindowKind {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
type CodexWindowSummary = Pick<CodexQuotaWindow, 'label' | 'resetAfterSeconds'>;
|
||||
/**
|
||||
* Strip a leading "GPT-X.Y-Codex-" prefix from a feature label and turn the
|
||||
* remainder into a "Codex <Feature>" display name. Other labels pass through unchanged.
|
||||
*
|
||||
* Examples:
|
||||
* "GPT-5.3-Codex-Spark" -> "Codex Spark"
|
||||
* "OmniReview" -> "OmniReview"
|
||||
* "" -> ""
|
||||
*/
|
||||
export function prettifyCodexFeatureLabel(featureLabel: string): string {
|
||||
const trimmed = (featureLabel || '').trim();
|
||||
if (!trimmed) return '';
|
||||
const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, '');
|
||||
if (stripped !== trimmed && stripped.length > 0) {
|
||||
return `Codex ${stripped}`;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
type CodexWindowSummary = Pick<
|
||||
CodexQuotaWindow,
|
||||
'label' | 'resetAfterSeconds' | 'category' | 'cadence' | 'featureLabel'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Infer code-review window cadence by comparing against usage windows.
|
||||
@@ -364,7 +407,7 @@ function inferCodeReviewCadence(
|
||||
window: CodexWindowSummary,
|
||||
allWindows: CodexWindowSummary[]
|
||||
): '5h' | 'weekly' | null {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
const kind = getCodexWindowKind(window);
|
||||
if (kind === 'code-review-weekly') return 'weekly';
|
||||
|
||||
const reset = window.resetAfterSeconds;
|
||||
@@ -372,14 +415,14 @@ function inferCodeReviewCadence(
|
||||
|
||||
const usage5h = allWindows.find(
|
||||
(w) =>
|
||||
getCodexWindowKind(w.label) === 'usage-5h' &&
|
||||
getCodexWindowKind(w) === 'usage-5h' &&
|
||||
typeof w.resetAfterSeconds === 'number' &&
|
||||
isFinite(w.resetAfterSeconds) &&
|
||||
w.resetAfterSeconds > 0
|
||||
);
|
||||
const usageWeekly = allWindows.find(
|
||||
(w) =>
|
||||
getCodexWindowKind(w.label) === 'usage-weekly' &&
|
||||
getCodexWindowKind(w) === 'usage-weekly' &&
|
||||
typeof w.resetAfterSeconds === 'number' &&
|
||||
isFinite(w.resetAfterSeconds) &&
|
||||
w.resetAfterSeconds > 0
|
||||
@@ -400,10 +443,16 @@ export function getCodexWindowDisplayLabel(
|
||||
const currentWindow: CodexWindowSummary =
|
||||
typeof labelOrWindow === 'string'
|
||||
? { label, resetAfterSeconds: null }
|
||||
: { label, resetAfterSeconds: labelOrWindow.resetAfterSeconds };
|
||||
: {
|
||||
label,
|
||||
resetAfterSeconds: labelOrWindow.resetAfterSeconds,
|
||||
category: labelOrWindow.category,
|
||||
cadence: labelOrWindow.cadence,
|
||||
featureLabel: labelOrWindow.featureLabel,
|
||||
};
|
||||
const context = allWindows.length > 0 ? allWindows : [currentWindow];
|
||||
|
||||
switch (getCodexWindowKind(label)) {
|
||||
switch (getCodexWindowKind(currentWindow)) {
|
||||
case 'usage-5h':
|
||||
return i18n.t('quotaTooltip.fiveHourLimit');
|
||||
case 'usage-weekly':
|
||||
@@ -416,6 +465,15 @@ export function getCodexWindowDisplayLabel(
|
||||
if (inferred === 'weekly') return i18n.t('utils.codeReviewWeekly');
|
||||
return i18n.t('utils.codeReview');
|
||||
}
|
||||
case 'additional-5h':
|
||||
case 'additional-weekly': {
|
||||
const pretty = prettifyCodexFeatureLabel(currentWindow.featureLabel ?? '');
|
||||
const name = pretty || label;
|
||||
const kind = getCodexWindowKind(currentWindow);
|
||||
if (kind === 'additional-5h') return i18n.t('utils.codexAdditional5h', { name });
|
||||
if (kind === 'additional-weekly') return i18n.t('utils.codexAdditionalWeekly', { name });
|
||||
return i18n.t('utils.codexAdditional', { name });
|
||||
}
|
||||
case 'unknown':
|
||||
return label;
|
||||
}
|
||||
@@ -425,11 +483,18 @@ export interface CodexQuotaBreakdown {
|
||||
fiveHourWindow: CodexQuotaWindow | null;
|
||||
weeklyWindow: CodexQuotaWindow | null;
|
||||
codeReviewWindows: CodexQuotaWindow[];
|
||||
/** Additional rate-limit windows (e.g. GPT-5.3 Codex Spark). Excluded from core 5h/weekly summary. */
|
||||
additionalWindows: CodexQuotaWindow[];
|
||||
unknownWindows: CodexQuotaWindow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Break down Codex windows into core usage windows (5h + weekly) and auxiliary windows.
|
||||
* Break down Codex windows into core usage windows (5h + weekly), code review,
|
||||
* additional (e.g. Spark), and unknown buckets.
|
||||
*
|
||||
* Prefers explicit category metadata when present so 'additional' windows
|
||||
* (e.g. GPT-5.3 Codex Spark) do not displace core usage windows in the summary.
|
||||
* Falls back to label sniffing for legacy cached data without metadata.
|
||||
*/
|
||||
export function getCodexQuotaBreakdown(windows: CodexQuotaWindow[]): CodexQuotaBreakdown {
|
||||
if (!windows || windows.length === 0) {
|
||||
@@ -437,6 +502,7 @@ export function getCodexQuotaBreakdown(windows: CodexQuotaWindow[]): CodexQuotaB
|
||||
fiveHourWindow: null,
|
||||
weeklyWindow: null,
|
||||
codeReviewWindows: [],
|
||||
additionalWindows: [],
|
||||
unknownWindows: [],
|
||||
};
|
||||
}
|
||||
@@ -444,34 +510,64 @@ export function getCodexQuotaBreakdown(windows: CodexQuotaWindow[]): CodexQuotaB
|
||||
let fiveHourWindow: CodexQuotaWindow | null = null;
|
||||
let weeklyWindow: CodexQuotaWindow | null = null;
|
||||
const codeReviewWindows: CodexQuotaWindow[] = [];
|
||||
const additionalWindows: CodexQuotaWindow[] = [];
|
||||
const unknownWindows: CodexQuotaWindow[] = [];
|
||||
// Eligible windows for the 5h/weekly fallback (must NOT include code-review or additional).
|
||||
const nonCodeReviewWindows: CodexQuotaWindow[] = [];
|
||||
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
const hasCategoryMetadata = windows.some((w) => Boolean(w.category));
|
||||
|
||||
switch (kind) {
|
||||
case 'usage-5h':
|
||||
if (!fiveHourWindow) fiveHourWindow = window;
|
||||
if (hasCategoryMetadata) {
|
||||
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);
|
||||
break;
|
||||
case 'usage-weekly':
|
||||
if (!weeklyWindow) weeklyWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
break;
|
||||
case 'code-review-5h':
|
||||
case 'code-review-weekly':
|
||||
case 'code-review':
|
||||
} else if (window.category === 'code-review') {
|
||||
codeReviewWindows.push(window);
|
||||
break;
|
||||
case 'unknown':
|
||||
} else if (window.category === 'additional') {
|
||||
additionalWindows.push(window);
|
||||
} else {
|
||||
// Window has no category but the batch carries metadata for others -> treat as unknown.
|
||||
unknownWindows.push(window);
|
||||
nonCodeReviewWindows.push(window);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy path: classify via label sniffing for cached windows without metadata.
|
||||
for (const window of windows) {
|
||||
const kind = getCodexWindowKind(window.label);
|
||||
|
||||
switch (kind) {
|
||||
case 'usage-5h':
|
||||
if (!fiveHourWindow) fiveHourWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
break;
|
||||
case 'usage-weekly':
|
||||
if (!weeklyWindow) weeklyWindow = window;
|
||||
nonCodeReviewWindows.push(window);
|
||||
break;
|
||||
case 'code-review-5h':
|
||||
case 'code-review-weekly':
|
||||
case 'code-review':
|
||||
codeReviewWindows.push(window);
|
||||
break;
|
||||
case 'additional-5h':
|
||||
case 'additional-weekly':
|
||||
// Unreachable from label-only kind, but kept for exhaustiveness.
|
||||
additionalWindows.push(window);
|
||||
break;
|
||||
case 'unknown':
|
||||
unknownWindows.push(window);
|
||||
nonCodeReviewWindows.push(window);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for API label changes: infer 5h/weekly from reset horizon when explicit labels are absent.
|
||||
// 'additional' windows are intentionally excluded from this pool to avoid leaking Spark windows
|
||||
// into the core usage badges on a fresh Pro account.
|
||||
if ((!fiveHourWindow || !weeklyWindow) && nonCodeReviewWindows.length > 0) {
|
||||
const withReset = nonCodeReviewWindows
|
||||
.filter((w) => typeof w.resetAfterSeconds === 'number' && w.resetAfterSeconds >= 0)
|
||||
@@ -493,6 +589,7 @@ export function getCodexQuotaBreakdown(windows: CodexQuotaWindow[]): CodexQuotaB
|
||||
fiveHourWindow,
|
||||
weeklyWindow,
|
||||
codeReviewWindows,
|
||||
additionalWindows,
|
||||
unknownWindows,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Tests for Codex quota breakdown logic.
|
||||
*
|
||||
* Covers the dashboard-side mirror of the server's CodexQuotaWindow handling:
|
||||
* additional rate-limit windows (e.g. GPT-5.3 Codex Spark) must NOT pollute
|
||||
* the core 5h/weekly buckets, and category metadata takes precedence over
|
||||
* label sniffing when present.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
getCodexQuotaBreakdown,
|
||||
getCodexWindowKind,
|
||||
getCodexWindowDisplayLabel,
|
||||
prettifyCodexFeatureLabel,
|
||||
} from '@/lib/utils';
|
||||
import type { CodexQuotaWindow } from '@/lib/api-client';
|
||||
|
||||
describe('prettifyCodexFeatureLabel', () => {
|
||||
it('strips "GPT-X.Y-Codex-" prefix and prepends "Codex"', () => {
|
||||
expect(prettifyCodexFeatureLabel('GPT-5.3-Codex-Spark')).toBe('Codex Spark');
|
||||
});
|
||||
|
||||
it('is case-insensitive when stripping the GPT prefix', () => {
|
||||
expect(prettifyCodexFeatureLabel('gpt-5.3-codex-spark')).toBe('Codex spark');
|
||||
});
|
||||
|
||||
it('returns labels without the GPT prefix verbatim', () => {
|
||||
expect(prettifyCodexFeatureLabel('OmniReview')).toBe('OmniReview');
|
||||
});
|
||||
|
||||
it('returns empty string for empty input', () => {
|
||||
expect(prettifyCodexFeatureLabel('')).toBe('');
|
||||
});
|
||||
|
||||
it('trims whitespace and handles whitespace-only input', () => {
|
||||
expect(prettifyCodexFeatureLabel(' ')).toBe('');
|
||||
expect(prettifyCodexFeatureLabel(' GPT-5.3-Codex-Spark ')).toBe('Codex Spark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCodexWindowKind', () => {
|
||||
it('uses category metadata when present (additional weekly)', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'GPT-5.3-Codex-Spark (Secondary)',
|
||||
usedPercent: 10,
|
||||
remainingPercent: 90,
|
||||
resetAfterSeconds: 600000,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: 'weekly',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
};
|
||||
expect(getCodexWindowKind(window)).toBe('additional-weekly');
|
||||
});
|
||||
|
||||
it('uses category metadata when present (additional 5h)', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'GPT-5.3-Codex-Spark (Primary)',
|
||||
usedPercent: 5,
|
||||
remainingPercent: 95,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T00:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: '5h',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
};
|
||||
expect(getCodexWindowKind(window)).toBe('additional-5h');
|
||||
});
|
||||
|
||||
it('uses category metadata when present (usage 5h)', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'Primary',
|
||||
usedPercent: 25,
|
||||
remainingPercent: 75,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T00:00:00Z',
|
||||
category: 'usage',
|
||||
cadence: '5h',
|
||||
};
|
||||
expect(getCodexWindowKind(window)).toBe('usage-5h');
|
||||
});
|
||||
|
||||
it('uses category metadata when present (code-review weekly)', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'Code Review (Secondary)',
|
||||
usedPercent: 50,
|
||||
remainingPercent: 50,
|
||||
resetAfterSeconds: 604800,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'code-review',
|
||||
cadence: 'weekly',
|
||||
featureLabel: 'Code Review',
|
||||
};
|
||||
expect(getCodexWindowKind(window)).toBe('code-review-weekly');
|
||||
});
|
||||
|
||||
it('falls back to label sniffing for legacy windows without category', () => {
|
||||
expect(getCodexWindowKind('Primary')).toBe('usage-5h');
|
||||
expect(getCodexWindowKind('Secondary')).toBe('usage-weekly');
|
||||
expect(getCodexWindowKind('Code Review (Primary)')).toBe('code-review-5h');
|
||||
expect(getCodexWindowKind('Code Review (Secondary)')).toBe('code-review-weekly');
|
||||
expect(getCodexWindowKind('Code Review')).toBe('code-review');
|
||||
expect(getCodexWindowKind('Random Label')).toBe('unknown');
|
||||
});
|
||||
|
||||
it('falls back to label sniffing when window has no category set', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'Primary',
|
||||
usedPercent: 25,
|
||||
remainingPercent: 75,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T00:00:00Z',
|
||||
};
|
||||
expect(getCodexWindowKind(window)).toBe('usage-5h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCodexQuotaBreakdown', () => {
|
||||
it('returns empty buckets for empty windows', () => {
|
||||
const result = getCodexQuotaBreakdown([]);
|
||||
expect(result.fiveHourWindow).toBeNull();
|
||||
expect(result.weeklyWindow).toBeNull();
|
||||
expect(result.codeReviewWindows).toEqual([]);
|
||||
expect(result.additionalWindows).toEqual([]);
|
||||
expect(result.unknownWindows).toEqual([]);
|
||||
});
|
||||
|
||||
it('routes Pro-account windows by category metadata (Spark stays out of core)', () => {
|
||||
const windows: CodexQuotaWindow[] = [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 0,
|
||||
remainingPercent: 100,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
category: 'usage',
|
||||
cadence: '5h',
|
||||
},
|
||||
{
|
||||
label: 'Secondary',
|
||||
usedPercent: 0,
|
||||
remainingPercent: 100,
|
||||
resetAfterSeconds: 604800,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'usage',
|
||||
cadence: 'weekly',
|
||||
},
|
||||
{
|
||||
label: 'GPT-5.3-Codex-Spark (Primary)',
|
||||
usedPercent: 12,
|
||||
remainingPercent: 88,
|
||||
// Intentionally short reset to verify Spark cannot displace core 5h via reset-horizon fallback.
|
||||
resetAfterSeconds: 60,
|
||||
resetAt: '2026-04-27T00:01:00Z',
|
||||
category: 'additional',
|
||||
cadence: '5h',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
},
|
||||
{
|
||||
label: 'GPT-5.3-Codex-Spark (Secondary)',
|
||||
usedPercent: 30,
|
||||
remainingPercent: 70,
|
||||
resetAfterSeconds: 600000,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: 'weekly',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
},
|
||||
{
|
||||
label: 'Code Review (Primary)',
|
||||
usedPercent: 25,
|
||||
remainingPercent: 75,
|
||||
resetAfterSeconds: 17000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
category: 'code-review',
|
||||
cadence: '5h',
|
||||
featureLabel: 'Code Review',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getCodexQuotaBreakdown(windows);
|
||||
|
||||
expect(result.fiveHourWindow?.label).toBe('Primary');
|
||||
expect(result.fiveHourWindow?.category).toBe('usage');
|
||||
expect(result.weeklyWindow?.label).toBe('Secondary');
|
||||
expect(result.weeklyWindow?.category).toBe('usage');
|
||||
|
||||
expect(result.additionalWindows).toHaveLength(2);
|
||||
expect(result.additionalWindows.map((w) => w.featureLabel)).toEqual([
|
||||
'GPT-5.3-Codex-Spark',
|
||||
'GPT-5.3-Codex-Spark',
|
||||
]);
|
||||
expect(result.additionalWindows.map((w) => w.cadence)).toEqual(['5h', 'weekly']);
|
||||
|
||||
expect(result.codeReviewWindows).toHaveLength(1);
|
||||
expect(result.codeReviewWindows[0]?.featureLabel).toBe('Code Review');
|
||||
|
||||
expect(result.unknownWindows).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty additionalWindows for Plus-shape data without additional categories', () => {
|
||||
const windows: CodexQuotaWindow[] = [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 30,
|
||||
remainingPercent: 70,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
category: 'usage',
|
||||
cadence: '5h',
|
||||
},
|
||||
{
|
||||
label: 'Secondary',
|
||||
usedPercent: 60,
|
||||
remainingPercent: 40,
|
||||
resetAfterSeconds: 604800,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'usage',
|
||||
cadence: 'weekly',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getCodexQuotaBreakdown(windows);
|
||||
|
||||
expect(result.fiveHourWindow?.label).toBe('Primary');
|
||||
expect(result.weeklyWindow?.label).toBe('Secondary');
|
||||
expect(result.additionalWindows).toEqual([]);
|
||||
expect(result.codeReviewWindows).toEqual([]);
|
||||
expect(result.unknownWindows).toEqual([]);
|
||||
});
|
||||
|
||||
it('falls back to label sniffing for legacy cached windows without metadata', () => {
|
||||
const windows: CodexQuotaWindow[] = [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 25,
|
||||
remainingPercent: 75,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
},
|
||||
{
|
||||
label: 'Secondary',
|
||||
usedPercent: 65,
|
||||
remainingPercent: 35,
|
||||
resetAfterSeconds: 604800,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
},
|
||||
{
|
||||
label: 'Code Review (Primary)',
|
||||
usedPercent: 10,
|
||||
remainingPercent: 90,
|
||||
resetAfterSeconds: 17000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
},
|
||||
{
|
||||
label: 'Code Review (Secondary)',
|
||||
usedPercent: 5,
|
||||
remainingPercent: 95,
|
||||
resetAfterSeconds: 600000,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
const result = getCodexQuotaBreakdown(windows);
|
||||
|
||||
expect(result.fiveHourWindow?.label).toBe('Primary');
|
||||
expect(result.weeklyWindow?.label).toBe('Secondary');
|
||||
expect(result.codeReviewWindows).toHaveLength(2);
|
||||
expect(result.additionalWindows).toEqual([]);
|
||||
expect(result.unknownWindows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCodexWindowDisplayLabel for additional windows', () => {
|
||||
it('renders additional 5h windows with prettified Codex feature name', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'GPT-5.3-Codex-Spark (Primary)',
|
||||
usedPercent: 12,
|
||||
remainingPercent: 88,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: '5h',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
};
|
||||
expect(getCodexWindowDisplayLabel(window)).toBe('Codex Spark (5h)');
|
||||
});
|
||||
|
||||
it('renders additional weekly windows with prettified Codex feature name', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'GPT-5.3-Codex-Spark (Secondary)',
|
||||
usedPercent: 30,
|
||||
remainingPercent: 70,
|
||||
resetAfterSeconds: 600000,
|
||||
resetAt: '2026-05-04T00:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: 'weekly',
|
||||
featureLabel: 'GPT-5.3-Codex-Spark',
|
||||
};
|
||||
expect(getCodexWindowDisplayLabel(window)).toBe('Codex Spark (weekly)');
|
||||
});
|
||||
|
||||
it('falls back to raw label when featureLabel is missing', () => {
|
||||
const window: CodexQuotaWindow = {
|
||||
label: 'OmniReview (Primary)',
|
||||
usedPercent: 0,
|
||||
remainingPercent: 100,
|
||||
resetAfterSeconds: 18000,
|
||||
resetAt: '2026-04-28T05:00:00Z',
|
||||
category: 'additional',
|
||||
cadence: '5h',
|
||||
};
|
||||
expect(getCodexWindowDisplayLabel(window)).toBe('OmniReview (Primary) (5h)');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user