diff --git a/apps/backend/.env.example b/apps/backend/.env.example index 06eeb777..a0bb7ad7 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -32,8 +32,8 @@ # API_TIMEOUT_MS=600000 # Model override (OPTIONAL) -# Default: claude-opus-4-5-20251101 -# AUTO_BUILD_MODEL=claude-opus-4-5-20251101 +# Default: claude-opus-4-6 +# AUTO_BUILD_MODEL=claude-opus-4-6 # ============================================================================= diff --git a/apps/backend/agents/coder.py b/apps/backend/agents/coder.py index 18476c9c..f565906a 100644 --- a/apps/backend/agents/coder.py +++ b/apps/backend/agents/coder.py @@ -21,7 +21,12 @@ from linear_updater import ( linear_task_started, linear_task_stuck, ) -from phase_config import get_phase_model, get_phase_thinking_budget +from phase_config import ( + get_fast_mode, + get_phase_client_thinking_kwargs, + get_phase_model, + get_phase_model_betas, +) from phase_event import ExecutionPhase, emit_phase from progress import ( count_subtasks, @@ -579,9 +584,14 @@ async def run_autonomous_agent( # first_run means we're in planning phase, otherwise coding phase current_phase = "planning" if first_run else "coding" phase_model = get_phase_model(spec_dir, current_phase, model) - phase_thinking_budget = get_phase_thinking_budget(spec_dir, current_phase) + phase_betas = get_phase_model_betas(spec_dir, current_phase, model) + thinking_kwargs = get_phase_client_thinking_kwargs( + spec_dir, current_phase, phase_model + ) # Generate appropriate prompt + fast_mode = get_fast_mode(spec_dir) + if first_run: # Create client for planning phase client = create_client( @@ -589,7 +599,9 @@ async def run_autonomous_agent( spec_dir, phase_model, agent_type="planner", - max_thinking_tokens=phase_thinking_budget, + betas=phase_betas, + fast_mode=fast_mode, + **thinking_kwargs, ) prompt = generate_planner_prompt(spec_dir, project_dir) if planning_retry_context: @@ -727,7 +739,9 @@ async def run_autonomous_agent( spec_dir, phase_model, agent_type="coder", - max_thinking_tokens=phase_thinking_budget, + betas=phase_betas, + fast_mode=fast_mode, + **thinking_kwargs, ) # Get attempt count for recovery context diff --git a/apps/backend/agents/planner.py b/apps/backend/agents/planner.py index 0cc06212..bc4fcaa6 100644 --- a/apps/backend/agents/planner.py +++ b/apps/backend/agents/planner.py @@ -9,7 +9,12 @@ import logging from pathlib import Path from core.client import create_client -from phase_config import get_phase_model, get_phase_thinking_budget +from phase_config import ( + get_fast_mode, + get_phase_client_thinking_kwargs, + get_phase_model, + get_phase_model_betas, +) from phase_event import ExecutionPhase, emit_phase from task_logger import ( LogPhase, @@ -94,12 +99,19 @@ async def run_followup_planner( # Create client with phase-specific model and thinking budget # Respects task_metadata.json configuration when no CLI override planning_model = get_phase_model(spec_dir, "planning", model) - planning_thinking_budget = get_phase_thinking_budget(spec_dir, "planning") + planning_betas = get_phase_model_betas(spec_dir, "planning", model) + thinking_kwargs = get_phase_client_thinking_kwargs( + spec_dir, "planning", planning_model + ) + fast_mode = get_fast_mode(spec_dir) client = create_client( project_dir, spec_dir, planning_model, - max_thinking_tokens=planning_thinking_budget, + agent_type="planner", + betas=planning_betas, + fast_mode=fast_mode, + **thinking_kwargs, ) # Generate follow-up planner prompt diff --git a/apps/backend/agents/tools_pkg/models.py b/apps/backend/agents/tools_pkg/models.py index 8f9fcb9f..46671dfa 100644 --- a/apps/backend/agents/tools_pkg/models.py +++ b/apps/backend/agents/tools_pkg/models.py @@ -158,7 +158,7 @@ AGENT_CONFIGS = { "tools": BASE_READ_TOOLS, "mcp_servers": [], # Self-critique, no external tools "auto_claude_tools": [], - "thinking_default": "ultrathink", + "thinking_default": "high", }, "spec_discovery": { "tools": BASE_READ_TOOLS + WEB_TOOLS, @@ -210,7 +210,7 @@ AGENT_CONFIGS = { TOOL_RECORD_GOTCHA, TOOL_GET_SESSION_CONTEXT, ], - "thinking_default": "none", # Coding doesn't use extended thinking + "thinking_default": "low", # Coding uses minimal thinking (effort: low for Opus, 1024 tokens for Sonnet/Haiku) }, # ═══════════════════════════════════════════════════════════════════════ # QA PHASES (Read + test + browser + Graphiti memory) @@ -247,9 +247,9 @@ AGENT_CONFIGS = { "tools": BASE_READ_TOOLS + WEB_TOOLS, "mcp_servers": [], "auto_claude_tools": [], - # Note: Default to "none" because insight_extractor uses Haiku which doesn't support thinking - # If using Sonnet/Opus models, override max_thinking_tokens in create_simple_client() - "thinking_default": "none", + # Note: Default to "low" for minimal thinking overhead + # Haiku doesn't support thinking; create_simple_client() handles this + "thinking_default": "low", }, "merge_resolver": { "tools": [], # Text-only analysis @@ -524,7 +524,7 @@ def get_default_thinking_level(agent_type: str) -> str: agent_type: The agent type identifier Returns: - Thinking level string (none, low, medium, high, ultrathink) + Thinking level string (low, medium, high) """ config = get_agent_config(agent_type) return config.get("thinking_default", "medium") diff --git a/apps/backend/core/client.py b/apps/backend/core/client.py index aaa2c32b..c46c208a 100644 --- a/apps/backend/core/client.py +++ b/apps/backend/core/client.py @@ -449,6 +449,9 @@ def create_client( max_thinking_tokens: int | None = None, output_format: dict | None = None, agents: dict | None = None, + betas: list[str] | None = None, + effort_level: str | None = None, + fast_mode: bool = False, ) -> ClaudeSDKClient: """ Create a Claude Agent SDK client with multi-layered security. @@ -464,10 +467,9 @@ def create_client( agent_type: Agent type identifier from AGENT_CONFIGS (e.g., 'coder', 'planner', 'qa_reviewer', 'spec_gatherer') max_thinking_tokens: Token budget for extended thinking (None = disabled) - - ultrathink: 16000 (spec creation) - - high: 10000 (QA review) - - medium: 5000 (planning, validation) - - None: disabled (coding) + - high: 16384 (spec creation, QA review) + - medium: 4096 (planning, validation) + - low: 1024 (coding) output_format: Optional structured output format for validated JSON responses. Use {"type": "json_schema", "schema": Model.model_json_schema()} See: https://platform.claude.com/docs/en/agent-sdk/structured-outputs @@ -475,6 +477,15 @@ def create_client( Format: {"agent-name": {"description": "...", "prompt": "...", "tools": [...], "model": "inherit"}} See: https://platform.claude.com/docs/en/agent-sdk/subagents + betas: Optional list of SDK beta header strings (e.g., ["context-1m-2025-08-07"] + for 1M context window). Use get_phase_model_betas() to compute from config. + effort_level: Optional effort level for adaptive thinking models (e.g., "low", + "medium", "high"). When set, injected as CLAUDE_CODE_EFFORT_LEVEL + env var for the SDK subprocess. Only meaningful for models that + support adaptive thinking (e.g., Opus 4.6). + fast_mode: Enable Fast Mode for faster Opus 4.6 output. When True, injected + as CLAUDE_CODE_FAST_MODE=true env var. Requires extra usage enabled + on Claude subscription; falls back to standard speed automatically. Returns: Configured ClaudeSDKClient @@ -502,6 +513,14 @@ def create_client( if config_dir: logger.info(f"Using CLAUDE_CONFIG_DIR for profile: {config_dir}") + # Inject effort level for adaptive thinking models (e.g., Opus 4.6) + if effort_level: + sdk_env["CLAUDE_CODE_EFFORT_LEVEL"] = effort_level + + # Inject fast mode for faster Opus 4.6 output + if fast_mode: + sdk_env["CLAUDE_CODE_FAST_MODE"] = "true" + # Debug: Log git-bash path detection on Windows if "CLAUDE_CODE_GIT_BASH_PATH" in sdk_env: logger.info(f"Git Bash path found: {sdk_env['CLAUDE_CODE_GIT_BASH_PATH']}") @@ -665,7 +684,12 @@ def create_client( print(" - Worktree permissions: granted for original project directories") print(" - Bash commands restricted to allowlist") if max_thinking_tokens: - print(f" - Extended thinking: {max_thinking_tokens:,} tokens") + thinking_info = f"{max_thinking_tokens:,} tokens" + if effort_level: + thinking_info += f" + effort={effort_level}" + if fast_mode: + thinking_info += " + fast mode" + print(f" - Extended thinking: {thinking_info}") else: print(" - Extended thinking: disabled") @@ -834,4 +858,8 @@ def create_client( if agents: options_kwargs["agents"] = agents + # Add beta headers if specified (e.g., for 1M context window) + if betas: + options_kwargs["betas"] = betas + return ClaudeSDKClient(options=ClaudeAgentOptions(**options_kwargs)) diff --git a/apps/backend/core/simple_client.py b/apps/backend/core/simple_client.py index 3e26d8de..8c53a42f 100644 --- a/apps/backend/core/simple_client.py +++ b/apps/backend/core/simple_client.py @@ -44,6 +44,9 @@ def create_simple_client( cwd: Path | None = None, max_turns: int = 1, max_thinking_tokens: int | None = None, + betas: list[str] | None = None, + effort_level: str | None = None, + fast_mode: bool = False, ) -> ClaudeSDKClient: """ Create a minimal Claude SDK client for single-turn utility operations. @@ -65,6 +68,11 @@ def create_simple_client( max_turns: Maximum conversation turns (default: 1 for single-turn) max_thinking_tokens: Override thinking budget (None = use agent default from AGENT_CONFIGS, converted using phase_config.THINKING_BUDGET_MAP) + betas: Optional list of SDK beta header strings (e.g., ["context-1m-2025-08-07"]) + effort_level: Optional effort level for adaptive thinking models (e.g., "low", + "medium", "high"). Injected as CLAUDE_CODE_EFFORT_LEVEL env var. + fast_mode: Enable Fast Mode for faster Opus 4.6 output. Injected as + CLAUDE_CODE_FAST_MODE=true env var. Returns: Configured ClaudeSDKClient for single-turn operations @@ -82,6 +90,14 @@ def create_simple_client( # Configure SDK authentication (OAuth or API profile mode) configure_sdk_authentication(config_dir) + # Inject effort level for adaptive thinking models (e.g., Opus 4.6) + if effort_level: + sdk_env["CLAUDE_CODE_EFFORT_LEVEL"] = effort_level + + # Inject fast mode for faster Opus 4.6 output + if fast_mode: + sdk_env["CLAUDE_CODE_FAST_MODE"] = "true" + # Get agent configuration (raises ValueError if unknown type) config = get_agent_config(agent_type) @@ -108,6 +124,10 @@ def create_simple_client( if max_thinking_tokens is not None: options_kwargs["max_thinking_tokens"] = max_thinking_tokens + # Add beta headers if specified (e.g., for 1M context window) + if betas: + options_kwargs["betas"] = betas + # Optional: Allow CLI path override via environment variable env_cli_path = os.environ.get("CLAUDE_CLI_PATH") if env_cli_path and validate_cli_path(env_cli_path): diff --git a/apps/backend/ideation/config.py b/apps/backend/ideation/config.py index 0f56a893..67808df2 100644 --- a/apps/backend/ideation/config.py +++ b/apps/backend/ideation/config.py @@ -29,6 +29,7 @@ class IdeationConfigManager: thinking_level: str = "medium", refresh: bool = False, append: bool = False, + fast_mode: bool = False, ): """Initialize configuration manager. @@ -64,6 +65,7 @@ class IdeationConfigManager: self.model, self.thinking_level, self.max_ideas_per_type, + fast_mode=fast_mode, ) self.analyzer = ProjectAnalyzer( self.project_dir, diff --git a/apps/backend/ideation/generator.py b/apps/backend/ideation/generator.py index b03a097e..8b5e7a51 100644 --- a/apps/backend/ideation/generator.py +++ b/apps/backend/ideation/generator.py @@ -17,7 +17,12 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent)) from client import create_client -from phase_config import get_thinking_budget, resolve_model_id +from phase_config import ( + get_model_betas, + get_thinking_budget, + get_thinking_kwargs_for_model, + resolve_model_id, +) from ui import print_status # Ideation types @@ -59,6 +64,7 @@ class IdeationGenerator: model: str = "sonnet", # Changed from "opus" (fix #433) thinking_level: str = "medium", max_ideas_per_type: int = 5, + fast_mode: bool = False, ): self.project_dir = Path(project_dir) self.output_dir = Path(output_dir) @@ -66,6 +72,7 @@ class IdeationGenerator: self.thinking_level = thinking_level self.thinking_budget = get_thinking_budget(thinking_level) self.max_ideas_per_type = max_ideas_per_type + self.fast_mode = fast_mode self.prompts_dir = Path(__file__).parent.parent / "prompts" async def run_agent( @@ -93,12 +100,19 @@ class IdeationGenerator: # Create client with thinking budget # Use agent_type="ideation" to avoid loading unnecessary MCP servers # which can cause 60-second timeout delays + resolved_model = resolve_model_id(self.model) + betas = get_model_betas(self.model) + thinking_kwargs = get_thinking_kwargs_for_model( + resolved_model, self.thinking_level + ) client = create_client( self.project_dir, self.output_dir, - resolve_model_id(self.model), - max_thinking_tokens=self.thinking_budget, + resolved_model, agent_type="ideation", + betas=betas, + fast_mode=self.fast_mode, + **thinking_kwargs, ) try: @@ -188,12 +202,19 @@ Write the fixed JSON to the file now. """ # Use agent_type="ideation" for recovery agent as well + resolved_model = resolve_model_id(self.model) + betas = get_model_betas(self.model) + thinking_kwargs = get_thinking_kwargs_for_model( + resolved_model, self.thinking_level + ) client = create_client( self.project_dir, self.output_dir, - resolve_model_id(self.model), - max_thinking_tokens=self.thinking_budget, + resolved_model, agent_type="ideation", + betas=betas, + fast_mode=self.fast_mode, + **thinking_kwargs, ) try: diff --git a/apps/backend/ideation/runner.py b/apps/backend/ideation/runner.py index 70fb2fbc..be48f271 100644 --- a/apps/backend/ideation/runner.py +++ b/apps/backend/ideation/runner.py @@ -46,6 +46,7 @@ class IdeationOrchestrator: thinking_level: str = "medium", refresh: bool = False, append: bool = False, + fast_mode: bool = False, ): """Initialize the ideation orchestrator. @@ -60,6 +61,7 @@ class IdeationOrchestrator: thinking_level: Thinking level for extended reasoning refresh: Force regeneration of existing files append: Preserve existing ideas when merging + fast_mode: Enable Fast Mode for faster Opus 4.6 output """ # Initialize configuration manager self.config_manager = IdeationConfigManager( @@ -73,6 +75,7 @@ class IdeationOrchestrator: thinking_level=thinking_level, refresh=refresh, append=append, + fast_mode=fast_mode, ) # Expose configuration for convenience diff --git a/apps/backend/phase_config.py b/apps/backend/phase_config.py index 41af2d81..5abd7244 100644 --- a/apps/backend/phase_config.py +++ b/apps/backend/phase_config.py @@ -12,30 +12,44 @@ from pathlib import Path from typing import Literal, TypedDict # Model shorthand to full model ID mapping +# Values must match apps/frontend/src/shared/constants/models.ts MODEL_ID_MAP MODEL_ID_MAP: dict[str, str] = { - "opus": "claude-opus-4-5-20251101", + "opus": "claude-opus-4-6", + "opus-1m": "claude-opus-4-6", "sonnet": "claude-sonnet-4-5-20250929", "haiku": "claude-haiku-4-5-20251001", } -# Thinking level to budget tokens mapping (None = no extended thinking) -# Values must match auto-claude-ui/src/shared/constants/models.ts THINKING_BUDGET_MAP -THINKING_BUDGET_MAP: dict[str, int | None] = { - "none": None, +# Model shorthand to required SDK beta headers +# Maps model shorthands that need special beta flags (e.g., 1M context window) +MODEL_BETAS_MAP: dict[str, list[str]] = { + "opus-1m": ["context-1m-2025-08-07"], +} + +# Thinking level to budget tokens mapping +# Values must match apps/frontend/src/shared/constants/models.ts THINKING_BUDGET_MAP +THINKING_BUDGET_MAP: dict[str, int] = { "low": 1024, "medium": 4096, # Moderate analysis "high": 16384, # Deep thinking for QA review - "ultrathink": 63999, # Maximum reasoning depth (API requires max_tokens >= budget + 1, so 63999 + 1 = 64000 limit) } +# Effort level mapping for adaptive thinking models (e.g., Opus 4.6) +# These models support CLAUDE_CODE_EFFORT_LEVEL env var for effort-based routing +EFFORT_LEVEL_MAP: dict[str, str] = {"low": "low", "medium": "medium", "high": "high"} + +# Models that support adaptive thinking via effort level (env var) +# These models get both max_thinking_tokens AND effort_level +ADAPTIVE_THINKING_MODELS: set[str] = {"claude-opus-4-6"} + # Spec runner phase-specific thinking levels -# Heavy phases use ultrathink for deep analysis +# Heavy phases use high for deep analysis # Light phases use medium after compaction SPEC_PHASE_THINKING_LEVELS: dict[str, str] = { - # Heavy phases - ultrathink (discovery, spec creation, self-critique) - "discovery": "ultrathink", - "spec_writing": "ultrathink", - "self_critique": "ultrathink", + # Heavy phases - high (discovery, spec creation, self-critique) + "discovery": "high", + "spec_writing": "high", + "self_critique": "high", # Light phases - medium (after first invocation with compaction) "requirements": "medium", "research": "medium", @@ -85,6 +99,7 @@ class TaskMetadataConfig(TypedDict, total=False): phaseThinking: PhaseThinkingConfig model: str thinkingLevel: str + fastMode: bool Phase = Literal["spec", "planning", "coding", "qa"] @@ -112,6 +127,7 @@ def resolve_model_id(model: str) -> str: "haiku": "ANTHROPIC_DEFAULT_HAIKU_MODEL", "sonnet": "ANTHROPIC_DEFAULT_SONNET_MODEL", "opus": "ANTHROPIC_DEFAULT_OPUS_MODEL", + "opus-1m": "ANTHROPIC_DEFAULT_OPUS_MODEL", } env_var = env_var_map.get(model) if env_var: @@ -126,15 +142,31 @@ def resolve_model_id(model: str) -> str: return model -def get_thinking_budget(thinking_level: str) -> int | None: +def get_model_betas(model_short: str) -> list[str]: + """ + Get required SDK beta headers for a model shorthand. + + Some model configurations (e.g., opus-1m for 1M context window) require + passing beta headers to the Claude Agent SDK. + + Args: + model_short: Model shorthand (e.g., 'opus', 'opus-1m', 'sonnet') + + Returns: + List of beta header strings, or empty list if none required + """ + return MODEL_BETAS_MAP.get(model_short, []) + + +def get_thinking_budget(thinking_level: str) -> int: """ Get the thinking budget for a thinking level. Args: - thinking_level: Thinking level (none, low, medium, high, ultrathink) + thinking_level: Thinking level (low, medium, high) Returns: - Token budget or None for no extended thinking + Token budget for extended thinking """ import logging @@ -214,6 +246,43 @@ def get_phase_model( return resolve_model_id(DEFAULT_PHASE_MODELS[phase]) +def get_phase_model_betas( + spec_dir: Path, + phase: Phase, + cli_model: str | None = None, +) -> list[str]: + """ + Get required SDK beta headers for the model selected for a specific phase. + + Uses the same priority logic as get_phase_model() to determine which model + shorthand is selected, then looks up any required beta headers. + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + cli_model: Model from CLI argument (optional) + + Returns: + List of beta header strings, or empty list if none required + """ + # Determine the model shorthand (before resolution to full ID) + if cli_model: + return get_model_betas(cli_model) + + metadata = load_task_metadata(spec_dir) + + if metadata: + if metadata.get("isAutoProfile") and metadata.get("phaseModels"): + phase_models = metadata["phaseModels"] + model_short = phase_models.get(phase, DEFAULT_PHASE_MODELS[phase]) + return get_model_betas(model_short) + + if metadata.get("model"): + return get_model_betas(metadata["model"]) + + return get_model_betas(DEFAULT_PHASE_MODELS[phase]) + + def get_phase_thinking( spec_dir: Path, phase: Phase, @@ -261,7 +330,7 @@ def get_phase_thinking_budget( spec_dir: Path, phase: Phase, cli_thinking: str | None = None, -) -> int | None: +) -> int: """ Get the thinking budget tokens for a specific execution phase. @@ -271,7 +340,7 @@ def get_phase_thinking_budget( cli_thinking: Thinking level from CLI argument (optional) Returns: - Token budget or None for no extended thinking + Token budget for extended thinking """ thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking) return get_thinking_budget(thinking_level) @@ -282,7 +351,7 @@ def get_phase_config( phase: Phase, cli_model: str | None = None, cli_thinking: str | None = None, -) -> tuple[str, str, int | None]: +) -> tuple[str, str, int]: """ Get the full configuration for a specific execution phase. @@ -302,7 +371,87 @@ def get_phase_config( return model_id, thinking_level, thinking_budget -def get_spec_phase_thinking_budget(phase_name: str) -> int | None: +def is_adaptive_model(model_id: str) -> bool: + """ + Check if a model supports adaptive thinking via effort level. + + Adaptive models support the CLAUDE_CODE_EFFORT_LEVEL environment variable + for effort-based routing in addition to max_thinking_tokens. + + Args: + model_id: Full model ID (e.g., 'claude-opus-4-6') + + Returns: + True if the model supports adaptive thinking + """ + return model_id in ADAPTIVE_THINKING_MODELS + + +def get_thinking_kwargs_for_model(model_id: str, thinking_level: str) -> dict: + """ + Get thinking-related kwargs for create_client() based on model type. + + For adaptive models (Opus 4.6): returns both max_thinking_tokens and effort_level. + For other models (Sonnet, Haiku): returns only max_thinking_tokens. + + Args: + model_id: Full model ID (e.g., 'claude-opus-4-6') + thinking_level: Thinking level string (low, medium, high) + + Returns: + Dict with 'max_thinking_tokens' and optionally 'effort_level' + """ + kwargs: dict = {"max_thinking_tokens": get_thinking_budget(thinking_level)} + if is_adaptive_model(model_id): + kwargs["effort_level"] = EFFORT_LEVEL_MAP.get(thinking_level, "medium") + return kwargs + + +def get_phase_client_thinking_kwargs( + spec_dir: Path, + phase: Phase, + phase_model: str, + cli_thinking: str | None = None, +) -> dict: + """ + Get thinking kwargs for create_client() for a specific execution phase. + + Combines get_phase_thinking() and get_thinking_kwargs_for_model() to produce + the correct kwargs dict based on phase config and model capabilities. + + Args: + spec_dir: Path to the spec directory + phase: Execution phase (spec, planning, coding, qa) + phase_model: Resolved full model ID for this phase + cli_thinking: Thinking level from CLI argument (optional) + + Returns: + Dict with 'max_thinking_tokens' and optionally 'effort_level' + """ + thinking_level = get_phase_thinking(spec_dir, phase, cli_thinking) + return get_thinking_kwargs_for_model(phase_model, thinking_level) + + +def get_fast_mode(spec_dir: Path) -> bool: + """ + Check if Fast Mode is enabled for this task. + + Fast Mode provides faster Opus 4.6 output at higher cost. + Reads the fastMode flag from task_metadata.json. + + Args: + spec_dir: Path to the spec directory + + Returns: + True if Fast Mode is enabled, False otherwise + """ + metadata = load_task_metadata(spec_dir) + if metadata: + return bool(metadata.get("fastMode", False)) + return False + + +def get_spec_phase_thinking_budget(phase_name: str) -> int: """ Get the thinking budget for a specific spec runner phase. @@ -313,7 +462,7 @@ def get_spec_phase_thinking_budget(phase_name: str) -> int | None: phase_name: Name of the spec phase (e.g., 'discovery', 'spec_writing') Returns: - Token budget for extended thinking, or None for no extended thinking + Token budget for extended thinking """ thinking_level = SPEC_PHASE_THINKING_LEVELS.get(phase_name, "medium") return get_thinking_budget(thinking_level) diff --git a/apps/backend/qa/loop.py b/apps/backend/qa/loop.py index 1685a99a..a798f9ba 100644 --- a/apps/backend/qa/loop.py +++ b/apps/backend/qa/loop.py @@ -21,7 +21,12 @@ from linear_updater import ( linear_qa_rejected, linear_qa_started, ) -from phase_config import get_phase_model, get_phase_thinking_budget +from phase_config import ( + get_fast_mode, + get_phase_client_thinking_kwargs, + get_phase_model, + get_phase_model_betas, +) from phase_event import ExecutionPhase, emit_phase from progress import count_subtasks, is_build_complete from security.constants import PROJECT_DIR_ENV_VAR @@ -125,6 +130,8 @@ async def run_qa_validation_loop( {"iteration": 1, "maxIterations": MAX_QA_ITERATIONS}, ) + fast_mode = get_fast_mode(spec_dir) + # Check if there's pending human feedback that needs to be processed fix_request_file = spec_dir / "QA_FIX_REQUEST.md" has_human_feedback = fix_request_file.exists() @@ -155,14 +162,19 @@ async def run_qa_validation_loop( # Get model and thinking budget for fixer (uses QA phase config) qa_model = get_phase_model(spec_dir, "qa", model) - fixer_thinking_budget = get_phase_thinking_budget(spec_dir, "qa") + qa_betas = get_phase_model_betas(spec_dir, "qa", model) + fixer_thinking_kwargs = get_phase_client_thinking_kwargs( + spec_dir, "qa", qa_model + ) fix_client = create_client( project_dir, spec_dir, qa_model, agent_type="qa_fixer", - max_thinking_tokens=fixer_thinking_budget, + betas=qa_betas, + fast_mode=fast_mode, + **fixer_thinking_kwargs, ) async with fix_client: @@ -262,19 +274,22 @@ async def run_qa_validation_loop( # Run QA reviewer with phase-specific model and thinking budget qa_model = get_phase_model(spec_dir, "qa", model) - qa_thinking_budget = get_phase_thinking_budget(spec_dir, "qa") + qa_betas = get_phase_model_betas(spec_dir, "qa", model) + qa_thinking_kwargs = get_phase_client_thinking_kwargs(spec_dir, "qa", qa_model) debug( "qa_loop", "Creating client for QA reviewer session...", model=qa_model, - thinking_budget=qa_thinking_budget, + thinking_budget=qa_thinking_kwargs.get("max_thinking_tokens"), ) client = create_client( project_dir, spec_dir, qa_model, agent_type="qa_reviewer", - max_thinking_tokens=qa_thinking_budget, + betas=qa_betas, + fast_mode=fast_mode, + **qa_thinking_kwargs, ) async with client: @@ -450,12 +465,15 @@ async def run_qa_validation_loop( break # Run fixer with phase-specific thinking budget - fixer_thinking_budget = get_phase_thinking_budget(spec_dir, "qa") + fixer_betas = get_phase_model_betas(spec_dir, "qa", model) + fixer_thinking_kwargs = get_phase_client_thinking_kwargs( + spec_dir, "qa", qa_model + ) debug( "qa_loop", "Starting QA fixer session...", model=qa_model, - thinking_budget=fixer_thinking_budget, + thinking_budget=fixer_thinking_kwargs.get("max_thinking_tokens"), ) emit_phase(ExecutionPhase.QA_FIXING, "Fixing QA issues") task_event_emitter.emit( @@ -469,7 +487,9 @@ async def run_qa_validation_loop( spec_dir, qa_model, agent_type="qa_fixer", - max_thinking_tokens=fixer_thinking_budget, + betas=fixer_betas, + fast_mode=fast_mode, + **fixer_thinking_kwargs, ) async with fix_client: diff --git a/apps/backend/runners/github/models.py b/apps/backend/runners/github/models.py index 25c376f2..e6aac73c 100644 --- a/apps/backend/runners/github/models.py +++ b/apps/backend/runners/github/models.py @@ -997,6 +997,7 @@ class GitHubRunnerConfig: # to respect environment variable overrides (e.g., ANTHROPIC_DEFAULT_SONNET_MODEL) model: str = "sonnet" thinking_level: str = "medium" + fast_mode: bool = False def to_dict(self) -> dict: return { @@ -1019,6 +1020,7 @@ class GitHubRunnerConfig: "allow_fix_commits": self.allow_fix_commits, "model": self.model, "thinking_level": self.thinking_level, + "fast_mode": self.fast_mode, } def save_settings(self, github_dir: Path) -> None: diff --git a/apps/backend/runners/github/rate_limiter.py b/apps/backend/runners/github/rate_limiter.py index 9561ccc5..633bce80 100644 --- a/apps/backend/runners/github/rate_limiter.py +++ b/apps/backend/runners/github/rate_limiter.py @@ -163,6 +163,10 @@ AI_PRICING = { # Claude 4.5 models (current) "claude-sonnet-4-5-20250929": {"input": 3.00, "output": 15.00}, "claude-opus-4-5-20251101": {"input": 15.00, "output": 75.00}, + "claude-opus-4-6": {"input": 15.00, "output": 75.00}, + # Note: Opus 4.6 with 1M context (opus-1m) uses the same model ID with a beta + # header, so it shares the same pricing key. Requests >200K tokens incur premium + # rates (2x input, 1.5x output) automatically on the API side. "claude-haiku-4-5-20251001": {"input": 0.80, "output": 4.00}, # Extended thinking models (higher output costs) "claude-sonnet-4-5-20250929-thinking": {"input": 3.00, "output": 15.00}, diff --git a/apps/backend/runners/github/runner.py b/apps/backend/runners/github/runner.py index 430b0b4c..3c1d138e 100644 --- a/apps/backend/runners/github/runner.py +++ b/apps/backend/runners/github/runner.py @@ -182,6 +182,7 @@ def get_config(args) -> GitHubRunnerConfig: bot_token=bot_token, model=args.model, thinking_level=args.thinking_level, + fast_mode=getattr(args, "fast_mode", False), auto_fix_enabled=getattr(args, "auto_fix_enabled", False), auto_fix_labels=getattr(args, "auto_fix_labels", ["auto-fix"]), auto_post_reviews=getattr(args, "auto_post", False), @@ -688,9 +689,14 @@ def main(): "--thinking-level", type=str, default="medium", - choices=["none", "low", "medium", "high"], + choices=["low", "medium", "high"], help="Thinking level for extended reasoning", ) + parser.add_argument( + "--fast-mode", + action="store_true", + help="Enable Fast Mode for faster Opus 4.6 output", + ) subparsers = parser.add_subparsers(dest="command", help="Command to run") diff --git a/apps/backend/runners/github/services/parallel_followup_reviewer.py b/apps/backend/runners/github/services/parallel_followup_reviewer.py index a8e72f18..b5e11652 100644 --- a/apps/backend/runners/github/services/parallel_followup_reviewer.py +++ b/apps/backend/runners/github/services/parallel_followup_reviewer.py @@ -31,7 +31,11 @@ from claude_agent_sdk import AgentDefinition try: from ...core.client import create_client - from ...phase_config import get_thinking_budget, resolve_model_id + from ...phase_config import ( + get_model_betas, + get_thinking_kwargs_for_model, + resolve_model_id, + ) from ..context_gatherer import _validate_git_ref from ..gh_client import GHClient from ..models import ( @@ -62,7 +66,11 @@ except (ImportError, ValueError, SystemError): PRReviewResult, ReviewSeverity, ) - from phase_config import get_thinking_budget, resolve_model_id + from phase_config import ( + get_model_betas, + get_thinking_kwargs_for_model, + resolve_model_id, + ) from services.agent_utils import create_working_dir_injector from services.category_utils import map_category from services.io_utils import safe_print @@ -517,12 +525,13 @@ The SDK will run invoked agents in parallel automatically. # Resolve model shorthand via environment variable override if configured model_shorthand = self.config.model or "sonnet" model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) thinking_level = self.config.thinking_level or "medium" - thinking_budget = get_thinking_budget(thinking_level) + thinking_kwargs = get_thinking_kwargs_for_model(model, thinking_level) logger.info( f"[ParallelFollowup] Using model={model}, " - f"thinking_level={thinking_level}, thinking_budget={thinking_budget}" + f"thinking_level={thinking_level}, thinking_kwargs={thinking_kwargs}" ) # Create client with subagents defined (using worktree path) @@ -531,12 +540,14 @@ The SDK will run invoked agents in parallel automatically. spec_dir=self.github_dir, model=model, agent_type="pr_followup_parallel", - max_thinking_tokens=thinking_budget, + betas=betas, + fast_mode=self.config.fast_mode, agents=self._define_specialist_agents(project_root), output_format={ "type": "json_schema", "schema": ParallelFollowupResponse.model_json_schema(), }, + **thinking_kwargs, ) self._report_progress( diff --git a/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py b/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py index 385fa9df..0c76cf56 100644 --- a/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py +++ b/apps/backend/runners/github/services/parallel_orchestrator_reviewer.py @@ -33,7 +33,12 @@ from claude_agent_sdk import AgentDefinition # noqa: F401 try: from ...core.client import create_client - from ...phase_config import get_thinking_budget, resolve_model_id + from ...phase_config import ( + get_model_betas, + get_thinking_budget, + get_thinking_kwargs_for_model, + resolve_model_id, + ) from ..context_gatherer import PRContext, _validate_git_ref from ..gh_client import GHClient from ..models import ( @@ -69,7 +74,12 @@ except (ImportError, ValueError, SystemError): PRReviewResult, ReviewSeverity, ) - from phase_config import get_thinking_budget, resolve_model_id + from phase_config import ( + get_model_betas, + get_thinking_budget, + get_thinking_kwargs_for_model, + resolve_model_id, + ) from services.agent_utils import create_working_dir_injector from services.category_utils import map_category from services.io_utils import safe_print @@ -498,16 +508,23 @@ Report findings with specific file paths, line numbers, and code evidence. # Note: Agent type uses the generic "pr_reviewer" since individual # specialist types aren't registered in AGENT_CONFIGS. The specialist-specific # system prompt handles differentiation. + # Get betas from model shorthand (before resolution to full ID) + betas = get_model_betas(self.config.model or "sonnet") + thinking_kwargs = get_thinking_kwargs_for_model( + model, self.config.thinking_level or "medium" + ) client = create_client( project_dir=project_root, spec_dir=self.github_dir, model=model, agent_type="pr_reviewer", - max_thinking_tokens=thinking_budget, + betas=betas, + fast_mode=self.config.fast_mode, output_format={ "type": "json_schema", "schema": SpecialistResponse.model_json_schema(), }, + **thinking_kwargs, ) async with client: @@ -793,17 +810,24 @@ The SDK will run invoked agents in parallel automatically. Returns: Configured SDK client instance """ + # Get betas from model shorthand (before resolution to full ID) + betas = get_model_betas(self.config.model or "sonnet") + thinking_kwargs = get_thinking_kwargs_for_model( + model, self.config.thinking_level or "medium" + ) return create_client( project_dir=project_root, spec_dir=self.github_dir, model=model, agent_type="pr_orchestrator_parallel", - max_thinking_tokens=thinking_budget, + betas=betas, + fast_mode=self.config.fast_mode, agents=self._define_specialist_agents(project_root), output_format={ "type": "json_schema", "schema": ParallelOrchestratorResponse.model_json_schema(), }, + **thinking_kwargs, ) def _extract_structured_output( @@ -1718,16 +1742,21 @@ For EACH finding above: # Create validator client (inherits worktree filesystem access) try: + # Get betas from model shorthand (before resolution to full ID) + betas = get_model_betas(self.config.model or "sonnet") + thinking_kwargs = get_thinking_kwargs_for_model(model, "medium") validator_client = create_client( project_dir=worktree_path, spec_dir=self.github_dir, model=model, agent_type="pr_finding_validator", - max_thinking_tokens=get_thinking_budget("medium"), + betas=betas, + fast_mode=self.config.fast_mode, output_format={ "type": "json_schema", "schema": FindingValidationResponse.model_json_schema(), }, + **thinking_kwargs, ) except Exception as e: logger.error(f"[PRReview] Failed to create validator client: {e}") diff --git a/apps/backend/runners/github/services/pr_review_engine.py b/apps/backend/runners/github/services/pr_review_engine.py index 15263d4c..cb45f204 100644 --- a/apps/backend/runners/github/services/pr_review_engine.py +++ b/apps/backend/runners/github/services/pr_review_engine.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Any try: - from ...phase_config import resolve_model_id + from ...phase_config import get_model_betas, resolve_model_id from ..context_gatherer import PRContext from ..models import ( AICommentTriage, @@ -34,7 +34,7 @@ except (ImportError, ValueError, SystemError): ReviewPass, StructuralIssue, ) - from phase_config import resolve_model_id + from phase_config import get_model_betas, resolve_model_id from services.io_utils import safe_print from services.prompt_manager import PromptManager from services.response_parsers import ResponseParser @@ -222,12 +222,16 @@ class PRReviewEngine: ) # Resolve model shorthand (e.g., "sonnet") to full model ID for API compatibility - model = resolve_model_id(self.config.model or "sonnet") + model_shorthand = self.config.model or "sonnet" + model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) client = create_client( project_dir=project_root, spec_dir=self.github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas, + fast_mode=self.config.fast_mode, ) result_text = "" @@ -487,12 +491,16 @@ class PRReviewEngine: ) # Resolve model shorthand (e.g., "sonnet") to full model ID for API compatibility - model = resolve_model_id(self.config.model or "sonnet") + model_shorthand = self.config.model or "sonnet" + model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) client = create_client( project_dir=project_root, spec_dir=self.github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas, + fast_mode=self.config.fast_mode, ) result_text = "" @@ -547,12 +555,16 @@ class PRReviewEngine: ) # Resolve model shorthand (e.g., "sonnet") to full model ID for API compatibility - model = resolve_model_id(self.config.model or "sonnet") + model_shorthand = self.config.model or "sonnet" + model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) client = create_client( project_dir=project_root, spec_dir=self.github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas, + fast_mode=self.config.fast_mode, ) result_text = "" diff --git a/apps/backend/runners/github/services/review_tools.py b/apps/backend/runners/github/services/review_tools.py index 5d5163e1..6853833f 100644 --- a/apps/backend/runners/github/services/review_tools.py +++ b/apps/backend/runners/github/services/review_tools.py @@ -75,6 +75,8 @@ async def spawn_security_review( project_dir: Path, github_dir: Path, model: str = "claude-sonnet-4-5-20250929", + betas: list[str] | None = None, + fast_mode: bool = False, ) -> list[PRReviewFinding]: """ Spawn a focused security review subagent for specific files. @@ -129,6 +131,8 @@ async def spawn_security_review( spec_dir=github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas or [], + fast_mode=fast_mode, ) # Run review session @@ -164,6 +168,8 @@ async def spawn_quality_review( project_dir: Path, github_dir: Path, model: str = "claude-sonnet-4-5-20250929", + betas: list[str] | None = None, + fast_mode: bool = False, ) -> list[PRReviewFinding]: """ Spawn a focused code quality review subagent for specific files. @@ -215,6 +221,8 @@ async def spawn_quality_review( spec_dir=github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas or [], + fast_mode=fast_mode, ) result_text = "" @@ -246,6 +254,8 @@ async def spawn_deep_analysis( project_dir: Path, github_dir: Path, model: str = "claude-sonnet-4-5-20250929", + betas: list[str] | None = None, + fast_mode: bool = False, ) -> list[PRReviewFinding]: """ Spawn a deep analysis subagent to investigate a specific concern. @@ -310,6 +320,8 @@ Output findings in JSON format: spec_dir=github_dir, model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas or [], + fast_mode=fast_mode, ) result_text = "" diff --git a/apps/backend/runners/github/services/sdk_utils.py b/apps/backend/runners/github/services/sdk_utils.py index 69f50736..79d11501 100644 --- a/apps/backend/runners/github/services/sdk_utils.py +++ b/apps/backend/runners/github/services/sdk_utils.py @@ -40,6 +40,11 @@ def _short_model_name(model: str | None) -> str: model_lower = model.lower() # Handle new model naming (claude-{model}-{version}-{date}) + # Check 1M context variant first (more specific match) + if "opus-4-6-1m" in model_lower or "opus-4.6-1m" in model_lower: + return "opus-4.6-1m" + if "opus-4-6" in model_lower or "opus-4.6" in model_lower: + return "opus-4.6" if "opus-4-5" in model_lower or "opus-4.5" in model_lower: return "opus-4.5" if "sonnet-4-5" in model_lower or "sonnet-4.5" in model_lower: diff --git a/apps/backend/runners/github/services/triage_engine.py b/apps/backend/runners/github/services/triage_engine.py index de38370f..e5abdf5e 100644 --- a/apps/backend/runners/github/services/triage_engine.py +++ b/apps/backend/runners/github/services/triage_engine.py @@ -10,13 +10,13 @@ from __future__ import annotations from pathlib import Path try: - from ...phase_config import resolve_model_id + from ...phase_config import get_model_betas, resolve_model_id from ..models import GitHubRunnerConfig, TriageCategory, TriageResult from .prompt_manager import PromptManager from .response_parsers import ResponseParser except (ImportError, ValueError, SystemError): from models import GitHubRunnerConfig, TriageCategory, TriageResult - from phase_config import resolve_model_id + from phase_config import get_model_betas, resolve_model_id from services.prompt_manager import PromptManager from services.response_parsers import ResponseParser @@ -74,12 +74,16 @@ class TriageEngine: # Run AI # Resolve model shorthand (e.g., "sonnet") to full model ID for API compatibility - model = resolve_model_id(self.config.model or "sonnet") + model_shorthand = self.config.model or "sonnet" + model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) client = create_client( project_dir=self.project_dir, spec_dir=self.github_dir, model=model, agent_type="qa_reviewer", + betas=betas, + fast_mode=self.config.fast_mode, ) try: diff --git a/apps/backend/runners/gitlab/models.py b/apps/backend/runners/gitlab/models.py index b0ccb4d6..33b2a660 100644 --- a/apps/backend/runners/gitlab/models.py +++ b/apps/backend/runners/gitlab/models.py @@ -210,6 +210,7 @@ class GitLabRunnerConfig: # Model settings model: str = "claude-sonnet-4-5-20250929" thinking_level: str = "medium" + fast_mode: bool = False def to_dict(self) -> dict: return { @@ -218,6 +219,7 @@ class GitLabRunnerConfig: "instance_url": self.instance_url, "model": self.model, "thinking_level": self.thinking_level, + "fast_mode": self.fast_mode, } diff --git a/apps/backend/runners/gitlab/services/mr_review_engine.py b/apps/backend/runners/gitlab/services/mr_review_engine.py index 0a8d518a..11a3a00e 100644 --- a/apps/backend/runners/gitlab/services/mr_review_engine.py +++ b/apps/backend/runners/gitlab/services/mr_review_engine.py @@ -168,6 +168,7 @@ Provide your review in the following JSON format: Tuple of (findings, verdict, summary, blockers) """ from core.client import create_client + from phase_config import get_model_betas, resolve_model_id self._report_progress( "analyzing", 30, "Running AI analysis...", mr_iid=context.mr_iid @@ -229,11 +230,16 @@ Provide your review in the following JSON format: project_root = self.project_dir.parent.parent # Create the client + model_shorthand = self.config.model or "sonnet" + model = resolve_model_id(model_shorthand) + betas = get_model_betas(model_shorthand) client = create_client( project_dir=project_root, spec_dir=self.gitlab_dir, - model=self.config.model, + model=model, agent_type="pr_reviewer", # Read-only - no bash, no edits + betas=betas, + fast_mode=self.config.fast_mode, ) result_text = "" diff --git a/apps/backend/runners/ideation_runner.py b/apps/backend/runners/ideation_runner.py index 76773c04..fd0b1e8a 100644 --- a/apps/backend/runners/ideation_runner.py +++ b/apps/backend/runners/ideation_runner.py @@ -109,7 +109,7 @@ def main(): "--thinking-level", type=str, default="medium", - choices=["none", "low", "medium", "high", "ultrathink"], + choices=["low", "medium", "high"], help="Thinking level for extended reasoning (default: medium)", ) parser.add_argument( @@ -122,6 +122,11 @@ def main(): action="store_true", help="Append new ideas to existing session instead of replacing", ) + parser.add_argument( + "--fast-mode", + action="store_true", + help="Enable Fast Mode for faster Opus 4.6 output", + ) args = parser.parse_args() @@ -152,6 +157,7 @@ def main(): thinking_level=args.thinking_level, refresh=args.refresh, append=args.append, + fast_mode=args.fast_mode, ) try: diff --git a/apps/backend/runners/insights_runner.py b/apps/backend/runners/insights_runner.py index 7a1dc408..4992dfa8 100644 --- a/apps/backend/runners/insights_runner.py +++ b/apps/backend/runners/insights_runner.py @@ -190,7 +190,6 @@ Current question: {message}""" ) try: - # Build options dict - only include max_thinking_tokens if not None options_kwargs = { "model": resolve_model_id(model), # Resolve via API Profile if configured "system_prompt": system_prompt, @@ -199,9 +198,7 @@ Current question: {message}""" "cwd": str(project_path), } - # Only add thinking tokens if the thinking level is not "none" - if max_thinking_tokens is not None: - options_kwargs["max_thinking_tokens"] = max_thinking_tokens + options_kwargs["max_thinking_tokens"] = max_thinking_tokens # Create Claude SDK client with appropriate settings for insights client = ClaudeSDKClient(options=ClaudeAgentOptions(**options_kwargs)) @@ -356,7 +353,7 @@ def main(): parser.add_argument( "--thinking-level", default="medium", - choices=["none", "low", "medium", "high", "ultrathink"], + choices=["low", "medium", "high"], help="Thinking level for extended reasoning (default: medium)", ) args = parser.parse_args() diff --git a/apps/backend/runners/roadmap_runner.py b/apps/backend/runners/roadmap_runner.py index cc76ea84..5c15f0df 100644 --- a/apps/backend/runners/roadmap_runner.py +++ b/apps/backend/runners/roadmap_runner.py @@ -71,7 +71,7 @@ def main(): "--thinking-level", type=str, default="medium", - choices=["none", "low", "medium", "high", "ultrathink"], + choices=["low", "medium", "high"], help="Thinking level for extended reasoning (default: medium)", ) parser.add_argument( diff --git a/apps/backend/runners/spec_runner.py b/apps/backend/runners/spec_runner.py index a8d95c7e..a149fd4c 100644 --- a/apps/backend/runners/spec_runner.py +++ b/apps/backend/runners/spec_runner.py @@ -185,8 +185,8 @@ Examples: "--thinking-level", type=str, default="medium", - choices=["none", "low", "medium", "high", "ultrathink"], - help="Thinking level for extended thinking (none, low, medium, high, ultrathink)", + choices=["low", "medium", "high"], + help="Thinking level for extended thinking (low, medium, high)", ) parser.add_argument( "--no-ai-assessment", diff --git a/apps/backend/spec/pipeline/agent_runner.py b/apps/backend/spec/pipeline/agent_runner.py index 42c380d5..6500514c 100644 --- a/apps/backend/spec/pipeline/agent_runner.py +++ b/apps/backend/spec/pipeline/agent_runner.py @@ -54,6 +54,7 @@ class AgentRunner: additional_context: str = "", interactive: bool = False, thinking_budget: int | None = None, + thinking_level: str = "medium", prior_phase_summaries: str | None = None, ) -> tuple[bool, str]: """Run an agent with the given prompt. @@ -63,6 +64,7 @@ class AgentRunner: additional_context: Additional context to add to the prompt interactive: Whether to run in interactive mode thinking_budget: Token budget for extended thinking (None = disabled) + thinking_level: Thinking level string (low, medium, high) prior_phase_summaries: Summaries from previous phases for context Returns: @@ -121,12 +123,27 @@ class AgentRunner: ) # Lazy import to avoid circular import with core.client from core.client import create_client + from phase_config import ( + get_fast_mode, + get_model_betas, + get_thinking_kwargs_for_model, + resolve_model_id, + ) + + betas = get_model_betas(self.model) + fast_mode = get_fast_mode(self.spec_dir) + resolved_model = resolve_model_id(self.model) + thinking_kwargs = get_thinking_kwargs_for_model( + resolved_model, thinking_level or "medium" + ) client = create_client( self.project_dir, self.spec_dir, - self.model, - max_thinking_tokens=thinking_budget, + resolved_model, + betas=betas, + fast_mode=fast_mode, + **thinking_kwargs, ) current_tool = None diff --git a/apps/backend/spec/pipeline/orchestrator.py b/apps/backend/spec/pipeline/orchestrator.py index 5f3ba923..e8dc040e 100644 --- a/apps/backend/spec/pipeline/orchestrator.py +++ b/apps/backend/spec/pipeline/orchestrator.py @@ -71,7 +71,7 @@ class SpecOrchestrator: spec_name: Optional spec name (for existing specs) spec_dir: Optional existing spec directory (for UI integration) model: The model to use for agent execution - thinking_level: Thinking level (none, low, medium, high, ultrathink) + thinking_level: Thinking level (low, medium, high) complexity_override: Force a specific complexity level use_ai_assessment: Whether to use AI for complexity assessment """ @@ -158,6 +158,7 @@ class SpecOrchestrator: additional_context, interactive, thinking_budget=thinking_budget, + thinking_level=self.thinking_level, prior_phase_summaries=prior_summaries if prior_summaries else None, ) diff --git a/apps/frontend/src/main/agent/agent-queue.ts b/apps/frontend/src/main/agent/agent-queue.ts index 8ac65ca4..2c75a184 100644 --- a/apps/frontend/src/main/agent/agent-queue.ts +++ b/apps/frontend/src/main/agent/agent-queue.ts @@ -19,6 +19,7 @@ import { transformIdeaFromSnakeCase, transformSessionFromSnakeCase } from '../ip import { transformRoadmapFromSnakeCase } from '../ipc-handlers/roadmap/transformers'; import type { RawIdea } from '../ipc-handlers/ideation/types'; import { getPathDelimiter } from '../platform'; +import { readSettingsFile } from '../settings-utils'; import { debounce } from '../utils/debounce'; import { writeFileWithRetry } from '../utils/atomic-file'; @@ -312,6 +313,16 @@ export class AgentQueueManager { args.push('--thinking-level', config.thinkingLevel); } + // Pass Fast Mode flag from app settings + try { + const settings = readSettingsFile(); + if (settings?.fastMode) { + args.push('--fast-mode'); + } + } catch { + // Settings read failure is non-fatal + } + debugLog('[Agent Queue] Spawning ideation process with args:', args); // Use projectId as taskId for ideation operations diff --git a/apps/frontend/src/main/agent/types.ts b/apps/frontend/src/main/agent/types.ts index 259a16b5..3a3a0097 100644 --- a/apps/frontend/src/main/agent/types.ts +++ b/apps/frontend/src/main/agent/types.ts @@ -41,7 +41,7 @@ export interface AgentManagerEvents { export interface RoadmapConfig { model?: string; // Model shorthand (opus, sonnet, haiku) - thinkingLevel?: string; // Thinking level (none, low, medium, high, ultrathink) + thinkingLevel?: string; // Thinking level (low, medium, high) } export interface TaskExecutionOptions { @@ -57,20 +57,20 @@ export interface SpecCreationMetadata { // Auto profile - phase-based model and thinking configuration isAutoProfile?: boolean; phaseModels?: { - spec: 'haiku' | 'sonnet' | 'opus'; - planning: 'haiku' | 'sonnet' | 'opus'; - coding: 'haiku' | 'sonnet' | 'opus'; - qa: 'haiku' | 'sonnet' | 'opus'; + spec: 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; + planning: 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; + coding: 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; + qa: 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; }; phaseThinking?: { - spec: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; - planning: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; - coding: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; - qa: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + spec: 'low' | 'medium' | 'high'; + planning: 'low' | 'medium' | 'high'; + coding: 'low' | 'medium' | 'high'; + qa: 'low' | 'medium' | 'high'; }; // Non-auto profile - single model and thinking level - model?: 'haiku' | 'sonnet' | 'opus'; - thinkingLevel?: 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; + model?: 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; + thinkingLevel?: 'low' | 'medium' | 'high'; // Workspace mode - whether to use worktree isolation useWorktree?: boolean; // If false, use --direct mode (no worktree isolation) useLocalBranch?: boolean; // If true, use local branch directly instead of preferring origin/branch diff --git a/apps/frontend/src/main/ipc-handlers/env-handlers.ts b/apps/frontend/src/main/ipc-handlers/env-handlers.ts index 021656a5..b8daa83d 100644 --- a/apps/frontend/src/main/ipc-handlers/env-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/env-handlers.ts @@ -235,7 +235,7 @@ export function registerEnvHandlers( CLAUDE_CODE_OAUTH_TOKEN=${existingVars['CLAUDE_CODE_OAUTH_TOKEN'] || ''} # Model override (OPTIONAL) -${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-5-20251101'} +${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-6'} # ============================================================================= # LINEAR INTEGRATION (OPTIONAL) diff --git a/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts b/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts index 89dc83f5..6000b3bf 100644 --- a/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/github/pr-handlers.ts @@ -1446,15 +1446,17 @@ async function runPRReview( ); const { model, thinkingLevel } = getGitHubPRSettings(); + const settings = readSettingsFile(); + const fastMode = !!settings?.fastMode; const args = buildRunnerArgs( getRunnerPath(backendPath), project.path, "review-pr", [prNumber.toString()], - { model, thinkingLevel } + { model, thinkingLevel, fastMode } ); - debugLog("Spawning PR review process", { args, model, thinkingLevel }); + debugLog("Spawning PR review process", { args, model, thinkingLevel, fastMode }); // Create log collector for this review const config = getGitHubConfig(project); @@ -2891,15 +2893,17 @@ export function registerPRHandlers(getMainWindow: () => BrowserWindow | null): v ciWaitAbortControllers.delete(reviewKey); const { model, thinkingLevel } = getGitHubPRSettings(); + const followupSettings = readSettingsFile(); + const followupFastMode = !!followupSettings?.fastMode; const args = buildRunnerArgs( getRunnerPath(backendPath), project.path, "followup-review-pr", [prNumber.toString()], - { model, thinkingLevel } + { model, thinkingLevel, fastMode: followupFastMode } ); - debugLog("Spawning follow-up review process", { args, model, thinkingLevel }); + debugLog("Spawning follow-up review process", { args, model, thinkingLevel, fastMode: followupFastMode }); // Create log collector for this follow-up review (config already declared above) const repo = config?.repo || project.name || "unknown"; diff --git a/apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts b/apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts index a352702a..0468e76c 100644 --- a/apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts +++ b/apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts @@ -736,6 +736,7 @@ export function buildRunnerArgs( options?: { model?: string; thinkingLevel?: string; + fastMode?: boolean; } ): string[] { const args = [runnerPath, '--project', projectPath]; @@ -748,6 +749,10 @@ export function buildRunnerArgs( args.push('--thinking-level', options.thinkingLevel); } + if (options?.fastMode) { + args.push('--fast-mode'); + } + args.push(command); args.push(...additionalArgs); diff --git a/apps/frontend/src/main/ipc-handlers/sections/integration-section.txt b/apps/frontend/src/main/ipc-handlers/sections/integration-section.txt index ff5bb4bd..5b2378ba 100644 --- a/apps/frontend/src/main/ipc-handlers/sections/integration-section.txt +++ b/apps/frontend/src/main/ipc-handlers/sections/integration-section.txt @@ -98,7 +98,7 @@ CLAUDE_CODE_OAUTH_TOKEN=${existingVars['CLAUDE_CODE_OAUTH_TOKEN'] || ''} # Model override (OPTIONAL) -${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-5-20251101'} +${existingVars['AUTO_BUILD_MODEL'] ? `AUTO_BUILD_MODEL=${existingVars['AUTO_BUILD_MODEL']}` : '# AUTO_BUILD_MODEL=claude-opus-4-6'} # ============================================================================= # LINEAR INTEGRATION (OPTIONAL) diff --git a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx index b597eab1..9f7bed5f 100644 --- a/apps/frontend/src/renderer/components/TaskCreationWizard.tsx +++ b/apps/frontend/src/renderer/components/TaskCreationWizard.tsx @@ -442,6 +442,7 @@ export function TaskCreationWizard({ // Set useLocalBranch when user explicitly selects a local branch // This preserves gitignored files (.env, configs) by not switching to origin if (isSelectedBranchLocal) metadata.useLocalBranch = true; + if (settings.fastMode) metadata.fastMode = true; const task = await createTask(projectId, title.trim(), description.trim(), metadata); if (task) { diff --git a/apps/frontend/src/renderer/components/TaskEditDialog.tsx b/apps/frontend/src/renderer/components/TaskEditDialog.tsx index 59e83f39..6e008ae6 100644 --- a/apps/frontend/src/renderer/components/TaskEditDialog.tsx +++ b/apps/frontend/src/renderer/components/TaskEditDialog.tsx @@ -232,6 +232,8 @@ export function TaskEditDialog({ task, open, onOpenChange, onSaved }: TaskEditDi // Always set attachedImages to persist removal when all images are deleted metadataUpdates.attachedImages = images.length > 0 ? images : []; metadataUpdates.requireReviewBeforeCoding = requireReviewBeforeCoding; + // Preserve fastMode from original task metadata (set at creation from settings) + if (task.metadata?.fastMode) metadataUpdates.fastMode = true; const success = await persistUpdateTask(task.id, { title: trimmedTitle, diff --git a/apps/frontend/src/renderer/components/__tests__/AgentTools.test.tsx b/apps/frontend/src/renderer/components/__tests__/AgentTools.test.tsx index 679b7a46..9bc6b519 100644 --- a/apps/frontend/src/renderer/components/__tests__/AgentTools.test.tsx +++ b/apps/frontend/src/renderer/components/__tests__/AgentTools.test.tsx @@ -69,7 +69,7 @@ describe('AgentTools - Agent Profile Resolution', () => { const phaseThinking = profile?.phaseThinking; expect(phaseThinking).toBeDefined(); - expect(phaseThinking?.spec).toBe('ultrathink'); + expect(phaseThinking?.spec).toBe('high'); expect(phaseThinking?.planning).toBe('high'); expect(phaseThinking?.coding).toBe('low'); expect(phaseThinking?.qa).toBe('low'); @@ -174,7 +174,7 @@ describe('AgentTools - Agent Profile Resolution', () => { resolvedSettings ); expect(specAgent.model).toBe('opus'); - expect(specAgent.thinking).toBe('ultrathink'); + expect(specAgent.thinking).toBe('high'); // Planning phase agent const planningAgent = resolveAgentSettings( diff --git a/apps/frontend/src/renderer/components/settings/AgentProfileSettings.tsx b/apps/frontend/src/renderer/components/settings/AgentProfileSettings.tsx index 891c412e..4ff91c80 100644 --- a/apps/frontend/src/renderer/components/settings/AgentProfileSettings.tsx +++ b/apps/frontend/src/renderer/components/settings/AgentProfileSettings.tsx @@ -1,18 +1,20 @@ import { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Brain, Scale, Zap, Check, Sparkles, ChevronDown, ChevronUp, RotateCcw, Settings2 } from 'lucide-react'; +import { Brain, Scale, Zap, Check, Sparkles, ChevronDown, ChevronUp, RotateCcw, Settings2, Info } from 'lucide-react'; import { cn } from '../../lib/utils'; import { DEFAULT_AGENT_PROFILES, AVAILABLE_MODELS, THINKING_LEVELS, DEFAULT_PHASE_MODELS, - DEFAULT_PHASE_THINKING + DEFAULT_PHASE_THINKING, + FAST_MODE_MODELS } from '../../../shared/constants'; import { useSettingsStore, saveSettings } from '../../stores/settings-store'; import { SettingsSection } from './SettingsSection'; import { Label } from '../ui/label'; import { Button } from '../ui/button'; +import { Switch } from '../ui/switch'; import { Select, SelectContent, @@ -103,6 +105,17 @@ export function AgentProfileSettings() { await saveSettings({ customPhaseThinking: newPhaseThinking }); }; + // Show Fast Mode toggle when any phase uses an Opus model + const showFastModeToggle = useMemo(() => { + return PHASE_KEYS.some(phase => + FAST_MODE_MODELS.includes(currentPhaseModels[phase]) + ); + }, [currentPhaseModels]); + + const handleFastModeToggle = async (checked: boolean) => { + await saveSettings({ fastMode: checked }); + }; + const handleResetToProfileDefaults = async () => { // Reset to the selected profile's defaults await saveSettings({ @@ -319,6 +332,37 @@ export function AgentProfileSettings() { )} + {/* Fast Mode Toggle - shown when any phase uses an Opus model */} + {showFastModeToggle && ( +
+ {t('agentProfile.fastMode.description')} +
++ {t('agentProfile.fastMode.extraUsageNotice')} +
+