Repository: kserve/kserve

Issue:
Float values are truncated in output parameters
/kind bug

**What steps did you take and what happened:**
[A clear and concise description of what the bug is.]
In transformer postprocess I am setting the parameters which also contains a float value. But in the end response, the float value is truncated to int. The transformer uses REST v2(OIP) protocol.
```python
def postprocess(self, response: InferResponse, headers: Dict[str, str]=None) -> dict:
        return_parameters = {"threshold":  0.4, "revision": "/mnt/models"}
        print(f"Returning: {return_parameters}")
        response.outputs[0]._parameters = return_parameters
        return response
```
I did some logging in the transformer and I get the float as expected: 
`Returning: {'threshold': 0.6, 'revision': '/mnt/models'}`. 

but the response received:
```python
{
    "model_name": "treatment1",
    "model_version": None,
    "id": "",
    "parameters": None,
    "outputs": [
        {
            "name": "OUTPUT__0",
            "shape": [1],
            "datatype": "FP32",
            "parameters": {"threshold": 0, "revision": "/mnt/models"},
            "data": [0.12074008584022522],
        }
    ],
}
```

**What did you expect to happen:**
Float values should not be truncated in parameters

**What's the InferenceService yaml:**
[To help us debug please run `kubectl get isvc $name -n $namespace -oyaml` and paste the output]

**Anything else you would like to add:**
[Miscellaneous information that will assist in solving the issue.]
This is because of the way parameters type hints defined. https://github.com/kserve/kserve/blob/e21b913fe10b377d72e67fbf92955dbcedd6f465/python/kserve/kserve/protocol/rest/v2_datamodels.py#L25. Since `int` is defined first, the pydantic tries to converts it to int. So the value is truncated.
We can resolve this by using [strict types](https://docs.pydantic.dev/latest/api/types/#pydantic.types.StrictInt) instead. 
Related #2754.
For more info read, https://docs.pydantic.dev/latest/concepts/unions, https://docs.pydantic.dev/latest/concepts/strict_mode/ 


**Environment:**

- Istio Version:
- Knative Version:
- KServe Version: 0.12.0rc1
- Kubeflow version:
- Cloud Environment:[k8s_istio/istio_dex/gcp_basic_auth/gcp_iap/aws/aws_cognito/ibm]
- Minikube/Kind version:
- Kubernetes version: (use `kubectl version`):
- OS (e.g. from `/etc/os-release`):



Discussion:

Resolving patch:
diff --git a/python/huggingfaceserver/huggingfaceserver/model.py b/python/huggingfaceserver/huggingfaceserver/model.py
--- a/python/huggingfaceserver/huggingfaceserver/model.py
+++ b/python/huggingfaceserver/huggingfaceserver/model.py
@@ -29,7 +29,7 @@
 from kserve.storage import Storage
 
 from kserve.protocol.infer_type import InferRequest, InferResponse, InferInput
-from kserve.utils.utils import get_predict_response, get_predict_input
+from kserve.utils.utils import get_predict_response, get_predict_input, from_np_dtype
 from kserve import Model
 import torch
 
@@ -139,7 +139,7 @@ def preprocess(self, payload: Union[Dict, InferRequest], context: Dict[str, Any]
             infer_inputs = []
             for key, input_tensor in inputs.items():
                 if (not self.tensor_input_names) or (key in self.tensor_input_names):
-                    infer_input = InferInput(name=key, datatype=input_tensor.dtype,
+                    infer_input = InferInput(name=key, datatype=from_np_dtype(input_tensor.dtype),
                                              shape=list(input_tensor.shape), data=input_tensor)
                     infer_inputs.append(infer_input)
             infer_request = InferRequest(infer_inputs=infer_inputs, model_name=self.name)
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
@@ -11,6 +11,7 @@
 # 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.
+
 import struct
 from typing import Optional, List, Dict, Union
 
@@ -89,7 +90,7 @@ def __init__(self, name: str, shape: List[int], datatype: str,
 
         self._name = name
         self._shape = shape
-        self._datatype = datatype
+        self._datatype = datatype.upper()
         self._parameters = parameters
         self._data = data
         self._raw_data = None
@@ -183,11 +184,11 @@ def set_data_from_numpy(self, input_tensor: np.ndarray, binary_data: bool = True
             InferenceError if failed to set data for the tensor.
         """
         if not isinstance(input_tensor, (np.ndarray,)):
-            InferenceError("input_tensor must be a numpy array")
+            raise InferenceError("input_tensor must be a numpy array")
 
         dtype = from_np_dtype(input_tensor.dtype)
         if self._datatype != dtype:
-            InferenceError(
+            raise InferenceError(
                 "got unexpected datatype {} from numpy array, expected {}".format(dtype, self._datatype))
         valid_shape = True
         if len(self._shape) != len(input_tensor.shape):
@@ -197,7 +198,7 @@ def set_data_from_numpy(self, input_tensor: np.ndarray, binary_data: bool = True
                 if self._shape[i] != input_tensor.shape[i]:
                     valid_shape = False
         if not valid_shape:
-            InferenceError(
+            raise InferenceError(
                 "got unexpected numpy array shape [{}], expected [{}]".format(
                     str(input_tensor.shape)[1:-1],
                     str(self._shape)[1:-1]))
@@ -226,7 +227,7 @@ def set_data_from_numpy(self, input_tensor: np.ndarray, binary_data: bool = True
                                 self._data.append(
                                     str(obj.item(), encoding='utf-8'))
                 except UnicodeDecodeError:
-                    InferenceError(
+                    raise InferenceError(
                         f'Failed to encode "{obj.item()}" using UTF-8. Please use binary_data=True, if'
                         ' you want to pass a byte array.')
             else:
@@ -446,7 +447,7 @@ def __init__(self, name: str, shape: List[int], datatype: str,
 
         self._name = name
         self._shape = shape
-        self._datatype = datatype
+        self._datatype = datatype.upper()
         self._parameters = parameters
         self._data = data
         self._raw_data = None
@@ -496,6 +497,10 @@ def parameters(self) -> Union[Dict, MessageMap[str, InferParameter], None]:
         """
         return self._parameters
 
+    @parameters.setter
+    def parameters(self, params: Union[Dict, MessageMap[str, InferParameter]]):
+        self._parameters = params
+
     def set_shape(self, shape: List[int]):
         """Set the shape of inference output.
 
@@ -534,11 +539,11 @@ def set_data_from_numpy(self, output_tensor: np.ndarray, binary_data: bool = Tru
             InferenceError if failed to set data for the output tensor.
         """
         if not isinstance(output_tensor, (np.ndarray,)):
-            InferenceError("input_tensor must be a numpy array")
+            raise InferenceError("input_tensor must be a numpy array")
 
         dtype = from_np_dtype(output_tensor.dtype)
         if self._datatype != dtype:
-            InferenceError(
+            raise InferenceError(
                 "got unexpected datatype {} from numpy array, expected {}".format(dtype, self._datatype))
         valid_shape = True
         if len(self._shape) != len(output_tensor.shape):
@@ -548,7 +553,7 @@ def set_data_from_numpy(self, output_tensor: np.ndarray, binary_data: bool = Tru
                 if self._shape[i] != output_tensor.shape[i]:
                     valid_shape = False
         if not valid_shape:
-            InferenceError(
+            raise InferenceError(
                 "got unexpected numpy array shape [{}], expected [{}]".format(
                     str(output_tensor.shape)[1:-1],
                     str(self._shape)[1:-1]))
@@ -577,7 +582,7 @@ def set_data_from_numpy(self, output_tensor: np.ndarray, binary_data: bool = Tru
                                 self._data.append(
                                     str(obj.item(), encoding='utf-8'))
                 except UnicodeDecodeError:
-                    InferenceError(
+                    raise InferenceError(
                         f'Failed to encode "{obj.item()}" using UTF-8. Please use binary_data=True, if'
                         ' you want to pass a byte array.')
             else:
diff --git a/python/kserve/kserve/protocol/rest/v2_datamodels.py b/python/kserve/kserve/protocol/rest/v2_datamodels.py
--- a/python/kserve/kserve/protocol/rest/v2_datamodels.py
+++ b/python/kserve/kserve/protocol/rest/v2_datamodels.py
@@ -14,7 +14,7 @@
 from typing import Optional, List, Union, Dict
 
 import orjson
-from pydantic import BaseModel
+from pydantic import BaseModel, StrictBool, StrictInt, StrictFloat
 
 # TODO: in the future, this file can be auto generated
 # https://pydantic-docs.helpmanual.io/datamodel_code_generator/
@@ -22,7 +22,7 @@
 # Reference: https://github.com/kserve/kserve/blob/master/docs/predict-api/v2/rest_predict_v2.yaml
 
 
-InferParameter = Union[int, float, bool, str]
+InferParameter = Union[StrictFloat, StrictInt, StrictBool, str]
 Parameters = Dict[str, InferParameter]
 
 


Test patch:
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,7 +18,6 @@
 import re
 from typing import Dict
 from unittest import mock
-from unittest.mock import patch
 
 import avro.io
 import avro.schema
@@ -82,6 +81,10 @@ async def predict(self, request, headers=None):
         if isinstance(request, InferRequest):
             inputs = get_predict_input(request)
             infer_response = get_predict_response(request, inputs, self.name)
+            if request.parameters:
+                infer_response.parameters = request.parameters
+            if request.inputs[0].parameters:
+                infer_response.outputs[0].parameters = request.inputs[0].parameters
             return infer_response
         else:
             return {"predictions": request["instances"]}
@@ -303,44 +306,46 @@ def test_explain_v2(self, http_server_client):
         assert resp.headers['content-type'] == "application/json"
 
     def test_infer_parameters_v2(self, http_server_client):
-        req = InferRequest(model_name="TestModel", request_id="123",
+        model_name = "TestModel"
+        req = InferRequest(model_name=model_name, request_id="123",
                            parameters={
                                "test-str": "dummy",
                                "test-bool": True,
-                               "test-int": 100
+                               "test-int": 100,
+                               "test-float": 1.3,
                            },
                            infer_inputs=[
                                InferInput(name="input-0", datatype="INT32", shape=[1, 2], data=[1, 2],
                                           parameters={
                                               "test-str": "dummy",
                                               "test-bool": True,
-                                              "test-int": 100
+                                              "test-int": 100,
+                                              "test-float": 1.3,
                                           })])
 
         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)
+        expected_res = InferResponse(model_name=model_name, response_id="123",
+                                     parameters={
+                                         "test-str": "dummy",
+                                         "test-bool": True,
+                                         "test-int": 100,
+                                         "test-float": 1.3,
+                                     },
+                                     infer_outputs=[
+                                         InferOutput(name="output-0",
+                                                     datatype="INT32",
+                                                     shape=[1, 2], data=[1, 2],
+                                                     parameters={
+                                                         "test-str": "dummy",
+                                                         "test-bool": True,
+                                                         "test-int": 100,
+                                                         "test-float": 1.3,
+                                                     })])
+        resp = http_server_client.post('/v2/models/TestModel/infer', content=input_data)
         assert resp.status_code == 200
-        assert result["outputs"][0]["data"] == [1, 2]
         assert resp.headers['content-type'] == "application/json"
+        result = InferResponse.from_rest(model_name=model_name, response=json.loads(resp.content))
+        assert result == expected_res
 
 
 class TestRayServer: