mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 02:14:32 +00:00
fix(usage): preserve pricing overrides and legacy merges
This commit is contained in:
1 parent
42fc5281a1
commit
30f350a74f
6 files changed
+292
-24
No files matched your search
@@ -858,6 +858,30 @@ function getCcsStaticPricing(model: string): ModelPricing | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCcsPolicyOverridePricing(model: string): ModelPricing | undefined {
|
||||||
|
const providerlessModel = stripProviderPrefix(model);
|
||||||
|
const normalized = normalizeModelName(providerlessModel);
|
||||||
|
|
||||||
|
for (const candidate of getLookupCandidates(providerlessModel)) {
|
||||||
|
const alias = MODEL_PRICING_ALIASES[candidate];
|
||||||
|
if (alias !== undefined) {
|
||||||
|
const aliasPricing = NORMALIZED_PRICING_REGISTRY[alias];
|
||||||
|
if (aliasPricing !== undefined) {
|
||||||
|
return aliasPricing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidate !== normalized) {
|
||||||
|
const variantPricing = NORMALIZED_PRICING_REGISTRY[candidate];
|
||||||
|
if (variantPricing !== undefined) {
|
||||||
|
return variantPricing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function hasProviderContext(model: string, options: PricingLookupOptions): boolean {
|
function hasProviderContext(model: string, options: PricingLookupOptions): boolean {
|
||||||
return Boolean(options.provider || /^[^/]+\//.test(model.trim()));
|
return Boolean(options.provider || /^[^/]+\//.test(model.trim()));
|
||||||
}
|
}
|
||||||
@@ -869,6 +893,11 @@ function hasProviderContext(model: string, options: PricingLookupOptions): boole
|
|||||||
*/
|
*/
|
||||||
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
||||||
if (hasProviderContext(model, options)) {
|
if (hasProviderContext(model, options)) {
|
||||||
|
const ccsOverridePricing = getCcsPolicyOverridePricing(model);
|
||||||
|
if (ccsOverridePricing !== undefined) {
|
||||||
|
return ccsOverridePricing;
|
||||||
|
}
|
||||||
|
|
||||||
const providerPricing = resolveModelsDevPricing(model, options);
|
const providerPricing = resolveModelsDevPricing(model, options);
|
||||||
if (providerPricing !== undefined) {
|
if (providerPricing !== undefined) {
|
||||||
return providerPricing.pricing;
|
return providerPricing.pricing;
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ import {
|
|||||||
import { scanCodexNativeUsageEntries } from './codex-native-usage-collector';
|
import { scanCodexNativeUsageEntries } from './codex-native-usage-collector';
|
||||||
import { scanDroidNativeUsageEntries } from './droid-native-usage-collector';
|
import { scanDroidNativeUsageEntries } from './droid-native-usage-collector';
|
||||||
import { startModelsDevRegistryRefresh } from '../models-dev/registry-cache';
|
import { startModelsDevRegistryRefresh } from '../models-dev/registry-cache';
|
||||||
import { getModelsUsed, getProviderModelKey } from './model-identity';
|
import {
|
||||||
|
coalesceLegacyProviderlessBreakdowns,
|
||||||
|
getModelsUsed,
|
||||||
|
getProviderModelKey,
|
||||||
|
} from './model-identity';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Multi-Instance Support - Aggregate usage from CCS profiles
|
// Multi-Instance Support - Aggregate usage from CCS profiles
|
||||||
@@ -113,6 +117,33 @@ function getHourlyRequestCount(hour: HourlyUsage): number {
|
|||||||
return hour.requestCount ?? hour.modelBreakdowns.length;
|
return hour.requestCount ?? hour.modelBreakdowns.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function finalizeDailyUsage(day: DailyUsage): DailyUsage {
|
||||||
|
const modelBreakdowns = coalesceLegacyProviderlessBreakdowns(day.modelBreakdowns);
|
||||||
|
return {
|
||||||
|
...day,
|
||||||
|
modelsUsed: getModelsUsed(modelBreakdowns),
|
||||||
|
modelBreakdowns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeMonthlyUsage(month: MonthlyUsage): MonthlyUsage {
|
||||||
|
const modelBreakdowns = coalesceLegacyProviderlessBreakdowns(month.modelBreakdowns);
|
||||||
|
return {
|
||||||
|
...month,
|
||||||
|
modelsUsed: getModelsUsed(modelBreakdowns),
|
||||||
|
modelBreakdowns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeHourlyUsage(hour: HourlyUsage): HourlyUsage {
|
||||||
|
const modelBreakdowns = coalesceLegacyProviderlessBreakdowns(hour.modelBreakdowns);
|
||||||
|
return {
|
||||||
|
...hour,
|
||||||
|
modelsUsed: getModelsUsed(modelBreakdowns),
|
||||||
|
modelBreakdowns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge daily usage data from multiple sources
|
* Merge daily usage data from multiple sources
|
||||||
* Combines entries with same date by aggregating tokens
|
* Combines entries with same date by aggregating tokens
|
||||||
@@ -146,7 +177,6 @@ export function mergeDailyData(sources: DailyUsage[][]): DailyUsage[] {
|
|||||||
existing.modelBreakdowns.push({ ...breakdown });
|
existing.modelBreakdowns.push({ ...breakdown });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
existing.modelsUsed = getModelsUsed(existing.modelBreakdowns);
|
|
||||||
} else {
|
} else {
|
||||||
// Clone to avoid mutating original
|
// Clone to avoid mutating original
|
||||||
const modelBreakdowns = day.modelBreakdowns.map((b) => ({ ...b }));
|
const modelBreakdowns = day.modelBreakdowns.map((b) => ({ ...b }));
|
||||||
@@ -159,7 +189,9 @@ export function mergeDailyData(sources: DailyUsage[][]): DailyUsage[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(dateMap.values()).sort((a, b) => a.date.localeCompare(b.date));
|
return Array.from(dateMap.values())
|
||||||
|
.map(finalizeDailyUsage)
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,7 +224,6 @@ export function mergeMonthlyData(sources: MonthlyUsage[][]): MonthlyUsage[] {
|
|||||||
existing.modelBreakdowns.push({ ...breakdown });
|
existing.modelBreakdowns.push({ ...breakdown });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
existing.modelsUsed = getModelsUsed(existing.modelBreakdowns);
|
|
||||||
} else {
|
} else {
|
||||||
const modelBreakdowns = month.modelBreakdowns.map((breakdown) => ({ ...breakdown }));
|
const modelBreakdowns = month.modelBreakdowns.map((breakdown) => ({ ...breakdown }));
|
||||||
monthMap.set(month.month, {
|
monthMap.set(month.month, {
|
||||||
@@ -204,7 +235,9 @@ export function mergeMonthlyData(sources: MonthlyUsage[][]): MonthlyUsage[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month));
|
return Array.from(monthMap.values())
|
||||||
|
.map(finalizeMonthlyUsage)
|
||||||
|
.sort((a, b) => a.month.localeCompare(b.month));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,7 +273,6 @@ export function mergeHourlyData(sources: HourlyUsage[][]): HourlyUsage[] {
|
|||||||
existing.modelBreakdowns.push({ ...breakdown });
|
existing.modelBreakdowns.push({ ...breakdown });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
existing.modelsUsed = getModelsUsed(existing.modelBreakdowns);
|
|
||||||
} else {
|
} else {
|
||||||
const modelBreakdowns = hour.modelBreakdowns.map((b) => ({ ...b }));
|
const modelBreakdowns = hour.modelBreakdowns.map((b) => ({ ...b }));
|
||||||
hourMap.set(hour.hour, {
|
hourMap.set(hour.hour, {
|
||||||
@@ -253,7 +285,9 @@ export function mergeHourlyData(sources: HourlyUsage[][]): HourlyUsage[] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(hourMap.values()).sort((a, b) => a.hour.localeCompare(b.hour));
|
return Array.from(hourMap.values())
|
||||||
|
.map(finalizeHourlyUsage)
|
||||||
|
.sort((a, b) => a.hour.localeCompare(b.hour));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import {
|
|||||||
getLastFetchTimestamp,
|
getLastFetchTimestamp,
|
||||||
refreshUsageCache,
|
refreshUsageCache,
|
||||||
} from './aggregator';
|
} from './aggregator';
|
||||||
import { getModelsUsed } from './model-identity';
|
import {
|
||||||
|
coalesceLegacyProviderlessBreakdowns,
|
||||||
|
getModelsUsed,
|
||||||
|
getProviderModelKey,
|
||||||
|
} from './model-identity';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Types
|
// Types
|
||||||
@@ -142,10 +146,6 @@ function calculateUsageTotalTokens(
|
|||||||
return input + output + cacheCreation + cacheRead;
|
return input + output + cacheCreation + cacheRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBreakdownKey(breakdown: { modelName: string; provider?: string }): string {
|
|
||||||
return `${breakdown.provider ?? ''}\u0000${breakdown.modelName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseDateKey(dateString: string): Date {
|
function parseDateKey(dateString: string): Date {
|
||||||
return new Date(
|
return new Date(
|
||||||
Date.UTC(
|
Date.UTC(
|
||||||
@@ -557,7 +557,7 @@ export async function handleModels(
|
|||||||
|
|
||||||
for (const day of filtered) {
|
for (const day of filtered) {
|
||||||
for (const breakdown of day.modelBreakdowns) {
|
for (const breakdown of day.modelBreakdowns) {
|
||||||
const modelKey = getBreakdownKey(breakdown);
|
const modelKey = getProviderModelKey(breakdown);
|
||||||
const existing = modelMap.get(modelKey) || {
|
const existing = modelMap.get(modelKey) || {
|
||||||
model: breakdown.modelName,
|
model: breakdown.modelName,
|
||||||
provider: breakdown.provider,
|
provider: breakdown.provider,
|
||||||
@@ -750,7 +750,7 @@ export async function handleMonthly(
|
|||||||
existing.cacheReadTokens += day.cacheReadTokens;
|
existing.cacheReadTokens += day.cacheReadTokens;
|
||||||
existing.totalCost += day.totalCost;
|
existing.totalCost += day.totalCost;
|
||||||
for (const breakdown of day.modelBreakdowns) {
|
for (const breakdown of day.modelBreakdowns) {
|
||||||
const breakdownKey = getBreakdownKey(breakdown);
|
const breakdownKey = getProviderModelKey(breakdown);
|
||||||
const existingBreakdown = existing.modelBreakdowns.get(breakdownKey) ?? {
|
const existingBreakdown = existing.modelBreakdowns.get(breakdownKey) ?? {
|
||||||
modelName: breakdown.modelName,
|
modelName: breakdown.modelName,
|
||||||
provider: breakdown.provider,
|
provider: breakdown.provider,
|
||||||
@@ -772,16 +772,21 @@ export async function handleMonthly(
|
|||||||
}
|
}
|
||||||
|
|
||||||
filtered = Array.from(monthMap.values())
|
filtered = Array.from(monthMap.values())
|
||||||
.map((month) => ({
|
.map((month) => {
|
||||||
month: month.month,
|
const modelBreakdowns = coalesceLegacyProviderlessBreakdowns(
|
||||||
inputTokens: month.inputTokens,
|
Array.from(month.modelBreakdowns.values())
|
||||||
outputTokens: month.outputTokens,
|
);
|
||||||
cacheCreationTokens: month.cacheCreationTokens,
|
return {
|
||||||
cacheReadTokens: month.cacheReadTokens,
|
month: month.month,
|
||||||
totalCost: month.totalCost,
|
inputTokens: month.inputTokens,
|
||||||
modelBreakdowns: Array.from(month.modelBreakdowns.values()),
|
outputTokens: month.outputTokens,
|
||||||
modelsUsed: getModelsUsed(Array.from(month.modelBreakdowns.values())),
|
cacheCreationTokens: month.cacheCreationTokens,
|
||||||
}))
|
cacheReadTokens: month.cacheReadTokens,
|
||||||
|
totalCost: month.totalCost,
|
||||||
|
modelBreakdowns,
|
||||||
|
modelsUsed: getModelsUsed(modelBreakdowns),
|
||||||
|
};
|
||||||
|
})
|
||||||
.sort((a, b) => a.month.localeCompare(b.month));
|
.sort((a, b) => a.month.localeCompare(b.month));
|
||||||
} else {
|
} else {
|
||||||
filtered = await getCachedMonthlyData();
|
filtered = await getCachedMonthlyData();
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ export interface ProviderModelIdentity {
|
|||||||
provider?: string;
|
provider?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MergeableProviderModelBreakdown extends ProviderModelIdentity {
|
||||||
|
inputTokens: number;
|
||||||
|
outputTokens: number;
|
||||||
|
cacheCreationTokens: number;
|
||||||
|
cacheReadTokens: number;
|
||||||
|
cost: number;
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeUsageProvider(provider: string | undefined): string | undefined {
|
export function normalizeUsageProvider(provider: string | undefined): string | undefined {
|
||||||
return normalizeModelsDevProviderId(provider);
|
return normalizeModelsDevProviderId(provider);
|
||||||
}
|
}
|
||||||
@@ -41,3 +49,46 @@ export function getModelsUsed(items: ProviderModelIdentity[]): string[] {
|
|||||||
|
|
||||||
return [...new Set(items.map((item) => getModelUsageLabel(item, ambiguousModelNames)))];
|
return [...new Set(items.map((item) => getModelUsageLabel(item, ambiguousModelNames)))];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addBreakdownTokens<T extends MergeableProviderModelBreakdown>(target: T, source: T): void {
|
||||||
|
target.inputTokens += source.inputTokens;
|
||||||
|
target.outputTokens += source.outputTokens;
|
||||||
|
target.cacheCreationTokens += source.cacheCreationTokens;
|
||||||
|
target.cacheReadTokens += source.cacheReadTokens;
|
||||||
|
target.cost += source.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coalesceLegacyProviderlessBreakdowns<T extends MergeableProviderModelBreakdown>(
|
||||||
|
items: T[]
|
||||||
|
): T[] {
|
||||||
|
const byModel = new Map<string, T[]>();
|
||||||
|
for (const item of items) {
|
||||||
|
const existing = byModel.get(item.modelName) ?? [];
|
||||||
|
existing.push(item);
|
||||||
|
byModel.set(item.modelName, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
const coalesced: T[] = [];
|
||||||
|
for (const group of byModel.values()) {
|
||||||
|
const providerBreakdowns = group.filter((item) => getProviderKey(item.provider));
|
||||||
|
const legacyBreakdowns = group.filter((item) => !getProviderKey(item.provider));
|
||||||
|
const providerKeys = new Set(providerBreakdowns.map((item) => getProviderKey(item.provider)));
|
||||||
|
|
||||||
|
if (legacyBreakdowns.length > 0 && providerKeys.size === 1 && providerBreakdowns.length > 0) {
|
||||||
|
const provider = Array.from(providerKeys)[0];
|
||||||
|
const [firstProviderBreakdown, ...remainingProviderBreakdowns] = providerBreakdowns;
|
||||||
|
const merged = { ...firstProviderBreakdown, provider } as T;
|
||||||
|
|
||||||
|
for (const breakdown of [...remainingProviderBreakdowns, ...legacyBreakdowns]) {
|
||||||
|
addBreakdownTokens(merged, breakdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
coalesced.push(merged);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
coalesced.push(...group.map((item) => ({ ...item })));
|
||||||
|
}
|
||||||
|
|
||||||
|
return coalesced;
|
||||||
|
}
|
||||||
@@ -355,6 +355,17 @@ describe('model-pricing', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
google: {
|
||||||
|
id: 'google',
|
||||||
|
name: 'Google',
|
||||||
|
models: {
|
||||||
|
'gemini-3-flash-preview': {
|
||||||
|
id: 'gemini-3-flash-preview',
|
||||||
|
name: 'Gemini 3 Flash Preview',
|
||||||
|
cost: { input: 99, output: 99 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -388,6 +399,14 @@ describe('model-pricing', () => {
|
|||||||
expect(getModelPricing('gpt-4o').inputPerMillion).toBe(2.5);
|
expect(getModelPricing('gpt-4o').inputPerMillion).toBe(2.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps CCS compatibility aliases ahead of provider-aware models.dev matches', () => {
|
||||||
|
const pricing = getModelPricing('gemini-3-flash-preview', { provider: 'google' });
|
||||||
|
const canonical = getModelPricing('gemini-2.5-flash');
|
||||||
|
|
||||||
|
expect(pricing).toEqual(canonical);
|
||||||
|
expect(pricing.inputPerMillion).not.toBe(99);
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to CCS static pricing when provider-aware models.dev lookup misses a known model', () => {
|
it('falls back to CCS static pricing when provider-aware models.dev lookup misses a known model', () => {
|
||||||
const staticPricing = getModelPricing('claude-sonnet-4-5');
|
const staticPricing = getModelPricing('claude-sonnet-4-5');
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,136 @@ describe('usage aggregator cliproxy integration', () => {
|
|||||||
expect(result[0].modelBreakdowns).toHaveLength(2);
|
expect(result[0].modelBreakdowns).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('coalesces legacy providerless breakdowns into the only known provider', () => {
|
||||||
|
const result = aggregator.mergeDailyData([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
date: '2026-03-02',
|
||||||
|
source: 'legacy-cache',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 1,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 1,
|
||||||
|
totalCost: 1,
|
||||||
|
modelsUsed: ['gpt-5.5'],
|
||||||
|
modelBreakdowns: [
|
||||||
|
{
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 1,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
date: '2026-03-02',
|
||||||
|
source: 'cliproxy',
|
||||||
|
inputTokens: 20,
|
||||||
|
outputTokens: 2,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 2,
|
||||||
|
totalCost: 2,
|
||||||
|
modelsUsed: ['gpt-5.5'],
|
||||||
|
modelBreakdowns: [
|
||||||
|
{
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
provider: 'openai',
|
||||||
|
inputTokens: 20,
|
||||||
|
outputTokens: 2,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result[0].modelBreakdowns).toHaveLength(1);
|
||||||
|
expect(result[0].modelBreakdowns[0]).toMatchObject({
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
provider: 'openai',
|
||||||
|
inputTokens: 30,
|
||||||
|
outputTokens: 3,
|
||||||
|
cost: 3,
|
||||||
|
});
|
||||||
|
expect(result[0].modelsUsed).toEqual(['gpt-5.5']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps legacy providerless breakdowns separate when providers are ambiguous', () => {
|
||||||
|
const result = aggregator.mergeHourlyData([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
hour: '2026-03-02 10:00',
|
||||||
|
source: 'legacy-cache',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 1,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 1,
|
||||||
|
totalCost: 1,
|
||||||
|
modelsUsed: ['gpt-5.5'],
|
||||||
|
modelBreakdowns: [
|
||||||
|
{
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 1,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
hour: '2026-03-02 10:00',
|
||||||
|
source: 'providers',
|
||||||
|
inputTokens: 50,
|
||||||
|
outputTokens: 5,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 5,
|
||||||
|
totalCost: 5,
|
||||||
|
modelsUsed: ['openai/gpt-5.5', 'github-copilot/gpt-5.5'],
|
||||||
|
modelBreakdowns: [
|
||||||
|
{
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
provider: 'openai',
|
||||||
|
inputTokens: 20,
|
||||||
|
outputTokens: 2,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
provider: 'github-copilot',
|
||||||
|
inputTokens: 30,
|
||||||
|
outputTokens: 3,
|
||||||
|
cacheCreationTokens: 0,
|
||||||
|
cacheReadTokens: 0,
|
||||||
|
cost: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result[0].modelBreakdowns).toHaveLength(3);
|
||||||
|
expect(result[0].modelsUsed).toEqual([
|
||||||
|
'gpt-5.5',
|
||||||
|
'openai/gpt-5.5',
|
||||||
|
'github-copilot/gpt-5.5',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to model cardinality when merging legacy hourly buckets without requestCount', () => {
|
it('falls back to model cardinality when merging legacy hourly buckets without requestCount', () => {
|
||||||
const result = aggregator.mergeHourlyData([
|
const result = aggregator.mergeHourlyData([
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in new issue
Block a user