mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
fix: normalize cliproxy v3 usage snapshots
This commit is contained in:
1 parent
c21f589247
commit
65adab5bec
6 files changed
+279
-36
No files matched your search
@@ -483,9 +483,8 @@ export async function reconcileExhaustedRotationAccounts(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = await import(
|
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } =
|
||||||
'../accounts/account-safety'
|
await import('../accounts/account-safety');
|
||||||
);
|
|
||||||
restoreExpiredQuotaPauses();
|
restoreExpiredQuotaPauses();
|
||||||
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
@@ -593,9 +592,8 @@ export async function preflightCheck(provider: CLIProxyProvider): Promise<Prefli
|
|||||||
return { proceed: true, accountId: defaultAccount?.id || '' };
|
return { proceed: true, accountId: defaultAccount?.id || '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } = await import(
|
const { pauseAccountForQuotaCooldown, restoreExpiredQuotaPauses } =
|
||||||
'../accounts/account-safety'
|
await import('../accounts/account-safety');
|
||||||
);
|
|
||||||
restoreExpiredQuotaPauses();
|
restoreExpiredQuotaPauses();
|
||||||
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
|||||||
@@ -112,9 +112,8 @@ export async function runImageAnalysisCheck(results: HealthCheck): Promise<void>
|
|||||||
* Fix image analysis configuration issues
|
* Fix image analysis configuration issues
|
||||||
*/
|
*/
|
||||||
export async function fixImageAnalysisConfig(): Promise<boolean> {
|
export async function fixImageAnalysisConfig(): Promise<boolean> {
|
||||||
const { updateConfig, loadOrCreateUnifiedConfig } = await import(
|
const { updateConfig, loadOrCreateUnifiedConfig } =
|
||||||
'../../config/config-loader-facade'
|
await import('../../config/config-loader-facade');
|
||||||
);
|
|
||||||
|
|
||||||
const config = loadOrCreateUnifiedConfig();
|
const config = loadOrCreateUnifiedConfig();
|
||||||
let fixed = false;
|
let fixed = false;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
buildCliproxyUsageHistoryAggregates,
|
buildCliproxyUsageHistoryAggregates,
|
||||||
extractCliproxyUsageHistoryDetails,
|
extractCliproxyUsageHistoryDetails,
|
||||||
mergeCliproxyUsageHistoryDetails,
|
mergeCliproxyUsageHistoryDetails,
|
||||||
|
normalizeCliproxyUsageHistoryDetail,
|
||||||
pruneCliproxyUsageHistoryDetails,
|
pruneCliproxyUsageHistoryDetails,
|
||||||
type CliproxyUsageHistoryDetail,
|
type CliproxyUsageHistoryDetail,
|
||||||
} from './cliproxy-usage-transformer';
|
} from './cliproxy-usage-transformer';
|
||||||
@@ -197,14 +198,27 @@ function readSnapshot(emitWarnings = true): CliproxyUsageSnapshot | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray((snapshot as CliproxyUsageSnapshot).details)) {
|
const details = (snapshot as CliproxyUsageSnapshot).details;
|
||||||
|
if (!Array.isArray(details)) {
|
||||||
if (emitWarnings) {
|
if (emitWarnings) {
|
||||||
console.log(info('CLIProxy snapshot details missing, will refresh on next sync'));
|
console.log(info('CLIProxy snapshot details missing, will refresh on next sync'));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return snapshot as CliproxyUsageSnapshot;
|
const normalizedDetails = details
|
||||||
|
.map((detail) => normalizeCliproxyUsageHistoryDetail(detail))
|
||||||
|
.filter((detail): detail is CliproxyUsageHistoryDetail => detail !== null);
|
||||||
|
const { daily, hourly, monthly } = buildCliproxyUsageHistoryAggregates(normalizedDetails);
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: SNAPSHOT_VERSION,
|
||||||
|
timestamp: Number(snapshot.timestamp),
|
||||||
|
details: normalizedDetails,
|
||||||
|
daily,
|
||||||
|
hourly,
|
||||||
|
monthly,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.version === 1 || snapshot.version === 2) {
|
if (snapshot.version === 1 || snapshot.version === 2) {
|
||||||
|
|||||||
@@ -64,30 +64,112 @@ function createHistoryDetail(
|
|||||||
detail: CliproxyRequestDetail
|
detail: CliproxyRequestDetail
|
||||||
): CliproxyUsageHistoryDetail {
|
): CliproxyUsageHistoryDetail {
|
||||||
const pricingProvider = normalizeUsageProvider(provider) ?? provider.trim().toLowerCase();
|
const pricingProvider = normalizeUsageProvider(provider) ?? provider.trim().toLowerCase();
|
||||||
|
const inputTokens = detail.tokens?.input_tokens ?? 0;
|
||||||
|
const outputTokens = detail.tokens?.output_tokens ?? 0;
|
||||||
|
const cacheReadTokens = detail.tokens?.cached_tokens ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
model,
|
model,
|
||||||
provider: pricingProvider,
|
provider: pricingProvider,
|
||||||
timestamp: detail.timestamp,
|
timestamp: detail.timestamp,
|
||||||
source: detail.source,
|
source: detail.source,
|
||||||
authIndex: String(detail.auth_index),
|
authIndex: String(detail.auth_index),
|
||||||
inputTokens: detail.tokens?.input_tokens ?? 0,
|
inputTokens,
|
||||||
outputTokens: detail.tokens?.output_tokens ?? 0,
|
outputTokens,
|
||||||
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
cacheReadTokens,
|
||||||
requestCount: 1,
|
requestCount: 1,
|
||||||
cost: calculateCost(
|
cost: calculateHistoryDetailCost(
|
||||||
{
|
|
||||||
inputTokens: detail.tokens?.input_tokens ?? 0,
|
|
||||||
outputTokens: detail.tokens?.output_tokens ?? 0,
|
|
||||||
cacheCreationTokens: 0,
|
|
||||||
cacheReadTokens: detail.tokens?.cached_tokens ?? 0,
|
|
||||||
},
|
|
||||||
model,
|
model,
|
||||||
{ provider: pricingProvider }
|
pricingProvider,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheReadTokens
|
||||||
),
|
),
|
||||||
failed: detail.failed,
|
failed: detail.failed,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateHistoryDetailCost(
|
||||||
|
model: string,
|
||||||
|
provider: string | undefined,
|
||||||
|
inputTokens: number,
|
||||||
|
outputTokens: number,
|
||||||
|
cacheReadTokens: number
|
||||||
|
): number {
|
||||||
|
return calculateCost(
|
||||||
|
{
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens,
|
||||||
|
},
|
||||||
|
model,
|
||||||
|
provider ? { provider } : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePersistedNumber(value: unknown, fallback = 0): number {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePersistedProvider(value: unknown): string | undefined {
|
||||||
|
if (typeof value !== 'string') return undefined;
|
||||||
|
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length === 0) return undefined;
|
||||||
|
|
||||||
|
return normalizeUsageProvider(trimmed) ?? trimmed.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeCliproxyUsageHistoryDetail(
|
||||||
|
detail: unknown
|
||||||
|
): CliproxyUsageHistoryDetail | null {
|
||||||
|
if (!detail || typeof detail !== 'object') return null;
|
||||||
|
|
||||||
|
const candidate = detail as Record<string, unknown>;
|
||||||
|
if (
|
||||||
|
typeof candidate.model !== 'string' ||
|
||||||
|
typeof candidate.timestamp !== 'string' ||
|
||||||
|
typeof candidate.source !== 'string' ||
|
||||||
|
!Number.isFinite(Date.parse(candidate.timestamp))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = normalizePersistedProvider(candidate.provider);
|
||||||
|
const inputTokens = normalizePersistedNumber(candidate.inputTokens);
|
||||||
|
const outputTokens = normalizePersistedNumber(candidate.outputTokens);
|
||||||
|
const cacheReadTokens = normalizePersistedNumber(candidate.cacheReadTokens);
|
||||||
|
const requestCount = Math.max(1, normalizePersistedNumber(candidate.requestCount, 1));
|
||||||
|
const cost = normalizePersistedNumber(
|
||||||
|
candidate.cost,
|
||||||
|
calculateHistoryDetailCost(
|
||||||
|
candidate.model,
|
||||||
|
provider,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheReadTokens
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
model: candidate.model,
|
||||||
|
...(provider && { provider }),
|
||||||
|
timestamp: candidate.timestamp,
|
||||||
|
source: candidate.source,
|
||||||
|
authIndex:
|
||||||
|
typeof candidate.authIndex === 'string'
|
||||||
|
? candidate.authIndex
|
||||||
|
: String(candidate.authIndex ?? ''),
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheReadTokens,
|
||||||
|
requestCount,
|
||||||
|
cost,
|
||||||
|
failed: candidate.failed === true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FLATTEN
|
// FLATTEN
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -143,12 +225,53 @@ function createHistorySignature(detail: CliproxyUsageHistoryDetail): string {
|
|||||||
].join('|');
|
].join('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createProviderlessHistorySignature(detail: CliproxyUsageHistoryDetail): string {
|
||||||
|
return [
|
||||||
|
detail.model,
|
||||||
|
detail.timestamp,
|
||||||
|
detail.source,
|
||||||
|
detail.authIndex,
|
||||||
|
detail.inputTokens,
|
||||||
|
detail.outputTokens,
|
||||||
|
detail.cacheReadTokens,
|
||||||
|
detail.requestCount,
|
||||||
|
detail.failed ? '1' : '0',
|
||||||
|
].join('|');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrateProviderlessHistoryDetails(
|
||||||
|
existing: CliproxyUsageHistoryDetail[],
|
||||||
|
incoming: CliproxyUsageHistoryDetail[]
|
||||||
|
): CliproxyUsageHistoryDetail[] {
|
||||||
|
const incomingByProviderlessSignature = new Map<string, CliproxyUsageHistoryDetail[]>();
|
||||||
|
for (const detail of incoming) {
|
||||||
|
if (!detail.provider) continue;
|
||||||
|
|
||||||
|
const signature = createProviderlessHistorySignature(detail);
|
||||||
|
incomingByProviderlessSignature.set(signature, [
|
||||||
|
...(incomingByProviderlessSignature.get(signature) ?? []),
|
||||||
|
detail,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.map((detail) => {
|
||||||
|
if (detail.provider) return detail;
|
||||||
|
|
||||||
|
const matches = incomingByProviderlessSignature.get(createProviderlessHistorySignature(detail));
|
||||||
|
const providers = new Set(matches?.map((match) => match.provider).filter(Boolean));
|
||||||
|
if (!matches || providers.size !== 1) return detail;
|
||||||
|
|
||||||
|
return { ...detail, provider: matches[0].provider, cost: matches[0].cost };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function mergeCliproxyUsageHistoryDetails(
|
export function mergeCliproxyUsageHistoryDetails(
|
||||||
existing: CliproxyUsageHistoryDetail[],
|
existing: CliproxyUsageHistoryDetail[],
|
||||||
incoming: CliproxyUsageHistoryDetail[]
|
incoming: CliproxyUsageHistoryDetail[]
|
||||||
): CliproxyUsageHistoryDetail[] {
|
): CliproxyUsageHistoryDetail[] {
|
||||||
|
const hydratedExisting = hydrateProviderlessHistoryDetails(existing, incoming);
|
||||||
const existingCounts = new Map<string, { detail: CliproxyUsageHistoryDetail; count: number }>();
|
const existingCounts = new Map<string, { detail: CliproxyUsageHistoryDetail; count: number }>();
|
||||||
for (const detail of existing) {
|
for (const detail of hydratedExisting) {
|
||||||
const signature = createHistorySignature(detail);
|
const signature = createHistorySignature(detail);
|
||||||
const entry = existingCounts.get(signature);
|
const entry = existingCounts.get(signature);
|
||||||
if (entry) {
|
if (entry) {
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ function fetchRawResponse(): Promise<CliproxyUsageApiResponse | null> {
|
|||||||
return Promise.resolve(rawResponse);
|
return Promise.resolve(rawResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildResponse(inputTokens: number, timestamp = '2026-03-02T12:00:00.000Z'): CliproxyUsageApiResponse {
|
function buildResponse(
|
||||||
|
inputTokens: number,
|
||||||
|
timestamp = '2026-03-02T12:00:00.000Z'
|
||||||
|
): CliproxyUsageApiResponse {
|
||||||
return {
|
return {
|
||||||
usage: {
|
usage: {
|
||||||
apis: {
|
apis: {
|
||||||
@@ -175,6 +178,69 @@ describe('cliproxy usage syncer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes old v3 snapshot details before loading and merging history', async () => {
|
||||||
|
await runWithScopedConfigDir(ccsDir, async () => {
|
||||||
|
const snapshotPath = path.join(ccsDir, 'cache', 'cliproxy-usage', 'latest.json');
|
||||||
|
fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
snapshotPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 3,
|
||||||
|
timestamp: Date.now() - 60_000,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
model: 'gemini-2.5-pro',
|
||||||
|
timestamp: '2026-03-02T12:00:00.000Z',
|
||||||
|
source: 'account-a',
|
||||||
|
authIndex: '0',
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 20,
|
||||||
|
cacheReadTokens: 10,
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
daily: [
|
||||||
|
{
|
||||||
|
date: '2026-03-02',
|
||||||
|
source: 'cliproxy',
|
||||||
|
inputTokens: 100,
|
||||||
|
outputTokens: 20,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 10,
|
||||||
|
cost: null,
|
||||||
|
totalCost: null,
|
||||||
|
modelsUsed: ['gemini-2.5-pro'],
|
||||||
|
modelBreakdowns: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hourly: [],
|
||||||
|
monthly: [],
|
||||||
|
}),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const loaded = await loadCachedCliproxyData();
|
||||||
|
expect(loaded.daily[0].inputTokens).toBe(100);
|
||||||
|
expect(Number.isFinite(loaded.daily[0].totalCost)).toBe(true);
|
||||||
|
expect(loaded.hourly[0].requestCount).toBe(1);
|
||||||
|
|
||||||
|
await syncCliproxyUsage(fetchRawResponse);
|
||||||
|
|
||||||
|
const cached = await loadCachedCliproxyData();
|
||||||
|
expect(cached.daily).toHaveLength(1);
|
||||||
|
expect(cached.daily[0].inputTokens).toBe(100);
|
||||||
|
expect(cached.hourly[0].requestCount).toBe(1);
|
||||||
|
|
||||||
|
const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')) as {
|
||||||
|
details: Array<{ requestCount?: number; cost?: number; provider?: string }>;
|
||||||
|
};
|
||||||
|
expect(snapshot.details).toHaveLength(1);
|
||||||
|
expect(snapshot.details[0].requestCount).toBe(1);
|
||||||
|
expect(Number.isFinite(snapshot.details[0].cost)).toBe(true);
|
||||||
|
expect(snapshot.details[0].provider).toBe('google');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('migrates legacy v1 and v2 snapshots forward before merging new history', async () => {
|
it('migrates legacy v1 and v2 snapshots forward before merging new history', async () => {
|
||||||
for (const version of [1, 2]) {
|
for (const version of [1, 2]) {
|
||||||
await runWithScopedConfigDir(ccsDir, async () => {
|
await runWithScopedConfigDir(ccsDir, async () => {
|
||||||
@@ -264,7 +330,9 @@ 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);
|
expect(cached.hourly.find((entry) => entry.hour === '2026-03-01 12:00')?.requestCount).toBe(
|
||||||
|
7
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -274,9 +342,7 @@ describe('cliproxy usage syncer', () => {
|
|||||||
await syncCliproxyUsage(() =>
|
await syncCliproxyUsage(() =>
|
||||||
Promise.resolve(buildResponse(100, '2024-01-01T12:00:00.000Z'))
|
Promise.resolve(buildResponse(100, '2024-01-01T12:00:00.000Z'))
|
||||||
);
|
);
|
||||||
await syncCliproxyUsage(() =>
|
await syncCliproxyUsage(() => Promise.resolve(buildResponse(200, new Date().toISOString())));
|
||||||
Promise.resolve(buildResponse(200, new Date().toISOString()))
|
|
||||||
);
|
|
||||||
|
|
||||||
const cached = await loadCachedCliproxyData();
|
const cached = await loadCachedCliproxyData();
|
||||||
expect(cached.daily.some((entry) => entry.date === '2024-01-01')).toBe(false);
|
expect(cached.daily.some((entry) => entry.date === '2024-01-01')).toBe(false);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
buildCliproxyUsageHistoryAggregates,
|
buildCliproxyUsageHistoryAggregates,
|
||||||
extractCliproxyUsageHistoryDetails,
|
extractCliproxyUsageHistoryDetails,
|
||||||
mergeCliproxyUsageHistoryDetails,
|
mergeCliproxyUsageHistoryDetails,
|
||||||
|
normalizeCliproxyUsageHistoryDetail,
|
||||||
transformCliproxyToDailyUsage,
|
transformCliproxyToDailyUsage,
|
||||||
transformCliproxyToHourlyUsage,
|
transformCliproxyToHourlyUsage,
|
||||||
transformCliproxyToMonthlyUsage,
|
transformCliproxyToMonthlyUsage,
|
||||||
@@ -111,18 +112,12 @@ describe('cliproxy usage transformer', () => {
|
|||||||
expect(flat[0].provider).toBe('google');
|
expect(flat[0].provider).toBe('google');
|
||||||
expect(
|
expect(
|
||||||
flat.some(
|
flat.some(
|
||||||
(entry) =>
|
(entry) => entry.failed === true && entry.inputTokens === 40 && entry.outputTokens === 10
|
||||||
entry.failed === true &&
|
|
||||||
entry.inputTokens === 40 &&
|
|
||||||
entry.outputTokens === 10
|
|
||||||
)
|
)
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
flat.some(
|
flat.some(
|
||||||
(entry) =>
|
(entry) => entry.failed === true && entry.inputTokens === 0 && entry.outputTokens === 0
|
||||||
entry.failed === true &&
|
|
||||||
entry.inputTokens === 0 &&
|
|
||||||
entry.outputTokens === 0
|
|
||||||
)
|
)
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
@@ -134,6 +129,54 @@ describe('cliproxy usage transformer', () => {
|
|||||||
expect(merged).toHaveLength(details.length);
|
expect(merged).toHaveLength(details.length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalizes old v3 history details before merging with provider-aware entries', () => {
|
||||||
|
const [incoming] = extractCliproxyUsageHistoryDetails({
|
||||||
|
usage: {
|
||||||
|
apis: {
|
||||||
|
gemini: {
|
||||||
|
models: {
|
||||||
|
'gemini-2.5-pro': {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
timestamp: '2026-03-01T10:15:00.000Z',
|
||||||
|
source: 'account-a',
|
||||||
|
auth_index: 0,
|
||||||
|
tokens: {
|
||||||
|
input_tokens: 100,
|
||||||
|
output_tokens: 50,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
cached_tokens: 20,
|
||||||
|
total_tokens: 170,
|
||||||
|
},
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const legacyDetail = normalizeCliproxyUsageHistoryDetail({
|
||||||
|
model: incoming.model,
|
||||||
|
timestamp: incoming.timestamp,
|
||||||
|
source: incoming.source,
|
||||||
|
authIndex: incoming.authIndex,
|
||||||
|
inputTokens: incoming.inputTokens,
|
||||||
|
outputTokens: incoming.outputTokens,
|
||||||
|
cacheReadTokens: incoming.cacheReadTokens,
|
||||||
|
failed: incoming.failed,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(legacyDetail?.requestCount).toBe(1);
|
||||||
|
expect(Number.isFinite(legacyDetail?.cost)).toBe(true);
|
||||||
|
|
||||||
|
const merged = mergeCliproxyUsageHistoryDetails([legacyDetail!], [incoming]);
|
||||||
|
expect(merged).toHaveLength(1);
|
||||||
|
expect(merged[0].provider).toBe(incoming.provider);
|
||||||
|
expect(merged[0].cost).toBe(incoming.cost);
|
||||||
|
});
|
||||||
|
|
||||||
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