Skip to content
Draft
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
237 changes: 237 additions & 0 deletions src/utils/__tests__/model-friendly-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
describe,
expect,
it
} from 'vitest';

import { getFriendlyModelName } from '../model-friendly-name';

describe('getFriendlyModelName', () => {
describe('Claude 4+ models — Opus 4.6', () => {
it('should parse global.anthropic opus 4.6 with [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-opus-4-6-v1[1m]'
)).toBe('Opus 4.6 (1M context)');
});

it('should parse global.anthropic opus 4.6 without [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-opus-4-6-v1'
)).toBe('Opus 4.6');
});

it('should parse us.anthropic opus 4.6 with [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-opus-4-6-v1[1m]'
)).toBe('Opus 4.6 (1M context)');
});

it('should parse us.anthropic opus 4.6 without [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-opus-4-6-v1'
)).toBe('Opus 4.6');
});

it('should parse anthropic.claude opus 4.6', () => {
expect(getFriendlyModelName(
'anthropic.claude-opus-4-6-v1'
)).toBe('Opus 4.6');
});
});

describe('Claude 4+ models — Sonnet 4.6', () => {
it('should parse global.anthropic sonnet 4.6 with [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-sonnet-4-6[1m]'
)).toBe('Sonnet 4.6 (1M context)');
});

it('should parse global.anthropic sonnet 4.6 without [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-sonnet-4-6'
)).toBe('Sonnet 4.6');
});

it('should parse us.anthropic sonnet 4.6 with [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-sonnet-4-6[1m]'
)).toBe('Sonnet 4.6 (1M context)');
});

it('should parse us.anthropic sonnet 4.6 without [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-sonnet-4-6'
)).toBe('Sonnet 4.6');
});

it('should parse anthropic.claude sonnet 4.6', () => {
expect(getFriendlyModelName(
'anthropic.claude-sonnet-4-6'
)).toBe('Sonnet 4.6');
});
});

describe('Claude 4+ models — Sonnet 4.5', () => {
it('should parse global.anthropic sonnet 4.5 with [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-sonnet-4-5-20250929-v1:0[1m]'
)).toBe('Sonnet 4.5 (1M context)');
});

it('should parse global.anthropic sonnet 4.5 without [1m]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-sonnet-4-5-20250929-v1:0'
)).toBe('Sonnet 4.5');
});

it('should parse us.anthropic sonnet 4.5 with [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-sonnet-4-5-20250929-v1:0[1m]'
)).toBe('Sonnet 4.5 (1M context)');
});

it('should parse us.anthropic sonnet 4.5 without [1m]', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-sonnet-4-5-20250929-v1:0'
)).toBe('Sonnet 4.5');
});

it('should parse anthropic.claude sonnet 4.5', () => {
expect(getFriendlyModelName(
'anthropic.claude-sonnet-4-5-20250929-v1:0'
)).toBe('Sonnet 4.5');
});
});

describe('Claude 4+ models — Haiku 4.5', () => {
it('should parse global.anthropic haiku 4.5', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-haiku-4-5-20251001-v1:0'
)).toBe('Haiku 4.5');
});

it('should parse us.anthropic haiku 4.5', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-haiku-4-5-20251001-v1:0'
)).toBe('Haiku 4.5');
});

it('should parse anthropic.claude haiku 4.5', () => {
expect(getFriendlyModelName(
'anthropic.claude-haiku-4-5-20251001-v1:0'
)).toBe('Haiku 4.5');
});
});

describe('Claude 4+ models — Opus 4.5', () => {
it('should parse anthropic.claude opus 4.5', () => {
expect(getFriendlyModelName(
'anthropic.claude-opus-4-5-20251101-v1:0'
)).toBe('Opus 4.5');
});
});

describe('Claude 3.x models — Haiku 3.5', () => {
it('should parse anthropic.claude haiku 3.5', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-5-haiku-20241022-v1:0'
)).toBe('Haiku 3.5');
});

it('should parse global.anthropic haiku 3.5', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-3-5-haiku-20241022-v1:0'
)).toBe('Haiku 3.5');
});

it('should parse us.anthropic haiku 3.5', () => {
expect(getFriendlyModelName(
'us.anthropic.claude-3-5-haiku-20241022-v1:0'
)).toBe('Haiku 3.5');
});
});

describe('Claude 3.x models — Sonnet 3.7', () => {
it('should parse anthropic.claude sonnet 3.7', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-7-sonnet-20250219-v1:0'
)).toBe('Sonnet 3.7');
});
});

describe('Claude 3.x models — Sonnet 3.5', () => {
it('should parse anthropic.claude sonnet 3.5 v2', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-5-sonnet-20241022-v2:0'
)).toBe('Sonnet 3.5');
});

it('should parse anthropic.claude sonnet 3.5 v2 with size qualifier', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-5-sonnet-20241022-v2:0:200k'
)).toBe('Sonnet 3.5');
});
});

describe('Claude 3.x models — without minor version', () => {
it('should parse anthropic.claude opus 3', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-opus-20240229-v1:0'
)).toBe('Opus 3');
});

it('should parse anthropic.claude haiku 3', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-haiku-20240307-v1:0'
)).toBe('Haiku 3');
});

it('should parse anthropic.claude sonnet 3', () => {
expect(getFriendlyModelName(
'anthropic.claude-3-sonnet-20240229-v1:0'
)).toBe('Sonnet 3');
});
});

describe('bare claude model IDs (Pro/Max)', () => {
it('should parse bare claude opus with [1m]', () => {
expect(getFriendlyModelName(
'claude-opus-4-6[1m]'
)).toBe('Opus 4.6 (1M context)');
});

it('should parse bare claude sonnet with date', () => {
expect(getFriendlyModelName(
'claude-sonnet-4-5-20250929'
)).toBe('Sonnet 4.5');
});

it('should parse bare claude sonnet with date and [1m]', () => {
expect(getFriendlyModelName(
'claude-sonnet-4-5-20250929[1m]'
)).toBe('Sonnet 4.5 (1M context)');
});
});

describe('case insensitive [1M] suffix', () => {
it('should handle uppercase [1M]', () => {
expect(getFriendlyModelName(
'global.anthropic.claude-opus-4-6-v1[1M]'
)).toBe('Opus 4.6 (1M context)');
});
});

describe('unrecognized model strings', () => {
it('should return the original string for non-claude models', () => {
expect(getFriendlyModelName('gpt-4o')).toBe('gpt-4o');
});

it('should return the original string for empty input', () => {
expect(getFriendlyModelName('')).toBe('');
});

it('should return the original string for unexpected formats', () => {
expect(getFriendlyModelName('some-random-model')).toBe('some-random-model');
});
});
});
58 changes: 58 additions & 0 deletions src/utils/model-friendly-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* Parses a raw model ID string and returns a user-friendly display name.
*
* Handles Claude 4+ format (family before version):
* global.anthropic.claude-opus-4-6-v1[1m]
* us.anthropic.claude-sonnet-4-5-20250929-v1:0[1m]
* anthropic.claude-haiku-4-5-20251001-v1:0
* claude-sonnet-4-5-20250929[1m]
*
* Handles Claude 3.x format (version before family):
* anthropic.claude-3-5-haiku-20241022-v1:0
* us.anthropic.claude-3-7-sonnet-20250219-v1:0
* anthropic.claude-3-opus-20240229-v1:0
*
* Returns the original string if it cannot be parsed.
*/
export function getFriendlyModelName(modelId: string): string {
// Claude 4+ format: claude-{family}-{major}-{minor}[-date][-vN][:N][[1m]]
const claude4Match = /(?:[\w.-]*\.)?claude-(\w+)-(\d+)-(\d+)(?:-\d{8})?(?:-v\d+)?(?::\d+)?(\[1[mM]\])?$/.exec(modelId);

if (claude4Match) {
const [, rawFamily, major, minor, extendedContext] = claude4Match;

if (rawFamily && major && minor) {
const family = rawFamily.charAt(0).toUpperCase() + rawFamily.slice(1);
const name = `${family} ${major}.${minor}`;
return extendedContext ? `${name} (1M context)` : name;
}
}

// Claude 3.x format with minor version: claude-{major}-{minor}-{family}[-date][-vN][:N][:Nk]
const claude3MinorMatch = /(?:[\w.-]*\.)?claude-(\d+)-(\d+)-(\w+)(?:-\d{8})?(?:-v\d+)?(?::\d+)?(?::\d+k)?(\[1[mM]\])?$/.exec(modelId);

if (claude3MinorMatch) {
const [, major, minor, rawFamily, extendedContext] = claude3MinorMatch;

if (major && minor && rawFamily) {
const family = rawFamily.charAt(0).toUpperCase() + rawFamily.slice(1);
const name = `${family} ${major}.${minor}`;
return extendedContext ? `${name} (1M context)` : name;
}
}

// Claude 3.x format without minor version: claude-{major}-{family}[-date][-vN][:N][:Nk]
const claude3Match = /(?:[\w.-]*\.)?claude-(\d+)-(\w+)(?:-\d{8})?(?:-v\d+)?(?::\d+)?(?::\d+k)?(\[1[mM]\])?$/.exec(modelId);

if (claude3Match) {
const [, major, rawFamily, extendedContext] = claude3Match;

if (major && rawFamily) {
const family = rawFamily.charAt(0).toUpperCase() + rawFamily.slice(1);
const name = `${family} ${major}`;
return extendedContext ? `${name} (1M context)` : name;
}
}

return modelId;
}
6 changes: 4 additions & 2 deletions src/widgets/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
WidgetEditorDisplay,
WidgetItem
} from '../types/Widget';
import { getFriendlyModelName } from '../utils/model-friendly-name';

export class ModelWidget implements Widget {
getDefaultColor(): string { return 'cyan'; }
Expand All @@ -20,9 +21,10 @@ export class ModelWidget implements Widget {
}

const model = context.data?.model;
const modelDisplayName = typeof model === 'string'
const rawName = typeof model === 'string'
? model
: (model?.display_name ?? model?.id);
: (model?.display_name ?? model?.id ?? '');
const modelDisplayName = getFriendlyModelName(rawName);

if (modelDisplayName) {
return item.rawValue ? modelDisplayName : `Model: ${modelDisplayName}`;
Expand Down