fix(pricing): normalize model aliases for MiniMax and Qwen

This commit is contained in:
Tam Nhu Tran
2026-02-26 14:15:32 +07:00
parent 089aa08136
commit 7ac5e3edab
2 changed files with 105 additions and 24 deletions
+66 -24
View File
@@ -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;
}
+39
View File
@@ -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);
});