mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 14:15:56 +00:00
fix: harden cliproxy usage cache
This commit is contained in:
1 parent
62a4d44b58
commit
6bba193cdd
4 files changed
+65
-16
No files matched your search
@@ -50,6 +50,8 @@ const HISTORY_RETENTION_DAYS = Math.max(
|
|||||||
);
|
);
|
||||||
const HISTORY_RETENTION_MS = HISTORY_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
const HISTORY_RETENTION_MS = HISTORY_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
const MAX_WRITE_ATTEMPTS = 3;
|
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) */
|
/** Sync interval in ms, configurable via CCS_CLIPROXY_SYNC_INTERVAL env var (default: 5 min) */
|
||||||
const SYNC_INTERVAL_MS = Math.max(
|
const SYNC_INTERVAL_MS = Math.max(
|
||||||
@@ -68,11 +70,19 @@ function getLatestSnapshotPath(): string {
|
|||||||
return path.join(getCliproxyCacheDir(), 'latest.json');
|
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 {
|
function ensureCliproxyCacheDir(): void {
|
||||||
const dir = getCliproxyCacheDir();
|
const ccsDir = getCcsDir();
|
||||||
if (!fs.existsSync(dir)) {
|
const cacheDir = path.join(ccsDir, 'cache');
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
const cliproxyCacheDir = path.join(cacheDir, 'cliproxy-usage');
|
||||||
}
|
|
||||||
|
ensurePrivateDirectory(ccsDir);
|
||||||
|
ensurePrivateDirectory(cacheDir);
|
||||||
|
ensurePrivateDirectory(cliproxyCacheDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSnapshotTimestamp(): number {
|
function getSnapshotTimestamp(): number {
|
||||||
@@ -112,8 +122,6 @@ function buildLegacyHistoryDetails(
|
|||||||
details.push({
|
details.push({
|
||||||
model: breakdown.modelName,
|
model: breakdown.modelName,
|
||||||
timestamp: buildHourlyTimestamp(hour.hour),
|
timestamp: buildHourlyTimestamp(hour.hour),
|
||||||
source: hour.source,
|
|
||||||
authIndex: 'legacy-hourly',
|
|
||||||
inputTokens: breakdown.inputTokens,
|
inputTokens: breakdown.inputTokens,
|
||||||
outputTokens: breakdown.outputTokens,
|
outputTokens: breakdown.outputTokens,
|
||||||
cacheReadTokens: breakdown.cacheReadTokens,
|
cacheReadTokens: breakdown.cacheReadTokens,
|
||||||
@@ -135,8 +143,6 @@ function buildLegacyHistoryDetails(
|
|||||||
details.push({
|
details.push({
|
||||||
model: breakdown.modelName,
|
model: breakdown.modelName,
|
||||||
timestamp: buildDailyTimestamp(day.date),
|
timestamp: buildDailyTimestamp(day.date),
|
||||||
source: day.source,
|
|
||||||
authIndex: 'legacy-daily',
|
|
||||||
inputTokens: breakdown.inputTokens,
|
inputTokens: breakdown.inputTokens,
|
||||||
outputTokens: breakdown.outputTokens,
|
outputTokens: breakdown.outputTokens,
|
||||||
cacheReadTokens: breakdown.cacheReadTokens,
|
cacheReadTokens: breakdown.cacheReadTokens,
|
||||||
@@ -255,7 +261,11 @@ async function writeSnapshotWithMerge(
|
|||||||
const snapshot = buildSnapshot(baseSnapshot?.details ?? [], incomingDetails);
|
const snapshot = buildSnapshot(baseSnapshot?.details ?? [], incomingDetails);
|
||||||
const tempFile = `${snapshotPath}.${process.pid}.${snapshot.timestamp}.tmp`;
|
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 latestSnapshot = readSnapshot(false);
|
||||||
const latestTimestamp = latestSnapshot?.timestamp ?? -Infinity;
|
const latestTimestamp = latestSnapshot?.timestamp ?? -Infinity;
|
||||||
@@ -265,6 +275,7 @@ async function writeSnapshotWithMerge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(tempFile, snapshotPath);
|
fs.renameSync(tempFile, snapshotPath);
|
||||||
|
fs.chmodSync(snapshotPath, PRIVATE_FILE_MODE);
|
||||||
console.log(ok('CLIProxy usage snapshot updated'));
|
console.log(ok('CLIProxy usage snapshot updated'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ export interface CliproxyUsageHistoryDetail {
|
|||||||
model: string;
|
model: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
source: string;
|
|
||||||
authIndex: string;
|
|
||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
cacheReadTokens: number;
|
cacheReadTokens: number;
|
||||||
@@ -68,8 +66,6 @@ function createHistoryDetail(
|
|||||||
model,
|
model,
|
||||||
provider: pricingProvider,
|
provider: pricingProvider,
|
||||||
timestamp: detail.timestamp,
|
timestamp: detail.timestamp,
|
||||||
source: detail.source,
|
|
||||||
authIndex: String(detail.auth_index),
|
|
||||||
inputTokens: detail.tokens?.input_tokens ?? 0,
|
inputTokens: detail.tokens?.input_tokens ?? 0,
|
||||||
outputTokens: detail.tokens?.output_tokens ?? 0,
|
outputTokens: detail.tokens?.output_tokens ?? 0,
|
||||||
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
||||||
@@ -128,13 +124,25 @@ export function extractCliproxyUsageHistoryDetails(
|
|||||||
return results;
|
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 {
|
function createHistorySignature(detail: CliproxyUsageHistoryDetail): string {
|
||||||
return [
|
return [
|
||||||
detail.model,
|
detail.model,
|
||||||
detail.provider ?? '',
|
detail.provider ?? '',
|
||||||
detail.timestamp,
|
detail.timestamp,
|
||||||
detail.source,
|
|
||||||
detail.authIndex,
|
|
||||||
detail.inputTokens,
|
detail.inputTokens,
|
||||||
detail.outputTokens,
|
detail.outputTokens,
|
||||||
detail.cacheReadTokens,
|
detail.cacheReadTokens,
|
||||||
@@ -182,7 +190,7 @@ export function mergeCliproxyUsageHistoryDetails(
|
|||||||
const merged: CliproxyUsageHistoryDetail[] = [];
|
const merged: CliproxyUsageHistoryDetail[] = [];
|
||||||
for (const { detail, count } of existingCounts.values()) {
|
for (const { detail, count } of existingCounts.values()) {
|
||||||
for (let index = 0; index < count; index++) {
|
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');
|
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
|
||||||
expect(fs.existsSync(snapshotPath)).toBe(true);
|
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 () => {
|
const cached = await runWithScopedConfigDir(ccsDir, async () => {
|
||||||
return await loadCachedCliproxyData();
|
return await loadCachedCliproxyData();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ describe('cliproxy usage transformer', () => {
|
|||||||
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
|
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||||
expect(flat).toHaveLength(4);
|
expect(flat).toHaveLength(4);
|
||||||
expect(flat[0].provider).toBe('google');
|
expect(flat[0].provider).toBe('google');
|
||||||
|
expect(flat[0]).not.toHaveProperty('source');
|
||||||
|
expect(flat[0]).not.toHaveProperty('authIndex');
|
||||||
expect(
|
expect(
|
||||||
flat.some(
|
flat.some(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
@@ -134,6 +136,19 @@ describe('cliproxy usage transformer', () => {
|
|||||||
expect(merged).toHaveLength(details.length);
|
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', () => {
|
it('preserves legitimate duplicate requests when the incoming batch has more occurrences', () => {
|
||||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||||
const repeated = [details[0], { ...details[0] }];
|
const repeated = [details[0], { ...details[0] }];
|
||||||
|
|||||||
Reference in new issue
Block a user