diff --git a/CLAUDE.md b/CLAUDE.md index 128cd8d7..84dd1fbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,7 +161,7 @@ Use `resolveModelString()` from `@automaker/model-resolver` to convert model ali - `haiku` → `claude-haiku-4-5` - `sonnet` → `claude-sonnet-4-20250514` -- `opus` → `claude-opus-4-5-20251101` +- `opus` → `claude-opus-4-6` ## Environment Variables diff --git a/apps/server/package.json b/apps/server/package.json index 0b4deeac..ed005c54 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,7 +24,7 @@ "test:unit": "vitest run tests/unit" }, "dependencies": { - "@anthropic-ai/claude-agent-sdk": "0.1.76", + "@anthropic-ai/claude-agent-sdk": "0.2.32", "@automaker/dependency-resolver": "1.0.0", "@automaker/git-utils": "1.0.0", "@automaker/model-resolver": "1.0.0", diff --git a/apps/server/src/lib/sdk-options.ts b/apps/server/src/lib/sdk-options.ts index cc1df2f5..674350a5 100644 --- a/apps/server/src/lib/sdk-options.ts +++ b/apps/server/src/lib/sdk-options.ts @@ -253,11 +253,27 @@ function buildMcpOptions(config: CreateSdkOptionsConfig): McpOptions { /** * Build thinking options for SDK configuration. * Converts ThinkingLevel to maxThinkingTokens for the Claude SDK. + * For adaptive thinking (Opus 4.6), omits maxThinkingTokens to let the model + * decide its own reasoning depth. * * @param thinkingLevel - The thinking level to convert - * @returns Object with maxThinkingTokens if thinking is enabled + * @returns Object with maxThinkingTokens if thinking is enabled with a budget */ function buildThinkingOptions(thinkingLevel?: ThinkingLevel): Partial { + if (!thinkingLevel || thinkingLevel === 'none') { + return {}; + } + + // Adaptive thinking (Opus 4.6): don't set maxThinkingTokens + // The model will use adaptive thinking by default + if (thinkingLevel === 'adaptive') { + logger.debug( + `buildThinkingOptions: thinkingLevel="adaptive" -> no maxThinkingTokens (model decides)` + ); + return {}; + } + + // Manual budget-based thinking for Haiku/Sonnet const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); logger.debug( `buildThinkingOptions: thinkingLevel="${thinkingLevel}" -> maxThinkingTokens=${maxThinkingTokens}` diff --git a/apps/server/src/providers/claude-provider.ts b/apps/server/src/providers/claude-provider.ts index cfb59093..78a0a0c7 100644 --- a/apps/server/src/providers/claude-provider.ts +++ b/apps/server/src/providers/claude-provider.ts @@ -219,8 +219,11 @@ export class ClaudeProvider extends BaseProvider { // claudeCompatibleProvider takes precedence over claudeApiProfile const providerConfig = claudeCompatibleProvider || claudeApiProfile; - // Convert thinking level to token budget - const maxThinkingTokens = getThinkingTokenBudget(thinkingLevel); + // Build thinking configuration + // Adaptive thinking (Opus 4.6): don't set maxThinkingTokens, model uses adaptive by default + // Manual thinking (Haiku/Sonnet): use budget_tokens + const maxThinkingTokens = + thinkingLevel === 'adaptive' ? undefined : getThinkingTokenBudget(thinkingLevel); // Build Claude SDK options const sdkOptions: Options = { @@ -349,13 +352,13 @@ export class ClaudeProvider extends BaseProvider { getAvailableModels(): ModelDefinition[] { const models = [ { - id: 'claude-opus-4-5-20251101', - name: 'Claude Opus 4.5', - modelString: 'claude-opus-4-5-20251101', + id: 'claude-opus-4-6', + name: 'Claude Opus 4.6', + modelString: 'claude-opus-4-6', provider: 'anthropic', - description: 'Most capable Claude model', + description: 'Most capable Claude model with adaptive thinking', contextWindow: 200000, - maxOutputTokens: 16000, + maxOutputTokens: 128000, supportsVision: true, supportsTools: true, tier: 'premium' as const, diff --git a/apps/server/src/providers/provider-factory.ts b/apps/server/src/providers/provider-factory.ts index 1e91760f..a6dff69e 100644 --- a/apps/server/src/providers/provider-factory.ts +++ b/apps/server/src/providers/provider-factory.ts @@ -103,7 +103,7 @@ export class ProviderFactory { /** * Get the appropriate provider for a given model ID * - * @param modelId Model identifier (e.g., "claude-opus-4-5-20251101", "cursor-gpt-4o", "cursor-auto") + * @param modelId Model identifier (e.g., "claude-opus-4-6", "cursor-gpt-4o", "cursor-auto") * @param options Optional settings * @param options.throwOnDisconnected Throw error if provider is disconnected (default: true) * @returns Provider instance for the model diff --git a/apps/server/tests/unit/lib/model-resolver.test.ts b/apps/server/tests/unit/lib/model-resolver.test.ts index c1bff78d..65e3115d 100644 --- a/apps/server/tests/unit/lib/model-resolver.test.ts +++ b/apps/server/tests/unit/lib/model-resolver.test.ts @@ -35,7 +35,7 @@ describe('model-resolver.ts', () => { it("should resolve 'opus' alias to full model string", () => { const result = resolveModelString('opus'); - expect(result).toBe('claude-opus-4-5-20251101'); + expect(result).toBe('claude-opus-4-6'); expect(consoleSpy.log).toHaveBeenCalledWith( expect.stringContaining('Migrated legacy ID: "opus" -> "claude-opus"') ); @@ -117,7 +117,7 @@ describe('model-resolver.ts', () => { describe('getEffectiveModel', () => { it('should prioritize explicit model over session and default', () => { const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2'); - expect(result).toBe('claude-opus-4-5-20251101'); + expect(result).toBe('claude-opus-4-6'); }); it('should use session model when explicit is not provided', () => { diff --git a/apps/server/tests/unit/lib/sdk-options.test.ts b/apps/server/tests/unit/lib/sdk-options.test.ts index 029cd8fa..69d69794 100644 --- a/apps/server/tests/unit/lib/sdk-options.test.ts +++ b/apps/server/tests/unit/lib/sdk-options.test.ts @@ -491,5 +491,29 @@ describe('sdk-options.ts', () => { expect(options.maxThinkingTokens).toBeUndefined(); }); }); + + describe('adaptive thinking for Opus 4.6', () => { + it('should not set maxThinkingTokens for adaptive thinking (model decides)', async () => { + const { createAutoModeOptions } = await import('@/lib/sdk-options.js'); + + const options = createAutoModeOptions({ + cwd: '/test/path', + thinkingLevel: 'adaptive', + }); + + expect(options.maxThinkingTokens).toBeUndefined(); + }); + + it('should not include maxThinkingTokens when thinkingLevel is "none"', async () => { + const { createAutoModeOptions } = await import('@/lib/sdk-options.js'); + + const options = createAutoModeOptions({ + cwd: '/test/path', + thinkingLevel: 'none', + }); + + expect(options.maxThinkingTokens).toBeUndefined(); + }); + }); }); }); diff --git a/apps/server/tests/unit/providers/claude-provider.test.ts b/apps/server/tests/unit/providers/claude-provider.test.ts index c3f83f8f..7df211ef 100644 --- a/apps/server/tests/unit/providers/claude-provider.test.ts +++ b/apps/server/tests/unit/providers/claude-provider.test.ts @@ -39,7 +39,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Hello', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -59,7 +59,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test prompt', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test/dir', systemPrompt: 'You are helpful', maxTurns: 10, @@ -71,7 +71,7 @@ describe('claude-provider.ts', () => { expect(sdk.query).toHaveBeenCalledWith({ prompt: 'Test prompt', options: expect.objectContaining({ - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', systemPrompt: 'You are helpful', maxTurns: 10, cwd: '/test/dir', @@ -91,7 +91,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -116,7 +116,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', abortController, }); @@ -145,7 +145,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Current message', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', conversationHistory, sdkSessionId: 'test-session-id', @@ -176,7 +176,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: arrayPrompt as any, - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -196,7 +196,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -222,7 +222,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -286,7 +286,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -313,7 +313,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -341,7 +341,7 @@ describe('claude-provider.ts', () => { const generator = provider.executeQuery({ prompt: 'Test', - model: 'claude-opus-4-5-20251101', + model: 'claude-opus-4-6', cwd: '/test', }); @@ -366,12 +366,12 @@ describe('claude-provider.ts', () => { expect(models).toHaveLength(4); }); - it('should include Claude Opus 4.5', () => { + it('should include Claude Opus 4.6', () => { const models = provider.getAvailableModels(); - const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); + const opus = models.find((m) => m.id === 'claude-opus-4-6'); expect(opus).toBeDefined(); - expect(opus?.name).toBe('Claude Opus 4.5'); + expect(opus?.name).toBe('Claude Opus 4.6'); expect(opus?.provider).toBe('anthropic'); }); @@ -400,7 +400,7 @@ describe('claude-provider.ts', () => { it('should mark Opus as default', () => { const models = provider.getAvailableModels(); - const opus = models.find((m) => m.id === 'claude-opus-4-5-20251101'); + const opus = models.find((m) => m.id === 'claude-opus-4-6'); expect(opus?.default).toBe(true); }); diff --git a/apps/server/tests/unit/providers/provider-factory.test.ts b/apps/server/tests/unit/providers/provider-factory.test.ts index fbf01e90..b9aef928 100644 --- a/apps/server/tests/unit/providers/provider-factory.test.ts +++ b/apps/server/tests/unit/providers/provider-factory.test.ts @@ -54,8 +54,8 @@ describe('provider-factory.ts', () => { describe('getProviderForModel', () => { describe('Claude models (claude-* prefix)', () => { - it('should return ClaudeProvider for claude-opus-4-5-20251101', () => { - const provider = ProviderFactory.getProviderForModel('claude-opus-4-5-20251101'); + it('should return ClaudeProvider for claude-opus-4-6', () => { + const provider = ProviderFactory.getProviderForModel('claude-opus-4-6'); expect(provider).toBeInstanceOf(ClaudeProvider); }); @@ -70,7 +70,7 @@ describe('provider-factory.ts', () => { }); it('should be case-insensitive for claude models', () => { - const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-5-20251101'); + const provider = ProviderFactory.getProviderForModel('CLAUDE-OPUS-4-6'); expect(provider).toBeInstanceOf(ClaudeProvider); }); }); diff --git a/apps/ui/docs/AGENT_ARCHITECTURE.md b/apps/ui/docs/AGENT_ARCHITECTURE.md index 4c9f0d11..f5c374c4 100644 --- a/apps/ui/docs/AGENT_ARCHITECTURE.md +++ b/apps/ui/docs/AGENT_ARCHITECTURE.md @@ -199,7 +199,7 @@ The agent is configured with: ```javascript { - model: "claude-opus-4-5-20251101", + model: "claude-opus-4-6", maxTurns: 20, cwd: workingDirectory, allowedTools: [ diff --git a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx index b8dd8776..2dbf0808 100644 --- a/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx +++ b/apps/ui/src/components/views/board-view/dialogs/add-feature-dialog.tsx @@ -264,7 +264,21 @@ export function AddFeatureDialog({ }, [planningMode]); const handleModelChange = (entry: PhaseModelEntry) => { - setModelEntry(entry); + // Normalize thinking level when switching between adaptive and non-adaptive models + const isNewModelAdaptive = + entry.model === 'claude-opus' || + (typeof entry.model === 'string' && entry.model.includes('opus-4-6')); + const currentLevel = entry.thinkingLevel || 'none'; + + if (isNewModelAdaptive && currentLevel !== 'none' && currentLevel !== 'adaptive') { + // Switching TO Opus 4.6 with a manual level -> auto-switch to 'adaptive' + setModelEntry({ ...entry, thinkingLevel: 'adaptive' }); + } else if (!isNewModelAdaptive && currentLevel === 'adaptive') { + // Switching FROM Opus 4.6 with adaptive -> auto-switch to 'high' + setModelEntry({ ...entry, thinkingLevel: 'high' }); + } else { + setModelEntry(entry); + } }; const buildFeatureData = (): FeatureData | null => { diff --git a/apps/ui/src/components/views/board-view/shared/model-constants.ts b/apps/ui/src/components/views/board-view/shared/model-constants.ts index c56ad46a..2816e556 100644 --- a/apps/ui/src/components/views/board-view/shared/model-constants.ts +++ b/apps/ui/src/components/views/board-view/shared/model-constants.ts @@ -167,7 +167,14 @@ export const ALL_MODELS: ModelOption[] = [ ...COPILOT_MODELS, ]; -export const THINKING_LEVELS: ThinkingLevel[] = ['none', 'low', 'medium', 'high', 'ultrathink']; +export const THINKING_LEVELS: ThinkingLevel[] = [ + 'none', + 'low', + 'medium', + 'high', + 'ultrathink', + 'adaptive', +]; export const THINKING_LEVEL_LABELS: Record = { none: 'None', @@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record = { medium: 'Med', high: 'High', ultrathink: 'Ultra', + adaptive: 'Adaptive', }; /** diff --git a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx index 74b791a3..3e111a31 100644 --- a/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx +++ b/apps/ui/src/components/views/board-view/shared/thinking-level-selector.tsx @@ -2,19 +2,25 @@ import { Label } from '@/components/ui/label'; import { Brain } from 'lucide-react'; import { cn } from '@/lib/utils'; import { ThinkingLevel } from '@/store/app-store'; -import { THINKING_LEVELS, THINKING_LEVEL_LABELS } from './model-constants'; +import { THINKING_LEVEL_LABELS } from './model-constants'; +import { getThinkingLevelsForModel } from '@automaker/types'; interface ThinkingLevelSelectorProps { selectedLevel: ThinkingLevel; onLevelSelect: (level: ThinkingLevel) => void; testIdPrefix?: string; + /** Optional model ID to filter available thinking levels (e.g., Opus 4.6 only shows None/Adaptive) */ + model?: string; } export function ThinkingLevelSelector({ selectedLevel, onLevelSelect, testIdPrefix = 'thinking-level', + model, }: ThinkingLevelSelectorProps) { + const levels = model ? getThinkingLevelsForModel(model) : getThinkingLevelsForModel(''); + return (
- {THINKING_LEVELS.map((level) => ( + {levels.map((level) => (
); diff --git a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx index 20420388..25424fa6 100644 --- a/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx +++ b/apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx @@ -21,6 +21,7 @@ import { isGroupSelected, getSelectedVariant, codexModelHasThinking, + getThinkingLevelsForModel, } from '@automaker/types'; import { CLAUDE_MODELS, @@ -28,7 +29,6 @@ import { OPENCODE_MODELS, GEMINI_MODELS, COPILOT_MODELS, - THINKING_LEVELS, THINKING_LEVEL_LABELS, REASONING_EFFORT_LEVELS, REASONING_EFFORT_LABELS, @@ -1296,7 +1296,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => ( + {getThinkingLevelsForModel( + model.mapsToClaudeModel === 'opus' ? 'claude-opus' : '' + ).map((level) => (
{isSelected && currentThinking === level && ( @@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
Thinking Level
- {THINKING_LEVELS.map((level) => ( + {getThinkingLevelsForModel( + model.mapsToClaudeModel === 'opus' ? 'claude-opus' : '' + ).map((level) => (