mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix: harden cliproxy usage cache
This commit is contained in:
committed by
Tam Nhu Tran
parent
62a4d44b58
commit
6bba193cdd
@@ -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] }];
|
||||
|
||||
Reference in New Issue
Block a user