Skip to content
Open
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
12 changes: 11 additions & 1 deletion cli/src/runner/buildCliArgs.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { buildCliArgs } from './run'
import { buildCliArgs, shouldSetClaudeHaikuModelEnv } from './run'

describe('buildCliArgs', () => {
it('adds --permission-mode for valid permission mode', () => {
Expand Down Expand Up @@ -61,4 +61,14 @@ describe('buildCliArgs', () => {
expect(args).toContain(mode)
}
})

it('detects custom Claude models for Haiku env override', () => {
expect(shouldSetClaudeHaikuModelEnv('claude', 'claude-3-5-haiku-latest')).toBe(true)
expect(shouldSetClaudeHaikuModelEnv('claude', 'opus')).toBe(false)
expect(shouldSetClaudeHaikuModelEnv('claude', 'opus[1m]')).toBe(false)
expect(shouldSetClaudeHaikuModelEnv('claude', 'sonnet')).toBe(false)
expect(shouldSetClaudeHaikuModelEnv('claude', 'sonnet[1m]')).toBe(false)
expect(shouldSetClaudeHaikuModelEnv('codex', 'claude-3-5-haiku-latest')).toBe(false)
expect(shouldSetClaudeHaikuModelEnv('claude')).toBe(false)
})
})
17 changes: 17 additions & 0 deletions cli/src/runner/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ import { buildMachineMetadata } from '@/agent/sessionFactory';
import { resolveWorkspaceRoot } from '@/utils/workspaceRoot';
import { hashRunnerCliApiToken } from './runnerIdentity';

const CLAUDE_BUILT_IN_MODEL_ALIASES = new Set(['opus', 'opus[1m]', 'sonnet', 'sonnet[1m]']);

export function shouldSetClaudeHaikuModelEnv(agent: string, model?: string): boolean {
if (agent !== 'claude') {
return false;
}
const normalizedModel = model?.trim();
return Boolean(normalizedModel && !CLAUDE_BUILT_IN_MODEL_ALIASES.has(normalizedModel));
}

export async function startRunner(options: { workspaceRoot?: string } = {}): Promise<void> {
// We don't have cleanup function at the time of server construction
// Control flow is:
Expand Down Expand Up @@ -378,6 +388,13 @@ export async function startRunner(options: { workspaceRoot?: string } = {}): Pro
};
}

if (shouldSetClaudeHaikuModelEnv(agent, options.model) && options.model) {
extraEnv = {
...extraEnv,
ANTHROPIC_DEFAULT_HAIKU_MODEL: options.model
};
}

const args = buildCliArgs(agent, options, yolo);

// sessionId reserved for future use
Expand Down
12 changes: 12 additions & 0 deletions web/src/components/NewSession/ModelSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import { useTranslation } from '@/lib/use-translation'
export function ModelSelector(props: {
agent: AgentType
model: string
customModel: string
options?: Array<{ value: string; label: string }>
isDisabled: boolean
isLoading?: boolean
error?: string | null
onModelChange: (value: string) => void
onCustomModelChange: (value: string) => void
}) {
const { t } = useTranslation()
const options = props.options ?? MODEL_OPTIONS[props.agent]
Expand Down Expand Up @@ -40,6 +42,16 @@ export function ModelSelector(props: {
{props.error}
</div>
) : null}
{props.agent === 'claude' && props.model === 'custom' ? (
<input
type="text"
value={props.customModel}
onChange={(e) => props.onCustomModelChange(e.target.value)}
disabled={props.isDisabled || props.isLoading}
placeholder={t('newSession.model.custom.placeholder')}
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
/>
) : null}
</div>
)
}
15 changes: 14 additions & 1 deletion web/src/components/NewSession/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function NewSession(props: {
const [isDirectoryFocused, setIsDirectoryFocused] = useState(false)
const [agent, setAgent] = useState<AgentType>(loadPreferredAgent)
const [model, setModel] = useState('auto')
const [customModel, setCustomModel] = useState('')
const [effort, setEffort] = useState<ClaudeEffort>('auto')
const [modelReasoningEffort, setModelReasoningEffort] = useState<CodexReasoningEffort>('default')
const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode)
Expand All @@ -68,6 +69,7 @@ export function NewSession(props: {

useEffect(() => {
setModel('auto')
setCustomModel('')
setEffort('auto')
}, [agent])

Expand Down Expand Up @@ -277,7 +279,16 @@ export function NewSession(props: {
return
}

const resolvedModel = model !== 'auto' && agent !== 'opencode' ? model : undefined
const trimmedCustomModel = customModel.trim()
if (agent === 'claude' && model === 'custom' && !trimmedCustomModel) {
setError(t('newSession.model.custom.required'))
return
}
const resolvedModel = agent === 'claude' && model === 'custom'
? trimmedCustomModel
: model !== 'auto' && agent !== 'opencode'
? model
: undefined
const resolvedEffort = agent === 'claude' && effort !== 'auto' ? effort : undefined
const resolvedModelReasoningEffort = agent === 'codex' && modelReasoningEffort !== 'default'
? modelReasoningEffort
Expand Down Expand Up @@ -358,13 +369,15 @@ export function NewSession(props: {
<ModelSelector
agent={agent}
model={model}
customModel={customModel}
options={agent === 'codex' ? codexModelOptions : undefined}
isDisabled={isFormDisabled || (agent === 'codex' && Boolean(codexModelsState.error))}
isLoading={agent === 'codex' && codexModelsState.isLoading}
error={agent === 'codex' && codexModelsState.error
? `${t('newSession.model.loadFailed')}: ${codexModelsState.error}`
: null}
onModelChange={setModel}
onCustomModelChange={setCustomModel}
/>
<ClaudeEffortSelector
agent={agent}
Expand Down
1 change: 1 addition & 0 deletions web/src/components/NewSession/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('Claude model options', () => {
{ value: 'opus[1m]', label: 'Opus 1M' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
{ value: 'custom', label: 'Custom' },
])
})

Expand Down
1 change: 1 addition & 0 deletions web/src/components/NewSession/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
{ value: 'opus[1m]', label: 'Opus 1M' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
{ value: 'custom', label: 'Custom' },
],
codex: [
{ value: 'auto', label: 'Default' },
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export default {
'newSession.model': 'Model',
'newSession.effort': 'Effort',
'newSession.model.optional': 'optional',
'newSession.model.custom.placeholder': 'Enter Claude model name',
'newSession.model.custom.required': 'Custom model is required',
'newSession.model.loadFailed': 'Failed to load Codex models',
'newSession.reasoningEffort': 'Reasoning effort',
'newSession.yolo': 'YOLO mode',
Expand Down
2 changes: 2 additions & 0 deletions web/src/lib/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export default {
'newSession.model': '模型',
'newSession.effort': '思考强度',
'newSession.model.optional': '可选',
'newSession.model.custom.placeholder': '输入自定义模型名称',
'newSession.model.custom.required': '请输入自定义模型名称',
'newSession.model.loadFailed': '加载 Codex 模型失败',
'newSession.reasoningEffort': '推理强度',
'newSession.yolo': 'YOLO 模式',
Expand Down
Loading