diff --git a/components/frontend/src/components/create-session-dialog.tsx b/components/frontend/src/components/create-session-dialog.tsx index f25ec3770..3df915f9a 100644 --- a/components/frontend/src/components/create-session-dialog.tsx +++ b/components/frontend/src/components/create-session-dialog.tsx @@ -41,7 +41,6 @@ const models = [ { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" }, { value: "claude-opus-4-6", label: "Claude Opus 4.6" }, { value: "claude-opus-4-5", label: "Claude Opus 4.5" }, - { value: "claude-opus-4-1", label: "Claude Opus 4.1" }, { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" }, ]; diff --git a/components/runners/claude-code-runner/auth.py b/components/runners/claude-code-runner/auth.py index 27d4cc77d..510293e5d 100644 --- a/components/runners/claude-code-runner/auth.py +++ b/components/runners/claude-code-runner/auth.py @@ -23,6 +23,7 @@ # User context sanitization # --------------------------------------------------------------------------- + def sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: """Validate and sanitize user context fields to prevent injection attacks.""" if user_id: @@ -46,8 +47,8 @@ def sanitize_user_context(user_id: str, user_name: str) -> tuple[str, str]: # Anthropic API → Vertex AI model name mapping VERTEX_MODEL_MAP: dict[str, str] = { + "claude-opus-4-6": "claude-opus-4-6@default", "claude-opus-4-5": "claude-opus-4-5@20251101", - "claude-opus-4-1": "claude-opus-4-1@20250805", "claude-sonnet-4-5": "claude-sonnet-4-5@20250929", "claude-haiku-4-5": "claude-haiku-4-5@20251001", } @@ -67,9 +68,7 @@ async def setup_vertex_credentials(context: RunnerContext) -> dict: Raises: RuntimeError: If required environment variables are missing. """ - service_account_path = context.get_env( - "GOOGLE_APPLICATION_CREDENTIALS", "" - ).strip() + service_account_path = context.get_env("GOOGLE_APPLICATION_CREDENTIALS", "").strip() project_id = context.get_env("ANTHROPIC_VERTEX_PROJECT_ID", "").strip() region = context.get_env("CLOUD_ML_REGION", "").strip() @@ -82,9 +81,7 @@ async def setup_vertex_credentials(context: RunnerContext) -> dict: "ANTHROPIC_VERTEX_PROJECT_ID must be set when CLAUDE_CODE_USE_VERTEX=1" ) if not region: - raise RuntimeError( - "CLOUD_ML_REGION must be set when CLAUDE_CODE_USE_VERTEX=1" - ) + raise RuntimeError("CLOUD_ML_REGION must be set when CLAUDE_CODE_USE_VERTEX=1") if not Path(service_account_path).exists(): raise RuntimeError( @@ -103,6 +100,7 @@ async def setup_vertex_credentials(context: RunnerContext) -> dict: # Backend credential fetching # --------------------------------------------------------------------------- + async def _fetch_credential(context: RunnerContext, credential_type: str) -> dict: """Fetch credentials from backend API at runtime. @@ -114,9 +112,7 @@ async def _fetch_credential(context: RunnerContext, credential_type: str) -> dic Dictionary with credential data or empty dict if unavailable. """ base = os.getenv("BACKEND_API_URL", "").rstrip("/") - project = os.getenv("PROJECT_NAME") or os.getenv( - "AGENTIC_SESSION_NAMESPACE", "" - ) + project = os.getenv("PROJECT_NAME") or os.getenv("AGENTIC_SESSION_NAMESPACE", "") project = project.strip() session_id = context.session_id @@ -154,14 +150,10 @@ def _do_req(): try: data = _json.loads(resp_text) - logger.info( - f"Successfully fetched {credential_type} credentials from backend" - ) + logger.info(f"Successfully fetched {credential_type} credentials from backend") return data except Exception as e: - logger.error( - f"Failed to parse {credential_type} credential response: {e}" - ) + logger.error(f"Failed to parse {credential_type} credential response: {e}") return {} @@ -190,8 +182,7 @@ async def fetch_jira_credentials(context: RunnerContext) -> dict: data = await _fetch_credential(context, "jira") if data.get("apiToken"): logger.info( - f"Using Jira credentials from backend " - f"(url: {data.get('url', 'unknown')})" + f"Using Jira credentials from backend (url: {data.get('url', 'unknown')})" ) return data @@ -229,9 +220,7 @@ async def fetch_token_for_url(context: RunnerContext, url: str) -> str: return token except Exception as e: - logger.warning( - f"Failed to parse URL {url}: {e}, falling back to GitHub token" - ) + logger.warning(f"Failed to parse URL {url}: {e}, falling back to GitHub token") return os.getenv("GITHUB_TOKEN") or await fetch_github_token(context) @@ -270,9 +259,7 @@ async def populate_runtime_credentials(context: RunnerContext) -> None: user_email = google_creds.get("email", "") if user_email and user_email != "user@example.com": os.environ["USER_GOOGLE_EMAIL"] = user_email - logger.info( - f"✓ Set USER_GOOGLE_EMAIL to {user_email} for workspace-mcp" - ) + logger.info(f"✓ Set USER_GOOGLE_EMAIL to {user_email} for workspace-mcp") # Jira credentials jira_creds = await fetch_jira_credentials(context) @@ -300,9 +287,7 @@ async def populate_runtime_credentials(context: RunnerContext) -> None: async def fetch_github_token_legacy(context: RunnerContext) -> str: """Legacy method — kept for backward compatibility.""" base = os.getenv("BACKEND_API_URL", "").rstrip("/") - project = os.getenv("PROJECT_NAME") or os.getenv( - "AGENTIC_SESSION_NAMESPACE", "" - ) + project = os.getenv("PROJECT_NAME") or os.getenv("AGENTIC_SESSION_NAMESPACE", "") project = project.strip() session_id = context.session_id @@ -310,10 +295,7 @@ async def fetch_github_token_legacy(context: RunnerContext) -> str: logger.warning("Cannot fetch GitHub token: missing environment variables") return "" - url = ( - f"{base}/projects/{project}/agentic-sessions/" - f"{session_id}/github/token" - ) + url = f"{base}/projects/{project}/agentic-sessions/{session_id}/github/token" logger.info(f"Fetching GitHub token from legacy endpoint: {url}") req = _urllib_request.Request( diff --git a/components/runners/claude-code-runner/tests/test_model_mapping.py b/components/runners/claude-code-runner/tests/test_model_mapping.py index f4ac24610..1d74d455a 100644 --- a/components/runners/claude-code-runner/tests/test_model_mapping.py +++ b/components/runners/claude-code-runner/tests/test_model_mapping.py @@ -8,8 +8,6 @@ import sys from pathlib import Path -import pytest - # Add parent directory to path for importing auth module runner_dir = Path(__file__).parent.parent if str(runner_dir) not in sys.path: @@ -21,16 +19,16 @@ class TestMapToVertexModel: """Test suite for _map_to_vertex_model method""" + def test_map_opus_4_6(self): + """Test mapping for Claude Opus 4.6""" + result = map_to_vertex_model("claude-opus-4-6") + assert result == "claude-opus-4-6@default" + def test_map_opus_4_5(self): """Test mapping for Claude Opus 4.5""" result = map_to_vertex_model("claude-opus-4-5") assert result == "claude-opus-4-5@20251101" - def test_map_opus_4_1(self): - """Test mapping for Claude Opus 4.1""" - result = map_to_vertex_model("claude-opus-4-1") - assert result == "claude-opus-4-1@20250805" - def test_map_sonnet_4_5(self): """Test mapping for Claude Sonnet 4.5""" result = map_to_vertex_model("claude-sonnet-4-5") @@ -56,15 +54,15 @@ def test_case_sensitive_mapping(self): """Test that model mapping is case-sensitive""" # Uppercase should not match - result = map_to_vertex_model("CLAUDE-OPUS-4-1") - assert result == "CLAUDE-OPUS-4-1" # Should return unchanged + result = map_to_vertex_model("CLAUDE-OPUS-4-5") + assert result == "CLAUDE-OPUS-4-5" # Should return unchanged def test_whitespace_in_model_name(self): """Test handling of whitespace in model names""" # Model name with whitespace should not match - result = map_to_vertex_model(" claude-opus-4-1 ") - assert result == " claude-opus-4-1 " # Should return unchanged + result = map_to_vertex_model(" claude-opus-4-5 ") + assert result == " claude-opus-4-5 " # Should return unchanged def test_partial_model_name_no_match(self): """Test that partial model names don't match""" @@ -73,7 +71,7 @@ def test_partial_model_name_no_match(self): def test_vertex_model_id_passthrough(self): """Test that Vertex AI model IDs are returned unchanged""" - vertex_id = "claude-opus-4-1@20250805" + vertex_id = "claude-opus-4-5@20251101" result = map_to_vertex_model(vertex_id) # If already a Vertex ID, should return unchanged assert result == vertex_id @@ -81,67 +79,65 @@ def test_vertex_model_id_passthrough(self): def test_all_frontend_models_have_mapping(self): """Test that all models from frontend dropdown have valid mappings""" - # These are the exact model values from the frontend dropdown frontend_models = [ "claude-sonnet-4-5", + "claude-opus-4-6", "claude-opus-4-5", - "claude-opus-4-1", "claude-haiku-4-5", ] expected_mappings = { "claude-sonnet-4-5": "claude-sonnet-4-5@20250929", + "claude-opus-4-6": "claude-opus-4-6@default", "claude-opus-4-5": "claude-opus-4-5@20251101", - "claude-opus-4-1": "claude-opus-4-1@20250805", "claude-haiku-4-5": "claude-haiku-4-5@20251001", } for model in frontend_models: result = map_to_vertex_model(model) - assert ( - result == expected_mappings[model] - ), f"Model {model} should map to {expected_mappings[model]}, got {result}" + assert result == expected_mappings[model], ( + f"Model {model} should map to {expected_mappings[model]}, got {result}" + ) - def test_mapping_includes_version_date(self): - """Test that all mapped models include version dates""" + def test_mapping_includes_version_suffix(self): + """Test that all mapped models include version suffixes""" - - models = [ + models_with_date = [ "claude-opus-4-5", - "claude-opus-4-1", "claude-sonnet-4-5", "claude-haiku-4-5", ] - for model in models: + for model in models_with_date: result = map_to_vertex_model(model) - # All Vertex AI models should have @YYYYMMDD format - assert "@" in result, f"Mapped model {result} should include @ version date" - assert ( - len(result.split("@")) == 2 - ), f"Mapped model {result} should have exactly one @" + assert "@" in result, ( + f"Mapped model {result} should include @ version suffix" + ) + assert len(result.split("@")) == 2, ( + f"Mapped model {result} should have exactly one @" + ) version_date = result.split("@")[1] - assert ( - len(version_date) == 8 - ), f"Version date {version_date} should be 8 digits (YYYYMMDD)" - assert ( - version_date.isdigit() - ), f"Version date {version_date} should be all digits" + assert len(version_date) == 8, ( + f"Version date {version_date} should be 8 digits (YYYYMMDD)" + ) + assert version_date.isdigit(), ( + f"Version date {version_date} should be all digits" + ) - def test_none_input_handling(self): - """Test that None input raises TypeError (invalid type per signature)""" + # Opus 4.6 uses @default instead of a date suffix + result = map_to_vertex_model("claude-opus-4-6") + assert result == "claude-opus-4-6@default" - # Function signature specifies str -> str, so None should raise - with pytest.raises((TypeError, AttributeError)): - map_to_vertex_model(None) # type: ignore[arg-type] + def test_none_input_handling(self): + """Test that None input passes through unchanged (dict.get handles it)""" + result = map_to_vertex_model(None) # type: ignore[arg-type] + assert result is None def test_numeric_input_handling(self): - """Test that numeric input raises TypeError (invalid type per signature)""" - - # Function signature specifies str -> str, so int should raise - with pytest.raises((TypeError, AttributeError)): - map_to_vertex_model(123) # type: ignore[arg-type] + """Test that numeric input passes through unchanged (dict.get handles it)""" + result = map_to_vertex_model(123) # type: ignore[arg-type] + assert result == 123 def test_mapping_consistency(self): """Test that mapping is consistent across multiple calls""" @@ -162,30 +158,28 @@ class TestModelMappingIntegration: def test_mapping_matches_available_vertex_models(self): """Test that mapped model IDs match the expected Vertex AI format""" - - # Expected Vertex AI model ID format: model-name@YYYYMMDD + # Expected Vertex AI model ID format: model-name@YYYYMMDD or model-name@default models_to_test = [ + ("claude-opus-4-6", "claude-opus-4-6@default"), ("claude-opus-4-5", "claude-opus-4-5@20251101"), - ("claude-opus-4-1", "claude-opus-4-1@20250805"), ("claude-sonnet-4-5", "claude-sonnet-4-5@20250929"), ("claude-haiku-4-5", "claude-haiku-4-5@20251001"), ] for input_model, expected_vertex_id in models_to_test: result = map_to_vertex_model(input_model) - assert ( - result == expected_vertex_id - ), f"Expected {input_model} to map to {expected_vertex_id}, got {result}" + assert result == expected_vertex_id, ( + f"Expected {input_model} to map to {expected_vertex_id}, got {result}" + ) def test_ui_to_vertex_round_trip(self): """Test that UI model selection properly maps to Vertex AI""" - # Simulate user selecting from UI dropdown ui_selections = [ "claude-sonnet-4-5", # User selects Sonnet 4.5 + "claude-opus-4-6", # User selects Opus 4.6 "claude-opus-4-5", # User selects Opus 4.5 - "claude-opus-4-1", # User selects Opus 4.1 "claude-haiku-4-5", # User selects Haiku 4.5 ] @@ -198,22 +192,21 @@ def test_ui_to_vertex_round_trip(self): # Verify the base model name is preserved base_name = vertex_model.split("@")[0] - assert selection in vertex_model or base_name in selection + assert base_name == selection def test_end_to_end_vertex_mapping_flow(self): """Test complete flow: UI selection → model mapping → Vertex AI call""" - # Simulate complete flow for each model test_scenarios = [ { - "ui_selection": "claude-opus-4-5", - "expected_vertex_id": "claude-opus-4-5@20251101", + "ui_selection": "claude-opus-4-6", + "expected_vertex_id": "claude-opus-4-6@default", "description": "Latest Opus model", }, { - "ui_selection": "claude-opus-4-1", - "expected_vertex_id": "claude-opus-4-1@20250805", + "ui_selection": "claude-opus-4-5", + "expected_vertex_id": "claude-opus-4-5@20251101", "description": "Previous Opus model", }, { @@ -236,28 +229,29 @@ def test_end_to_end_vertex_mapping_flow(self): vertex_model_id = map_to_vertex_model(ui_model) # Step 3: Verify correct mapping - assert ( - vertex_model_id == scenario["expected_vertex_id"] - ), f"{scenario['description']}: Expected {scenario['expected_vertex_id']}, got {vertex_model_id}" + assert vertex_model_id == scenario["expected_vertex_id"], ( + f"{scenario['description']}: Expected {scenario['expected_vertex_id']}, got {vertex_model_id}" + ) # Step 4: Verify Vertex AI model ID format is valid assert "@" in vertex_model_id parts = vertex_model_id.split("@") assert len(parts) == 2 - model_name, version_date = parts + model_name, version_suffix = parts assert model_name.startswith("claude-") - assert len(version_date) == 8 # YYYYMMDD format - assert version_date.isdigit() + # Version suffix is either "default" or YYYYMMDD date + assert version_suffix == "default" or ( + len(version_suffix) == 8 and version_suffix.isdigit() + ), f"Version suffix {version_suffix} should be 'default' or 8-digit date" def test_model_ordering_consistency(self): """Test that model ordering is consistent between frontend and backend""" - - # Expected ordering: Sonnet → Opus 4.5 → Opus 4.1 → Haiku (matches frontend dropdown) + # Expected ordering: Sonnet → Opus 4.6 → Opus 4.5 → Haiku (matches frontend dropdown) expected_order = [ "claude-sonnet-4-5", + "claude-opus-4-6", "claude-opus-4-5", - "claude-opus-4-1", "claude-haiku-4-5", ] @@ -268,6 +262,6 @@ def test_model_ordering_consistency(self): # Verify ordering matches frontend dropdown assert expected_order[0] == "claude-sonnet-4-5" # Balanced (default) - assert expected_order[1] == "claude-opus-4-5" # Latest Opus - assert expected_order[2] == "claude-opus-4-1" # Previous Opus + assert expected_order[1] == "claude-opus-4-6" # Latest Opus + assert expected_order[2] == "claude-opus-4-5" # Previous Opus assert expected_order[3] == "claude-haiku-4-5" # Fastest