Skip to content

Commit f53bdda

Browse files
feat(settings): use model aliases + 1M context toggle + custom ID (#213)
The Default Model dropdown shipped hardcoded full IDs (claude-sonnet-4-5-*, claude-opus-4-6, claude-haiku-4-5-*) that go stale every Anthropic release. Switch to documented model aliases (opus, sonnet, haiku, opusplan, best) which auto-resolve to the latest version on the provider side, plus add the missing 1M-context toggle and a custom-ID escape hatch. Changes: - CLAUDE_MODELS now lists aliases with a supports1m flag per entry - ModelConfigEditor: add "Other (custom)" option with free-text input - ModelConfigEditor: add "Use 1M token context window" checkbox that appends/strips the documented [1m] suffix; auto-disables for Haiku - Tests updated to verify the alias contract and 1M-capability flags - Inline doc links to code.claude.com/docs/en/model-config Refs: #212 Co-authored-by: Scot Campbell <prefrontalsys@users.noreply.github.com>
1 parent 74fe948 commit f53bdda

3 files changed

Lines changed: 136 additions & 17 deletions

File tree

src/lib/components/claude-settings/ModelConfigEditor.svelte

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,70 @@
1515
1616
let { settings, onsave }: Props = $props();
1717
18-
let model = $state(settings.model ?? '');
18+
// Parse a saved model value into (base, has1m) so the UI can split the [1m] suffix
19+
// from the underlying ID. The suffix is documented as a Claude Code extension that
20+
// is stripped before the request reaches the provider.
21+
function splitModelValue(raw: string | undefined): { base: string; has1m: boolean } {
22+
if (!raw) return { base: '', has1m: false };
23+
if (raw.endsWith('[1m]')) return { base: raw.slice(0, -'[1m]'.length), has1m: true };
24+
return { base: raw, has1m: false };
25+
}
26+
27+
const initial = splitModelValue(settings.model);
28+
const knownValues = CLAUDE_MODELS.map((m) => m.value) as readonly string[];
29+
const initialIsKnown = !initial.base || knownValues.includes(initial.base);
30+
31+
let modelChoice = $state<string>(initialIsKnown ? initial.base : '__custom__');
32+
let customModel = $state<string>(initialIsKnown ? '' : initial.base);
33+
let use1mContext = $state<boolean>(initial.has1m);
1934
let availableModels = $state<string[]>([...settings.availableModels]);
2035
let outputStyle = $state(settings.outputStyle ?? '');
2136
let language = $state(settings.language ?? '');
2237
let alwaysThinkingEnabled = $state<boolean | undefined>(settings.alwaysThinkingEnabled);
2338
2439
// Reset local state when settings prop changes
2540
$effect(() => {
26-
model = settings.model ?? '';
41+
const next = splitModelValue(settings.model);
42+
const known = !next.base || knownValues.includes(next.base);
43+
modelChoice = known ? next.base : '__custom__';
44+
customModel = known ? '' : next.base;
45+
use1mContext = next.has1m;
2746
availableModels = [...settings.availableModels];
2847
outputStyle = settings.outputStyle ?? '';
2948
language = settings.language ?? '';
3049
alwaysThinkingEnabled = settings.alwaysThinkingEnabled;
3150
});
3251
52+
// Resolve the effective model string from the dropdown + custom field + 1m toggle.
53+
// Returns undefined when nothing is selected so the setting is omitted entirely.
54+
function resolveModelValue(): string | undefined {
55+
const base = modelChoice === '__custom__' ? customModel.trim() : modelChoice;
56+
if (!base) return undefined;
57+
if (!use1mContext) return base;
58+
// Don't double-append [1m] if the user typed it themselves
59+
return base.endsWith('[1m]') ? base : `${base}[1m]`;
60+
}
61+
62+
// 1M context only applies to models that support it. Built-in entries declare this
63+
// via supports1m; custom IDs are assumed eligible (we can't verify) and the user
64+
// keeps responsibility for typing a valid ID.
65+
function selectionSupports1m(): boolean {
66+
if (modelChoice === '__custom__') return customModel.trim().length > 0;
67+
const entry = CLAUDE_MODELS.find((m) => m.value === modelChoice);
68+
return entry?.supports1m ?? false;
69+
}
70+
71+
// Auto-clear the 1M flag if the user picks a model that doesn't support it
72+
$effect(() => {
73+
if (!selectionSupports1m() && use1mContext) {
74+
use1mContext = false;
75+
}
76+
});
77+
3378
function handleSave() {
3479
onsave({
3580
...settings,
36-
model: model || undefined,
81+
model: resolveModelValue(),
3782
availableModels,
3883
outputStyle: outputStyle || undefined,
3984
language: language || undefined,
@@ -74,14 +119,61 @@
74119
</label>
75120
<select
76121
id="model-select"
77-
bind:value={model}
122+
bind:value={modelChoice}
78123
class="input text-sm w-full"
79124
>
80125
<option value="">Not set (use default)</option>
81126
{#each CLAUDE_MODELS as m}
82127
<option value={m.value}>{m.label} — {m.description}</option>
83128
{/each}
129+
<option value="__custom__">Other (custom model ID)…</option>
84130
</select>
131+
{#if modelChoice === '__custom__'}
132+
<input
133+
type="text"
134+
bind:value={customModel}
135+
placeholder="e.g. claude-opus-4-7 or arn:aws:bedrock:…"
136+
class="input text-sm w-full mt-2"
137+
aria-label="Custom model ID"
138+
/>
139+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
140+
Enter any model ID, alias, or provider-specific identifier. Append <code>[1m]</code>
141+
yourself if not using the toggle below.
142+
</p>
143+
{/if}
144+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
145+
Aliases like <code>opus</code>, <code>sonnet</code>, <code>haiku</code> auto-resolve
146+
to the latest version. See
147+
<a
148+
href="https://code.claude.com/docs/en/model-config#available-models"
149+
target="_blank"
150+
rel="noopener"
151+
class="underline hover:text-primary-600">model configuration docs</a>.
152+
</p>
153+
</div>
154+
155+
<!-- 1M Context Window -->
156+
<div>
157+
<label class="flex items-center gap-2 cursor-pointer">
158+
<input
159+
type="checkbox"
160+
bind:checked={use1mContext}
161+
disabled={!selectionSupports1m()}
162+
class="rounded border-gray-300 dark:border-gray-600"
163+
/>
164+
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
165+
Use 1M token context window
166+
</span>
167+
</label>
168+
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-6">
169+
Appends the <code>[1m]</code> suffix to the model ID. Supported on Opus and Sonnet
170+
(not Haiku). Standard pricing — no premium beyond 200K tokens.
171+
<a
172+
href="https://code.claude.com/docs/en/model-config#extended-context"
173+
target="_blank"
174+
rel="noopener"
175+
class="underline hover:text-primary-600">Docs</a>.
176+
</p>
85177
</div>
86178

87179
<!-- Available Models -->

src/lib/types/claudeSettings.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,21 +118,39 @@ export interface AllClaudeSettings {
118118
local?: ClaudeSettings;
119119
}
120120

121+
// Model aliases auto-resolve to the latest version Claude Code supports.
122+
// See https://code.claude.com/docs/en/model-config#available-models
123+
// The [1m] suffix is documented at https://code.claude.com/docs/en/model-config#extended-context
121124
export const CLAUDE_MODELS = [
122125
{
123-
value: 'claude-sonnet-4-5-20250929',
124-
label: 'Claude Sonnet 4.5',
125-
description: 'Best balance of speed and intelligence'
126+
value: 'opus',
127+
label: 'Opus (latest)',
128+
description: 'Most capable model for complex reasoning',
129+
supports1m: true
126130
},
127131
{
128-
value: 'claude-opus-4-6',
129-
label: 'Claude Opus 4.6',
130-
description: 'Most capable model for complex tasks'
132+
value: 'sonnet',
133+
label: 'Sonnet (latest)',
134+
description: 'Balanced speed and intelligence for daily coding',
135+
supports1m: true
131136
},
132137
{
133-
value: 'claude-haiku-4-5-20251001',
134-
label: 'Claude Haiku 4.5',
135-
description: 'Fastest model for simple tasks'
138+
value: 'haiku',
139+
label: 'Haiku (latest)',
140+
description: 'Fastest model for simple tasks',
141+
supports1m: false
142+
},
143+
{
144+
value: 'opusplan',
145+
label: 'Opus + Plan (opusplan)',
146+
description: 'Opus during plan mode, Sonnet for execution',
147+
supports1m: true
148+
},
149+
{
150+
value: 'best',
151+
label: 'Best available',
152+
description: 'Most capable model available (currently equivalent to Opus)',
153+
supports1m: true
136154
}
137155
] as const;
138156

src/tests/components/claude-settings.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,21 @@ describe('AttributionEditor Component', () => {
125125
});
126126
});
127127

128-
describe('Extended Context Types', () => {
129-
it('should include extended context models in CLAUDE_MODELS', async () => {
128+
describe('Model aliases (CLAUDE_MODELS)', () => {
129+
it('should expose the core Anthropic aliases', async () => {
130130
const { CLAUDE_MODELS } = await import('$lib/types');
131131
const values = CLAUDE_MODELS.map(m => m.value);
132-
expect(values).toContain('claude-sonnet-4-5-20250929');
133-
expect(values).toContain('claude-opus-4-6');
132+
expect(values).toContain('opus');
133+
expect(values).toContain('sonnet');
134+
expect(values).toContain('haiku');
135+
});
136+
137+
it('should mark Opus and Sonnet as 1M-capable, Haiku as not', async () => {
138+
const { CLAUDE_MODELS } = await import('$lib/types');
139+
const byValue = Object.fromEntries(CLAUDE_MODELS.map(m => [m.value, m]));
140+
expect(byValue.opus.supports1m).toBe(true);
141+
expect(byValue.sonnet.supports1m).toBe(true);
142+
expect(byValue.haiku.supports1m).toBe(false);
134143
});
135144

136145
it('should include extended context shortcuts in AVAILABLE_MODEL_SHORTCUTS', async () => {

0 commit comments

Comments
 (0)