From f266d418feb31ec5f9e9cc7e7a406af64735d7bd Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 18 May 2026 16:05:53 -0300 Subject: [PATCH 1/4] fix(deepseek): route messages api through anthropic config Add a DeepSeek-specific Anthropic Messages config so deepseek/... models use the native messages endpoint and preserve thinking blocks. Strip Anthropic custom tool type markers that DeepSeek rejects while keeping hosted tool types intact. --- .../llms/deepseek/messages/transformation.py | 118 ++++++++++++ litellm/utils.py | 6 + ...pseek_anthropic_messages_transformation.py | 169 ++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 litellm/llms/deepseek/messages/transformation.py create mode 100644 tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py diff --git a/litellm/llms/deepseek/messages/transformation.py b/litellm/llms/deepseek/messages/transformation.py new file mode 100644 index 000000000000..a0e8db077ae0 --- /dev/null +++ b/litellm/llms/deepseek/messages/transformation.py @@ -0,0 +1,118 @@ +""" +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"): + return base_url + if base_url.endswith("/v1"): + return f"{base_url}/messages" + + 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 diff --git a/litellm/utils.py b/litellm/utils.py index cefd348078b4..f6cfab8279c2 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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 diff --git a/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py b/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py new file mode 100644 index 000000000000..52e2e1c35453 --- /dev/null +++ b/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py @@ -0,0 +1,169 @@ +import litellm +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" + ) + + +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" From 479b93f0f3ed8644b12b8785118ec0d92657462e Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 18 May 2026 16:48:55 -0300 Subject: [PATCH 2/4] fix(deepseek): normalize anthropic messages api base Handle OpenAI-style DeepSeek api_base values ending in /v1 or /v1/messages by stripping those suffixes before adding the /anthropic messages path. --- .../llms/deepseek/messages/transformation.py | 6 ++++-- ...pseek_anthropic_messages_transformation.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/litellm/llms/deepseek/messages/transformation.py b/litellm/llms/deepseek/messages/transformation.py index a0e8db077ae0..8ce3ab1b68e5 100644 --- a/litellm/llms/deepseek/messages/transformation.py +++ b/litellm/llms/deepseek/messages/transformation.py @@ -73,10 +73,12 @@ def get_complete_url( ) -> str: base_url = self.get_api_base(api_base=api_base).rstrip("/") - if base_url.endswith("/v1/messages"): + 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"): - return f"{base_url}/messages" + base_url = base_url[: -len("/v1")] if "/anthropic" not in base_url: base_url = f"{base_url}/anthropic" diff --git a/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py b/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py index 52e2e1c35453..7c5f0483ded7 100644 --- a/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py +++ b/tests/test_litellm/llms/deepseek/messages/test_deepseek_anthropic_messages_transformation.py @@ -79,6 +79,26 @@ def test_deepseek_anthropic_messages_url_defaults_to_anthropic_endpoint(): ) == "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(): From d116f9bca2f3c19829e31076db96e889b59d84d6 Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 18 May 2026 17:07:34 -0300 Subject: [PATCH 3/4] chore(deepseek): format messages transformation --- litellm/llms/deepseek/messages/transformation.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/litellm/llms/deepseek/messages/transformation.py b/litellm/llms/deepseek/messages/transformation.py index 8ce3ab1b68e5..0bfc59cc2466 100644 --- a/litellm/llms/deepseek/messages/transformation.py +++ b/litellm/llms/deepseek/messages/transformation.py @@ -32,7 +32,11 @@ def get_api_key(api_key: Optional[str] = None) -> Optional[str]: @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" + return ( + api_base + or get_secret_str("DEEPSEEK_ANTHROPIC_API_BASE") + or "https://api.deepseek.com/anthropic" + ) def validate_anthropic_messages_environment( self, @@ -46,7 +50,11 @@ def validate_anthropic_messages_environment( ) -> 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: + 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: @@ -116,5 +124,7 @@ def transform_anthropic_messages_request( headers=headers, ) if "tools" in anthropic_messages_request: - anthropic_messages_request["tools"] = self._sanitize_tools_for_deepseek(anthropic_messages_request["tools"]) + anthropic_messages_request["tools"] = self._sanitize_tools_for_deepseek( + anthropic_messages_request["tools"] + ) return anthropic_messages_request From b2a6ce74c41603132de2b07307c41912712b217c Mon Sep 17 00:00:00 2001 From: Felipe Rodrigues Gare Carnielli Date: Mon, 18 May 2026 17:48:55 -0300 Subject: [PATCH 4/4] chore(deepseek): add test package markers --- tests/test_litellm/llms/deepseek/__init__.py | 0 tests/test_litellm/llms/deepseek/messages/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/test_litellm/llms/deepseek/__init__.py create mode 100644 tests/test_litellm/llms/deepseek/messages/__init__.py diff --git a/tests/test_litellm/llms/deepseek/__init__.py b/tests/test_litellm/llms/deepseek/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/test_litellm/llms/deepseek/messages/__init__.py b/tests/test_litellm/llms/deepseek/messages/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1