mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(pricing): normalize model aliases for MiniMax and Qwen
This commit is contained in:
@@ -686,6 +686,14 @@ const PRICING_REGISTRY: Record<string, ModelPricing> = {
|
||||
},
|
||||
};
|
||||
|
||||
const MODEL_PRICING_ALIASES: Record<string, string> = {
|
||||
// Keep catalog-only IDs on explicit priced equivalents.
|
||||
'qwen3-coder': 'qwen3-coder-plus',
|
||||
'qwen3-235b': 'qwen3-max',
|
||||
'qwen3-vl-plus': 'qwen3.5-plus',
|
||||
'qwen3-32b': 'qwen3.5-plus',
|
||||
};
|
||||
|
||||
// Default pricing for unknown models
|
||||
const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
||||
inputPerMillion: 3.0,
|
||||
@@ -704,39 +712,76 @@ const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
||||
*/
|
||||
function normalizeModelName(model: string): string {
|
||||
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
|
||||
const normalized = model.toLowerCase().replace(/^[^/]+\//, '');
|
||||
const normalized = model
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/^[^/]+\//, '');
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const NORMALIZED_PRICING_REGISTRY: Record<string, ModelPricing> = Object.entries(
|
||||
PRICING_REGISTRY
|
||||
).reduce<Record<string, ModelPricing>>((acc, [key, pricing]) => {
|
||||
acc[normalizeModelName(key)] = pricing;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
function getLookupCandidates(model: string): string[] {
|
||||
const normalized = normalizeModelName(model);
|
||||
const baseModel = normalized.split(':')[0];
|
||||
|
||||
return baseModel === normalized ? [normalized] : [normalized, baseModel];
|
||||
}
|
||||
|
||||
function getDirectOrAliasPricing(model: string): ModelPricing | undefined {
|
||||
const directPricing = PRICING_REGISTRY[model];
|
||||
if (directPricing !== undefined) {
|
||||
return directPricing;
|
||||
}
|
||||
|
||||
for (const candidate of getLookupCandidates(model)) {
|
||||
const normalizedPricing = NORMALIZED_PRICING_REGISTRY[candidate];
|
||||
if (normalizedPricing !== undefined) {
|
||||
return normalizedPricing;
|
||||
}
|
||||
|
||||
const alias = MODEL_PRICING_ALIASES[candidate];
|
||||
if (alias !== undefined) {
|
||||
const aliasPricing = NORMALIZED_PRICING_REGISTRY[alias];
|
||||
if (aliasPricing !== undefined) {
|
||||
return aliasPricing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pricing for a model with fuzzy matching fallback
|
||||
* @param model - Model name (exact or with provider prefix)
|
||||
* @returns ModelPricing for the model or fallback pricing
|
||||
*/
|
||||
export function getModelPricing(model: string): ModelPricing {
|
||||
// Try exact match first
|
||||
if (PRICING_REGISTRY[model]) {
|
||||
return PRICING_REGISTRY[model];
|
||||
const directOrAliasPricing = getDirectOrAliasPricing(model);
|
||||
if (directOrAliasPricing !== undefined) {
|
||||
return directOrAliasPricing;
|
||||
}
|
||||
|
||||
// Try normalized match
|
||||
const normalized = normalizeModelName(model);
|
||||
if (PRICING_REGISTRY[normalized]) {
|
||||
return PRICING_REGISTRY[normalized];
|
||||
}
|
||||
|
||||
// Try suffix matching (e.g., "claude-sonnet-4-5" matches "*-claude-sonnet-4-5")
|
||||
for (const [key, pricing] of Object.entries(PRICING_REGISTRY)) {
|
||||
if (normalized.endsWith(key) || key.endsWith(normalized)) {
|
||||
return pricing;
|
||||
for (const candidate of getLookupCandidates(model)) {
|
||||
// Try suffix matching (e.g., "claude-sonnet-4-5" matches "*-claude-sonnet-4-5")
|
||||
for (const [key, pricing] of Object.entries(NORMALIZED_PRICING_REGISTRY)) {
|
||||
if (candidate.endsWith(key) || key.endsWith(candidate)) {
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try partial matching for model families
|
||||
for (const [key, pricing] of Object.entries(PRICING_REGISTRY)) {
|
||||
// Match by model family prefix
|
||||
if (normalized.startsWith(key.split('-').slice(0, 2).join('-'))) {
|
||||
return pricing;
|
||||
// Try partial matching for model families
|
||||
for (const [key, pricing] of Object.entries(NORMALIZED_PRICING_REGISTRY)) {
|
||||
// Match by model family prefix
|
||||
if (candidate.startsWith(key.split('-').slice(0, 2).join('-'))) {
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,8 +818,5 @@ export function getKnownModels(): string[] {
|
||||
* Check if a model has custom pricing (not using fallback)
|
||||
*/
|
||||
export function hasCustomPricing(model: string): boolean {
|
||||
return (
|
||||
PRICING_REGISTRY[model] !== undefined ||
|
||||
PRICING_REGISTRY[normalizeModelName(model)] !== undefined
|
||||
);
|
||||
return getDirectOrAliasPricing(model) !== undefined;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,41 @@ describe('model-pricing', () => {
|
||||
// Should match via normalization
|
||||
});
|
||||
|
||||
it('should resolve lowercase MiniMax model IDs to custom pricing', () => {
|
||||
const pricing = getModelPricing('minimax-m2.5');
|
||||
expect(pricing.inputPerMillion).toBe(0.3);
|
||||
expect(pricing.outputPerMillion).toBe(1.2);
|
||||
});
|
||||
|
||||
it('should resolve provider-prefixed MiniMax model IDs to custom pricing', () => {
|
||||
const pricing = getModelPricing('minimax/MiniMax-M2.5');
|
||||
expect(pricing.inputPerMillion).toBe(0.3);
|
||||
expect(pricing.outputPerMillion).toBe(1.2);
|
||||
});
|
||||
|
||||
it('should use updated MiniMax-M2.1-lightning input pricing', () => {
|
||||
const pricing = getModelPricing('MiniMax-M2.1-lightning');
|
||||
expect(pricing.inputPerMillion).toBe(0.6);
|
||||
});
|
||||
|
||||
it('should not use fallback pricing for known Qwen catalog IDs', () => {
|
||||
const fallback = getModelPricing('unknown-model-xyz');
|
||||
const catalogIds = ['qwen3-235b', 'qwen3-vl-plus', 'qwen3-32b'];
|
||||
|
||||
for (const model of catalogIds) {
|
||||
const pricing = getModelPricing(model);
|
||||
expect(pricing).not.toEqual(fallback);
|
||||
}
|
||||
});
|
||||
|
||||
it('should map qwen3-coder to deterministic custom pricing', () => {
|
||||
const pricing = getModelPricing('qwen3-coder');
|
||||
const canonical = getModelPricing('qwen3-coder-plus');
|
||||
|
||||
expect(pricing).toEqual(canonical);
|
||||
expect(pricing).not.toEqual(getModelPricing('unknown-model-xyz'));
|
||||
});
|
||||
|
||||
it('should return different pricing for different model tiers', () => {
|
||||
const sonnet = getModelPricing('claude-sonnet-4-5');
|
||||
const opus = getModelPricing('claude-opus-4-5-20251101');
|
||||
@@ -134,6 +169,10 @@ describe('model-pricing', () => {
|
||||
expect(hasCustomPricing('glm-4.6')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for deterministic qwen3-coder alias', () => {
|
||||
expect(hasCustomPricing('qwen3-coder')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unknown models', () => {
|
||||
expect(hasCustomPricing('unknown-model-xyz')).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user