feat: update Claude model to Opus 4.6 and enhance adaptive thinking support

- Changed model identifier from `claude-opus-4-5-20251101` to `claude-opus-4-6` across various files, including documentation and code references.
- Updated the SDK to support adaptive thinking for Opus 4.6, allowing the model to determine its own reasoning depth.
- Enhanced the thinking level options to include 'adaptive' and adjusted related components to reflect this change.
- Updated tests to ensure compatibility with the new model and its features.

These changes improve the model's capabilities and user experience by leveraging adaptive reasoning.
This commit is contained in:
Kacper
2026-02-05 22:43:22 +01:00
parent 3b361cb0b9
commit 835ffe3185
25 changed files with 178 additions and 71 deletions

View File

@@ -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",

View File

@@ -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<Options> {
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}`

View File

@@ -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,

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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();
});
});
});
});

View File

@@ -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);
});

View File

@@ -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);
});
});

View File

@@ -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: [

View File

@@ -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 => {

View File

@@ -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<ThinkingLevel, string> = {
none: 'None',
@@ -175,6 +182,7 @@ export const THINKING_LEVEL_LABELS: Record<ThinkingLevel, string> = {
medium: 'Med',
high: 'High',
ultrathink: 'Ultra',
adaptive: 'Adaptive',
};
/**

View File

@@ -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 (
<div className="space-y-2 pt-2 border-t border-border">
<Label className="flex items-center gap-2 text-sm">
@@ -22,7 +28,7 @@ export function ThinkingLevelSelector({
Thinking Level
</Label>
<div className="flex gap-2 flex-wrap">
{THINKING_LEVELS.map((level) => (
{levels.map((level) => (
<button
key={level}
type="button"
@@ -40,7 +46,9 @@ export function ThinkingLevelSelector({
))}
</div>
<p className="text-xs text-muted-foreground">
Higher levels give more time to reason through complex problems.
{model && getThinkingLevelsForModel(model).includes('adaptive')
? 'Adaptive thinking lets the model decide how much reasoning to use.'
: 'Higher levels give more time to reason through complex problems.'}
</p>
</div>
);

View File

@@ -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({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
).map((level) => (
<button
key={level}
onClick={() => {
@@ -1322,6 +1324,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1402,7 +1405,9 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(
model.mapsToClaudeModel === 'opus' ? 'claude-opus' : ''
).map((level) => (
<button
key={level}
onClick={() => {
@@ -1428,6 +1433,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1564,7 +1570,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1 text-xs font-medium text-muted-foreground">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(model.id).map((level) => (
<button
key={level}
onClick={() => {
@@ -1589,6 +1595,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (
@@ -1685,7 +1692,7 @@ export function PhaseModelSelector({
<div className="px-2 py-1.5 text-xs font-medium text-muted-foreground border-b border-border/50 mb-1">
Thinking Level
</div>
{THINKING_LEVELS.map((level) => (
{getThinkingLevelsForModel(model.id).map((level) => (
<button
key={level}
onClick={() => {
@@ -1710,6 +1717,7 @@ export function PhaseModelSelector({
{level === 'medium' && 'Moderate reasoning (10k tokens)'}
{level === 'high' && 'Deep reasoning (16k tokens)'}
{level === 'ultrathink' && 'Maximum reasoning (32k tokens)'}
{level === 'adaptive' && 'Model decides reasoning depth'}
</span>
</div>
{isSelected && currentThinking === level && (

View File

@@ -27,13 +27,14 @@ export interface AgentTaskInfo {
/**
* Default model used by the feature executor
*/
export const DEFAULT_MODEL = 'claude-opus-4-5-20251101';
export const DEFAULT_MODEL = 'claude-opus-4-6';
/**
* Formats a model name for display
*/
export function formatModelName(model: string): string {
// Claude models
if (model.includes('opus-4-6')) return 'Opus 4.6';
if (model.includes('opus')) return 'Opus 4.5';
if (model.includes('sonnet')) return 'Sonnet 4.5';
if (model.includes('haiku')) return 'Haiku 4.5';