Repository: kserve/kserve

Issue:
Python SDK: Optional `InferInput.parameters` missing from `InferRequest.to_rest()/.to_grpc()`
/kind bug

**What steps did you take and what happened:**
Hi! In attempting to pass `InferInput.parameters` from a transformer to a predictor I realised the request reaching the predictor had completely removed the parameters.

This can be replicated with:

```python
import numpy as np
from kserve import InferRequest, InferInput

test_array = np.zeros((2, 2, 1), dtype="uint8")

test_input = InferInput(name="test", shape=test_array.shape, datatype="UINT8", parameters={"hello": "world"})
test_input.set_data_from_numpy(test_array)

test_infer = InferRequest(model_name="test", infer_inputs=[test_input])

print(test_infer.inputs[0].parameters)

test_infer.to_rest()
```

Output:
```terminal
{'hello': 'world', 'binary_data_size': 4}

{'id': None,
 'inputs': [{'name': 'test',
   'shape': (2, 2, 1),
   'datatype': 'UINT8',
   'data': None}]}
```

And is expected from looking at the code as the parameters are not handled:

https://github.com/kserve/kserve/blob/c52753519e1802e73581856b1ae3a0f2f178d457/python/kserve/kserve/protocol/infer_type.py#L271-L329

Which is called by the `_X_predict` functions in the `kserve.Model` class.

**What did you expect to happen:**
I expect the parameters to be preserved when using the `to_rest/grpc` methods.

Am I mis-understanding something about parameters in that this is expected behaviour?

**What's the InferenceService yaml:**

N/A

**Anything else you would like to add:**

Would appear to be the same for `InferOutput`.

**Environment:**

- KServe Version: 0.11.2



Discussion:

Resolving patch:
diff --git a/python/kserve/kserve/protocol/infer_type.py b/python/kserve/kserve/protocol/infer_type.py
--- a/python/kserve/kserve/protocol/infer_type.py
+++ b/python/kserve/kserve/protocol/infer_type.py
@@ -49,8 +49,7 @@ def __init__(self, name: str, shape: List[int], datatype: str,
                    by using `set_data_from_numpy`.
             parameters : The additional inference parameters.
         """
-        if parameters is None:
-            parameters = {}
+
         self._name = name
         self._shape = shape
         self._datatype = datatype
@@ -95,7 +94,7 @@ def shape(self) -> List[int]:
         return self._shape
 
     @property
-    def parameters(self) -> Union[Dict, MessageMap[str, InferParameter]]:
+    def parameters(self) -> Union[Dict, MessageMap[str, InferParameter], None]:
         """Get the parameters of the inference input associated with this object.
 
         Returns:
@@ -161,7 +160,8 @@ def set_data_from_numpy(self, input_tensor: np.ndarray, binary_data: bool = True
                     str(self._shape)[1:-1]))
 
         if not binary_data:
-            self._parameters.pop('binary_data_size', None)
+            if self._parameters:
+                self._parameters.pop('binary_data_size', None)
             self._raw_data = None
             if self._datatype == "BYTES":
                 self._data = []
@@ -198,7 +198,25 @@ def set_data_from_numpy(self, input_tensor: np.ndarray, binary_data: bool = True
                     self._raw_data = b''
             else:
                 self._raw_data = input_tensor.tobytes()
-            self._parameters['binary_data_size'] = len(self._raw_data)
+            if self._parameters is None:
+                self._parameters = {'binary_data_size': len(self._raw_data)}
+            else:
+                self._parameters['binary_data_size'] = len(self._raw_data)
+
+    def __eq__(self, other):
+        if not isinstance(other, InferInput):
+            return False
+        if self.name != other.name:
+            return False
+        if self.shape != other.shape:
+            return False
+        if self.datatype != other.datatype:
+            return False
+        if self.parameters != other.parameters:
+            return False
+        if self.data != other.data:
+            return False
+        return True
 
 
 def get_content(datatype: str, data: InferTensorContents):
@@ -244,8 +262,7 @@ def __init__(self, model_name: str, infer_inputs: List[InferInput],
             from_grpc: Indicate if the data model is constructed from gRPC request.
             parameters: The additional inference parameters.
         """
-        if parameters is None:
-            parameters = {}
+
         self.id = request_id
         self.model_name = model_name
         self.inputs = infer_inputs
@@ -281,6 +298,8 @@ def to_rest(self) -> Dict:
                 "shape": infer_input.shape,
                 "datatype": infer_input.datatype
             }
+            if infer_input.parameters:
+                infer_input_dict["parameters"] = to_http_parameters(infer_input.parameters)
             if isinstance(infer_input.data, numpy.ndarray):
                 infer_input.set_data_from_numpy(infer_input.data, binary_data=False)
                 infer_input_dict["data"] = infer_input.data
@@ -291,6 +310,8 @@ def to_rest(self) -> Dict:
             'id': self.id if self.id else str(uuid.uuid4()),
             'inputs': infer_inputs
         }
+        if self.parameters:
+            infer_request['parameters'] = to_http_parameters(self.parameters)
         return infer_request
 
     def to_grpc(self) -> ModelInferRequest:
@@ -309,6 +330,8 @@ def to_grpc(self) -> ModelInferRequest:
                 "shape": infer_input.shape,
                 "datatype": infer_input.datatype,
             }
+            if infer_input.parameters:
+                infer_input_dict["parameters"] = to_grpc_parameters(infer_input.parameters)
             if infer_input._raw_data is not None:
                 raw_input_contents.append(infer_input._raw_data)
             else:
@@ -326,7 +349,8 @@ def to_grpc(self) -> ModelInferRequest:
             infer_inputs.append(infer_input_dict)
 
         return ModelInferRequest(id=self.id, model_name=self.model_name, inputs=infer_inputs,
-                                 raw_input_contents=raw_input_contents)
+                                 raw_input_contents=raw_input_contents,
+                                 parameters=to_grpc_parameters(self.parameters) if self.parameters else None)
 
     def as_dataframe(self) -> pd.DataFrame:
         """ Decode the tensor inputs as pandas dataframe.
@@ -343,6 +367,21 @@ def as_dataframe(self) -> pd.DataFrame:
             dfs.append(pd.DataFrame(input_data, columns=[input.name]))
         return pd.concat(dfs, axis=1)
 
+    def __eq__(self, other):
+        if not isinstance(other, InferRequest):
+            return False
+        if self.model_name != other.model_name:
+            return False
+        if self.id != other.id:
+            return False
+        if self.from_grpc != other.from_grpc:
+            return False
+        if self.parameters != other.parameters:
+            return False
+        if self.inputs != other.inputs:
+            return False
+        return True
+
 
 class InferOutput:
     def __init__(self, name: str, shape: List[int], datatype: str,
@@ -358,8 +397,7 @@ def __init__(self, name: str, shape: List[int], datatype: str,
                    raw_data is used for gRPC with numpy array bytes by calling set_data_from_numpy.
             parameters : The additional inference parameters.
         """
-        if parameters is None:
-            parameters = {}
+
         self._name = name
         self._shape = shape
         self._datatype = datatype
@@ -404,7 +442,7 @@ def shape(self) -> List[int]:
         return self._shape
 
     @property
-    def parameters(self) -> Union[Dict, MessageMap[str, InferParameter]]:
+    def parameters(self) -> Union[Dict, MessageMap[str, InferParameter], None]:
         """Get the parameters of inference output associated with this object.
 
         Returns:
@@ -470,7 +508,8 @@ def set_data_from_numpy(self, output_tensor: np.ndarray, binary_data: bool = Tru
                     str(self._shape)[1:-1]))
 
         if not binary_data:
-            self._parameters.pop('binary_data_size', None)
+            if self._parameters:
+                self._parameters.pop('binary_data_size', None)
             self._raw_data = None
             if self._datatype == "BYTES":
                 self._data = []
@@ -507,7 +546,25 @@ def set_data_from_numpy(self, output_tensor: np.ndarray, binary_data: bool = Tru
                     self._raw_data = b''
             else:
                 self._raw_data = output_tensor.tobytes()
-            self._parameters['binary_data_size'] = len(self._raw_data)
+            if self._parameters is None:
+                self._parameters = {'binary_data_size': len(self._raw_data)}
+            else:
+                self._parameters['binary_data_size'] = len(self._raw_data)
+
+    def __eq__(self, other):
+        if not isinstance(other, InferOutput):
+            return False
+        if self.name != other.name:
+            return False
+        if self.shape != other.shape:
+            return False
+        if self.datatype != other.datatype:
+            return False
+        if self.parameters != other.parameters:
+            return False
+        if self.data != other.data:
+            return False
+        return True
 
 
 class InferResponse:
@@ -530,8 +587,7 @@ def __init__(self, response_id: str, model_name: str, infer_outputs: List[InferO
             from_grpc: Indicate if the InferResponse is constructed from a gRPC response.
             parameters: The additional inference parameters.
         """
-        if parameters is None:
-            parameters = {}
+
         self.id = response_id
         self.model_name = model_name
         self.outputs = infer_outputs
@@ -562,11 +618,11 @@ def from_rest(cls, model_name: str, response: Dict) -> 'InferResponse':
                                      shape=list(output['shape']),
                                      datatype=output['datatype'],
                                      data=output['data'],
-                                     parameters=output.get('parameters', {}))
+                                     parameters=output.get('parameters', None))
                          for output in response['outputs']]
         return cls(model_name=model_name,
                    response_id=response.get('id', None),
-                   parameters=response.get('parameters', {}),
+                   parameters=response.get('parameters', None),
                    infer_outputs=infer_outputs)
 
     def to_rest(self) -> Dict:
@@ -582,6 +638,8 @@ def to_rest(self) -> Dict:
                 "shape": infer_output.shape,
                 "datatype": infer_output.datatype
             }
+            if infer_output.parameters:
+                infer_output_dict["parameters"] = to_http_parameters(infer_output.parameters)
             if isinstance(infer_output.data, numpy.ndarray):
                 infer_output.set_data_from_numpy(infer_output.data, binary_data=False)
                 infer_output_dict["data"] = infer_output.data
@@ -595,6 +653,8 @@ def to_rest(self) -> Dict:
             'model_name': self.model_name,
             'outputs': infer_outputs
         }
+        if self.parameters:
+            res['parameters'] = to_http_parameters(self.parameters)
         return res
 
     def to_grpc(self) -> ModelInferResponse:
@@ -613,6 +673,8 @@ def to_grpc(self) -> ModelInferResponse:
                 "shape": infer_output.shape,
                 "datatype": infer_output.datatype,
             }
+            if infer_output.parameters:
+                infer_output_dict["parameters"] = to_grpc_parameters(infer_output.parameters)
             if infer_output._raw_data is not None:
                 raw_output_contents.append(infer_output._raw_data)
             else:
@@ -630,4 +692,65 @@ def to_grpc(self) -> ModelInferResponse:
             infer_outputs.append(infer_output_dict)
 
         return ModelInferResponse(id=self.id, model_name=self.model_name, outputs=infer_outputs,
-                                  raw_output_contents=raw_output_contents)
+                                  raw_output_contents=raw_output_contents,
+                                  parameters=to_grpc_parameters(self.parameters) if self.parameters else None)
+
+    def __eq__(self, other):
+        if not isinstance(other, InferResponse):
+            return False
+        if self.model_name != other.model_name:
+            return False
+        if self.id != other.id:
+            return False
+        if self.from_grpc != other.from_grpc:
+            return False
+        if self.parameters != other.parameters:
+            return False
+        if self.outputs != other.outputs:
+            return False
+        return True
+
+
+def to_grpc_parameters(parameters: Union[Dict[str, Union[str, bool, int]], MessageMap[str, InferParameter]]) \
+        -> Dict[str, InferParameter]:
+    """
+    Converts REST parameters to GRPC InferParameter objects
+
+    :param parameters: parameters to be converted.
+    :return: converted parameters as Dict[str, InferParameter]
+    :raises InvalidInput: if the parameter type is not supported.
+    """
+    grpc_params: Dict[str, InferParameter] = {}
+    for key, val in parameters.items():
+        if isinstance(val, str):
+            grpc_params[key] = InferParameter(string_param=val)
+        elif isinstance(val, bool):
+            grpc_params[key] = InferParameter(bool_param=val)
+        elif isinstance(val, int):
+            grpc_params[key] = InferParameter(int64_param=val)
+        elif isinstance(val, InferParameter):
+            grpc_params[key] = val
+        else:
+            raise InvalidInput(f"to_grpc: invalid parameter value: {val}")
+    return grpc_params
+
+
+def to_http_parameters(parameters: Union[dict, MessageMap[str, InferParameter]]) -> Dict[str, Union[str, bool, int]]:
+    """
+        Converts GRPC InferParameter parameters to REST parameters
+
+        :param parameters: parameters to be converted.
+        :return: converted parameters as Dict[str, Union[str, bool, int]]
+        """
+    http_params: Dict[str, Union[str, bool, int]] = {}
+    for key, val in parameters.items():
+        if isinstance(val, InferParameter):
+            if val.HasField("bool_param"):
+                http_params[key] = val.bool_param
+            elif val.HasField("int64_param"):
+                http_params[key] = val.int64_param
+            elif val.HasField("string_param"):
+                http_params[key] = val.string_param
+        else:
+            http_params[key] = val
+    return http_params


Test patch:
diff --git a/python/kserve/test/test_infer_type.py b/python/kserve/test/test_infer_type.py
new file mode 100644
--- /dev/null
+++ b/python/kserve/test/test_infer_type.py
@@ -0,0 +1,258 @@
+# Copyright 2024 The KServe Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from kserve import InferRequest, InferInput, InferResponse, InferOutput
+from kserve.protocol.grpc.grpc_predict_v2_pb2 import ModelInferRequest, InferParameter, ModelInferResponse
+
+
+class TestInferRequest:
+    def test_to_rest(self):
+        infer_req = InferRequest(model_name="TestModel", request_id="123",
+                                 parameters={
+                                     "test-str": InferParameter(string_param="dummy"),
+                                     "test-bool": InferParameter(bool_param=True),
+                                     "test-int": InferParameter(int64_param=100)
+                                 },
+                                 infer_inputs=[
+                                     InferInput(name="input-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                                parameters={
+                                                    "test-str": InferParameter(string_param="dummy"),
+                                                    "test-bool": InferParameter(bool_param=True),
+                                                    "test-int": InferParameter(int64_param=100)
+                                                })]
+                                 )
+        # model_name should not be present for rest
+        expected = {
+            "id": "123",
+            "inputs": [
+                {
+                    "name": "input-0",
+                    "shape": [1, 2],
+                    "datatype": "INT32",
+                    "data": [1, 2],
+                    "parameters": {
+                        "test-str": "dummy",
+                        "test-bool": True,
+                        "test-int": 100
+                    }
+                }
+            ],
+            "parameters": {
+                "test-str": "dummy",
+                "test-bool": True,
+                "test-int": 100
+            }
+        }
+        res = infer_req.to_rest()
+        assert res == expected
+
+    def test_to_grpc(self):
+        infer_req = InferRequest(model_name="TestModel", request_id="123",
+                                 parameters={
+                                     "test-str": "dummy",
+                                     "test-bool": True,
+                                     "test-int": 100
+                                 },
+                                 infer_inputs=[
+                                     InferInput(name="input-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                                parameters={
+                                                    "test-str": "dummy",
+                                                    "test-bool": True,
+                                                    "test-int": 100
+                                                })]
+                                 )
+        expected = ModelInferRequest(model_name="TestModel", id="123",
+                                     parameters={
+                                         "test-str": InferParameter(string_param="dummy"),
+                                         "test-bool": InferParameter(bool_param=True),
+                                         "test-int": InferParameter(int64_param=100)
+                                     },
+                                     inputs=[
+                                         {
+                                             "name": "input-0",
+                                             "shape": [1, 2],
+                                             "datatype": "INT32",
+                                             "contents": {
+                                                 "int_contents": [1, 2]
+                                             },
+                                             "parameters": {
+                                                 "test-str": InferParameter(string_param="dummy"),
+                                                 "test-bool": InferParameter(bool_param=True),
+                                                 "test-int": InferParameter(int64_param=100)
+                                             },
+                                         }]
+                                     )
+        res = infer_req.to_grpc()
+        assert res == expected
+
+    def test_from_grpc(self):
+        infer_req = ModelInferRequest(model_name="TestModel", id="123",
+                                      parameters={
+                                          "test-str": InferParameter(string_param="dummy"),
+                                          "test-bool": InferParameter(bool_param=True),
+                                          "test-int": InferParameter(int64_param=100)
+                                      },
+                                      inputs=[
+                                          {
+                                              "name": "input-0",
+                                              "shape": [1, 2],
+                                              "datatype": "INT32",
+                                              "contents": {
+                                                  "int_contents": [1, 2]
+                                              },
+                                              "parameters": {
+                                                  "test-str": InferParameter(string_param="dummy"),
+                                                  "test-bool": InferParameter(bool_param=True),
+                                                  "test-int": InferParameter(int64_param=100)
+                                              },
+                                          }]
+                                      )
+        expected = InferRequest(model_name="TestModel", request_id="123",
+                                parameters={
+                                    "test-str": InferParameter(string_param="dummy"),
+                                    "test-bool": InferParameter(bool_param=True),
+                                    "test-int": InferParameter(int64_param=100)
+                                },
+                                infer_inputs=[
+                                    InferInput(name="input-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                               parameters={
+                                                   "test-str": InferParameter(string_param="dummy"),
+                                                   "test-bool": InferParameter(bool_param=True),
+                                                   "test-int": InferParameter(int64_param=100)
+                                               })],
+                                from_grpc=True
+                                )
+        res = InferRequest.from_grpc(infer_req)
+        assert res == expected
+
+    class TestInferResponse:
+        def test_to_rest(self):
+            infer_res = InferResponse(model_name="TestModel", response_id="123",
+                                      parameters={
+                                          "test-str": InferParameter(string_param="dummy"),
+                                          "test-bool": InferParameter(bool_param=True),
+                                          "test-int": InferParameter(int64_param=100)
+                                      },
+                                      infer_outputs=[
+                                          InferOutput(name="output-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                                      parameters={
+                                                          "test-str": InferParameter(string_param="dummy"),
+                                                          "test-bool": InferParameter(bool_param=True),
+                                                          "test-int": InferParameter(int64_param=100)
+                                                      })]
+                                      )
+            expected = {
+                "id": "123",
+                "model_name": "TestModel",
+                "outputs": [
+                    {
+                        "name": "output-0",
+                        "shape": [1, 2],
+                        "datatype": "INT32",
+                        "data": [1, 2],
+                        "parameters": {
+                            "test-str": "dummy",
+                            "test-bool": True,
+                            "test-int": 100
+                        }
+                    }
+                ],
+                "parameters": {
+                    "test-str": "dummy",
+                    "test-bool": True,
+                    "test-int": 100
+                }
+            }
+            res = infer_res.to_rest()
+            assert res == expected
+
+        def test_to_grpc(self):
+            infer_res = InferResponse(model_name="TestModel", response_id="123",
+                                      parameters={
+                                          "test-str": "dummy",
+                                          "test-bool": True,
+                                          "test-int": 100
+                                      },
+                                      infer_outputs=[
+                                          InferOutput(name="output-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                                      parameters={
+                                                          "test-str": "dummy",
+                                                          "test-bool": True,
+                                                          "test-int": 100
+                                                      })]
+                                      )
+            expected = ModelInferResponse(model_name="TestModel", id="123",
+                                          parameters={
+                                              "test-str": InferParameter(string_param="dummy"),
+                                              "test-bool": InferParameter(bool_param=True),
+                                              "test-int": InferParameter(int64_param=100)
+                                          },
+                                          outputs=[
+                                              {
+                                                  "name": "output-0",
+                                                  "shape": [1, 2],
+                                                  "datatype": "INT32",
+                                                  "contents": {
+                                                      "int_contents": [1, 2]
+                                                  },
+                                                  "parameters": {
+                                                      "test-str": InferParameter(string_param="dummy"),
+                                                      "test-bool": InferParameter(bool_param=True),
+                                                      "test-int": InferParameter(int64_param=100)
+                                                  },
+                                              }]
+                                          )
+            res = infer_res.to_grpc()
+            assert res == expected
+
+        def test_from_grpc(self):
+            infer_res = ModelInferResponse(model_name="TestModel", id="123",
+                                           parameters={
+                                               "test-str": InferParameter(string_param="dummy"),
+                                               "test-bool": InferParameter(bool_param=True),
+                                               "test-int": InferParameter(int64_param=100)
+                                           },
+                                           outputs=[
+                                               {
+                                                   "name": "output-0",
+                                                   "shape": [1, 2],
+                                                   "datatype": "INT32",
+                                                   "contents": {
+                                                       "int_contents": [1, 2]
+                                                   },
+                                                   "parameters": {
+                                                       "test-str": InferParameter(string_param="dummy"),
+                                                       "test-bool": InferParameter(bool_param=True),
+                                                       "test-int": InferParameter(int64_param=100)
+                                                   },
+                                               }]
+                                           )
+            expected = InferResponse(model_name="TestModel", response_id="123",
+                                     parameters={
+                                         "test-str": InferParameter(string_param="dummy"),
+                                         "test-bool": InferParameter(bool_param=True),
+                                         "test-int": InferParameter(int64_param=100)
+                                     },
+                                     infer_outputs=[
+                                         InferOutput(name="output-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                                     parameters={
+                                                         "test-str": InferParameter(string_param="dummy"),
+                                                         "test-bool": InferParameter(bool_param=True),
+                                                         "test-int": InferParameter(int64_param=100)
+                                                     })],
+                                     from_grpc=True
+                                     )
+            res = InferResponse.from_grpc(infer_res)
+            assert res == expected
diff --git a/python/kserve/test/test_server.py b/python/kserve/test/test_server.py
--- a/python/kserve/test/test_server.py
+++ b/python/kserve/test/test_server.py
@@ -18,6 +18,7 @@
 import re
 from typing import Dict
 from unittest import mock
+from unittest.mock import patch
 
 import avro.io
 import avro.schema
@@ -32,7 +33,7 @@
 from kserve.model import PredictorProtocol
 from kserve.protocol.rest.server import RESTServer
 
-from kserve.protocol.infer_type import InferRequest
+from kserve.protocol.infer_type import InferRequest, InferInput, InferResponse, InferOutput
 from kserve.utils.utils import get_predict_input, get_predict_response
 
 test_avsc_schema = '''
@@ -200,6 +201,7 @@ async def test_validate(self):
         with pytest.raises(InvalidInput):
             model.validate(bad_request)
 
+
 # Separate out v1 and v2 endpoint unit tests in
 # https://github.com/kserve/kserve/blob/master/python/kserve/test/test_server.py.
 
@@ -240,14 +242,14 @@ def test_list_models_v1(self, http_server_client):
 
     def test_predict_v1(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:predict',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 200
         assert resp.content == b'{"predictions":[[1,2]]}'
         assert resp.headers['content-type'] == "application/json"
 
     def test_explain_v1(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:explain',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 200
         assert resp.content == b'{"predictions":[[1,2]]}'
         assert resp.headers['content-type'] == "application/json"
@@ -286,7 +288,7 @@ def test_list_models_v2(self, http_server_client):
     def test_infer_v2(self, http_server_client):
         input_data = b'{"inputs": [{"name": "input-0","shape": [1, 2],"datatype": "INT32","data": [[1,2]]}]}'
         resp = http_server_client.post('/v2/models/TestModel/infer',
-                                       data=input_data)
+                                       content=input_data)
 
         result = json.loads(resp.content)
         assert resp.status_code == 200
@@ -295,11 +297,51 @@ def test_infer_v2(self, http_server_client):
 
     def test_explain_v2(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:explain',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 200
         assert resp.content == b'{"predictions":[[1,2]]}'
         assert resp.headers['content-type'] == "application/json"
 
+    def test_infer_parameters_v2(self, http_server_client):
+        req = InferRequest(model_name="TestModel", request_id="123",
+                           parameters={
+                               "test-str": "dummy",
+                               "test-bool": True,
+                               "test-int": 100
+                           },
+                           infer_inputs=[
+                               InferInput(name="input-0", datatype="INT32", shape=[1, 2], data=[1, 2],
+                                          parameters={
+                                              "test-str": "dummy",
+                                              "test-bool": True,
+                                              "test-int": 100
+                                          })])
+
+        input_data = json.dumps(req.to_rest()).encode('utf-8')
+        with patch.object(DummyModel, 'predict', new_callable=mock.Mock) as mock_predict:
+            mock_predict.return_value = InferResponse(model_name="TestModel", response_id="123",
+                                                      parameters={
+                                                          "test-str": "dummy",
+                                                          "test-bool": True,
+                                                          "test-int": 100
+                                                      },
+                                                      infer_outputs=[
+                                                          InferOutput(name="input-0",
+                                                                      datatype="INT32",
+                                                                      shape=[1, 2], data=[1, 2],
+                                                                      parameters={
+                                                                          "test-str": "dummy",
+                                                                          "test-bool": True,
+                                                                          "test-int": 100
+                                                                      })])
+            resp = http_server_client.post('/v2/models/TestModel/infer', content=input_data)
+            mock_predict.assert_called_with(req, mock.ANY)
+
+        result = json.loads(resp.content)
+        assert resp.status_code == 200
+        assert result["outputs"][0]["data"] == [1, 2]
+        assert resp.headers['content-type'] == "application/json"
+
 
 class TestRayServer:
     @pytest.fixture(scope="class")
@@ -338,7 +380,7 @@ def test_health_handler(self, http_server_client):
 
     def test_predict(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:predict',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 200
         assert resp.content == b'{"predictions":[[1,2]]}'
         assert resp.headers['content-type'] == "application/json"
@@ -346,7 +388,7 @@ def test_predict(self, http_server_client):
     def test_infer(self, http_server_client):
         input_data = b'{"inputs": [{"name": "input-0","shape": [1, 2],"datatype": "INT32","data": [[1,2]]}]}'
         resp = http_server_client.post('/v2/models/TestModel/infer',
-                                       data=input_data)
+                                       content=input_data)
 
         result = json.loads(resp.content)
         assert resp.status_code == 200
@@ -355,7 +397,7 @@ def test_infer(self, http_server_client):
 
     def test_explain(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:explain',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 200
         assert resp.content == b'{"predictions":[[1,2]]}'
         assert resp.headers['content-type'] == "application/json"
@@ -398,7 +440,7 @@ def test_predict_ce_structured(self, http_server_client):
         event = dummy_cloud_event({"instances": [[1, 2]]})
         headers, body = to_structured(event)
 
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
         body = json.loads(resp.content)
 
         assert resp.status_code == 200
@@ -417,7 +459,7 @@ def test_predict_custom_ce_attributes(self, http_server_client):
             event = dummy_cloud_event({"instances": [[1, 2]]})
             headers, body = to_structured(event)
 
-            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
             body = json.loads(resp.content)
 
             assert resp.status_code == 200
@@ -433,7 +475,7 @@ def test_predict_merge_structured_ce_attributes(self, http_server_client):
             event = dummy_cloud_event({"instances": [[1, 2]]}, add_extension=True)
             headers, body = to_structured(event)
 
-            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
             body = json.loads(resp.content)
 
             assert resp.status_code == 200
@@ -451,7 +493,7 @@ def test_predict_merge_binary_ce_attributes(self, http_server_client):
             event = dummy_cloud_event({"instances": [[1, 2]]}, set_contenttype=True, add_extension=True)
             headers, body = to_binary(event)
 
-            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+            resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
             assert resp.status_code == 200
             assert resp.headers['content-type'] == "application/json"
@@ -468,7 +510,7 @@ def test_predict_ce_binary_dict(self, http_server_client):
         event = dummy_cloud_event({"instances": [[1, 2]]}, set_contenttype=True)
         headers, body = to_binary(event)
 
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
         assert resp.status_code == 200
         assert resp.headers['content-type'] == "application/json"
@@ -482,7 +524,7 @@ def test_predict_ce_binary_dict(self, http_server_client):
     def test_predict_ce_binary_bytes(self, http_server_client):
         event = dummy_cloud_event(b'{"instances":[[1,2]]}', set_contenttype=True)
         headers, body = to_binary(event)
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
         assert resp.status_code == 200
         assert resp.headers['content-type'] == "application/json"
@@ -497,7 +539,7 @@ def test_predict_ce_bytes_bad_format_exception(self, http_server_client):
         event = dummy_cloud_event(b'{', set_contenttype=True)
         headers, body = to_binary(event)
 
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
         assert resp.status_code == 400
         error_regex = re.compile("Failed to decode or parse binary json cloudevent: "
@@ -509,7 +551,7 @@ def test_predict_ce_bytes_bad_hex_format_exception(self, http_server_client):
         event = dummy_cloud_event(b'0\x80\x80\x06World!\x00\x00', set_contenttype=True)
         headers, body = to_binary(event)
 
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
         assert resp.status_code == 400
         error_regex = re.compile("Failed to decode or parse binary json cloudevent: "
@@ -546,7 +588,7 @@ def test_predict_ce_avro_binary(self, http_server_client):
         event = dummy_cloud_event(data, set_contenttype=True, contenttype="application/avro")
         # Creates the HTTP request representation of the CloudEvent in binary content mode
         headers, body = to_binary(event)
-        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, data=body)
+        resp = http_server_client.post('/v1/models/TestModel:predict', headers=headers, content=body)
 
         assert resp.status_code == 200
         assert resp.headers['content-type'] == "application/json"
@@ -571,12 +613,12 @@ def http_server_client(self, app):
         return TestClient(app)
 
     def test_load(self, http_server_client):
-        resp = http_server_client.post('/v2/repository/models/model/load', data=b'')
+        resp = http_server_client.post('/v2/repository/models/model/load', content=b'')
         assert resp.status_code == 200
         assert resp.content == b'{"name":"model","load":true}'
 
     def test_unload(self, http_server_client):
-        resp = http_server_client.post('/v2/repository/models/model/unload', data=b'')
+        resp = http_server_client.post('/v2/repository/models/model/unload', content=b'')
         assert resp.status_code == 200
         assert resp.content == b'{"name":"model","unload":true}'
 
@@ -593,11 +635,11 @@ def http_server_client(self, app):
         return TestClient(app)
 
     def test_load_fail(self, http_server_client):
-        resp = http_server_client.post('/v2/repository/models/model/load', data=b'')
+        resp = http_server_client.post('/v2/repository/models/model/load', content=b'')
         assert resp.status_code == 503
 
     def test_unload_fail(self, http_server_client):
-        resp = http_server_client.post('/v2/repository/models/model/unload', data=b'')
+        resp = http_server_client.post('/v2/repository/models/model/unload', content=b'')
         assert resp.status_code == 404
 
 
@@ -624,16 +666,16 @@ def test_model_not_ready_v2(self, http_server_client):
 
     def test_predict(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:predict',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 503
 
     def test_infer(self, http_server_client):
         input_data = b'{"inputs": [{"name": "input-0","shape": [1, 2],"datatype": "INT32","data": [[1,2]]}]}'
         resp = http_server_client.post('/v2/models/TestModel/infer',
-                                       data=input_data)
+                                       content=input_data)
         assert resp.status_code == 503
 
     def test_explain(self, http_server_client):
         resp = http_server_client.post('/v1/models/TestModel:explain',
-                                       data=b'{"instances":[[1,2]]}')
+                                       content=b'{"instances":[[1,2]]}')
         assert resp.status_code == 503
diff --git a/test/e2e/data/custom_model_input_v2.json b/test/e2e/data/custom_model_input_v2.json
--- a/test/e2e/data/custom_model_input_v2.json
+++ b/test/e2e/data/custom_model_input_v2.json
@@ -9,4 +9,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}