From f8eaf7f8bd7c08b0ed591f8b3eaaa8158738913b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lodri=20P=C3=A9ter?= <267054113+peterlodri-sec@users.noreply.github.com> Date: Wed, 24 Jun 2026 22:34:32 +0200 Subject: [PATCH] fix(anthropic): propagate api_base to count_tokens endpoint When a deployment configures a custom api_base, /v1/messages/count_tokens was silently routing to api.anthropic.com instead. The token counter read api_base from litellm_params but never passed it to the handler; the handler had the parameter but used it as a raw URL without appending the path. Fixes: propagate api_base (and ANTHROPIC_API_BASE env fallback) from AnthropicTokenCounter.count_tokens into the handler, and add _build_count_tokens_url to AnthropicCountTokensConfig to correctly append /v1/messages/count_tokens to any base variant. --- .../llms/anthropic/count_tokens/handler.py | 4 +- .../anthropic/count_tokens/token_counter.py | 5 ++ .../anthropic/count_tokens/transformation.py | 22 ++++++--- ...t_anthropic_count_tokens_transformation.py | 47 +++++++++++++++++++ 4 files changed, 69 insertions(+), 9 deletions(-) diff --git a/litellm/llms/anthropic/count_tokens/handler.py b/litellm/llms/anthropic/count_tokens/handler.py index 4d0af0b36c8..fde0304c3f7 100644 --- a/litellm/llms/anthropic/count_tokens/handler.py +++ b/litellm/llms/anthropic/count_tokens/handler.py @@ -68,8 +68,8 @@ class AnthropicCountTokensHandler(AnthropicCountTokensConfig): verbose_logger.debug(f"Transformed request: {request_body}") - # Get endpoint URL - endpoint_url = api_base or self.get_anthropic_count_tokens_endpoint() + # Get endpoint URL; append the count_tokens path when a custom base is given + endpoint_url = self._build_count_tokens_url(api_base) verbose_logger.debug(f"Making request to: {endpoint_url}") diff --git a/litellm/llms/anthropic/count_tokens/token_counter.py b/litellm/llms/anthropic/count_tokens/token_counter.py index 93989c58547..249b20e7243 100644 --- a/litellm/llms/anthropic/count_tokens/token_counter.py +++ b/litellm/llms/anthropic/count_tokens/token_counter.py @@ -63,11 +63,16 @@ class AnthropicTokenCounter(BaseTokenCounter): verbose_logger.warning("No Anthropic API key found for token counting") return None + api_base: Optional[str] = litellm_params.get("api_base") or os.getenv( + "ANTHROPIC_API_BASE" + ) + try: result = await anthropic_count_tokens_handler.handle_count_tokens_request( model=model_to_use, messages=messages, api_key=api_key, + api_base=api_base, tools=tools, system=system, ) diff --git a/litellm/llms/anthropic/count_tokens/transformation.py b/litellm/llms/anthropic/count_tokens/transformation.py index ad5bbbda25f..81dea02a02b 100644 --- a/litellm/llms/anthropic/count_tokens/transformation.py +++ b/litellm/llms/anthropic/count_tokens/transformation.py @@ -19,14 +19,22 @@ class AnthropicCountTokensConfig: - Response: {"input_tokens": } """ - def get_anthropic_count_tokens_endpoint(self) -> str: - """ - Get the Anthropic CountTokens API endpoint. + _COUNT_TOKENS_PATH: str = "/v1/messages/count_tokens" + _DEFAULT_ENDPOINT: str = f"https://api.anthropic.com{_COUNT_TOKENS_PATH}" - Returns: - The endpoint URL for the CountTokens API - """ - return "https://api.anthropic.com/v1/messages/count_tokens" + def get_anthropic_count_tokens_endpoint(self) -> str: + return self._DEFAULT_ENDPOINT + + def _build_count_tokens_url(self, api_base: Optional[str]) -> str: + """Return the full count_tokens URL, appending the path when a custom base is provided.""" + if not api_base: + return self._DEFAULT_ENDPOINT + base = api_base.rstrip("/") + if base.endswith(self._COUNT_TOKENS_PATH): + return base + if base.endswith("/v1/messages"): + return f"{base}/count_tokens" + return f"{base}{self._COUNT_TOKENS_PATH}" def transform_request_to_count_tokens( self, diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py b/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py index 889809140f8..d3c6272429b 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py @@ -93,3 +93,50 @@ def test_transform_no_system_no_tools(): assert "system" not in result assert "tools" not in result + + +def test_build_count_tokens_url_no_base(): + """No custom base -> default Anthropic endpoint.""" + config = AnthropicCountTokensConfig() + assert ( + config._build_count_tokens_url(None) + == "https://api.anthropic.com/v1/messages/count_tokens" + ) + + +def test_build_count_tokens_url_bare_base(): + """Plain domain base gets full path appended.""" + config = AnthropicCountTokensConfig() + assert ( + config._build_count_tokens_url("https://my-proxy.example.com") + == "https://my-proxy.example.com/v1/messages/count_tokens" + ) + + +def test_build_count_tokens_url_base_with_v1_messages(): + """Base already ending in /v1/messages gets only /count_tokens.""" + config = AnthropicCountTokensConfig() + assert ( + config._build_count_tokens_url("https://my-proxy.example.com/v1/messages") + == "https://my-proxy.example.com/v1/messages/count_tokens" + ) + + +def test_build_count_tokens_url_already_full(): + """Base already containing the full path is returned as-is.""" + config = AnthropicCountTokensConfig() + assert ( + config._build_count_tokens_url( + "https://my-proxy.example.com/v1/messages/count_tokens" + ) + == "https://my-proxy.example.com/v1/messages/count_tokens" + ) + + +def test_build_count_tokens_url_trailing_slash_stripped(): + """Trailing slash on the base is ignored.""" + config = AnthropicCountTokensConfig() + assert ( + config._build_count_tokens_url("https://my-proxy.example.com/") + == "https://my-proxy.example.com/v1/messages/count_tokens" + )