Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions litellm/llms/deepseek/messages/transformation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""
DeepSeek Anthropic-compatible messages transformation config.
"""

from typing import Any, Dict, List, Optional, Tuple

import litellm
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams


class DeepSeekAnthropicMessagesConfig(AnthropicMessagesConfig):
"""
DeepSeek exposes an Anthropic-compatible Messages API at
https://api.deepseek.com/anthropic.

It accepts the native Anthropic Messages conversation shape, including
thinking blocks in assistant history, but rejects Anthropic's explicit
custom-tool discriminator (`{"type": "custom"}`).
"""

@property
def custom_llm_provider(self) -> Optional[str]:
return "deepseek"

@staticmethod
def get_api_key(api_key: Optional[str] = None) -> Optional[str]:
return api_key or get_secret_str("DEEPSEEK_API_KEY") or litellm.api_key

@staticmethod
def get_api_base(api_base: Optional[str] = None) -> str:
return (
api_base
or get_secret_str("DEEPSEEK_ANTHROPIC_API_BASE")
or "https://api.deepseek.com/anthropic"
)

def validate_anthropic_messages_environment(
self,
headers: dict,
model: str,
messages: List[Any],
optional_params: dict,
litellm_params: dict,
api_key: Optional[str] = None,
api_base: Optional[str] = None,
) -> Tuple[dict, Optional[str]]:
dynamic_api_key = self.get_api_key(api_key=api_key)

if (
"x-api-key" not in headers
and "authorization" not in headers
and dynamic_api_key is not None
):
headers["x-api-key"] = dynamic_api_key

if "anthropic-version" not in headers:
headers["anthropic-version"] = "2023-06-01"
if "content-type" not in headers:
headers["content-type"] = "application/json"

headers = self._update_headers_with_anthropic_beta(
headers=headers,
optional_params=optional_params,
custom_llm_provider=self.custom_llm_provider or "deepseek",
)

return headers, api_base

def get_complete_url(
self,
api_base: Optional[str],
api_key: Optional[str],
model: str,
optional_params: dict,
litellm_params: dict,
stream: Optional[bool] = None,
) -> str:
base_url = self.get_api_base(api_base=api_base).rstrip("/")

if base_url.endswith("/v1/messages") and "/anthropic" in base_url:
return base_url
if base_url.endswith("/v1/messages"):
base_url = base_url[: -len("/v1/messages")]
if base_url.endswith("/v1"):
base_url = base_url[: -len("/v1")]

if "/anthropic" not in base_url:
base_url = f"{base_url}/anthropic"

return f"{base_url}/v1/messages"

@staticmethod
def _sanitize_tools_for_deepseek(tools: Any) -> Any:
if not isinstance(tools, list):
return tools

sanitized_tools = []
for tool in tools:
if isinstance(tool, dict) and tool.get("type") == "custom":
sanitized_tool = dict(tool)
sanitized_tool.pop("type", None)
sanitized_tools.append(sanitized_tool)
else:
sanitized_tools.append(tool)
return sanitized_tools

def transform_anthropic_messages_request(
self,
model: str,
messages: List[Dict],
anthropic_messages_optional_request_params: Dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Dict:
anthropic_messages_request = super().transform_anthropic_messages_request(
model=model,
messages=messages,
anthropic_messages_optional_request_params=anthropic_messages_optional_request_params,
litellm_params=litellm_params,
headers=headers,
)
if "tools" in anthropic_messages_request:
anthropic_messages_request["tools"] = self._sanitize_tools_for_deepseek(
anthropic_messages_request["tools"]
)
return anthropic_messages_request
6 changes: 6 additions & 0 deletions litellm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8527,6 +8527,12 @@ def _get_provider_anthropic_messages_config_cached(
)

return MinimaxMessagesConfig()
elif litellm.LlmProviders.DEEPSEEK == provider:
from litellm.llms.deepseek.messages.transformation import (
DeepSeekAnthropicMessagesConfig,
)

return DeepSeekAnthropicMessagesConfig()
return None

@staticmethod
Expand Down
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import litellm
Comment thread
FelipeRodriguesGare marked this conversation as resolved.
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.llms.deepseek.messages.transformation import (
DeepSeekAnthropicMessagesConfig,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.utils import ProviderConfigManager


def test_deepseek_provider_uses_anthropic_messages_config():
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="deepseek-v4-pro",
provider=litellm.LlmProviders.DEEPSEEK,
)

assert isinstance(config, DeepSeekAnthropicMessagesConfig)
assert config.custom_llm_provider == "deepseek"


def test_deepseek_anthropic_messages_config_defaults():
config = DeepSeekAnthropicMessagesConfig()

assert config.custom_llm_provider == "deepseek"
assert config.get_api_base() == "https://api.deepseek.com/anthropic"


def test_anthropic_provider_keeps_default_config_for_deepseek_named_model():
config = ProviderConfigManager.get_provider_anthropic_messages_config(
model="deepseek-v4-pro",
provider=litellm.LlmProviders.ANTHROPIC,
)

assert isinstance(config, AnthropicMessagesConfig)
assert not isinstance(config, DeepSeekAnthropicMessagesConfig)


def test_deepseek_anthropic_messages_url_defaults_to_anthropic_endpoint():
config = DeepSeekAnthropicMessagesConfig()

assert (
config.get_complete_url(
api_base=None,
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)
assert (
config.get_complete_url(
api_base="https://api.deepseek.com/anthropic/v1",
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)
assert (
config.get_complete_url(
api_base="https://api.deepseek.com/anthropic",
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)
assert (
config.get_complete_url(
api_base="https://api.deepseek.com",
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)
assert (
config.get_complete_url(
api_base="https://api.deepseek.com/v1",
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)
assert (
config.get_complete_url(
api_base="https://api.deepseek.com/v1/messages",
api_key=None,
model="deepseek-v4-pro",
optional_params={},
litellm_params={},
)
== "https://api.deepseek.com/anthropic/v1/messages"
)


def test_deepseek_anthropic_messages_headers_use_deepseek_key():
config = DeepSeekAnthropicMessagesConfig()

headers, api_base = config.validate_anthropic_messages_environment(
headers={},
model="deepseek-v4-pro",
messages=[],
optional_params={},
litellm_params={},
api_key="sk-deepseek",
api_base="https://example.test/anthropic",
)

assert api_base == "https://example.test/anthropic"
assert headers["x-api-key"] == "sk-deepseek"
assert headers["anthropic-version"] == "2023-06-01"
assert headers["content-type"] == "application/json"


def test_deepseek_anthropic_messages_preserves_thinking_and_sanitizes_custom_tools():
config = DeepSeekAnthropicMessagesConfig()
messages = [
{
"role": "user",
"content": "Use the tool.",
},
{
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": "I should call the tool.",
"signature": "sig",
},
{
"type": "tool_use",
"id": "toolu_123",
"name": "get_weather",
"input": {"city": "Sao Paulo"},
},
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_123",
"content": "Sunny",
}
],
},
]

request = config.transform_anthropic_messages_request(
model="deepseek-v4-pro",
messages=messages,
anthropic_messages_optional_request_params={
"max_tokens": 100,
"thinking": {"type": "enabled", "budget_tokens": 1024},
"tools": [
{
"type": "custom",
"name": "get_weather",
"description": "Get weather",
"input_schema": {"type": "object"},
},
{
"type": "web_search_20260209",
"name": "web_search",
"max_uses": 1,
},
],
},
litellm_params=GenericLiteLLMParams(),
headers={},
)

assert request["messages"] == messages
assert request["thinking"] == {"type": "enabled", "budget_tokens": 1024}
assert request["tools"][0] == {
"name": "get_weather",
"description": "Get weather",
"input_schema": {"type": "object"},
}
assert request["tools"][1]["type"] == "web_search_20260209"
Loading