diff --git a/CHANGELOG.md b/CHANGELOG.md index a6aa0625..56e3b8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Fix: MCP `initialize` now reports Serena's version instead of the installed mcp SDK version (#1889) + - Fix: importing Serena no longer loads the `anthropic` package unless the Anthropic token counter is + actually used; the unconditional import added seconds to CLI/MCP startup on some machines (#2012) - Fix: Parallel agents auto-registering projects could overwrite each other's changes to the global project list in `serena_config.yml` - Fix: `TextUtils.insert_text_at_position` returned a wrong position when the inserted text merged diff --git a/src/serena/analytics.py b/src/serena/analytics.py index 315cd57d..8fcc5636 100644 --- a/src/serena/analytics.py +++ b/src/serena/analytics.py @@ -9,10 +9,15 @@ from collections import defaultdict from copy import copy from dataclasses import asdict, dataclass from enum import Enum +from typing import TYPE_CHECKING -from anthropic.types import MessageParam, MessageTokensCount from dotenv import load_dotenv +if TYPE_CHECKING: + # Imported for annotations only: loading the anthropic package costs seconds on some + # machines (see #2012) and is only needed when the Anthropic token counter is used. + from anthropic.types import MessageTokensCount + log = logging.getLogger(__name__) @@ -64,7 +69,7 @@ class AnthropicTokenCount(TokenCountEstimator): def _send_count_tokens_request(self, text: str) -> MessageTokensCount: return self._anthropic_client.messages.count_tokens( model=self._model_name, - messages=[MessageParam(role="user", content=text)], + messages=[{"role": "user", "content": text}], ) def estimate_token_count(self, text: str) -> int: diff --git a/test/serena/test_analytics_imports.py b/test/serena/test_analytics_imports.py new file mode 100644 index 00000000..ec18f378 --- /dev/null +++ b/test/serena/test_analytics_imports.py @@ -0,0 +1,29 @@ +"""The anthropic package must only be loaded when the Anthropic token counter is used (#2012).""" + +import subprocess +import sys +from unittest.mock import MagicMock + +import pytest + +from serena.analytics import AnthropicTokenCount + + +@pytest.mark.parametrize("module", ["serena.cli", "serena.agent", "serena.analytics"]) +def test_importing_serena_does_not_load_anthropic(module: str) -> None: + code = f"import sys, {module}; print('anthropic' in sys.modules)" + result = subprocess.run([sys.executable, "-c", code], capture_output=True, text=True, check=True, timeout=120) + assert result.stdout.strip() == "False", f"importing {module} loaded the anthropic package" + + +def test_anthropic_token_count_sends_a_plain_user_message() -> None: + estimator = AnthropicTokenCount.__new__(AnthropicTokenCount) + estimator._model_name = "claude-sonnet-4-20250514" + estimator._anthropic_client = MagicMock() + estimator._anthropic_client.messages.count_tokens.return_value = MagicMock(input_tokens=7) + + assert estimator.estimate_token_count("hello") == 7 + estimator._anthropic_client.messages.count_tokens.assert_called_once_with( + model="claude-sonnet-4-20250514", + messages=[{"role": "user", "content": "hello"}], + )