From 16322651bb8a18c7a4d43a539dac308bbed02c50 Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 10 Sep 2026 22:45:09 +0200 Subject: [PATCH] fix: do not load the anthropic package on import `serena.analytics` imported `anthropic.types` at module level for two type annotations, so every `import serena.cli` / `import serena.agent` paid for loading the whole anthropic package (2-4 s on the reporter's Windows machine) even with the default CHAR_COUNT token estimator. That delay can push MCP startup past a client's initial tool-discovery window. The import now lives behind `TYPE_CHECKING`; `AnthropicTokenCount` already imported `anthropic` lazily in `__init__`, and the request body is a plain dict (what `MessageParam` produces at runtime anyway). Tests: importing serena.cli / serena.agent / serena.analytics in a fresh interpreter leaves `anthropic` out of `sys.modules`; the Anthropic estimator still sends the expected count_tokens request. Fixes #2012 --- CHANGELOG.md | 2 ++ src/serena/analytics.py | 9 +++++++-- test/serena/test_analytics_imports.py | 29 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 test/serena/test_analytics_imports.py 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"}], + )