From d0a63b472a75b03115189b3f6db81e3f5074f560 Mon Sep 17 00:00:00 2001 From: lokesh75-kank Date: Fri, 14 Aug 2026 10:11:23 -0700 Subject: [PATCH 1/2] fix(anthropic): dedup trailing /v1 in api_base before appending /v1/messages AnthropicMessagesConfig.get_complete_url appended /v1/messages to any api_base that didn't already end in it, so a base ending in /v1 (e.g. an Anthropic-compatible gateway root) produced /v1/v1/messages and a 404. The openai_like and deepseek messages configs already normalize this; mirror their rstrip/removesuffix handling and add the matching parametrized tests. Fixes #36956 --- .../messages/transformation.py | 10 +++-- ...est_anthropic_messages_get_complete_url.py | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 4d3354c58b7..9f318cf84b1 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -225,9 +225,13 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): stream: bool | None = None, ) -> str: api_base = AnthropicModelInfo.get_api_base(api_base) or "https://api.anthropic.com" - if not api_base.endswith("/v1/messages"): - api_base = f"{api_base}/v1/messages" - return api_base + # Normalize like OpenAILikeAnthropicMessagesConfig: an api_base that + # already ends in /v1 must not become /v1/v1/messages. + base = api_base.rstrip("/") + if base.endswith("/v1/messages"): + return base + base = base.removesuffix("/v1") + return f"{base}/v1/messages" def validate_anthropic_messages_environment( self, diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py new file mode 100644 index 00000000000..80f4fbd8f68 --- /dev/null +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py @@ -0,0 +1,41 @@ +import pytest + +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) + + +@pytest.fixture +def config() -> AnthropicMessagesConfig: + return AnthropicMessagesConfig() + + +@pytest.mark.parametrize( + "api_base, expected", + [ + # default when no api_base is configured + (None, "https://api.anthropic.com/v1/messages"), + # bases without a version suffix + ("https://host", "https://host/v1/messages"), + ("https://host/", "https://host/v1/messages"), + # bases that already carry /v1 must not become /v1/v1/messages + ("https://host/v1", "https://host/v1/messages"), + ("https://host/v1/", "https://host/v1/messages"), + ("https://api.groq.com/openai/v1", "https://api.groq.com/openai/v1/messages"), + # full endpoint passes through untouched + ("https://host/v1/messages", "https://host/v1/messages"), + # provider-prefixed path segments are preserved + ("https://gateway.example.com/anthropic", "https://gateway.example.com/anthropic/v1/messages"), + ("https://gateway.example.com/anthropic/v1", "https://gateway.example.com/anthropic/v1/messages"), + ], +) +def test_get_complete_url_deduplicates_trailing_v1(config, api_base, expected, monkeypatch): + monkeypatch.delenv("ANTHROPIC_API_BASE", raising=False) + url = config.get_complete_url( + api_base=api_base, + api_key="sk-test", + model="claude-sonnet-4-5", + optional_params={}, + litellm_params={}, + ) + assert url == expected From 1822f01ed74be6b7f56a609ff8ef25c2c066c301 Mon Sep 17 00:00:00 2001 From: lokesh75-kank Date: Fri, 14 Aug 2026 10:20:00 -0700 Subject: [PATCH 2/2] style: drop redundant comments and isolate ANTHROPIC_BASE_URL in url tests --- .../experimental_pass_through/messages/transformation.py | 2 -- .../messages/test_anthropic_messages_get_complete_url.py | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 9f318cf84b1..49dd758a8f3 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -225,8 +225,6 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): stream: bool | None = None, ) -> str: api_base = AnthropicModelInfo.get_api_base(api_base) or "https://api.anthropic.com" - # Normalize like OpenAILikeAnthropicMessagesConfig: an api_base that - # already ends in /v1 must not become /v1/v1/messages. base = api_base.rstrip("/") if base.endswith("/v1/messages"): return base diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py index 80f4fbd8f68..46082bb8198 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_get_complete_url.py @@ -13,24 +13,20 @@ def config() -> AnthropicMessagesConfig: @pytest.mark.parametrize( "api_base, expected", [ - # default when no api_base is configured (None, "https://api.anthropic.com/v1/messages"), - # bases without a version suffix ("https://host", "https://host/v1/messages"), ("https://host/", "https://host/v1/messages"), - # bases that already carry /v1 must not become /v1/v1/messages ("https://host/v1", "https://host/v1/messages"), ("https://host/v1/", "https://host/v1/messages"), ("https://api.groq.com/openai/v1", "https://api.groq.com/openai/v1/messages"), - # full endpoint passes through untouched ("https://host/v1/messages", "https://host/v1/messages"), - # provider-prefixed path segments are preserved ("https://gateway.example.com/anthropic", "https://gateway.example.com/anthropic/v1/messages"), ("https://gateway.example.com/anthropic/v1", "https://gateway.example.com/anthropic/v1/messages"), ], ) def test_get_complete_url_deduplicates_trailing_v1(config, api_base, expected, monkeypatch): monkeypatch.delenv("ANTHROPIC_API_BASE", raising=False) + monkeypatch.delenv("ANTHROPIC_BASE_URL", raising=False) url = config.get_complete_url( api_base=api_base, api_key="sk-test",