diff --git a/src/lib/components/claude-settings/ModelConfigEditor.svelte b/src/lib/components/claude-settings/ModelConfigEditor.svelte index e6d5edf..357317c 100644 --- a/src/lib/components/claude-settings/ModelConfigEditor.svelte +++ b/src/lib/components/claude-settings/ModelConfigEditor.svelte @@ -15,7 +15,22 @@ let { settings, onsave }: Props = $props(); - let model = $state(settings.model ?? ''); + // Parse a saved model value into (base, has1m) so the UI can split the [1m] suffix + // from the underlying ID. The suffix is documented as a Claude Code extension that + // is stripped before the request reaches the provider. + function splitModelValue(raw: string | undefined): { base: string; has1m: boolean } { + if (!raw) return { base: '', has1m: false }; + if (raw.endsWith('[1m]')) return { base: raw.slice(0, -'[1m]'.length), has1m: true }; + return { base: raw, has1m: false }; + } + + const initial = splitModelValue(settings.model); + const knownValues = CLAUDE_MODELS.map((m) => m.value) as readonly string[]; + const initialIsKnown = !initial.base || knownValues.includes(initial.base); + + let modelChoice = $state(initialIsKnown ? initial.base : '__custom__'); + let customModel = $state(initialIsKnown ? '' : initial.base); + let use1mContext = $state(initial.has1m); let availableModels = $state([...settings.availableModels]); let outputStyle = $state(settings.outputStyle ?? ''); let language = $state(settings.language ?? ''); @@ -23,17 +38,47 @@ // Reset local state when settings prop changes $effect(() => { - model = settings.model ?? ''; + const next = splitModelValue(settings.model); + const known = !next.base || knownValues.includes(next.base); + modelChoice = known ? next.base : '__custom__'; + customModel = known ? '' : next.base; + use1mContext = next.has1m; availableModels = [...settings.availableModels]; outputStyle = settings.outputStyle ?? ''; language = settings.language ?? ''; alwaysThinkingEnabled = settings.alwaysThinkingEnabled; }); + // Resolve the effective model string from the dropdown + custom field + 1m toggle. + // Returns undefined when nothing is selected so the setting is omitted entirely. + function resolveModelValue(): string | undefined { + const base = modelChoice === '__custom__' ? customModel.trim() : modelChoice; + if (!base) return undefined; + if (!use1mContext) return base; + // Don't double-append [1m] if the user typed it themselves + return base.endsWith('[1m]') ? base : `${base}[1m]`; + } + + // 1M context only applies to models that support it. Built-in entries declare this + // via supports1m; custom IDs are assumed eligible (we can't verify) and the user + // keeps responsibility for typing a valid ID. + function selectionSupports1m(): boolean { + if (modelChoice === '__custom__') return customModel.trim().length > 0; + const entry = CLAUDE_MODELS.find((m) => m.value === modelChoice); + return entry?.supports1m ?? false; + } + + // Auto-clear the 1M flag if the user picks a model that doesn't support it + $effect(() => { + if (!selectionSupports1m() && use1mContext) { + use1mContext = false; + } + }); + function handleSave() { onsave({ ...settings, - model: model || undefined, + model: resolveModelValue(), availableModels, outputStyle: outputStyle || undefined, language: language || undefined, @@ -74,14 +119,61 @@ + {#if modelChoice === '__custom__'} + +

+ Enter any model ID, alias, or provider-specific identifier. Append [1m] + yourself if not using the toggle below. +

+ {/if} +

+ Aliases like opus, sonnet, haiku auto-resolve + to the latest version. See + model configuration docs. +

+ + + +
+ +

+ Appends the [1m] suffix to the model ID. Supported on Opus and Sonnet + (not Haiku). Standard pricing — no premium beyond 200K tokens. + Docs. +

diff --git a/src/lib/types/claudeSettings.ts b/src/lib/types/claudeSettings.ts index 599475a..ffa7cc5 100644 --- a/src/lib/types/claudeSettings.ts +++ b/src/lib/types/claudeSettings.ts @@ -118,21 +118,39 @@ export interface AllClaudeSettings { local?: ClaudeSettings; } +// Model aliases auto-resolve to the latest version Claude Code supports. +// See https://code.claude.com/docs/en/model-config#available-models +// The [1m] suffix is documented at https://code.claude.com/docs/en/model-config#extended-context export const CLAUDE_MODELS = [ { - value: 'claude-sonnet-4-5-20250929', - label: 'Claude Sonnet 4.5', - description: 'Best balance of speed and intelligence' + value: 'opus', + label: 'Opus (latest)', + description: 'Most capable model for complex reasoning', + supports1m: true }, { - value: 'claude-opus-4-6', - label: 'Claude Opus 4.6', - description: 'Most capable model for complex tasks' + value: 'sonnet', + label: 'Sonnet (latest)', + description: 'Balanced speed and intelligence for daily coding', + supports1m: true }, { - value: 'claude-haiku-4-5-20251001', - label: 'Claude Haiku 4.5', - description: 'Fastest model for simple tasks' + value: 'haiku', + label: 'Haiku (latest)', + description: 'Fastest model for simple tasks', + supports1m: false + }, + { + value: 'opusplan', + label: 'Opus + Plan (opusplan)', + description: 'Opus during plan mode, Sonnet for execution', + supports1m: true + }, + { + value: 'best', + label: 'Best available', + description: 'Most capable model available (currently equivalent to Opus)', + supports1m: true } ] as const; diff --git a/src/tests/components/claude-settings.test.ts b/src/tests/components/claude-settings.test.ts index a2e25cd..703f252 100644 --- a/src/tests/components/claude-settings.test.ts +++ b/src/tests/components/claude-settings.test.ts @@ -125,12 +125,21 @@ describe('AttributionEditor Component', () => { }); }); -describe('Extended Context Types', () => { - it('should include extended context models in CLAUDE_MODELS', async () => { +describe('Model aliases (CLAUDE_MODELS)', () => { + it('should expose the core Anthropic aliases', async () => { const { CLAUDE_MODELS } = await import('$lib/types'); const values = CLAUDE_MODELS.map(m => m.value); - expect(values).toContain('claude-sonnet-4-5-20250929'); - expect(values).toContain('claude-opus-4-6'); + expect(values).toContain('opus'); + expect(values).toContain('sonnet'); + expect(values).toContain('haiku'); + }); + + it('should mark Opus and Sonnet as 1M-capable, Haiku as not', async () => { + const { CLAUDE_MODELS } = await import('$lib/types'); + const byValue = Object.fromEntries(CLAUDE_MODELS.map(m => [m.value, m])); + expect(byValue.opus.supports1m).toBe(true); + expect(byValue.sonnet.supports1m).toBe(true); + expect(byValue.haiku.supports1m).toBe(false); }); it('should include extended context shortcuts in AVAILABLE_MODEL_SHORTCUTS', async () => {