From d6cd50dfdbe9bea6dfd378f0a7717b467ef055fe Mon Sep 17 00:00:00 2001 From: 0x5751 Date: Tue, 26 Aug 2025 01:22:51 +0800 Subject: [PATCH] feat: Add support for custom Anthropic-compatible API endpoints This commit adds support for custom Anthropic-compatible API endpoints that don't follow the standard /v1/messages or /v1/complete path convention. ## Changes - Added LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX environment variable - When set to true, prevents automatic appending of /v1/messages (for anthropic) - When set to true, prevents automatic appending of /v1/complete (for anthropic_text) - Added debug logging to indicate when suffix is being skipped - Maintained full backward compatibility - existing deployments are unaffected --- litellm/main.py | 24 ++++++- tests/test_litellm/test_main.py | 122 +++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/litellm/main.py b/litellm/main.py index c8442d483e..948c9a1189 100644 --- a/litellm/main.py +++ b/litellm/main.py @@ -2169,8 +2169,18 @@ def completion( # type: ignore # noqa: PLR0915 or "https://api.anthropic.com/v1/complete" ) - if api_base is not None and not api_base.endswith("/v1/complete"): + # Check if we should disable automatic URL suffix appending + disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX") + if ( + api_base is not None + and not disable_url_suffix + and not api_base.endswith("/v1/complete") + ): api_base += "/v1/complete" + elif disable_url_suffix: + verbose_logger.debug( + "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/complete suffix" + ) response = base_llm_http_handler.completion( model=model, @@ -2206,8 +2216,18 @@ def completion( # type: ignore # noqa: PLR0915 or "https://api.anthropic.com/v1/messages" ) - if api_base is not None and not api_base.endswith("/v1/messages"): + # Check if we should disable automatic URL suffix appending + disable_url_suffix = get_secret_bool("LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX") + if ( + api_base is not None + and not disable_url_suffix + and not api_base.endswith("/v1/messages") + ): api_base += "/v1/messages" + elif disable_url_suffix: + verbose_logger.debug( + "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX is set, skipping /v1/messages suffix" + ) response = anthropic_chat_completions.completion( model=model, diff --git a/tests/test_litellm/test_main.py b/tests/test_litellm/test_main.py index b9a71e9621..4b79486bb5 100644 --- a/tests/test_litellm/test_main.py +++ b/tests/test_litellm/test_main.py @@ -1121,4 +1121,124 @@ async def test_retrying() -> None: model="gpt-4o-mini", messages=[{"role": "user", "content": "Hello"}], ) - assert mock_request.call_count >= 10, "Expected retrying to be used" + + +def test_anthropic_disable_url_suffix_env_var(): + """Test that LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX prevents /v1/messages suffix.""" + from unittest.mock import patch, MagicMock + import os + from litellm import completion + from litellm.llms.anthropic.chat.handler import AnthropicLLM + + # Test with environment variable disabled (default behavior) + with patch.dict(os.environ, {"ANTHROPIC_API_BASE": "https://api.example.com"}): + with patch.object(AnthropicLLM, "__init__", return_value=None) as mock_init: + with patch.object(AnthropicLLM, "completion") as mock_completion: + mock_completion.return_value = MagicMock() + + # Mock the initialization to capture api_base + actual_api_base = None + def capture_init(self, **kwargs): + nonlocal actual_api_base + actual_api_base = kwargs.get("api_base") + + with patch("litellm.main.anthropic_chat_completions") as mock_anthropic: + def capture_completion(**kwargs): + nonlocal actual_api_base + actual_api_base = kwargs.get("api_base") + return MagicMock() + + mock_anthropic.completion = capture_completion + + # This should append /v1/messages + completion( + model="anthropic/claude-3-sonnet", + messages=[{"role": "user", "content": "test"}], + api_key="test-key" + ) + + # Verify the api_base has /v1/messages appended + assert actual_api_base.endswith("/v1/messages") + assert actual_api_base == "https://api.example.com/v1/messages" + + # Test with environment variable enabled + with patch.dict(os.environ, { + "ANTHROPIC_API_BASE": "https://api.example.com/custom/path", + "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX": "true" + }): + actual_api_base = None + + with patch("litellm.main.anthropic_chat_completions") as mock_anthropic: + def capture_completion(**kwargs): + nonlocal actual_api_base + actual_api_base = kwargs.get("api_base") + return MagicMock() + + mock_anthropic.completion = capture_completion + + # This should NOT append /v1/messages + completion( + model="anthropic/claude-3-sonnet", + messages=[{"role": "user", "content": "test"}], + api_key="test-key" + ) + + # Verify the api_base does not have /v1/messages appended + assert actual_api_base == "https://api.example.com/custom/path" + assert not actual_api_base.endswith("/v1/messages") + + +def test_anthropic_text_disable_url_suffix_env_var(): + """Test that LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX prevents /v1/complete suffix for anthropic_text.""" + from unittest.mock import patch, MagicMock + import os + from litellm import completion + + # Test with environment variable disabled (default behavior) + with patch.dict(os.environ, {"ANTHROPIC_API_BASE": "https://api.example.com"}): + actual_api_base = None + + with patch("litellm.main.base_llm_http_handler") as mock_handler: + def capture_completion(**kwargs): + nonlocal actual_api_base + actual_api_base = kwargs.get("api_base") + return MagicMock() + + mock_handler.completion = capture_completion + + # This should append /v1/complete + completion( + model="anthropic_text/claude-instant-1", + messages=[{"role": "user", "content": "test"}], + api_key="test-key" + ) + + # Verify the api_base has /v1/complete appended + assert actual_api_base.endswith("/v1/complete") + assert actual_api_base == "https://api.example.com/v1/complete" + + # Test with environment variable enabled + with patch.dict(os.environ, { + "ANTHROPIC_API_BASE": "https://api.example.com/custom/complete", + "LITELLM_ANTHROPIC_DISABLE_URL_SUFFIX": "true" + }): + actual_api_base = None + + with patch("litellm.main.base_llm_http_handler") as mock_handler: + def capture_completion(**kwargs): + nonlocal actual_api_base + actual_api_base = kwargs.get("api_base") + return MagicMock() + + mock_handler.completion = capture_completion + + # This should NOT append /v1/complete + completion( + model="anthropic_text/claude-instant-1", + messages=[{"role": "user", "content": "test"}], + api_key="test-key" + ) + + # Verify the api_base does not have /v1/complete appended + assert actual_api_base == "https://api.example.com/custom/complete" + assert not actual_api_base.endswith("/v1/complete")