Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions src/utils/format-model-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ function stripVersionSuffix(part: string): string {
return part.replace(/@(default|\d{6,8})$/i, '')
}

/**
* Like `looksLikeVersionComponent`, but tolerates a single leading
* version letter — only the letters seen in real version positions
* (`v3`, `k2`, `m2`), so quantization prefixes like `q4` never pair.
* Dotted ids like `deepseek-v3.2` or `kimi-k2.5` have their version
* split apart by the tokenizer, and the letter-prefixed half must
* still merge back with the digits after the dot.
*/
function looksLikePrefixedVersionComponent(token: string): boolean {
return /^[vkm]?\d{1,2}$/i.test(token)
}

/**
* Heuristic: does this numeric token look like a real version component
* (single or double digit) rather than a date stamp or build number?
Expand All @@ -31,6 +43,15 @@ function looksLikeVersionComponent(token: string): boolean {
* version pair, but the formatter would otherwise render it as
* "Claude Opus 4 5".
*
* The left-hand token may also carry a single version letter (`v3`,
* `k2`, `m2`): ids like `deepseek-v3.2` or `kimi-k2.5` get their dot
* split apart by the tokenizer and must re-join as `v3.2` / `k2.5`,
* never render as "DeepSeek V3 2" / "Kimi K2 5".
* Quantization prefixes never pair (`q4_0` tokenises as `q4` + `0`,
* but `q` is not a version letter), so `mistral-7b-instruct-q4_0`
* renders "Mistral 7B Instruct Q4 0" while ids with a real version
* pair like `llama-3.1-8b-instruct-q4_0` keep merging `3.1`.
*
* Conservative on purpose:
* - Only the FIRST mergeable pair scanning from the right is collapsed:
* `gpt-3-5-turbo-16k` merges `3-5` into `3.5` and stops there.
Expand All @@ -49,7 +70,7 @@ function joinTrailingVersionPair(tokens: string[]): string[] {
for (let i = tokens.length - 1; i >= 1; i--) {
const a = tokens[i - 1]
const b = tokens[i]
if (looksLikeVersionComponent(a) && looksLikeVersionComponent(b)) {
if (looksLikePrefixedVersionComponent(a) && looksLikeVersionComponent(b)) {
const prev = tokens[i - 2]
const next = tokens[i + 1]
const prevIsShortNumeric = prev !== undefined && looksLikeVersionComponent(prev)
Expand Down Expand Up @@ -101,7 +122,11 @@ export function categorizeModel(model: LiteLLMModel): 'chat' | 'embedding' | 'im
* - Collapse a trailing pair of bare numeric tokens into a single
* dotted version. Anthropic uses dash-only ids — `claude-opus-4-7`
* would otherwise render as "Claude Opus 4 7" instead of
* "Claude Opus 4.7".
* "Claude Opus 4.7". The left-hand token may carry a version
* letter, so `deepseek-v3.2` renders "DeepSeek V3.2", not
* "Deepseek V3 2".
* - Keep the official casing of known brand names (GLM, DeepSeek,
* MiniMax) instead of generic title-case.
*
* Examples:
* "openai/gpt-4o-mini" -> "GPT 4o Mini"
Expand All @@ -110,6 +135,10 @@ export function categorizeModel(model: LiteLLMModel): 'chat' | 'embedding' | 'im
* "claude-sonnet-4-6@default" -> "Claude Sonnet 4.6"
* "bedrock/amazon.nova-pro-v1" -> "Amazon Nova Pro V1"
* "qwen/qwen3-30b-a3b" -> "Qwen3 30B A3B"
* "litellm/deepseek-v3.2" -> "DeepSeek V3.2"
* "litellm/glm-5.3" -> "GLM 5.3"
* "litellm/kimi-k2.5" -> "Kimi K2.5"
* "litellm/minimax-m2.5" -> "MiniMax M2.5"
*/
export function formatModelName(model: LiteLLMModel): string {
const { id } = model
Expand All @@ -124,10 +153,17 @@ export function formatModelName(model: LiteLLMModel): string {
const modelPart = stripVersionSuffix(afterProvider)

const acronyms = new Set([
'gpt', 'oss', 'api', 'gguf', 'ggml', 'nomic', 'vl', 'it', 'mlx',
'gpt', 'glm', 'oss', 'api', 'gguf', 'ggml', 'nomic', 'vl', 'it', 'mlx',
'llm', 'ai', 'sdk', 'aws', 'gcp', 'tts', 'stt', 'mm',
])

// Mixed-case brand names that generic title-case would mangle
// ("Deepseek", "Minimax").
const brandNames: Record<string, string> = {
deepseek: 'DeepSeek',
minimax: 'MiniMax',
}

const rawTokens = modelPart
.split(/[-_.]/) // split on -, _ and .
.filter(Boolean)
Expand All @@ -138,6 +174,7 @@ export function formatModelName(model: LiteLLMModel): string {
// when both sides are pure digits).
const tokens = joinTrailingVersionPair(rawTokens).map((token) => {
const lower = token.toLowerCase()
if (brandNames[lower]) return brandNames[lower]
if (acronyms.has(lower)) return token.toUpperCase()
// sizes (30b, 7b, 4k...)
if (/^\d+[bkmg]$/i.test(token)) return token.toUpperCase()
Expand Down
8 changes: 8 additions & 0 deletions test/format-model-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ describe('formatModelName', () => {
['claude-sonnet-4-6@default', 'Claude Sonnet 4.6'],
['bedrock/amazon.nova-pro-v1', 'Amazon Nova Pro V1'],
['qwen/qwen3-30b-a3b', 'Qwen3 30B A3B'],
['gemini/gemini-2.5-flash', 'Gemini 2.5 Flash'],
['openai/whisper-large-v3', 'Whisper Large V3'],
['llama-3.1-8b-instruct-q4_0', 'Llama 3.1 8B Instruct Q4 0'],
['mistral-7b-instruct-q4_0', 'Mistral 7B Instruct Q4 0'],
['litellm/deepseek-v3.2', 'DeepSeek V3.2'],
['litellm/glm-5.3', 'GLM 5.3'],
['litellm/kimi-k2.5', 'Kimi K2.5'],
['litellm/minimax-m2.5', 'MiniMax M2.5'],
]

it.each(cases)('formats %s as %s', (id, expected) => {
Expand Down
Loading