mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-05 14:22:12 +00:00
fix(usage): honor provider-aware pricing precedence
This commit is contained in:
@@ -845,11 +845,6 @@ function hasProviderContext(model: string, options: PricingLookupOptions): boole
|
|||||||
* first known family tier that happens to share a prefix.
|
* first known family tier that happens to share a prefix.
|
||||||
*/
|
*/
|
||||||
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
export function getModelPricing(model: string, options: PricingLookupOptions = {}): ModelPricing {
|
||||||
const exactPricing = PRICING_REGISTRY[model];
|
|
||||||
if (exactPricing !== undefined) {
|
|
||||||
return exactPricing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasProviderContext(model, options)) {
|
if (hasProviderContext(model, options)) {
|
||||||
const providerPricing = resolveModelsDevPricing(model, options);
|
const providerPricing = resolveModelsDevPricing(model, options);
|
||||||
if (providerPricing !== undefined) {
|
if (providerPricing !== undefined) {
|
||||||
@@ -857,6 +852,11 @@ export function getModelPricing(model: string, options: PricingLookupOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exactPricing = PRICING_REGISTRY[model];
|
||||||
|
if (exactPricing !== undefined) {
|
||||||
|
return exactPricing;
|
||||||
|
}
|
||||||
|
|
||||||
const directOrAliasPricing = getDirectOrAliasPricing(model);
|
const directOrAliasPricing = getDirectOrAliasPricing(model);
|
||||||
if (directOrAliasPricing !== undefined) {
|
if (directOrAliasPricing !== undefined) {
|
||||||
return directOrAliasPricing;
|
return directOrAliasPricing;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import type { CliproxyUsageApiResponse, CliproxyRequestDetail } from '../../cliproxy/stats-fetcher';
|
import type { CliproxyUsageApiResponse, CliproxyRequestDetail } from '../../cliproxy/stats-fetcher';
|
||||||
import { calculateCost } from '../model-pricing';
|
import { calculateCost } from '../model-pricing';
|
||||||
import type { ModelBreakdown, DailyUsage, HourlyUsage, MonthlyUsage } from './types';
|
import type { ModelBreakdown, DailyUsage, HourlyUsage, MonthlyUsage } from './types';
|
||||||
import { getModelsUsed } from './model-identity';
|
import { getModelsUsed, normalizeUsageProvider } from './model-identity';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERNAL HELPERS
|
// INTERNAL HELPERS
|
||||||
@@ -60,7 +60,7 @@ function createHistoryDetail(
|
|||||||
model: string,
|
model: string,
|
||||||
detail: CliproxyRequestDetail
|
detail: CliproxyRequestDetail
|
||||||
): CliproxyUsageHistoryDetail {
|
): CliproxyUsageHistoryDetail {
|
||||||
const pricingProvider = provider.trim().toLowerCase();
|
const pricingProvider = normalizeUsageProvider(provider) ?? provider.trim().toLowerCase();
|
||||||
return {
|
return {
|
||||||
model,
|
model,
|
||||||
provider: pricingProvider,
|
provider: pricingProvider,
|
||||||
|
|||||||
@@ -327,6 +327,11 @@ describe('model-pricing', () => {
|
|||||||
name: 'GPT-5.5',
|
name: 'GPT-5.5',
|
||||||
cost: { input: 5, output: 30, cache_read: 0.5 },
|
cost: { input: 5, output: 30, cache_read: 0.5 },
|
||||||
},
|
},
|
||||||
|
'gpt-4o': {
|
||||||
|
id: 'gpt-4o',
|
||||||
|
name: 'GPT-4o',
|
||||||
|
cost: { input: 2.5, output: 10, cache_read: 1.25 },
|
||||||
|
},
|
||||||
'openai-exclusive-model': {
|
'openai-exclusive-model': {
|
||||||
id: 'openai-exclusive-model',
|
id: 'openai-exclusive-model',
|
||||||
name: 'OpenAI Exclusive Model',
|
name: 'OpenAI Exclusive Model',
|
||||||
@@ -343,6 +348,11 @@ describe('model-pricing', () => {
|
|||||||
name: 'GPT-5.5',
|
name: 'GPT-5.5',
|
||||||
cost: { input: 0, output: 0 },
|
cost: { input: 0, output: 0 },
|
||||||
},
|
},
|
||||||
|
'gpt-4o': {
|
||||||
|
id: 'gpt-4o',
|
||||||
|
name: 'GPT-4o',
|
||||||
|
cost: { input: 0, output: 0 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -371,6 +381,13 @@ describe('model-pricing', () => {
|
|||||||
expect(pricing.outputPerMillion).toBe(0);
|
expect(pricing.outputPerMillion).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prefers provider-aware models.dev pricing over exact static table matches', () => {
|
||||||
|
const pricing = getModelPricing('gpt-4o', { provider: 'github-copilot' });
|
||||||
|
expect(pricing.inputPerMillion).toBe(0);
|
||||||
|
expect(pricing.outputPerMillion).toBe(0);
|
||||||
|
expect(getModelPricing('gpt-4o').inputPerMillion).toBe(2.5);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not use ambiguous model-only models.dev matches', () => {
|
it('does not use ambiguous model-only models.dev matches', () => {
|
||||||
const pricing = getModelPricing('gpt-5.5');
|
const pricing = getModelPricing('gpt-5.5');
|
||||||
expect(pricing).toEqual(getModelPricing('unknown-model-xyz'));
|
expect(pricing).toEqual(getModelPricing('unknown-model-xyz'));
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ describe('cliproxy usage transformer', () => {
|
|||||||
it('retains failed requests when they carry usage and skips zero-usage failures', () => {
|
it('retains failed requests when they carry usage and skips zero-usage failures', () => {
|
||||||
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
|
const flat = extractCliproxyUsageHistoryDetails(sampleResponse);
|
||||||
expect(flat).toHaveLength(4);
|
expect(flat).toHaveLength(4);
|
||||||
expect(flat[0].provider).toBe('gemini');
|
expect(flat[0].provider).toBe('google');
|
||||||
expect(
|
expect(
|
||||||
flat.some(
|
flat.some(
|
||||||
(entry) =>
|
(entry) =>
|
||||||
@@ -296,5 +296,71 @@ describe('cliproxy usage transformer', () => {
|
|||||||
expect(paid?.cost).toBe(35.5);
|
expect(paid?.cost).toBe(35.5);
|
||||||
expect(subscription?.cost).toBe(0);
|
expect(subscription?.cost).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('canonicalizes CLIProxy provider aliases before grouping history details', () => {
|
||||||
|
const response: CliproxyUsageApiResponse = {
|
||||||
|
usage: {
|
||||||
|
apis: {
|
||||||
|
ghcp: {
|
||||||
|
models: {
|
||||||
|
'gpt-5.5': {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
timestamp: '2026-03-03T10:00:00.000Z',
|
||||||
|
source: 'copilot-alias',
|
||||||
|
auth_index: 0,
|
||||||
|
tokens: {
|
||||||
|
input_tokens: 1_000_000,
|
||||||
|
output_tokens: 0,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
cached_tokens: 0,
|
||||||
|
total_tokens: 1_000_000,
|
||||||
|
},
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'github-copilot': {
|
||||||
|
models: {
|
||||||
|
'gpt-5.5': {
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
timestamp: '2026-03-03T11:00:00.000Z',
|
||||||
|
source: 'copilot-canonical',
|
||||||
|
auth_index: 1,
|
||||||
|
tokens: {
|
||||||
|
input_tokens: 2_000_000,
|
||||||
|
output_tokens: 0,
|
||||||
|
reasoning_tokens: 0,
|
||||||
|
cached_tokens: 0,
|
||||||
|
total_tokens: 2_000_000,
|
||||||
|
},
|
||||||
|
failed: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const details = extractCliproxyUsageHistoryDetails(response);
|
||||||
|
expect(details.map((detail) => detail.provider)).toEqual([
|
||||||
|
'github-copilot',
|
||||||
|
'github-copilot',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [daily] = transformCliproxyToDailyUsage(response);
|
||||||
|
expect(daily.modelBreakdowns).toHaveLength(1);
|
||||||
|
expect(daily.modelBreakdowns[0]).toMatchObject({
|
||||||
|
modelName: 'gpt-5.5',
|
||||||
|
provider: 'github-copilot',
|
||||||
|
inputTokens: 3_000_000,
|
||||||
|
});
|
||||||
|
expect(daily.modelsUsed).toEqual(['gpt-5.5']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user