Skip to content

Commit 4b4e112

Browse files
authored
fix claude model option merging (#726)
1 parent 25662a7 commit 4b4e112

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

web/src/components/AssistantChat/modelOptions.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ describe('getModelOptionsForFlavor', () => {
1616
expect(options.some((o) => o.value === 'opus')).toBe(true)
1717
})
1818

19+
it('keeps Claude presets when explicit options only include Sonnet models', () => {
20+
const options = getModelOptionsForFlavor('claude', null, [
21+
{ value: null, label: 'Default' },
22+
{ value: 'sonnet', label: 'Sonnet' },
23+
{ value: 'sonnet[1m]', label: 'Sonnet 1M' }
24+
])
25+
expect(options).toEqual([
26+
{ value: null, label: 'Default' },
27+
{ value: 'sonnet', label: 'Sonnet' },
28+
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
29+
{ value: 'opus', label: 'Opus' },
30+
{ value: 'opus[1m]', label: 'Opus 1M' }
31+
])
32+
})
33+
34+
it('adds non-preset Claude options without hiding Opus presets', () => {
35+
const options = getModelOptionsForFlavor('claude', null, [
36+
{ value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1' }
37+
])
38+
expect(options).toEqual([
39+
{ value: null, label: 'Default' },
40+
{ value: 'claude-opus-4-1-20250805', label: 'Claude Opus 4.1' },
41+
{ value: 'sonnet', label: 'Sonnet' },
42+
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
43+
{ value: 'opus', label: 'Opus' },
44+
{ value: 'opus[1m]', label: 'Opus 1M' }
45+
])
46+
})
47+
1948
it('includes custom Gemini model from env/config in options', () => {
2049
const options = getModelOptionsForFlavor('gemini', 'gemini-custom-experiment')
2150
expect(options.some((o) => o.value === 'gemini-custom-experiment')).toBe(true)
@@ -94,6 +123,14 @@ describe('getNextModelForFlavor', () => {
94123
expect(next).not.toBeNull()
95124
})
96125

126+
it('cycles through Claude presets when explicit options only include Sonnet models', () => {
127+
const next = getNextModelForFlavor('claude', 'sonnet[1m]', [
128+
{ value: 'sonnet', label: 'Sonnet' },
129+
{ value: 'sonnet[1m]', label: 'Sonnet 1M' }
130+
])
131+
expect(next).toBe('opus')
132+
})
133+
97134
it('cycles explicit model options', () => {
98135
const next = getNextModelForFlavor('codex', 'gpt-5.5', [
99136
{ value: 'gpt-5.5', label: 'GPT-5.5' },

web/src/components/AssistantChat/modelOptions.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ function withCurrentModelOption(options: ModelOption[], currentModel?: string |
2828
return nextOptions
2929
}
3030

31+
function getClaudeModelOptions(currentModel?: string | null, customOptions?: ModelOption[]): ModelOption[] {
32+
if (!customOptions || customOptions.length === 0) {
33+
return getClaudeComposerModelOptions(currentModel)
34+
}
35+
36+
const options = getClaudeComposerModelOptions(currentModel)
37+
const nextOptions = [...options]
38+
let insertIndex = Math.max(1, nextOptions.findIndex((option) => option.value !== null))
39+
40+
for (const option of customOptions) {
41+
const normalizedValue = normalizeCurrentModel(option.value)
42+
if (!normalizedValue) {
43+
continue
44+
}
45+
46+
const existingIndex = nextOptions.findIndex((nextOption) => nextOption.value === normalizedValue)
47+
if (existingIndex >= 0) {
48+
if (nextOptions[existingIndex]?.label === normalizedValue) {
49+
nextOptions[existingIndex] = option
50+
}
51+
continue
52+
}
53+
54+
nextOptions.splice(insertIndex, 0, {
55+
value: normalizedValue,
56+
label: option.label
57+
})
58+
insertIndex += 1
59+
}
60+
61+
return nextOptions
62+
}
63+
3164
function getGeminiModelOptions(currentModel?: string | null): ModelOption[] {
3265
const options = MODEL_OPTIONS.gemini.map((m) => ({
3366
value: m.value === 'auto' ? null : m.value,
@@ -50,6 +83,9 @@ export function getModelOptionsForFlavor(
5083
currentModel?: string | null,
5184
customOptions?: ModelOption[]
5285
): ModelOption[] {
86+
if (flavor === 'claude') {
87+
return getClaudeModelOptions(currentModel, customOptions)
88+
}
5389
if (customOptions && customOptions.length > 0) {
5490
return withCurrentModelOption(customOptions, currentModel)
5591
}
@@ -69,14 +105,22 @@ export function getModelOptionsForFlavor(
69105
if (flavor === 'kimi') {
70106
return withCurrentModelOption([{ value: null, label: 'Default' }], currentModel)
71107
}
72-
return getClaudeComposerModelOptions(currentModel)
108+
return getClaudeModelOptions(currentModel)
73109
}
74110

75111
export function getNextModelForFlavor(
76112
flavor: string | undefined | null,
77113
currentModel?: string | null,
78114
customOptions?: ModelOption[]
79115
): string | null {
116+
if (flavor === 'claude') {
117+
const options = getClaudeModelOptions(currentModel, customOptions)
118+
const currentIndex = options.findIndex((option) => option.value === (normalizeCurrentModel(currentModel) ?? null))
119+
if (currentIndex === -1) {
120+
return options[0]?.value ?? null
121+
}
122+
return options[(currentIndex + 1) % options.length]?.value ?? null
123+
}
80124
if (customOptions && customOptions.length > 0) {
81125
const options = getModelOptionsForFlavor(flavor, currentModel, customOptions)
82126
const currentIndex = options.findIndex((option) => option.value === (normalizeCurrentModel(currentModel) ?? null))

0 commit comments

Comments
 (0)