Repository: BerriAI/litellm

Issue:
[Bug]: Deepseek is unresponsive after configuration.
### What happened?

A bug happened!
Deepseek is unresponsive after configuration.

![Image](https://github.com/user-attachments/assets/fbb87e27-db72-4178-a1e5-c3d1feafb4c1)

![Image](https://github.com/user-attachments/assets/078f5192-df02-450d-b080-535f3102abb3)

### Relevant log output

```shell

```

### Are you a ML Ops Team?

No

### What LiteLLM version are you on ?

1.5

### Twitter / LinkedIn details

_No response_

Hints from issue comments:
@ohmyboroda: I'm also seeing problems since yesterday, before that everything was working.

```
Error occurred while generating model response. Please try again. Error: Error: 500 litellm.APIConnectionError: APIConnectionError: DeepseekException - 'NoneType' object has no attribute 'rstrip' Received Model Group=DeepSeek-V3 Available Model Group Fallbacks=None
```

<img width="1668" alt="Image" src="https://github.com/user-attachments/assets/2ec0f6a1-e59a-4322-8e79-c7bcc55e7bfb" />@hipur: Same here... When doing a health check, I get this:
```
   {
      "custom_llm_provider": "deepseek",
      "use_in_pass_through": false,
      "model": "deepseek/deepseek-reasoner",
      "cache": {
        "no-cache": true
      },
      "error": "litellm.APIConnectionError: APIConnectionError: DeepseekException - 'NoneType' object has no attribute 'rstrip'\nHave you set 'mode' - https://docs.litellm.ai/docs/proxy/health#embedding-models\nstack trace: Traceback (most recent call last):\n  File \"/usr/lib/python3.13/site-packages/litellm/main.py\", line 1401, in completion\n    raise e\n  File \"/usr/lib/python3.13/site-packages/litellm/main.py\", line 1376, in completion\n    response = base_llm_http_handler.completion(\n        model=model,\n    ...<13 lines>...\n        stream=stream,\n    )\n  File \"/usr/lib/python3.13/site-packages/litellm/llms/custom_httpx/llm_http_handler.py\", line 228, in completion\n    api_base = provider_config.get_complete_url(\n        api_base=api_base,\n    ...<2 lines>...\n        stream=stream,\n    )\n  File \"/usr/lib/python3.13/site-packages/litellm/llms/openai/chat/gpt_transformation.py\", line 280, in get_complete_url\n    api_base = api_base.rstrip(\"/\")\n               ^^^^^^^^^^^^^^^\nAttributeError: 'NoneType' object has no attribute 'rstrip'\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n  File \"/usr/lib/python3.13/site-packages/litellm/main.py\", line 5474, "
    },
```@admon: same here.@neubig: same here@krrishdholakia: > pi_base = api_base.rstrip(\"/\")\n               ^^^^^^^^^^^^^^^\nAttributeError: 'NoneType'

looks like this happens when the api_base is None @ishaan-jaff: here's a minimal repro 

```
response = await litellm.acompletion(
        model="deepseek/deepseek-reasoner",
        custom_llm_provider="deepseek",
        messages=[{"role": "user", "content": "Hello, world!"}],
        stream=True,
        api_base=None,
    )
```@ishaan-jaff: fixed here https://github.com/BerriAI/litellm/pull/8860 @github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

Solution patch:
diff --git a/litellm/litellm_core_utils/streaming_handler.py b/litellm/litellm_core_utils/streaming_handler.py
index a5bc08ef1..a1d29f328 100644
--- a/litellm/litellm_core_utils/streaming_handler.py
+++ b/litellm/litellm_core_utils/streaming_handler.py
@@ -714,7 +714,7 @@ class CustomStreamWrapper:
 
     def is_delta_empty(self, delta: Delta) -> bool:
         is_empty = True
-        if delta.content is not None:
+        if delta.content:
             is_empty = False
         elif delta.tool_calls is not None:
             is_empty = False
@@ -879,6 +879,9 @@ class CustomStreamWrapper:
             _is_delta_empty = self.is_delta_empty(delta=model_response.choices[0].delta)
 
             if _is_delta_empty:
+                model_response.choices[0].delta = Delta(
+                    content=None
+                )  # ensure empty delta chunk returned
                 # get any function call arguments
                 model_response.choices[0].finish_reason = map_finish_reason(
                     finish_reason=self.received_finish_reason
diff --git a/litellm/llms/aiohttp_openai/chat/transformation.py b/litellm/llms/aiohttp_openai/chat/transformation.py
index 53157ad11..625704dbe 100644
--- a/litellm/llms/aiohttp_openai/chat/transformation.py
+++ b/litellm/llms/aiohttp_openai/chat/transformation.py
@@ -26,7 +26,7 @@ else:
 class AiohttpOpenAIChatConfig(OpenAILikeChatConfig):
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
@@ -35,6 +35,8 @@ class AiohttpOpenAIChatConfig(OpenAILikeChatConfig):
         Ensure - /v1/chat/completions is at the end of the url
 
         """
+        if api_base is None:
+            api_base = "https://api.openai.com"
 
         if not api_base.endswith("/chat/completions"):
             api_base += "/chat/completions"
diff --git a/litellm/llms/azure_ai/chat/transformation.py b/litellm/llms/azure_ai/chat/transformation.py
index 2815eaa14..46a1a6bf9 100644
--- a/litellm/llms/azure_ai/chat/transformation.py
+++ b/litellm/llms/azure_ai/chat/transformation.py
@@ -51,7 +51,7 @@ class AzureAIStudioConfig(OpenAIConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
@@ -72,6 +72,10 @@ class AzureAIStudioConfig(OpenAIConfig):
         - A complete URL string, e.g.,
         "https://litellm8397336933.services.ai.azure.com/models/chat/completions?api-version=2024-05-01-preview"
         """
+        if api_base is None:
+            raise ValueError(
+                f"api_base is required for Azure AI Studio. Please set the api_base parameter. Passed `api_base={api_base}`"
+            )
         original_url = httpx.URL(api_base)
 
         # Extract api_version or use default
diff --git a/litellm/llms/base_llm/chat/transformation.py b/litellm/llms/base_llm/chat/transformation.py
index ac82476a0..020223f98 100644
--- a/litellm/llms/base_llm/chat/transformation.py
+++ b/litellm/llms/base_llm/chat/transformation.py
@@ -249,7 +249,7 @@ class BaseConfig(ABC):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
@@ -261,6 +261,8 @@ class BaseConfig(ABC):
 
         Some providers need `model` in `api_base`
         """
+        if api_base is None:
+            raise ValueError("api_base is required")
         return api_base
 
     @abstractmethod
diff --git a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py
index b7d4f0ae6..e98cb4fa9 100644
--- a/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py
+++ b/litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py
@@ -73,7 +73,7 @@ class AmazonInvokeConfig(BaseConfig, BaseAWSLLM):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
diff --git a/litellm/llms/cloudflare/chat/transformation.py b/litellm/llms/cloudflare/chat/transformation.py
index 1ef6da5a4..555e3c21f 100644
--- a/litellm/llms/cloudflare/chat/transformation.py
+++ b/litellm/llms/cloudflare/chat/transformation.py
@@ -11,6 +11,7 @@ from litellm.llms.base_llm.chat.transformation import (
     BaseLLMException,
     LiteLLMLoggingObj,
 )
+from litellm.secret_managers.main import get_secret_str
 from litellm.types.llms.openai import AllMessageValues
 from litellm.types.utils import (
     ChatCompletionToolCallChunk,
@@ -75,11 +76,16 @@ class CloudflareChatConfig(BaseConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
     ) -> str:
+        if api_base is None:
+            account_id = get_secret_str("CLOUDFLARE_ACCOUNT_ID")
+            api_base = (
+                f"https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/run/"
+            )
         return api_base + model
 
     def get_supported_openai_params(self, model: str) -> List[str]:
diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py
index e6704de1a..747129ddd 100644
--- a/litellm/llms/deepseek/chat/transformation.py
+++ b/litellm/llms/deepseek/chat/transformation.py
@@ -34,3 +34,21 @@ class DeepSeekChatConfig(OpenAIGPTConfig):
         )  # type: ignore
         dynamic_api_key = api_key or get_secret_str("DEEPSEEK_API_KEY")
         return api_base, dynamic_api_key
+
+    def get_complete_url(
+        self,
+        api_base: Optional[str],
+        model: str,
+        optional_params: dict,
+        stream: Optional[bool] = None,
+    ) -> str:
+        """
+        If api_base is not provided, use the default DeepSeek /chat/completions endpoint.
+        """
+        if not api_base:
+            api_base = "https://api.deepseek.com/beta"
+
+        if not api_base.endswith("/chat/completions"):
+            api_base = f"{api_base}/chat/completions"
+
+        return api_base
diff --git a/litellm/llms/ollama/completion/transformation.py b/litellm/llms/ollama/completion/transformation.py
index da981b6af..283b2a243 100644
--- a/litellm/llms/ollama/completion/transformation.py
+++ b/litellm/llms/ollama/completion/transformation.py
@@ -353,7 +353,7 @@ class OllamaConfig(BaseConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
@@ -365,6 +365,8 @@ class OllamaConfig(BaseConfig):
 
         Some providers need `model` in `api_base`
         """
+        if api_base is None:
+            api_base = "http://localhost:11434"
         if api_base.endswith("/api/generate"):
             url = api_base
         else:
diff --git a/litellm/llms/openai/chat/gpt_transformation.py b/litellm/llms/openai/chat/gpt_transformation.py
index 84a57bbaa..9c1f177fc 100644
--- a/litellm/llms/openai/chat/gpt_transformation.py
+++ b/litellm/llms/openai/chat/gpt_transformation.py
@@ -263,7 +263,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
@@ -274,6 +274,8 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
         Returns:
             str: The complete URL for the API call.
         """
+        if api_base is None:
+            api_base = "https://api.openai.com"
         endpoint = "chat/completions"
 
         # Remove trailing slash from api_base if present
diff --git a/litellm/llms/replicate/chat/transformation.py b/litellm/llms/replicate/chat/transformation.py
index e9934dada..39aaad680 100644
--- a/litellm/llms/replicate/chat/transformation.py
+++ b/litellm/llms/replicate/chat/transformation.py
@@ -138,7 +138,7 @@ class ReplicateConfig(BaseConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
diff --git a/litellm/llms/watsonx/chat/transformation.py b/litellm/llms/watsonx/chat/transformation.py
index 208da82ef..d5e0ed654 100644
--- a/litellm/llms/watsonx/chat/transformation.py
+++ b/litellm/llms/watsonx/chat/transformation.py
@@ -80,7 +80,7 @@ class IBMWatsonXChatConfig(IBMWatsonXMixin, OpenAIGPTConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,
diff --git a/litellm/llms/watsonx/completion/transformation.py b/litellm/llms/watsonx/completion/transformation.py
index ebebbde02..7a4df2394 100644
--- a/litellm/llms/watsonx/completion/transformation.py
+++ b/litellm/llms/watsonx/completion/transformation.py
@@ -315,7 +315,7 @@ class IBMWatsonXAIConfig(IBMWatsonXMixin, BaseConfig):
 
     def get_complete_url(
         self,
-        api_base: str,
+        api_base: Optional[str],
         model: str,
         optional_params: dict,
         stream: Optional[bool] = None,


Test patch:
diff --git a/tests/llm_translation/test_deepseek_completion.py b/tests/llm_translation/test_deepseek_completion.py
index a07bf3ffe..703c9a33a 100644
--- a/tests/llm_translation/test_deepseek_completion.py
+++ b/tests/llm_translation/test_deepseek_completion.py
@@ -47,26 +47,67 @@ def test_deepseek_mock_completion(stream):
         assert response is not None
 
 
-@pytest.mark.parametrize("stream", [True, False])
+@pytest.mark.parametrize("stream", [False, True])
 @pytest.mark.asyncio
-async def test_deepseek_mock_async_completion(stream):
+async def test_deepseek_provider_async_completion(stream):
     """
-    Deepseek API is hanging. Mock the call, to a fake endpoint, so we can confirm our integration is working.
+    Test that Deepseek provider requests are formatted correctly with the proper parameters
     """
     import litellm
-    from litellm import completion, acompletion
+    import json
+    from unittest.mock import patch, AsyncMock, MagicMock
+    from litellm import acompletion
 
     litellm._turn_on_debug()
 
-    response = await acompletion(
-        model="deepseek/deepseek-reasoner",
-        messages=[{"role": "user", "content": "Hello, world!"}],
-        api_base="https://exampleopenaiendpoint-production.up.railway.app/v1/chat/completions",
-        stream=stream,
-    )
-    print(f"response: {response}")
-    if stream:
-        async for chunk in response:
-            print(chunk)
-    else:
-        assert response is not None
+    # Set up the test parameters
+    api_key = "fake_api_key"
+    model = "deepseek/deepseek-reasoner"
+    messages = [{"role": "user", "content": "Hello, world!"}]
+
+    # Mock AsyncHTTPHandler.post method for async test
+    with patch(
+        "litellm.llms.custom_httpx.llm_http_handler.AsyncHTTPHandler.post"
+    ) as mock_post:
+        mock_response_data = litellm.ModelResponse(
+            choices=[
+                litellm.Choices(
+                    message=litellm.Message(content="Hello!"),
+                    index=0,
+                    finish_reason="stop",
+                )
+            ]
+        ).model_dump()
+        # Create a proper mock response
+        mock_response = MagicMock()  # Use MagicMock instead of AsyncMock
+        mock_response.status_code = 200
+        mock_response.text = json.dumps(mock_response_data)
+        mock_response.headers = {"Content-Type": "application/json"}
+
+        # Make json() return a value directly, not a coroutine
+        mock_response.json.return_value = mock_response_data
+
+        # Set the return value for the post method
+        mock_post.return_value = mock_response
+
+        await acompletion(
+            custom_llm_provider="deepseek",
+            api_key=api_key,
+            model=model,
+            messages=messages,
+            stream=stream,
+        )
+
+    # Verify the request was made with the correct parameters
+    mock_post.assert_called_once()
+    call_args = mock_post.call_args
+    print("request call=", json.dumps(call_args.kwargs, indent=4, default=str))
+
+    # Check request body
+    request_body = json.loads(call_args.kwargs["data"])
+    assert call_args.kwargs["url"] == "https://api.deepseek.com/beta/chat/completions"
+    assert (
+        request_body["model"] == "deepseek-reasoner"
+    )  # Model name should be stripped of provider prefix
+    assert request_body["messages"] == messages
+    assert request_body["stream"] == stream
diff --git a/tests/local_testing/test_streaming.py b/tests/local_testing/test_streaming.py
index 5301ba1ef..f438fdc5c 100644
--- a/tests/local_testing/test_streaming.py
+++ b/tests/local_testing/test_streaming.py
@@ -4095,3 +4095,26 @@ def test_reasoning_content_completion(model):
         assert reasoning_content_exists
     except litellm.Timeout:
         pytest.skip("Model is timing out")
+
+
+def test_is_delta_empty():
+    from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper
+    from litellm.types.utils import Delta
+
+    custom_stream_wrapper = CustomStreamWrapper(
+        completion_stream=None,
+        model=None,
+        logging_obj=MagicMock(),
+        custom_llm_provider=None,
+        stream_options=None,
+    )
+
+    assert custom_stream_wrapper.is_delta_empty(
+        delta=Delta(
+            content="",
+            role="assistant",
+            function_call=None,
+            tool_calls=None,
+            audio=None,
+        )
+    )