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
This commit is contained in:
0x5751
2025-08-26 01:22:51 +08:00
parent c1ee8c26af
commit d6cd50dfdb
2 changed files with 143 additions and 3 deletions
+22 -2
View File
@@ -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,
+121 -1
View File
@@ -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")