feat: add display.modelOverride for custom model display names

Users can now set a fully custom model name in their config:

  { "display": { "modelOverride": "zane's intelligent opus 4.6" } }

When set, the override completely replaces the auto-detected model
name (while preserving the provider qualifier like "| Bedrock").

Follows the same pattern as customLine: string type, max 80 chars,
empty string means disabled (falls through to modelFormat).
This commit is contained in:
Zane Wang
2026-04-03 06:42:25 +08:00
parent 5676041f19
commit ab4c564892
6 changed files with 45 additions and 4 deletions

View File

@@ -90,6 +90,7 @@ export interface HudConfig {
sevenDayThreshold: number;
environmentThreshold: number;
modelFormat: ModelFormatMode;
modelOverride: string;
customLine: string;
};
colors: HudColorOverrides;
@@ -128,6 +129,7 @@ export const DEFAULT_CONFIG: HudConfig = {
sevenDayThreshold: 80,
environmentThreshold: 0,
modelFormat: 'full',
modelOverride: '',
customLine: '',
},
colors: {
@@ -340,6 +342,9 @@ export function mergeConfig(userConfig: Partial<HudConfig>): HudConfig {
modelFormat: validateModelFormat(migrated.display?.modelFormat)
? migrated.display.modelFormat
: DEFAULT_CONFIG.display.modelFormat,
modelOverride: typeof migrated.display?.modelOverride === 'string'
? migrated.display.modelOverride.slice(0, 80)
: DEFAULT_CONFIG.display.modelOverride,
customLine: typeof migrated.display?.customLine === 'string'
? migrated.display.customLine.slice(0, 80)
: DEFAULT_CONFIG.display.customLine,

View File

@@ -9,7 +9,7 @@ export function renderProjectLine(ctx: RenderContext): string | null {
const parts: string[] = [];
if (display?.showModel !== false) {
const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat);
const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat, ctx.config?.display?.modelOverride);
const providerLabel = getProviderLabel(ctx.stdin);
const showUsage = display?.showUsage !== false;
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;

View File

@@ -12,7 +12,7 @@ const DEBUG = process.env.DEBUG?.includes('claude-hud') || process.env.DEBUG ===
* Used for compact layout mode.
*/
export function renderSessionLine(ctx: RenderContext): string {
const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat);
const model = formatModelName(getModelName(ctx.stdin), ctx.config?.display?.modelFormat, ctx.config?.display?.modelOverride);
const rawPercent = getContextPercent(ctx.stdin);
const bufferedPercent = getBufferedPercent(ctx.stdin);

View File

@@ -167,13 +167,20 @@ export function stripContextSuffix(name: string): string {
}
/**
* Formats a model name according to the user's chosen display format.
* Formats a model name according to the user's chosen display settings.
*
* When `override` is set, it replaces the model name entirely.
* Otherwise, `format` controls how the raw name is abbreviated:
*
* full: Return raw name unchanged (e.g. "Opus 4.6 (1M context)")
* compact: Strip context-window suffix (e.g. "Opus 4.6")
* short: Strip context suffix AND leading "Claude " prefix (e.g. "Opus 4.6")
*/
export function formatModelName(name: string, format?: ModelFormatMode): string {
export function formatModelName(name: string, format?: ModelFormatMode, override?: string): string {
if (override) {
return override;
}
if (!format || format === 'full') {
return name;
}

View File

@@ -135,6 +135,24 @@ test('mergeConfig falls back to full for invalid modelFormat', () => {
assert.equal(mergeConfig({ display: { modelFormat: null } }).display.modelFormat, 'full');
});
test('mergeConfig defaults modelOverride to empty string', () => {
const config = mergeConfig({});
assert.equal(config.display.modelOverride, '');
});
test('mergeConfig preserves modelOverride and truncates long values', () => {
const override = 'x'.repeat(120);
const config = mergeConfig({ display: { modelOverride: override } });
assert.equal(config.display.modelOverride.length, 80);
assert.equal(config.display.modelOverride, override.slice(0, 80));
});
test('mergeConfig falls back to empty for non-string modelOverride', () => {
assert.equal(mergeConfig({ display: { modelOverride: 123 } }).display.modelOverride, '');
assert.equal(mergeConfig({ display: { modelOverride: null } }).display.modelOverride, '');
assert.equal(mergeConfig({ display: { modelOverride: true } }).display.modelOverride, '');
});
test('getConfigPath respects CLAUDE_CONFIG_DIR', async () => {
const originalConfigDir = process.env.CLAUDE_CONFIG_DIR;
const customConfigDir = await mkdtemp(path.join(tmpdir(), 'claude-hud-config-dir-'));

View File

@@ -319,6 +319,17 @@ test('formatModelName short mode strips context suffix and Claude prefix', () =>
assert.equal(formatModelName('claude Opus 4.5', 'short'), 'Opus 4.5');
});
test('formatModelName override replaces model name entirely', () => {
// Override takes precedence over format
assert.equal(formatModelName('Claude Opus 4.5', 'full', "zane's intelligent opus"), "zane's intelligent opus");
assert.equal(formatModelName('Claude Opus 4.5', 'compact', 'My Model'), 'My Model');
assert.equal(formatModelName('Claude Opus 4.5', 'short', 'Custom'), 'Custom');
assert.equal(formatModelName('Claude Opus 4.5', undefined, 'Override'), 'Override');
// Empty override is treated as unset (falls through to format)
assert.equal(formatModelName('Claude Opus 4.5 (1M context)', 'compact', ''), 'Claude Opus 4.5');
assert.equal(formatModelName('Opus 4.6', 'full', ''), 'Opus 4.6');
});
test('bedrock model detection recognizes bedrock ids', () => {
assert.ok(isBedrockModelId('anthropic.claude-3-5-sonnet-20240620-v1:0'));
assert.ok(isBedrockModelId('eu.anthropic.claude-opus-4-5-20251101-v1:0'));