mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
fix(pricing): normalize model aliases for MiniMax and Qwen
This commit is contained in:
1 parent
089aa08136
commit
7ac5e3edab
2 files changed
+105
-24
No files matched your search
@@ -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
|
// Default pricing for unknown models
|
||||||
const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
||||||
inputPerMillion: 3.0,
|
inputPerMillion: 3.0,
|
||||||
@@ -704,39 +712,76 @@ const UNKNOWN_MODEL_PRICING: ModelPricing = {
|
|||||||
*/
|
*/
|
||||||
function normalizeModelName(model: string): string {
|
function normalizeModelName(model: string): string {
|
||||||
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
|
// Remove provider prefixes (e.g., "anthropic/claude-..." -> "claude-...")
|
||||||
const normalized = model.toLowerCase().replace(/^[^/]+\//, '');
|
const normalized = model
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^[^/]+\//, '');
|
||||||
return normalized;
|
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
|
* Get pricing for a model with fuzzy matching fallback
|
||||||
* @param model - Model name (exact or with provider prefix)
|
* @param model - Model name (exact or with provider prefix)
|
||||||
* @returns ModelPricing for the model or fallback pricing
|
* @returns ModelPricing for the model or fallback pricing
|
||||||
*/
|
*/
|
||||||
export function getModelPricing(model: string): ModelPricing {
|
export function getModelPricing(model: string): ModelPricing {
|
||||||
// Try exact match first
|
const directOrAliasPricing = getDirectOrAliasPricing(model);
|
||||||
if (PRICING_REGISTRY[model]) {
|
if (directOrAliasPricing !== undefined) {
|
||||||
return PRICING_REGISTRY[model];
|
return directOrAliasPricing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try normalized match
|
for (const candidate of getLookupCandidates(model)) {
|
||||||
const normalized = normalizeModelName(model);
|
// Try suffix matching (e.g., "claude-sonnet-4-5" matches "*-claude-sonnet-4-5")
|
||||||
if (PRICING_REGISTRY[normalized]) {
|
for (const [key, pricing] of Object.entries(NORMALIZED_PRICING_REGISTRY)) {
|
||||||
return PRICING_REGISTRY[normalized];
|
if (candidate.endsWith(key) || key.endsWith(candidate)) {
|
||||||
}
|
return pricing;
|
||||||
|
}
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Try partial matching for model families
|
// Try partial matching for model families
|
||||||
for (const [key, pricing] of Object.entries(PRICING_REGISTRY)) {
|
for (const [key, pricing] of Object.entries(NORMALIZED_PRICING_REGISTRY)) {
|
||||||
// Match by model family prefix
|
// Match by model family prefix
|
||||||
if (normalized.startsWith(key.split('-').slice(0, 2).join('-'))) {
|
if (candidate.startsWith(key.split('-').slice(0, 2).join('-'))) {
|
||||||
return pricing;
|
return pricing;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,8 +818,5 @@ export function getKnownModels(): string[] {
|
|||||||
* Check if a model has custom pricing (not using fallback)
|
* Check if a model has custom pricing (not using fallback)
|
||||||
*/
|
*/
|
||||||
export function hasCustomPricing(model: string): boolean {
|
export function hasCustomPricing(model: string): boolean {
|
||||||
return (
|
return getDirectOrAliasPricing(model) !== undefined;
|
||||||
PRICING_REGISTRY[model] !== undefined ||
|
|
||||||
PRICING_REGISTRY[normalizeModelName(model)] !== undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,41 @@ describe('model-pricing', () => {
|
|||||||
// Should match via normalization
|
// 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', () => {
|
it('should return different pricing for different model tiers', () => {
|
||||||
const sonnet = getModelPricing('claude-sonnet-4-5');
|
const sonnet = getModelPricing('claude-sonnet-4-5');
|
||||||
const opus = getModelPricing('claude-opus-4-5-20251101');
|
const opus = getModelPricing('claude-opus-4-5-20251101');
|
||||||
@@ -134,6 +169,10 @@ describe('model-pricing', () => {
|
|||||||
expect(hasCustomPricing('glm-4.6')).toBe(true);
|
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', () => {
|
it('should return false for unknown models', () => {
|
||||||
expect(hasCustomPricing('unknown-model-xyz')).toBe(false);
|
expect(hasCustomPricing('unknown-model-xyz')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in new issue
Block a user