mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 10:15:40 +00:00
fix(cliproxy): sanitize Codex quota labels
This commit is contained in:
1 parent
c21f589247
commit
457412277b
5 files changed
+105
-10
No files matched your search
@@ -382,6 +382,44 @@ describe('Codex Quota Fetcher', () => {
|
|||||||
expect(windows[0].featureLabel).toBe('Custom-Feature');
|
expect(windows[0].featureLabel).toBe('Custom-Feature');
|
||||||
expect(windows[0].usedPercent).toBe(50);
|
expect(windows[0].usedPercent).toBe(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should remove terminal control characters from additional limit labels', () => {
|
||||||
|
const response = {
|
||||||
|
additional_rate_limits: [
|
||||||
|
{
|
||||||
|
limit_name: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007',
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 25, reset_after_seconds: 3600 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].featureLabel).toBe('GPT-5.3-Codex-Spark');
|
||||||
|
expect(windows[0].label).toBe('GPT-5.3-Codex-Spark (Primary)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bound additional limit labels before storing them', () => {
|
||||||
|
const response = {
|
||||||
|
additional_rate_limits: [
|
||||||
|
{
|
||||||
|
limit_name: `Feature-${'x'.repeat(120)}`,
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: { used_percent: 25, reset_after_seconds: 3600 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].featureLabel).toHaveLength(80);
|
||||||
|
expect(windows[0].label).toBe(`${windows[0].featureLabel} (Primary)`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('buildCodexCoreUsageSummary', () => {
|
describe('buildCodexCoreUsageSummary', () => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getAuthDir } from '../config/config-generator';
|
|||||||
import { getAccount, getProviderAccounts, getPausedDir } from '../accounts/account-manager';
|
import { getAccount, getProviderAccounts, getPausedDir } from '../accounts/account-manager';
|
||||||
import { sanitizeEmail, isTokenExpired } from '../auth/auth-utils';
|
import { sanitizeEmail, isTokenExpired } from '../auth/auth-utils';
|
||||||
import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types';
|
import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types';
|
||||||
|
import { sanitizeCodexFeatureLabel } from './quota-label-sanitizer';
|
||||||
import { extractCanonicalEmailFromAccountId } from '../accounts/email-account-identity';
|
import { extractCanonicalEmailFromAccountId } from '../accounts/email-account-identity';
|
||||||
|
|
||||||
/** ChatGPT backend API base URL */
|
/** ChatGPT backend API base URL */
|
||||||
@@ -58,8 +59,8 @@ interface CodexRateLimitWindow {
|
|||||||
* Each entry surfaces its own primary/secondary windows under a feature-specific limit name.
|
* Each entry surfaces its own primary/secondary windows under a feature-specific limit name.
|
||||||
*/
|
*/
|
||||||
interface CodexAdditionalRateLimit {
|
interface CodexAdditionalRateLimit {
|
||||||
limit_name?: string;
|
limit_name?: unknown;
|
||||||
limitName?: string;
|
limitName?: unknown;
|
||||||
metered_feature?: string;
|
metered_feature?: string;
|
||||||
meteredFeature?: string;
|
meteredFeature?: string;
|
||||||
rate_limit?: CodexRateLimitWindow;
|
rate_limit?: CodexRateLimitWindow;
|
||||||
@@ -380,11 +381,7 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[]
|
|||||||
const entryRateLimit = entry.rate_limit || entry.rateLimit;
|
const entryRateLimit = entry.rate_limit || entry.rateLimit;
|
||||||
if (!entryRateLimit) continue;
|
if (!entryRateLimit) continue;
|
||||||
|
|
||||||
const rawFeatureLabel = entry.limit_name ?? entry.limitName;
|
const featureLabel = sanitizeCodexFeatureLabel(entry.limit_name ?? entry.limitName);
|
||||||
const featureLabel =
|
|
||||||
typeof rawFeatureLabel === 'string' && rawFeatureLabel.trim().length > 0
|
|
||||||
? rawFeatureLabel.trim()
|
|
||||||
: 'Additional';
|
|
||||||
addWindow(
|
addWindow(
|
||||||
`${featureLabel} (Primary)`,
|
`${featureLabel} (Primary)`,
|
||||||
entryRateLimit.primary_window || entryRateLimit.primaryWindow,
|
entryRateLimit.primary_window || entryRateLimit.primaryWindow,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const CODEX_FEATURE_LABEL_FALLBACK = 'Additional';
|
||||||
|
const CODEX_FEATURE_LABEL_MAX_LENGTH = 80;
|
||||||
|
const TERMINAL_ESCAPE_SEQUENCE_REGEX =
|
||||||
|
/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b\[[0-?]*[ -/]*[@-~]/g;
|
||||||
|
const TERMINAL_CONTROL_CHARS_REGEX = /[\u0000-\u001f\u007f-\u009f]/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize upstream Codex feature labels before storing or rendering them.
|
||||||
|
*
|
||||||
|
* The quota API is not schema-validated at runtime, so additional-rate-limit
|
||||||
|
* labels must be constrained to safe, printable strings before they reach the
|
||||||
|
* terminal.
|
||||||
|
*/
|
||||||
|
export function sanitizeCodexFeatureLabel(value: unknown): string {
|
||||||
|
if (typeof value !== 'string') return CODEX_FEATURE_LABEL_FALLBACK;
|
||||||
|
|
||||||
|
const sanitized = value
|
||||||
|
.replace(TERMINAL_ESCAPE_SEQUENCE_REGEX, '')
|
||||||
|
.replace(TERMINAL_CONTROL_CHARS_REGEX, '')
|
||||||
|
.trim()
|
||||||
|
.slice(0, CODEX_FEATURE_LABEL_MAX_LENGTH)
|
||||||
|
.trimEnd();
|
||||||
|
|
||||||
|
return sanitized.length > 0 ? sanitized : CODEX_FEATURE_LABEL_FALLBACK;
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from '../../cliproxy/accounts/account-manager';
|
} from '../../cliproxy/accounts/account-manager';
|
||||||
import { fetchAllProviderQuotas } from '../../cliproxy/quota/quota-fetcher';
|
import { fetchAllProviderQuotas } from '../../cliproxy/quota/quota-fetcher';
|
||||||
import { fetchAllCodexQuotas } from '../../cliproxy/quota/quota-fetcher-codex';
|
import { fetchAllCodexQuotas } from '../../cliproxy/quota/quota-fetcher-codex';
|
||||||
|
import { sanitizeCodexFeatureLabel } from '../../cliproxy/quota/quota-label-sanitizer';
|
||||||
import { fetchAllClaudeQuotas } from '../../cliproxy/quota/quota-fetcher-claude';
|
import { fetchAllClaudeQuotas } from '../../cliproxy/quota/quota-fetcher-claude';
|
||||||
import { pickMostRestrictiveClaudeWeeklyWindow } from '../../cliproxy/quota/quota-fetcher-claude-normalizer';
|
import { pickMostRestrictiveClaudeWeeklyWindow } from '../../cliproxy/quota/quota-fetcher-claude-normalizer';
|
||||||
import { fetchAllGeminiCliQuotas } from '../../cliproxy/quota/quota-fetcher-gemini-cli';
|
import { fetchAllGeminiCliQuotas } from '../../cliproxy/quota/quota-fetcher-gemini-cli';
|
||||||
@@ -279,8 +280,8 @@ function inferCodeReviewCadence(
|
|||||||
* Strip a leading "GPT-X.Y-Codex-" prefix from a feature label and turn the
|
* 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.
|
* remainder into a Codex-prefixed display name. Other labels pass through unchanged.
|
||||||
*/
|
*/
|
||||||
function prettifyCodexFeatureLabel(featureLabel: string): string {
|
function prettifyCodexFeatureLabel(featureLabel: unknown): string {
|
||||||
const trimmed = featureLabel.trim();
|
const trimmed = sanitizeCodexFeatureLabel(featureLabel);
|
||||||
if (!trimmed) return 'Additional';
|
if (!trimmed) return 'Additional';
|
||||||
const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, '');
|
const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, '');
|
||||||
if (stripped !== trimmed && stripped.length > 0) {
|
if (stripped !== trimmed && stripped.length > 0) {
|
||||||
@@ -302,7 +303,7 @@ function getCodexWindowDisplayLabel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (window.category === 'additional') {
|
if (window.category === 'additional') {
|
||||||
const pretty = prettifyCodexFeatureLabel(window.featureLabel || window.label || 'Additional');
|
const pretty = prettifyCodexFeatureLabel(window.featureLabel ?? window.label);
|
||||||
if (window.cadence === '5h') return `${pretty} (5h)`;
|
if (window.cadence === '5h') return `${pretty} (5h)`;
|
||||||
if (window.cadence === 'weekly') return `${pretty} (weekly)`;
|
if (window.cadence === 'weekly') return `${pretty} (weekly)`;
|
||||||
return pretty;
|
return pretty;
|
||||||
@@ -850,7 +851,9 @@ const QUOTA_PROVIDER_RUNTIME: Record<QuotaSupportedProvider, QuotaProviderRuntim
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const __testExports = {
|
export const __testExports = {
|
||||||
|
getCodexWindowDisplayLabel,
|
||||||
getQuotaFailureDisplayEntries,
|
getQuotaFailureDisplayEntries,
|
||||||
|
prettifyCodexFeatureLabel,
|
||||||
resolveDisplayedTier,
|
resolveDisplayedTier,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,3 +84,35 @@ describe('cliproxy quota subcommand failure formatting', () => {
|
|||||||
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
|
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cliproxy quota subcommand Codex label formatting', () => {
|
||||||
|
it('falls back for non-string cached Codex feature labels', async () => {
|
||||||
|
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
|
||||||
|
|
||||||
|
const label = getCodexWindowDisplayLabel({
|
||||||
|
label: 'ignored',
|
||||||
|
resetAfterSeconds: 3600,
|
||||||
|
category: 'additional',
|
||||||
|
cadence: '5h',
|
||||||
|
featureLabel: { unexpected: true },
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
expect(label).toBe('Additional (5h)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes terminal control characters from cached Codex feature labels', async () => {
|
||||||
|
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
|
||||||
|
|
||||||
|
const label = getCodexWindowDisplayLabel({
|
||||||
|
label: 'ignored',
|
||||||
|
resetAfterSeconds: 3600,
|
||||||
|
category: 'additional',
|
||||||
|
cadence: 'weekly',
|
||||||
|
featureLabel: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(label).toBe('Codex Spark (weekly)');
|
||||||
|
expect(label).not.toContain('\u001b');
|
||||||
|
expect(label).not.toContain('\u0007');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user