fix: harden cliproxy usage cache

This commit is contained in:
Kai (Tam Nhu) Tran
2026-05-30 15:18:11 -04:00
committed by Tam Nhu Tran
parent 62a4d44b58
commit 6bba193cdd
4 changed files with 65 additions and 16 deletions
+20 -9
View File
@@ -50,6 +50,8 @@ const HISTORY_RETENTION_DAYS = Math.max(
);
const HISTORY_RETENTION_MS = HISTORY_RETENTION_DAYS * 24 * 60 * 60 * 1000;
const MAX_WRITE_ATTEMPTS = 3;
const PRIVATE_DIR_MODE = 0o700;
const PRIVATE_FILE_MODE = 0o600;
/** Sync interval in ms, configurable via CCS_CLIPROXY_SYNC_INTERVAL env var (default: 5 min) */
const SYNC_INTERVAL_MS = Math.max(
@@ -68,11 +70,19 @@ function getLatestSnapshotPath(): string {
return path.join(getCliproxyCacheDir(), 'latest.json');
}
function ensurePrivateDirectory(dir: string): void {
fs.mkdirSync(dir, { recursive: true, mode: PRIVATE_DIR_MODE });
fs.chmodSync(dir, PRIVATE_DIR_MODE);
}
function ensureCliproxyCacheDir(): void {
const dir = getCliproxyCacheDir();
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const ccsDir = getCcsDir();
const cacheDir = path.join(ccsDir, 'cache');
const cliproxyCacheDir = path.join(cacheDir, 'cliproxy-usage');
ensurePrivateDirectory(ccsDir);
ensurePrivateDirectory(cacheDir);
ensurePrivateDirectory(cliproxyCacheDir);
}
function getSnapshotTimestamp(): number {
@@ -112,8 +122,6 @@ function buildLegacyHistoryDetails(
details.push({
model: breakdown.modelName,
timestamp: buildHourlyTimestamp(hour.hour),
source: hour.source,
authIndex: 'legacy-hourly',
inputTokens: breakdown.inputTokens,
outputTokens: breakdown.outputTokens,
cacheReadTokens: breakdown.cacheReadTokens,
@@ -135,8 +143,6 @@ function buildLegacyHistoryDetails(
details.push({
model: breakdown.modelName,
timestamp: buildDailyTimestamp(day.date),
source: day.source,
authIndex: 'legacy-daily',
inputTokens: breakdown.inputTokens,
outputTokens: breakdown.outputTokens,
cacheReadTokens: breakdown.cacheReadTokens,
@@ -255,7 +261,11 @@ async function writeSnapshotWithMerge(
const snapshot = buildSnapshot(baseSnapshot?.details ?? [], incomingDetails);
const tempFile = `${snapshotPath}.${process.pid}.${snapshot.timestamp}.tmp`;
fs.writeFileSync(tempFile, JSON.stringify(snapshot), 'utf-8');
fs.writeFileSync(tempFile, JSON.stringify(snapshot), {
encoding: 'utf-8',
mode: PRIVATE_FILE_MODE,
});
fs.chmodSync(tempFile, PRIVATE_FILE_MODE);
const latestSnapshot = readSnapshot(false);
const latestTimestamp = latestSnapshot?.timestamp ?? -Infinity;
@@ -265,6 +275,7 @@ async function writeSnapshotWithMerge(
}
fs.renameSync(tempFile, snapshotPath);
fs.chmodSync(snapshotPath, PRIVATE_FILE_MODE);
console.log(ok('CLIProxy usage snapshot updated'));
return;
}
@@ -22,8 +22,6 @@ export interface CliproxyUsageHistoryDetail {
model: string;
provider?: string;
timestamp: string;
source: string;
authIndex: string;
inputTokens: number;
outputTokens: number;
cacheReadTokens: number;
@@ -68,8 +66,6 @@ function createHistoryDetail(
model,
provider: pricingProvider,
timestamp: detail.timestamp,
source: detail.source,
authIndex: String(detail.auth_index),
inputTokens: detail.tokens?.input_tokens ?? 0,
outputTokens: detail.tokens?.output_tokens ?? 0,
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
@@ -128,13 +124,25 @@ export function extractCliproxyUsageHistoryDetails(
return results;
}
function sanitizeHistoryDetail(detail: CliproxyUsageHistoryDetail): CliproxyUsageHistoryDetail {
return {
model: detail.model,
...(detail.provider && { provider: detail.provider }),
timestamp: detail.timestamp,
inputTokens: detail.inputTokens,
outputTokens: detail.outputTokens,
cacheReadTokens: detail.cacheReadTokens,
requestCount: detail.requestCount,
cost: detail.cost,
failed: detail.failed,
};
}
function createHistorySignature(detail: CliproxyUsageHistoryDetail): string {
return [
detail.model,
detail.provider ?? '',
detail.timestamp,
detail.source,
detail.authIndex,
detail.inputTokens,
detail.outputTokens,
detail.cacheReadTokens,
@@ -182,7 +190,7 @@ export function mergeCliproxyUsageHistoryDetails(
const merged: CliproxyUsageHistoryDetail[] = [];
for (const { detail, count } of existingCounts.values()) {
for (let index = 0; index < count; index++) {
merged.push({ ...detail });
merged.push(sanitizeHistoryDetail(detail));
}
}
@@ -109,6 +109,21 @@ describe('cliproxy usage syncer', () => {
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
expect(fs.existsSync(snapshotPath)).toBe(true);
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')) as {
details: Array<Record<string, unknown>>;
};
expect(snapshot.details[0]).not.toHaveProperty('source');
expect(snapshot.details[0]).not.toHaveProperty('authIndex');
if (process.platform !== 'win32') {
const cacheDir = path.join(ccsDir, 'cache');
const cliproxyCacheDir = path.dirname(snapshotPath);
expect(fs.statSync(ccsDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(cacheDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(cliproxyCacheDir).mode & 0o777).toBe(0o700);
expect(fs.statSync(snapshotPath).mode & 0o777).toBe(0o600);
}
const cached = await runWithScopedConfigDir(ccsDir, async () => {
return await loadCachedCliproxyData();
});
@@ -109,6 +109,8 @@ describe('cliproxy usage transformer', () => {
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
expect(flat).toHaveLength(4);
expect(flat[0].provider).toBe('google');
expect(flat[0]).not.toHaveProperty('source');
expect(flat[0]).not.toHaveProperty('authIndex');
expect(
flat.some(
(entry) =>
@@ -134,6 +136,19 @@ describe('cliproxy usage transformer', () => {
expect(merged).toHaveLength(details.length);
});
it('strips legacy account identifiers when merging persisted history', () => {
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
const legacyDetail = {
...details[0],
source: 'user@example.com',
authIndex: 'auth-file-7',
};
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail], []);
expect(merged[0]).not.toHaveProperty('source');
expect(merged[0]).not.toHaveProperty('authIndex');
});
it('preserves legitimate duplicate requests when the incoming batch has more occurrences', () => {
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
const repeated = [details[0], { ...details[0] }];