mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
fix(cli-proxy): preserve historical analytics across syncs
This commit is contained in:
@@ -87,6 +87,14 @@ function buildDailyTimestamp(date: string): string {
|
||||
return `${date}T00:00:00.000Z`;
|
||||
}
|
||||
|
||||
function distributeRequestCounts(total: number, buckets: number): number[] {
|
||||
if (buckets <= 0) return [];
|
||||
const normalizedTotal = Math.max(buckets, total);
|
||||
const base = Math.floor(normalizedTotal / buckets);
|
||||
const remainder = normalizedTotal % buckets;
|
||||
return Array.from({ length: buckets }, (_value, index) => base + (index < remainder ? 1 : 0));
|
||||
}
|
||||
|
||||
function buildLegacyHistoryDetails(
|
||||
snapshot: LegacyCliproxyUsageSnapshot
|
||||
): CliproxyUsageHistoryDetail[] {
|
||||
@@ -94,7 +102,12 @@ function buildLegacyHistoryDetails(
|
||||
const coveredDailyKeys = new Set<string>();
|
||||
|
||||
for (const hour of snapshot.hourly ?? []) {
|
||||
for (const breakdown of hour.modelBreakdowns) {
|
||||
const requestCounts = distributeRequestCounts(
|
||||
hour.requestCount ?? hour.modelBreakdowns.length,
|
||||
hour.modelBreakdowns.length
|
||||
);
|
||||
|
||||
hour.modelBreakdowns.forEach((breakdown, index) => {
|
||||
details.push({
|
||||
model: breakdown.modelName,
|
||||
timestamp: buildHourlyTimestamp(hour.hour),
|
||||
@@ -103,10 +116,12 @@ function buildLegacyHistoryDetails(
|
||||
inputTokens: breakdown.inputTokens,
|
||||
outputTokens: breakdown.outputTokens,
|
||||
cacheReadTokens: breakdown.cacheReadTokens,
|
||||
requestCount: requestCounts[index] ?? 1,
|
||||
cost: breakdown.cost,
|
||||
failed: false,
|
||||
});
|
||||
coveredDailyKeys.add(`${hour.hour.slice(0, 10)}|${breakdown.modelName}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const day of snapshot.daily ?? []) {
|
||||
@@ -124,6 +139,8 @@ function buildLegacyHistoryDetails(
|
||||
inputTokens: breakdown.inputTokens,
|
||||
outputTokens: breakdown.outputTokens,
|
||||
cacheReadTokens: breakdown.cacheReadTokens,
|
||||
requestCount: 1,
|
||||
cost: breakdown.cost,
|
||||
failed: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface CliproxyUsageHistoryDetail {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadTokens: number;
|
||||
requestCount: number;
|
||||
cost: number;
|
||||
failed: boolean;
|
||||
}
|
||||
|
||||
@@ -30,15 +32,12 @@ interface ModelAccumulator {
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
cacheReadTokens: number;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
/** Build ModelBreakdown from accumulated token counts */
|
||||
function buildModelBreakdown(modelName: string, acc: ModelAccumulator): ModelBreakdown {
|
||||
const { inputTokens, outputTokens, cacheReadTokens } = acc;
|
||||
const cost = calculateCost(
|
||||
{ inputTokens, outputTokens, cacheCreationTokens: 0, cacheReadTokens },
|
||||
modelName
|
||||
);
|
||||
const { inputTokens, outputTokens, cacheReadTokens, cost } = acc;
|
||||
return { modelName, inputTokens, outputTokens, cacheCreationTokens: 0, cacheReadTokens, cost };
|
||||
}
|
||||
|
||||
@@ -54,6 +53,16 @@ function createHistoryDetail(
|
||||
inputTokens: detail.tokens?.input_tokens ?? 0,
|
||||
outputTokens: detail.tokens?.output_tokens ?? 0,
|
||||
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
||||
requestCount: 1,
|
||||
cost: calculateCost(
|
||||
{
|
||||
inputTokens: detail.tokens?.input_tokens ?? 0,
|
||||
outputTokens: detail.tokens?.output_tokens ?? 0,
|
||||
cacheCreationTokens: 0,
|
||||
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
||||
},
|
||||
model
|
||||
),
|
||||
failed: detail.failed,
|
||||
};
|
||||
}
|
||||
@@ -107,6 +116,7 @@ function createHistorySignature(detail: CliproxyUsageHistoryDetail): string {
|
||||
detail.inputTokens,
|
||||
detail.outputTokens,
|
||||
detail.cacheReadTokens,
|
||||
detail.requestCount,
|
||||
detail.failed ? '1' : '0',
|
||||
].join('|');
|
||||
}
|
||||
@@ -185,15 +195,21 @@ function aggregateByKey<T>(
|
||||
for (const detail of flat) {
|
||||
const key = keyFn(detail.timestamp);
|
||||
if (!buckets.has(key)) buckets.set(key, new Map());
|
||||
requestCounts.set(key, (requestCounts.get(key) ?? 0) + 1);
|
||||
requestCounts.set(key, (requestCounts.get(key) ?? 0) + detail.requestCount);
|
||||
const modelMap = buckets.get(key) as Map<string, ModelAccumulator>;
|
||||
if (!modelMap.has(detail.model)) {
|
||||
modelMap.set(detail.model, { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0 });
|
||||
modelMap.set(detail.model, {
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReadTokens: 0,
|
||||
cost: 0,
|
||||
});
|
||||
}
|
||||
const acc = modelMap.get(detail.model) as ModelAccumulator;
|
||||
acc.inputTokens += detail.inputTokens;
|
||||
acc.outputTokens += detail.outputTokens;
|
||||
acc.cacheReadTokens += detail.cacheReadTokens;
|
||||
acc.cost += detail.cost;
|
||||
}
|
||||
|
||||
const records: T[] = [];
|
||||
|
||||
@@ -211,6 +211,7 @@ describe('cliproxy usage syncer', () => {
|
||||
{
|
||||
hour: '2026-03-01 12:00',
|
||||
source: 'cliproxy',
|
||||
requestCount: 7,
|
||||
inputTokens: 100,
|
||||
outputTokens: 20,
|
||||
cacheCreationTokens: 0,
|
||||
@@ -260,6 +261,21 @@ describe('cliproxy usage syncer', () => {
|
||||
|
||||
const cached = await loadCachedCliproxyData();
|
||||
expect(cached.daily.map((entry) => entry.date)).toEqual(['2026-03-02', '2026-03-01']);
|
||||
expect(cached.hourly.find((entry) => entry.hour === '2026-03-01 12:00')?.requestCount).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
it('prunes history details older than the configured retention window', async () => {
|
||||
await runWithScopedConfigDir(ccsDir, async () => {
|
||||
await syncCliproxyUsage(() =>
|
||||
Promise.resolve(buildResponse(100, '2024-01-01T12:00:00.000Z'))
|
||||
);
|
||||
await syncCliproxyUsage(() =>
|
||||
Promise.resolve(buildResponse(200, new Date().toISOString()))
|
||||
);
|
||||
|
||||
const cached = await loadCachedCliproxyData();
|
||||
expect(cached.daily.some((entry) => entry.date === '2024-01-01')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -134,6 +134,14 @@ describe('cliproxy usage transformer', () => {
|
||||
expect(merged).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('uses persisted cost from history instead of recomputing from current pricing', () => {
|
||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||
const seeded = details.map((detail) => ({ ...detail, cost: 999 }));
|
||||
const { daily } = buildCliproxyUsageHistoryAggregates(seeded);
|
||||
|
||||
expect(daily[0].modelBreakdowns[0]?.cost).toBe(999);
|
||||
});
|
||||
|
||||
it('rebuilds daily history aggregates from merged detail history', () => {
|
||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||
const { daily } = buildCliproxyUsageHistoryAggregates(details);
|
||||
|
||||
Reference in New Issue
Block a user