mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +00:00
fix(cli-proxy): preserve historical analytics across syncs
This commit is contained in:
1 parent
69b75c0449
commit
01fcd77573
4 files changed
+66
-9
No files matched your search
@@ -87,6 +87,14 @@ function buildDailyTimestamp(date: string): string {
|
|||||||
return `${date}T00:00:00.000Z`;
|
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(
|
function buildLegacyHistoryDetails(
|
||||||
snapshot: LegacyCliproxyUsageSnapshot
|
snapshot: LegacyCliproxyUsageSnapshot
|
||||||
): CliproxyUsageHistoryDetail[] {
|
): CliproxyUsageHistoryDetail[] {
|
||||||
@@ -94,7 +102,12 @@ function buildLegacyHistoryDetails(
|
|||||||
const coveredDailyKeys = new Set<string>();
|
const coveredDailyKeys = new Set<string>();
|
||||||
|
|
||||||
for (const hour of snapshot.hourly ?? []) {
|
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({
|
details.push({
|
||||||
model: breakdown.modelName,
|
model: breakdown.modelName,
|
||||||
timestamp: buildHourlyTimestamp(hour.hour),
|
timestamp: buildHourlyTimestamp(hour.hour),
|
||||||
@@ -103,10 +116,12 @@ function buildLegacyHistoryDetails(
|
|||||||
inputTokens: breakdown.inputTokens,
|
inputTokens: breakdown.inputTokens,
|
||||||
outputTokens: breakdown.outputTokens,
|
outputTokens: breakdown.outputTokens,
|
||||||
cacheReadTokens: breakdown.cacheReadTokens,
|
cacheReadTokens: breakdown.cacheReadTokens,
|
||||||
|
requestCount: requestCounts[index] ?? 1,
|
||||||
|
cost: breakdown.cost,
|
||||||
failed: false,
|
failed: false,
|
||||||
});
|
});
|
||||||
coveredDailyKeys.add(`${hour.hour.slice(0, 10)}|${breakdown.modelName}`);
|
coveredDailyKeys.add(`${hour.hour.slice(0, 10)}|${breakdown.modelName}`);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const day of snapshot.daily ?? []) {
|
for (const day of snapshot.daily ?? []) {
|
||||||
@@ -124,6 +139,8 @@ function buildLegacyHistoryDetails(
|
|||||||
inputTokens: breakdown.inputTokens,
|
inputTokens: breakdown.inputTokens,
|
||||||
outputTokens: breakdown.outputTokens,
|
outputTokens: breakdown.outputTokens,
|
||||||
cacheReadTokens: breakdown.cacheReadTokens,
|
cacheReadTokens: breakdown.cacheReadTokens,
|
||||||
|
requestCount: 1,
|
||||||
|
cost: breakdown.cost,
|
||||||
failed: false,
|
failed: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ export interface CliproxyUsageHistoryDetail {
|
|||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
cacheReadTokens: number;
|
cacheReadTokens: number;
|
||||||
|
requestCount: number;
|
||||||
|
cost: number;
|
||||||
failed: boolean;
|
failed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,15 +32,12 @@ interface ModelAccumulator {
|
|||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
outputTokens: number;
|
outputTokens: number;
|
||||||
cacheReadTokens: number;
|
cacheReadTokens: number;
|
||||||
|
cost: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build ModelBreakdown from accumulated token counts */
|
/** Build ModelBreakdown from accumulated token counts */
|
||||||
function buildModelBreakdown(modelName: string, acc: ModelAccumulator): ModelBreakdown {
|
function buildModelBreakdown(modelName: string, acc: ModelAccumulator): ModelBreakdown {
|
||||||
const { inputTokens, outputTokens, cacheReadTokens } = acc;
|
const { inputTokens, outputTokens, cacheReadTokens, cost } = acc;
|
||||||
const cost = calculateCost(
|
|
||||||
{ inputTokens, outputTokens, cacheCreationTokens: 0, cacheReadTokens },
|
|
||||||
modelName
|
|
||||||
);
|
|
||||||
return { modelName, inputTokens, outputTokens, cacheCreationTokens: 0, cacheReadTokens, cost };
|
return { modelName, inputTokens, outputTokens, cacheCreationTokens: 0, cacheReadTokens, cost };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +53,16 @@ function createHistoryDetail(
|
|||||||
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,
|
||||||
|
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,
|
failed: detail.failed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -107,6 +116,7 @@ function createHistorySignature(detail: CliproxyUsageHistoryDetail): string {
|
|||||||
detail.inputTokens,
|
detail.inputTokens,
|
||||||
detail.outputTokens,
|
detail.outputTokens,
|
||||||
detail.cacheReadTokens,
|
detail.cacheReadTokens,
|
||||||
|
detail.requestCount,
|
||||||
detail.failed ? '1' : '0',
|
detail.failed ? '1' : '0',
|
||||||
].join('|');
|
].join('|');
|
||||||
}
|
}
|
||||||
@@ -185,15 +195,21 @@ function aggregateByKey<T>(
|
|||||||
for (const detail of flat) {
|
for (const detail of flat) {
|
||||||
const key = keyFn(detail.timestamp);
|
const key = keyFn(detail.timestamp);
|
||||||
if (!buckets.has(key)) buckets.set(key, new Map());
|
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>;
|
const modelMap = buckets.get(key) as Map<string, ModelAccumulator>;
|
||||||
if (!modelMap.has(detail.model)) {
|
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;
|
const acc = modelMap.get(detail.model) as ModelAccumulator;
|
||||||
acc.inputTokens += detail.inputTokens;
|
acc.inputTokens += detail.inputTokens;
|
||||||
acc.outputTokens += detail.outputTokens;
|
acc.outputTokens += detail.outputTokens;
|
||||||
acc.cacheReadTokens += detail.cacheReadTokens;
|
acc.cacheReadTokens += detail.cacheReadTokens;
|
||||||
|
acc.cost += detail.cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
const records: T[] = [];
|
const records: T[] = [];
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ describe('cliproxy usage syncer', () => {
|
|||||||
{
|
{
|
||||||
hour: '2026-03-01 12:00',
|
hour: '2026-03-01 12:00',
|
||||||
source: 'cliproxy',
|
source: 'cliproxy',
|
||||||
|
requestCount: 7,
|
||||||
inputTokens: 100,
|
inputTokens: 100,
|
||||||
outputTokens: 20,
|
outputTokens: 20,
|
||||||
cacheCreationTokens: 0,
|
cacheCreationTokens: 0,
|
||||||
@@ -260,6 +261,21 @@ describe('cliproxy usage syncer', () => {
|
|||||||
|
|
||||||
const cached = await loadCachedCliproxyData();
|
const cached = await loadCachedCliproxyData();
|
||||||
expect(cached.daily.map((entry) => entry.date)).toEqual(['2026-03-02', '2026-03-01']);
|
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);
|
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', () => {
|
it('rebuilds daily history aggregates from merged detail history', () => {
|
||||||
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
const details = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||||
const { daily } = buildCliproxyUsageHistoryAggregates(details);
|
const { daily } = buildCliproxyUsageHistoryAggregates(details);
|
||||||
|
|||||||
Reference in new issue
Block a user