diff --git a/litellm/llms/aiohttp_openai/chat/transformation.py b/litellm/llms/aiohttp_openai/chat/transformation.py index 53157ad113..625704dbea 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 2815eaa14c..46a1a6bf9c 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 ac82476a0a..020223f98e 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 b7d4f0ae6d..e98cb4fa94 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 1ef6da5a4b..555e3c21f4 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 e6704de1a1..747129ddd8 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 da981b6afb..283b2a2437 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 84a57bbaa6..9c1f177fc1 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 e9934dada8..39aaad6808 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 208da82ef5..d5e0ed6544 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 ebebbde021..7a4df23944 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, diff --git a/tests/llm_translation/test_deepseek_completion.py b/tests/llm_translation/test_deepseek_completion.py index a07bf3ffe8..703c9a33ab 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