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')} +

+
+
+ )} + ); diff --git a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx index a1f8aa62..32f60b7a 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskLogs.tsx @@ -66,17 +66,16 @@ const LOG_PHASE_TO_CONFIG_PHASE: Record = // Short labels for models const MODEL_SHORT_LABELS: Record = { opus: 'Opus', + 'opus-1m': 'Opus (1M)', sonnet: 'Sonnet', haiku: 'Haiku' }; // Short labels for thinking levels const THINKING_SHORT_LABELS: Record = { - none: 'None', low: 'Low', medium: 'Med', - high: 'High', - ultrathink: 'Ultra' + high: 'High' }; // Helper to get model and thinking info for a log phase diff --git a/apps/frontend/src/shared/constants/models.ts b/apps/frontend/src/shared/constants/models.ts index 72e76a0c..be13e05a 100644 --- a/apps/frontend/src/shared/constants/models.ts +++ b/apps/frontend/src/shared/constants/models.ts @@ -10,25 +10,26 @@ import type { AgentProfile, PhaseModelConfig, FeatureModelConfig, FeatureThinkin // ============================================ export const AVAILABLE_MODELS = [ - { value: 'opus', label: 'Claude Opus 4.5' }, + { value: 'opus', label: 'Claude Opus 4.6' }, + { value: 'opus-1m', label: 'Claude Opus 4.6 (1M)' }, { value: 'sonnet', label: 'Claude Sonnet 4.5' }, { value: 'haiku', label: 'Claude Haiku 4.5' } ] as const; // Maps model shorthand to actual Claude model IDs +// Values must match apps/backend/phase_config.py MODEL_ID_MAP export const MODEL_ID_MAP: Record = { - 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' } as const; -// Maps thinking levels to budget tokens (null = no extended thinking) -export const THINKING_BUDGET_MAP: Record = { - none: null, +// Maps thinking levels to budget tokens +export const THINKING_BUDGET_MAP: Record = { low: 1024, medium: 4096, - high: 16384, - ultrathink: 63999 // Maximum reasoning depth (API requires max_tokens >= budget + 1, so 63999 + 1 = 64000 limit) + high: 16384 } as const; // ============================================ @@ -37,11 +38,9 @@ export const THINKING_BUDGET_MAP: Record = { // Thinking levels for Claude model (budget token allocation) export const THINKING_LEVELS = [ - { value: 'none', label: 'None', description: 'No extended thinking' }, { value: 'low', label: 'Low', description: 'Brief consideration' }, { value: 'medium', label: 'Medium', description: 'Moderate analysis' }, - { value: 'high', label: 'High', description: 'Deep thinking' }, - { value: 'ultrathink', label: 'Ultra Think', description: 'Maximum reasoning depth' } + { value: 'high', label: 'High', description: 'Deep thinking' } ] as const; // ============================================ @@ -60,13 +59,13 @@ export const AUTO_PHASE_MODELS: PhaseModelConfig = { }; export const AUTO_PHASE_THINKING: import('../types/settings').PhaseThinkingConfig = { - spec: 'ultrathink', // Deep thinking for comprehensive spec creation + spec: 'high', // Deep thinking for comprehensive spec creation planning: 'high', // High thinking for planning complex features coding: 'low', // Faster coding iterations qa: 'low' // Efficient QA review }; -// Complex Tasks - Opus with ultrathink across all phases +// Complex Tasks - Opus with high thinking across all phases export const COMPLEX_PHASE_MODELS: PhaseModelConfig = { spec: 'opus', planning: 'opus', @@ -75,10 +74,10 @@ export const COMPLEX_PHASE_MODELS: PhaseModelConfig = { }; export const COMPLEX_PHASE_THINKING: import('../types/settings').PhaseThinkingConfig = { - spec: 'ultrathink', - planning: 'ultrathink', - coding: 'ultrathink', - qa: 'ultrathink' + spec: 'high', + planning: 'high', + coding: 'high', + qa: 'high' }; // Balanced - Sonnet with medium thinking across all phases @@ -167,7 +166,7 @@ export const DEFAULT_AGENT_PROFILES: AgentProfile[] = [ name: 'Complex Tasks', description: 'For intricate, multi-step implementations requiring deep analysis', model: 'opus', - thinkingLevel: 'ultrathink', + thinkingLevel: 'high', icon: 'Brain', phaseModels: COMPLEX_PHASE_MODELS, phaseThinking: COMPLEX_PHASE_THINKING @@ -194,6 +193,9 @@ export const DEFAULT_AGENT_PROFILES: AgentProfile[] = [ } ]; +// Models that support Fast Mode (same model, faster API routing, higher cost) +export const FAST_MODE_MODELS: readonly string[] = ['opus', 'opus-1m'] as const; + // ============================================ // Memory Backends // ============================================ diff --git a/apps/frontend/src/shared/i18n/locales/en/settings.json b/apps/frontend/src/shared/i18n/locales/en/settings.json index c1a977df..6a410007 100644 --- a/apps/frontend/src/shared/i18n/locales/en/settings.json +++ b/apps/frontend/src/shared/i18n/locales/en/settings.json @@ -107,7 +107,8 @@ "defaultPlaceholder": "e.g., claude-sonnet-4-5-20250929", "haikuPlaceholder": "e.g., claude-haiku-4-5-20251001", "sonnetPlaceholder": "e.g., claude-sonnet-4-5-20250929", - "opusPlaceholder": "e.g., claude-opus-4-5-20251101" + "opusPlaceholder": "e.g., claude-opus-4-6", + "opus1mPlaceholder": "e.g., claude-opus-4-6 (1M context)" }, "empty": { "title": "No API profiles configured", @@ -399,6 +400,11 @@ "resetToProfileDefaults": "Reset to {{profile}} defaults", "customized": "Customized", "phaseConfigNote": "These settings will be used as defaults when creating new tasks with this profile. You can override them per-task in the task creation wizard.", + "fastMode": { + "label": "Fast Mode", + "description": "Same Opus 4.6 model with faster output. Higher cost per token.", + "extraUsageNotice": "Requires \"extra usage\" enabled on your Claude subscription. Falls back to standard speed automatically if unavailable." + }, "phases": { "spec": { "label": "Spec Creation", diff --git a/apps/frontend/src/shared/i18n/locales/fr/settings.json b/apps/frontend/src/shared/i18n/locales/fr/settings.json index 056bde35..b1e44556 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/settings.json +++ b/apps/frontend/src/shared/i18n/locales/fr/settings.json @@ -107,7 +107,8 @@ "defaultPlaceholder": "ex. : claude-sonnet-4-5-20250929", "haikuPlaceholder": "ex. : claude-haiku-4-5-20251001", "sonnetPlaceholder": "ex. : claude-sonnet-4-5-20250929", - "opusPlaceholder": "ex. : claude-opus-4-5-20251101" + "opusPlaceholder": "ex. : claude-opus-4-6", + "opus1mPlaceholder": "ex. : claude-opus-4-6 (contexte 1M)" }, "empty": { "title": "Aucun profil API configuré", @@ -399,6 +400,11 @@ "resetToProfileDefaults": "Réinitialiser aux défauts de {{profile}}", "customized": "Personnalisé", "phaseConfigNote": "Ces paramètres seront utilisés par défaut lors de la création de nouvelles tâches avec ce profil. Vous pouvez les modifier par tâche dans l'assistant de création.", + "fastMode": { + "label": "Mode Rapide", + "description": "Même modèle Opus 4.6 avec une sortie plus rapide. Coût plus élevé par token.", + "extraUsageNotice": "Nécessite « utilisation supplémentaire » activée sur votre abonnement Claude. Revient automatiquement à la vitesse standard si indisponible." + }, "phases": { "spec": { "label": "Création de spec", diff --git a/apps/frontend/src/shared/types/insights.ts b/apps/frontend/src/shared/types/insights.ts index ba5d83a5..fefbf04c 100644 --- a/apps/frontend/src/shared/types/insights.ts +++ b/apps/frontend/src/shared/types/insights.ts @@ -27,7 +27,7 @@ export interface IdeationConfig { maxIdeasPerType: number; append?: boolean; // If true, append to existing ideas instead of replacing 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 IdeaBase { diff --git a/apps/frontend/src/shared/types/settings.ts b/apps/frontend/src/shared/types/settings.ts index a8558478..7ede6b94 100644 --- a/apps/frontend/src/shared/types/settings.ts +++ b/apps/frontend/src/shared/types/settings.ts @@ -158,10 +158,10 @@ export interface ColorThemeDefinition { } // Thinking level for Claude model (budget token allocation) -export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink'; +export type ThinkingLevel = 'low' | 'medium' | 'high'; // Model type shorthand -export type ModelTypeShort = 'haiku' | 'sonnet' | 'opus'; +export type ModelTypeShort = 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; // Phase-based model configuration for Auto profile // Each phase can use a different model optimized for that task type @@ -293,6 +293,8 @@ export interface AppSettings { seenVersionWarnings?: string[]; // Sidebar collapsed state (icons only when true) sidebarCollapsed?: boolean; + /** Fast Mode — faster Opus 4.6 output, requires extra usage on Claude subscription */ + fastMode?: boolean; } // Auto-Claude Source Environment Configuration (for auto-claude repo .env) diff --git a/apps/frontend/src/shared/types/task.ts b/apps/frontend/src/shared/types/task.ts index 742c3227..ec0c8715 100644 --- a/apps/frontend/src/shared/types/task.ts +++ b/apps/frontend/src/shared/types/task.ts @@ -163,7 +163,7 @@ export type TaskImpact = 'low' | 'medium' | 'high' | 'critical'; export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'; // Re-export ThinkingLevel (defined in settings.ts) for convenience export type { ThinkingLevel }; -export type ModelType = 'haiku' | 'sonnet' | 'opus'; +export type ModelType = 'haiku' | 'sonnet' | 'opus' | 'opus-1m'; export type TaskCategory = | 'feature' | 'bug_fix' @@ -227,11 +227,12 @@ export interface TaskMetadata { // Agent configuration (from agent profile or manual selection) model?: ModelType; // Claude model to use (haiku, sonnet, opus) - used when not auto profile - thinkingLevel?: ThinkingLevel; // Thinking budget level (none, low, medium, high, ultrathink) + thinkingLevel?: ThinkingLevel; // Thinking budget level (low, medium, high) // Auto profile - per-phase model configuration isAutoProfile?: boolean; // True when using Auto (Optimized) profile phaseModels?: PhaseModelConfig; // Per-phase model configuration phaseThinking?: PhaseThinkingConfig; // Per-phase thinking configuration + fastMode?: boolean; // Fast Mode — faster Opus 4.6 output, higher cost per token // Git/Worktree configuration baseBranch?: string; // Override base branch for this task's worktree diff --git a/guides/CLI-USAGE.md b/guides/CLI-USAGE.md index dd41f507..261ecf29 100644 --- a/guides/CLI-USAGE.md +++ b/guides/CLI-USAGE.md @@ -193,7 +193,7 @@ cp .env.example .env | Variable | Required | Description | |----------|----------|-------------| | `CLAUDE_CODE_OAUTH_TOKEN` | Yes | OAuth token from `claude setup-token` | -| `AUTO_BUILD_MODEL` | No | Model override (default: claude-opus-4-5-20251101) | +| `AUTO_BUILD_MODEL` | No | Model override (default: claude-opus-4-6) | | `DEFAULT_BRANCH` | No | Base branch for worktrees (auto-detects main/master) | | `DEBUG` | No | Enable debug logging (default: false) | diff --git a/tests/test_agent_configs.py b/tests/test_agent_configs.py index d24d30bf..8e9e2832 100644 --- a/tests/test_agent_configs.py +++ b/tests/test_agent_configs.py @@ -190,12 +190,12 @@ class TestGetRequiredMcpServers: class TestGetDefaultThinkingLevel: """Tests for get_default_thinking_level() function.""" - def test_returns_none_for_coder(self): - """Coder should return 'none' thinking level.""" + def test_returns_low_for_coder(self): + """Coder should return 'low' thinking level.""" from agents.tools_pkg.models import get_default_thinking_level result = get_default_thinking_level("coder") - assert result == "none" + assert result == "low" def test_returns_high_for_qa_reviewer(self): """QA reviewer should return 'high' thinking level.""" @@ -204,12 +204,12 @@ class TestGetDefaultThinkingLevel: result = get_default_thinking_level("qa_reviewer") assert result == "high" - def test_returns_ultrathink_for_spec_critic(self): - """Spec critic should return 'ultrathink' level.""" + def test_returns_high_for_spec_critic(self): + """Spec critic should return 'high' thinking level.""" from agents.tools_pkg.models import get_default_thinking_level result = get_default_thinking_level("spec_critic") - assert result == "ultrathink" + assert result == "high" def test_can_convert_to_budget_via_phase_config(self): """Verify thinking level can be converted to budget using phase_config.""" diff --git a/tests/test_fast_mode.py b/tests/test_fast_mode.py new file mode 100644 index 00000000..4c26d5fa --- /dev/null +++ b/tests/test_fast_mode.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Tests for Fast Mode Configuration +=================================== + +Tests the get_fast_mode() function from phase_config which reads +the fastMode flag from task_metadata.json. +""" + +import json +import sys +from pathlib import Path + +import pytest + +# Add backend to path +sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend")) + +from phase_config import get_fast_mode + + +class TestGetFastMode: + """Tests for get_fast_mode() function.""" + + def test_fast_mode_enabled(self, tmp_path): + """Returns True when fastMode is true in task_metadata.json.""" + metadata = {"fastMode": True, "model": "opus"} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is True + + def test_fast_mode_disabled(self, tmp_path): + """Returns False when fastMode is false in task_metadata.json.""" + metadata = {"fastMode": False, "model": "opus"} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is False + + def test_fast_mode_missing_field(self, tmp_path): + """Returns False when fastMode field is absent from task_metadata.json.""" + metadata = {"model": "opus", "thinkingLevel": "high"} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is False + + def test_fast_mode_no_metadata(self, tmp_path): + """Returns False when task_metadata.json doesn't exist.""" + assert get_fast_mode(tmp_path) is False + + def test_fast_mode_truthy_value(self, tmp_path): + """Returns True for truthy non-boolean values (e.g., 1).""" + metadata = {"fastMode": 1} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is True + + def test_fast_mode_falsy_value(self, tmp_path): + """Returns False for falsy non-boolean values (e.g., 0, null).""" + metadata = {"fastMode": 0} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is False + + def test_fast_mode_invalid_json(self, tmp_path): + """Returns False when task_metadata.json contains invalid JSON.""" + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text("not valid json {{{", encoding="utf-8") + + assert get_fast_mode(tmp_path) is False diff --git a/tests/test_integration_phase4.py b/tests/test_integration_phase4.py index d34b8896..694442ae 100644 --- a/tests/test_integration_phase4.py +++ b/tests/test_integration_phase4.py @@ -62,7 +62,14 @@ io_utils_module = importlib.util.module_from_spec(io_utils_spec) sys.modules["services.io_utils"] = io_utils_module io_utils_spec.loader.exec_module(io_utils_module) -# Load pydantic_models +# Load pydantic_models (mock pydantic if not installed in test env) +_pydantic_was_mocked = False +try: + import pydantic # noqa: F401 +except ImportError: + pydantic_mock = MagicMock() + sys.modules["pydantic"] = pydantic_mock + _pydantic_was_mocked = True pydantic_models_spec = importlib.util.spec_from_file_location( "pydantic_models", backend_path / "runners" / "github" / "services" / "pydantic_models.py", @@ -71,6 +78,9 @@ pydantic_models_module = importlib.util.module_from_spec(pydantic_models_spec) sys.modules["services.pydantic_models"] = pydantic_models_module pydantic_models_spec.loader.exec_module(pydantic_models_module) AgentAgreement = pydantic_models_module.AgentAgreement +# Restore sys.modules to avoid leaking the mock to other tests +if _pydantic_was_mocked: + del sys.modules["pydantic"] # Load agent_utils (shared utility for working directory injection) agent_utils_spec = importlib.util.spec_from_file_location( diff --git a/tests/test_model_resolution.py b/tests/test_model_resolution.py index f3903299..3fe023df 100644 --- a/tests/test_model_resolution.py +++ b/tests/test_model_resolution.py @@ -17,6 +17,7 @@ while still verifying the critical implementation patterns that prevent regressi of the hardcoded fallback bug (ACS-294). """ +import json import os import sys from collections.abc import Generator @@ -28,7 +29,17 @@ import pytest # Add backend to path sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend")) -from phase_config import MODEL_ID_MAP, resolve_model_id +from phase_config import ( + ADAPTIVE_THINKING_MODELS, + MODEL_BETAS_MAP, + MODEL_ID_MAP, + get_fast_mode, + get_model_betas, + get_phase_model_betas, + get_thinking_kwargs_for_model, + is_adaptive_model, + resolve_model_id, +) # Common paths - extracted to avoid duplication and ease maintenance GITHUB_RUNNER_DIR = ( @@ -322,3 +333,224 @@ class TestParallelReviewerImportResolution: # Verify resolve_model_id is imported and used assert "resolve_model_id" in orchestrator_content assert "resolve_model_id" in followup_content + + +class TestModelBetasMap: + """Tests for MODEL_BETAS_MAP configuration.""" + + def test_model_betas_map_exists(self): + """MODEL_BETAS_MAP is a dict with expected entries.""" + assert isinstance(MODEL_BETAS_MAP, dict) + + def test_opus_1m_has_context_beta(self): + """opus-1m entry has the 1M context window beta header.""" + assert "opus-1m" in MODEL_BETAS_MAP + assert MODEL_BETAS_MAP["opus-1m"] == ["context-1m-2025-08-07"] + + def test_regular_models_not_in_betas_map(self): + """Regular model shorthands (opus, sonnet, haiku) are not in MODEL_BETAS_MAP.""" + assert "opus" not in MODEL_BETAS_MAP + assert "sonnet" not in MODEL_BETAS_MAP + assert "haiku" not in MODEL_BETAS_MAP + + +class TestGetModelBetas: + """Tests for get_model_betas() function.""" + + def test_opus_1m_returns_context_beta(self): + """get_model_betas('opus-1m') returns the 1M context beta header.""" + result = get_model_betas("opus-1m") + assert result == ["context-1m-2025-08-07"] + + def test_opus_returns_empty_list(self): + """get_model_betas('opus') returns empty list (no betas needed).""" + result = get_model_betas("opus") + assert result == [] + + def test_sonnet_returns_empty_list(self): + """get_model_betas('sonnet') returns empty list.""" + result = get_model_betas("sonnet") + assert result == [] + + def test_unknown_returns_empty_list(self): + """get_model_betas('unknown') returns empty list.""" + result = get_model_betas("unknown") + assert result == [] + + +class TestOpus1mModelResolution: + """Tests for opus-1m model ID resolution.""" + + def test_opus_1m_resolves_to_opus_model_id(self, clean_env): + """resolve_model_id('opus-1m') returns the same model ID as regular opus.""" + result = resolve_model_id("opus-1m") + assert result == "claude-opus-4-6" + + def test_opus_resolves_to_opus_model_id(self, clean_env): + """resolve_model_id('opus') returns claude-opus-4-6.""" + result = resolve_model_id("opus") + assert result == "claude-opus-4-6" + + def test_opus_1m_and_opus_resolve_to_same_id(self, clean_env): + """opus-1m and opus both resolve to the same underlying model ID.""" + assert resolve_model_id("opus-1m") == resolve_model_id("opus") + + def test_opus_1m_respects_env_override(self): + """opus-1m respects ANTHROPIC_DEFAULT_OPUS_MODEL environment variable.""" + custom_model = "custom-opus-model" + with patch.dict(os.environ, {"ANTHROPIC_DEFAULT_OPUS_MODEL": custom_model}): + result = resolve_model_id("opus-1m") + assert result == custom_model + + +class TestGetPhaseModelBetas: + """Tests for get_phase_model_betas() function.""" + + def test_cli_model_opus_1m_returns_betas(self, tmp_path): + """get_phase_model_betas with cli_model='opus-1m' returns the betas.""" + result = get_phase_model_betas(tmp_path, "coding", cli_model="opus-1m") + assert result == ["context-1m-2025-08-07"] + + def test_cli_model_opus_returns_empty(self, tmp_path): + """get_phase_model_betas with cli_model='opus' returns empty list.""" + result = get_phase_model_betas(tmp_path, "coding", cli_model="opus") + assert result == [] + + def test_cli_model_sonnet_returns_empty(self, tmp_path): + """get_phase_model_betas with cli_model='sonnet' returns empty list.""" + result = get_phase_model_betas(tmp_path, "coding", cli_model="sonnet") + assert result == [] + + def test_metadata_with_opus_1m_returns_betas(self, tmp_path): + """get_phase_model_betas reads opus-1m from task_metadata and returns betas.""" + metadata = {"model": "opus-1m"} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + result = get_phase_model_betas(tmp_path, "coding") + assert result == ["context-1m-2025-08-07"] + + def test_metadata_auto_profile_with_opus_1m_returns_betas(self, tmp_path): + """get_phase_model_betas reads opus-1m from auto profile phase config.""" + metadata = { + "isAutoProfile": True, + "phaseModels": {"coding": "opus-1m", "qa": "sonnet"}, + } + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + result = get_phase_model_betas(tmp_path, "coding") + assert result == ["context-1m-2025-08-07"] + + # QA phase should have no betas (sonnet) + result_qa = get_phase_model_betas(tmp_path, "qa") + assert result_qa == [] + + def test_no_metadata_returns_empty(self, tmp_path): + """get_phase_model_betas with no metadata returns empty list (defaults are sonnet).""" + result = get_phase_model_betas(tmp_path, "coding") + assert result == [] + + +class TestIsAdaptiveModel: + """Tests for is_adaptive_model() function.""" + + def test_opus_is_adaptive(self): + """claude-opus-4-6 is an adaptive thinking model.""" + assert is_adaptive_model("claude-opus-4-6") is True + + def test_sonnet_is_not_adaptive(self): + """claude-sonnet-4-5-20250929 is not an adaptive thinking model.""" + assert is_adaptive_model("claude-sonnet-4-5-20250929") is False + + def test_haiku_is_not_adaptive(self): + """claude-haiku-4-5-20251001 is not an adaptive thinking model.""" + assert is_adaptive_model("claude-haiku-4-5-20251001") is False + + def test_unknown_model_is_not_adaptive(self): + """Unknown models are not adaptive.""" + assert is_adaptive_model("some-unknown-model") is False + + def test_adaptive_models_set_contains_opus(self): + """ADAPTIVE_THINKING_MODELS set contains opus.""" + assert "claude-opus-4-6" in ADAPTIVE_THINKING_MODELS + + +class TestGetThinkingKwargsForModel: + """Tests for get_thinking_kwargs_for_model() function.""" + + def test_opus_gets_effort_level(self): + """Opus model gets both max_thinking_tokens and effort_level.""" + result = get_thinking_kwargs_for_model("claude-opus-4-6", "medium") + assert "max_thinking_tokens" in result + assert "effort_level" in result + assert result["effort_level"] == "medium" + assert result["max_thinking_tokens"] == 4096 + + def test_opus_high_thinking(self): + """Opus with high thinking level gets high effort.""" + result = get_thinking_kwargs_for_model("claude-opus-4-6", "high") + assert result["effort_level"] == "high" + assert result["max_thinking_tokens"] == 16384 + + def test_opus_low_thinking(self): + """Opus with low thinking level gets low effort.""" + result = get_thinking_kwargs_for_model("claude-opus-4-6", "low") + assert result["effort_level"] == "low" + assert result["max_thinking_tokens"] == 1024 + + def test_sonnet_no_effort_level(self): + """Sonnet model gets only max_thinking_tokens, no effort_level.""" + result = get_thinking_kwargs_for_model("claude-sonnet-4-5-20250929", "medium") + assert "max_thinking_tokens" in result + assert "effort_level" not in result + assert result["max_thinking_tokens"] == 4096 + + def test_haiku_no_effort_level(self): + """Haiku model gets only max_thinking_tokens, no effort_level.""" + result = get_thinking_kwargs_for_model("claude-haiku-4-5-20251001", "high") + assert "max_thinking_tokens" in result + assert "effort_level" not in result + assert result["max_thinking_tokens"] == 16384 + + + +class TestCreateClientFastMode: + """Tests for create_client() fast_mode parameter acceptance.""" + + def test_create_client_accepts_fast_mode_parameter(self): + """create_client() signature accepts fast_mode parameter.""" + import inspect + + from core.client import create_client + + sig = inspect.signature(create_client) + assert "fast_mode" in sig.parameters + # Default should be False + assert sig.parameters["fast_mode"].default is False + + def test_create_simple_client_accepts_fast_mode_parameter(self): + """create_simple_client() signature accepts fast_mode parameter.""" + import inspect + + from core.simple_client import create_simple_client + + sig = inspect.signature(create_simple_client) + assert "fast_mode" in sig.parameters + assert sig.parameters["fast_mode"].default is False + + +class TestGetFastModeIntegration: + """Tests for get_fast_mode() integration with task metadata.""" + + def test_fast_mode_reads_from_metadata(self, tmp_path): + """get_fast_mode reads fastMode from task_metadata.json.""" + metadata = {"fastMode": True, "model": "opus"} + metadata_path = tmp_path / "task_metadata.json" + metadata_path.write_text(json.dumps(metadata), encoding="utf-8") + + assert get_fast_mode(tmp_path) is True + + def test_fast_mode_defaults_to_false(self, tmp_path): + """get_fast_mode returns False when no metadata exists.""" + assert get_fast_mode(tmp_path) is False diff --git a/tests/test_thinking_level_validation.py b/tests/test_thinking_level_validation.py index 0d5bbe32..2fc08278 100644 --- a/tests/test_thinking_level_validation.py +++ b/tests/test_thinking_level_validation.py @@ -20,21 +20,13 @@ class TestThinkingLevelValidation: def test_valid_thinking_levels(self): """Test that all valid thinking levels return correct budgets.""" - valid_levels = ["none", "low", "medium", "high", "ultrathink"] + valid_levels = ["low", "medium", "high"] for level in valid_levels: budget = get_thinking_budget(level) expected = THINKING_BUDGET_MAP[level] assert budget == expected, f"Expected {expected} for {level}, got {budget}" - def test_none_level_returns_none(self): - """Test that 'none' thinking level returns None (no extended thinking).""" - assert get_thinking_budget("none") is None - - def test_ultrathink_max_budget(self): - """Test that 'ultrathink' returns maximum budget (63999 so max_tokens = 63999 + 1 = 64000 limit).""" - assert get_thinking_budget("ultrathink") == 63999 - def test_invalid_level_logs_warning(self, caplog): """Test that invalid thinking level logs a warning.""" with caplog.at_level(logging.WARNING): @@ -55,7 +47,7 @@ class TestThinkingLevelValidation: get_thinking_budget("bad_value") # Check all valid levels are mentioned - for level in ["none", "low", "medium", "high", "ultrathink"]: + for level in ["low", "medium", "high"]: assert level in caplog.text def test_empty_string_level(self, caplog): @@ -89,4 +81,17 @@ class TestThinkingLevelValidation: assert get_thinking_budget("low") == 1024 assert get_thinking_budget("medium") == 4096 assert get_thinking_budget("high") == 16384 - assert get_thinking_budget("ultrathink") == 63999 + + def test_removed_none_treated_as_invalid(self, caplog): + """Test that removed 'none' level is treated as invalid and defaults to medium.""" + with caplog.at_level(logging.WARNING): + budget = get_thinking_budget("none") + assert budget == THINKING_BUDGET_MAP["medium"] + assert "Invalid thinking_level 'none'" in caplog.text + + def test_removed_ultrathink_treated_as_invalid(self, caplog): + """Test that removed 'ultrathink' level is treated as invalid and defaults to medium.""" + with caplog.at_level(logging.WARNING): + budget = get_thinking_budget("ultrathink") + assert budget == THINKING_BUDGET_MAP["medium"] + assert "Invalid thinking_level 'ultrathink'" in caplog.text