""" Unit tests for Replicate provider, particularly testing DeepSeek models """ import asyncio import json import os import sys from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest sys.path.insert(0, os.path.abspath("../..")) import litellm from litellm import completion from litellm.llms.replicate.chat.handler import ( async_completion, completion as replicate_completion, ) class TestReplicateStartingStatus: """Test that Replicate handler correctly handles 'starting' status for DeepSeek models""" @pytest.mark.asyncio @patch("litellm.llms.replicate.chat.handler.get_async_httpx_client") async def test_async_completion_handles_starting_status( self, mock_get_client ): """Test that async completion polls correctly when status is 'starting'""" # Mock the async HTTP client mock_client = AsyncMock() mock_get_client.return_value = mock_client # Mock the initial POST response (creates prediction) post_response = Mock() post_response.json.return_value = { "id": "test-prediction-id", "urls": { "get": "https://api.replicate.com/v1/predictions/test-id", "cancel": "https://api.replicate.com/v1/predictions/test-id/cancel", }, } mock_client.post = AsyncMock(return_value=post_response) # Mock GET responses - first 'starting', then 'processing', then 'succeeded' get_response_starting = Mock() get_response_starting.status_code = 200 get_response_starting.json.return_value = { "id": "test-prediction-id", "status": "starting", "output": None, } get_response_processing = Mock() get_response_processing.status_code = 200 get_response_processing.json.return_value = { "id": "test-prediction-id", "status": "processing", "output": None, } get_response_succeeded = Mock() get_response_succeeded.status_code = 200 get_response_succeeded.json.return_value = { "id": "test-prediction-id", "status": "succeeded", "output": ["Hello", " from", " DeepSeek!"], } get_response_succeeded.text = json.dumps( get_response_succeeded.json.return_value ) get_response_succeeded.headers = {} # Configure mock to return different responses on successive calls mock_client.get = AsyncMock( side_effect=[ get_response_starting, get_response_processing, get_response_succeeded, ] ) # Create mock model response model_response = litellm.ModelResponse() model_response.choices = [litellm.Choices()] model_response.choices[0].message = litellm.Message(content="") # Create mock logging object mock_logging = Mock() mock_logging.post_call = Mock() # Call async_completion result = await async_completion( model_response=model_response, model="deepseek-ai/deepseek-v3", messages=[{"role": "user", "content": "Hi"}], encoding=None, optional_params={}, litellm_params={}, version_id="deepseek-ai/deepseek-v3", input_data={"input": {"prompt": "test"}}, api_key="test-key", api_base="https://api.replicate.com", logging_obj=mock_logging, print_verbose=print, headers={"Authorization": "Token test-key"}, ) # Assert that we got responses assert result is not None assert result.choices[0].message.content == "Hello from DeepSeek!" # Verify that GET was called 3 times (starting, processing, succeeded) assert mock_client.get.call_count == 3 @patch("litellm.llms.replicate.chat.handler._get_httpx_client") def test_sync_completion_handles_starting_status(self, mock_get_client): """Test that sync completion polls correctly when status is 'starting'""" # Mock the sync HTTP client mock_client = Mock() mock_get_client.return_value = mock_client # Mock the initial POST response post_response = Mock() post_response.json.return_value = { "id": "test-prediction-id", "urls": { "get": "https://api.replicate.com/v1/predictions/test-id", "cancel": "https://api.replicate.com/v1/predictions/test-id/cancel", }, } mock_client.post.return_value = post_response # Mock GET responses get_response_starting = Mock() get_response_starting.status_code = 200 get_response_starting.json.return_value = { "id": "test-prediction-id", "status": "starting", "output": None, } get_response_succeeded = Mock() get_response_succeeded.status_code = 200 get_response_succeeded.json.return_value = { "id": "test-prediction-id", "status": "succeeded", "output": ["Hello", " DeepSeek!"], } get_response_succeeded.text = json.dumps( get_response_succeeded.json.return_value ) get_response_succeeded.headers = {} # Configure mock to return different responses mock_client.get.side_effect = [get_response_starting, get_response_succeeded] # Create mock objects model_response = litellm.ModelResponse() model_response.choices = [litellm.Choices()] model_response.choices[0].message = litellm.Message(content="") mock_logging = Mock() mock_logging.post_call = Mock() # Call completion with mock_response to avoid actual API call with patch("time.sleep"): # Skip sleep delays in test result = replicate_completion( model="deepseek-ai/deepseek-v3", messages=[{"role": "user", "content": "Hi"}], api_base="https://api.replicate.com", model_response=model_response, print_verbose=print, optional_params={}, litellm_params={}, logging_obj=mock_logging, api_key="test-key", encoding=None, headers={}, ) # Assert results assert result is not None assert result.choices[0].message.content == "Hello DeepSeek!" # Verify GET was called multiple times assert mock_client.get.call_count >= 1 class TestReplicateOutputFormats: """Test that Replicate handler handles different output formats from models""" def test_transform_response_list_output(self): """Test standard list output format""" from litellm.llms.replicate.chat.transformation import ReplicateConfig config = ReplicateConfig() # Mock response with list output mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { "status": "succeeded", "output": ["Hello", " ", "world"], } mock_response.text = json.dumps(mock_response.json.return_value) mock_response.headers = {} model_response = litellm.ModelResponse() model_response.choices = [litellm.Choices()] model_response.choices[0].message = litellm.Message(content="") mock_logging = Mock() mock_logging.post_call = Mock() result = config.transform_response( model="meta/llama-2-70b-chat", raw_response=mock_response, model_response=model_response, logging_obj=mock_logging, request_data={"input": {"prompt": "test"}}, messages=[{"role": "user", "content": "Hi"}], optional_params={}, litellm_params={}, encoding=None, api_key="test-key", ) assert result.choices[0].message.content == "Hello world" def test_transform_response_string_output(self): """Test string output format (as used by some DeepSeek models)""" from litellm.llms.replicate.chat.transformation import ReplicateConfig config = ReplicateConfig() # Mock response with string output mock_response = Mock() mock_response.status_code = 200 mock_response.json.return_value = { "status": "succeeded", "output": "Hello from DeepSeek", } mock_response.text = json.dumps(mock_response.json.return_value) mock_response.headers = {} model_response = litellm.ModelResponse() model_response.choices = [litellm.Choices()] model_response.choices[0].message = litellm.Message(content="") mock_logging = Mock() mock_logging.post_call = Mock() result = config.transform_response( model="deepseek-ai/deepseek-v3", raw_response=mock_response, model_response=model_response, logging_obj=mock_logging, request_data={"input": {"prompt": "test"}}, messages=[{"role": "user", "content": "Hi"}], optional_params={}, litellm_params={}, encoding=None, api_key="test-key", ) assert result.choices[0].message.content == "Hello from DeepSeek" # Integration test (requires actual API key - skip in CI) @pytest.mark.skip(reason="Requires REPLICATE_API_KEY environment variable") def test_replicate_deepseek_integration(): """Integration test with actual DeepSeek model on Replicate""" try: response = completion( model="replicate/deepseek-ai/deepseek-v3", messages=[{"role": "user", "content": "Say 'Hello World' and nothing else"}], max_tokens=20, ) assert response is not None assert response.choices[0].message.content is not None assert len(response.choices[0].message.content) > 0 print(f"Response: {response.choices[0].message.content}") except Exception as e: pytest.fail(f"Integration test failed: {e}")