Repository: Xilinx/inference-server (language: python)

Issue:
Improve compatibility with KServe API 
Currently, we don't support a model metadata querying or versions in endpoints, for example


Discussion / hints:

Fix patch:
diff --git a/docker/generate.py b/docker/generate.py
index 6885c2d72..cd0600ecb 100644
--- a/docker/generate.py
+++ b/docker/generate.py
@@ -763,20 +763,38 @@ def install_python_packages():
 
 
 def vcpkg_build(manager):
-    return textwrap.dedent(
-        f"""\
-        RUN {manager.update} \\
-            && {manager.install} \\
-                curl \\
-                zip \\
-                unzip \\
-                tar \\
-                nasm \\
-                pkg-config \\
-                python3-dev \\
-            # clean up
-            {code_indent(manager.clean, 12)}"""
-    )
+    if manager.name == "apt":
+        return textwrap.dedent(
+            f"""\
+            RUN {manager.update} \\
+                && {manager.install} \\
+                    curl \\
+                    zip \\
+                    unzip \\
+                    tar \\
+                    nasm \\
+                    pkg-config \\
+                    python3-dev \\
+                # clean up
+                {code_indent(manager.clean, 16)}"""
+        )
+    elif manager.name == "yum":
+        return textwrap.dedent(
+            f"""\
+            RUN {manager.update} \\
+                && {manager.install} \\
+                    curl \\
+                    zip \\
+                    unzip \\
+                    tar \\
+                    nasm \\
+                    pkgconfig \\
+                    python3-devel \\
+                # clean up
+                {code_indent(manager.clean, 16)}"""
+        )
+    else:
+        raise ValueError(f"Unknown base image type: {manager.name}")
 
 
 def generate(args: argparse.Namespace):
diff --git a/docker/install_vcpkg.sh b/docker/install_vcpkg.sh
index c97e4d0dd..f77c9fa8e 100755
--- a/docker/install_vcpkg.sh
+++ b/docker/install_vcpkg.sh
@@ -24,7 +24,7 @@ do
 done
 
 # for some reason, vcpkg doesn't like passing an empty string as an argument
-# so set it to a real value when VITIS is "no" omitted
+# so set it to a real value when VITIS is "no"
 if [[ "$VITIS" == "yes" ]]; then
   FEATURES="--x-feature=testing --x-feature=vitis"
 else
diff --git a/docs/client_api.rst b/docs/client_api.rst
new file mode 100644
index 000000000..b476ca6b5
--- /dev/null
+++ b/docs/client_api.rst
@@ -0,0 +1,178 @@
+..
+    Copyright 2023 Advanced Micro Devices, Inc.
+
+    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.
+
+Client API
+==========
+
+The client API enables users to interact with the inference server.
+This page highlights some of the important methods that you can use.
+
+Include the API
+---------------
+
+After installing the client API, you can include it in your code with a single line:
+
+.. tabs::
+
+    .. code-tab:: c++ C++
+
+        #include "amdinfer/amdinfer.hpp"
+
+    .. code-tab:: python Python
+
+        import amdinfer
+
+Create a client object
+----------------------
+
+A client object enables you to talk to the server using the protocol of your choice.
+The inference server supports the following protocols which you can use independently.
+
+.. tabs::
+
+    .. code-tab:: c++ C++
+
+        // native - the server must be started in the same process
+        amdinfer::Server server;
+        amdinfer::NativeClient client(&server);
+
+        // HTTP/REST
+        amdinfer::HttpClient client{"http://127.0.0.1:8998"};
+
+        // gRPC
+        amdinfer::GrpcClient client{"127.0.0.1:50051"};
+
+    .. code-tab:: python Python
+
+        # native - the server must be started in the same script
+        server = amdinfer.Server()
+        client = amdinfer.NativeClient(server)
+
+        # HTTP/REST
+        client = amdinfer.HttpClient("http://127.0.0.1:8998")
+
+        # gRPC
+        client = amdinfer.GrpcClient("127.0.0.1:50051")
+
+All clients have the same interface after the initial construction so you can use any client in the next set of steps.
+
+Server status
+-------------
+
+You can check the state and health of the server using the following methods of all client objects: ``serverMetadata()``, ``serverLive()``, ``serverReady()``, ``modelReady()``, ``modelMetadata()`` and ``hasHardware()``.
+
+These base methods of client objects enable the following helper functions that take a client object as the first argument: ``serverHasExtension()``, ``waitUntilServerReady()``, ``waitUntilModelReady()`` and ``waitUntilModelNotReady()``.
+
+You can see more information about these functions in the API documentation for :ref:`C++ <cpp_user_api:C++>` and :ref:`Python <python_api>`.
+
+Loading a backend
+-----------------
+
+If the server you are using is not already ready to serve incoming inference requests for your model, you may need to load a :ref:`backend <backends:Backends>` to serve your model first.
+
+Client objects provide two methods to load backends: ``modelLoad()`` and ``workerLoad()``.
+The former loads the named model from the :ref:`model repository <model_repository:Model Repository>` while the latter is a lower-level method to directly load a backend with a path to a particular model file.
+The path to the model file, and other load-time parameters, can be passed to the server with these methods.
+Each backend defines its own load-time parameters so check the documentation for the backend you want to use.
+
+When you load a backend, you get an *endpoint* that you can use to make further requests to.
+For ``modelLoad()``, the endpoint is the same name as the model you pass to the method.
+For ``workerLoad()``, the server will assign it an endpoint and return it from the call to ``workerLoad()``.
+
+You can also load :ref:`ensembles <ensembles:Ensembles>` with ``loadEnsemble()``.
+
+Making an inference request
+---------------------------
+
+A basic inference request to the server consists of a list of input tensors.
+You can construct a request with something like this:
+
+.. tabs::
+
+    .. code-tab:: c++ C++
+
+        amdinfer::InferenceRequest request;
+
+        // void* data = ...;
+        amdinfer::InferenceRequestInput input_tensor{
+            data, {224, 224, 3}, amdinfer::DataType::FP32, "input"
+        };
+        request.addInputTensor(input_tensor);
+
+        // add other tensors?
+
+    .. code-tab:: python Python
+
+        request = amdinfer.InferenceRequest()
+
+        input_tensor = amdinfer.InferenceRequestInput()
+        input_tensor.name = "input"
+        input_tensor.datatype = amdinfer.DataType.FP32
+        input_tensor.shape = (224, 224, 3)
+        # data could be a list or a numpy array
+        input_tensor.setFp32Data(data)
+        request.addInputTensor(input_tensor)
+
+        # add other tensors?
+
+Once you have a request, you can use the client's ``modelInfer()`` method:
+
+.. tabs::
+
+    .. code-tab:: c++ C++
+
+        // endpoint is a string from loading a backend or provided to you
+        auto response = client.modelInfer(endpoint, request);
+
+    .. code-tab:: python Python
+
+        # endpoint is a string from loading a backend or provided to you
+        response = client.modelInfer(endpoint, request)
+
+The client API also provides other methods for making inferences such as ``modelInferAsync()`` and ``inferAsyncOrdered()``.
+You can see more information about the available methods in the API documentation for :ref:`C++ <cpp_user_api:C++>` and :ref:`Python <python_api>`.
+
+Parsing the response
+--------------------
+
+The basic response from the inference server consists of an array of output tensors.
+
+.. tabs::
+
+    .. code-tab:: c++ C++
+
+        auto response = client.modelInfer(endpoint, request);
+        if(!response.isError()){
+            auto output_tensors = response.getOutputs();
+            for(const auto& output_tensor : output_tensors){
+                // you can use methods to get the shape, datatype and name and data
+            }
+        }
+
+
+    .. code-tab:: python Python
+
+        response = client.modelInfer(endpoint, request)
+        if not response.isError():
+            output_tensors = response.getOutputs()
+            for output_tensor in output_tensors:
+                # you can use methods to get the shape, datatype and name and data
+
+You can see more information about the available methods in the API documentation for :ref:`C++ <cpp_user_api:C++>` and :ref:`Python <python_api>`.
+
+Next steps
+----------
+
+Take a look at the examples to see these APIs used in practice.
diff --git a/docs/index.rst b/docs/index.rst
index 678da39ec..11005f172 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -44,6 +44,7 @@ To see other versions, you can use "Read the Docs" panel on the bottom-left or e
     model_repository
     ensembles
     deployment
+    client_api
     kserve
     performance_factors
     troubleshooting
diff --git a/docs/model_repository.rst b/docs/model_repository.rst
index 74c88e5b9..6e94058f2 100644
--- a/docs/model_repository.rst
+++ b/docs/model_repository.rst
@@ -137,7 +137,7 @@ The configuration file for this ensemble could be:
 
     [[models.inputs]]
     name = "image_in"
-    datatype = "STRING"
+    datatype = "BYTES"
     shape = [1048576]
     id = ""
 
@@ -177,7 +177,7 @@ The configuration file for this ensemble could be:
 
     [[models.outputs]]
     name = "image_out"
-    datatype = "STRING"
+    datatype = "BYTES"
     shape = [1048576]
     id = ""
 
diff --git a/docs/python.rst b/docs/python.rst
index e890ca890..3b4ed2ea3 100644
--- a/docs/python.rst
+++ b/docs/python.rst
@@ -74,6 +74,7 @@ You can install these wheels in a virtual environment, Conda environment, a cont
 
     pip install <path/to/wheel>
 
+.. _python_api:
 
 API
 ---
diff --git a/examples/resnet50/migraphx.cpp b/examples/resnet50/migraphx.cpp
index ccc0a28a4..c9b4e4e7e 100644
--- a/examples/resnet50/migraphx.cpp
+++ b/examples/resnet50/migraphx.cpp
@@ -23,7 +23,7 @@
 #include <array>                // for array
 #include <cassert>              // for assert
 #include <chrono>               // for duration
-#include <cstdint>              // for uint64_t
+#include <cstdint>              // for int64_t
 #include <cstdlib>              // for exit, getenv
 #include <exception>            // for exception
 #include <filesystem>           // for path, oper...
@@ -101,11 +101,11 @@ std::vector<int> postprocess(const amdinfer::InferenceResponseOutput& output,
  * @return std::vector<amdinfer::InferenceRequest>
  */
 std::vector<amdinfer::InferenceRequest> constructRequests(const Images& images,
-                                                          uint64_t input_size) {
+                                                          int64_t input_size) {
   std::vector<amdinfer::InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+  const std::initializer_list<int64_t> shape = {input_size, input_size, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();
diff --git a/examples/resnet50/ptzendnn.cpp b/examples/resnet50/ptzendnn.cpp
index a2573dc4f..2bb864b17 100644
--- a/examples/resnet50/ptzendnn.cpp
+++ b/examples/resnet50/ptzendnn.cpp
@@ -23,7 +23,7 @@
 #include <array>                // for array
 #include <cassert>              // for assert
 #include <chrono>               // for duration
-#include <cstdint>              // for uint64_t
+#include <cstdint>              // for int64_t
 #include <cstdlib>              // for exit, getenv
 #include <exception>            // for exception
 #include <filesystem>           // for path, oper...
@@ -97,11 +97,11 @@ std::vector<int> postprocess(const amdinfer::InferenceResponseOutput& output,
  * @return std::vector<amdinfer::InferenceRequest>
  */
 std::vector<amdinfer::InferenceRequest> constructRequests(const Images& images,
-                                                          uint64_t input_size) {
+                                                          int64_t input_size) {
   std::vector<amdinfer::InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+  const std::initializer_list<int64_t> shape = {input_size, input_size, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();
diff --git a/examples/resnet50/tfzendnn.cpp b/examples/resnet50/tfzendnn.cpp
index d71875e82..1aff62fda 100644
--- a/examples/resnet50/tfzendnn.cpp
+++ b/examples/resnet50/tfzendnn.cpp
@@ -22,7 +22,7 @@
 #include <algorithm>            // for copy, max
 #include <cassert>              // for assert
 #include <chrono>               // for duration
-#include <cstdint>              // for uint64_t
+#include <cstdint>              // for int64_t
 #include <cstdlib>              // for exit, getenv
 #include <exception>            // for exception
 #include <filesystem>           // for path, oper...
@@ -86,11 +86,11 @@ std::vector<int> postprocess(const amdinfer::InferenceResponseOutput& output,
  * @return std::vector<amdinfer::InferenceRequest>
  */
 std::vector<amdinfer::InferenceRequest> constructRequests(const Images& images,
-                                                          uint64_t input_size) {
+                                                          int64_t input_size) {
   std::vector<amdinfer::InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+  const std::initializer_list<int64_t> shape = {input_size, input_size, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();
diff --git a/examples/resnet50/vitis.cpp b/examples/resnet50/vitis.cpp
index 92554aa8d..c9b0092ac 100644
--- a/examples/resnet50/vitis.cpp
+++ b/examples/resnet50/vitis.cpp
@@ -95,12 +95,12 @@ std::vector<int> postprocess(const amdinfer::InferenceResponseOutput& output,
  * @return std::vector<amdinfer::InferenceRequest>
  */
 std::vector<amdinfer::InferenceRequest> constructRequests(const Images& images,
-                                                          uint64_t input_size) {
+                                                          int64_t input_size) {
   // +construct request:
   std::vector<amdinfer::InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {input_size, input_size, 3};
+  const std::initializer_list<int64_t> shape = {input_size, input_size, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();
diff --git a/external/repository_metadata/invert_image.toml b/external/repository_metadata/invert_image.toml
index 0a718a73f..6497f1cb8 100644
--- a/external/repository_metadata/invert_image.toml
+++ b/external/repository_metadata/invert_image.toml
@@ -5,7 +5,7 @@ id = "base64_decode.so"
 
 [[models.inputs]]
 name = "image_in"
-datatype = "STRING"
+datatype = "BYTES"
 shape = [1048576]
 id = ""
 
@@ -45,6 +45,6 @@ id = "inverted_image"
 
 [[models.outputs]]
 name = "image_out"
-datatype = "STRING"
+datatype = "BYTES"
 shape = [1048576]
 id = "postprocessed_image"
diff --git a/include/amdinfer/clients/client.hpp b/include/amdinfer/clients/client.hpp
index 07e6aa7f9..e3216db0d 100644
--- a/include/amdinfer/clients/client.hpp
+++ b/include/amdinfer/clients/client.hpp
@@ -66,17 +66,20 @@ class Client {
    * @brief Checks if a model/worker is ready
    *
    * @param model name of the model to check
+   * @param version version of the model to check
    * @return bool - true if model is ready, false otherwise
    */
-  [[nodiscard]] virtual bool modelReady(const std::string& model) const = 0;
+  [[nodiscard]] bool modelReady(const std::string& model,
+                                const std::string& version = "") const;
   /**
    * @brief Returns the metadata associated with a ready model/worker
    *
    * @param model name of the model/worker to get metadata
+   * @param version version of the model to get metadata
    * @return ModelMetadata
    */
-  [[nodiscard]] virtual ModelMetadata modelMetadata(
-    const std::string& model) const = 0;
+  [[nodiscard]] ModelMetadata modelMetadata(
+    const std::string& model, const std::string& version = "") const;
 
   /**
    * @brief Loads a model with the given name and load-time parameters. This
@@ -86,16 +89,19 @@ class Client {
    *
    * @param model name of the model to load from the model repository directory
    * @param parameters load-time parameters for the worker supporting the model
+   * @param version model version to load from the model repository directory
    */
-  virtual void modelLoad(const std::string& model,
-                         const ParameterMap& parameters) const = 0;
+  void modelLoad(const std::string& model, const ParameterMap& parameters,
+                 const std::string& version = "") const;
   /**
    * @brief Unloads a previously loaded model and shut it down. This is
    * identical in functionality to workerUnload and is provided for symmetry.
    *
    * @param model name of the model to unload
+   * @param version version of the model to unload
    */
-  virtual void modelUnload(const std::string& model) const = 0;
+  void modelUnload(const std::string& model,
+                   const std::string& version = "") const;
 
   /**
    * @brief Makes a synchronous inference request to the given model/worker. The
@@ -104,10 +110,12 @@ class Client {
    *
    * @param model name of the model/worker to request inference to
    * @param request the request
+   * @param version version of the model to request inference to
    * @return InferenceResponse
    */
-  [[nodiscard]] virtual InferenceResponse modelInfer(
-    const std::string& model, const InferenceRequest& request) const = 0;
+  [[nodiscard]] InferenceResponse modelInfer(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version = "") const;
   /**
    * @brief Makes an asynchronous inference request to the given model/worker.
    * The contents of the request depends on the model/worker that the request
@@ -116,10 +124,12 @@ class Client {
    *
    * @param model name of the model/worker to request inference to
    * @param request the request
+   * @param version version of the model to request inference to
    * @return InferenceResponseFuture
    */
-  [[nodiscard]] virtual InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) const = 0;
+  [[nodiscard]] InferenceResponseFuture modelInferAsync(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version = "") const;
   /**
    * @brief Gets a list of active models on the server, returning their names
    *
@@ -158,6 +168,77 @@ class Client {
 
  protected:
   Client();
+
+  /**
+   * @brief Checks if a model/worker is ready
+   *
+   * @param model name of the model to check
+   * @param version version of the model to check
+   * @return bool - true if model is ready, false otherwise
+   */
+  [[nodiscard]] virtual bool modelReadyImpl(
+    const std::string& model, const std::string& version) const = 0;
+
+  /**
+   * @brief Returns the metadata associated with a ready model/worker
+   *
+   * @param model name of the model/worker to get metadata
+   * @param version version of the model to get metadata
+   * @return ModelMetadata
+   */
+  [[nodiscard]] virtual ModelMetadata modelMetadataImpl(
+    const std::string& model, const std::string& version) const = 0;
+
+  /**
+   * @brief Loads a model with the given name and load-time parameters. This
+   * method assumes that a directory with this model name already exists in the
+   * model repository directory for the server containing the model and its
+   * metadata in the right format.
+   *
+   * @param model name of the model to load from the model repository directory
+   * @param parameters load-time parameters for the worker supporting the model
+   * @param version model version to load from the model repository directory
+   */
+  virtual void modelLoadImpl(const std::string& model,
+                             const ParameterMap& parameters,
+                             const std::string& version) const = 0;
+  /**
+   * @brief Unloads a previously loaded model and shut it down. This is
+   * identical in functionality to workerUnload and is provided for symmetry.
+   *
+   * @param model name of the model to unload
+   * @param version version of the model to unload
+   */
+  virtual void modelUnloadImpl(const std::string& model,
+                               const std::string& version) const = 0;
+
+  /**
+   * @brief Makes a synchronous inference request to the given model/worker. The
+   * contents of the request depends on the model/worker that the request is
+   * for.
+   *
+   * @param model name of the model/worker to request inference to
+   * @param request the request
+   * @param version version of the model to request inference to
+   * @return InferenceResponse
+   */
+  [[nodiscard]] virtual InferenceResponse modelInferImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const = 0;
+  /**
+   * @brief Makes an asynchronous inference request to the given model/worker.
+   * The contents of the request depends on the model/worker that the request
+   * is for. The user must save the Future object and use it to get the results
+   * of the inference later.
+   *
+   * @param model name of the model/worker to request inference to
+   * @param request the request
+   * @param version version of the model to request inference to
+   * @return InferenceResponseFuture
+   */
+  [[nodiscard]] virtual InferenceResponseFuture modelInferAsyncImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const = 0;
 };
 
 /**
@@ -180,14 +261,16 @@ void waitUntilServerReady(const Client* client);
  * @param client a pointer to a client object
  * @param model the model/worker to wait for
  */
-void waitUntilModelReady(const Client* client, const std::string& model);
+void waitUntilModelReady(const Client* client, const std::string& model,
+                         const std::string& version = "");
 /**
  * @brief Blocks until the named model/worker is not ready
  *
  * @param client a pointer to a client object
  * @param model the model/worker to wait for
  */
-void waitUntilModelNotReady(const Client* client, const std::string& model);
+void waitUntilModelNotReady(const Client* client, const std::string& model,
+                            const std::string& version = "");
 
 /**
  * @brief Load an ensemble - a chain of connected workers. This implementation
@@ -209,7 +292,8 @@ std::vector<std::string> loadEnsemble(const Client* client,
  * @param client a pointer to a client object
  * @param models a list of models to unload
  */
-void unloadModels(const Client* client, const std::vector<std::string>& models);
+void unloadModels(const Client* client, const std::vector<std::string>& models,
+                  const std::string& version = "");
 
 /**
  * @brief Makes inference requests in parallel to the specified model. All
@@ -223,7 +307,8 @@ void unloadModels(const Client* client, const std::vector<std::string>& models);
  */
 std::vector<InferenceResponse> inferAsyncOrdered(
   const Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests);
+  const std::vector<InferenceRequest>& requests,
+  const std::string& version = "");
 /**
  * @brief Makes inference requests in parallel to the specified model in
  * batches. Each batch of requests are gathered and the responses are added to a
@@ -238,7 +323,8 @@ std::vector<InferenceResponse> inferAsyncOrdered(
  */
 std::vector<InferenceResponse> inferAsyncOrderedBatched(
   const Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests, size_t batch_size);
+  const std::vector<InferenceRequest>& requests, size_t batch_size,
+  const std::string& version = "");
 
 }  // namespace amdinfer
 
diff --git a/include/amdinfer/clients/grpc.hpp b/include/amdinfer/clients/grpc.hpp
index ce7b81ca0..6dc1a3309 100644
--- a/include/amdinfer/clients/grpc.hpp
+++ b/include/amdinfer/clients/grpc.hpp
@@ -96,21 +96,63 @@ class GrpcClient : public Client {
    * @return bool - true if server is ready, false otherwise
    */
   [[nodiscard]] bool serverReady() const override;
+
+  /**
+   * @brief Gets a list of active models on the server, returning their names
+   *
+   * @return std::vector<std::string>
+   */
+  [[nodiscard]] std::vector<std::string> modelList() const override;
+
+  /**
+   * @brief Loads a worker with the given name and load-time parameters.
+   *
+   * @param worker name of the worker to load
+   * @param parameters load-time parameters for the worker
+   * @return std::string
+   */
+  [[nodiscard]] std::string workerLoad(
+    const std::string& worker, const ParameterMap& parameters) const override;
+  /**
+   * @brief Unloads a previously loaded worker and shut it down. This is
+   * identical in functionality to modelUnload and is provided for symmetry.
+   *
+   * @param worker name of the worker to unload
+   */
+  void workerUnload(const std::string& worker) const override;
+
+  /**
+   * @brief Checks if the server has the requested number of a specific hardware
+   * device
+   *
+   * @param name name of the hardware device to check
+   * @param num number of the device that should exist at minimum
+   * @return bool - true if server has at least the requested number of the
+   * hardware device, false otherwise
+   */
+  [[nodiscard]] bool hasHardware(const std::string& name,
+                                 int num) const override;
+
+ private:
+  class GrpcClientImpl;
+  std::unique_ptr<GrpcClientImpl> impl_;
+
   /**
    * @brief Checks if a model/worker is ready
    *
    * @param model name of the model to check
    * @return bool - true if model is ready, false otherwise
    */
-  [[nodiscard]] bool modelReady(const std::string& model) const override;
+  [[nodiscard]] bool modelReadyImpl(const std::string& model,
+                                    const std::string& version) const override;
   /**
    * @brief Returns the metadata associated with a ready model/worker
    *
    * @param model name of the model/worker to get metadata
    * @return ModelMetadata
    */
-  [[nodiscard]] ModelMetadata modelMetadata(
-    const std::string& model) const override;
+  [[nodiscard]] ModelMetadata modelMetadataImpl(
+    const std::string& model, const std::string& version) const override;
 
   /**
    * @brief Loads a model with the given name and load-time parameters. This
@@ -121,15 +163,16 @@ class GrpcClient : public Client {
    * @param model name of the model to load from the model repository directory
    * @param parameters load-time parameters for the worker supporting the model
    */
-  void modelLoad(const std::string& model,
-                 const ParameterMap& parameters) const override;
+  void modelLoadImpl(const std::string& model, const ParameterMap& parameters,
+                     const std::string& version) const override;
   /**
    * @brief Unloads a previously loaded model and shut it down. This is
    * identical in functionality to workerUnload and is provided for symmetry.
    *
    * @param model name of the model to unload
    */
-  void modelUnload(const std::string& model) const override;
+  void modelUnloadImpl(const std::string& model,
+                       const std::string& version) const override;
 
   /**
    * @brief Makes a synchronous inference request to the given model/worker. The
@@ -140,8 +183,9 @@ class GrpcClient : public Client {
    * @param request the request
    * @return InferenceResponse
    */
-  [[nodiscard]] InferenceResponse modelInfer(
-    const std::string& model, const InferenceRequest& request) const override;
+  [[nodiscard]] InferenceResponse modelInferImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
   /**
    * @brief Makes an asynchronous inference request to the given model/worker.
    * The contents of the request depends on the model/worker that the request
@@ -152,47 +196,9 @@ class GrpcClient : public Client {
    * @param request the request
    * @return InferenceResponseFuture
    */
-  [[nodiscard]] InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) const override;
-  /**
-   * @brief Gets a list of active models on the server, returning their names
-   *
-   * @return std::vector<std::string>
-   */
-  [[nodiscard]] std::vector<std::string> modelList() const override;
-
-  /**
-   * @brief Loads a worker with the given name and load-time parameters.
-   *
-   * @param worker name of the worker to load
-   * @param parameters load-time parameters for the worker
-   * @return std::string
-   */
-  [[nodiscard]] std::string workerLoad(
-    const std::string& worker, const ParameterMap& parameters) const override;
-  /**
-   * @brief Unloads a previously loaded worker and shut it down. This is
-   * identical in functionality to modelUnload and is provided for symmetry.
-   *
-   * @param worker name of the worker to unload
-   */
-  void workerUnload(const std::string& worker) const override;
-
-  /**
-   * @brief Checks if the server has the requested number of a specific hardware
-   * device
-   *
-   * @param name name of the hardware device to check
-   * @param num number of the device that should exist at minimum
-   * @return bool - true if server has at least the requested number of the
-   * hardware device, false otherwise
-   */
-  [[nodiscard]] bool hasHardware(const std::string& name,
-                                 int num) const override;
-
- private:
-  class GrpcClientImpl;
-  std::unique_ptr<GrpcClientImpl> impl_;
+  [[nodiscard]] InferenceResponseFuture modelInferAsyncImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
 };
 
 }  // namespace amdinfer
diff --git a/include/amdinfer/clients/http.hpp b/include/amdinfer/clients/http.hpp
index 6d042b2dc..70c53d8a6 100644
--- a/include/amdinfer/clients/http.hpp
+++ b/include/amdinfer/clients/http.hpp
@@ -96,21 +96,63 @@ class HttpClient : public Client {
    * @return bool - true if server is ready, false otherwise
    */
   [[nodiscard]] bool serverReady() const override;
+
+  /**
+   * @brief Gets a list of active models on the server, returning their names
+   *
+   * @return std::vector<std::string>
+   */
+  [[nodiscard]] std::vector<std::string> modelList() const override;
+
+  /**
+   * @brief Loads a worker with the given name and load-time parameters.
+   *
+   * @param worker name of the worker to load
+   * @param parameters load-time parameters for the worker
+   * @return std::string
+   */
+  [[nodiscard]] std::string workerLoad(
+    const std::string& worker, const ParameterMap& parameters) const override;
+  /**
+   * @brief Unloads a previously loaded worker and shut it down. This is
+   * identical in functionality to modelUnload and is provided for symmetry.
+   *
+   * @param worker name of the worker to unload
+   */
+  void workerUnload(const std::string& worker) const override;
+
+  /**
+   * @brief Checks if the server has the requested number of a specific hardware
+   * device
+   *
+   * @param name name of the hardware device to check
+   * @param num number of the device that should exist at minimum
+   * @return bool - true if server has at least the requested number of the
+   * hardware device, false otherwise
+   */
+  [[nodiscard]] bool hasHardware(const std::string& name,
+                                 int num) const override;
+
+ private:
+  class HttpClientImpl;
+  std::unique_ptr<HttpClientImpl> impl_;
+
   /**
    * @brief Checks if a model/worker is ready
    *
    * @param model name of the model to check
    * @return bool - true if model is ready, false otherwise
    */
-  [[nodiscard]] bool modelReady(const std::string& model) const override;
+  [[nodiscard]] bool modelReadyImpl(const std::string& model,
+                                    const std::string& version) const override;
   /**
    * @brief Returns the metadata associated with a ready model/worker
    *
    * @param model name of the model/worker to get metadata
    * @return ModelMetadata
    */
-  [[nodiscard]] ModelMetadata modelMetadata(
-    const std::string& model) const override;
+  [[nodiscard]] ModelMetadata modelMetadataImpl(
+    const std::string& model, const std::string& version) const override;
 
   /**
    * @brief Loads a model with the given name and load-time parameters. This
@@ -121,15 +163,16 @@ class HttpClient : public Client {
    * @param model name of the model to load from the model repository directory
    * @param parameters load-time parameters for the worker supporting the model
    */
-  void modelLoad(const std::string& model,
-                 const ParameterMap& parameters) const override;
+  void modelLoadImpl(const std::string& model, const ParameterMap& parameters,
+                     const std::string& version) const override;
   /**
    * @brief Unloads a previously loaded model and shut it down. This is
    * identical in functionality to workerUnload and is provided for symmetry.
    *
    * @param model name of the model to unload
    */
-  void modelUnload(const std::string& model) const override;
+  void modelUnloadImpl(const std::string& model,
+                       const std::string& version) const override;
 
   /**
    * @brief Makes a synchronous inference request to the given model/worker. The
@@ -140,8 +183,9 @@ class HttpClient : public Client {
    * @param request the request
    * @return InferenceResponse
    */
-  [[nodiscard]] InferenceResponse modelInfer(
-    const std::string& model, const InferenceRequest& request) const override;
+  [[nodiscard]] InferenceResponse modelInferImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
   /**
    * @brief Makes an asynchronous inference request to the given model/worker.
    * The contents of the request depends on the model/worker that the request
@@ -152,47 +196,9 @@ class HttpClient : public Client {
    * @param request the request
    * @return InferenceResponseFuture
    */
-  [[nodiscard]] InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) const override;
-  /**
-   * @brief Gets a list of active models on the server, returning their names
-   *
-   * @return std::vector<std::string>
-   */
-  [[nodiscard]] std::vector<std::string> modelList() const override;
-
-  /**
-   * @brief Loads a worker with the given name and load-time parameters.
-   *
-   * @param worker name of the worker to load
-   * @param parameters load-time parameters for the worker
-   * @return std::string
-   */
-  [[nodiscard]] std::string workerLoad(
-    const std::string& worker, const ParameterMap& parameters) const override;
-  /**
-   * @brief Unloads a previously loaded worker and shut it down. This is
-   * identical in functionality to modelUnload and is provided for symmetry.
-   *
-   * @param worker name of the worker to unload
-   */
-  void workerUnload(const std::string& worker) const override;
-
-  /**
-   * @brief Checks if the server has the requested number of a specific hardware
-   * device
-   *
-   * @param name name of the hardware device to check
-   * @param num number of the device that should exist at minimum
-   * @return bool - true if server has at least the requested number of the
-   * hardware device, false otherwise
-   */
-  [[nodiscard]] bool hasHardware(const std::string& name,
-                                 int num) const override;
-
- private:
-  class HttpClientImpl;
-  std::unique_ptr<HttpClientImpl> impl_;
+  [[nodiscard]] InferenceResponseFuture modelInferAsyncImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
 };
 
 }  // namespace amdinfer
diff --git a/include/amdinfer/clients/native.hpp b/include/amdinfer/clients/native.hpp
index 2ee2dc8d5..da13a90cf 100644
--- a/include/amdinfer/clients/native.hpp
+++ b/include/amdinfer/clients/native.hpp
@@ -91,21 +91,62 @@ class NativeClient : public Client {
    */
   [[nodiscard]] bool serverReady() const override;
 
+  /**
+   * @brief Gets a list of active models on the server, returning their names
+   *
+   * @return std::vector<std::string>
+   */
+  [[nodiscard]] std::vector<std::string> modelList() const override;
+
+  /**
+   * @brief Loads a worker with the given name and load-time parameters.
+   *
+   * @param worker name of the worker to load
+   * @param parameters load-time parameters for the worker
+   * @return std::string
+   */
+  [[nodiscard]] std::string workerLoad(
+    const std::string& worker, const ParameterMap& parameters) const override;
+  /**
+   * @brief Unloads a previously loaded worker and shut it down. This is
+   * identical in functionality to modelUnload and is provided for symmetry.
+   *
+   * @param worker name of the worker to unload
+   */
+  void workerUnload(const std::string& worker) const override;
+
+  /**
+   * @brief Checks if the server has the requested number of a specific hardware
+   * device
+   *
+   * @param name name of the hardware device to check
+   * @param num number of the device that should exist at minimum
+   * @return bool - true if server has at least the requested number of the
+   * hardware device, false otherwise
+   */
+  [[nodiscard]] bool hasHardware(const std::string& name,
+                                 int num) const override;
+
+ private:
+  struct NativeClientImpl;
+  std::unique_ptr<NativeClientImpl> impl_;
+
   /**
    * @brief Checks if a model/worker is ready
    *
    * @param model name of the model to check
    * @return bool - true if model is ready, false otherwise
    */
-  [[nodiscard]] bool modelReady(const std::string& model) const override;
+  [[nodiscard]] bool modelReadyImpl(const std::string& model,
+                                    const std::string& version) const override;
   /**
    * @brief Returns the metadata associated with a ready model/worker
    *
    * @param model name of the model/worker to get metadata
    * @return ModelMetadata
    */
-  [[nodiscard]] ModelMetadata modelMetadata(
-    const std::string& model) const override;
+  [[nodiscard]] ModelMetadata modelMetadataImpl(
+    const std::string& model, const std::string& version) const override;
 
   /**
    * @brief Loads a model with the given name and load-time parameters. This
@@ -116,15 +157,16 @@ class NativeClient : public Client {
    * @param model name of the model to load from the model repository directory
    * @param parameters load-time parameters for the worker supporting the model
    */
-  void modelLoad(const std::string& model,
-                 const ParameterMap& parameters) const override;
+  void modelLoadImpl(const std::string& model, const ParameterMap& parameters,
+                     const std::string& version) const override;
   /**
    * @brief Unloads a previously loaded model and shut it down. This is
    * identical in functionality to workerUnload and is provided for symmetry.
    *
    * @param model name of the model to unload
    */
-  void modelUnload(const std::string& model) const override;
+  void modelUnloadImpl(const std::string& model,
+                       const std::string& version) const override;
 
   /**
    * @brief Makes a synchronous inference request to the given model/worker. The
@@ -135,8 +177,9 @@ class NativeClient : public Client {
    * @param request the request
    * @return InferenceResponse
    */
-  [[nodiscard]] InferenceResponse modelInfer(
-    const std::string& model, const InferenceRequest& request) const override;
+  [[nodiscard]] InferenceResponse modelInferImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
   /**
    * @brief Makes an asynchronous inference request to the given model/worker.
    * The contents of the request depends on the model/worker that the request
@@ -147,47 +190,9 @@ class NativeClient : public Client {
    * @param request the request
    * @return InferenceResponseFuture
    */
-  [[nodiscard]] InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) const override;
-  /**
-   * @brief Gets a list of active models on the server, returning their names
-   *
-   * @return std::vector<std::string>
-   */
-  [[nodiscard]] std::vector<std::string> modelList() const override;
-
-  /**
-   * @brief Loads a worker with the given name and load-time parameters.
-   *
-   * @param worker name of the worker to load
-   * @param parameters load-time parameters for the worker
-   * @return std::string
-   */
-  [[nodiscard]] std::string workerLoad(
-    const std::string& worker, const ParameterMap& parameters) const override;
-  /**
-   * @brief Unloads a previously loaded worker and shut it down. This is
-   * identical in functionality to modelUnload and is provided for symmetry.
-   *
-   * @param worker name of the worker to unload
-   */
-  void workerUnload(const std::string& worker) const override;
-
-  /**
-   * @brief Checks if the server has the requested number of a specific hardware
-   * device
-   *
-   * @param name name of the hardware device to check
-   * @param num number of the device that should exist at minimum
-   * @return bool - true if server has at least the requested number of the
-   * hardware device, false otherwise
-   */
-  [[nodiscard]] bool hasHardware(const std::string& name,
-                                 int num) const override;
-
- private:
-  struct NativeClientImpl;
-  std::unique_ptr<NativeClientImpl> impl_;
+  [[nodiscard]] InferenceResponseFuture modelInferAsyncImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
 };
 
 }  // namespace amdinfer
diff --git a/include/amdinfer/clients/websocket.hpp b/include/amdinfer/clients/websocket.hpp
index 488676cea..052b1fe2d 100644
--- a/include/amdinfer/clients/websocket.hpp
+++ b/include/amdinfer/clients/websocket.hpp
@@ -90,64 +90,7 @@ class WebSocketClient : public Client {
    * @return bool - true if server is ready, false otherwise
    */
   [[nodiscard]] bool serverReady() const override;
-  /**
-   * @brief Checks if a model/worker is ready
-   *
-   * @param model name of the model to check
-   * @return bool - true if model is ready, false otherwise
-   */
-  [[nodiscard]] bool modelReady(const std::string& model) const override;
-  /**
-   * @brief Returns the metadata associated with a ready model/worker
-   *
-   * @param model name of the model/worker to get metadata
-   * @return ModelMetadata
-   */
-  [[nodiscard]] ModelMetadata modelMetadata(
-    const std::string& model) const override;
-
-  /**
-   * @brief Loads a model with the given name and load-time parameters. This
-   * method assumes that a directory with this model name already exists in the
-   * model repository directory for the server containing the model and its
-   * metadata in the right format.
-   *
-   * @param model name of the model to load from the model repository directory
-   * @param parameters load-time parameters for the worker supporting the model
-   */
-  void modelLoad(const std::string& model,
-                 const ParameterMap& parameters) const override;
-  /**
-   * @brief Unloads a previously loaded model and shut it down. This is
-   * identical in functionality to workerUnload and is provided for symmetry.
-   *
-   * @param model name of the model to unload
-   */
-  void modelUnload(const std::string& model) const override;
 
-  /**
-   * @brief Makes a synchronous inference request to the given model/worker. The
-   * contents of the request depends on the model/worker that the request is
-   * for.
-   *
-   * @param model name of the model/worker to request inference to
-   * @param request the request
-   * @return InferenceResponse
-   */
-  [[nodiscard]] InferenceResponse modelInfer(
-    const std::string& model, const InferenceRequest& request) const override;
-  /**
-   * @brief Makes an asynchronous inference request to the given model/worker.
-   * The contents of the request depends on the model/worker that the request
-   * is for. The user must save the Future object and use it to get the results
-   * of the inference later.
-   *
-   * @param model name of the model/worker to request inference to
-   * @param request the request
-   * @return InferenceResponseFuture
-   */
-  [[nodiscard]] InferenceResponseFuture modelInferAsync(
-    const std::string& model, const InferenceRequest& request) const override;
   /**
    * @brief Gets a list of active models on the server, returning their names
    *
@@ -215,6 +158,69 @@ class WebSocketClient : public Client {
  private:
   class WebSocketClientImpl;
   std::unique_ptr<WebSocketClientImpl> impl_;
+
+  /**
+   * @brief Checks if a model/worker is ready
+   *
+   * @param model name of the model to check
+   * @return bool - true if model is ready, false otherwise
+   */
+  [[nodiscard]] bool modelReadyImpl(const std::string& model,
+                                    const std::string& version) const override;
+  /**
+   * @brief Returns the metadata associated with a ready model/worker
+   *
+   * @param model name of the model/worker to get metadata
+   * @return ModelMetadata
+   */
+  [[nodiscard]] ModelMetadata modelMetadataImpl(
+    const std::string& model, const std::string& version) const override;
+
+  /**
+   * @brief Loads a model with the given name and load-time parameters. This
+   * method assumes that a directory with this model name already exists in the
+   * model repository directory for the server containing the model and its
+   * metadata in the right format.
+   *
+   * @param model name of the model to load from the model repository directory
+   * @param parameters load-time parameters for the worker supporting the model
+   */
+  void modelLoadImpl(const std::string& model, const ParameterMap& parameters,
+                     const std::string& version) const override;
+  /**
+   * @brief Unloads a previously loaded model and shut it down. This is
+   * identical in functionality to workerUnload and is provided for symmetry.
+   *
+   * @param model name of the model to unload
+   */
+  void modelUnloadImpl(const std::string& model,
+                       const std::string& version) const override;
+
+  /**
+   * @brief Makes a synchronous inference request to the given model/worker. The
+   * contents of the request depends on the model/worker that the request is
+   * for.
+   *
+   * @param model name of the model/worker to request inference to
+   * @param request the request
+   * @return InferenceResponse
+   */
+  [[nodiscard]] InferenceResponse modelInferImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
+  /**
+   * @brief Makes an asynchronous inference request to the given model/worker.
+   * The contents of the request depends on the model/worker that the request
+   * is for. The user must save the Future object and use it to get the results
+   * of the inference later.
+   *
+   * @param model name of the model/worker to request inference to
+   * @param request the request
+   * @return InferenceResponseFuture
+   */
+  [[nodiscard]] InferenceResponseFuture modelInferAsyncImpl(
+    const std::string& model, const InferenceRequest& request,
+    const std::string& version) const override;
 };
 
 }  // namespace amdinfer
diff --git a/include/amdinfer/core/data_types.hpp b/include/amdinfer/core/data_types.hpp
index 6c9844a08..6e86e9984 100644
--- a/include/amdinfer/core/data_types.hpp
+++ b/include/amdinfer/core/data_types.hpp
@@ -76,7 +76,7 @@ class DataType {
     FP32 = Fp32,
     Fp64,
     FP64 = Fp64,
-    String,
+    Bytes,
     Unknown,
   };
 
@@ -141,8 +141,8 @@ class DataType {
         return sizeof(float);
       case DataType::Fp64:
         return sizeof(double);
-      case DataType::String:
-        return sizeof(char);
+      case DataType::Bytes:
+        return sizeof(std::byte);
       default:
         throw invalid_argument("Unknown datatype passed");
     }
@@ -182,8 +182,8 @@ class DataType {
         return "FP32";
       case DataType::Fp64:
         return "FP64";
-      case DataType::String:
-        return "STRING";
+      case DataType::Bytes:
+        return "BYTES";
       default:
         throw invalid_argument("Unknown datatype passed");
     }
@@ -228,9 +228,9 @@ class DataType {
       case detail::hash("FP64"):
       case detail::hash("Fp64"):
         return DataType::Fp64;
-      case detail::hash("STRING"):
-      case detail::hash("String"):
-        return DataType::String;
+      case detail::hash("BYTES"):
+      case detail::hash("Bytes"):
+        return DataType::Bytes;
       default:
         throw invalid_argument("Unknown datatype passed");
     }
@@ -289,7 +289,7 @@ auto switchOverTypes(F f, DataType type, [[maybe_unused]] const Args&... args) {
     case DataType::Fp64: {
       return f.template operator()<double>(args...);
     }
-    case DataType::String: {
+    case DataType::Bytes: {
       return f.template operator()<char>(args...);
     }
     default:
diff --git a/include/amdinfer/core/inference_request.hpp b/include/amdinfer/core/inference_request.hpp
index 8a205752a..b52869ec7 100644
--- a/include/amdinfer/core/inference_request.hpp
+++ b/include/amdinfer/core/inference_request.hpp
@@ -48,7 +48,7 @@ class InferenceRequestInput : public InferenceTensor {
    * @param data_type type of the data
    * @param name name to assign
    */
-  InferenceRequestInput(void *data, std::vector<uint64_t> shape,
+  InferenceRequestInput(void *data, std::vector<int64_t> shape,
                         DataType data_type, std::string name = "");
 
   /// Set the request's data
@@ -179,7 +179,7 @@ class InferenceRequest {
    * @param data_type the datatype of the data
    * @param name the name of the input tensor
    */
-  void addInputTensor(void *data, const std::vector<uint64_t> &shape,
+  void addInputTensor(void *data, const std::vector<int64_t> &shape,
                       DataType data_type, const std::string &name = "");
 
   /**
diff --git a/include/amdinfer/core/inference_response.hpp b/include/amdinfer/core/inference_response.hpp
index d9b3610a2..9f354c778 100644
--- a/include/amdinfer/core/inference_response.hpp
+++ b/include/amdinfer/core/inference_response.hpp
@@ -43,7 +43,7 @@ class InferenceResponseOutput : public InferenceTensor {
   //  * @param data_type type of the data
   //  * @param name name to assign
   //  */
-  // InferenceResponseOutput(void *data, std::vector<uint64_t> shape,
+  // InferenceResponseOutput(void *data, std::vector<int64_t> shape,
   //                       DataType data_type, std::string name = "");
 
   /// Set the request's data
diff --git a/include/amdinfer/core/inference_tensor.hpp b/include/amdinfer/core/inference_tensor.hpp
index 981832755..12d5a4a28 100644
--- a/include/amdinfer/core/inference_tensor.hpp
+++ b/include/amdinfer/core/inference_tensor.hpp
@@ -28,7 +28,7 @@ namespace amdinfer {
 class InferenceTensor : public Tensor {
  public:
   /// Construct a new InferenceTensor object
-  InferenceTensor(std::string name, std::vector<uint64_t> shape,
+  InferenceTensor(std::string name, std::vector<int64_t> shape,
                   DataType data_type);
   /// Construct a new InferenceTensor object
   explicit InferenceTensor(const Tensor &tensor);
diff --git a/include/amdinfer/core/model_metadata.hpp b/include/amdinfer/core/model_metadata.hpp
index 54ad90f22..90a63a6bd 100644
--- a/include/amdinfer/core/model_metadata.hpp
+++ b/include/amdinfer/core/model_metadata.hpp
@@ -55,7 +55,7 @@ class ModelMetadata {
    * @param datatype datatype of the tensor
    */
   void addInputTensor(const std::string &name,
-                      std::initializer_list<uint64_t> shape, DataType datatype);
+                      std::initializer_list<int64_t> shape, DataType datatype);
   /**
    * @brief Adds an input tensor to this model
    *
@@ -87,8 +87,7 @@ class ModelMetadata {
    * @param datatype datatype of the tensor
    */
   void addOutputTensor(const std::string &name,
-                       std::initializer_list<uint64_t> shape,
-                       DataType datatype);
+                       std::initializer_list<int64_t> shape, DataType datatype);
   /**
    * @brief Adds an output tensor to this model
    *
diff --git a/include/amdinfer/core/tensor.hpp b/include/amdinfer/core/tensor.hpp
index 8f36f5aff..678e98f62 100644
--- a/include/amdinfer/core/tensor.hpp
+++ b/include/amdinfer/core/tensor.hpp
@@ -33,7 +33,7 @@ namespace amdinfer {
  */
 class Tensor : public Serializable {
  public:
-  Tensor(std::string name, std::vector<uint64_t> shape, DataType data_type);
+  Tensor(std::string name, std::vector<int64_t> shape, DataType data_type);
 
   /// Get the tensor's name
   [[nodiscard]] const std::string &getName() const &;
@@ -42,10 +42,10 @@ class Tensor : public Serializable {
   void setName(std::string name);
 
   /// Get the tensor's shape
-  [[nodiscard]] const std::vector<uint64_t> &getShape() const &;
-  [[nodiscard]] std::vector<uint64_t> getShape() &&;
+  [[nodiscard]] const std::vector<int64_t> &getShape() const &;
+  [[nodiscard]] std::vector<int64_t> getShape() &&;
   /// Sets the tensor's shape
-  void setShape(std::vector<uint64_t> shape);
+  void setShape(std::vector<int64_t> shape);
 
   /// Get the tensor's datatype
   [[nodiscard]] DataType getDatatype() const;
@@ -80,7 +80,7 @@ class Tensor : public Serializable {
 
  private:
   std::string name_;
-  std::vector<uint64_t> shape_;
+  std::vector<int64_t> shape_;
   DataType data_type_;
 };
 
diff --git a/src/amdinfer/bindings/python/CMakeLists.txt b/src/amdinfer/bindings/python/CMakeLists.txt
index 0a70362b8..ea1a494d0 100644
--- a/src/amdinfer/bindings/python/CMakeLists.txt
+++ b/src/amdinfer/bindings/python/CMakeLists.txt
@@ -31,8 +31,10 @@ if(${Pybind11Mkdoc_FOUND})
 
   add_custom_command(
     OUTPUT ${docstrings_filepath}
-    COMMAND pybind11-mkdoc -o ${docstrings_filepath} ${ALL_PUBLIC_HEADER_FILES}
-            ${CXX_STANDARD_FLAG} -I${AMDINFER_PUBLIC_INCLUDE_DIRS}
+    COMMAND
+      pybind11-mkdoc -o ${docstrings_filepath} ${ALL_PUBLIC_HEADER_FILES}
+      ${CXX_STANDARD_FLAG} -I${AMDINFER_PUBLIC_INCLUDE_DIRS}
+      -I${HALF_INCLUDE_DIRS}
     DEPENDS ${ALL_PUBLIC_HEADER_FILES}
   )
 else()
diff --git a/src/amdinfer/bindings/python/clients/client.cpp b/src/amdinfer/bindings/python/clients/client.cpp
index a06692e67..5b057e8db 100644
--- a/src/amdinfer/bindings/python/clients/client.cpp
+++ b/src/amdinfer/bindings/python/clients/client.cpp
@@ -33,25 +33,40 @@ namespace py = pybind11;
 namespace amdinfer {
 
 void wrapClient(py::module_& m) {
-  py::class_<Client> client{m, "Client"};
+  py::class_<Client>(m, "Client")
+    .def("modelReady", &Client::modelReady, py::arg("model"),
+         py::arg("version") = "", DOCS(Client, modelReady))
+    .def("modelMetadata", &Client::modelMetadata, py::arg("model"),
+         py::arg("version") = "", DOCS(Client, modelMetadata))
+    .def("modelLoad", &Client::modelLoad, py::arg("model"),
+         py::arg("parameters") = ParameterMap(), py::arg("version") = "",
+         DOCS(Client, modelLoad))
+    .def("modelUnload", &Client::modelUnload, py::arg("model"),
+         py::arg("version") = "", DOCS(Client, modelUnload))
+    .def("modelInfer", &Client::modelInfer, py::arg("model"),
+         py::arg("request"), py::arg("version") = "", DOCS(Client, modelInfer));
+  // cannot wrap future directly in Python
+  // .def("modelInferAsync", &GrpcClient::modelInferAsync, py::arg("model"),
+  //      py::arg("request"), DOCS(GrpcClient, modelInferAsync))
 
   m.def("serverHasExtension", &serverHasExtension, py::arg("client"),
         py::arg("extension"));
   m.def("waitUntilServerReady", &waitUntilServerReady, py::arg("client"));
   m.def("waitUntilModelReady", &waitUntilModelReady, py::arg("client"),
-        py::arg("model"));
+        py::arg("model"), py::arg("version") = "");
   m.def("waitUntilModelNotReady", &waitUntilModelNotReady, py::arg("client"),
-        py::arg("model"));
+        py::arg("model"), py::arg("version") = "");
 
   m.def("inferAsyncOrdered", &inferAsyncOrdered, py::arg("client"),
-        py::arg("model"), py::arg("requests"));
+        py::arg("model"), py::arg("requests"), py::arg("version") = "");
   m.def("inferAsyncOrderedBatched", &inferAsyncOrderedBatched,
         py::arg("client"), py::arg("model"), py::arg("requests"),
-        py::arg("batch_sizes"));
+        py::arg("batch_sizes"), py::arg("version") = "");
 
   m.def("loadEnsemble", &loadEnsemble, py::arg("client"), py::arg("models"),
         py::arg("parameters"));
-  m.def("unloadModels", &unloadModels, py::arg("client"), py::arg("models"));
+  m.def("unloadModels", &unloadModels, py::arg("client"), py::arg("models"),
+        py::arg("version") = "");
 }
 
 }  // namespace amdinfer
diff --git a/src/amdinfer/bindings/python/clients/grpc.cpp b/src/amdinfer/bindings/python/clients/grpc.cpp
index 2766c0962..ea7e19871 100644
--- a/src/amdinfer/bindings/python/clients/grpc.cpp
+++ b/src/amdinfer/bindings/python/clients/grpc.cpp
@@ -42,23 +42,10 @@ void wrapGrpcClient(py::module_ &m) {
          DOCS(GrpcClient, serverMetadata))
     .def("serverLive", &GrpcClient::serverLive, DOCS(GrpcClient, serverLive))
     .def("serverReady", &GrpcClient::serverReady, DOCS(GrpcClient, serverReady))
-    .def("modelReady", &GrpcClient::modelReady, py::arg("model"),
-         DOCS(GrpcClient, modelReady))
-    .def("modelMetadata", &GrpcClient::modelMetadata, py::arg("model"),
-         DOCS(GrpcClient, modelMetadata))
-    .def("modelLoad", &GrpcClient::modelLoad, py::arg("model"),
-         py::arg("parameters") = ParameterMap(), DOCS(GrpcClient, modelLoad))
-    .def("modelUnload", &GrpcClient::modelUnload, py::arg("model"),
-         DOCS(GrpcClient, modelUnload))
     .def("workerLoad", &GrpcClient::workerLoad, py::arg("model"),
          py::arg("parameters") = ParameterMap(), DOCS(GrpcClient, workerLoad))
     .def("workerUnload", &GrpcClient::workerUnload, py::arg("model"),
          DOCS(GrpcClient, workerUnload))
-    .def("modelInfer", &GrpcClient::modelInfer, py::arg("model"),
-         py::arg("request"), DOCS(GrpcClient, modelInfer))
-    // cannot wrap future directly in Python
-    // .def("modelInferAsync", &GrpcClient::modelInferAsync, py::arg("model"),
-    //      py::arg("request"), DOCS(GrpcClient, modelInferAsync))
     .def("modelList", &GrpcClient::modelList, DOCS(GrpcClient, modelList))
     .def("hasHardware", &GrpcClient::hasHardware, py::arg("name"),
          py::arg("num"), DOCS(GrpcClient, hasHardware));
diff --git a/src/amdinfer/bindings/python/clients/http.cpp b/src/amdinfer/bindings/python/clients/http.cpp
index d879bc668..01e532c7f 100644
--- a/src/amdinfer/bindings/python/clients/http.cpp
+++ b/src/amdinfer/bindings/python/clients/http.cpp
@@ -53,23 +53,10 @@ void wrapHttpClient(py::module_ &m) {
          DOCS(HttpClient, serverMetadata))
     .def("serverLive", &HttpClient::serverLive, DOCS(HttpClient, serverLive))
     .def("serverReady", &HttpClient::serverReady, DOCS(HttpClient, serverReady))
-    .def("modelReady", &HttpClient::modelReady, py::arg("model"),
-         DOCS(HttpClient, modelReady))
-    .def("modelMetadata", &HttpClient::modelMetadata, py::arg("model"),
-         DOCS(HttpClient, modelMetadata))
-    .def("modelLoad", &HttpClient::modelLoad, py::arg("model"),
-         py::arg("parameters") = ParameterMap(), DOCS(HttpClient, modelLoad))
-    .def("modelUnload", &HttpClient::modelUnload, py::arg("model"),
-         DOCS(HttpClient, modelUnload))
     .def("workerLoad", &HttpClient::workerLoad, py::arg("model"),
          py::arg("parameters") = ParameterMap(), DOCS(HttpClient, workerLoad))
     .def("workerUnload", &HttpClient::workerUnload, py::arg("model"),
          DOCS(HttpClient, workerUnload))
-    .def("modelInfer", &HttpClient::modelInfer, py::arg("model"),
-         py::arg("request"), DOCS(HttpClient, modelInfer))
-    // cannot wrap future directly in Python
-    // .def("modelInferAsync", &HttpClient::modelInferAsync, py::arg("model"),
-    //      py::arg("request"), DOCS(HttpClient, modelInferAsync))
     .def("modelList", &HttpClient::modelList, DOCS(HttpClient, modelList))
     .def("hasHardware", &HttpClient::hasHardware, py::arg("name"),
          py::arg("num"), DOCS(HttpClient, hasHardware));
diff --git a/src/amdinfer/bindings/python/clients/native.cpp b/src/amdinfer/bindings/python/clients/native.cpp
index 7f9077d4f..719b7ab82 100644
--- a/src/amdinfer/bindings/python/clients/native.cpp
+++ b/src/amdinfer/bindings/python/clients/native.cpp
@@ -42,23 +42,10 @@ void wrapNativeClient(py::module_ &m) {
          DOCS(NativeClient, serverLive))
     .def("serverReady", &NativeClient::serverReady,
          DOCS(NativeClient, serverReady))
-    .def("modelReady", &NativeClient::modelReady, py::arg("model"),
-         DOCS(NativeClient, modelReady))
-    .def("modelMetadata", &NativeClient::modelMetadata, py::arg("model"),
-         DOCS(NativeClient, modelMetadata))
-    .def("modelLoad", &NativeClient::modelLoad, py::arg("model"),
-         py::arg("parameters") = ParameterMap(), DOCS(NativeClient, modelLoad))
-    .def("modelUnload", &NativeClient::modelUnload, py::arg("model"),
-         DOCS(NativeClient, modelUnload))
     .def("workerLoad", &NativeClient::workerLoad, py::arg("model"),
          py::arg("parameters") = ParameterMap(), DOCS(NativeClient, workerLoad))
     .def("workerUnload", &NativeClient::workerUnload, py::arg("model"),
          DOCS(NativeClient, workerUnload))
-    .def("modelInfer", &NativeClient::modelInfer, py::arg("model"),
-         py::arg("request"), DOCS(NativeClient, modelInfer))
-    // cannot wrap future directly in Python
-    // .def("modelInferAsync", &NativeClient::modelInferAsync, py::arg("model"),
-    //      py::arg("request"), DOCS(NativeClient, modelInferAsync))
     .def("modelList", &NativeClient::modelList, DOCS(NativeClient, modelList))
     .def("hasHardware", &NativeClient::hasHardware, py::arg("name"),
          py::arg("num"), DOCS(NativeClient, hasHardware));
diff --git a/src/amdinfer/bindings/python/clients/websocket.cpp b/src/amdinfer/bindings/python/clients/websocket.cpp
index 49c7df9c5..180bc70d1 100644
--- a/src/amdinfer/bindings/python/clients/websocket.cpp
+++ b/src/amdinfer/bindings/python/clients/websocket.cpp
@@ -48,26 +48,11 @@ void wrapWebSocketClient(py::module_ &m) {
          DOCS(WebSocketClient, serverLive))
     .def("serverReady", &WebSocketClient::serverReady,
          DOCS(WebSocketClient, serverReady))
-    .def("modelReady", &WebSocketClient::modelReady, py::arg("model"),
-         DOCS(WebSocketClient, modelReady))
-    .def("modelMetadata", &WebSocketClient::modelMetadata, py::arg("model"),
-         DOCS(WebSocketClient, modelMetadata))
-    .def("modelLoad", &WebSocketClient::modelLoad, py::arg("model"),
-         py::arg("parameters") = ParameterMap(),
-         DOCS(WebSocketClient, modelLoad))
-    .def("modelUnload", &WebSocketClient::modelUnload, py::arg("model"),
-         DOCS(WebSocketClient, modelUnload))
     .def("workerLoad", &WebSocketClient::workerLoad, py::arg("model"),
          py::arg("parameters") = ParameterMap(),
          DOCS(WebSocketClient, workerLoad))
     .def("workerUnload", &WebSocketClient::workerUnload, py::arg("model"),
          DOCS(WebSocketClient, workerUnload))
-    .def("modelInfer", &WebSocketClient::modelInfer, py::arg("model"),
-         py::arg("request"), DOCS(WebSocketClient, modelInfer))
-    // cannot wrap future directly in Python
-    // .def("modelInferAsync", &WebSocketClient::modelInferAsync,
-    // py::arg("model"),
-    //      py::arg("request"), DOCS(WebSocketClient, modelInferAsync))
     .def("modelInferWs", &WebSocketClient::modelInferWs, py::arg("model"),
          py::arg("request"), DOCS(WebSocketClient, modelInferWs))
     .def("modelRecv", &WebSocketClient::modelRecv,
diff --git a/src/amdinfer/bindings/python/core/data_types.cpp b/src/amdinfer/bindings/python/core/data_types.cpp
index bba226491..718054e33 100644
--- a/src/amdinfer/bindings/python/core/data_types.cpp
+++ b/src/amdinfer/bindings/python/core/data_types.cpp
@@ -66,7 +66,7 @@ void wrapDataType(py::module_& m) {
     .def_property_readonly_static(
       "FLOAT64", [](const py::object& /*self*/) { return DataType("FP64"); })
     .def_property_readonly_static(
-      "STRING", [](const py::object& /*self*/) { return DataType("STRING"); })
+      "BYTES", [](const py::object& /*self*/) { return DataType("BYTES"); })
     .def("size", &DataType::size)
     .def("str", &DataType::str)
     // defines the __eq__ operator for the class
@@ -95,7 +95,7 @@ void wrapDataType(py::module_& m) {
     .value("FLOAT32", DataType::Fp32)
     .value("FP64", DataType::Fp64)
     .value("FLOAT64", DataType::Fp64)
-    .value("STRING", DataType::String);
+    .value("BYTES", DataType::Bytes);
 
   py::implicitly_convertible<DataType::Value, DataType>();
 }
diff --git a/src/amdinfer/bindings/python/core/inference_request.cpp b/src/amdinfer/bindings/python/core/inference_request.cpp
index 9f35b0a6b..167849ad3 100644
--- a/src/amdinfer/bindings/python/core/inference_request.cpp
+++ b/src/amdinfer/bindings/python/core/inference_request.cpp
@@ -59,7 +59,7 @@ void wrapInferenceRequestInput(py::module_ &m) {
     .def(py::init<const Tensor &>(),
          DOCS(InferenceRequestInput, InferenceRequestInput, 2),
          py::arg("tensor"))
-    .def(py::init<void *, std::vector<uint64_t>, DataType, std::string>(),
+    .def(py::init<void *, std::vector<int64_t>, DataType, std::string>(),
          DOCS(InferenceRequestInput, InferenceRequestInput, 3), py::arg("data"),
          py::arg("shape"), py::arg("data_type"), py::arg("data") = "")
     .def("setUint8Data", &setData<uint8_t>, KeepAliveAssign())
diff --git a/src/amdinfer/bindings/python/core/inference_response.cpp b/src/amdinfer/bindings/python/core/inference_response.cpp
index ee2ba1570..53d70bb72 100644
--- a/src/amdinfer/bindings/python/core/inference_response.cpp
+++ b/src/amdinfer/bindings/python/core/inference_response.cpp
@@ -93,7 +93,7 @@ void wrapInferenceResponseOutput(py::module_ &m) {
   // need to use function pointer to disambiguate overloaded function
   // NOLINTNEXTLINE(readability-identifier-naming)
   auto setShape =
-    static_cast<void (InferenceResponseOutput::*)(std::vector<uint64_t>)>(
+    static_cast<void (InferenceResponseOutput::*)(std::vector<int64_t>)>(
       &InferenceResponseOutput::setShape);
 
   py::class_<InferenceResponseOutput, InferenceTensor>(
diff --git a/src/amdinfer/bindings/python/core/inference_tensor.cpp b/src/amdinfer/bindings/python/core/inference_tensor.cpp
index 883b54ae5..39ee218f5 100644
--- a/src/amdinfer/bindings/python/core/inference_tensor.cpp
+++ b/src/amdinfer/bindings/python/core/inference_tensor.cpp
@@ -41,7 +41,7 @@ namespace amdinfer {
 
 void wrapInferenceTensor(py::module_ &m) {
   py::class_<InferenceTensor, Tensor>(m, "InferenceTensor")
-    .def(py::init<std::string, std::vector<uint64_t>, amdinfer::DataType>(),
+    .def(py::init<std::string, std::vector<int64_t>, amdinfer::DataType>(),
          DOCS(InferenceTensor, InferenceTensor), py::arg("name"),
          py::arg("shape"), py::arg("dataType"))
     .def(py::init<Tensor>(), DOCS(InferenceTensor, InferenceTensor, 2),
diff --git a/src/amdinfer/bindings/python/core/tensor.cpp b/src/amdinfer/bindings/python/core/tensor.cpp
index f38d1125c..813384f1e 100644
--- a/src/amdinfer/bindings/python/core/tensor.cpp
+++ b/src/amdinfer/bindings/python/core/tensor.cpp
@@ -41,7 +41,7 @@ namespace amdinfer {
 
 void wrapTensor(py::module_ &m) {
   py::class_<Tensor>(m, "Tensor")
-    .def(py::init<std::string, std::vector<uint64_t>, amdinfer::DataType>(),
+    .def(py::init<std::string, std::vector<int64_t>, amdinfer::DataType>(),
          DOCS(Tensor), py::arg("name"), py::arg("shape"), py::arg("dataType"))
     .def_property("name", &Tensor::getName, &Tensor::setName)
     .def_property("shape", &Tensor::getShape, &Tensor::setShape)
diff --git a/src/amdinfer/bindings/python/src/amdinfer/__init__.py b/src/amdinfer/bindings/python/src/amdinfer/__init__.py
index 3d895760f..5b1ab5283 100644
--- a/src/amdinfer/bindings/python/src/amdinfer/__init__.py
+++ b/src/amdinfer/bindings/python/src/amdinfer/__init__.py
@@ -56,7 +56,7 @@ def _set_data(input_n, image):
         input_n.setFp32Data(image)
     elif input_n.datatype == DataType.FP64:
         input_n.setFp64Data(image)
-    elif input_n.datatype == DataType.STRING:
+    elif input_n.datatype == DataType.BYTES:
         input_n.setStringData(image)
     else:
         raise ValueError("Unsupported type")
@@ -100,7 +100,7 @@ def ImageInferenceRequest(images, asTensor=True):
                 input_n.shape = [*read_image.shape]  # Convert tuple to list
                 input_n = _set_data(input_n, read_image.flatten())
             else:
-                input_n.datatype = DataType.STRING
+                input_n.datatype = DataType.BYTES
                 with open(image, "rb") as f:
                     data = base64.b64encode(f.read()).decode("utf-8")
                     input_n = _set_data(input_n, stringToArray(data))
@@ -200,7 +200,7 @@ def _get_data(request_input: InferenceRequestInput):
         return request_input.getFp32Data()
     if datatype == DataType.FP64:
         return request_input.getFp64Data()
-    if datatype == DataType.STRING:
+    if datatype == DataType.BYTES:
         return request_input.getStringData()
     raise NotImplementedError(f"{datatype.str()} not supported")
 
diff --git a/src/amdinfer/clients/client.cpp b/src/amdinfer/clients/client.cpp
index 70e81e28c..4a404d755 100644
--- a/src/amdinfer/clients/client.cpp
+++ b/src/amdinfer/clients/client.cpp
@@ -30,6 +30,38 @@
 
 namespace amdinfer {
 
+bool Client::modelReady(const std::string& model,
+                        const std::string& version) const {
+  return modelReadyImpl(model, version);
+}
+
+ModelMetadata Client::modelMetadata(const std::string& model,
+                                    const std::string& version) const {
+  return modelMetadataImpl(model, version);
+}
+
+void Client::modelLoad(const std::string& model, const ParameterMap& parameters,
+                       const std::string& version) const {
+  modelLoadImpl(model, parameters, version);
+}
+
+void Client::modelUnload(const std::string& model,
+                         const std::string& version) const {
+  modelUnloadImpl(model, version);
+}
+
+InferenceResponse Client::modelInfer(const std::string& model,
+                                     const InferenceRequest& request,
+                                     const std::string& version) const {
+  return modelInferImpl(model, request, version);
+}
+
+InferenceResponseFuture Client::modelInferAsync(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
+  return modelInferAsyncImpl(model, request, version);
+}
+
 void initializeClientLogging() {
 #ifdef AMDINFER_ENABLE_LOGGING
   LogOptions options{
@@ -72,10 +104,10 @@ std::vector<std::string> loadEnsemble(const Client* client,
   return endpoints;
 }
 
-void unloadModels(const Client* client,
-                  const std::vector<std::string>& models) {
+void unloadModels(const Client* client, const std::vector<std::string>& models,
+                  const std::string& version) {
   for (const auto& model : models) {
-    client->modelUnload(model);
+    client->modelUnload(model, version);
   }
 }
 
@@ -98,32 +130,34 @@ void waitUntilServerReady(const Client* client) {
   }
 }
 
-void waitUntilModelReady(const Client* client, const std::string& model) {
+void waitUntilModelReady(const Client* client, const std::string& model,
+                         const std::string& version) {
   bool ready = false;
   // arbitrarily set to 1ms
   const auto sleep_time = std::chrono::milliseconds(1);
   while (!ready) {
-    ready = client->modelReady(model);
+    ready = client->modelReady(model, version);
     std::this_thread::sleep_for(sleep_time);
   }
 }
 
-void waitUntilModelNotReady(const Client* client, const std::string& model) {
+void waitUntilModelNotReady(const Client* client, const std::string& model,
+                            const std::string& version) {
   bool ready = true;
   // arbitrarily set to 1ms
   const auto sleep_time = std::chrono::milliseconds(1);
   while (ready) {
-    ready = client->modelReady(model);
+    ready = client->modelReady(model, version);
     std::this_thread::sleep_for(sleep_time);
   }
 }
 
 std::vector<InferenceResponse> inferAsyncOrdered(
   const Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests) {
+  const std::vector<InferenceRequest>& requests, const std::string& version) {
   std::queue<InferenceResponseFuture> q;
   for (const auto& request : requests) {
-    q.push(client->modelInferAsync(model, request));
+    q.push(client->modelInferAsync(model, request, version));
   }
 
   const auto num_requests = requests.size();
@@ -139,7 +173,8 @@ std::vector<InferenceResponse> inferAsyncOrdered(
 
 std::vector<InferenceResponse> inferAsyncOrderedBatched(
   const Client* client, const std::string& model,
-  const std::vector<InferenceRequest>& requests, size_t batch_size) {
+  const std::vector<InferenceRequest>& requests, size_t batch_size,
+  const std::string& version) {
   auto num_requests = requests.size();
   std::vector<InferenceResponse> responses;
   responses.reserve(num_requests);
@@ -148,7 +183,7 @@ std::vector<InferenceResponse> inferAsyncOrderedBatched(
 
   while (start_index + batch_size < num_requests) {
     for (auto i = start_index; i < batch_size; ++i) {
-      q.push(client->modelInferAsync(model, requests[i]));
+      q.push(client->modelInferAsync(model, requests[i], version));
     }
 
     for (auto i = 0U; i < batch_size; ++i) {
diff --git a/src/amdinfer/clients/grpc.cpp b/src/amdinfer/clients/grpc.cpp
index e82919d47..36f79389c 100644
--- a/src/amdinfer/clients/grpc.cpp
+++ b/src/amdinfer/clients/grpc.cpp
@@ -121,13 +121,15 @@ bool GrpcClient::serverReady() const {
   throw bad_status(status.error_message());
 }
 
-bool GrpcClient::modelReady(const std::string& model) const {
+bool GrpcClient::modelReadyImpl(const std::string& model,
+                                const std::string& version) const {
   inference::ModelReadyRequest request;
   inference::ModelReadyResponse reply;
 
   ClientContext context;
 
   request.set_name(model);
+  request.set_version(version);
 
   auto* stub = this->impl_->getStub();
   Status status = stub->ModelReady(&context, request, &reply);
@@ -164,13 +166,15 @@ ModelMetadata mapProtoToModelMetadata(
   return metadata;
 }
 
-ModelMetadata GrpcClient::modelMetadata(const std::string& model) const {
+ModelMetadata GrpcClient::modelMetadataImpl(const std::string& model,
+                                            const std::string& version) const {
   inference::ModelMetadataRequest request;
   inference::ModelMetadataResponse reply;
 
   ClientContext context;
 
   request.set_name(model);
+  request.set_version(version);
 
   auto* stub = this->impl_->getStub();
   Status status = stub->ModelMetadata(&context, request, &reply);
@@ -198,8 +202,9 @@ std::vector<std::string> GrpcClient::modelList() const {
   throw bad_status(status.error_message());
 }
 
-void GrpcClient::modelLoad(const std::string& model,
-                           const ParameterMap& parameters) const {
+void GrpcClient::modelLoadImpl(const std::string& model,
+                               const ParameterMap& parameters,
+                               const std::string& version) const {
   inference::ModelLoadRequest request;
   inference::ModelLoadResponse reply;
 
@@ -208,6 +213,7 @@ void GrpcClient::modelLoad(const std::string& model,
   request.set_name(model);
   auto* params = request.mutable_parameters();
   mapParametersToProto(parameters.data(), params);
+  request.set_version(version);
 
   auto* stub = this->impl_->getStub();
   Status status = stub->ModelLoad(&context, request, &reply);
@@ -217,13 +223,15 @@ void GrpcClient::modelLoad(const std::string& model,
   }
 }
 
-void GrpcClient::modelUnload(const std::string& model) const {
+void GrpcClient::modelUnloadImpl(const std::string& model,
+                                 const std::string& version) const {
   inference::ModelUnloadRequest request;
   inference::ModelUnloadResponse reply;
 
   ClientContext context;
 
   request.set_name(model);
+  request.set_version(version);
 
   auto* stub = this->impl_->getStub();
   Status status = stub->ModelUnload(&context, request, &reply);
@@ -271,7 +279,8 @@ void GrpcClient::workerUnload(const std::string& worker) const {
 
 InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
                                const std::string& model,
-                               const InferenceRequest& request) {
+                               const InferenceRequest& request,
+                               const std::string& version) {
   inference::ModelInferRequest grpc_request;
   inference::ModelInferResponse reply;
 
@@ -281,6 +290,7 @@ InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
   AMDINFER_IF_LOGGING(observer.logger = Logger{Loggers::Client});
 
   grpc_request.set_model_name(model);
+  grpc_request.set_model_version(version);
   mapRequestToProto(request, grpc_request, observer);
 
   Status status = stub->ModelInfer(&context, grpc_request, &reply);
@@ -294,14 +304,17 @@ InferenceResponse runInference(inference::GRPCInferenceService::Stub* stub,
   return response;
 }
 
-InferenceResponseFuture GrpcClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) const {
-  return std::async(runInference, this->impl_->getStub(), model, request);
+InferenceResponseFuture GrpcClient::modelInferAsyncImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
+  return std::async(runInference, this->impl_->getStub(), model, request,
+                    version);
 }
 
-InferenceResponse GrpcClient::modelInfer(
-  const std::string& model, const InferenceRequest& request) const {
-  return runInference(this->impl_->getStub(), model, request);
+InferenceResponse GrpcClient::modelInferImpl(const std::string& model,
+                                             const InferenceRequest& request,
+                                             const std::string& version) const {
+  return runInference(this->impl_->getStub(), model, request, version);
 }
 
 bool GrpcClient::hasHardware(const std::string& name, int num) const {
diff --git a/src/amdinfer/clients/grpc_internal.cpp b/src/amdinfer/clients/grpc_internal.cpp
index f8dfea63e..43f06b225 100644
--- a/src/amdinfer/clients/grpc_internal.cpp
+++ b/src/amdinfer/clients/grpc_internal.cpp
@@ -215,7 +215,7 @@ void mapProtoToResponse(const inference::ModelInferResponse& reply,
     InferenceResponseOutput output;
     output.setName(tensor.name());
     output.setDatatype(DataType(tensor.datatype().c_str()));
-    std::vector<uint64_t> shape;
+    std::vector<int64_t> shape;
     shape.reserve(tensor.shape_size());
     auto size = 1U;
     for (const auto& index : tensor.shape()) {
diff --git a/src/amdinfer/clients/http.cpp b/src/amdinfer/clients/http.cpp
index b86c34ab1..c176f4bd3 100644
--- a/src/amdinfer/clients/http.cpp
+++ b/src/amdinfer/clients/http.cpp
@@ -190,19 +190,34 @@ bool HttpClient::serverReady() const {
   return response->statusCode() == drogon::k200OK;
 }
 
-bool HttpClient::modelReady(const std::string& model) const {
+bool HttpClient::modelReadyImpl(const std::string& model,
+                                const std::string& version) const {
   auto* client = this->impl_->getClient();
-  auto req =
-    createGetRequest("/v2/models/" + model + "/ready", impl_->getHeaders());
+  drogon::HttpRequestPtr req;
+  if (version.empty()) {
+    req =
+      createGetRequest("/v2/models/" + model + "/ready", impl_->getHeaders());
+  } else {
+    req = createGetRequest(
+      "/v2/models/" + model + "/versions/" + version + "/ready",
+      impl_->getHeaders());
+  }
 
   auto [result, response] = client->sendRequest(req);
   checkError(result);
   return response->statusCode() == drogon::k200OK;
 }
 
-ModelMetadata HttpClient::modelMetadata(const std::string& model) const {
+ModelMetadata HttpClient::modelMetadataImpl(const std::string& model,
+                                            const std::string& version) const {
   auto* client = this->impl_->getClient();
-  auto req = createGetRequest("/v2/models/" + model, impl_->getHeaders());
+  drogon::HttpRequestPtr req;
+  if (version.empty()) {
+    req = createGetRequest("/v2/models/" + model, impl_->getHeaders());
+  } else {
+    req = createGetRequest("/v2/models/" + model + "/versions/" + version,
+                           impl_->getHeaders());
+  }
 
   auto [result, response] = client->sendRequest(req);
   checkError(result);
@@ -210,15 +225,23 @@ ModelMetadata HttpClient::modelMetadata(const std::string& model) const {
   return mapJsonToModelMetadata(resp.get());
 }
 
-void HttpClient::modelLoad(const std::string& model,
-                           const ParameterMap& parameters) const {
+void HttpClient::modelLoadImpl(const std::string& model,
+                               const ParameterMap& parameters,
+                               const std::string& version) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json = Json::objectValue;
   json = mapParametersToJson(parameters);
 
-  auto req = createPostRequest(json, "/v2/repository/models/" + model + "/load",
-                               impl_->getHeaders());
+  drogon::HttpRequestPtr req;
+  if (version.empty()) {
+    req = createPostRequest(json, "/v2/repository/models/" + model + "/load",
+                            impl_->getHeaders());
+  } else {
+    req = createPostRequest(
+      json, "/v2/repository/models/" + model + "/versions/" + version + "/load",
+      impl_->getHeaders());
+  }
 
   auto [result, response] = client->sendRequest(req);
   checkError(result);
@@ -227,12 +250,21 @@ void HttpClient::modelLoad(const std::string& model,
   }
 }
 
-void HttpClient::modelUnload(const std::string& model) const {
+void HttpClient::modelUnloadImpl(const std::string& model,
+                                 const std::string& version) const {
   auto* client = this->impl_->getClient();
 
   Json::Value json;
-  auto req = createPostRequest(
-    json, "/v2/repository/models/" + model + "/unload", impl_->getHeaders());
+  drogon::HttpRequestPtr req;
+  if (version.empty()) {
+    req = createPostRequest(json, "/v2/repository/models/" + model + "/unload",
+                            impl_->getHeaders());
+  } else {
+    req = createPostRequest(
+      json,
+      "/v2/repository/models/" + model + "/versions/" + version + "/unload",
+      impl_->getHeaders());
+  }
 
   auto [result, response] = client->sendRequest(req);
   checkError(result);
@@ -277,18 +309,28 @@ void HttpClient::workerUnload(const std::string& worker) const {
 
 auto createInferenceRequest(const std::string& model,
                             const InferenceRequest& request,
+                            const std::string& version,
                             const StringMap& headers) {
   if (request.getInputs().empty()) {
     throw invalid_argument("The request's inputs cannot be empty");
   }
 
   auto json = mapRequestToJson(request);
-  return createPostRequest(json, "/v2/models/" + model + "/infer", headers);
+  drogon::HttpRequestPtr req;
+  if (version.empty()) {
+    req = createPostRequest(json, "/v2/models/" + model + "/infer", headers);
+  } else {
+    req = createPostRequest(
+      json, "/v2/models/" + model + "/versions/" + version + "/infer", headers);
+  }
+  return req;
 }
 
-InferenceResponseFuture HttpClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) const {
-  auto req = createInferenceRequest(model, request, impl_->getHeaders());
+InferenceResponseFuture HttpClient::modelInferAsyncImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
+  auto req =
+    createInferenceRequest(model, request, version, impl_->getHeaders());
   auto prom = std::make_shared<std::promise<amdinfer::InferenceResponse>>();
   auto fut = prom->get_future();
 
@@ -319,9 +361,11 @@ InferenceResponseFuture HttpClient::modelInferAsync(
   return fut;
 }
 
-InferenceResponse HttpClient::modelInfer(
-  const std::string& model, const InferenceRequest& request) const {
-  auto req = createInferenceRequest(model, request, impl_->getHeaders());
+InferenceResponse HttpClient::modelInferImpl(const std::string& model,
+                                             const InferenceRequest& request,
+                                             const std::string& version) const {
+  auto req =
+    createInferenceRequest(model, request, version, impl_->getHeaders());
 
   auto* client = this->impl_->getClient();
   auto [result, response] = client->sendRequest(req);
diff --git a/src/amdinfer/clients/http_internal.cpp b/src/amdinfer/clients/http_internal.cpp
index d0c5a1ed2..50394e36b 100644
--- a/src/amdinfer/clients/http_internal.cpp
+++ b/src/amdinfer/clients/http_internal.cpp
@@ -30,7 +30,7 @@
 #include <algorithm>    // for fill
 #include <cassert>      // for assert
 #include <cstddef>      // for size_t, byte
-#include <cstdint>      // for uint64_t, int32_t
+#include <cstdint>      // for int64_t, int32_t
 #include <cstring>      // for memcpy
 #include <string_view>  // for basic_string_view
 #include <utility>      // for move
@@ -131,7 +131,7 @@ InferenceResponse mapJsonToResponse(Json::Value *json) {
     output.setDatatype(DataType(json_output["datatype"].asCString()));
 
     auto json_shape = json_output["shape"];
-    std::vector<uint64_t> shape;
+    std::vector<int64_t> shape;
     shape.reserve(json_shape.size());
     for (const auto &index : json_shape) {
       shape.push_back(index.asUInt());
diff --git a/src/amdinfer/clients/native.cpp b/src/amdinfer/clients/native.cpp
index e571f223d..4596782cb 100644
--- a/src/amdinfer/clients/native.cpp
+++ b/src/amdinfer/clients/native.cpp
@@ -60,14 +60,16 @@ ServerMetadata NativeClient::serverMetadata() const {
 bool NativeClient::serverLive() const { return true; }
 bool NativeClient::serverReady() const { return true; }
 
-ModelMetadata NativeClient::modelMetadata(const std::string& model) const {
-  return impl_->state->modelMetadata(model);
+ModelMetadata NativeClient::modelMetadataImpl(
+  const std::string& model, const std::string& version) const {
+  return impl_->state->modelMetadata(model, version);
 }
 
-void NativeClient::modelLoad(const std::string& model,
-                             const ParameterMap& parameters) const {
+void NativeClient::modelLoadImpl(const std::string& model,
+                                 const ParameterMap& parameters,
+                                 const std::string& version) const {
   auto model_lower = util::toLower(model);
-  impl_->state->modelLoad(model_lower, parameters);
+  impl_->state->modelLoad(model_lower, version, parameters);
 }
 
 std::string NativeClient::workerLoad(const std::string& worker,
@@ -103,8 +105,9 @@ InferenceResponseFuture setCallback(InferenceRequest* request) {
   return future;
 }
 
-InferenceResponseFuture NativeClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) const {
+InferenceResponseFuture NativeClient::modelInferAsyncImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::CppNative);
 #endif
@@ -122,20 +125,22 @@ InferenceResponseFuture NativeClient::modelInferAsync(
   trace->endSpan();
   request_container->trace = std::move(trace);
 #endif
-  impl_->state->modelInfer(model, std::move(request_container));
+  impl_->state->modelInfer(model, std::move(request_container), version);
 
   return future;
 }
 
-InferenceResponse NativeClient::modelInfer(
-  const std::string& model, const InferenceRequest& request) const {
-  auto future = modelInferAsync(model, request);
+InferenceResponse NativeClient::modelInferImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
+  auto future = modelInferAsync(model, request, version);
   return future.get();
 }
 
-void NativeClient::modelUnload(const std::string& model) const {
+void NativeClient::modelUnloadImpl(const std::string& model,
+                                   const std::string& version) const {
   auto model_lower = util::toLower(model);
-  impl_->state->modelUnload(model_lower);
+  impl_->state->modelUnload(model_lower, version);
 }
 
 void NativeClient::workerUnload(const std::string& worker) const {
@@ -143,9 +148,10 @@ void NativeClient::workerUnload(const std::string& worker) const {
   impl_->state->workerUnload(worker_lower);
 }
 
-bool NativeClient::modelReady(const std::string& model) const {
+bool NativeClient::modelReadyImpl(const std::string& model,
+                                  const std::string& version) const {
   try {
-    return impl_->state->modelReady(model);
+    return impl_->state->modelReady(model, version);
   } catch (const invalid_argument&) {
     return false;
   }
diff --git a/src/amdinfer/clients/websocket.cpp b/src/amdinfer/clients/websocket.cpp
index 3b0d14ba3..049b7c909 100644
--- a/src/amdinfer/clients/websocket.cpp
+++ b/src/amdinfer/clients/websocket.cpp
@@ -177,25 +177,29 @@ bool WebSocketClient::serverReady() const {
   return client->serverReady();
 }
 
-bool WebSocketClient::modelReady(const std::string& model) const {
+bool WebSocketClient::modelReadyImpl(const std::string& model,
+                                     const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  return client->modelReady(model);
+  return client->modelReady(model, version);
 }
 
-ModelMetadata WebSocketClient::modelMetadata(const std::string& model) const {
+ModelMetadata WebSocketClient::modelMetadataImpl(
+  const std::string& model, const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  return client->modelMetadata(model);
+  return client->modelMetadata(model, version);
 }
 
-void WebSocketClient::modelLoad(const std::string& model,
-                                const ParameterMap& parameters) const {
+void WebSocketClient::modelLoadImpl(const std::string& model,
+                                    const ParameterMap& parameters,
+                                    const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  client->modelLoad(model, parameters);
+  client->modelLoad(model, parameters, version);
 }
 
-void WebSocketClient::modelUnload(const std::string& model) const {
+void WebSocketClient::modelUnloadImpl(const std::string& model,
+                                      const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  client->modelUnload(model);
+  client->modelUnload(model, version);
 }
 
 std::string WebSocketClient::workerLoad(const std::string& worker,
@@ -209,16 +213,18 @@ void WebSocketClient::workerUnload(const std::string& worker) const {
   client->workerUnload(worker);
 }
 
-InferenceResponseFuture WebSocketClient::modelInferAsync(
-  const std::string& model, const InferenceRequest& request) const {
+InferenceResponseFuture WebSocketClient::modelInferAsyncImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  return client->modelInferAsync(model, request);
+  return client->modelInferAsync(model, request, version);
 }
 
-InferenceResponse WebSocketClient::modelInfer(
-  const std::string& model, const InferenceRequest& request) const {
+InferenceResponse WebSocketClient::modelInferImpl(
+  const std::string& model, const InferenceRequest& request,
+  const std::string& version) const {
   const auto* client = this->impl_->getHttpClient();
-  return client->modelInfer(model, request);
+  return client->modelInfer(model, request, version);
 }
 
 std::vector<std::string> WebSocketClient::modelList() const {
diff --git a/src/amdinfer/core/endpoints.cpp b/src/amdinfer/core/endpoints.cpp
index f28054dec..905c7c920 100644
--- a/src/amdinfer/core/endpoints.cpp
+++ b/src/amdinfer/core/endpoints.cpp
@@ -19,17 +19,19 @@
 
 #include "amdinfer/core/endpoints.hpp"
 
-#include <cassert>      // for assert
+#include <cassert>  // for assert
+#include <regex>
 #include <type_traits>  // for __decay_and_strip<>::__type
 
-#include "amdinfer/batching/batcher.hpp"        // for Batcher
-#include "amdinfer/build_options.hpp"           // for kMaxModelNameSize
-#include "amdinfer/core/exceptions.hpp"         // for invalid_argument
-#include "amdinfer/core/parameters.hpp"         // for ParameterMap
-#include "amdinfer/core/request_container.hpp"  // for RequestContainer
-#include "amdinfer/core/worker_info.hpp"        // for WorkerInfo
-#include "amdinfer/util/string.hpp"             // for startsWith
-#include "amdinfer/util/thread.hpp"             // for setThreadName
+#include "amdinfer/batching/batcher.hpp"         // for Batcher
+#include "amdinfer/build_options.hpp"            // for kMaxModelNameSize
+#include "amdinfer/core/exceptions.hpp"          // for invalid_argument
+#include "amdinfer/core/parameters.hpp"          // for ParameterMap
+#include "amdinfer/core/request_container.hpp"   // for RequestContainer
+#include "amdinfer/core/versioned_endpoint.hpp"  // for getVersionedEndpoint
+#include "amdinfer/core/worker_info.hpp"         // for WorkerInfo
+#include "amdinfer/util/string.hpp"              // for startsWith
+#include "amdinfer/util/thread.hpp"              // for setThreadName
 
 namespace amdinfer {
 
@@ -40,13 +42,15 @@ Endpoints::Endpoints() {
 Endpoints::~Endpoints() { this->shutdown(); }
 
 std::string Endpoints::load(const std::string& worker,
+                            const std::string& version,
                             ParameterMap parameters) {
   std::shared_ptr<amdinfer::UpdateCommand> request;
   std::string retval;
   retval.reserve(kMaxModelNameSize);
   retval = "";
-  request = std::make_shared<UpdateCommand>(UpdateCommandType::Load, worker,
-                                            &parameters, &retval);
+  const auto& versioned_endpoint = getVersionedEndpoint(worker, version);
+  request = std::make_shared<UpdateCommand>(
+    UpdateCommandType::Load, versioned_endpoint, &parameters, &retval);
   update_queue_.enqueue(request);
 
   while (static_cast<std::string*>(request->retval)->empty() &&
@@ -60,20 +64,24 @@ std::string Endpoints::load(const std::string& worker,
   return endpoint;
 }
 
-void Endpoints::unload(const std::string& endpoint) {
-  if (this->exists(endpoint)) {
-    auto request =
-      std::make_shared<UpdateCommand>(UpdateCommandType::Unload, endpoint);
+void Endpoints::unload(const std::string& endpoint,
+                       const std::string& version) {
+  const auto& versioned_endpoint = getVersionedEndpoint(endpoint, version);
+  if (this->exists(versioned_endpoint)) {
+    auto request = std::make_shared<UpdateCommand>(UpdateCommandType::Unload,
+                                                   versioned_endpoint);
     update_queue_.enqueue(request);
   }
 }
 
 // TODO(varunsh): race condition if workers are shutting down
 void Endpoints::infer(const std::string& endpoint,
-                      std::unique_ptr<RequestContainer> request) const {
-  WorkerInfo* worker = this->unsafeGet(endpoint);
+                      std::unique_ptr<RequestContainer> request,
+                      const std::string& version) const {
+  auto versioned_endpoint = getVersionedEndpoint(endpoint, version);
+  WorkerInfo* worker = this->unsafeGet(versioned_endpoint);
   if (worker == nullptr) {
-    throw invalid_argument("Worker " + endpoint + " not found");
+    throw invalid_argument("Worker " + versioned_endpoint + " not found");
   }
   const auto* batcher = worker->getBatcher();
   batcher->enqueue(std::move(request));
@@ -94,11 +102,12 @@ bool Endpoints::exists(const std::string& endpoint) {
   return retval != 0;
 }
 
-bool Endpoints::ready(const std::string& endpoint) {
+bool Endpoints::ready(const std::string& endpoint, const std::string& version) {
   std::shared_ptr<amdinfer::UpdateCommand> request;
   int ready = -1;
-  request = std::make_shared<UpdateCommand>(UpdateCommandType::Ready, endpoint,
-                                            nullptr, &ready);
+  auto versioned_endpoint = getVersionedEndpoint(endpoint, version);
+  request = std::make_shared<UpdateCommand>(
+    UpdateCommandType::Ready, versioned_endpoint, nullptr, &ready);
   update_queue_.enqueue(request);
   while (ready == -1 && request->eptr == nullptr) {
     std::this_thread::yield();
@@ -125,12 +134,14 @@ std::vector<std::string> Endpoints::list() {
   return endpoints;
 }
 
-ModelMetadata Endpoints::metadata(const std::string& endpoint) {
+ModelMetadata Endpoints::metadata(const std::string& endpoint,
+                                  const std::string& version) {
   std::shared_ptr<amdinfer::UpdateCommand> request;
   ModelMetadata metadata{"", ""};
   bool retval = false;
-  request = std::make_shared<UpdateCommand>(UpdateCommandType::Metadata,
-                                            endpoint, &metadata, &retval);
+  auto versioned_endpoint = getVersionedEndpoint(endpoint, version);
+  request = std::make_shared<UpdateCommand>(
+    UpdateCommandType::Metadata, versioned_endpoint, &metadata, &retval);
   update_queue_.enqueue(request);
   while (!retval && request->eptr == nullptr) {
     std::this_thread::yield();
@@ -221,6 +232,7 @@ void Endpoints::updateManager(UpdateCommandQueue* input_queue) {
   AMDINFER_LOG_DEBUG(logger_, "Ending update_thread");
 }
 
+// TODO(varunsh): clarify nomenclature here with worker/model/versioned_model
 std::string Endpoints::unsafeLoad(const std::string& worker,
                                   ParameterMap* parameters) {
   bool share = true;
@@ -247,6 +259,9 @@ std::string Endpoints::unsafeLoad(const std::string& worker,
       if (parameters->has("next")) {
         auto next_endpoint = parameters->get<std::string>("next");
         const auto* next_info = this->unsafeGet(next_endpoint);
+        if (next_info == nullptr) {
+          throw invalid_argument("No next endpoint found at: " + next_endpoint);
+        }
         next = next_info->getInputQueue();
         next_allocators = next_info->getAllocators();
       } else if (worker != "responder") {
diff --git a/src/amdinfer/core/endpoints.hpp b/src/amdinfer/core/endpoints.hpp
index 80e1fe28e..2b0e0a8b9 100644
--- a/src/amdinfer/core/endpoints.hpp
+++ b/src/amdinfer/core/endpoints.hpp
@@ -86,18 +86,21 @@ class Endpoints {
   Endpoints();
   ~Endpoints();
 
-  std::string load(const std::string& worker, ParameterMap parameters);
-  void unload(const std::string& endpoint);
+  std::string load(const std::string& worker, const std::string& version,
+                   ParameterMap parameters);
+  void unload(const std::string& endpoint, const std::string& version);
 
   void infer(const std::string& endpoint,
-             std::unique_ptr<RequestContainer> request) const;
+             std::unique_ptr<RequestContainer> request,
+             const std::string& version) const;
 
   bool exists(const std::string& endpoint);
   // WorkerInfo* get(const std::string& endpoint);
-  bool ready(const std::string& endpoint);
+  bool ready(const std::string& endpoint, const std::string& version);
 
   std::vector<std::string> list();
-  ModelMetadata metadata(const std::string& endpoint);
+  ModelMetadata metadata(const std::string& endpoint,
+                         const std::string& version);
 
   const MemoryPool* getPool() const;
 
diff --git a/src/amdinfer/core/inference.proto b/src/amdinfer/core/inference.proto
index d095cfa8c..301edfd14 100644
--- a/src/amdinfer/core/inference.proto
+++ b/src/amdinfer/core/inference.proto
@@ -296,8 +296,12 @@ message ModelLoadRequest{
   // Model name.
   string name = 1;
 
+  // The version of the model to load. If not given the
+  // server will choose a version based on the model and internal policy.
+  string version = 2;
+
   // Optional load-time parameters.
-  map<string, InferParameter> parameters = 2;
+  map<string, InferParameter> parameters = 3;
 }
 
 message ModelLoadResponse{}
@@ -305,6 +309,10 @@ message ModelLoadResponse{}
 message ModelUnloadRequest{
   // Model name.
   string name = 1;
+
+  // The version of the model to unload. If not given the
+  // server will choose a version based on the model and internal policy.
+  string version = 2;
 }
 
 message ModelUnloadResponse{}
diff --git a/src/amdinfer/core/inference_request.cpp b/src/amdinfer/core/inference_request.cpp
index aa04b95ac..0df7636b0 100644
--- a/src/amdinfer/core/inference_request.cpp
+++ b/src/amdinfer/core/inference_request.cpp
@@ -59,7 +59,7 @@ void InferenceRequest::runCallbackError(std::string_view error_msg) {
 }
 
 void InferenceRequest::addInputTensor(void *data,
-                                      const std::vector<uint64_t> &shape,
+                                      const std::vector<int64_t> &shape,
                                       DataType data_type,
                                       const std::string &name) {
   this->inputs_.emplace_back(data, shape, data_type, name);
@@ -92,7 +92,7 @@ void InferenceRequest::addOutputTensor(const InferenceRequestOutput &output) {
 }
 
 InferenceRequestInput::InferenceRequestInput(void *data,
-                                             std::vector<uint64_t> shape,
+                                             std::vector<int64_t> shape,
                                              DataType data_type,
                                              std::string name)
   : InferenceTensor(std::move(name), std::move(shape), data_type),
diff --git a/src/amdinfer/core/inference_tensor.cpp b/src/amdinfer/core/inference_tensor.cpp
index 637f32e20..646b1c62a 100644
--- a/src/amdinfer/core/inference_tensor.cpp
+++ b/src/amdinfer/core/inference_tensor.cpp
@@ -23,7 +23,7 @@
 
 namespace amdinfer {
 
-InferenceTensor::InferenceTensor(std::string name, std::vector<uint64_t> shape,
+InferenceTensor::InferenceTensor(std::string name, std::vector<int64_t> shape,
                                  DataType data_type)
   : Tensor(std::move(name), std::move(shape), data_type) {}
 
diff --git a/src/amdinfer/core/memory_pool/vart_tensor_allocator.hpp b/src/amdinfer/core/memory_pool/vart_tensor_allocator.hpp
index b3ba8d815..f7fd2a8a9 100644
--- a/src/amdinfer/core/memory_pool/vart_tensor_allocator.hpp
+++ b/src/amdinfer/core/memory_pool/vart_tensor_allocator.hpp
@@ -41,12 +41,12 @@ struct VartTensorHeader {
   bool free;
 
   std::string name;
-  std::vector<uint64_t> shape;
+  std::vector<int64_t> shape;
   DataType datatype;
   size_t batch_size;
 
   VartTensorHeader(std::byte* address, bool free, const std::string& name,
-                   const std::vector<uint64_t>& shape, DataType datatype,
+                   const std::vector<int64_t>& shape, DataType datatype,
                    size_t batch_size)
     : address(address),
       free(free),
diff --git a/src/amdinfer/core/model_config.cpp b/src/amdinfer/core/model_config.cpp
index 906f50a77..79df969aa 100644
--- a/src/amdinfer/core/model_config.cpp
+++ b/src/amdinfer/core/model_config.cpp
@@ -24,6 +24,7 @@
 #include <filesystem>
 
 #include "amdinfer/core/exceptions.hpp"
+#include "amdinfer/core/versioned_endpoint.hpp"  // for getVersionedEndpoint
 #include "amdinfer/util/filesystem.hpp"
 #include "model_config.pb.h"  // for Config, InferP...
 
@@ -107,7 +108,7 @@ std::vector<T> extractArray(const toml::table& table, const std::string& key) {
 ModelConfigTensor extractModelConfigTensor(const toml::table& table) {
   auto name = extractString(table, "name", true);
   auto datatype_str = extractString(table, "datatype", true);
-  auto shape = extractArray<uint64_t>(table, "shape");
+  auto shape = extractArray<int64_t>(table, "shape");
   auto id = extractString(table, "id", false);
 
   DataType datatype{datatype_str.c_str()};
@@ -127,7 +128,7 @@ ModelConfigData extractConfig(const toml::table& table, bool is_ensemble) {
 }
 
 ModelConfigTensor::ModelConfigTensor(std::string name,
-                                     std::vector<uint64_t> shape,
+                                     std::vector<int64_t> shape,
                                      DataType data_type, std::string id)
   : Tensor(std::move(name), std::move(shape), data_type), id_(std::move(id)) {}
 
@@ -187,7 +188,7 @@ void ModelConfig::createModels() {
   }
 }
 
-ModelConfig::ModelConfig(const toml::table& toml) {
+ModelConfig::ModelConfig(const toml::table& toml, const std::string& version) {
   if (toml.empty()) {
     throw invalid_argument("The configuration cannot be empty");
   }
@@ -198,10 +199,15 @@ ModelConfig::ModelConfig(const toml::table& toml) {
     configs_.emplace_back(extractConfig(toml, false));
   }
 
+  for (auto& config : configs_) {
+    config.name = getVersionedEndpoint(config.name, version);
+  }
+
   this->createModels();
 }
 
-ModelConfig::ModelConfig(const inference::Config& config) {
+ModelConfig::ModelConfig(const inference::Config& config,
+                         const std::string& version) {
   // proto does not support ensembles so id is fixed to empty
   const std::string id;
 
@@ -214,7 +220,7 @@ ModelConfig::ModelConfig(const inference::Config& config) {
     const auto& name = input.name();
     const DataType datatype{input.datatype().c_str()};
     const auto& proto_shape = input.shape();
-    std::vector<uint64_t> shape;
+    std::vector<int64_t> shape;
     shape.reserve(proto_shape.size());
     for (const auto& index : proto_shape) {
       shape.push_back(index);
@@ -228,7 +234,7 @@ ModelConfig::ModelConfig(const inference::Config& config) {
     const auto& name = output.name();
     const DataType datatype{output.datatype().c_str()};
     const auto& proto_shape = output.shape();
-    std::vector<uint64_t> shape;
+    std::vector<int64_t> shape;
     shape.reserve(proto_shape.size());
     for (const auto& index : proto_shape) {
       shape.push_back(index);
@@ -236,7 +242,8 @@ ModelConfig::ModelConfig(const inference::Config& config) {
     outputs.emplace_back(name, shape, datatype, id);
   }
 
-  configs_.emplace_back(model_name, platform, id, inputs, outputs);
+  configs_.emplace_back(getVersionedEndpoint(model_name, version), platform, id,
+                        inputs, outputs);
 
   this->createModels();
 }
diff --git a/src/amdinfer/core/model_config.hpp b/src/amdinfer/core/model_config.hpp
index bf9924ab7..8f1583d9a 100644
--- a/src/amdinfer/core/model_config.hpp
+++ b/src/amdinfer/core/model_config.hpp
@@ -42,7 +42,7 @@ namespace amdinfer {
 
 class ModelConfigTensor : public Tensor {
  public:
-  ModelConfigTensor(std::string name, std::vector<uint64_t> shape,
+  ModelConfigTensor(std::string name, std::vector<int64_t> shape,
                     DataType data_type, std::string id);
 
   const std::string& id() const&;
@@ -77,8 +77,8 @@ class ModelConfig {
   using ConstReverseIterator = Container::const_reverse_iterator;
 
  public:
-  explicit ModelConfig(const toml::v3::table& config);
-  explicit ModelConfig(const inference::Config& config);
+  ModelConfig(const toml::v3::table& config, const std::string& version);
+  ModelConfig(const inference::Config& config, const std::string& version);
 
   std::pair<std::string, ParameterMap> get(size_t index);
   void setModelFiles(const std::filesystem::path& base_path);
diff --git a/src/amdinfer/core/model_metadata.cpp b/src/amdinfer/core/model_metadata.cpp
index 84bf08f1c..e57745c22 100644
--- a/src/amdinfer/core/model_metadata.cpp
+++ b/src/amdinfer/core/model_metadata.cpp
@@ -31,14 +31,14 @@ ModelMetadata::ModelMetadata(const std::string &name,
 }
 
 void ModelMetadata::addInputTensor(const std::string &name,
-                                   std::initializer_list<uint64_t> shape,
+                                   std::initializer_list<int64_t> shape,
                                    DataType datatype) {
   this->inputs_.emplace_back(name, shape, datatype);
 }
 
 void ModelMetadata::addInputTensor(const std::string &name,
                                    std::vector<int> shape, DataType datatype) {
-  std::vector<uint64_t> new_shape;
+  std::vector<int64_t> new_shape;
   std::copy(shape.begin(), shape.end(), std::back_inserter(new_shape));
   this->inputs_.emplace_back(name, new_shape, datatype);
 }
@@ -49,14 +49,14 @@ void ModelMetadata::addInputTensor(const Tensor &tensor) {
 }
 
 void ModelMetadata::addOutputTensor(const std::string &name,
-                                    std::initializer_list<uint64_t> shape,
+                                    std::initializer_list<int64_t> shape,
                                     DataType datatype) {
   this->outputs_.emplace_back(name, shape, datatype);
 }
 
 void ModelMetadata::addOutputTensor(const std::string &name,
                                     std::vector<int> shape, DataType datatype) {
-  std::vector<uint64_t> new_shape;
+  std::vector<int64_t> new_shape;
   std::copy(shape.begin(), shape.end(), std::back_inserter(new_shape));
   this->outputs_.emplace_back(name, new_shape, datatype);
 }
diff --git a/src/amdinfer/core/model_repository.cpp b/src/amdinfer/core/model_repository.cpp
index c6431097d..2b0fb61e7 100644
--- a/src/amdinfer/core/model_repository.cpp
+++ b/src/amdinfer/core/model_repository.cpp
@@ -93,7 +93,8 @@ fs::path findConfigFile(const fs::path& model_path, const std::string& model) {
                              model_path.string());
 }
 
-ModelConfig openConfigFile(const fs::path& config_path) {
+ModelConfig openConfigFile(const fs::path& config_path,
+                           const std::string& version) {
   if (config_path.extension() == ".pbtxt") {
     inference::Config proto_config;
 
@@ -112,26 +113,27 @@ ModelConfig openConfigFile(const fs::path& config_path) {
                             " could not be parsed");
     }
 
-    return ModelConfig{proto_config};
+    return ModelConfig{proto_config, version};
   } else if (config_path.extension() == ".toml") {
     auto toml = toml::parse_file(config_path.string());
 
-    return ModelConfig{toml};
+    return ModelConfig{toml, version};
   }
   throw invalid_argument(
     "Unknown config file extension passed to openConfigFile: " +
     config_path.extension().string());
 }
 
-ModelConfig parseModel(const fs::path& repository, const std::string& model) {
+ModelConfig parseModel(const fs::path& repository, const std::string& model,
+                       const std::string& version) {
   AMDINFER_IF_LOGGING(Logger logger{Loggers::Server};)
   auto model_path = repository / model;
 
   auto config_path = findConfigFile(model_path, model);
-  auto config = openConfigFile(config_path);
+  auto config = openConfigFile(config_path, version);
 
-  // TODO(varunsh): support other versions than 1/
-  const auto model_base = config_path.parent_path() / "1";
+  const auto assigned_version = version.empty() ? "1" : version;
+  const auto model_base = config_path.parent_path() / assigned_version;
 
   config.setModelFiles(model_base);
   return config;
@@ -147,7 +149,7 @@ void ModelRepository::setRepository(const fs::path& repository_path,
         auto model = path.path().filename();
         try {
           ParameterMap params;
-          endpoints_->load(model, params);
+          endpoints_->load(model, "", params);
         } catch (const amdinfer::runtime_error& e) {
           AMDINFER_LOG_INFO(
             logger, "Error loading " + model.string() + ": " + e.what());
@@ -182,9 +184,9 @@ void UpdateListener::handleFileAction(
       std::this_thread::sleep_for(delay);
       auto model_name = fs::path(dir).parent_path().filename();
       try {
-        auto config = parseModel(repository_, model_name);
+        auto config = parseModel(repository_, model_name, "");
         for (const auto& [model, parameters] : config) {
-          endpoints_->load(model, parameters);
+          endpoints_->load(model, "", parameters);
         }
       } catch (const runtime_error&) {
         AMDINFER_LOG_INFO(logger, "Error loading " + model_name.string());
@@ -193,7 +195,7 @@ void UpdateListener::handleFileAction(
       // arbitrary delay to make sure filesystem has settled
       std::this_thread::sleep_for(delay);
       auto model = fs::path(dir).parent_path().filename();
-      endpoints_->unload(model.string());
+      endpoints_->unload(model.string(), "");
     }
   }
 
diff --git a/src/amdinfer/core/model_repository.hpp b/src/amdinfer/core/model_repository.hpp
index 22123215e..9f9a46423 100644
--- a/src/amdinfer/core/model_repository.hpp
+++ b/src/amdinfer/core/model_repository.hpp
@@ -41,7 +41,7 @@ class UpdateListener : public efsw::FileWatchListener {
 };
 
 ModelConfig parseModel(const std::filesystem::path& repository,
-                       const std::string& model);
+                       const std::string& model, const std::string& version);
 
 class ModelRepository {
  public:
diff --git a/src/amdinfer/core/shared_state.cpp b/src/amdinfer/core/shared_state.cpp
index 045618edf..eb2f8565f 100644
--- a/src/amdinfer/core/shared_state.cpp
+++ b/src/amdinfer/core/shared_state.cpp
@@ -73,10 +73,11 @@ ServerMetadata SharedState::serverMetadata() {
 }
 
 void SharedState::modelLoad(const std::string& model,
+                            const std::string& version,
                             const ParameterMap& parameters) {
   assert(util::isLower(model));
 
-  auto model_config = parseModel(repository_.getRepository(), model);
+  auto model_config = parseModel(repository_.getRepository(), model, version);
   const auto end = model_config.crend();
   for (auto it = model_config.crbegin(); it != end; ++it) {
     const auto& [model_name, model_parameters] = *it;
@@ -85,12 +86,14 @@ void SharedState::modelLoad(const std::string& model,
     for (const auto& [key, value] : parameters) {
       updated_parameters.put(key, value);
     }
-    endpoints_.load(model_name, updated_parameters);
+    // the config will add the versioned model name already
+    endpoints_.load(model_name, "", updated_parameters);
   }
 }
 
-void SharedState::modelUnload(const std::string& model) {
-  this->workerUnload(model);
+void SharedState::modelUnload(const std::string& model,
+                              const std::string& version) {
+  endpoints_.unload(model, version);
 }
 
 std::string SharedState::workerLoad(const std::string& worker,
@@ -99,27 +102,30 @@ std::string SharedState::workerLoad(const std::string& worker,
 
   auto updated_parameters = parameters;
   updated_parameters.put("worker", worker);
-  return endpoints_.load(worker, updated_parameters);
+  return endpoints_.load(worker, "", updated_parameters);
 }
 
 void SharedState::workerUnload(const std::string& worker) {
   assert(util::isLower(worker));
-  endpoints_.unload(worker);
+  endpoints_.unload(worker, "");
 }
 
 void SharedState::modelInfer(const std::string& model,
-                             std::unique_ptr<RequestContainer> request) {
-  endpoints_.infer(model, std::move(request));
+                             std::unique_ptr<RequestContainer> request,
+                             const std::string& version) {
+  endpoints_.infer(model, std::move(request), version);
 }
 
-bool SharedState::modelReady(const std::string& model) {
-  return endpoints_.ready(model);
+bool SharedState::modelReady(const std::string& model,
+                             const std::string& version) {
+  return endpoints_.ready(model, version);
 }
 
 std::vector<std::string> SharedState::modelList() { return endpoints_.list(); }
 
-ModelMetadata SharedState::modelMetadata(const std::string& model) {
-  return endpoints_.metadata(model);
+ModelMetadata SharedState::modelMetadata(const std::string& model,
+                                         const std::string& version) {
+  return endpoints_.metadata(model, version);
 }
 
 Kernels SharedState::getHardware() {
diff --git a/src/amdinfer/core/shared_state.hpp b/src/amdinfer/core/shared_state.hpp
index dec90ab82..223a2a73f 100644
--- a/src/amdinfer/core/shared_state.hpp
+++ b/src/amdinfer/core/shared_state.hpp
@@ -38,19 +38,22 @@ class ParameterMap;
 
 class SharedState {
  public:
-  void modelLoad(const std::string& model, const ParameterMap& parameters);
-  void modelUnload(const std::string& model);
+  void modelLoad(const std::string& model, const std::string& version,
+                 const ParameterMap& parameters);
+  void modelUnload(const std::string& model, const std::string& version);
   std::string workerLoad(const std::string& worker,
                          const ParameterMap& parameters);
   void workerUnload(const std::string& worker);
 
   static ServerMetadata serverMetadata();
   std::vector<std::string> modelList();
-  bool modelReady(const std::string& model);
-  ModelMetadata modelMetadata(const std::string& model);
+  bool modelReady(const std::string& model, const std::string& version = "");
+  ModelMetadata modelMetadata(const std::string& model,
+                              const std::string& version = "");
 
   void modelInfer(const std::string& model,
-                  std::unique_ptr<RequestContainer> request);
+                  std::unique_ptr<RequestContainer> request,
+                  const std::string& version = "");
 
   static Kernels getHardware();
   static bool hasHardware(const std::string& name, int num);
diff --git a/src/amdinfer/core/tensor.cpp b/src/amdinfer/core/tensor.cpp
index 4a15b822c..9f1858637 100644
--- a/src/amdinfer/core/tensor.cpp
+++ b/src/amdinfer/core/tensor.cpp
@@ -26,8 +26,7 @@
 
 namespace amdinfer {
 
-Tensor::Tensor(std::string name, std::vector<uint64_t> shape,
-               DataType data_type)
+Tensor::Tensor(std::string name, std::vector<int64_t> shape, DataType data_type)
   : name_(std::move(name)), shape_(std::move(shape)), data_type_(data_type) {}
 
 const std::string &Tensor::getName() const & { return this->name_; }
@@ -36,13 +35,11 @@ std::string Tensor::getName() && { return std::move(this->name_); }
 
 void Tensor::setName(std::string name) { name_ = std::move(name); }
 
-const std::vector<uint64_t> &Tensor::getShape() const & { return shape_; }
+const std::vector<int64_t> &Tensor::getShape() const & { return shape_; }
 
-std::vector<uint64_t> Tensor::getShape() && { return std::move(shape_); }
+std::vector<int64_t> Tensor::getShape() && { return std::move(shape_); }
 
-void Tensor::setShape(std::vector<uint64_t> shape) {
-  shape_ = std::move(shape);
-}
+void Tensor::setShape(std::vector<int64_t> shape) { shape_ = std::move(shape); }
 
 [[nodiscard]] DataType Tensor::getDatatype() const { return this->data_type_; }
 
@@ -59,14 +56,14 @@ struct TensorSizes {
 size_t Tensor::serializeSize() const {
   auto size = sizeof(TensorSizes);
   size += name_.length();
-  size += shape_.size() * sizeof(uint64_t);
+  size += shape_.size() * sizeof(int64_t);
   size += sizeof(uint8_t);
   return size;
 }
 
 std::byte *Tensor::serialize(std::byte *data_out) const {
   auto *data = data_out;
-  TensorSizes metadata{name_.length(), shape_.size() * sizeof(uint64_t),
+  TensorSizes metadata{name_.length(), shape_.size() * sizeof(int64_t),
                        sizeof(uint8_t)};
   data = util::copy(metadata, data, sizeof(TensorSizes));
   data = util::copy(name_.c_str(), data, metadata.name);
@@ -84,7 +81,7 @@ const std::byte *Tensor::deserialize(const std::byte *data_in) {
   std::memcpy(name_.data(), data_in, metadata.name);
   data_in += metadata.name;
 
-  shape_.resize(metadata.shape / sizeof(uint64_t));
+  shape_.resize(metadata.shape / sizeof(int64_t));
   std::memcpy(shape_.data(), data_in, metadata.shape);
   data_in += metadata.shape;
 
diff --git a/src/amdinfer/core/versioned_endpoint.hpp b/src/amdinfer/core/versioned_endpoint.hpp
new file mode 100644
index 000000000..ac1d4a0ed
--- /dev/null
+++ b/src/amdinfer/core/versioned_endpoint.hpp
@@ -0,0 +1,55 @@
+// Copyright 2023 Advanced Micro Devices, Inc.
+//
+// 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.
+
+/**
+ * @file
+ * @brief
+ */
+
+#ifndef GUARD_AMDINFER_CORE_VERSIONED_ENDPOINT
+#define GUARD_AMDINFER_CORE_VERSIONED_ENDPOINT
+
+#include <regex>
+#include <string>
+
+namespace amdinfer {
+
+inline std::string getVersionedEndpoint(const std::string& model,
+                                        const std::string& version) {
+  if (version.empty()) {
+    return model;
+  }
+  return model + "_" + version;
+}
+
+inline std::pair<std::string, std::string> splitVersionedEndpoint(
+  const std::string& endpoint) {
+  // match the last _<#> in the string
+  static std::regex version_regex{R"(([\w-]+)(_)(\d+))"};
+  // match[1] is the model, match[2] is _, and match[3] is the version
+  std::smatch match;
+  auto found = std::regex_search(endpoint, match, version_regex);
+  if (!found) {
+    return {std::string{endpoint}, ""};
+  }
+
+  auto model = std::string(match[1].first, match[1].second);
+  auto version = std::string(match[3].first, match[3].second);
+
+  return {model, version};
+}
+
+}  // namespace amdinfer
+
+#endif  // GUARD_AMDINFER_CORE_VERSIONED_ENDPOINT
diff --git a/src/amdinfer/models/base64_decode.cpp b/src/amdinfer/models/base64_decode.cpp
index 0930e91d6..57e7bc6e0 100644
--- a/src/amdinfer/models/base64_decode.cpp
+++ b/src/amdinfer/models/base64_decode.cpp
@@ -43,23 +43,12 @@ std::vector<amdinfer::Tensor> getInputs() {
 
 std::vector<amdinfer::Tensor> getOutputs() { return {}; }
 
-// Support up to Full HD
-const auto kMaxImageHeight = 1080;
-const auto kMaxImageWidth = 1920;
-const auto kMaxImageChannels = 3;
-const auto kMaxImageSize = kMaxImageHeight * kMaxImageWidth * kMaxImageChannels;
-
 amdinfer::BatchPtr run(amdinfer::Batch* batch) {
   amdinfer::Logger logger{amdinfer::Loggers::Server};
 
   auto new_batch = batch->propagate();
   const auto batch_size = batch->size();
 
-  // const auto data_size = amdinfer::DataType("Uint8").size();
-  // std::vector<amdinfer::BufferPtr> input_buffers;
-  // input_buffers.emplace_back(std::make_unique<amdinfer::VectorBuffer>(
-  //   kMaxImageSize * batch_size * data_size));
-
   std::vector<cv::Mat> decoded_images;
   size_t max_decoded_size = 0;
   const auto channels = 3;  // assuming 3 channels
@@ -104,8 +93,8 @@ amdinfer::BatchPtr run(amdinfer::Batch* batch) {
       max_decoded_size = decoded_size;
     }
 
-    std::vector<uint64_t> shape{static_cast<uint64_t>(img.rows),
-                                static_cast<uint64_t>(img.cols), 3};
+    std::vector<int64_t> shape{static_cast<int64_t>(img.rows),
+                               static_cast<int64_t>(img.cols), 3};
 
     new_request->addInputTensor(nullptr, shape, amdinfer::DataType::Uint8,
                                 "output");
diff --git a/src/amdinfer/models/base64_encode.cpp b/src/amdinfer/models/base64_encode.cpp
index a60c1a034..a904c1594 100644
--- a/src/amdinfer/models/base64_encode.cpp
+++ b/src/amdinfer/models/base64_encode.cpp
@@ -43,12 +43,6 @@ std::vector<amdinfer::Tensor> getInputs() {
 
 std::vector<amdinfer::Tensor> getOutputs() { return {}; }
 
-// Support up to Full HD
-const auto kMaxImageHeight = 1080;
-const auto kMaxImageWidth = 1920;
-const auto kMaxImageChannels = 3;
-const auto kMaxImageSize = kMaxImageHeight * kMaxImageWidth * kMaxImageChannels;
-
 amdinfer::BatchPtr run(amdinfer::Batch* batch) {
   AMDINFER_IF_LOGGING(amdinfer::Logger logger{amdinfer::Loggers::Server});
 
@@ -85,8 +79,8 @@ amdinfer::BatchPtr run(amdinfer::Batch* batch) {
       max_encoded_size = encoded_size;
     }
 
-    new_request->addInputTensor(nullptr, {encoded_size},
-                                amdinfer::DataType::String, "output");
+    new_request->addInputTensor(nullptr, {static_cast<int64_t>(encoded_size)},
+                                amdinfer::DataType::Bytes, "output");
     encoded_images.emplace_back(std::move(encoded));
 
     new_batch->addRequest(new_request);
diff --git a/src/amdinfer/models/echo.cpp b/src/amdinfer/models/echo.cpp
index 70d661e30..00dcbb772 100644
--- a/src/amdinfer/models/echo.cpp
+++ b/src/amdinfer/models/echo.cpp
@@ -31,14 +31,14 @@ extern "C" {
 
 std::vector<amdinfer::Tensor> getInputs() {
   std::vector<amdinfer::Tensor> input_tensors;
-  std::vector<size_t> shape = {1};
+  std::vector<int64_t> shape = {1};
   input_tensors.emplace_back("", shape, amdinfer::DataType::Uint32);
   return input_tensors;
 }
 
 std::vector<amdinfer::Tensor> getOutputs() {
   std::vector<amdinfer::Tensor> output_tensors;
-  std::vector<size_t> shape = {1};
+  std::vector<int64_t> shape = {1};
   output_tensors.emplace_back("", shape, amdinfer::DataType::Uint32);
   return output_tensors;
 }
diff --git a/src/amdinfer/models/echo_multi.cpp b/src/amdinfer/models/echo_multi.cpp
index c2ccb255e..aa51d4ec2 100644
--- a/src/amdinfer/models/echo_multi.cpp
+++ b/src/amdinfer/models/echo_multi.cpp
@@ -30,9 +30,9 @@
 #include "amdinfer/util/timer.hpp"
 
 const int kInputTensors = 2;
-const std::array<size_t, kInputTensors> kInputLengths = {1, 2};
+const std::array<int64_t, kInputTensors> kInputLengths = {1, 2};
 const int kOutputTensors = 3;
-const std::array<size_t, kOutputTensors> kOutputLengths = {1, 4, 3};
+const std::array<int64_t, kOutputTensors> kOutputLengths = {1, 4, 3};
 
 extern "C" {
 
@@ -40,7 +40,7 @@ std::vector<amdinfer::Tensor> getInputs() {
   std::vector<amdinfer::Tensor> input_tensors;
   input_tensors.reserve(kInputTensors);
   for (auto i = 0; i < kInputTensors; ++i) {
-    std::vector<size_t> shape = {kInputLengths.at(i)};
+    std::vector<int64_t> shape = {kInputLengths.at(i)};
     input_tensors.emplace_back("input" + std::to_string(i), shape,
                                amdinfer::DataType::Uint32);
   }
@@ -72,7 +72,7 @@ amdinfer::BatchPtr run(amdinfer::Batch* batch) {
     const auto& req = batch->getRequest(j);
     auto new_request = req->propagate();
     for (auto i = 0; i < kOutputTensors; ++i) {
-      std::vector<size_t> shape = {kOutputLengths.at(i)};
+      std::vector<int64_t> shape = {kOutputLengths.at(i)};
       auto* data_ptr =
         input_buffers.at(i)->data(j * kOutputLengths.at(i) * data_size);
       new_request->addInputTensor(data_ptr, shape, amdinfer::DataType::Uint32,
diff --git a/src/amdinfer/servers/grpc_server.cpp b/src/amdinfer/servers/grpc_server.cpp
index 3ed385318..a3f18c570 100644
--- a/src/amdinfer/servers/grpc_server.cpp
+++ b/src/amdinfer/servers/grpc_server.cpp
@@ -313,7 +313,7 @@ InferenceRequestInput getInput(
   InferenceRequestInput input;
   input.setName(req.name());
 
-  std::vector<uint64_t> shape_vector;
+  std::vector<int64_t> shape_vector;
   shape_vector.reserve(req.shape_size());
   for (const auto& index : req.shape()) {
     shape_vector.push_back(static_cast<size_t>(index));
@@ -441,8 +441,9 @@ CALLDATA_IMPL_END
 
 CALLDATA_IMPL(ModelReady, Unary) {
   const auto& model = request_.name();
+  const auto& version = request_.version();
   try {
-    reply_.set_ready(state_->modelReady(model));
+    reply_.set_ready(state_->modelReady(model, version));
     finish(::grpc::Status::OK);
   } catch (const invalid_argument& e) {
     reply_.set_ready(false);
@@ -456,8 +457,9 @@ CALLDATA_IMPL_END
 
 CALLDATA_IMPL(ModelMetadata, Unary) {
   const auto& model = request_.name();
+  const auto& version = request_.version();
   try {
-    auto metadata = state_->modelMetadata(model);
+    auto metadata = state_->modelMetadata(model, version);
     mapModelMetadataToProto(metadata, reply_);
     finish(::grpc::Status::OK);
   } catch (const invalid_argument& e) {
@@ -492,9 +494,10 @@ CALLDATA_IMPL(ModelLoad, Unary) {
   auto parameters = mapProtoToParameters(request_.parameters());
 
   auto* model = request_.mutable_name();
+  const auto& version = request_.version();
   util::toLower(model);
   try {
-    state_->modelLoad(*model, parameters);
+    state_->modelLoad(*model, version, parameters);
   } catch (const runtime_error& e) {
     AMDINFER_LOG_ERROR(logger_, e.what());
     finish(::grpc::Status(StatusCode::NOT_FOUND, e.what()));
@@ -511,7 +514,8 @@ CALLDATA_IMPL_END
 CALLDATA_IMPL(ModelUnload, Unary) {
   auto* model = request_.mutable_name();
   util::toLower(model);
-  state_->modelUnload(*model);
+  const std::string& version = request_.version();
+  state_->modelUnload(*model, version);
   finish(::grpc::Status::OK);
 }
 CALLDATA_IMPL_END
@@ -553,6 +557,7 @@ CALLDATA_IMPL_END
 
 void CallDataModelInfer::handleRequest() noexcept {
   const auto& model = request_.model_name();
+  const auto& version = request_.model_version();
 #ifdef AMDINFER_ENABLE_TRACING
   auto trace = startTrace(&(__func__[0]));
   trace->setAttribute("model", model);
@@ -568,7 +573,7 @@ void CallDataModelInfer::handleRequest() noexcept {
     trace->endSpan();
     request_container->trace = std::move(trace);
 #endif
-    state_->modelInfer(model, std::move(request_container));
+    state_->modelInfer(model, std::move(request_container), version);
   } catch (const invalid_argument& e) {
     AMDINFER_LOG_INFO(logger_, e.what());
     finish(::grpc::Status(StatusCode::NOT_FOUND, e.what()));
diff --git a/src/amdinfer/servers/http_server.cpp b/src/amdinfer/servers/http_server.cpp
index 94cf83284..bb3b10755 100644
--- a/src/amdinfer/servers/http_server.cpp
+++ b/src/amdinfer/servers/http_server.cpp
@@ -175,17 +175,14 @@ drogon::HttpResponsePtr errorHttpResponse(const std::string &error,
   return resp;
 }
 
-using DrogonCallback = std::function<void(const drogon::HttpResponsePtr &)>;
-
 HttpServer::HttpServer(SharedState *state) : state_(state) {
   AMDINFER_LOG_DEBUG(logger_, "Constructed HttpServer");
 }
 
 #ifdef AMDINFER_ENABLE_HTTP
 
-void HttpServer::getServerLive(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback) const {
+void HttpServer::getServerLive(const HttpRequestPtr &req,
+                               DrogonCallback &&callback) const {
   AMDINFER_LOG_INFO(logger_, "Received getServerLive request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
@@ -206,9 +203,8 @@ void HttpServer::getServerLive(
   callback(resp);
 }
 
-void HttpServer::getServerReady(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback) const {
+void HttpServer::getServerReady(const HttpRequestPtr &req,
+                                DrogonCallback &&callback) const {
   AMDINFER_LOG_INFO(logger_, "Received getServerReady request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
@@ -222,19 +218,16 @@ void HttpServer::getServerReady(
   callback(resp);
 }
 
-void HttpServer::getModelReady(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  std::string const &model) const {
-  AMDINFER_LOG_INFO(logger_, "Received getModelReady request");
+void getModelReady(DrogonCallback &&callback, SharedState *state,
+                   const std::string &endpoint, const std::string &version) {
+  AMDINFER_IF_LOGGING(Logger logger{Loggers::Server});
+  AMDINFER_LOG_INFO(logger, "Received getModelReady request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
 #endif
-  (void)req;  // suppress unused variable warning
-
   auto resp = HttpResponse::newHttpResponse();
   try {
-    if (!state_->modelReady(model)) {
+    if (!state->modelReady(endpoint, version)) {
       resp->setStatusCode(HttpStatusCode::k503ServiceUnavailable);
     }
   } catch (const invalid_argument &e) {
@@ -244,9 +237,20 @@ void HttpServer::getModelReady(
   callback(resp);
 }
 
-void HttpServer::getServerMetadata(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback) const {
+void HttpServer::getModelReady([[maybe_unused]] const HttpRequestPtr &req,
+                               DrogonCallback &&callback,
+                               std::string const &model) const {
+  amdinfer::getModelReady(std::move(callback), state_, model, "");
+}
+
+void HttpServer::getModelReadyVersion(
+  [[maybe_unused]] const HttpRequestPtr &req, DrogonCallback &&callback,
+  const std::string &model, const std::string &version) const {
+  amdinfer::getModelReady(std::move(callback), state_, model, version);
+}
+
+void HttpServer::getServerMetadata(const HttpRequestPtr &req,
+                                   DrogonCallback &&callback) const {
   AMDINFER_LOG_INFO(logger_, "Received getServerMetadata request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
@@ -266,20 +270,18 @@ void HttpServer::getServerMetadata(
   callback(resp);
 }
 
-void HttpServer::getModelMetadata(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  const std::string &model) const {
-  AMDINFER_LOG_INFO(logger_, "Received getModelMetadata request");
+void getModelMetadata(DrogonCallback &&callback, SharedState *state,
+                      const std::string &endpoint, const std::string &version) {
+  AMDINFER_IF_LOGGING(Logger logger{Loggers::Server});
+  AMDINFER_LOG_INFO(logger, "Received getModelMetadata request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
 #endif
-  (void)req;  // suppress unused variable warning
 
   Json::Value ret;
   bool error = false;
   try {
-    auto metadata = state_->modelMetadata(model);
+    auto metadata = state->modelMetadata(endpoint, version);
     ret = modelMetadataToJson(metadata);
   } catch (const runtime_error &e) {
     ret["error"] = e.what();
@@ -293,9 +295,20 @@ void HttpServer::getModelMetadata(
   callback(resp);
 }
 
-void HttpServer::modelList(
-  const drogon::HttpRequestPtr &req,
-  std::function<void(const drogon::HttpResponsePtr &)> &&callback) const {
+void HttpServer::getModelMetadata([[maybe_unused]] const HttpRequestPtr &req,
+                                  DrogonCallback &&callback,
+                                  const std::string &model) const {
+  amdinfer::getModelMetadata(std::move(callback), state_, model, "");
+}
+
+void HttpServer::getModelMetadataVersion(
+  [[maybe_unused]] const HttpRequestPtr &req, DrogonCallback &&callback,
+  const std::string &model, const std::string &version) const {
+  amdinfer::getModelMetadata(std::move(callback), state_, model, version);
+}
+
+void HttpServer::modelList(const drogon::HttpRequestPtr &req,
+                           DrogonCallback &&callback) const {
   AMDINFER_LOG_INFO(logger_, "Received modelList request");
   (void)req;  // suppress unused variable warning
   const auto models = state_->modelList();
@@ -310,9 +323,8 @@ void HttpServer::modelList(
   callback(resp);
 }
 
-void HttpServer::hasHardware(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback) const {
+void HttpServer::hasHardware(const HttpRequestPtr &req,
+                             DrogonCallback &&callback) const {
   AMDINFER_LOG_INFO(logger_, "Received hasHardware request");
 #ifdef AMDINFER_ENABLE_METRICS
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestGet);
@@ -350,7 +362,7 @@ InferenceRequestInput getInput(const Json::Value &json,
     throw invalid_argument("No 'shape' key present in request input");
   }
   auto shape = json.get("shape", Json::arrayValue);
-  std::vector<uint64_t> shape_vector;
+  std::vector<int64_t> shape_vector;
   shape_vector.reserve(shape.size());
   for (auto const &i : shape) {
     if (!i.isUInt64()) {
@@ -471,10 +483,9 @@ InferenceRequestPtr getRequest(const std::shared_ptr<Json::Value> &json,
   return request;
 }
 
-void HttpServer::modelInfer(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  std::string const &model) const {
+void modelInfer(const HttpRequestPtr &req, DrogonCallback &&callback,
+                SharedState *state, const std::string &endpoint,
+                const std::string &version) {
 #ifdef AMDINFER_ENABLE_TRACING
   const auto &drogon_headers = req->getHeaders();
   StringMap headers{drogon_headers.begin(), drogon_headers.end()};
@@ -482,7 +493,8 @@ void HttpServer::modelInfer(
   trace->setAttribute("model", model);
 #endif
 
-  AMDINFER_LOG_INFO(logger_, "Received modelInfer request for " + model);
+  AMDINFER_IF_LOGGING(Logger logger{Loggers::Server});
+  AMDINFER_LOG_INFO(logger, "Received modelInfer request for " + endpoint);
 #ifdef AMDINFER_ENABLE_METRICS
   auto now = std::chrono::high_resolution_clock::now();
   Metrics::getInstance().incrementCounter(MetricCounterIDs::RestPost);
@@ -494,7 +506,7 @@ void HttpServer::modelInfer(
 
   auto json = req->getJsonObject();
   try {
-    auto request = getRequest(json, state_->getPool());
+    auto request = getRequest(json, state->getPool());
     setCallback(request.get(), std::move(callback));
     auto request_container = std::make_unique<RequestContainer>();
     request_container->request = request;
@@ -505,9 +517,9 @@ void HttpServer::modelInfer(
     trace->endSpan();
     request_container->trace = std::move(trace);
 #endif
-    state_->modelInfer(model, std::move(request_container));
+    state->modelInfer(endpoint, std::move(request_container), version);
   } catch (const invalid_argument &e) {
-    AMDINFER_LOG_INFO(logger_, e.what());
+    AMDINFER_LOG_INFO(logger, e.what());
     auto resp = errorHttpResponse(e.what(), HttpStatusCode::k400BadRequest);
     // TODO(varunsh): could use after move here from the try block
     // #ifdef AMDINFER_ENABLE_TRACING
@@ -518,10 +530,22 @@ void HttpServer::modelInfer(
   }
 }
 
-void HttpServer::modelLoad(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  const std::string &model) const {
+void HttpServer::modelInfer(const HttpRequestPtr &req,
+                            DrogonCallback &&callback,
+                            const std::string &model) const {
+  amdinfer::modelInfer(req, std::move(callback), state_, model, "");
+}
+
+void HttpServer::modelInferVersion(const HttpRequestPtr &req,
+                                   DrogonCallback &&callback,
+                                   const std::string &model,
+                                   const std::string &version) const {
+  amdinfer::modelInfer(req, std::move(callback), state_, model, version);
+}
+
+void modelLoad(const HttpRequestPtr &req, DrogonCallback &&callback,
+               SharedState *state, const std::string &model,
+               const std::string &version) {
   auto model_lower = util::toLower(model);
 #ifdef AMDINFER_ENABLE_TRACING
   const auto &drogon_headers = req->getHeaders();
@@ -529,7 +553,8 @@ void HttpServer::modelLoad(
   auto trace = startTrace(&(__func__[0]), headers);
   trace->setAttribute("model", model_lower);
 #endif
-  AMDINFER_LOG_INFO(logger_, "Received modelLoad request for " + model_lower);
+  AMDINFER_IF_LOGGING(Logger logger{Loggers::Server});
+  AMDINFER_LOG_INFO(logger, "Received modelLoad request for " + model_lower);
 
   auto json = req->getJsonObject();
   ParameterMap parameters;
@@ -541,9 +566,9 @@ void HttpServer::modelLoad(
 #endif
 
   try {
-    state_->modelLoad(model_lower, parameters);
+    state->modelLoad(model_lower, version, parameters);
   } catch (const runtime_error &e) {
-    AMDINFER_LOG_ERROR(logger_, e.what());
+    AMDINFER_LOG_ERROR(logger, e.what());
     auto resp = errorHttpResponse(e.what(), HttpStatusCode::k400BadRequest);
 #ifdef AMDINFER_ENABLE_TRACING
     auto context = trace->propagate();
@@ -560,24 +585,36 @@ void HttpServer::modelLoad(
   callback(resp);
 }
 
-void HttpServer::modelUnload(
-  [[maybe_unused]] const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  const std::string &model) const {
-  AMDINFER_LOG_INFO(logger_, "Received modelUnload request");
+void HttpServer::modelLoad(const HttpRequestPtr &req, DrogonCallback &&callback,
+                           const std::string &model) const {
+  amdinfer::modelLoad(req, std::move(callback), state_, model, "");
+}
+
+void HttpServer::modelLoadVersion(const HttpRequestPtr &req,
+                                  DrogonCallback &&callback,
+                                  const std::string &model,
+                                  const std::string &version) const {
+  amdinfer::modelLoad(req, std::move(callback), state_, model, version);
+}
+
+void modelUnload([[maybe_unused]] const HttpRequestPtr &req,
+                 DrogonCallback &&callback, SharedState *state,
+                 const std::string &endpoint, const std::string &version) {
+  AMDINFER_IF_LOGGING(Logger logger{Loggers::Server});
+  AMDINFER_LOG_INFO(logger, "Received modelUnload request");
 #ifdef AMDINFER_ENABLE_TRACING
   const auto &drogon_headers = req->getHeaders();
   StringMap headers{drogon_headers.begin(), drogon_headers.end()};
   auto trace = startTrace(&(__func__[0]), headers);
 #endif
 
-  auto model_lower = util::toLower(model);
+  auto endpoint_lower = util::toLower(endpoint);
 
 #ifdef AMDINFER_ENABLE_TRACING
-  trace->setAttribute("model", model_lower);
+  trace->setAttribute("model", endpoint_lower);
 #endif
 
-  state_->modelUnload(model_lower);
+  state->modelUnload(endpoint_lower, version);
 
   auto resp = HttpResponse::newHttpResponse();
 #ifdef AMDINFER_ENABLE_TRACING
@@ -587,10 +624,22 @@ void HttpServer::modelUnload(
   callback(resp);
 }
 
-void HttpServer::workerLoad(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  const std::string &worker) const {
+void HttpServer::modelUnload(const HttpRequestPtr &req,
+                             DrogonCallback &&callback,
+                             const std::string &model) const {
+  amdinfer::modelUnload(req, std::move(callback), state_, model, "");
+}
+
+void HttpServer::modelUnloadVersion(const HttpRequestPtr &req,
+                                    DrogonCallback &&callback,
+                                    const std::string &model,
+                                    const std::string &version) const {
+  amdinfer::modelUnload(req, std::move(callback), state_, model, version);
+}
+
+void HttpServer::workerLoad(const HttpRequestPtr &req,
+                            DrogonCallback &&callback,
+                            const std::string &worker) const {
   AMDINFER_LOG_INFO(logger_, "Received load request");
 #ifdef AMDINFER_ENABLE_TRACING
   const auto &drogon_headers = req->getHeaders();
@@ -631,10 +680,9 @@ void HttpServer::workerLoad(
   callback(resp);
 }
 
-void HttpServer::workerUnload(
-  [[maybe_unused]] const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback,
-  const std::string &worker) const {
+void HttpServer::workerUnload([[maybe_unused]] const HttpRequestPtr &req,
+                              DrogonCallback &&callback,
+                              const std::string &worker) const {
 #ifdef AMDINFER_ENABLE_TRACING
   const auto &drogon_headers = req->getHeaders();
   StringMap headers{drogon_headers.begin(), drogon_headers.end()};
@@ -662,9 +710,8 @@ void HttpServer::workerUnload(
 #endif  // AMDINFER_ENABLE_HTTP
 
 #ifdef AMDINFER_ENABLE_METRICS
-void HttpServer::metrics(
-  const HttpRequestPtr &req,
-  std::function<void(const HttpResponsePtr &)> &&callback) const {
+void HttpServer::metrics(const HttpRequestPtr &req,
+                         DrogonCallback &&callback) const {
   (void)req;  // suppress unused variable warning
   AMDINFER_LOG_INFO(logger_, "Received metrics request");
   std::string body = Metrics::getInstance().getMetrics();
diff --git a/src/amdinfer/servers/http_server.hpp b/src/amdinfer/servers/http_server.hpp
index 21243ecdf..d903122fb 100644
--- a/src/amdinfer/servers/http_server.hpp
+++ b/src/amdinfer/servers/http_server.hpp
@@ -46,6 +46,8 @@ class MemoryPool;
 InferenceRequestPtr getRequest(const std::shared_ptr<Json::Value> &json,
                                const MemoryPool *pool);
 
+using DrogonCallback = std::function<void(const drogon::HttpResponsePtr &)>;
+
 /**
  * @brief The HTTP server for handling REST requests extends the base
  * HttpController in Drogon and adds the endpoints of interest.
@@ -58,44 +60,46 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
 
   METHOD_LIST_BEGIN
 
-  /// Register the getServerLive endpoint
   ADD_METHOD_TO(HttpServer::getServerLive, "v2/health/live", drogon::Get,
                 drogon::Options);
-  /// Register the getServerReady endpoint
   ADD_METHOD_TO(HttpServer::getServerReady, "v2/health/ready", drogon::Get,
                 drogon::Options);
-  /// Register the getModelReady endpoint
   ADD_METHOD_TO(HttpServer::getModelReady, "v2/models/{model}/ready",
                 drogon::Get, drogon::Options);
-  /// Register the getServerMetadata endpoint
+  ADD_METHOD_TO(HttpServer::getModelReadyVersion,
+                "v2/models/{model}/versions/{version}/ready", drogon::Get,
+                drogon::Options);
   ADD_METHOD_TO(HttpServer::getServerMetadata, "v2", drogon::Get,
                 drogon::Options);
-  /// Register the getModelMetadata endpoint
   ADD_METHOD_TO(HttpServer::getModelMetadata, "v2/models/{model}", drogon::Get,
                 drogon::Options);
-  /// Register the getHardware endpoint
+  ADD_METHOD_TO(HttpServer::getModelMetadataVersion,
+                "v2/models/{model}/versions/{version}", drogon::Get,
+                drogon::Options);
   ADD_METHOD_TO(HttpServer::hasHardware, "v2/hardware", drogon::Post,
                 drogon::Options);
-  /// Register the modelList endpoint
   ADD_METHOD_TO(HttpServer::modelList, "v2/models", drogon::Get,
                 drogon::Options);
-  /// Register the modelInfer endpoint
   ADD_METHOD_TO(HttpServer::modelInfer, "v2/models/{model}/infer", drogon::Post,
                 drogon::Options);
-  /// Register the load endpoint
+  ADD_METHOD_TO(HttpServer::modelInferVersion,
+                "v2/models/{model}/versions/{version}/infer", drogon::Post,
+                drogon::Options);
   ADD_METHOD_TO(HttpServer::modelLoad, "v2/repository/models/{model}/load",
                 drogon::Post, drogon::Options);
-  /// Register the unload endpoint
+  ADD_METHOD_TO(HttpServer::modelLoadVersion,
+                "v2/repository/models/{model}/versions/{version}/load",
+                drogon::Post, drogon::Options);
   ADD_METHOD_TO(HttpServer::modelUnload, "v2/repository/models/{model}/unload",
                 drogon::Post, drogon::Options);
-  /// Register the workerLoad endpoint
+  ADD_METHOD_TO(HttpServer::modelUnloadVersion,
+                "v2/repository/models/{model}/versions/{version}/unload",
+                drogon::Post, drogon::Options);
   ADD_METHOD_TO(HttpServer::workerLoad, "v2/workers/{worker}/load",
                 drogon::Post, drogon::Options);
-  /// Register the workerUnload endpoint
   ADD_METHOD_TO(HttpServer::workerUnload, "v2/workers/{worker}/unload",
                 drogon::Post, drogon::Options);
 #ifdef AMDINFER_ENABLE_METRICS
-  /// Register the metrics endpoint
   ADD_METHOD_TO(HttpServer::metrics, "metrics", drogon::Get);
 #endif
   METHOD_LIST_END
@@ -107,9 +111,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void getServerLive(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void getServerLive(const drogon::HttpRequestPtr &req,
+                     DrogonCallback &&callback) const;
 
   /**
    * @brief Returns 200 if all models are ready for inferencing
@@ -117,9 +120,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void getServerReady(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void getServerReady(const drogon::HttpRequestPtr &req,
+                      DrogonCallback &&callback) const;
 
   /**
    * @brief Returns 200 if a specific model is ready for inferencing
@@ -128,10 +130,20 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param callback the callback function to respond to the client
    * @param model name of the model to serve the request
    */
-  void getModelReady(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &model) const;
+  void getModelReady(const drogon::HttpRequestPtr &req,
+                     DrogonCallback &&callback, const std::string &model) const;
+
+  /**
+   * @brief Returns 200 if a specific model is ready for inferencing
+   *
+   * @param req the REST request object
+   * @param callback the callback function to respond to the client
+   * @param model name of the model to check
+   * @param version version of the model to check
+   */
+  void getModelReadyVersion(const drogon::HttpRequestPtr &req,
+                            DrogonCallback &&callback, const std::string &model,
+                            const std::string &version) const;
 
   /**
    * @brief Returns metadata associated with the server
@@ -139,9 +151,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void getServerMetadata(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void getServerMetadata(const drogon::HttpRequestPtr &req,
+                         DrogonCallback &&callback) const;
 
   /**
    * @brief Returns metadata associated with a model
@@ -150,10 +161,21 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param callback the callback function to respond to the client
    * @param model name of the model to serve the request
    */
-  void getModelMetadata(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &model) const;
+  void getModelMetadata(const drogon::HttpRequestPtr &req,
+                        DrogonCallback &&callback,
+                        const std::string &model) const;
+
+  /**
+   * @brief Returns metadata associated with a model
+   *
+   * @param req the REST request object
+   * @param callback the callback function to respond to the client
+   * @param model name of the model to serve the request
+   */
+  void getModelMetadataVersion(const drogon::HttpRequestPtr &req,
+                               DrogonCallback &&callback,
+                               const std::string &model,
+                               const std::string &version) const;
 
   /**
    * @brief Returns the available hardware on the server
@@ -161,9 +183,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void hasHardware(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void hasHardware(const drogon::HttpRequestPtr &req,
+                   DrogonCallback &&callback) const;
 
   /**
    * @brief Returns the active models on the server
@@ -171,9 +192,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void modelList(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void modelList(const drogon::HttpRequestPtr &req,
+                 DrogonCallback &&callback) const;
 
   /**
    * @brief Handles inference requests for named models
@@ -182,10 +202,30 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param callback the callback function to respond to the client
    * @param model name of the model to serve the request
    */
-  void modelInfer(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &model) const;
+  void modelInfer(const drogon::HttpRequestPtr &req, DrogonCallback &&callback,
+                  const std::string &model) const;
+
+  /**
+   * @brief Handles inference requests for named models
+   *
+   * @param req the REST request object
+   * @param callback the callback function to respond to the client
+   * @param model name of the model to serve the request
+   * @param version version of the model to serve the request
+   */
+  void modelInferVersion(const drogon::HttpRequestPtr &req,
+                         DrogonCallback &&callback, const std::string &model,
+                         const std::string &version) const;
+
+  /**
+   * @brief Loads and starts a model
+   *
+   * @param req the REST request object
+   * @param callback the callback function to respond to the client
+   * @param model name of the model to load
+   */
+  void modelLoad(const drogon::HttpRequestPtr &req, DrogonCallback &&callback,
+                 const std::string &model) const;
 
   /**
    * @brief Loads and starts a model
@@ -193,11 +233,21 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    * @param model name of the model to load
+   * @param version version of the model to load
+   */
+  void modelLoadVersion(const drogon::HttpRequestPtr &req,
+                        DrogonCallback &&callback, const std::string &model,
+                        const std::string &version) const;
+
+  /**
+   * @brief Unloads a model
+   *
+   * @param req the REST request object
+   * @param callback the callback function to respond to the client
+   * @param model name of the model to unload
    */
-  void modelLoad(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &model) const;
+  void modelUnload(const drogon::HttpRequestPtr &req, DrogonCallback &&callback,
+                   const std::string &model) const;
 
   /**
    * @brief Unloads a model
@@ -205,11 +255,11 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    * @param model name of the model to unload
+   * @param version version of the model to unload
    */
-  void modelUnload(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &model) const;
+  void modelUnloadVersion(const drogon::HttpRequestPtr &req,
+                          DrogonCallback &&callback, const std::string &model,
+                          const std::string &version) const;
 
   /**
    * @brief Loads and starts a worker
@@ -218,10 +268,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param callback the callback function to respond to the client
    * @param worker name of the worker to load
    */
-  void workerLoad(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &worker) const;
+  void workerLoad(const drogon::HttpRequestPtr &req, DrogonCallback &&callback,
+                  std::string const &worker) const;
 
   /**
    * @brief Unloads a worker
@@ -230,10 +278,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param callback the callback function to respond to the client
    * @param worker name of the worker to unload
    */
-  void workerUnload(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback,
-    std::string const &worker) const;
+  void workerUnload(const drogon::HttpRequestPtr &req,
+                    DrogonCallback &&callback, std::string const &worker) const;
 
 #ifdef AMDINFER_ENABLE_METRICS
   /**
@@ -242,9 +288,8 @@ class HttpServer : public drogon::HttpController<HttpServer, false> {
    * @param req the REST request object
    * @param callback the callback function to respond to the client
    */
-  void metrics(
-    const drogon::HttpRequestPtr &req,
-    std::function<void(const drogon::HttpResponsePtr &)> &&callback) const;
+  void metrics(const drogon::HttpRequestPtr &req,
+               DrogonCallback &&callback) const;
 #endif
  private:
   SharedState *state_;
diff --git a/src/amdinfer/workers/aks_detect.cpp b/src/amdinfer/workers/aks_detect.cpp
index 5ab793efd..821b2f050 100644
--- a/src/amdinfer/workers/aks_detect.cpp
+++ b/src/amdinfer/workers/aks_detect.cpp
@@ -135,9 +135,10 @@ void AksDetect::doAcquire(ParameterMap* parameters) {
 
   this->graph_ = this->sys_manager_->getGraph(this->graph_name_);
 
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, kImageHeight, kImageWidth, kImageChannels},
-    DataType::Int8);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  kImageHeight, kImageWidth, kImageChannels},
+                                 DataType::Int8);
   this->metadata_.addOutputTensor("output", {0, 6}, DataType::FP32);
   this->metadata_.setName(this->graph_name_);
 }
@@ -210,14 +211,19 @@ BatchPtr AksDetect::doRun(Batch* batch, const MemoryPool* pool) {
   std::vector<BufferPtr> input_buffers;
   input_buffers.push_back(
     pool->get(next_allocators_,
-              Tensor{"name", {max_boxes, kDetectResponseSize}, DataType::Fp32},
+              Tensor{"name",
+                     {static_cast<int64_t>(max_boxes), kDetectResponseSize},
+                     DataType::Fp32},
               batch_size));
 
   for (auto i = 0U; i < batch_size; ++i) {
     const auto& req = batch->getRequest(i);
     auto new_request = req->propagate();
     new_request->addInputTensor(InferenceRequestInput{
-      nullptr, {boxes.at(i), kDetectResponseSize}, DataType::Fp32, ""});
+      nullptr,
+      {static_cast<int64_t>(boxes.at(i)), kDetectResponseSize},
+      DataType::Fp32,
+      ""});
     auto* data_ptr = input_buffers.at(0)->data(
       i * max_boxes * kDetectResponseSize * DataType("Fp32").size());
     new_request->setInputTensorData(0, data_ptr);
diff --git a/src/amdinfer/workers/aks_detect_stream.cpp b/src/amdinfer/workers/aks_detect_stream.cpp
index 1fb41a120..fb8e009d5 100644
--- a/src/amdinfer/workers/aks_detect_stream.cpp
+++ b/src/amdinfer/workers/aks_detect_stream.cpp
@@ -42,7 +42,7 @@
 
 #include "amdinfer/batching/batcher.hpp"  // for BatchPtr, Batch, BatchP...
 #include "amdinfer/build_options.hpp"     // for AMDINFER_ENABLE_TRACING
-#include "amdinfer/core/data_types.hpp"   // for DataType, DataType::String
+#include "amdinfer/core/data_types.hpp"   // for DataType, DataType::Bytes
 #include "amdinfer/core/inference_request.hpp"   // for InferenceRequest
 #include "amdinfer/core/inference_response.hpp"  // for InferenceResponse
 #include "amdinfer/core/parameters.hpp"          // for ParameterMap
@@ -126,9 +126,10 @@ void AksDetectStream::doAcquire(ParameterMap* parameters) {
   }
   this->graph_ = this->sys_manager_->getGraph(graph_name);
 
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, kImageHeight, kImageWidth, kImageChannels},
-    DataType::Int8);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  kImageHeight, kImageWidth, kImageChannels},
+                                 DataType::Int8);
   // TODO(varunsh): what should we return here?
   this->metadata_.addOutputTensor("output", {0}, DataType::Uint32);
   this->metadata_.setName(graph_name);
@@ -177,7 +178,7 @@ BatchPtr AksDetectStream::doRun(Batch* batch,
 
       InferenceResponseOutput output;
       output.setName("key");
-      output.setDatatype(DataType::String);
+      output.setDatatype(DataType::Bytes);
       std::string metadata = "[" + std::to_string(video_width) + "," +
                              std::to_string(video_height) + "]";
       auto message = constructMessage(key, std::to_string(fps), metadata);
@@ -185,7 +186,7 @@ BatchPtr AksDetectStream::doRun(Batch* batch,
       buffer.resize(message.size());
       memcpy(buffer.data(), message.data(), message.size());
       output.setData(std::move(buffer));
-      output.setShape({message.size()});
+      output.setShape({static_cast<int64_t>(message.size())});
       resp.addOutput(output);
       req->runCallback(resp);
 
@@ -276,13 +277,13 @@ BatchPtr AksDetectStream::doRun(Batch* batch,
 
             InferenceResponseOutput output;
             output.setName("image");
-            output.setDatatype(DataType::String);
+            output.setDatatype(DataType::Bytes);
             auto message = constructMessage(key, frames.front(), labels[j]);
             std::vector<std::byte> buffer;
             buffer.resize(message.size());
             memcpy(buffer.data(), message.data(), message.size());
             output.setData(std::move(buffer));
-            output.setShape({message.size()});
+            output.setShape({static_cast<int64_t>(message.size())});
             resp.addOutput(output);
             req->runCallback(resp);
             frames.pop();
@@ -327,12 +328,12 @@ BatchPtr AksDetectStream::doRun(Batch* batch,
 
           InferenceResponseOutput output;
           output.setName("image");
-          output.setDatatype(DataType::String);
+          output.setDatatype(DataType::Bytes);
           auto message = constructMessage(key, frames.front(), labels[j]);
           buffer.resize(message.size());
           memcpy(buffer.data(), message.data(), message.size());
           output.setData(std::move(buffer));
-          output.setShape({message.size()});
+          output.setShape({static_cast<int64_t>(message.size())});
           resp.addOutput(output);
           req->runCallback(resp);
           frames.pop();
diff --git a/src/amdinfer/workers/invert_video.cpp b/src/amdinfer/workers/invert_video.cpp
index 178b29f8d..19c7c8f5e 100644
--- a/src/amdinfer/workers/invert_video.cpp
+++ b/src/amdinfer/workers/invert_video.cpp
@@ -28,10 +28,10 @@
 #include <thread>                 // for thread
 #include <vector>                 // for vector
 
-#include "amdinfer/batching/batcher.hpp"  // for Batch, BatchPtrQueue
-#include "amdinfer/build_options.hpp"     // for AMDINFER_ENABLE_TRACING
-#include "amdinfer/core/data_types.hpp"   // for DataType, DataType::String
-#include "amdinfer/core/inference_request.hpp"   // for InferenceRequest
+#include "amdinfer/batching/batcher.hpp"        // for Batch, BatchPtrQueue
+#include "amdinfer/build_options.hpp"           // for AMDINFER_ENABLE_TRACING
+#include "amdinfer/core/data_types.hpp"         // for DataType, DataType::Bytes
+#include "amdinfer/core/inference_request.hpp"  // for InferenceRequest
 #include "amdinfer/core/inference_response.hpp"  // for InferenceResponse
 #include "amdinfer/core/parameters.hpp"          // for ParameterMap
 #include "amdinfer/declarations.hpp"         // for BufferPtr, InferenceRes...
@@ -90,7 +90,7 @@ const auto kMaxUrlLength = 128;
 void InvertVideo::doAcquire(ParameterMap* parameters) {
   (void)parameters;  // suppress unused variable warning
 
-  this->metadata_.addInputTensor("input", {kMaxUrlLength}, DataType::String);
+  this->metadata_.addInputTensor("input", {kMaxUrlLength}, DataType::Bytes);
   // TODO(varunsh): output is variable
   this->metadata_.addOutputTensor(
     "output", {kMaxImageHeight, kMaxImageWidth, kMaxImageChannels},
@@ -137,13 +137,13 @@ BatchPtr InvertVideo::doRun(Batch* batch,
 
       InferenceResponseOutput output;
       output.setName("key");
-      output.setDatatype(DataType::String);
+      output.setDatatype(DataType::Bytes);
       auto message = constructMessage(key, std::to_string(fps));
       std::vector<std::byte> buffer;
       buffer.resize(message.size());
       memcpy(buffer.data(), message.data(), message.size());
       output.setData(std::move(buffer));
-      output.setShape({message.size()});
+      output.setShape({static_cast<int64_t>(message.size())});
       resp.addOutput(output);
       req->runCallback(resp);
       for (int num_frames = 0; num_frames < count; num_frames++) {
@@ -166,12 +166,12 @@ BatchPtr InvertVideo::doRun(Batch* batch,
 
         InferenceResponseOutput output;
         output.setName("image");
-        output.setDatatype(DataType::String);
+        output.setDatatype(DataType::Bytes);
         message = constructMessage(key, encoded);
         buffer.resize(message.size());
         memcpy(buffer.data(), message.data(), message.size());
         output.setData(std::move(buffer));
-        output.setShape({message.size()});
+        output.setShape({static_cast<int64_t>(message.size())});
         resp.addOutput(output);
         req->runCallback(resp);
       }
diff --git a/src/amdinfer/workers/migraphx.cpp b/src/amdinfer/workers/migraphx.cpp
index a80f0941c..e5e991772 100644
--- a/src/amdinfer/workers/migraphx.cpp
+++ b/src/amdinfer/workers/migraphx.cpp
@@ -444,10 +444,10 @@ BatchPtr MIGraphXWorker::doRun(Batch* batch, const MemoryPool* pool) {
     assert(output_shapes.size() == num_output_tensors);
     input_buffers.reserve(num_output_tensors);
     for (auto i = 0U; i < num_output_tensors; ++i) {
-      auto migraphx_shape = migraphx_output[i].get_shape();
-      auto shape = migraphx_shape.lengths();
+      auto migraphx_shape = migraphx_output[i].get_shape().lengths();
       // erase the leading batch size to get the tensor size
-      shape.erase(shape.begin());
+      std::vector<int64_t> shape{migraphx_shape.begin() + 1,
+                                 migraphx_shape.end()};
 
       datatypes.push_back(toDataType(output_shapes[i].type()));
 
@@ -461,10 +461,10 @@ BatchPtr MIGraphXWorker::doRun(Batch* batch, const MemoryPool* pool) {
       auto new_request = req->propagate();
 
       for (auto i = 0U; i < num_output_tensors; ++i) {
-        auto migraphx_shape = migraphx_output[i].get_shape();
-        auto shape = migraphx_shape.lengths();
+        auto migraphx_shape = migraphx_output[i].get_shape().lengths();
         // erase the leading batch size to get the tensor size
-        shape.erase(shape.begin());
+        std::vector<int64_t> shape{migraphx_shape.begin() + 1,
+                                   migraphx_shape.end()};
 
         const auto& datatype = datatypes.at(i);
 
diff --git a/src/amdinfer/workers/pt_zendnn.cpp b/src/amdinfer/workers/pt_zendnn.cpp
index f32ac32cd..36371d696 100644
--- a/src/amdinfer/workers/pt_zendnn.cpp
+++ b/src/amdinfer/workers/pt_zendnn.cpp
@@ -164,9 +164,10 @@ void PtZendnn::doAcquire(ParameterMap* parameters) {
   this->model_ = torch_module;
 
   // Adding metadata for input and output
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, image_height_, image_width_, image_channels_},
-    input_dt_);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  image_height_, image_width_, image_channels_},
+                                 input_dt_);
   this->metadata_.addOutputTensor("output", {0}, DataType::FP32);
   this->metadata_.setName("PtZendnn");
 }
@@ -247,7 +248,7 @@ BatchPtr PtZendnn::doRun(Batch* batch, const MemoryPool* pool) {
 
   // Copy the output from the model to the response object
   size_t response_size = output_classes_;
-  std::vector<size_t> new_shape = {response_size};
+  std::vector<int64_t> new_shape = {static_cast<int64_t>(response_size)};
 
   auto new_batch = batch->propagate();
   std::vector<BufferPtr> input_buffers;
diff --git a/src/amdinfer/workers/resnet50.cpp b/src/amdinfer/workers/resnet50.cpp
index 1f4f27fb9..e8c8b512a 100644
--- a/src/amdinfer/workers/resnet50.cpp
+++ b/src/amdinfer/workers/resnet50.cpp
@@ -121,9 +121,10 @@ void ResNet50::doAcquire(ParameterMap* parameters) {
 
   this->graph_ = this->sys_manager_->getGraph(this->graph_name_);
 
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, kImageHeight, kImageWidth, kImageChannels},
-    DataType::Int8);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  kImageHeight, kImageWidth, kImageChannels},
+                                 DataType::Int8);
   // TODO(varunsh): what should we return here?
   this->metadata_.addOutputTensor("output", {0}, DataType::Uint32);
   this->metadata_.setName(this->graph_name_);
@@ -172,7 +173,7 @@ BatchPtr ResNet50::doRun(Batch* batch, const MemoryPool* pool) {
   auto shape = out_data_descriptor[0]->get_tensor()->get_shape();
 
   int response_size = 0;
-  std::vector<uint64_t> new_shape;
+  std::vector<int64_t> new_shape;
   if (shape.size() > 1) {  // [batch, a, b, c]
     response_size = util::containerProduct(shape.begin() + 1, shape.end());
     // We exclude the batch size from the returned shape
diff --git a/src/amdinfer/workers/resnet50_stream.cpp b/src/amdinfer/workers/resnet50_stream.cpp
index 200053ebf..5a8d5b094 100644
--- a/src/amdinfer/workers/resnet50_stream.cpp
+++ b/src/amdinfer/workers/resnet50_stream.cpp
@@ -41,10 +41,10 @@
 #include <xir/tensor/tensor.hpp>   // for Tensor
 #include <xir/util/data_type.hpp>  // for create_data_type
 
-#include "amdinfer/batching/batcher.hpp"  // for BatchPtr, BatchPtrQueue
-#include "amdinfer/build_options.hpp"     // for AMDINFER_ENABLE_LOGGING
-#include "amdinfer/core/data_types.hpp"   // for DataType, DataType::String
-#include "amdinfer/core/inference_request.hpp"   // for InferenceRequest
+#include "amdinfer/batching/batcher.hpp"        // for BatchPtr, BatchPtrQueue
+#include "amdinfer/build_options.hpp"           // for AMDINFER_ENABLE_LOGGING
+#include "amdinfer/core/data_types.hpp"         // for DataType, DataType::Bytes
+#include "amdinfer/core/inference_request.hpp"  // for InferenceRequest
 #include "amdinfer/core/inference_response.hpp"  // for InferenceResponse
 #include "amdinfer/core/parameters.hpp"          // for ParameterMap
 #include "amdinfer/declarations.hpp"         // for BufferPtrs, InferenceRe...
@@ -133,9 +133,10 @@ void ResNet50Stream::doAcquire(ParameterMap* parameters) {
 
   this->graph_ = this->sys_manager_->getGraph(this->graph_name_);
 
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, kImageHeight, kImageWidth, kImageChannels},
-    DataType::Int8);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  kImageHeight, kImageWidth, kImageChannels},
+                                 DataType::Int8);
   // TODO(varunsh): what should we return here?
   this->metadata_.addOutputTensor("output", {0}, DataType::Uint32);
   this->metadata_.setName(this->graph_name_);
@@ -182,7 +183,7 @@ BatchPtr ResNet50Stream::doRun(Batch* batch,
 
       InferenceResponseOutput output;
       output.setName("key");
-      output.setDatatype(DataType::String);
+      output.setDatatype(DataType::Bytes);
       std::string metadata = "[" + std::to_string(video_width) + "," +
                              std::to_string(video_height) + "]";
       auto message = constructMessage(key, std::to_string(fps), metadata);
@@ -190,7 +191,7 @@ BatchPtr ResNet50Stream::doRun(Batch* batch,
       buffer.resize(message.size());
       memcpy(buffer.data(), message.data(), message.size());
       output.setData(std::move(buffer));
-      output.setShape({message.size()});
+      output.setShape({static_cast<int64_t>(message.size())});
       resp.addOutput(output);
       req->runCallback(resp);
       // round to nearest multiple of batch size
@@ -255,13 +256,13 @@ BatchPtr ResNet50Stream::doRun(Batch* batch,
 
             InferenceResponseOutput output;
             output.setName("image");
-            output.setDatatype(DataType::String);
+            output.setDatatype(DataType::Bytes);
             auto message = constructMessage(key, frames.front(), labels);
             std::vector<std::byte> buffer;
             buffer.resize(message.size());
             memcpy(buffer.data(), message.data(), message.size());
             output.setData(std::move(buffer));
-            output.setShape({message.size()});
+            output.setShape({static_cast<int64_t>(message.size())});
             resp.addOutput(output);
             req->runCallback(resp);
             frames.pop();
@@ -294,13 +295,13 @@ BatchPtr ResNet50Stream::doRun(Batch* batch,
 
           InferenceResponseOutput output;
           output.setName("image");
-          output.setDatatype(DataType::String);
+          output.setDatatype(DataType::Bytes);
           auto message = constructMessage(key, frames.front(), labels);
           std::vector<std::byte> buffer;
           buffer.resize(message.size());
           memcpy(buffer.data(), message.data(), message.size());
           output.setData(std::move(buffer));
-          output.setShape({message.size()});
+          output.setShape({static_cast<int64_t>(message.size())});
           resp.addOutput(output);
           req->runCallback(resp);
           frames.pop();
diff --git a/src/amdinfer/workers/tf_zendnn.cpp b/src/amdinfer/workers/tf_zendnn.cpp
index 32978b8a2..7afbccb17 100644
--- a/src/amdinfer/workers/tf_zendnn.cpp
+++ b/src/amdinfer/workers/tf_zendnn.cpp
@@ -245,9 +245,10 @@ void TfZendnn::doAcquire(ParameterMap* parameters) {
   AMDINFER_LOG_INFO(logger, "TF Session Created, Ready for prediction");
 
   // Adding metadata for input and output
-  this->metadata_.addInputTensor(
-    "input", {this->batch_size_, image_height_, image_width_, image_channels_},
-    input_dt_);
+  this->metadata_.addInputTensor("input",
+                                 {static_cast<int64_t>(batch_size_),
+                                  image_height_, image_width_, image_channels_},
+                                 input_dt_);
   this->metadata_.addOutputTensor("output", {output_classes_}, DataType::Fp32);
   this->metadata_.setName("TfZendnn");
 }
@@ -314,7 +315,7 @@ BatchPtr TfZendnn::doRun(Batch* batch, const MemoryPool* pool) {
 
   // Copy the output from the model to the response object
   size_t response_size = output_classes_;
-  std::vector<size_t> new_shape = {response_size};
+  std::vector<int64_t> new_shape = {static_cast<int64_t>(response_size)};
 
   auto new_batch = batch->propagate();
   std::vector<BufferPtr> input_buffers;
diff --git a/src/amdinfer/workers/xmodel.cpp b/src/amdinfer/workers/xmodel.cpp
index cd948f0eb..89ca68dc2 100644
--- a/src/amdinfer/workers/xmodel.cpp
+++ b/src/amdinfer/workers/xmodel.cpp
@@ -192,7 +192,7 @@ BatchPtr XModel::doRun(Batch* batch, const MemoryPool* pool) {
   auto output_tensors = this->getRunner()->get_output_tensors();
   for (const auto* tensor : output_tensors) {
     auto xir_shape = tensor->get_shape();
-    std::vector<size_t> shape{xir_shape.begin(), xir_shape.end()};
+    std::vector<int64_t> shape{xir_shape.begin(), xir_shape.end()};
     auto xir_type = tensor->get_data_type();
     auto type = mapXirToType(xir_type);
     InferenceRequestInput input(nullptr, shape, type, tensor->get_name());
@@ -242,7 +242,7 @@ BatchPtr XModel::doRun(Batch* batch, const MemoryPool* pool) {
     const auto num_outputs = outputs_ptr.size();
     for (unsigned int i = 0; i < num_outputs; i++) {
       auto output_shape = output_tensors[i]->get_shape();
-      std::vector<uint64_t> new_shape;
+      std::vector<int64_t> new_shape;
       new_shape.reserve(output_shape.size() - 1);
       for (auto j = 1U; j < output_shape.size(); j++) {
         new_shape.push_back(output_shape[j]);


Test patch:
diff --git a/tests/api/test_infer_async.cpp b/tests/api/test_infer_async.cpp
index 5b10957cf..1fdf55b5c 100644
--- a/tests/api/test_infer_async.cpp
+++ b/tests/api/test_infer_async.cpp
@@ -33,7 +33,7 @@ TEST_F(HttpFixture, Ordered) {
   EXPECT_EQ(endpoint, "cplusplus");
 
   std::vector<uint32_t> img_data;
-  const auto shape = {1UL};
+  const auto shape = {1L};
   const auto size = 1;
   img_data.reserve(size);
   img_data.push_back(1);
diff --git a/tests/api/test_model_infer.cpp b/tests/api/test_model_infer.cpp
index 1917022a9..d09557274 100644
--- a/tests/api/test_model_infer.cpp
+++ b/tests/api/test_model_infer.cpp
@@ -28,7 +28,7 @@ void test(const Client* client) {
   EXPECT_EQ(endpoint, "cplusplus");
 
   std::vector<uint32_t> img_data;
-  auto shape = {1UL};
+  auto shape = {1L};
   auto size = 1;
   img_data.reserve(size);
   img_data.push_back(1);
diff --git a/tests/api/test_model_infer_async.cpp b/tests/api/test_model_infer_async.cpp
index df4204290..b694a54a5 100644
--- a/tests/api/test_model_infer_async.cpp
+++ b/tests/api/test_model_infer_async.cpp
@@ -30,7 +30,7 @@ void test(const Client* client) {
   EXPECT_EQ(endpoint, "cplusplus");
 
   std::vector<uint32_t> img_data;
-  auto shape = {1UL};
+  auto shape = {1L};
   auto size = 1;
   img_data.reserve(size);
   img_data.push_back(1);
diff --git a/tests/api/test_model_load.cpp b/tests/api/test_model_load.cpp
index 617480869..374696202 100644
--- a/tests/api/test_model_load.cpp
+++ b/tests/api/test_model_load.cpp
@@ -24,44 +24,68 @@
 
 namespace amdinfer {
 
-void test(const amdinfer::Client* client) {
+void prerequisites(const Client* client) {
   if (!serverHasExtension(client, "tfzendnn")) {
     GTEST_SKIP() << "This test requires TF+ZenDNN support.";
   }
 
-  const std::string model = "mnist";
-
   EXPECT_TRUE(client->modelList().empty());
+}
 
-  // load one worker
-  client->modelLoad(model, {});
+std::string getModel() { return "mnist"; }
 
+void testWithoutVersion(const Client* client) {
+  prerequisites(client);
+
+  const auto model = getModel();
+  client->modelLoad(model, {});
   EXPECT_TRUE(client->modelReady(model));
+  client->modelUnload(model);
+  waitUntilModelNotReady(client, model);
+}
 
-  client->modelUnload(model);  // unload the model
+void testWithVersion(const Client* client, const std::string& version) {
+  prerequisites(client);
 
-  while (!client->modelList().empty()) {
-    std::this_thread::yield();
-  }
+  const auto model = getModel();
+  client->modelLoad(model, {}, version);
+  EXPECT_TRUE(client->modelReady(model, version));
+  client->modelUnload(model, version);
+  waitUntilModelNotReady(client, model, version);
 }
 
 #ifdef AMDINFER_ENABLE_GRPC
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(GrpcFixture, modelLoad) { test(client_.get()); }
+TEST_F(GrpcFixture, modelLoad) { testWithoutVersion(client_.get()); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(GrpcFixture, modelLoadVersioned) { testWithVersion(client_.get(), "1"); }
 #endif
 
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
 TEST_F(BaseFixture, modelLoad) {
   amdinfer::NativeClient client(&server_);
-  test(&client);
+  testWithoutVersion(&client);
+}
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(BaseFixture, modelLoadVersioned) {
+  amdinfer::NativeClient client(&server_);
+  testWithVersion(&client, "1");
 }
 
 #ifdef AMDINFER_ENABLE_HTTP
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(HttpFixture, modelLoad) { test(client_.get()); }
+TEST_F(HttpFixture, modelLoad) { testWithoutVersion(client_.get()); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(HttpFixture, modelLoadVersioned) { testWithVersion(client_.get(), "1"); }
 #endif
 
 }  // namespace amdinfer
diff --git a/tests/api/test_model_load.py b/tests/api/test_model_load.py
index da4592c98..c512ba7d7 100644
--- a/tests/api/test_model_load.py
+++ b/tests/api/test_model_load.py
@@ -32,10 +32,30 @@ def test_model_load(self):
         models = self.rest_client.modelList()
         assert len(models) == 0
 
-        self.rest_client.modelLoad(model)  # this loads the model
+        self.rest_client.modelLoad(model)
         assert self.rest_client.modelReady(model)
 
         self.rest_client.modelUnload(model)
 
-        while self.rest_client.modelList():
-            time.sleep(1)
+        amdinfer.waitUntilModelNotReady(self.rest_client, model)
+
+
+@pytest.mark.extensions(["tfzendnn"])
+@pytest.mark.usefixtures("server", "assign_client")
+class TestModelLoadVersioned:
+    def test_model_load(self):
+        """
+        Test loading multiple times
+        """
+
+        model = "mnist"
+        models = self.rest_client.modelList()
+        assert len(models) == 0
+
+        version = "1"
+        self.rest_client.modelLoad(model, amdinfer.ParameterMap(), version)
+        assert self.rest_client.modelReady(model, version)
+
+        self.rest_client.modelUnload(model, version)
+
+        amdinfer.waitUntilModelNotReady(self.rest_client, model, version)
diff --git a/tests/models/test_invert_image.cpp b/tests/models/test_invert_image.cpp
index 82fdf7833..6772ca0ad 100644
--- a/tests/models/test_invert_image.cpp
+++ b/tests/models/test_invert_image.cpp
@@ -29,7 +29,7 @@
 
 namespace amdinfer {
 
-void test(const amdinfer::Client* client) {
+void test(const amdinfer::Client* client, const std::string& version) {
   const auto path = getPathToAsset("asset_dog-3619020_640.jpg");
 
   const std::string model = "invert_image";
@@ -38,9 +38,9 @@ void test(const amdinfer::Client* client) {
 
   EXPECT_TRUE(client->modelList().empty());
 
-  client->modelLoad(model, {});
+  client->modelLoad(model, {}, version);
   for (const auto& endpoint : endpoints) {
-    EXPECT_TRUE(client->modelReady(endpoint));
+    EXPECT_TRUE(client->modelReady(endpoint, version));
   }
 
   auto img = cv::imread(path);
@@ -54,26 +54,26 @@ void test(const amdinfer::Client* client) {
 
   InferenceRequestInput input_0;
   input_0.setName("input0");
-  input_0.setDatatype(DataType::String);
-  input_0.setShape({encoded_data.size()});
+  input_0.setDatatype(DataType::Bytes);
+  input_0.setShape({static_cast<int64_t>(encoded_data.size())});
   input_0.setData(encoded_data.data());
 
   InferenceRequest request;
   request.addInputTensor(input_0);
 
-  auto response = client->modelInfer(endpoints[0], request);
+  auto response = client->modelInfer(endpoints[0], request, version);
 
   const auto& outputs = response.getOutputs();
   EXPECT_EQ(outputs.size(), 1);
   const auto& output = outputs[0];
   const auto& output_shape = output.getShape();
   EXPECT_TRUE(output_shape.size() == 1);
-  EXPECT_EQ(output.getDatatype(), DataType::String);
+  EXPECT_EQ(output.getDatatype(), DataType::Bytes);
   const auto* data = static_cast<char*>(output.getData());
 
   auto decoded_data = util::base64Decode(data, output_shape.at(0));
-  std::vector<char> foo(decoded_data.begin(), decoded_data.end());
-  auto received_image = cv::imdecode(foo, cv::IMREAD_UNCHANGED);
+  std::vector<char> buffer(decoded_data.begin(), decoded_data.end());
+  auto received_image = cv::imdecode(buffer, cv::IMREAD_UNCHANGED);
   ASSERT_FALSE(received_image.empty());
 
   cv::Mat diff;
@@ -82,30 +82,45 @@ void test(const amdinfer::Client* client) {
   auto manhattan_norm_per_pixel = manhattan_norm / golden_shape;
   EXPECT_TRUE(manhattan_norm_per_pixel < 2);
 
-  unloadModels(client, endpoints);
+  unloadModels(client, endpoints, version);
 
   while (!client->modelList().empty()) {
-    std::this_thread::yield();
+    std::this_thread::sleep_for(std::chrono::seconds(1));
   }
 }
 
 #ifdef AMDINFER_ENABLE_GRPC
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(GrpcFixture, InvertImage) { test(client_.get()); }
+TEST_F(GrpcFixture, InvertImage) { test(client_.get(), ""); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(GrpcFixture, InvertImageVersioned) { test(client_.get(), "1"); }
 #endif
 
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
 TEST_F(BaseFixture, InvertImage) {
   NativeClient client(&server_);
-  test(&client);
+  test(&client, "");
+}
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(BaseFixture, InvertImageVersioned) {
+  NativeClient client(&server_);
+  test(&client, "1");
 }
 
 #ifdef AMDINFER_ENABLE_HTTP
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(HttpFixture, InvertImage) { test(client_.get()); }
+TEST_F(HttpFixture, InvertImage) { test(client_.get(), ""); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(HttpFixture, InvertImageVersioned) { test(client_.get(), "1"); }
 #endif
 
 }  // namespace amdinfer
diff --git a/tests/models/test_mnist.cpp b/tests/models/test_mnist.cpp
index 553bb06ae..15965eca8 100644
--- a/tests/models/test_mnist.cpp
+++ b/tests/models/test_mnist.cpp
@@ -32,7 +32,7 @@ const int kImageSize = 28;
 // mnist classifies 10 digits
 const int kOutputClasses = 10;
 
-void test(amdinfer::Client* client) {
+void test(const Client* client, const std::string& version) {
   if (!serverHasExtension(client, "tfzendnn")) {
     GTEST_SKIP() << "This test requires TF+ZenDNN support.";
   }
@@ -44,8 +44,8 @@ void test(amdinfer::Client* client) {
 
   EXPECT_TRUE(client->modelList().empty());
 
-  client->modelLoad(model, {});
-  EXPECT_TRUE(client->modelReady(model));
+  client->modelLoad(model, {}, version);
+  EXPECT_TRUE(client->modelReady(model, version));
 
   auto img = cv::imread(path);
   cv::cvtColor(img, img, cv::COLOR_BGR2GRAY);
@@ -62,7 +62,7 @@ void test(amdinfer::Client* client) {
   InferenceRequest request;
   request.addInputTensor(input_0);
 
-  auto response = client->modelInfer(model, request);
+  auto response = client->modelInfer(model, request, version);
 
   const auto& outputs = response.getOutputs();
   EXPECT_EQ(outputs.size(), 1);
@@ -81,7 +81,7 @@ void test(amdinfer::Client* client) {
   }
   EXPECT_EQ(index, 9);
 
-  client->modelUnload(model);  // unload the model
+  client->modelUnload(model, version);
 
   while (!client->modelList().empty()) {
     std::this_thread::yield();
@@ -91,20 +91,35 @@ void test(amdinfer::Client* client) {
 #ifdef AMDINFER_ENABLE_GRPC
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(GrpcFixture, mnist) { test(client_.get()); }
+TEST_F(GrpcFixture, mnist) { test(client_.get(), ""); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(GrpcFixture, mnistVersioned) { test(client_.get(), "1"); }
 #endif
 
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
 TEST_F(BaseFixture, mnist) {
   NativeClient client(&server_);
-  test(&client);
+  test(&client, "");
+}
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(BaseFixture, mnistVersioned) {
+  NativeClient client(&server_);
+  test(&client, "1");
 }
 
 #ifdef AMDINFER_ENABLE_HTTP
 // @pytest.mark.extensions(["tfzendnn"])
 // NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
-TEST_F(HttpFixture, mnist) { test(client_.get()); }
+TEST_F(HttpFixture, mnist) { test(client_.get(), ""); }
+
+// @pytest.mark.extensions(["tfzendnn"])
+// NOLINTNEXTLINE(cert-err58-cpp, cppcoreguidelines-owning-memory)
+TEST_F(HttpFixture, mnistVersioned) { test(client_.get(), "1"); }
 #endif
 
 }  // namespace amdinfer
diff --git a/tests/performance/batching/benchmark_soft.cpp b/tests/performance/batching/benchmark_soft.cpp
index 09ab201cf..6fe4df8c6 100644
--- a/tests/performance/batching/benchmark_soft.cpp
+++ b/tests/performance/batching/benchmark_soft.cpp
@@ -68,7 +68,7 @@ class PerfSoftBatcherFixture : public ::benchmark::Fixture {
   void SetUp(const ::benchmark::State& state) override {
     auto batch_size = static_cast<int>(state.range(0));
 
-    const auto data_shape = {1UL, 2UL, 50UL};
+    const auto data_shape = {1L, 2L, 50L};
     const auto data_size = 100;
 
     ParameterMap parameters;
diff --git a/tests/unit/batching/test_soft_batching.cpp b/tests/unit/batching/test_soft_batching.cpp
index c8665377d..13daee076 100644
--- a/tests/unit/batching/test_soft_batching.cpp
+++ b/tests/unit/batching/test_soft_batching.cpp
@@ -81,7 +81,7 @@ class UnitSoftBatcherFixture : public testing::TestWithParam<BatchConfig> {
     const auto& batch_config = GetParam();
     int batch_size = batch_config.batch_size;
 
-    const auto data_shape = {1UL, 2UL, 50UL};
+    const auto data_shape = {1L, 2L, 50L};
     // the product of the data shape < 255 to fit into uint8
     const uint8_t data_size = 100;
 
@@ -179,7 +179,7 @@ class UnitSoftBatcherFixture : public testing::TestWithParam<BatchConfig> {
  private:
   int data_size_ = 0;
   BufferPtr buffer_;
-  std::initializer_list<uint64_t> data_shape_;
+  std::initializer_list<int64_t> data_shape_;
   InferenceRequestPtr request_;
   std::optional<WorkerInfo> worker_;
   std::optional<SoftBatcher> batcher_;
diff --git a/tests/unit/buffers/test_cpu.cpp b/tests/unit/buffers/test_cpu.cpp
index 042b5f775..3e24f23ba 100644
--- a/tests/unit/buffers/test_cpu.cpp
+++ b/tests/unit/buffers/test_cpu.cpp
@@ -76,7 +76,7 @@ TEST_P(UnitVectorBufferFixture, TestFixtureState) {  // NOLINT
   }
 }
 
-// Excluding STRING as it doesn't have a defined size to pre-allocate
+// Excluding BYTES as it doesn't have a defined size to pre-allocate
 // NOLINTNEXTLINE(cert-err58-cpp)
 const std::array<DataType, 12> kDataTypes{
   amdinfer::DataType::Bool,   amdinfer::DataType::Uint8,
diff --git a/tests/unit/clients/test_grpc_internal.cpp b/tests/unit/clients/test_grpc_internal.cpp
index 4b6aeaad4..4ad76ca1d 100644
--- a/tests/unit/clients/test_grpc_internal.cpp
+++ b/tests/unit/clients/test_grpc_internal.cpp
@@ -144,7 +144,7 @@ TEST_P(Fixture, TestRequestToProto) {  // NOLINT
   switchOverTypes(CheckData(), datatype, &tensor);
 }
 
-// we exclude STRING as it doesn't have a defined size we can pre-allocate
+// we exclude BYTES as it doesn't have a defined size we can pre-allocate
 // NOLINTNEXTLINE(cert-err58-cpp)
 const std::array<DataType, 12> kDataTypes{
   DataType::Bool,   DataType::Uint8, DataType::Uint16, DataType::Uint32,
diff --git a/tests/unit/core/test_model_config.cpp b/tests/unit/core/test_model_config.cpp
index b937ad0ef..fc2f4a251 100644
--- a/tests/unit/core/test_model_config.cpp
+++ b/tests/unit/core/test_model_config.cpp
@@ -16,8 +16,9 @@
 
 #include <iostream>
 
-#include "amdinfer/core/model_config.hpp"  // for ModelConfig
-#include "gtest/gtest.h"                   // for Message, TestPartResult, Test
+#include "amdinfer/core/model_config.hpp"        // for ModelConfig
+#include "amdinfer/core/versioned_endpoint.hpp"  // for getVersionedEndpoint
+#include "gtest/gtest.h"  // for Message, TestPartResult, Test
 
 using namespace std::string_view_literals;
 
@@ -41,7 +42,7 @@ TEST(UnitModelConfig, Basic) {
   )"sv;
 
   const auto toml = toml::parse(toml_str);
-  ModelConfig config{toml};
+  ModelConfig config{toml, ""};
 
   ASSERT_EQ(config.size(), 1);
   for (const auto& [model, parameters] : config) {
@@ -72,7 +73,7 @@ TEST(UnitModelConfig, SingleEnsemble) {
   )"sv;
 
   const auto toml = toml::parse(toml_str);
-  ModelConfig config{toml};
+  ModelConfig config{toml, ""};
 
   ASSERT_EQ(config.size(), 1);
   for (const auto& [model, parameters] : config) {
@@ -91,7 +92,7 @@ TEST(UnitModelConfig, Chain) {
 
     [[models.inputs]]
     name = "image_in"
-    datatype = "STRING"
+    datatype = "BYTES"
     shape = [1048576]
     id = ""
 
@@ -131,13 +132,14 @@ TEST(UnitModelConfig, Chain) {
 
     [[models.outputs]]
     name = "image_out"
-    datatype = "STRING"
+    datatype = "BYTES"
     shape = [1048576]
     id = "postprocessed_image"
   )"sv;
 
   const auto toml = toml::parse(toml_str);
-  ModelConfig config{toml};
+  const std::string version{"1"};
+  ModelConfig config{toml, version};
 
   ASSERT_EQ(config.size(), 3);
   std::array<std::string, 3> models{"invert_image", "execute",
@@ -145,7 +147,7 @@ TEST(UnitModelConfig, Chain) {
 
   auto index = 0;
   for (const auto& [model, parameters] : config) {
-    ASSERT_EQ(model, models.at(index));
+    ASSERT_EQ(model, getVersionedEndpoint(models.at(index), version));
     ASSERT_EQ(parameters.get<std::string>("worker"), "cplusplus");
     index++;
   }
diff --git a/tests/workers/facedetect.hpp b/tests/workers/facedetect.hpp
index 00d3d7241..0d6125d95 100644
--- a/tests/workers/facedetect.hpp
+++ b/tests/workers/facedetect.hpp
@@ -48,9 +48,9 @@ inline void enqueue(const amdinfer::NativeClient& client,
                     FutureQueue& my_queue) {
   for (int i = 0; i < count; i++) {
     auto img = cv::imread(image_paths[start_index]);
-    auto shape = {static_cast<uint64_t>(img.size[0]),
-                  static_cast<uint64_t>(img.size[1]),
-                  static_cast<uint64_t>(img.channels())};
+    auto shape = {static_cast<int64_t>(img.size[0]),
+                  static_cast<int64_t>(img.size[1]),
+                  static_cast<int64_t>(img.channels())};
     auto size = img.size[0] * img.size[1] * 3;
     img_data.reserve(size);
     memcpy(img_data.data(), img.data, size);
diff --git a/tests/workers/migraphx/test_resnet50.cpp b/tests/workers/migraphx/test_resnet50.cpp
index 5a8edbb46..15e943382 100644
--- a/tests/workers/migraphx/test_resnet50.cpp
+++ b/tests/workers/migraphx/test_resnet50.cpp
@@ -94,7 +94,7 @@ std::vector<InferenceRequest> constructRequests(const Images<T>& images) {
   std::vector<InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {224, 224, 3};
+  const std::initializer_list<int64_t> shape = {224, 224, 3};
 
   DataType type;
   if constexpr (std::is_same_v<T, fp16>) {
diff --git a/tests/workers/test_facedetect_stream.py b/tests/workers/test_facedetect_stream.py
index f7916b2a8..1f3ed51be 100644
--- a/tests/workers/test_facedetect_stream.py
+++ b/tests/workers/test_facedetect_stream.py
@@ -47,7 +47,7 @@ def construct_request(self, requested_frames_count):
 
         input_0 = amdinfer.InferenceRequestInput()
         input_0.name = "input0"
-        input_0.datatype = amdinfer.DataType.STRING
+        input_0.datatype = amdinfer.DataType.BYTES
         input_0.setStringData(amdinfer.stringToArray(video_path))
         input_0.shape = [len(video_path)]
         parameters = amdinfer.ParameterMap()
diff --git a/tests/workers/test_invert_image.py b/tests/workers/test_invert_image.py
index d38ed84f7..922859541 100644
--- a/tests/workers/test_invert_image.py
+++ b/tests/workers/test_invert_image.py
@@ -70,7 +70,7 @@ def validate(response, image):
     assert len(outputs) == 1
 
     output = outputs[0]
-    if output.datatype == amdinfer.DataType.STRING:
+    if output.datatype == amdinfer.DataType.BYTES:
         data = output.getStringData()
         compare_jpgs(data, image)
         assert output.parameters.empty()
diff --git a/tests/workers/test_invert_video.py b/tests/workers/test_invert_video.py
index 1d46b155d..1e640c507 100644
--- a/tests/workers/test_invert_video.py
+++ b/tests/workers/test_invert_video.py
@@ -40,7 +40,7 @@ def get_config():
     def construct_request(self, video_path, requested_frames_count):
         input_0 = amdinfer.InferenceRequestInput()
         input_0.name = "input0"
-        input_0.datatype = amdinfer.DataType.STRING
+        input_0.datatype = amdinfer.DataType.BYTES
         input_0.setStringData(amdinfer.stringToArray(video_path))
         input_0.shape = [len(video_path)]
         parameters = amdinfer.ParameterMap()
diff --git a/tests/workers/test_resnet50_stream.py b/tests/workers/test_resnet50_stream.py
index 8d1f50abb..a6e079d19 100644
--- a/tests/workers/test_resnet50_stream.py
+++ b/tests/workers/test_resnet50_stream.py
@@ -42,7 +42,7 @@ def construct_request(self, requested_frames_count):
 
         input_0 = amdinfer.InferenceRequestInput()
         input_0.name = "input0"
-        input_0.datatype = amdinfer.DataType.STRING
+        input_0.datatype = amdinfer.DataType.BYTES
         input_0.setStringData(amdinfer.stringToArray(video_path))
         input_0.shape = [len(video_path)]
         parameters = amdinfer.ParameterMap()
diff --git a/tests/workers/xmodel.hpp b/tests/workers/xmodel.hpp
index 6ac9acc62..a9e78e9ee 100644
--- a/tests/workers/xmodel.hpp
+++ b/tests/workers/xmodel.hpp
@@ -188,7 +188,7 @@ inline int run(const std::string& xmodel, int images, int threads,
     std::ignore = client.workerLoad("Xmodel", &parameters);
   }
 
-  std::vector<uint64_t> shape;
+  std::vector<int64_t> shape;
   amdinfer::DataType type = amdinfer::DataType::Int8;
   {
     std::unique_ptr<xir::Graph> graph = xir::Graph::deserialize(xmodel);
diff --git a/tests/workers/xmodel/test_resnet50.cpp b/tests/workers/xmodel/test_resnet50.cpp
index a827734bb..656b1c071 100644
--- a/tests/workers/xmodel/test_resnet50.cpp
+++ b/tests/workers/xmodel/test_resnet50.cpp
@@ -57,7 +57,7 @@ std::vector<InferenceRequest> constructRequests(const Images& images) {
   std::vector<InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {224, 224, 3};
+  const std::initializer_list<int64_t> shape = {224, 224, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();
diff --git a/tests/workers/xmodel/test_yolov3.cpp b/tests/workers/xmodel/test_yolov3.cpp
index 4fedaa13f..552847b49 100644
--- a/tests/workers/xmodel/test_yolov3.cpp
+++ b/tests/workers/xmodel/test_yolov3.cpp
@@ -69,7 +69,7 @@ std::vector<InferenceRequest> constructRequests(const Images& images) {
   std::vector<InferenceRequest> requests;
   requests.reserve(images.size());
 
-  const std::initializer_list<uint64_t> shape = {416, 416, 3};
+  const std::initializer_list<int64_t> shape = {416, 416, 3};
 
   for (const auto& image : images) {
     requests.emplace_back();