Repository: kaito-project/kaito

Issue:
Add support for phi 4 preset
**Describe the solution you'd like**

I would like to use a preset that deploys phi 4 model that was announced last week: https://techcommunity.microsoft.com/blog/aiplatformblog/introducing-phi-4-microsoft%E2%80%99s-newest-small-language-model-specializing-in-comple/4357090

Something like this:

```yaml
apiVersion: kaito.sh/v1alpha1
kind: Workspace
metadata:
  name: workspace-phi-4
resource:
  instanceType: "Standard_NC6s_v3"
  labelSelector:
    matchLabels:
      apps: phi-4
inference:
  preset:
    name: phi-4
```

Discussion:
@zhuangqh: https://huggingface.co/microsoft/phi-4. has been released. can you write a proposal following this guide ? https://github.com/kaito-project/kaito/blob/main/docs/proposals/20240527-phi3-instruct.md

Solution patch:
diff --git a/.github/actions/e2e-base-setup/action.yml b/.github/actions/e2e-base-setup/action.yml
new file mode 100644
index 0000000..9613958
--- /dev/null
+++ b/.github/actions/e2e-base-setup/action.yml
@@ -0,0 +1,203 @@
+name: "e2e-base-setup"
+description: "Composite action for e2e base setup"
+
+inputs:
+  git_sha:
+    description: "Git commit SHA"
+    required: true
+  node_provisioner:
+    description: "Provisioner type"
+    required: false
+    default: "gpuprovisioner"
+  tag:
+    description: "Tag"
+    required: false
+  isRelease:
+    description: "Is Release"
+    required: false
+    default: "false"
+  registry:
+    description: "Registry"
+    required: false
+  region:
+    description: "the azure location to run the e2e test in"
+    required: false
+    default: "eastus"
+  k8s_version:
+    description: "Kubernetes version"
+    required: false
+    default: "1.30.0"
+
+outputs:
+  cluster_name:
+    description: "Cluster name"
+  registry:
+    description: "Registry value"
+  run_llama_13b:
+    description: "Whether to run llama 13b"
+
+env:
+  GO_VERSION: "1.24"
+
+runs:
+  using: "composite"
+  steps:
+    - name: Checkout
+      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+      with:
+        ref: ${{ inputs.git_sha }}
+
+    - name: Set e2e Resource and Cluster Name
+      shell: bash
+      run: |
+        ID="ri${{ github.run_id }}rn${{ github.run_number }}ra${{ github.run_attempt }}"
+
+        echo "VERSION=${ID}" >> $GITHUB_ENV
+        echo "CLUSTER_NAME=kaito${ID}" >> $GITHUB_ENV
+        echo "REGISTRY=kaito${ID}.azurecr.io" >> $GITHUB_ENV
+        echo "RUN_LLAMA_13B=false" >> $GITHUB_ENV
+
+    - name: Set Registry
+      if: ${{ inputs.isRelease == 'true' }}
+      shell: bash
+      run: |
+        echo "REGISTRY=${{ inputs.registry }}" >> $GITHUB_ENV
+        echo "VERSION=$(echo ${{ inputs.tag }} | tr -d v)" >> $GITHUB_ENV
+
+    - name: Remove existing Go modules directory
+      shell: bash
+      run: sudo rm -rf ~/go/pkg/mod
+
+    - name: Set up Go 1.24
+      uses: actions/setup-go@v5.2.0
+      with:
+        go-version: "1.24"
+
+    - name: Install Azure CLI latest
+      shell: bash
+      run: |
+        if ! which az > /dev/null; then
+          echo "Azure CLI not found. Installing..."
+          curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
+        else
+          echo "Azure CLI already installed."
+        fi
+
+    - name: Azure CLI Login
+      shell: bash
+      run: |
+        az login --identity
+
+    - name: Install Helm
+      uses: azure/setup-helm@v4
+
+    - name: Create Resource Group
+      shell: bash
+      run: |
+        make create-rg
+      env:
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+
+    - name: Create ACR
+      shell: bash
+      run: |
+        make create-acr
+      env:
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        AZURE_ACR_NAME: ${{ env.CLUSTER_NAME }}
+
+    - name: Create Azure Identity
+      uses: azure/CLI@v2.1.0
+      with:
+        inlineScript: |
+          az identity create --name "${{ inputs.node_provisioner }}Identity" --resource-group "${{ env.CLUSTER_NAME }}"
+
+    - name: Generate APIs
+      shell: bash
+      run: |
+        make generate
+
+    - name: build KAITO image
+      if: ${{ inputs.isRelease == 'false' }}
+      shell: bash
+      run: |
+        make docker-build-workspace
+
+    - name: create cluster
+      shell: bash
+      run: |
+        if [ "${{ inputs.node_provisioner }}" == "gpuprovisioner" ]; then
+          make create-aks-cluster
+        else
+          make create-aks-cluster-for-karpenter
+        fi
+      env:
+        AZURE_ACR_NAME: ${{ env.CLUSTER_NAME }}
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+        AZURE_LOCATION: ${{ inputs.region }}
+        AKS_K8S_VERSION: ${{ inputs.k8s_version }}
+
+    - name: Create Identities and Permissions for ${{ inputs.node_provisioner }}
+      shell: bash
+      run: |
+        AZURE_SUBSCRIPTION_ID=$E2E_SUBSCRIPTION_ID \
+        make generate-identities
+      env:
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+        TEST_SUITE: ${{ inputs.node_provisioner }}
+
+    - name: Install gpu-provisioner helm chart
+      if: ${{ inputs.node_provisioner == 'gpuprovisioner' }}
+      shell: bash
+      run: |
+        AZURE_TENANT_ID=$E2E_TENANT_ID \
+        AZURE_SUBSCRIPTION_ID=$E2E_SUBSCRIPTION_ID \
+        make gpu-provisioner-helm
+      env:
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+
+    - name: Install KAITO Workspace helm chart
+      shell: bash
+      run: |
+        make az-patch-install-helm
+        kubectl wait --for=condition=available deploy "kaito-workspace" -n kaito-workspace --timeout=300s
+      env:
+        AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+        REGISTRY: ${{ env.REGISTRY }}
+        VERSION: ${{ env.VERSION }}
+        TEST_SUITE: ${{ inputs.node_provisioner }}
+
+    - name: Set up E2E ACR Credentials and Secret
+      shell: bash
+      run: |
+        ACR_USERNAME=$(az acr credential show --name "${{ env.CLUSTER_NAME }}" --resource-group "${{ env.CLUSTER_NAME }}" --query "username" -o tsv)
+        ACR_PASSWORD=$(az acr credential show --name "${{ env.CLUSTER_NAME }}" --resource-group "${{ env.CLUSTER_NAME }}" --query "passwords[0].value" -o tsv)
+
+        if [ -z "$ACR_USERNAME" ] || [ -z "$ACR_PASSWORD" ]; then
+          echo "Failed to retrieve ACR credentials"
+          exit 1
+        fi
+
+        kubectl create secret docker-registry "${{ env.CLUSTER_NAME }}-acr-secret" \
+          --docker-server="${{ env.CLUSTER_NAME }}.azurecr.io" \
+          --docker-username="${ACR_USERNAME}" \
+          --docker-password="${ACR_PASSWORD}"
+
+    # Add Private-Hosted ACR secret for private models like llama
+    - name: Add Private-Hosted ACR Secret Credentials
+      shell: bash
+      run: |
+        E2E_AMRT_SECRET_NAME=$(echo "$E2E_AMRT_SECRET_NAME" | sed 's/[\"'\'']//g')
+        if kubectl get secret "$E2E_AMRT_SECRET_NAME" >/dev/null 2>&1; then
+          echo "Secret $E2E_AMRT_SECRET_NAME already exists. Skipping creation."
+        else
+          kubectl create secret docker-registry "$E2E_AMRT_SECRET_NAME" \
+            --docker-server="$E2E_ACR_AMRT_USERNAME.azurecr.io" \
+            --docker-username="$E2E_ACR_AMRT_USERNAME" \
+            --docker-password="$E2E_ACR_AMRT_PASSWORD"
+          echo "Secret $E2E_AMRT_SECRET_NAME created successfully."
+        fi
\ No newline at end of file
diff --git a/.github/e2e-preset-configs.json b/.github/e2e-preset-configs.json
index 9d20b0e..9efbd2d 100644
--- a/.github/e2e-preset-configs.json
+++ b/.github/e2e-preset-configs.json
@@ -7,16 +7,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
-      },
-      {
-        "name": "falcon-7b-adapter",
-        "node-count": 1,
-        "node-vm-size": "Standard_NC12s_v3",
-        "node-osdisk-size": 100,
-        "OSS": true,
-        "loads_adapter": true,
-        "expected_adapter": "amod-mental-health"
+        "loads_adapter": false,
+        "node_pool": "falcon7b",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16 --chat-template /workspace/chat_templates/falcon-instruct.jinja",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "falcon-7b-instruct",
@@ -24,7 +26,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "falcon7binst",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16 --chat-template /workspace/chat_templates/falcon-instruct.jinja",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "falcon-40b",
@@ -32,7 +45,18 @@
         "node-vm-size": "Standard_NC48ads_A100_v4",
         "node-osdisk-size": 400,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "falcon40b",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 2
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --dtype bfloat16 --chat-template /workspace/chat_templates/falcon-instruct.jinja --tensor-parallel-size 2",
+            "gpu_count": 2
+          }
+        }
       },
       {
         "name": "falcon-40b-instruct",
@@ -40,7 +64,18 @@
         "node-vm-size": "Standard_NC48ads_A100_v4",
         "node-osdisk-size": 400,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "falcon40bins",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 2
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --dtype bfloat16 --chat-template /workspace/chat_templates/falcon-instruct.jinja --tensor-parallel-size 2",
+            "gpu_count": 2
+          }
+        }
       },
       {
         "name": "mistral-7b",
@@ -48,7 +83,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "mistral7b",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16 --chat-template /workspace/chat_templates/mistral-instruct.jinja",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "mistral-7b-instruct",
@@ -56,7 +102,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "mistral7bins",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "phi-2",
@@ -64,7 +121,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 50,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "phi2",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "phi-3-mini-4k-instruct",
@@ -72,7 +140,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 50,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "phi3mini4kin",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "phi-3-mini-128k-instruct",
@@ -80,7 +159,18 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 50,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "phi3mini128k",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml --dtype float16",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "phi-3-medium-4k-instruct",
@@ -88,7 +178,18 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "phi3medium4k",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-max-probe-steps 6 --dtype float16 --tensor-parallel-size 2",
+            "gpu_count": 2
+          }
+        }
       },
       {
         "name": "phi-3-medium-128k-instruct",
@@ -96,7 +197,56 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "phi3medium12",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml --dtype float16 --max-model-len 1024 --tensor-parallel-size 2",
+            "gpu_count": 2
+          }
+        }
+      },
+      {
+        "name": "phi-4",
+        "node-count": 1,
+        "node-vm-size": "Standard_NC24ads_A100_v4",
+        "node-osdisk-size": 100,
+        "OSS": true,
+        "loads_adapter": false,
+        "node_pool": "phi4",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml",
+            "gpu_count": 1
+          }
+        }
+      },
+      {
+        "name": "phi-4-mini-instruct",
+        "node-count": 1,
+        "node-vm-size": "Standard_NC6s_v3",
+        "node-osdisk-size": 100,
+        "OSS": true,
+        "loads_adapter": false,
+        "node_pool": "phi4mini",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml --dtype float16",
+            "gpu_count": 1
+          }
+        }
       },
       {
         "name": "qwen2.5-coder-7b-instruct",
@@ -105,7 +255,76 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "qwen25coder7",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml --tensor-parallel-size 2",
+            "gpu_count": 2
+          }
+        }
+      },
+      {
+        "name": "qwen2.5-coder-32b-instruct",
+        "workload": "qwen2-5-coder-32b-instruct",
+        "node-count": 1,
+        "node-vm-size": "Standard_NC24ads_A100_v4",
+        "node-osdisk-size": 200,
+        "OSS": true,
+        "loads_adapter": false,
+        "node_pool": "qwen25c32",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml",
+            "gpu_count": 1
+          }
+        }
+      },
+      {
+        "name": "deepseek-r1-distill-qwen-14b",
+        "node-count": 1,
+        "node-vm-size": "Standard_NC24ads_A100_v4",
+        "node-osdisk-size": 200,
+        "OSS": true,
+        "loads_adapter": false,
+        "node_pool": "dsqwen14b",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 1
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml",
+            "gpu_count": 1
+          }
+        }
+      },
+      {
+        "name": "deepseek-r1-distill-llama-8b",
+        "node-count": 1,
+        "node-vm-size": "Standard_NC12s_v3",
+        "node-osdisk-size": 100,
+        "OSS": true,
+        "loads_adapter": false,
+        "node_pool": "dsqwen8b",
+        "runtimes": {
+          "hf": {
+            "command": "accelerate launch --num_processes 1 --num_machines 2 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code",
+            "gpu_count": 2
+          },
+          "vllm": {
+            "command": "python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml --tensor-parallel-size 2 --max-model-len 32768",
+            "gpu_count": 2
+          }
+        }
       },
       {
         "name": "llama-2-7b",
@@ -113,7 +332,8 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 100,
         "OSS": false,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "llama27b"
       },
       {
         "name": "llama-2-7b-chat",
@@ -121,7 +341,8 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 100,
         "OSS": false,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "llama27bchat"
       },
       {
         "name": "llama-2-13b",
@@ -129,7 +350,8 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 150,
         "OSS": false,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "llama213b"
       },
       {
         "name": "llama-2-13b-chat",
@@ -137,7 +359,8 @@
         "node-vm-size": "Standard_NC12s_v3",
         "node-osdisk-size": 150,
         "OSS": false,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "llama213bcha"
       },
       {
         "name": "tuning",
@@ -145,7 +368,8 @@
         "node-vm-size": "Standard_NC6s_v3",
         "node-osdisk-size": 100,
         "OSS": true,
-        "loads_adapter": false
+        "loads_adapter": false,
+        "node_pool": "tuning"
       }
     ]
   }
diff --git a/.github/matrix-configs.json b/.github/matrix-configs.json
deleted file mode 100644
index 0b36667..0000000
--- a/.github/matrix-configs.json
+++ /dev/null
@@ -1,100 +0,0 @@
-[
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "falcon-7b",
-            "dockerfile": "docker/presets/inference/falcon/Dockerfile",
-            "build_args": "--build-arg FALCON_MODEL_NAME=tiiuae/falcon-7b"
-        },
-        "shouldBuildFalcon": "true"
-    },
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "falcon-7b-instruct",
-            "dockerfile": "docker/presets/inference/falcon/Dockerfile",
-            "build_args": "--build-arg FALCON_MODEL_NAME=tiiuae/falcon-7b-instruct"
-        },
-        "shouldBuildFalcon": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "falcon-40b",
-            "dockerfile": "docker/presets/inference/falcon/Dockerfile",
-            "build_args": "--build-arg FALCON_MODEL_NAME=tiiuae/falcon-40b"
-        },
-        "shouldBuildFalcon": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "falcon-40b-instruct",
-            "dockerfile": "docker/presets/inference/falcon/Dockerfile",
-            "build_args": "--build-arg FALCON_MODEL_NAME=tiiuae/falcon-40b-instruct"
-        },
-        "shouldBuildFalcon": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-7b",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-7b --build-arg SRC_DIR=/home/presets/llama-2"
-        },
-        "shouldBuildLlama2": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-13b",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-13b --build-arg SRC_DIR=/home/presets/llama-2"
-        },
-        "shouldBuildLlama2": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-70b",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-70b --build-arg SRC_DIR=/home/presets/llama-2"
-        },
-        "shouldBuildLlama2": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-7b-chat",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-7b-chat --build-arg SRC_DIR=/home/presets/llama-2-chat"
-        },
-        "shouldBuildLlama2Chat": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-13b-chat",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-13b-chat --build-arg SRC_DIR=/home/presets/llama-2-chat"
-        },
-        "shouldBuildLlama2Chat": "true"
-    },
-
-    {
-        "model": {
-            "runs_on": "self-hosted",
-            "name": "llama-2-70b-chat",
-            "dockerfile": "docker/presets/inference/llama-2/Dockerfile",
-            "build_args": "--build-arg LLAMA_WEIGHTS=/llama/llama-2-70b-chat --build-arg SRC_DIR=/home/presets/llama-2-chat"
-        },
-        "shouldBuildLlama2Chat": "true"
-    }
-]
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 27fb938..2896f3a 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -22,9 +22,11 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout repository
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -32,7 +34,7 @@ jobs:
           submodules: true
           fetch-depth: 0
        
-      - uses: actions/setup-go@v5.2.0
+      - uses: actions/setup-go@v5.4.0
         with:
           go-version-file: 'go.mod'
 
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index a77149c..bde08d2 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -9,19 +9,21 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
 
 jobs:
   create-release:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Set up Go ${{ env.GO_VERSION }}
-        uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
         with:
           go-version: ${{ env.GO_VERSION  }}
 
@@ -33,7 +35,7 @@ jobs:
           ref: ${{ github.event.client_payload.tag }}
 
       - name: Goreleaser
-        uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0
+        uses: goreleaser/goreleaser-action@90a3faa9d0182683851fbfa97ca1a2cb983bfca3 # v6.2.1
         with:
           version: latest
           args: release --clean --timeout 60m --verbose
diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml
index 21a469b..4864720 100644
--- a/.github/workflows/dependency-review.yml
+++ b/.github/workflows/dependency-review.yml
@@ -17,11 +17,13 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: 'Checkout Repository'
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - name: 'Dependency Review'
-        uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0
+        uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
diff --git a/.github/workflows/e2e-preset-test.yml b/.github/workflows/e2e-preset-test.yml
index 123bc14..728c50d 100644
--- a/.github/workflows/e2e-preset-test.yml
+++ b/.github/workflows/e2e-preset-test.yml
@@ -21,7 +21,7 @@ on:
                 description: "Test on VLLM runtime"
 
 env:
-    GO_VERSION: "1.23"
+    GO_VERSION: "1.24"
     BRANCH_NAME: ${{ github.head_ref || github.ref_name}} 
     FORCE_RUN_ALL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force-run-all == 'true' }}
     FORCE_RUN_ALL_PHI:  ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force-run-all-phi-models== 'true' }}
@@ -107,6 +107,7 @@ jobs:
     environment: preset-env
     strategy:
       fail-fast: false
+      max-parallel: 10
       matrix:
         # Ex matrix element:
         # {"name":"falcon-40b","type":"text-generation","version":"#",
@@ -124,7 +125,7 @@ jobs:
         run: echo "MODEL_IS_OSS=${{ matrix.model.OSS }}" >> $GITHUB_ENV
 
       - name: 'Az CLI login'
-        uses: azure/login@v2.2.0
+        uses: azure/login@v2.3.0
         with:
             client-id: ${{ secrets.AZURE_CLIENT_ID }}
             tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -164,44 +165,48 @@ jobs:
       - name: Set up kubectl context
         run: |
           az aks get-credentials --resource-group llm-test --name GitRunner
-    
-      - name: Get Nodepool Name
-        id: get_nodepool_name
+
+      - name: Get testing workload
+        id: workload
         run: |
-            NAME_SUFFIX=${{ matrix.model.name }}
-            NAME_SUFFIX_WITHOUT_DASHES=${NAME_SUFFIX//-/}  # Removing all '-' symbols
-            NAME_SUFFIX_WITHOUT_DASHES=${NAME_SUFFIX_WITHOUT_DASHES//./} # Removing all '.' symbols
+            NODEPOOL_NAME=${{ matrix.model.node_pool }}
+            echo "NODEPOOL_NAME=$NODEPOOL_NAME" >> $GITHUB_OUTPUT
 
-            if [ ${#NAME_SUFFIX_WITHOUT_DASHES} -gt 12 ]; then
-                TRUNCATED_NAME_SUFFIX=${NAME_SUFFIX_WITHOUT_DASHES:0:12}
-            else
-                TRUNCATED_NAME_SUFFIX=$NAME_SUFFIX_WITHOUT_DASHES
-            fi
-            echo "Nodepool Name: $TRUNCATED_NAME_SUFFIX"
-            echo "NODEPOOL_NAME=$TRUNCATED_NAME_SUFFIX" >> $GITHUB_OUTPUT
+            WORKLOAD_NAME=${{ matrix.model.workload || matrix.model.name }}
+            echo "WORKLOAD_NAME=$WORKLOAD_NAME" >> $GITHUB_OUTPUT
+            WORKLOAD_FILE=$WORKLOAD_NAME.yaml
+            echo "WORKLOAD_FILE=$WORKLOAD_FILE" >> $GITHUB_OUTPUT
+            RESOURCE_TYPE=$(echo "${{ matrix.model.name }}" | grep -q "llama-2" && echo "statefulset" || echo "deployment")
+            echo "RESOURCE_TYPE=$RESOURCE_TYPE" >> $GITHUB_OUTPUT
+
+            pip install pyyaml
+            python3 presets/workspace/test/scripts/generate_manifests.py ${{ matrix.model.name }} ${{ env.RUNTIME }} \
+                --tag ${{ matrix.model.tag }} \
+                --repo ${{ secrets.ACR_AMRT_USERNAME }}.azurecr.io > $WORKLOAD_FILE
+
+            cat $WORKLOAD_FILE
 
       - name: Create Nodepool
         run: |
             NODEPOOL_EXIST=$(az aks nodepool show \
-                            --name ${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
+                            --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
                             --cluster-name GitRunner \
                             --resource-group llm-test \
                             --query 'name' -o tsv || echo "")
             echo "NODEPOOL_EXIST: $NODEPOOL_EXIST"
             if [ -z "$NODEPOOL_EXIST" ]; then
                 az aks nodepool add \
-                    --name ${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
+                    --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
                     --cluster-name GitRunner \
                     --resource-group llm-test \
                     --node-count ${{ matrix.model.node-count }} \
                     --node-vm-size ${{ matrix.model.node-vm-size }} \
                     --node-osdisk-size ${{ matrix.model.node-osdisk-size }} \
-                    --labels pool=${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
-                    --node-taints sku=gpu:NoSchedule \
-                    --aks-custom-headers UseGPUDedicatedVHD=true
+                    --labels pool=${{ steps.workload.outputs.NODEPOOL_NAME }} \
+                    --node-taints sku=gpu:NoSchedule
             else
                 NODEPOOL_STATE=$(az aks nodepool show \
-                                --name ${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
+                                --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
                                 --cluster-name GitRunner \
                                 --resource-group llm-test \
                                 --query 'provisioningState' -o tsv)
@@ -214,44 +219,14 @@ jobs:
                 fi
             fi
 
-      - name: Get testing workload
-        id: workload
+      - name: Create workload
         run: |
-            WORKLOAD_NAME=${{ matrix.model.workload || matrix.model.name }}
-            echo "WORKLOAD_NAME=$WORKLOAD_NAME" >> $GITHUB_OUTPUT
-            echo "WORKLOAD_FILE_PREFIX=presets/workspace/test/manifests/$WORKLOAD_NAME/$WORKLOAD_NAME" >> $GITHUB_OUTPUT
-
-      - name: Create Service
-        run: |
-            kubectl apply -f ${{steps.workload.outputs.WORKLOAD_FILE_PREFIX}}-service.yaml
-      
-      - name: Retrieve External Service IP
-        id: get_ip
-        run: |
-            SERVICE_IP=$(kubectl get svc ${{steps.workload.outputs.WORKLOAD_NAME}} -o=jsonpath='{.spec.clusterIP}')
-            echo "Service IP is $SERVICE_IP"
-            echo "SERVICE_IP=$SERVICE_IP" >> $GITHUB_OUTPUT
-
-      - name: Get Resource Type
-        id: resource
-        run: |
-            RESOURCE_TYPE=$(echo "${{ matrix.model.name }}" | grep -q "llama" && echo "statefulset" || echo "deployment")
-            echo "RESOURCE_TYPE=$RESOURCE_TYPE" >> $GITHUB_OUTPUT
-      
-      - name: Replace IP and Deploy Resource to K8s
-        run: |
-            POSTFIX=$(echo "${{ matrix.model.name }}" | grep -q "llama" && echo "" || echo "_${{ env.RUNTIME }}")
-            WORKLOAD_FILE=${{steps.workload.outputs.WORKLOAD_FILE_PREFIX}}$POSTFIX.yaml
-
-            sed -i "s/MASTER_ADDR_HERE/${{ steps.get_ip.outputs.SERVICE_IP }}/g" $WORKLOAD_FILE
-            sed -i "s/TAG_HERE/${{ matrix.model.tag }}/g" $WORKLOAD_FILE
-            sed -i "s/REPO_HERE/${{ secrets.ACR_AMRT_USERNAME }}/g" $WORKLOAD_FILE
-            kubectl apply -f $WORKLOAD_FILE
+            kubectl apply -f ${{steps.workload.outputs.WORKLOAD_FILE}}
 
       - name: Wait for Resource to be ready
         run: |
-            kubectl rollout status ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} --timeout=1800s
-    
+            kubectl rollout status ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} --timeout=3000s
+
       - name: Check Adapter Loading from Logs
         if: matrix.model.loads_adapter == true
         run: |
@@ -260,19 +235,19 @@ jobs:
 
       - name: Install testing commands
         run: |
-            kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- apt-get update
-            kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- apt-get install -y curl
+            kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- apt-get update
+            kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- apt-get install -y curl
 
       - name: Test healthz endpoint
         run: |
-            kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
+            kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
             curl -s http://localhost:5000/health
 
       - name: Test inference endpoint
         run: |
             echo "Testing inference for ${{ matrix.model.name }}"
-            if [[ "${{ matrix.model.name }}" == *"llama"* && "${{ matrix.model.name }}" == *"-chat"* ]]; then
-                kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
+            if [[ "${{ matrix.model.name }}" == *"llama-2"* && "${{ matrix.model.name }}" == *"-chat"* ]]; then
+                kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
                 curl -s -X POST \
                 -H "Content-Type: application/json" \
                 -d '{
@@ -292,8 +267,8 @@ jobs:
                     }
                 }' \
                 http://localhost:5000/chat
-            elif [[ "${{ matrix.model.name }}" == *"llama"* ]]; then
-                kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
+            elif [[ "${{ matrix.model.name }}" == *"llama-2"* ]]; then
+                kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
                 curl -s -X POST \
                 -H "Content-Type: application/json" \
                 -d '{
@@ -309,7 +284,7 @@ jobs:
                 }' \
                 http://localhost:5000/generate
             elif [[ "${{ env.RUNTIME }}" == *"vllm"* ]]; then
-                kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
+                kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
                 curl -s -X POST \
                 -H "accept: application/json" \
                 -H "Content-Type: application/json" \
@@ -328,7 +303,7 @@ jobs:
                     }' \
                 http://localhost:5000/v1/chat/completions
             else
-                kubectl exec ${{steps.resource.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
+                kubectl exec ${{steps.workload.outputs.RESOURCE_TYPE}}/${{steps.workload.outputs.WORKLOAD_NAME}} -- \
                 curl -s -X POST \
                 -H "accept: application/json" \
                 -H "Content-Type: application/json" \
@@ -370,9 +345,9 @@ jobs:
         if: always()
         run: |
             # Only proceed if RESOURCE_TYPE is set (else resource wasn't created)
-            if [ -n "${{ steps.resource.outputs.RESOURCE_TYPE }}" ]; then
+            if [ -n "${{ steps.workload.outputs.RESOURCE_TYPE }}" ]; then
                 # Use RESOURCE_TYPE from the previous step
-                RESOURCE_TYPE=${{ steps.resource.outputs.RESOURCE_TYPE }}
+                RESOURCE_TYPE=${{ steps.workload.outputs.RESOURCE_TYPE }}
                 
                 # Check and Delete K8s Resource (Deployment or StatefulSet)
                 if kubectl get $RESOURCE_TYPE ${{steps.workload.outputs.WORKLOAD_NAME}} > /dev/null 2>&1; then
@@ -387,16 +362,16 @@ jobs:
             fi
         
             # Check and Delete AKS Nodepool if it exists            
-            if [ -n "${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }}" ]; then
+            if [ -n "${{ steps.workload.outputs.NODEPOOL_NAME }}" ]; then
                 NODEPOOL_EXIST=$(az aks nodepool show \
-                                --name ${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
+                                --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
                                 --cluster-name GitRunner \
                                 --resource-group llm-test \
                                 --query 'name' -o tsv || echo "")
 
                 if [ -n "$NODEPOOL_EXIST" ]; then
                     az aks nodepool delete \
-                    --name ${{ steps.get_nodepool_name.outputs.NODEPOOL_NAME }} \
+                    --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
                     --cluster-name GitRunner \
                     --resource-group llm-test
                 fi
diff --git a/.github/workflows/e2e-preset-tuning-test.yml b/.github/workflows/e2e-preset-tuning-test.yml
index 18b08d4..810d750 100644
--- a/.github/workflows/e2e-preset-tuning-test.yml
+++ b/.github/workflows/e2e-preset-tuning-test.yml
@@ -8,7 +8,7 @@ on:
     workflow_dispatch: {}
 
 env:
-    GO_VERSION: "1.23"
+    GO_VERSION: "1.24"
 
 permissions:
     id-token: write
@@ -27,7 +27,7 @@ jobs:
             fetch-depth: 0
 
       - name: 'Az CLI login'
-        uses: azure/login@v2.2.0
+        uses: azure/login@v2.3.0
         with:
             client-id: ${{ secrets.AZURE_CLIENT_ID }}
             tenant-id: ${{ secrets.AZURE_TENANT_ID }}
@@ -67,8 +67,7 @@ jobs:
                     --node-vm-size ${{ steps.get_test_meta.outputs.node-vm-size }} \
                     --node-osdisk-size ${{ steps.get_test_meta.outputs.node-osdisk-size }} \
                     --labels pool=${{ steps.get_test_meta.outputs.name }} \
-                    --node-taints sku=gpu:NoSchedule \
-                    --aks-custom-headers UseGPUDedicatedVHD=true
+                    --node-taints sku=gpu:NoSchedule
             else
                 NODEPOOL_STATE=$(az aks nodepool show \
                                 --name ${{ steps.get_test_meta.outputs.name }} \
diff --git a/.github/workflows/e2e-workflow.yml b/.github/workflows/e2e-workflow.yml
index ee9f641..0cd6a92 100644
--- a/.github/workflows/e2e-workflow.yml
+++ b/.github/workflows/e2e-workflow.yml
@@ -29,102 +29,46 @@ jobs:
   e2e-tests:
     runs-on: [ "self-hosted", "hostname:kaito-e2e-github-runner" ]
     name: e2e-tests-${{ inputs.node_provisioner }}
+    environment: e2e-test
     permissions:
       contents: read
       id-token: write # This is required for requesting the JWT
-    environment: e2e-test
+    
     env:
-      GO_VERSION: "1.23"
+      TEST_SUITE: ${{ inputs.node_provisioner }}
+      GO_VERSION: "1.24"
       KARPENTER_NAMESPACE: "karpenter"
       GPU_PROVISIONER_NAMESPACE: "gpu-provisioner"
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
         with:
           ref: ${{ inputs.git_sha }}
 
-      - name: Set e2e Resource and Cluster Name
-        run: |
-          rand=$(git rev-parse --short ${{ inputs.git_sha }})
-
-          if [ "$rand" = "" ]; then
-             rand=$RANDOM
-          fi
-
-          echo "VERSION=${rand}" >> $GITHUB_ENV
-          echo "CLUSTER_NAME=${{ inputs.node_provisioner }}${rand}" >> $GITHUB_ENV
-          echo "REGISTRY=${{ inputs.node_provisioner }}${rand}.azurecr.io" >> $GITHUB_ENV
-          echo "RUN_LLAMA_13B=false" >> $GITHUB_ENV
+      - name: Run e2e-base-setup composite action
+        id: base-setup
+        uses: ./.github/actions/e2e-base-setup
+        with:
+          git_sha: ${{ inputs.git_sha }}
+          node_provisioner: ${{ inputs.node_provisioner }}
+          tag: ${{ inputs.tag }}
+          isRelease: ${{ inputs.isRelease }}
+          region: ${{ inputs.region }}
+          k8s_version: ${{ inputs.k8s_version }}
 
       - name: Set Registry
         if: ${{ inputs.isRelease }}
         run: |
           echo "REGISTRY=${{ inputs.registry }}" >> $GITHUB_ENV
           echo "VERSION=$(echo ${{ inputs.tag }} | tr -d v)" >> $GITHUB_ENV
-      
-      - name: Remove existing Go modules directory
-        run: sudo rm -rf ~/go/pkg/mod
-      
-      - name: Set up Go ${{ env.GO_VERSION }}
-        uses: actions/setup-go@v5.2.0
-        with:
-          go-version: ${{ env.GO_VERSION  }}
-
-      - name: Install Azure CLI latest
-        run: |
-          if ! which az > /dev/null; then
-            echo "Azure CLI not found. Installing..."
-            curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
-          else
-            echo "Azure CLI already installed."
-          fi
-
-      - name: Azure CLI Login
-        run: |
-          az login --identity
-
-      - uses: azure/setup-helm@v4
-        id: install
-
-      - name: Create Resource Group
-        shell: bash
-        run: |
-          make create-rg
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-
-      - name: Create ACR
-        shell: bash
-        run: |
-          make create-acr
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_ACR_NAME: ${{ env.CLUSTER_NAME }}
-
-      - name: Create Azure Identity
-        uses: azure/CLI@v2.1.0
-        with:
-          inlineScript: |
-            az identity create --name ${{ inputs.node_provisioner }}Identity --resource-group  ${{ env.CLUSTER_NAME }}
-
-      - name: Generate APIs
-        run: |
-          make generate
-
-      - name: build KAITO image
-        if: ${{ !inputs.isRelease }}
-        shell: bash
-        run: |
-          make docker-build-workspace
-        env:
-          REGISTRY: ${{ env.REGISTRY }}
-          VERSION: ${{ env.VERSION }}
 
       - name: build adapter image
         shell: bash
@@ -139,103 +83,14 @@ jobs:
           make docker-build-dataset
         env:
           REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
-
-      - name: create cluster
-        shell: bash
-        run: |
-          if [ "${{ inputs.node_provisioner }}" == "gpuprovisioner" ]; then
-             make create-aks-cluster
-          else
-            make create-aks-cluster-for-karpenter
-          fi
-        env:
-          AZURE_ACR_NAME: ${{ env.CLUSTER_NAME }}
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-          AZURE_LOCATION: ${{ inputs.region }}
-          AKS_K8S_VERSION: ${{ inputs.k8s_version }}
-
-      - name: Create Identities and Permissions for ${{ inputs.node_provisioner }}
-        shell: bash
-        run: |
-          AZURE_SUBSCRIPTION_ID=$E2E_SUBSCRIPTION_ID \
-          make generate-identities
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-          TEST_SUITE: ${{ inputs.node_provisioner }}
-
-      - name: Install gpu-provisioner helm chart
-        if: ${{ inputs.node_provisioner == 'gpuprovisioner' }}
-        shell: bash
-        run: |
-          AZURE_TENANT_ID=$E2E_TENANT_ID \
-          AZURE_SUBSCRIPTION_ID=$E2E_SUBSCRIPTION_ID \
-          make gpu-provisioner-helm
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-
-      - name: Install karpenter Azure provider helm chart
-        if: ${{ inputs.node_provisioner == 'azkarpenter' }}
-        shell: bash
-        run: |
-          AZURE_TENANT_ID=$E2E_TENANT_ID \
-          AZURE_SUBSCRIPTION_ID=$E2E_SUBSCRIPTION_ID \
-          make azure-karpenter-helm
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-          KARPENTER_VERSION: ${{ vars.KARPENTER_VERSION }}
-          KARPENTER_NAMESPACE: ${{ env.KARPENTER_NAMESPACE }}
-
-      - name: Install KAITO Workspace helm chart
-        shell: bash
-        run: |
-          make az-patch-install-helm
-          kubectl wait --for=condition=available deploy "kaito-workspace" -n kaito-workspace --timeout=300s
-        env:
-          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
-          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-          REGISTRY: ${{ env.REGISTRY }}
-          VERSION: ${{ env.VERSION }}
-          TEST_SUITE: ${{ inputs.node_provisioner }}
-
-      # Retrieve E2E ACR credentials and create Kubernetes secret
-      - name: Set up E2E ACR Credentials and Secret
-        shell: bash
-        run: |
-          # Retrieve the ACR username and password
-          ACR_USERNAME=$(az acr credential show --name ${{ env.CLUSTER_NAME }} --resource-group ${{ env.CLUSTER_NAME }} --query "username" -o tsv)
-          ACR_PASSWORD=$(az acr credential show --name ${{ env.CLUSTER_NAME }} --resource-group ${{ env.CLUSTER_NAME }} --query "passwords[0].value" -o tsv)
-
-          # Ensure credentials were retrieved successfully
-          if [ -z "$ACR_USERNAME" ] || [ -z "$ACR_PASSWORD" ]; then
-            echo "Failed to retrieve ACR credentials"
-            exit 1
-          fi
-
-          # Create the Kubernetes secret with the retrieved credentials
-          kubectl create secret docker-registry ${{ env.CLUSTER_NAME }}-acr-secret \
-          --docker-server=${{ env.CLUSTER_NAME }}.azurecr.io \
-          --docker-username=${ACR_USERNAME} \
-          --docker-password=${ACR_PASSWORD}
-
-      # Add Private-Hosted ACR secret for private models like llama
-      - name: Add Private-Hosted ACR Secret Credentials
-        run: |
-          # Ensure E2E_AMRT_SECRET_NAME is sanitized to remove any accidental quotes
-          E2E_AMRT_SECRET_NAME=$(echo "$E2E_AMRT_SECRET_NAME" | sed 's/[\"'\'']//g')
-
-          if kubectl get secret "$E2E_AMRT_SECRET_NAME" >/dev/null 2>&1; then
-            echo "Secret $E2E_AMRT_SECRET_NAME already exists. Skipping creation."
-          else
-            kubectl create secret docker-registry "$E2E_AMRT_SECRET_NAME" \
-              --docker-server="$E2E_ACR_AMRT_USERNAME.azurecr.io" \
-              --docker-username="$E2E_ACR_AMRT_USERNAME" \
-              --docker-password="$E2E_ACR_AMRT_PASSWORD"
-            echo "Secret $E2E_AMRT_SECRET_NAME created successfully."
-          fi
+      
+      - name: Remove existing Go modules directory
+        run: sudo rm -rf ~/go/pkg/mod
+      
+      - name: Set up Go ${{ env.GO_VERSION }}
+        uses: actions/setup-go@v5.4.0
+        with:
+          go-version: ${{ env.GO_VERSION  }}
 
       - name: Log ${{ inputs.node_provisioner }}
         run: |
@@ -249,23 +104,36 @@ jobs:
         run: |
           kubectl get pods -n kaito-workspace -o name | grep "^pod/kaito-workspace" | sed 's/^pod\///' | xargs -I {} kubectl logs -n kaito-workspace {}
 
-      - name: Run e2e test
+      - name: Run full e2e test
+        if: ${{ inputs.isRelease }}
+        run: |
+          AI_MODELS_REGISTRY=$E2E_ACR_AMRT_USERNAME.azurecr.io \
+          AI_MODELS_REGISTRY_SECRET=$E2E_AMRT_SECRET_NAME \
+          make kaito-workspace-e2e-test
+        env:
+          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+          E2E_ACR_REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
+          E2E_ACR_REGISTRY_SECRET: ${{ env.CLUSTER_NAME }}-acr-secret
+
+      - name: Run fast e2e test
+        if: ${{ !inputs.isRelease }}
         run: |
           AI_MODELS_REGISTRY=$E2E_ACR_AMRT_USERNAME.azurecr.io \
           AI_MODELS_REGISTRY_SECRET=$E2E_AMRT_SECRET_NAME \
+          GINKGO_LABEL=FastCheck \
           make kaito-workspace-e2e-test
         env:
           AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
-          RUN_LLAMA_13B: ${{ env.RUN_LLAMA_13B }}
-          REGISTRY: ${{ env.REGISTRY }}
-          TEST_SUITE: ${{ inputs.node_provisioner }}
           E2E_ACR_REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
           E2E_ACR_REGISTRY_SECRET: ${{ env.CLUSTER_NAME }}-acr-secret
 
+
       - name: Cleanup e2e resources
         if: ${{ always() }}
+        env:
+          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
         uses: azure/CLI@v2.1.0
         with:
           inlineScript: |
             set +e
-            az group delete --name "${{ env.CLUSTER_NAME }}" --yes --no-wait || true
+            az group delete --name "${AZURE_RESOURCE_GROUP}" --yes --no-wait || true
\ No newline at end of file
diff --git a/.github/workflows/helm-chart.yml b/.github/workflows/helm-chart.yml
index 325738b..f303fb7 100644
--- a/.github/workflows/helm-chart.yml
+++ b/.github/workflows/helm-chart.yml
@@ -1,4 +1,4 @@
-name: publish_helm_chart
+name: Publish helm chart
 
 on:
   repository_dispatch:
@@ -18,9 +18,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
diff --git a/.github/workflows/lint-go.yml b/.github/workflows/lint-go.yml
index e0c11ff..cef464d 100644
--- a/.github/workflows/lint-go.yml
+++ b/.github/workflows/lint-go.yml
@@ -1,4 +1,4 @@
-name: lint-go
+name: Go Lint
 
 concurrency:
   group:  ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -13,7 +13,7 @@ on:
       - main
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
 
 permissions:
   contents: read
@@ -25,9 +25,11 @@ jobs:
       contents: read
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
         with:
@@ -35,7 +37,7 @@ jobs:
           fetch-depth: 0
 
       - name: Set up Go ${{ env.GO_VERSION }}
-        uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
         with:
           go-version: ${{ env.GO_VERSION }}
 
diff --git a/.github/workflows/markdown-link-check.yml b/.github/workflows/markdown-link-check.yml
index 3b4f52e..436957d 100644
--- a/.github/workflows/markdown-link-check.yml
+++ b/.github/workflows/markdown-link-check.yml
@@ -1,4 +1,4 @@
-name: markdown link
+name: Markdown link check
 
 on:
   pull_request:
@@ -14,9 +14,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: gaurav-nelson/github-action-markdown-link-check@5c5dfc0ac2e225883c0e5f03a85311ec2830d368 # v1
diff --git a/.github/workflows/pr-title-lint.yml b/.github/workflows/pr-title-lint.yml
index d105d47..cb13e02 100644
--- a/.github/workflows/pr-title-lint.yml
+++ b/.github/workflows/pr-title-lint.yml
@@ -1,4 +1,4 @@
-name: pr Title Checker
+name: PR Title Checker
 on:
   pull_request:
     types:
@@ -13,9 +13,11 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3
         with:
diff --git a/.github/workflows/preset-image-build-1ES.yml b/.github/workflows/preset-image-build-1ES.yml
index a2bfb4d..e375dcb 100644
--- a/.github/workflows/preset-image-build-1ES.yml
+++ b/.github/workflows/preset-image-build-1ES.yml
@@ -33,7 +33,7 @@ on:
         description: "Run all Phi models for build"
 
 env:
-    GO_VERSION: "1.23"
+    GO_VERSION: "1.24"
     BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
     WEIGHTS_DIR: "/mnt/storage"
 
diff --git a/.github/workflows/preset-image-build.yml b/.github/workflows/preset-image-build.yml
index 20ba362..2cb45e3 100644
--- a/.github/workflows/preset-image-build.yml
+++ b/.github/workflows/preset-image-build.yml
@@ -28,7 +28,7 @@ on:
         default: false
         description: "Run all public models for build"
 env:
-    GO_VERSION: "1.23"
+    GO_VERSION: "1.24"
     BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
     WEIGHTS_DIR: "/datadrive"
 
@@ -105,7 +105,7 @@ jobs:
               fi
     
       - name: 'Az CLI login'
-        uses: azure/login@v2.2.0
+        uses: azure/login@v2.3.0
         with:
             client-id: ${{ secrets.AZURE_CLIENT_ID }}
             tenant-id: ${{ secrets.AZURE_TENANT_ID }}
diff --git a/.github/workflows/publish-rag-controller-gh-image.yml b/.github/workflows/publish-rag-controller-gh-image.yml
index fa24674..614a31f 100644
--- a/.github/workflows/publish-rag-controller-gh-image.yml
+++ b/.github/workflows/publish-rag-controller-gh-image.yml
@@ -12,7 +12,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
   IMAGE_NAME: 'kaito-rag-engine'
   REGISTRY: ghcr.io
 
@@ -33,9 +33,11 @@ jobs:
           echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -87,7 +89,7 @@ jobs:
           ref: ${{ needs.check-tag.outputs.tag }}
 
       - name: Login to ${{ steps.get-registry.outputs.registry_repository }}
-        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
         with:
           registry: ${{ env.REGISTRY }}
           username: ${{ github.actor }}
@@ -113,4 +115,14 @@ jobs:
           timeout: '5m0s'
         env:
           TRIVY_USERNAME: ${{ github.actor }}
-          TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+          TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
+
+  run-e2e-gh-image:
+    needs: [ check-tag, build-scan-publish-gh-images ]
+    uses: ./.github/workflows/ragengine-e2e-workflow.yml
+    with:
+      git_sha: ${{ github.sha }}
+      isRelease: true
+      registry: ${{ needs.build-scan-publish-gh-images.outputs.registry_repository }}
+      k8s_version: ${{ vars.AKS_K8S_VERSION }}
+      tag: ${{ needs.check-tag.outputs.tag }}
\ No newline at end of file
diff --git a/.github/workflows/publish-rag-service-gh-image.yml b/.github/workflows/publish-rag-service-gh-image.yml
index 47b4e5b..ae5ef53 100644
--- a/.github/workflows/publish-rag-service-gh-image.yml
+++ b/.github/workflows/publish-rag-service-gh-image.yml
@@ -12,7 +12,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
   IMAGE_NAME: 'kaito-rag-service'
   REGISTRY: ghcr.io
 
@@ -33,9 +33,11 @@ jobs:
           echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -87,7 +89,7 @@ jobs:
           ref: ${{ needs.check-tag.outputs.tag }}
 
       - name: Login to ${{ steps.get-registry.outputs.registry_repository }}
-        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
         with:
           registry: ${{ env.REGISTRY }}
           username: ${{ github.actor }}
diff --git a/.github/workflows/publish-workspace-gh-image.yml b/.github/workflows/publish-workspace-gh-image.yml
index 837d0bb..ba9e938 100644
--- a/.github/workflows/publish-workspace-gh-image.yml
+++ b/.github/workflows/publish-workspace-gh-image.yml
@@ -12,7 +12,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
   IMAGE_NAME: 'workspace'
   REGISTRY: ghcr.io
 
@@ -33,9 +33,11 @@ jobs:
             echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Checkout
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -87,7 +89,7 @@ jobs:
           ref: ${{ needs.check-tag.outputs.tag }}
 
       - name: Login to ${{ steps.get-registry.outputs.registry_repository }}
-        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
+        uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772
         with:
           registry: ${{ env.REGISTRY }}
           username: ${{ github.actor }}
diff --git a/.github/workflows/publish-workspace-mcr-image.yml b/.github/workflows/publish-workspace-mcr-image.yml
index 8598744..9ef106d 100644
--- a/.github/workflows/publish-workspace-mcr-image.yml
+++ b/.github/workflows/publish-workspace-mcr-image.yml
@@ -1,4 +1,4 @@
-name: Push image to MCR
+name: Publish KAITO MCR image
 on:
   repository_dispatch:
     types: [ publish-mcr-image ]
@@ -8,7 +8,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: '1.23'
+  GO_VERSION: '1.24'
   IMAGE_NAME: 'workspace'
 
 jobs:
@@ -18,7 +18,7 @@ jobs:
     environment: publish-mcr
     steps:
       - name: Set up Go ${{ env.GO_VERSION }}
-        uses: actions/setup-go@v5.2.0
+        uses: actions/setup-go@v5.4.0
         with:
           go-version: ${{ env.GO_VERSION  }}
 
diff --git a/.github/workflows/ragengine-e2e-workflow.yml b/.github/workflows/ragengine-e2e-workflow.yml
new file mode 100644
index 0000000..0c7e908
--- /dev/null
+++ b/.github/workflows/ragengine-e2e-workflow.yml
@@ -0,0 +1,119 @@
+name: ragengine-e2e-workflow
+
+on:
+  workflow_call:
+    inputs:
+      git_sha:
+        type: string
+        required: true
+      node_provisioner:
+        type: string
+        required: false
+        default: gpuprovisioner
+      tag:
+        type: string
+      isRelease:
+        type: boolean
+        default: false
+      registry:
+        type: string
+      region:
+        type: string
+        description: "the azure location to run the e2e test in"
+        default: "eastus"
+      k8s_version:
+        type: string
+        default: "1.30.0"
+
+jobs:
+  e2e-tests:
+    runs-on: [ "self-hosted", "hostname:kaito-e2e-github-runner" ]
+    environment: e2e-test
+    permissions:
+      contents: read
+      id-token: write
+    
+    env:
+      TEST_SUITE: ${{ inputs.node_provisioner }}      
+      GO_VERSION: "1.24"
+      KARPENTER_NAMESPACE: "karpenter"
+      GPU_PROVISIONER_NAMESPACE: "gpu-provisioner"
+
+    steps:
+      - name: Harden Runner
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
+        with:
+          egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
+
+      - name: Checkout
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+        with:
+          ref: ${{ inputs.git_sha }}
+      
+      - name: Run e2e-base-setup composite action
+        id: base-setup
+        uses: ./.github/actions/e2e-base-setup
+        with:
+          git_sha: ${{ inputs.git_sha }}
+          node_provisioner: ${{ inputs.node_provisioner }}
+          tag: ${{ inputs.tag }}
+          isRelease: ${{ inputs.isRelease }}
+          region: ${{ inputs.region }}
+          k8s_version: ${{ inputs.k8s_version }}
+
+      - name: Set Registry
+        if: ${{ inputs.isRelease }}
+        run: |
+          echo "REGISTRY=${{ inputs.registry }}" >> $GITHUB_ENV
+          echo "VERSION=$(echo ${{ inputs.tag }} | tr -d v)" >> $GITHUB_ENV
+      
+      - name: build kaito RAG Engine image
+        if: ${{ !inputs.isRelease }}
+        shell: bash
+        run: |
+          make docker-build-ragengine
+        env:
+          REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
+      
+      - name: Remove existing Go modules directory
+        run: sudo rm -rf ~/go/pkg/mod
+      
+      - name: Set up Go ${{ env.GO_VERSION }}
+        uses: actions/setup-go@v5.4.0
+        with:
+          go-version: ${{ env.GO_VERSION }}
+
+      - name: Install Helm
+        uses: azure/setup-helm@v4
+      
+      - name: Install KAITO RAG Engine helm chart
+        shell: bash
+        run: |
+          make az-patch-install-ragengine-helm
+          kubectl wait --for=condition=available deploy "kaito-ragengine" -n kaito-ragengine --timeout=300s
+        env:
+          REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
+          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+      
+      - name: Run e2e test
+        run: |
+          AI_MODELS_REGISTRY=$E2E_ACR_AMRT_USERNAME.azurecr.io \
+          AI_MODELS_REGISTRY_SECRET=$E2E_AMRT_SECRET_NAME \
+          make kaito-ragengine-e2e-test
+        env:
+          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+          E2E_ACR_REGISTRY: ${{ env.CLUSTER_NAME }}.azurecr.io
+          E2E_ACR_REGISTRY_SECRET: ${{ env.CLUSTER_NAME }}-acr-secret
+
+      - name: Cleanup e2e resources
+        if: ${{ always() }}
+        env:
+          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+        uses: azure/CLI@v2.1.0
+        with:
+          inlineScript: |
+            set +e
+            az group delete --name "${AZURE_RESOURCE_GROUP}" --yes --no-wait || true
diff --git a/.github/workflows/ragengine-e2e.yml b/.github/workflows/ragengine-e2e.yml
new file mode 100644
index 0000000..f839789
--- /dev/null
+++ b/.github/workflows/ragengine-e2e.yml
@@ -0,0 +1,34 @@
+name: ragengine-e2e-test
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+
+on:
+  pull_request:
+    paths:
+      - 'test/**'
+      - 'pkg/ragengine/**'
+      - '.github/**'
+
+env:
+  GO_VERSION: "1.24"
+
+permissions:
+  id-token: write # This is required for requesting the JWT
+  contents: read # This is required for actions/checkout
+
+jobs:
+  run-e2e:
+    strategy:
+      fail-fast: false
+      matrix:
+        node-provisioner: [gpuprovisioner] # WIP: azkarpenter]
+    permissions:
+      contents: read
+      id-token: write
+      statuses: write
+    uses: ./.github/workflows/ragengine-e2e-workflow.yml
+    with:
+      git_sha: ${{ github.event.pull_request.head.sha }}
+      node_provisioner: ${{ matrix.node-provisioner }}
diff --git a/.github/workflows/unit-tests-ragengine.yml b/.github/workflows/unit-tests-ragengine.yml
index fe03561..5b8295c 100644
--- a/.github/workflows/unit-tests-ragengine.yml
+++ b/.github/workflows/unit-tests-ragengine.yml
@@ -17,7 +17,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
 
 jobs:
   unit-tests:
@@ -25,9 +25,11 @@ jobs:
     environment: unit-tests
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Check out the code
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -40,8 +42,7 @@ jobs:
           make rag-service-test
 
       - name: Upload Coverage to Codecov
-        uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
+        uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
         with:
-          fail_ci_if_error: true
-          token: ${{ secrets.CODECOV_TOKEN }}
           verbose: true
+          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 4ef7091..7fb92bd 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -1,4 +1,4 @@
-name: unit-tests
+name: Unit Tests
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -17,7 +17,7 @@ permissions:
   packages: write
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
 
 jobs:
   unit-tests:
@@ -25,9 +25,11 @@ jobs:
     environment: unit-tests
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
+        uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
         with:
           egress-policy: audit
+          disable-sudo: true
+          disable-telemetry: true
 
       - name: Check out the code
         uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -36,7 +38,7 @@ jobs:
           fetch-depth: 0
 
       - name: Set up Go ${{ env.GO_VERSION }}
-        uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+        uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
         with:
           go-version: ${{ env.GO_VERSION  }}
 
@@ -54,9 +56,8 @@ jobs:
           DEVICE=cpu make inference-api-e2e
 
       - name: Upload Codecov report
-        uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
+        uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
         with:
-          ## Comma-separated list of files to upload
+          verbose: true
           files: ./coverage.txt
-        env:
-          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+          token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/kaito-e2e.yml b/.github/workflows/workspace-e2e.yml
similarity index 94%
rename from .github/workflows/kaito-e2e.yml
rename to .github/workflows/workspace-e2e.yml
index c3ac054..60176c5 100644
--- a/.github/workflows/kaito-e2e.yml
+++ b/.github/workflows/workspace-e2e.yml
@@ -1,4 +1,4 @@
-name: pr-e2e-test
+name: E2E Tests
 
 concurrency:
   group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -9,7 +9,7 @@ on:
     paths-ignore: ['docs/**', '**.md', '**.mdx', '**.png', '**.jpg']
 
 env:
-  GO_VERSION: "1.23"
+  GO_VERSION: "1.24"
 
 permissions:
   id-token: write # This is required for requesting the JWT
diff --git a/.gitignore b/.gitignore
index 501d73b..deb7cd0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,3 @@
-
 # Binaries for programs and plugins
 *.exe
 *.exe~
@@ -31,4 +30,8 @@ hack/tools/bin/*
 /coverage.txt
 
 # values override file for helm chart installation
-values.override.yaml
\ No newline at end of file
+values.override.yaml
+
+# tilt artifacts
+tilt_bin
+tilt-settings*
diff --git a/Makefile b/Makefile
index 509a8ae..f21bb41 100644
--- a/Makefile
+++ b/Makefile
@@ -2,8 +2,9 @@
 # Image URL to use all building/pushing image targets
 REGISTRY ?= YOUR_REGISTRY
 IMG_NAME ?= workspace
-VERSION ?= v0.4.2
-GPU_PROVISIONER_VERSION ?= 0.3.1
+VERSION ?= v0.4.4
+GPU_PROVISIONER_VERSION ?= 0.3.3
+RAGENGINE_IMG_NAME ?= ragengine
 IMG_TAG ?= $(subst v,,$(VERSION))
 
 ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
@@ -19,6 +20,9 @@ GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN)-$(GOLANGCI_LINT
 E2E_TEST_BIN := e2e.test
 E2E_TEST := $(BIN_DIR)/$(E2E_TEST_BIN)
 
+RAGENGINE_E2E_TEST_BIN := rage2e.test
+RAGENGINE_E2E_TEST := $(BIN_DIR)/$(RAGENGINE_E2E_TEST_BIN)
+
 GINKGO_VER := v2.19.0
 GINKGO_BIN := ginkgo
 GINKGO := $(TOOLS_BIN_DIR)/$(GINKGO_BIN)-$(GINKGO_VER)
@@ -32,6 +36,7 @@ AZURE_CLUSTER_NAME ?= kaito-demo
 AZURE_RESOURCE_GROUP_MC=MC_$(AZURE_RESOURCE_GROUP)_$(AZURE_CLUSTER_NAME)_$(AZURE_LOCATION)
 GPU_PROVISIONER_NAMESPACE ?= gpu-provisioner
 KAITO_NAMESPACE ?= kaito-workspace
+KAITO_RAGENGINE_NAMESPACE ?= kaito-ragengine
 GPU_PROVISIONER_MSI_NAME ?= gpuprovisionerIdentity
 
 ## Azure Karpenter parameters
@@ -43,7 +48,7 @@ AZURE_KARPENTER_MSI_NAME ?= azkarpenterIdentity
 RUN_LLAMA_13B ?= false
 AI_MODELS_REGISTRY ?= modelregistry.azurecr.io
 AI_MODELS_REGISTRY_SECRET ?= modelregistry
-SUPPORTED_MODELS_YAML_PATH ?= ~/runner/_work/kaito/kaito/presets/workspace/models/supported_models.yaml
+SUPPORTED_MODELS_YAML_PATH ?= $(ROOT_DIR)/presets/workspace/models/supported_models.yaml
 
 ## AWS parameters
 CLUSTER_CONFIG_FILE ?= ./docs/aws/clusterconfig.yaml.template
@@ -98,13 +103,13 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and
 .PHONY: unit-test
 unit-test: ## Run unit tests.
 	go test -v $(shell go list ./pkg/... ./api/... | \
-	grep -v -e /vendor -e /api/v1alpha1/zz_generated.deepcopy.go -e /pkg/utils/test/...) \
+	grep -v -e /vendor -e /api/v1alpha1/zz_generated.deepcopy.go -e /api/v1beta1/zz_generated.deepcopy.go -e /pkg/utils/test/...) \
 	-race -coverprofile=coverage.txt -covermode=atomic
 	go tool cover -func=coverage.txt
 
 .PHONY: rag-service-test
 rag-service-test:
-	pip install -r presets/ragengine/requirements.txt
+	pip install -r presets/ragengine/requirements-test.txt
 	pip install pytest-cov
 	pytest --cov -o log_cli=true -o log_cli_level=INFO presets/ragengine/tests
 
@@ -127,14 +132,18 @@ inference-api-e2e:
 # Ginkgo configurations
 GINKGO_FOCUS ?=
 GINKGO_SKIP ?=
+GINKGO_LABEL ?=
 GINKGO_NODES ?= 2
 GINKGO_NO_COLOR ?= false
 GINKGO_TIMEOUT ?= 120m
-GINKGO_ARGS ?= -focus="$(GINKGO_FOCUS)" -skip="$(GINKGO_SKIP)" -nodes=$(GINKGO_NODES) -no-color=$(GINKGO_NO_COLOR) -timeout=$(GINKGO_TIMEOUT) --fail-fast
+GINKGO_ARGS ?= --label-filter="$(GINKGO_LABEL)" -focus="$(GINKGO_FOCUS)" -skip="$(GINKGO_SKIP)" -nodes=$(GINKGO_NODES) -no-color=$(GINKGO_NO_COLOR) -timeout=$(GINKGO_TIMEOUT) --fail-fast
 
 $(E2E_TEST):
 	(cd test/e2e && go test -c . -o $(E2E_TEST))
 
+$(RAGENGINE_E2E_TEST):
+	(cd test/rage2e && go test -c . -o $(RAGENGINE_E2E_TEST))
+
 .PHONY: kaito-workspace-e2e-test
 kaito-workspace-e2e-test: $(E2E_TEST) $(GINKGO)
 	AI_MODELS_REGISTRY_SECRET=$(AI_MODELS_REGISTRY_SECRET) RUN_LLAMA_13B=$(RUN_LLAMA_13B) \
@@ -143,6 +152,14 @@ kaito-workspace-e2e-test: $(E2E_TEST) $(GINKGO)
 	SUPPORTED_MODELS_YAML_PATH=$(SUPPORTED_MODELS_YAML_PATH) \
  	$(GINKGO) -v -trace $(GINKGO_ARGS) $(E2E_TEST)
 
+.PHONY: kaito-ragengine-e2e-test
+kaito-ragengine-e2e-test: $(RAGENGINE_E2E_TEST) $(GINKGO)
+	AI_MODELS_REGISTRY_SECRET=$(AI_MODELS_REGISTRY_SECRET) RUN_LLAMA_13B=$(RUN_LLAMA_13B) \
+	AI_MODELS_REGISTRY=$(AI_MODELS_REGISTRY) GPU_PROVISIONER_NAMESPACE=$(GPU_PROVISIONER_NAMESPACE)  KAITO_NAMESPACE=$(KAITO_NAMESPACE) \
+	KARPENTER_NAMESPACE=$(KARPENTER_NAMESPACE) KAITO_RAGENGINE_NAMESPACE=$(KAITO_RAGENGINE_NAMESPACE) TEST_SUITE=$(TEST_SUITE) \
+	SUPPORTED_MODELS_YAML_PATH=$(SUPPORTED_MODELS_YAML_PATH) \
+	$(GINKGO) -v -trace $(GINKGO_ARGS) $(RAGENGINE_E2E_TEST)
+
 ## --------------------------------------
 ## Azure resources
 ## --------------------------------------
@@ -237,11 +254,11 @@ docker-build-workspace: docker-buildx
 .PHONY: docker-build-ragengine
 docker-build-ragengine: docker-buildx
 	docker buildx build \
-                --file ./docker/ragengine/Dockerfile \
-                --output=$(OUTPUT_TYPE) \
-                --platform="linux/$(ARCH)" \
-                --pull \
-                --tag $(REGISTRY)/$(RAGENGINE_IMG_NAME):$(RAGENGINE_IMG_TAG) .
+		--file ./docker/ragengine/Dockerfile \
+		--output=$(OUTPUT_TYPE) \
+		--platform="linux/$(ARCH)" \
+		--pull \
+		--tag $(REGISTRY)/$(RAGENGINE_IMAGE_NAME):$(IMG_TAG) .
 
 .PHONY: docker-build-rag-service
 docker-build-ragservice: docker-buildx
@@ -314,13 +331,20 @@ az-patch-install-helm: ## Update Azure client env vars and settings in helm valu
 
 	yq -i '(.image.repository)                                              = "$(REGISTRY)/workspace"'                    ./charts/kaito/workspace/values.yaml
 	yq -i '(.image.tag)                                                     = "$(IMG_TAG)"'                               ./charts/kaito/workspace/values.yaml
-	if [ $(TEST_SUITE) = "azkarpenter" ]; then \
-		yq -i '(.featureGates.Karpenter)                                    = "true"'                                       ./charts/kaito/workspace/values.yaml; \
-	fi
 	yq -i '(.clusterName)                                                   = "$(AZURE_CLUSTER_NAME)"'                    ./charts/kaito/workspace/values.yaml
 
 	helm install kaito-workspace ./charts/kaito/workspace --namespace $(KAITO_NAMESPACE) --create-namespace
 
+.PHONY: az-patch-install-ragengine-helm
+az-patch-install-ragengine-helm:
+	az aks get-credentials --name $(AZURE_CLUSTER_NAME) --resource-group $(AZURE_RESOURCE_GROUP)
+
+	yq -i '(.image.repository)                                              = "$(REGISTRY)/ragengine"'                    ./charts/kaito/ragengine/values.yaml
+	yq -i '(.image.tag)                                                     = "$(IMG_TAG)"'                               ./charts/kaito/ragengine/values.yaml
+	yq -i '(.clusterName)                                                   = "$(AZURE_CLUSTER_NAME)"'                    ./charts/kaito/ragengine/values.yaml
+
+	helm install kaito-ragengine ./charts/kaito/ragengine --namespace $(KAITO_RAGENGINE_NAMESPACE) --create-namespace
+
 .PHONY: aws-patch-install-helm ##install kaito on AWS cluster
 aws-patch-install-helm: 
 	yq -i '(.image.repository)                                              = "$(REGISTRY)/workspace"'                    	./charts/kaito/workspace/values.yaml
diff --git a/README.md b/README.md
index 54d208f..ce1e155 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,8 @@
 
 | ![notification](docs/img/bell.svg) What is NEW! |
 |-------------------------------------------------|
-| Latest Release: Jan 3rd, 2025. Kaito v0.4.1.    |
+| **Coming soon:** Kaito v0.5.0. Retrieval-augmented generation (RAG) - RagEngine support with LlamaIndex orchestration and Faiss as the default vectorDB, learn about recent updates [here](https://github.com/kaito-project/kaito/issues/734)! | 
+| Latest Release: Jan 31th, 2025. Kaito v0.4.4. **DeepSeek-R1 distilled models are added! (llama-8b and qwen-14b).**  |
 | First Release: Nov 15th, 2023. Kaito v0.1.0.    |
 
 Kaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster.
@@ -78,7 +79,7 @@ NAME                     TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)
 workspace-phi-3-5-mini   ClusterIP   <CLUSTERIP>  <none>        80/TCP,29500/TCP   10m
 $ export CLUSTERIP=$(kubectl get svc workspace-phi-3-5-mini -o jsonpath="{.spec.clusterIPs[0]}")
 
-# find availalbe models
+# find available models
 $ kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -s  http://$CLUSTERIP/v1/models | jq
 {
   "object": "list",
diff --git a/Tiltfile b/Tiltfile
new file mode 100644
index 0000000..bbb5e37
--- /dev/null
+++ b/Tiltfile
@@ -0,0 +1,115 @@
+# Based on https://github.com/tilt-dev/tilt-extensions/blob/master/kubebuilder/Tiltfile
+
+load('ext://restart_process', 'docker_build_with_restart')
+
+settings = {}
+
+tilt_settings = "./tilt-settings.yaml" if os.path.exists("./tilt-settings.yaml") else "./tilt-settings.json"
+settings.update(read_yaml(tilt_settings, default = {}))
+
+if 'allowed_contexts' in settings:
+    allow_k8s_contexts(settings['allowed_contexts'])
+if 'default_registry' in settings:
+    # Set the default registry for tilt to use
+    default_registry(settings['default_registry'].removesuffix('/') + '/kaito/workspace')
+
+def main(IMG='controller:latest', DISABLE_SECURITY_CONTEXT=True):
+
+    DOCKERFILE = '''
+    FROM --platform=linux/amd64 mcr.microsoft.com/oss/go/microsoft/golang:1.24
+    WORKDIR /
+    COPY ./tilt_bin/manager /
+    CMD ["/manager"]
+    '''
+
+    def yaml():
+        cluster_name = k8s_context() if not settings.get('cluster_name') else settings['cluster_name']
+        helm_template = 'helm template kaito-workspace ./charts/kaito/workspace --namespace kaito-workspace --set clusterName={} --set image.repository=controller --set image.tag=latest'.format(k8s_context())
+        # Set the image name and tag to controller:latest for Tilt to
+        # substitute later during docker_build_with_restart
+        data = local(helm_template, quiet=True)
+        # Tilt's live update conflicts with SecurityContext, until a better solution, just remove it
+        if DISABLE_SECURITY_CONTEXT:
+            decoded = decode_yaml_stream(data)
+            if decoded:
+                for d in decoded:
+                    if d["kind"] == "Deployment":
+                        if "securityContext" in d['spec']['template']['spec']:
+                            d['spec']['template']['spec'].pop('securityContext')
+                        for c in d['spec']['template']['spec']['containers']:
+                            if "securityContext" in c:
+                                c.pop('securityContext')
+            return encode_yaml_stream(decoded)
+        return data
+
+    def make_manifests():
+        return 'make manifests'
+
+    def make_generate():
+        return 'make generate'
+
+    def create_namespace(name):
+        return 'kubectl create namespace {} || true'.format(name)
+
+    def manager():
+        return 'CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tilt_bin/manager cmd/workspace/main.go'
+
+    # Create the namespace if it doesn't exist
+    local(create_namespace('kaito-workspace'), quiet=True)
+
+    # Generate the CRD manifests and kubectl apply them. Re-generate if anything in deps changes.
+    local_resource(
+        'crd',
+        make_generate() + ' && ' + make_manifests() + ' && kubectl apply --server-side -f charts/kaito/workspace/crds',
+        deps=['api'],
+        ignore=['*/*/zz_generated.deepcopy.go'],
+        labels='Workspace',
+    )
+
+    # Deploy the rendered helm chart
+    k8s_yaml(yaml())
+
+    # Labels resources from kaito-workspace
+    k8s_resource(
+        workload='kaito-workspace',
+        new_name='deployment',
+        labels='Workspace',
+        objects=['kaito-workspace-sa:serviceaccount',
+                 'kaito-workspace-role:role',
+                 'kaito-workspace-clusterrole:clusterrole',
+                 'kaito-workspace-rolebinding:rolebinding',
+                 'kaito-workspace-rolebinding:clusterrolebinding',
+                 'inference-params-template:configmap',
+                 'lora-params-template:configmap',
+                 'qlora-params-template:configmap',
+                 'workspace-webhook-cert:secret',
+                 'validation.workspace.kaito.sh:validatingwebhookconfiguration',
+        ],
+    )
+    k8s_resource(
+        workload='nvidia-device-plugin-daemonset',
+        new_name='device plugin',
+        labels='Dependencies',
+    )
+
+    # Re-compile the manager binary when anything in deps changes.
+    local_resource(
+        'manager',
+        manager(),
+        deps=['api', 'cmd', 'pkg', 'presets'],
+        ignore=['*/*/zz_generated.deepcopy.go'],
+        labels='Workspace',
+    )
+
+    # Build the initial controller image. When a newer manager binary is built,
+    # Tilt will replace the old one in the running container with the new one.
+    docker_build_with_restart(IMG, '.',
+     dockerfile_contents=DOCKERFILE,
+     entrypoint='/manager',
+     only=['./tilt_bin/manager'],
+     live_update=[
+           sync('./tilt_bin/manager', '/manager'),
+       ]
+    )
+
+main()
diff --git a/api/v1alpha1/labels.go b/api/v1alpha1/labels.go
index 99b9a54..c8628f8 100644
--- a/api/v1alpha1/labels.go
+++ b/api/v1alpha1/labels.go
@@ -39,6 +39,9 @@ const (
 
 	// AnnotationWorkspaceRuntime is the annotation for runtime selection.
 	AnnotationWorkspaceRuntime = KAITOPrefix + "runtime"
+
+	// AnnotationBypassResourceChecks allows bypassing resource requirement checks like GPU memory.
+	AnnotationBypassResourceChecks = KAITOPrefix + "bypass-resource-checks"
 )
 
 // GetWorkspaceRuntimeName returns the runtime name of the workspace.
diff --git a/api/v1alpha1/workspace_types.go b/api/v1alpha1/workspace_types.go
index 0800985..bbb3394 100644
--- a/api/v1alpha1/workspace_types.go
+++ b/api/v1alpha1/workspace_types.go
@@ -178,7 +178,6 @@ type WorkspaceStatus struct {
 // +kubebuilder:object:root=true
 // +kubebuilder:subresource:status
 // +kubebuilder:resource:path=workspaces,scope=Namespaced,categories=workspace,shortName={wk,wks}
-// +kubebuilder:storageversion
 // +kubebuilder:printcolumn:name="Instance",type="string",JSONPath=".resource.instanceType",description=""
 // +kubebuilder:printcolumn:name="ResourceReady",type="string",JSONPath=".status.conditions[?(@.type==\"ResourceReady\")].status",description=""
 // +kubebuilder:printcolumn:name="InferenceReady",type="string",JSONPath=".status.conditions[?(@.type==\"InferenceReady\")].status",description=""
diff --git a/api/v1alpha1/workspace_validation.go b/api/v1alpha1/workspace_validation.go
index 9720d26..10cd57d 100644
--- a/api/v1alpha1/workspace_validation.go
+++ b/api/v1alpha1/workspace_validation.go
@@ -8,10 +8,10 @@ import (
 	"fmt"
 	"os"
 	"reflect"
-	"regexp"
 	"strconv"
 	"strings"
 
+	"github.com/distribution/reference"
 	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -55,8 +55,16 @@ func (w *Workspace) Validate(ctx context.Context) (errs *apis.FieldError) {
 		klog.InfoS("Validate creation", "workspace", fmt.Sprintf("%s/%s", w.Namespace, w.Name))
 		errs = errs.Also(w.validateCreate().ViaField("spec"))
 		if w.Inference != nil {
+			// Check if the bypass resource checks annotation is set
+			bypassResourceChecks := false
+			if w.GetAnnotations() != nil {
+				if _, exists := w.GetAnnotations()[AnnotationBypassResourceChecks]; exists {
+					bypassResourceChecks = true
+				}
+			}
+
 			// TODO: Add Adapter Spec Validation - Including DataSource Validation for Adapter
-			errs = errs.Also(w.Resource.validateCreateWithInference(w.Inference).ViaField("resource"),
+			errs = errs.Also(w.Resource.validateCreateWithInference(w.Inference, bypassResourceChecks).ViaField("resource"),
 				w.Inference.validateCreate(ctx, w.Namespace).ViaField("inference"))
 		}
 		if w.Tuning != nil {
@@ -207,18 +215,11 @@ func (r *DataSource) validateCreate() (errs *apis.FieldError) {
 		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
 		sourcesSpecified++
 	}
-	// Regex checks for a / and a colon followed by a tag
-	if r.Image != "" {
-		re := regexp.MustCompile(`^(.+/[^:/]+):([^:/]+)$`)
-		if !re.MatchString(r.Image) {
-			errs = errs.Also(apis.ErrInvalidValue("Invalid image format, require full input image URL", "Image"))
-		} else {
-			// Executes if image is of correct format
-			err := utils.ExtractAndValidateRepoName(r.Image)
-			if err != nil {
-				errs = errs.Also(apis.ErrInvalidValue(err.Error(), "Image"))
-			}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse input image reference: %s", err), "Image"))
 		}
+
 		sourcesSpecified++
 	}
 
@@ -237,6 +238,11 @@ func (r *DataSource) validateUpdate(old *DataSource, isTuning bool) (errs *apis.
 	if r.Volume != nil {
 		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
 	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse input image reference: %s", err), "Image"))
+		}
+	}
 
 	return errs
 }
@@ -248,22 +254,16 @@ func (r *DataDestination) validateCreate() (errs *apis.FieldError) {
 		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
 		destinationsSpecified++
 	}
-	if r.Image != "" {
-		// Regex checks for a / and a colon followed by a tag
-		re := regexp.MustCompile(`^(.+/[^:/]+):([^:/]+)$`)
-		if !re.MatchString(r.Image) {
-			errs = errs.Also(apis.ErrInvalidValue("Invalid image format, require full output image URL", "Image"))
-		} else {
-			// Executes if image is of correct format
-			err := utils.ExtractAndValidateRepoName(r.Image)
-			if err != nil {
-				errs = errs.Also(apis.ErrInvalidValue(err.Error(), "Image"))
-			}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse output image reference: %s", err), "Image"))
 		}
+
 		// Cloud Provider requires credentials to push image
 		if r.ImagePushSecret == "" {
 			errs = errs.Also(apis.ErrMissingField("Must specify imagePushSecret with destination image"))
 		}
+
 		destinationsSpecified++
 	}
 
@@ -279,6 +279,11 @@ func (r *DataDestination) validateUpdate() (errs *apis.FieldError) {
 	if r.Volume != nil {
 		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
 	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse output image reference: %s", err), "Image"))
+		}
+	}
 
 	return errs
 }
@@ -290,10 +295,15 @@ func (r *ResourceSpec) validateCreateWithTuning(tuning *TuningSpec) (errs *apis.
 	return errs
 }
 
-func (r *ResourceSpec) validateCreateWithInference(inference *InferenceSpec) (errs *apis.FieldError) {
+func (r *ResourceSpec) validateCreateWithInference(inference *InferenceSpec, bypassResourceChecks bool) (errs *apis.FieldError) {
 	var presetName string
 	if inference.Preset != nil {
 		presetName = strings.ToLower(string(inference.Preset.Name))
+		// Since inference.Preset exists, we must validate preset name.
+		if !plugin.IsValidPreset(presetName) {
+			// Return to skip the rest of checks, the Inference spec validation will return proper err msg.
+			return errs
+		}
 	}
 	instanceType := string(r.InstanceType)
 
@@ -320,42 +330,57 @@ func (r *ResourceSpec) validateCreateWithInference(inference *InferenceSpec) (er
 
 			// Separate the checks for specific error messages
 			if machineTotalNumGPUs.Cmp(modelGPUCount) < 0 {
-				errs = errs.Also(apis.ErrInvalidValue(
-					fmt.Sprintf(
-						"Insufficient number of GPUs: Instance type %s provides %s, but preset %s requires at least %d",
-						instanceType,
-						machineTotalNumGPUs.String(),
-						presetName,
-						modelGPUCount.Value(),
-					),
-					"instanceType",
-				))
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient number of GPUs detected but continuing due to bypass flag. Instance type %s provides %s, but preset %s requires at least %d",
+						instanceType, machineTotalNumGPUs.String(), presetName, modelGPUCount.Value())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient number of GPUs: Instance type %s provides %s, but preset %s requires at least %d",
+							instanceType,
+							machineTotalNumGPUs.String(),
+							presetName,
+							modelGPUCount.Value(),
+						),
+						"instanceType",
+					))
+				}
 			}
 
 			if machinePerGPUMemory.Cmp(modelPerGPUMemory) < 0 {
-				errs = errs.Also(apis.ErrInvalidValue(
-					fmt.Sprintf(
-						"Insufficient per GPU memory: Instance type %s provides %s per GPU, but preset %s requires at least %s per GPU",
-						instanceType,
-						machinePerGPUMemory.String(),
-						presetName,
-						modelPerGPUMemory.String(),
-					),
-					"instanceType",
-				))
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient per GPU memory but continuing due to bypass flag. Instance type %s provides %s per GPU, but preset %s requires at least %s per GPU",
+						instanceType, machinePerGPUMemory.String(), presetName, modelPerGPUMemory.String())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient per GPU memory: Instance type %s provides %s per GPU, but preset %s requires at least %s per GPU",
+							instanceType,
+							machinePerGPUMemory.String(),
+							presetName,
+							modelPerGPUMemory.String(),
+						),
+						"instanceType",
+					))
+				}
 			}
 
 			if machineTotalGPUMem.Cmp(modelTotalGPUMemory) < 0 {
-				errs = errs.Also(apis.ErrInvalidValue(
-					fmt.Sprintf(
-						"Insufficient total GPU memory: Instance type %s has a total of %s, but preset %s requires at least %s",
-						instanceType,
-						machineTotalGPUMem.String(),
-						presetName,
-						modelTotalGPUMemory.String(),
-					),
-					"instanceType",
-				))
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient total GPU memory detected but continuing due to bypass flag. Instance type %s has a total of %s, but preset %s requires at least %s",
+						instanceType, machineTotalGPUMem.String(), presetName, modelTotalGPUMemory.String())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient total GPU memory: Instance type %s has a total of %s, but preset %s requires at least %s",
+							instanceType,
+							machineTotalGPUMem.String(),
+							presetName,
+							modelTotalGPUMemory.String(),
+						),
+						"instanceType",
+					))
+				}
 			}
 		}
 	} else {
@@ -410,6 +435,8 @@ func (i *InferenceSpec) validateCreate(ctx context.Context, namespace string) (e
 		// Validate preset name
 		if !plugin.IsValidPreset(presetName) {
 			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unsupported inference preset name %s", presetName), "presetName"))
+			// Need to return here. Otherwise, a panic will be hit when doing following checks.
+			return errs
 		}
 		// Validate private preset has private image specified
 		if plugin.KaitoModelRegister.MustGet(string(i.Preset.Name)).GetInferenceParameters().ImageAccessMode == string(ModelImageAccessModePrivate) &&
diff --git a/api/v1beta1/condition_types.go b/api/v1beta1/condition_types.go
new file mode 100644
index 0000000..1766d75
--- /dev/null
+++ b/api/v1beta1/condition_types.go
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+// ConditionType is a valid value for Condition.Type.
+type ConditionType string
+
+const (
+	// ConditionTypeNodeClaimStatus is the state when checking nodeClaim status.
+	ConditionTypeNodeClaimStatus = ConditionType("NodeClaimReady")
+
+	// ConditionTypeResourceStatus is the state when Resource has been created.
+	ConditionTypeResourceStatus = ConditionType("ResourceReady")
+
+	// WorkspaceConditionTypeInferenceStatus is the state when Inference service has been ready.
+	WorkspaceConditionTypeInferenceStatus = ConditionType("InferenceReady")
+
+	// WorkspaceConditionTypeTuningJobStatus is the state when the tuning job starts normally.
+	WorkspaceConditionTypeTuningJobStatus ConditionType = ConditionType("JobStarted")
+
+	//WorkspaceConditionTypeDeleting is the Workspace state when starts to get deleted.
+	WorkspaceConditionTypeDeleting = ConditionType("WorkspaceDeleting")
+
+	//WorkspaceConditionTypeSucceeded is the Workspace state that summarizes all operations' states.
+	//For inference, the "True" condition means the inference service is ready to serve requests.
+	//For fine tuning, the "True" condition means the tuning job completes successfully.
+	WorkspaceConditionTypeSucceeded ConditionType = ConditionType("WorkspaceSucceeded")
+)
diff --git a/api/v1beta1/doc.go b/api/v1beta1/doc.go
new file mode 100644
index 0000000..5977ad1
--- /dev/null
+++ b/api/v1beta1/doc.go
@@ -0,0 +1,8 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+// Package v1alpha1 contains API Schema definitions for the kaito v1alpha1 API group
+// +kubebuilder:object:generate=true
+// +k8s:defaulter-gen=TypeMeta
+// +groupName=kaito.sh
+package v1beta1 // doc.go is discovered by codegen, but not by go build
diff --git a/api/v1beta1/labels.go b/api/v1beta1/labels.go
new file mode 100644
index 0000000..7fe9d5b
--- /dev/null
+++ b/api/v1beta1/labels.go
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"github.com/kaito-project/kaito/pkg/featuregates"
+	"github.com/kaito-project/kaito/pkg/model"
+	"github.com/kaito-project/kaito/pkg/utils/consts"
+)
+
+const (
+
+	// Non-prefixed labels/annotations are reserved for end-use.
+
+	// KAITOPrefix Kubernetes Data Mining prefix.
+	KAITOPrefix = "kaito.sh/"
+
+	// AnnotationEnableLB determines whether kaito creates LoadBalancer type service for testing.
+	AnnotationEnableLB = KAITOPrefix + "enablelb"
+
+	// LabelWorkspaceName is the label for workspace name.
+	LabelWorkspaceName = KAITOPrefix + "workspace"
+
+	// LabelWorkspaceName is the label for workspace namespace.
+	LabelWorkspaceNamespace = KAITOPrefix + "workspacenamespace"
+
+	// WorkspaceRevisionAnnotation is the Annotations for revision number
+	WorkspaceRevisionAnnotation = "workspace.kaito.io/revision"
+
+	// AnnotationWorkspaceRuntime is the annotation for runtime selection.
+	AnnotationWorkspaceRuntime = KAITOPrefix + "runtime"
+
+	// AnnotationBypassResourceChecks allows bypassing resource requirement checks like GPU memory.
+	AnnotationBypassResourceChecks = KAITOPrefix + "bypass-resource-checks"
+)
+
+// GetWorkspaceRuntimeName returns the runtime name of the workspace.
+func GetWorkspaceRuntimeName(ws *Workspace) model.RuntimeName {
+	if ws == nil {
+		panic("workspace is nil")
+	}
+
+	if !featuregates.FeatureGates[consts.FeatureFlagVLLM] {
+		return model.RuntimeNameHuggingfaceTransformers
+	}
+
+	runtime := model.RuntimeNameVLLM
+	name := ws.Annotations[AnnotationWorkspaceRuntime]
+	switch name {
+	case string(model.RuntimeNameHuggingfaceTransformers):
+		runtime = model.RuntimeNameHuggingfaceTransformers
+	case string(model.RuntimeNameVLLM):
+		runtime = model.RuntimeNameVLLM
+	}
+
+	return runtime
+}
diff --git a/api/v1beta1/params_validation.go b/api/v1beta1/params_validation.go
new file mode 100644
index 0000000..c1cf719
--- /dev/null
+++ b/api/v1beta1/params_validation.go
@@ -0,0 +1,278 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+	"reflect"
+	"strings"
+
+	"gopkg.in/yaml.v2"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"knative.dev/pkg/apis"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/kaito-project/kaito/pkg/k8sclient"
+	"github.com/kaito-project/kaito/pkg/utils"
+)
+
+type Config struct {
+	TrainingConfig TrainingConfig `yaml:"training_config"`
+}
+
+type TrainingConfig struct {
+	ModelConfig        map[string]runtime.RawExtension `yaml:"ModelConfig"`
+	QuantizationConfig map[string]runtime.RawExtension `yaml:"QuantizationConfig"`
+	LoraConfig         map[string]runtime.RawExtension `yaml:"LoraConfig"`
+	TrainingArguments  map[string]runtime.RawExtension `yaml:"TrainingArguments"`
+	DatasetConfig      map[string]runtime.RawExtension `yaml:"DatasetConfig"`
+	DataCollator       map[string]runtime.RawExtension `yaml:"DataCollator"`
+}
+
+func validateNilOrBool(value interface{}) error {
+	if value == nil {
+		return nil // nil is acceptable
+	}
+	if _, ok := value.(bool); ok {
+		return nil // Correct type
+	}
+	return fmt.Errorf("value must be either nil or a boolean, got type %T", value)
+}
+
+// UnmarshalYAML custom method
+func (t *TrainingConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var raw map[string]interface{}
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	// This function converts a map[string]interface{} to a map[string]runtime.RawExtension.
+	// It does this by setting the raw marshalled data of the unmarshalled YAML to
+	// be the raw data of the runtime.RawExtension object.
+	handleRawExtension := func(raw map[string]interface{}, field string) (map[string]runtime.RawExtension, error) {
+		var target map[string]runtime.RawExtension
+		if value, found := raw[field]; found {
+			delete(raw, field)
+			var ext runtime.RawExtension
+			data, err := yaml.Marshal(value)
+			if err != nil {
+				return nil, err
+			}
+			ext.Raw = data
+			if target == nil {
+				target = make(map[string]runtime.RawExtension)
+			}
+			target[field] = ext
+		}
+		return target, nil
+	}
+
+	fields := []struct {
+		name   string
+		target *map[string]runtime.RawExtension
+	}{
+		{"ModelConfig", &t.ModelConfig},
+		{"QuantizationConfig", &t.QuantizationConfig},
+		{"LoraConfig", &t.LoraConfig},
+		{"TrainingArguments", &t.TrainingArguments},
+		{"DatasetConfig", &t.DatasetConfig},
+		{"DataCollator", &t.DataCollator},
+	}
+
+	var err error
+	for _, field := range fields {
+		*field.target, err = handleRawExtension(raw, field.name)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func UnmarshalTrainingConfig(cm *corev1.ConfigMap) (*Config, *apis.FieldError) {
+	trainingConfigYAML, ok := cm.Data["training_config.yaml"]
+	if !ok {
+		return nil, apis.ErrGeneric(fmt.Sprintf("ConfigMap '%s' does not contain 'training_config.yaml' in namespace '%s'", cm.Name, cm.Namespace), "config")
+	}
+
+	var config Config
+	if err := yaml.Unmarshal([]byte(trainingConfigYAML), &config); err != nil {
+		return nil, apis.ErrGeneric(fmt.Sprintf("Failed to parse 'training_config.yaml' in ConfigMap '%s' in namespace '%s': %v", cm.Name, cm.Namespace, err), "config")
+	}
+	return &config, nil
+}
+
+func validateTrainingArgsViaConfigMap(cm *corev1.ConfigMap) *apis.FieldError {
+	config, err := UnmarshalTrainingConfig(cm)
+	if err != nil {
+		return err
+	}
+
+	trainingArgs := config.TrainingConfig.TrainingArguments
+	if trainingArgs != nil {
+		trainingArgsRaw, trainingArgsExists := trainingArgs["TrainingArguments"]
+		if trainingArgsExists {
+			// If specified, ensure output dir is of type string
+			outputDirValue, found, err := utils.SearchRawExtension(trainingArgsRaw, "output_dir")
+			if err != nil {
+				return apis.ErrGeneric(fmt.Sprintf("Failed to parse 'output_dir' in ConfigMap '%s' in namespace '%s': %v", cm.Name, cm.Namespace, err), "output_dir")
+			}
+			if found {
+				userSpecifiedDir, ok := outputDirValue.(string)
+				if !ok {
+					return apis.ErrInvalidValue(fmt.Sprintf("output_dir is not a string in ConfigMap '%s' in namespace '%s'", cm.Name, cm.Namespace), "output_dir")
+				}
+
+				// Ensure the user-specified directory is under baseDir
+				baseDir := "/mnt"
+				cleanPath := filepath.Clean(filepath.Join(baseDir, userSpecifiedDir))
+				if cleanPath == baseDir || !strings.HasPrefix(cleanPath, baseDir) {
+					return apis.ErrInvalidValue(fmt.Sprintf("Invalid output_dir specified: '%s', must be a directory", userSpecifiedDir), "output_dir")
+				}
+			}
+
+			// TODO: Here we perform the tuning GPU Memory Checks!
+			fmt.Println(trainingArgsRaw)
+		}
+	}
+	return nil
+}
+
+func validateMethodViaConfigMap(cm *corev1.ConfigMap, methodLowerCase string) *apis.FieldError {
+	config, err := UnmarshalTrainingConfig(cm)
+	if err != nil {
+		return err
+	}
+
+	// Validate QuantizationConfig if it exists
+	quantConfig := config.TrainingConfig.QuantizationConfig
+	if quantConfig != nil {
+		quantConfigRaw, quantConfigExists := quantConfig["QuantizationConfig"]
+		if quantConfigExists {
+			// Dynamic field search for quantization settings within ModelConfig
+			loadIn4bit, _, err := utils.SearchRawExtension(quantConfigRaw, "load_in_4bit")
+			if err != nil {
+				return apis.ErrInvalidValue(err.Error(), "load_in_4bit")
+			}
+			loadIn8bit, _, err := utils.SearchRawExtension(quantConfigRaw, "load_in_8bit")
+			if err != nil {
+				return apis.ErrInvalidValue(err.Error(), "load_in_8bit")
+			}
+
+			// Validate both loadIn4bit and loadIn8bit
+			if err := validateNilOrBool(loadIn4bit); err != nil {
+				return apis.ErrInvalidValue(err.Error(), "load_in_4bit")
+			}
+			if err := validateNilOrBool(loadIn8bit); err != nil {
+				return apis.ErrInvalidValue(err.Error(), "load_in_8bit")
+			}
+
+			loadIn4bitBool, _ := loadIn4bit.(bool)
+			loadIn8bitBool, _ := loadIn8bit.(bool)
+
+			// Validation Logic
+			if loadIn4bitBool && loadIn8bitBool {
+				return apis.ErrGeneric(fmt.Sprintf("Cannot set both 'load_in_4bit' and 'load_in_8bit' to true in ConfigMap '%s'", cm.Name), "QuantizationConfig")
+			}
+			if methodLowerCase == string(TuningMethodLora) {
+				if loadIn4bitBool || loadIn8bitBool {
+					return apis.ErrGeneric(fmt.Sprintf("For method 'lora', 'load_in_4bit' or 'load_in_8bit' in ConfigMap '%s' must not be true", cm.Name), "QuantizationConfig")
+				}
+			} else if methodLowerCase == string(TuningMethodQLora) {
+				if !loadIn4bitBool && !loadIn8bitBool {
+					return apis.ErrMissingField(fmt.Sprintf("For method 'qlora', either 'load_in_4bit' or 'load_in_8bit' must be true in ConfigMap '%s'", cm.Name), "QuantizationConfig")
+				}
+			}
+		}
+	} else if methodLowerCase == string(TuningMethodQLora) {
+		return apis.ErrMissingField(fmt.Sprintf("For method 'qlora', either 'load_in_4bit' or 'load_in_8bit' must be true in ConfigMap '%s'", cm.Name), "QuantizationConfig")
+	}
+	return nil
+}
+
+// getStructInstances dynamically generates instances of all sections in any config struct.
+func getStructInstances(s any) map[string]any {
+	t := reflect.TypeOf(s)
+	if t.Kind() == reflect.Ptr {
+		t = t.Elem() // Dereference pointer to get the struct type
+	}
+	instances := make(map[string]any, t.NumField())
+
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		yamlTag := field.Tag.Get("yaml")
+		if yamlTag != "" {
+			// Create a new instance of the type pointed to by the field
+			instance := reflect.MakeMap(field.Type).Interface()
+			instances[yamlTag] = instance
+		}
+	}
+
+	return instances
+}
+
+func validateTuningConfigMapSchema(cm *corev1.ConfigMap) *apis.FieldError {
+	trainingConfigData, ok := cm.Data["training_config.yaml"]
+	if !ok {
+		return apis.ErrMissingField("training_config.yaml in ConfigMap")
+	}
+
+	var rawConfig map[string]interface{}
+	if err := yaml.Unmarshal([]byte(trainingConfigData), &rawConfig); err != nil {
+		return apis.ErrInvalidValue(err.Error(), "training_config.yaml")
+	}
+
+	// Extract the actual training configuration map
+	trainingConfigMap, ok := rawConfig["training_config"].(map[interface{}]interface{})
+	if !ok {
+		return apis.ErrInvalidValue("Expected 'training_config' key to contain a map", "training_config.yaml")
+	}
+
+	sectionStructs := getStructInstances(TrainingConfig{})
+	recognizedSections := make([]string, 0, len(sectionStructs))
+	for section := range sectionStructs {
+		recognizedSections = append(recognizedSections, section)
+	}
+
+	// Check if valid sections
+	for section := range trainingConfigMap {
+		sectionStr := section.(string)
+		if !utils.Contains(recognizedSections, sectionStr) {
+			return apis.ErrInvalidValue(fmt.Sprintf("Unrecognized section: %s", section), "training_config.yaml")
+		}
+	}
+	return nil
+}
+
+func (r *TuningSpec) validateConfigMap(ctx context.Context, namespace string, methodLowerCase string, configMapName string) (errs *apis.FieldError) {
+	var cm corev1.ConfigMap
+	if k8sclient.Client == nil {
+		errs = errs.Also(apis.ErrGeneric("Failed to obtain client from context.Context"))
+		return errs
+	}
+	err := k8sclient.Client.Get(ctx, client.ObjectKey{Name: configMapName, Namespace: namespace}, &cm)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("ConfigMap '%s' specified in 'config' not found in namespace '%s'", r.Config, namespace), "config"))
+		} else {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to get ConfigMap '%s' in namespace '%s': %v", r.Config, namespace, err), "config"))
+		}
+	} else {
+		if err := validateTuningConfigMapSchema(&cm); err != nil {
+			errs = errs.Also(err)
+		}
+		if err := validateMethodViaConfigMap(&cm, methodLowerCase); err != nil {
+			errs = errs.Also(err)
+		}
+		if err := validateTrainingArgsViaConfigMap(&cm); err != nil {
+			errs = errs.Also(err)
+		}
+	}
+	return errs
+}
diff --git a/api/v1beta1/register.go b/api/v1beta1/register.go
new file mode 100644
index 0000000..3814644
--- /dev/null
+++ b/api/v1beta1/register.go
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects
+	GroupVersion = schema.GroupVersion{Group: "kaito.sh", Version: "v1beta1"}
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+)
diff --git a/api/v1beta1/workspace_default.go b/api/v1beta1/workspace_default.go
new file mode 100644
index 0000000..554804f
--- /dev/null
+++ b/api/v1beta1/workspace_default.go
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"context"
+)
+
+// SetDefaults for the Workspace
+func (w *Workspace) SetDefaults(_ context.Context) {
+}
diff --git a/api/v1beta1/workspace_types.go b/api/v1beta1/workspace_types.go
new file mode 100644
index 0000000..32e3a93
--- /dev/null
+++ b/api/v1beta1/workspace_types.go
@@ -0,0 +1,208 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	ModelImageAccessModePublic  ModelImageAccessMode = "public"
+	ModelImageAccessModePrivate ModelImageAccessMode = "private"
+)
+
+// ResourceSpec describes the resource requirement of running the workload.
+// If the number of nodes in the cluster that meet the InstanceType and
+// LabelSelector requirements is small than the Count, controller
+// will provision new nodes before deploying the workload.
+// The final list of nodes used to run the workload is presented in workspace Status.
+type ResourceSpec struct {
+	// Count is the required number of GPU nodes.
+	// +optional
+	// +kubebuilder:default:=1
+	Count *int `json:"count,omitempty"`
+
+	// InstanceType specifies the GPU node SKU.
+	// This field defaults to "Standard_NC24ads_A100_v4" if not specified.
+	// +optional
+	// +kubebuilder:default:="Standard_NC24ads_A100_v4"
+	InstanceType string `json:"instanceType,omitempty"`
+
+	// LabelSelector specifies the required labels for the GPU nodes.
+	LabelSelector *metav1.LabelSelector `json:"labelSelector"`
+
+	// PreferredNodes is an optional node list specified by the user.
+	// If a node in the list does not have the required labels, it will be ignored.
+	// +optional
+	PreferredNodes []string `json:"preferredNodes,omitempty"`
+}
+
+type ModelName string
+
+// +kubebuilder:validation:Enum=public;private
+type ModelImageAccessMode string
+
+type PresetMeta struct {
+	// Name of the supported models with preset configurations.
+	Name ModelName `json:"name"`
+	// AccessMode specifies whether the containerized model image is accessible via public registry
+	// or private registry. This field defaults to "public" if not specified.
+	// If this field is "private", user needs to provide the private image information in PresetOptions.
+	// +kubebuilder:default:="public"
+	// +optional
+	AccessMode ModelImageAccessMode `json:"accessMode,omitempty"`
+}
+
+type PresetOptions struct {
+	// Image is the name of the containerized model image.
+	// +optional
+	Image string `json:"image,omitempty"`
+	// ImagePullSecrets is a list of secret names in the same namespace used for pulling the model image.
+	// +optional
+	ImagePullSecrets []string `json:"imagePullSecrets,omitempty"`
+}
+
+// PresetSpec provides the information for rendering preset configurations to run the model inference service.
+type PresetSpec struct {
+	PresetMeta `json:",inline"`
+	// +optional
+	PresetOptions `json:"presetOptions,omitempty"`
+}
+
+type InferenceSpec struct {
+	// Preset describes the base model that will be deployed with preset configurations.
+	// +optional
+	Preset *PresetSpec `json:"preset,omitempty"`
+	// Template specifies the Pod template used to run the inference service. Users can specify custom Pod settings
+	// if the preset configurations cannot meet the requirements. Note that if Preset is specified, Template should not
+	// be specified and vice versa.
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// +kubebuilder:validation:Schemaless
+	// +optional
+	Template *v1.PodTemplateSpec `json:"template,omitempty"`
+	// Config specifies the name of a custom ConfigMap that contains inference arguments.
+	// If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+	// +optional
+	Config string `json:"config,omitempty"`
+	// Adapters are integrated into the base model for inference.
+	// Users can specify multiple adapters for the model and the respective weight of using each of them.
+	// +optional
+	Adapters []AdapterSpec `json:"adapters,omitempty"`
+}
+
+type AdapterSpec struct {
+	// Source describes where to obtain the adapter data.
+	// +optional
+	Source *DataSource `json:"source,omitempty"`
+	// Strength specifies the default multiplier for applying the adapter weights to the raw model weights.
+	// It is usually a float number between 0 and 1. It is defined as a string type to be language agnostic.
+	// +optional
+	Strength *string `json:"strength,omitempty"`
+}
+
+type DataSource struct {
+	// The name of the dataset. The same name will be used as a container name.
+	// It must be a valid DNS subdomain value,
+	Name string `json:"name,omitempty"`
+	// URLs specifies the links to the public data sources. E.g., files in a public github repository.
+	// +optional
+	URLs []string `json:"urls,omitempty"`
+	// The mounted volume that contains the data.
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// +kubebuilder:validation:Schemaless
+	// +optional
+	Volume *v1.VolumeSource `json:"volumeSource,omitempty"`
+	// The name of the image that contains the source data. The assumption is that the source data locates in the
+	// `data` directory in the image.
+	// +optional
+	Image string `json:"image,omitempty"`
+	// ImagePullSecrets is a list of secret names in the same namespace used for pulling the data image.
+	// +optional
+	ImagePullSecrets []string `json:"imagePullSecrets,omitempty"`
+}
+
+type DataDestination struct {
+	// The mounted volume that is used to save the output data.
+	// +kubebuilder:pruning:PreserveUnknownFields
+	// +kubebuilder:validation:Schemaless
+	// +optional
+	Volume *v1.VolumeSource `json:"volumeSource,omitempty"`
+	// Name of the image where the output data is pushed to.
+	// +optional
+	Image string `json:"image,omitempty"`
+	// ImagePushSecret is the name of the secret in the same namespace that contains the authentication
+	// information that is needed for running `docker push`.
+	// +optional
+	ImagePushSecret string `json:"imagePushSecret,omitempty"`
+}
+
+type TuningMethod string
+
+const (
+	TuningMethodLora  TuningMethod = "lora"
+	TuningMethodQLora TuningMethod = "qlora"
+)
+
+type TuningSpec struct {
+	// Preset describes which model to load for tuning.
+	// +optional
+	Preset *PresetSpec `json:"preset,omitempty"`
+	// Method specifies the Parameter-Efficient Fine-Tuning(PEFT) method, such as lora, qlora, used for the tuning.
+	// +optional
+	Method TuningMethod `json:"method,omitempty"`
+	// Config specifies the name of a custom ConfigMap that contains tuning arguments.
+	// If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+	// If not specified, a default Config is used based on the specified tuning method.
+	// +optional
+	Config string `json:"config,omitempty"`
+	// Input describes the input used by the tuning method.
+	Input *DataSource `json:"input"`
+	// Output specified where to store the tuning output.
+	Output *DataDestination `json:"output"`
+}
+
+// WorkspaceStatus defines the observed state of Workspace
+type WorkspaceStatus struct {
+	// WorkerNodes is the list of nodes chosen to run the workload based on the workspace resource requirement.
+	// +optional
+	WorkerNodes []string `json:"workerNodes,omitempty"`
+
+	// Conditions report the current conditions of the workspace.
+	// +optional
+	Conditions []metav1.Condition `json:"conditions,omitempty"`
+}
+
+// Workspace is the Schema for the workspaces API
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:path=workspaces,scope=Namespaced,categories=workspace,shortName={wk,wks}
+// +kubebuilder:storageversion
+// +kubebuilder:printcolumn:name="Instance",type="string",JSONPath=".resource.instanceType",description=""
+// +kubebuilder:printcolumn:name="ResourceReady",type="string",JSONPath=".status.conditions[?(@.type==\"ResourceReady\")].status",description=""
+// +kubebuilder:printcolumn:name="InferenceReady",type="string",JSONPath=".status.conditions[?(@.type==\"InferenceReady\")].status",description=""
+// +kubebuilder:printcolumn:name="JobStarted",type="string",JSONPath=".status.conditions[?(@.type==\"JobStarted\")].status",description=""
+// +kubebuilder:printcolumn:name="WorkspaceSucceeded",type="string",JSONPath=".status.conditions[?(@.type==\"WorkspaceSucceeded\")].status",description=""
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
+type Workspace struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Resource  ResourceSpec    `json:"resource,omitempty"`
+	Inference *InferenceSpec  `json:"inference,omitempty"`
+	Tuning    *TuningSpec     `json:"tuning,omitempty"`
+	Status    WorkspaceStatus `json:"status,omitempty"`
+}
+
+// WorkspaceList contains a list of Workspace
+// +kubebuilder:object:root=true
+type WorkspaceList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []Workspace `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&Workspace{}, &WorkspaceList{})
+}
diff --git a/api/v1beta1/workspace_validation.go b/api/v1beta1/workspace_validation.go
new file mode 100644
index 0000000..31ff76d
--- /dev/null
+++ b/api/v1beta1/workspace_validation.go
@@ -0,0 +1,526 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"reflect"
+	"strconv"
+	"strings"
+
+	"github.com/distribution/reference"
+	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/validation"
+	"k8s.io/klog/v2"
+	"knative.dev/pkg/apis"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/kaito-project/kaito/pkg/k8sclient"
+	"github.com/kaito-project/kaito/pkg/utils"
+	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/utils/plugin"
+)
+
+const (
+	N_SERIES_PREFIX = "Standard_N"
+	D_SERIES_PREFIX = "Standard_D"
+
+	DefaultLoraConfigMapTemplate   = "lora-params-template"
+	DefaultQloraConfigMapTemplate  = "qlora-params-template"
+	DefaultInferenceConfigTemplate = "inference-params-template"
+	MaxAdaptersNumber              = 10
+)
+
+func (w *Workspace) SupportedVerbs() []admissionregistrationv1.OperationType {
+	return []admissionregistrationv1.OperationType{
+		admissionregistrationv1.Create,
+		admissionregistrationv1.Update,
+	}
+}
+
+func (w *Workspace) Validate(ctx context.Context) (errs *apis.FieldError) {
+	errmsgs := validation.IsDNS1123Label(w.Name)
+	if len(errmsgs) > 0 {
+		errs = errs.Also(apis.ErrInvalidValue(strings.Join(errmsgs, ", "), "name"))
+	}
+	base := apis.GetBaseline(ctx)
+	if base == nil {
+		klog.InfoS("Validate creation", "workspace", fmt.Sprintf("%s/%s", w.Namespace, w.Name))
+		errs = errs.Also(w.validateCreate().ViaField("spec"))
+		if w.Inference != nil {
+			// Check if the bypass resource checks annotation is set
+			bypassResourceChecks := false
+			if w.GetAnnotations() != nil {
+				if _, exists := w.GetAnnotations()[AnnotationBypassResourceChecks]; exists {
+					bypassResourceChecks = true
+				}
+			}
+
+			// TODO: Add Adapter Spec Validation - Including DataSource Validation for Adapter
+			errs = errs.Also(w.Resource.validateCreateWithInference(w.Inference, bypassResourceChecks).ViaField("resource"),
+				w.Inference.validateCreate(ctx, w.Namespace).ViaField("inference"))
+		}
+		if w.Tuning != nil {
+			// TODO: Add validate resource based on Tuning Spec
+			errs = errs.Also(w.Resource.validateCreateWithTuning(w.Tuning).ViaField("resource"),
+				w.Tuning.validateCreate(ctx, w.Namespace).ViaField("tuning"))
+		}
+	} else {
+		klog.InfoS("Validate update", "workspace", fmt.Sprintf("%s/%s", w.Namespace, w.Name))
+		old := base.(*Workspace)
+		errs = errs.Also(
+			w.validateUpdate(old).ViaField("spec"),
+			w.Resource.validateUpdate(&old.Resource).ViaField("resource"),
+		)
+		if w.Inference != nil {
+			errs = errs.Also(w.Inference.validateUpdate(old.Inference).ViaField("inference"))
+		}
+		if w.Tuning != nil {
+			errs = errs.Also(w.Tuning.validateUpdate(old.Tuning).ViaField("tuning"))
+		}
+	}
+	return errs
+}
+
+func (w *Workspace) validateCreate() (errs *apis.FieldError) {
+	if w.Inference == nil && w.Tuning == nil {
+		errs = errs.Also(apis.ErrGeneric("Either Inference or Tuning must be specified, not neither", ""))
+	}
+	if w.Inference != nil && w.Tuning != nil {
+		errs = errs.Also(apis.ErrGeneric("Either Inference or Tuning must be specified, but not both", ""))
+	}
+	return errs
+}
+
+func (w *Workspace) validateUpdate(old *Workspace) (errs *apis.FieldError) {
+	if (old.Inference == nil && w.Inference != nil) || (old.Inference != nil && w.Inference == nil) {
+		errs = errs.Also(apis.ErrGeneric("Inference field cannot be toggled once set", "inference"))
+	}
+
+	if (old.Tuning == nil && w.Tuning != nil) || (old.Tuning != nil && w.Tuning == nil) {
+		errs = errs.Also(apis.ErrGeneric("Tuning field cannot be toggled once set", "tuning"))
+	}
+	return errs
+}
+
+func (r *AdapterSpec) validateCreateorUpdate() (errs *apis.FieldError) {
+	if r.Source == nil {
+		errs = errs.Also(apis.ErrMissingField("Source"))
+	} else {
+		errs = errs.Also(r.Source.validateCreate().ViaField("Adapters"))
+
+		if r.Source.Name == "" {
+			errs = errs.Also(apis.ErrMissingField("Name of Adapter field must be specified"))
+		} else if errmsgs := validation.IsDNS1123Subdomain(r.Source.Name); len(errmsgs) > 0 {
+			errs = errs.Also(apis.ErrInvalidValue(strings.Join(errmsgs, ", "), "adapters.source.name"))
+		}
+		if r.Source.Image == "" {
+			errs = errs.Also(apis.ErrMissingField("Image of Adapter field must be specified"))
+		}
+		if r.Strength == nil {
+			var defaultStrength = "1.0"
+			r.Strength = &defaultStrength
+		}
+		strength, err := strconv.ParseFloat(*r.Strength, 64)
+		if err != nil {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Invalid strength value for Adapter '%s': %v", r.Source.Name, err), "adapter"))
+		}
+		if strength < 0 || strength > 1.0 {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Strength value for Adapter '%s' must be between 0 and 1", r.Source.Name), "adapter"))
+		}
+
+	}
+	return errs
+}
+
+func (r *TuningSpec) validateCreate(ctx context.Context, workspaceNamespace string) (errs *apis.FieldError) {
+	methodLowerCase := strings.ToLower(string(r.Method))
+	if methodLowerCase != string(TuningMethodLora) && methodLowerCase != string(TuningMethodQLora) {
+		errs = errs.Also(apis.ErrInvalidValue(r.Method, "Method"))
+	}
+	if r.Config == "" {
+		klog.InfoS("Tuning config not specified. Using default based on method.")
+		releaseNamespace, err := utils.GetReleaseNamespace()
+		if err != nil {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to determine release namespace: %v", err), "namespace"))
+		}
+		defaultConfigMapTemplateName := ""
+		if methodLowerCase == string(TuningMethodLora) {
+			defaultConfigMapTemplateName = DefaultLoraConfigMapTemplate
+		} else if methodLowerCase == string(TuningMethodQLora) {
+			defaultConfigMapTemplateName = DefaultQloraConfigMapTemplate
+		}
+		if err := r.validateConfigMap(ctx, releaseNamespace, methodLowerCase, defaultConfigMapTemplateName); err != nil {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to evaluate validateConfigMap: %v", err), "Config"))
+		}
+	} else {
+		if err := r.validateConfigMap(ctx, workspaceNamespace, methodLowerCase, r.Config); err != nil {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to evaluate validateConfigMap: %v", err), "Config"))
+		}
+	}
+	if r.Input == nil {
+		errs = errs.Also(apis.ErrMissingField("Input"))
+	} else {
+		errs = errs.Also(r.Input.validateCreate().ViaField("Input"))
+	}
+	if r.Output == nil {
+		errs = errs.Also(apis.ErrMissingField("Output"))
+	} else {
+		errs = errs.Also(r.Output.validateCreate().ViaField("Output"))
+	}
+	// Currently require a preset to specified, in future we can consider defining a template
+	if r.Preset == nil {
+		errs = errs.Also(apis.ErrMissingField("Preset"))
+	} else if presetName := string(r.Preset.Name); !plugin.IsValidPreset(presetName) {
+		errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unsupported tuning preset name %s", presetName), "presetName"))
+	}
+	return errs
+}
+
+func (r *TuningSpec) validateUpdate(old *TuningSpec) (errs *apis.FieldError) {
+	if r.Input == nil {
+		errs = errs.Also(apis.ErrMissingField("Input"))
+	} else {
+		errs = errs.Also(r.Input.validateUpdate(old.Input, true).ViaField("Input"))
+	}
+	if r.Output == nil {
+		errs = errs.Also(apis.ErrMissingField("Output"))
+	} else {
+		errs = errs.Also(r.Output.validateUpdate().ViaField("Output"))
+	}
+	if !reflect.DeepEqual(old.Preset, r.Preset) {
+		errs = errs.Also(apis.ErrGeneric("Preset cannot be changed", "Preset"))
+	}
+	oldMethod, newMethod := strings.ToLower(string(old.Method)), strings.ToLower(string(r.Method))
+	if !reflect.DeepEqual(oldMethod, newMethod) {
+		errs = errs.Also(apis.ErrGeneric("Method cannot be changed", "Method"))
+	}
+	// Consider supporting config fields changing
+	return errs
+}
+
+func (r *DataSource) validateCreate() (errs *apis.FieldError) {
+	sourcesSpecified := 0
+	if len(r.URLs) > 0 {
+		sourcesSpecified++
+	}
+	if r.Volume != nil {
+		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
+		sourcesSpecified++
+	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse input image reference: %s", err), "Image"))
+		}
+
+		sourcesSpecified++
+	}
+
+	// Ensure exactly one of URLs, Volume, or Image is specified
+	if sourcesSpecified != 1 {
+		errs = errs.Also(apis.ErrGeneric("Exactly one of URLs, Volume, or Image must be specified", "URLs", "Volume", "Image"))
+	}
+
+	return errs
+}
+
+func (r *DataSource) validateUpdate(old *DataSource, isTuning bool) (errs *apis.FieldError) {
+	if isTuning && !reflect.DeepEqual(old.Name, r.Name) {
+		errs = errs.Also(apis.ErrInvalidValue("During tuning Name field cannot be changed once set", "Name"))
+	}
+	if r.Volume != nil {
+		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
+	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse input image reference: %s", err), "Image"))
+		}
+	}
+
+	return errs
+}
+
+func (r *DataDestination) validateCreate() (errs *apis.FieldError) {
+	destinationsSpecified := 0
+	// TODO: Implement Volumes
+	if r.Volume != nil {
+		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
+		destinationsSpecified++
+	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse output image reference: %s", err), "Image"))
+		}
+
+		// Cloud Provider requires credentials to push image
+		if r.ImagePushSecret == "" {
+			errs = errs.Also(apis.ErrMissingField("Must specify imagePushSecret with destination image"))
+		}
+
+		destinationsSpecified++
+	}
+
+	// If no destination is specified, return an error
+	if destinationsSpecified == 0 {
+		errs = errs.Also(apis.ErrMissingField("At least one of Volume or Image must be specified"))
+	}
+	return errs
+}
+
+func (r *DataDestination) validateUpdate() (errs *apis.FieldError) {
+	// TODO: Implement Volumes
+	if r.Volume != nil {
+		errs = errs.Also(apis.ErrInvalidValue("Volume support is not implemented yet", "Volume"))
+	}
+	if image := r.Image; image != "" {
+		if _, err := reference.ParseDockerRef(image); err != nil {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unable to parse output image reference: %s", err), "Image"))
+		}
+	}
+
+	return errs
+}
+
+func (r *ResourceSpec) validateCreateWithTuning(tuning *TuningSpec) (errs *apis.FieldError) {
+	if *r.Count > 1 {
+		errs = errs.Also(apis.ErrInvalidValue("Tuning does not currently support multinode configurations. Please set the node count to 1. Future support with DeepSpeed will allow this.", "count"))
+	}
+	return errs
+}
+
+func (r *ResourceSpec) validateCreateWithInference(inference *InferenceSpec, bypassResourceChecks bool) (errs *apis.FieldError) {
+	var presetName string
+	if inference.Preset != nil {
+		presetName = strings.ToLower(string(inference.Preset.Name))
+		// Since inference.Preset exists, we must validate preset name.
+		if !plugin.IsValidPreset(presetName) {
+			// Return to skip the rest of checks, the Inference spec validation will return proper err msg.
+			return errs
+		}
+	}
+	instanceType := string(r.InstanceType)
+
+	skuHandler, err := utils.GetSKUHandler()
+	if err != nil {
+		errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to get SKU handler: %v", err), "instanceType"))
+		return errs
+	}
+	gpuConfigs := skuHandler.GetGPUConfigs()
+
+	// Check if instancetype exists in our SKUs map for the particular cloud provider
+	if skuConfig, exists := gpuConfigs[instanceType]; exists {
+		if presetName != "" {
+			model := plugin.KaitoModelRegister.MustGet(presetName) // InferenceSpec has been validated so the name is valid.
+
+			machineCount := *r.Count
+			machineTotalNumGPUs := resource.NewQuantity(int64(machineCount*skuConfig.GPUCount), resource.DecimalSI)
+			machinePerGPUMemory := resource.NewQuantity(int64(skuConfig.GPUMem/skuConfig.GPUCount)*consts.GiBToBytes, resource.BinarySI) // Ensure it's per GPU
+			machineTotalGPUMem := resource.NewQuantity(int64(machineCount*skuConfig.GPUMem)*consts.GiBToBytes, resource.BinarySI)        // Total GPU memory
+
+			modelGPUCount := resource.MustParse(model.GetInferenceParameters().GPUCountRequirement)
+			modelPerGPUMemory := resource.MustParse(model.GetInferenceParameters().PerGPUMemoryRequirement)
+			modelTotalGPUMemory := resource.MustParse(model.GetInferenceParameters().TotalGPUMemoryRequirement)
+
+			// Separate the checks for specific error messages
+			if machineTotalNumGPUs.Cmp(modelGPUCount) < 0 {
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient number of GPUs detected but continuing due to bypass flag. Instance type %s provides %s, but preset %s requires at least %d",
+						instanceType, machineTotalNumGPUs.String(), presetName, modelGPUCount.Value())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient number of GPUs: Instance type %s provides %s, but preset %s requires at least %d",
+							instanceType,
+							machineTotalNumGPUs.String(),
+							presetName,
+							modelGPUCount.Value(),
+						),
+						"instanceType",
+					))
+				}
+			}
+
+			if machinePerGPUMemory.Cmp(modelPerGPUMemory) < 0 {
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient per GPU memory but continuing due to bypass flag. Instance type %s provides %s per GPU, but preset %s requires at least %s per GPU",
+						instanceType, machinePerGPUMemory.String(), presetName, modelPerGPUMemory.String())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient per GPU memory: Instance type %s provides %s per GPU, but preset %s requires at least %s per GPU",
+							instanceType,
+							machinePerGPUMemory.String(),
+							presetName,
+							modelPerGPUMemory.String(),
+						),
+						"instanceType",
+					))
+				}
+			}
+
+			if machineTotalGPUMem.Cmp(modelTotalGPUMemory) < 0 {
+				if bypassResourceChecks {
+					klog.Warningf("Bypassing resource check: Insufficient total GPU memory detected but continuing due to bypass flag. Instance type %s has a total of %s, but preset %s requires at least %s",
+						instanceType, machineTotalGPUMem.String(), presetName, modelTotalGPUMemory.String())
+				} else {
+					errs = errs.Also(apis.ErrInvalidValue(
+						fmt.Sprintf(
+							"Insufficient total GPU memory: Instance type %s has a total of %s, but preset %s requires at least %s",
+							instanceType,
+							machineTotalGPUMem.String(),
+							presetName,
+							modelTotalGPUMemory.String(),
+						),
+						"instanceType",
+					))
+				}
+			}
+		}
+	} else {
+		provider := os.Getenv("CLOUD_PROVIDER")
+		// Check for other instance types pattern matches if cloud provider is Azure
+		if provider != consts.AzureCloudName || (!strings.HasPrefix(instanceType, N_SERIES_PREFIX) && !strings.HasPrefix(instanceType, D_SERIES_PREFIX)) {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unsupported instance type %s. Supported SKUs: %s", instanceType, skuHandler.GetSupportedSKUs()), "instanceType"))
+		}
+	}
+
+	// Validate labelSelector
+	if _, err := metav1.LabelSelectorAsMap(r.LabelSelector); err != nil {
+		errs = errs.Also(apis.ErrInvalidValue(err.Error(), "labelSelector"))
+	}
+
+	return errs
+}
+
+func (r *ResourceSpec) validateUpdate(old *ResourceSpec) (errs *apis.FieldError) {
+	// We disable changing node count for now.
+	if r.Count != nil && old.Count != nil && *r.Count != *old.Count {
+		errs = errs.Also(apis.ErrGeneric("field is immutable", "count"))
+	}
+	if r.InstanceType != old.InstanceType {
+		errs = errs.Also(apis.ErrGeneric("field is immutable", "instanceType"))
+	}
+	newLabels, err0 := metav1.LabelSelectorAsMap(r.LabelSelector)
+	oldLabels, err1 := metav1.LabelSelectorAsMap(old.LabelSelector)
+	if err0 != nil || err1 != nil {
+		errs = errs.Also(apis.ErrGeneric("Only allow matchLabels or 'IN' matchExpression", "labelSelector"))
+	} else {
+		if !reflect.DeepEqual(newLabels, oldLabels) {
+			errs = errs.Also(apis.ErrGeneric("field is immutable", "labelSelector"))
+		}
+	}
+	return errs
+}
+
+func (i *InferenceSpec) validateCreate(ctx context.Context, namespace string) (errs *apis.FieldError) {
+	// Check if both Preset and Template are not set
+	if i.Preset == nil && i.Template == nil {
+		errs = errs.Also(apis.ErrMissingField("Preset or Template must be specified"))
+	}
+
+	// Check if both Preset and Template are set at the same time
+	if i.Preset != nil && i.Template != nil {
+		errs = errs.Also(apis.ErrGeneric("Preset and Template cannot be set at the same time"))
+	}
+
+	if i.Preset != nil {
+		presetName := string(i.Preset.Name)
+		// Validate preset name
+		if !plugin.IsValidPreset(presetName) {
+			errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("Unsupported inference preset name %s", presetName), "presetName"))
+			// Need to return here. Otherwise, a panic will be hit when doing following checks.
+			return errs
+		}
+		// Validate private preset has private image specified
+		if plugin.KaitoModelRegister.MustGet(string(i.Preset.Name)).GetInferenceParameters().ImageAccessMode == string(ModelImageAccessModePrivate) &&
+			i.Preset.PresetMeta.AccessMode != ModelImageAccessModePrivate {
+			errs = errs.Also(apis.ErrGeneric("This preset only supports private AccessMode, AccessMode must be private to continue"))
+		}
+		// Additional validations for Preset
+		if i.Preset.PresetMeta.AccessMode == ModelImageAccessModePrivate && i.Preset.PresetOptions.Image == "" {
+			errs = errs.Also(apis.ErrGeneric("When AccessMode is private, an image must be provided in PresetOptions"))
+		}
+		// Note: we don't enforce private access mode to have image secrets, in case anonymous pulling is enabled
+	}
+	if len(i.Adapters) > MaxAdaptersNumber {
+		errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Number of Adapters exceeds the maximum limit, maximum of %s allowed", strconv.Itoa(MaxAdaptersNumber))))
+	}
+
+	// check if adapter names are duplicate
+	if len(i.Adapters) > 0 {
+		nameMap := make(map[string]bool)
+		errs = errs.Also(validateDuplicateName(i.Adapters, nameMap))
+	}
+
+	if i.Config != "" {
+		errs = errs.Also(i.validateConfigMap(ctx, namespace))
+	}
+
+	return errs
+}
+
+func (i *InferenceSpec) validateConfigMap(ctx context.Context, namespace string) (errs *apis.FieldError) {
+	var cm corev1.ConfigMap
+	if k8sclient.Client == nil {
+		errs = errs.Also(apis.ErrGeneric("Failed to obtain client from context.Context"))
+		return errs
+	}
+	err := k8sclient.Client.Get(ctx, client.ObjectKey{Name: i.Config, Namespace: namespace}, &cm)
+	if err != nil {
+		if errors.IsNotFound(err) {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("ConfigMap '%s' specified in 'config' not found in namespace '%s'", i.Config, namespace), "config"))
+		} else {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to get ConfigMap '%s' in namespace '%s': %v", i.Config, namespace, err), "config"))
+		}
+		return errs
+	}
+
+	// basic check here, it's hard to validate the content of the configmap in controller
+	_, ok := cm.Data["inference_config.yaml"]
+	if !ok {
+		return apis.ErrMissingField("inference_config.yaml in ConfigMap")
+	}
+
+	return errs
+}
+
+func (i *InferenceSpec) validateUpdate(old *InferenceSpec) (errs *apis.FieldError) {
+	if !reflect.DeepEqual(i.Preset, old.Preset) {
+		errs = errs.Also(apis.ErrGeneric("field is immutable", "preset"))
+	}
+	// inference.template can be changed, but cannot be set/unset.
+	if (i.Template != nil && old.Template == nil) || (i.Template == nil && old.Template != nil) {
+		errs = errs.Also(apis.ErrGeneric("field cannot be unset/set if it was set/unset", "template"))
+	}
+
+	// check if adapter names are duplicate
+	for _, adapter := range i.Adapters {
+		errs = errs.Also(adapter.validateCreateorUpdate())
+	}
+
+	// check if adapter names are duplicate
+
+	if len(i.Adapters) > 0 {
+		nameMap := make(map[string]bool)
+		errs = errs.Also(validateDuplicateName(i.Adapters, nameMap))
+	}
+	return errs
+}
+
+func validateDuplicateName(adapters []AdapterSpec, nameMap map[string]bool) (errs *apis.FieldError) {
+	for _, adapter := range adapters {
+		if _, ok := nameMap[adapter.Source.Name]; ok {
+			errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Duplicate adapter source name found: %s", adapter.Source.Name)))
+		} else {
+			nameMap[adapter.Source.Name] = true
+		}
+	}
+	return errs
+}
diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go
new file mode 100644
index 0000000..1d199aa
--- /dev/null
+++ b/api/v1beta1/zz_generated.deepcopy.go
@@ -0,0 +1,402 @@
+//go:build !ignore_autogenerated
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1beta1
+
+import (
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdapterSpec) DeepCopyInto(out *AdapterSpec) {
+	*out = *in
+	if in.Source != nil {
+		in, out := &in.Source, &out.Source
+		*out = new(DataSource)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Strength != nil {
+		in, out := &in.Strength, &out.Strength
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdapterSpec.
+func (in *AdapterSpec) DeepCopy() *AdapterSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(AdapterSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Config) DeepCopyInto(out *Config) {
+	*out = *in
+	in.TrainingConfig.DeepCopyInto(&out.TrainingConfig)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Config.
+func (in *Config) DeepCopy() *Config {
+	if in == nil {
+		return nil
+	}
+	out := new(Config)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataDestination) DeepCopyInto(out *DataDestination) {
+	*out = *in
+	if in.Volume != nil {
+		in, out := &in.Volume, &out.Volume
+		*out = new(corev1.VolumeSource)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataDestination.
+func (in *DataDestination) DeepCopy() *DataDestination {
+	if in == nil {
+		return nil
+	}
+	out := new(DataDestination)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *DataSource) DeepCopyInto(out *DataSource) {
+	*out = *in
+	if in.URLs != nil {
+		in, out := &in.URLs, &out.URLs
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Volume != nil {
+		in, out := &in.Volume, &out.Volume
+		*out = new(corev1.VolumeSource)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.ImagePullSecrets != nil {
+		in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource.
+func (in *DataSource) DeepCopy() *DataSource {
+	if in == nil {
+		return nil
+	}
+	out := new(DataSource)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *InferenceSpec) DeepCopyInto(out *InferenceSpec) {
+	*out = *in
+	if in.Preset != nil {
+		in, out := &in.Preset, &out.Preset
+		*out = new(PresetSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Template != nil {
+		in, out := &in.Template, &out.Template
+		*out = new(corev1.PodTemplateSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Adapters != nil {
+		in, out := &in.Adapters, &out.Adapters
+		*out = make([]AdapterSpec, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InferenceSpec.
+func (in *InferenceSpec) DeepCopy() *InferenceSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(InferenceSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PresetMeta) DeepCopyInto(out *PresetMeta) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PresetMeta.
+func (in *PresetMeta) DeepCopy() *PresetMeta {
+	if in == nil {
+		return nil
+	}
+	out := new(PresetMeta)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PresetOptions) DeepCopyInto(out *PresetOptions) {
+	*out = *in
+	if in.ImagePullSecrets != nil {
+		in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PresetOptions.
+func (in *PresetOptions) DeepCopy() *PresetOptions {
+	if in == nil {
+		return nil
+	}
+	out := new(PresetOptions)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PresetSpec) DeepCopyInto(out *PresetSpec) {
+	*out = *in
+	out.PresetMeta = in.PresetMeta
+	in.PresetOptions.DeepCopyInto(&out.PresetOptions)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PresetSpec.
+func (in *PresetSpec) DeepCopy() *PresetSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PresetSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) {
+	*out = *in
+	if in.Count != nil {
+		in, out := &in.Count, &out.Count
+		*out = new(int)
+		**out = **in
+	}
+	if in.LabelSelector != nil {
+		in, out := &in.LabelSelector, &out.LabelSelector
+		*out = new(v1.LabelSelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.PreferredNodes != nil {
+		in, out := &in.PreferredNodes, &out.PreferredNodes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSpec.
+func (in *ResourceSpec) DeepCopy() *ResourceSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(ResourceSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TrainingConfig) DeepCopyInto(out *TrainingConfig) {
+	*out = *in
+	if in.ModelConfig != nil {
+		in, out := &in.ModelConfig, &out.ModelConfig
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.QuantizationConfig != nil {
+		in, out := &in.QuantizationConfig, &out.QuantizationConfig
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.LoraConfig != nil {
+		in, out := &in.LoraConfig, &out.LoraConfig
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.TrainingArguments != nil {
+		in, out := &in.TrainingArguments, &out.TrainingArguments
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.DatasetConfig != nil {
+		in, out := &in.DatasetConfig, &out.DatasetConfig
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+	if in.DataCollator != nil {
+		in, out := &in.DataCollator, &out.DataCollator
+		*out = make(map[string]runtime.RawExtension, len(*in))
+		for key, val := range *in {
+			(*out)[key] = *val.DeepCopy()
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrainingConfig.
+func (in *TrainingConfig) DeepCopy() *TrainingConfig {
+	if in == nil {
+		return nil
+	}
+	out := new(TrainingConfig)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *TuningSpec) DeepCopyInto(out *TuningSpec) {
+	*out = *in
+	if in.Preset != nil {
+		in, out := &in.Preset, &out.Preset
+		*out = new(PresetSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Input != nil {
+		in, out := &in.Input, &out.Input
+		*out = new(DataSource)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Output != nil {
+		in, out := &in.Output, &out.Output
+		*out = new(DataDestination)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TuningSpec.
+func (in *TuningSpec) DeepCopy() *TuningSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(TuningSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Workspace) DeepCopyInto(out *Workspace) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Resource.DeepCopyInto(&out.Resource)
+	if in.Inference != nil {
+		in, out := &in.Inference, &out.Inference
+		*out = new(InferenceSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Tuning != nil {
+		in, out := &in.Tuning, &out.Tuning
+		*out = new(TuningSpec)
+		(*in).DeepCopyInto(*out)
+	}
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workspace.
+func (in *Workspace) DeepCopy() *Workspace {
+	if in == nil {
+		return nil
+	}
+	out := new(Workspace)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Workspace) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkspaceList) DeepCopyInto(out *WorkspaceList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Workspace, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceList.
+func (in *WorkspaceList) DeepCopy() *WorkspaceList {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkspaceList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *WorkspaceList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkspaceStatus) DeepCopyInto(out *WorkspaceStatus) {
+	*out = *in
+	if in.WorkerNodes != nil {
+		in, out := &in.WorkerNodes, &out.WorkerNodes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]v1.Condition, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkspaceStatus.
+func (in *WorkspaceStatus) DeepCopy() *WorkspaceStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkspaceStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/charts/kaito/ragengine/values.yaml b/charts/kaito/ragengine/values.yaml
index 0baf443..db0e23c 100644
--- a/charts/kaito/ragengine/values.yaml
+++ b/charts/kaito/ragengine/values.yaml
@@ -1,7 +1,6 @@
 # Default values for kaito.
 # This is a YAML-formatted file.
 # Declare variables to be passed into your templates.
-fullnameOverride: ragengine
 replicaCount: 1
 image:
   repository: mcr.microsoft.com/aks/kaito/ragengine
diff --git a/charts/kaito/workspace/Chart.yaml b/charts/kaito/workspace/Chart.yaml
index e1c0b38..80fa813 100644
--- a/charts/kaito/workspace/Chart.yaml
+++ b/charts/kaito/workspace/Chart.yaml
@@ -6,13 +6,13 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.4.2
+version: 0.4.4
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
 # follow Semantic Versioning. They should reflect the version the application is using.
 # It is recommended to use it with quotes.
-appVersion: 0.4.2
+appVersion: 0.4.4
 home: https://github.com/kaito-project/kaito
 sources:
   - https://github.com/kaito-project/kaito
diff --git a/charts/kaito/workspace/README.md b/charts/kaito/workspace/README.md
index 9ecb44f..9b4282e 100644
--- a/charts/kaito/workspace/README.md
+++ b/charts/kaito/workspace/README.md
@@ -5,7 +5,7 @@
 ```bash
 export REGISTRY=mcr.microsoft.com/aks/kaito
 export IMG_NAME=workspace
-export IMG_TAG=0.4.2
+export IMG_TAG=0.4.4
 helm install workspace ./charts/kaito/workspace  \
 --set image.repository=${REGISTRY}/${IMG_NAME} --set image.tag=${IMG_TAG} \
 --namespace kaito-workspace --create-namespace
diff --git a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
index 9fe21d1..973e178 100644
--- a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
+++ b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
@@ -400,6 +400,390 @@ spec:
             type: object
         type: object
     served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .resource.instanceType
+      name: Instance
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="ResourceReady")].status
+      name: ResourceReady
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="InferenceReady")].status
+      name: InferenceReady
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="JobStarted")].status
+      name: JobStarted
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="WorkspaceSucceeded")].status
+      name: WorkspaceSucceeded
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Workspace is the Schema for the workspaces API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          inference:
+            properties:
+              adapters:
+                description: |-
+                  Adapters are integrated into the base model for inference.
+                  Users can specify multiple adapters for the model and the respective weight of using each of them.
+                items:
+                  properties:
+                    source:
+                      description: Source describes where to obtain the adapter data.
+                      properties:
+                        image:
+                          description: |-
+                            The name of the image that contains the source data. The assumption is that the source data locates in the
+                            `data` directory in the image.
+                          type: string
+                        imagePullSecrets:
+                          description: ImagePullSecrets is a list of secret names
+                            in the same namespace used for pulling the data image.
+                          items:
+                            type: string
+                          type: array
+                        name:
+                          description: |-
+                            The name of the dataset. The same name will be used as a container name.
+                            It must be a valid DNS subdomain value,
+                          type: string
+                        urls:
+                          description: URLs specifies the links to the public data
+                            sources. E.g., files in a public github repository.
+                          items:
+                            type: string
+                          type: array
+                        volumeSource:
+                          description: The mounted volume that contains the data.
+                          x-kubernetes-preserve-unknown-fields: true
+                      type: object
+                    strength:
+                      description: |-
+                        Strength specifies the default multiplier for applying the adapter weights to the raw model weights.
+                        It is usually a float number between 0 and 1. It is defined as a string type to be language agnostic.
+                      type: string
+                  type: object
+                type: array
+              config:
+                description: |-
+                  Config specifies the name of a custom ConfigMap that contains inference arguments.
+                  If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+                type: string
+              preset:
+                description: Preset describes the base model that will be deployed
+                  with preset configurations.
+                properties:
+                  accessMode:
+                    default: public
+                    description: |-
+                      AccessMode specifies whether the containerized model image is accessible via public registry
+                      or private registry. This field defaults to "public" if not specified.
+                      If this field is "private", user needs to provide the private image information in PresetOptions.
+                    enum:
+                    - public
+                    - private
+                    type: string
+                  name:
+                    description: Name of the supported models with preset configurations.
+                    type: string
+                  presetOptions:
+                    properties:
+                      image:
+                        description: Image is the name of the containerized model
+                          image.
+                        type: string
+                      imagePullSecrets:
+                        description: ImagePullSecrets is a list of secret names in
+                          the same namespace used for pulling the model image.
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                required:
+                - name
+                type: object
+              template:
+                description: |-
+                  Template specifies the Pod template used to run the inference service. Users can specify custom Pod settings
+                  if the preset configurations cannot meet the requirements. Note that if Preset is specified, Template should not
+                  be specified and vice versa.
+                x-kubernetes-preserve-unknown-fields: true
+            type: object
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          resource:
+            description: |-
+              ResourceSpec describes the resource requirement of running the workload.
+              If the number of nodes in the cluster that meet the InstanceType and
+              LabelSelector requirements is small than the Count, controller
+              will provision new nodes before deploying the workload.
+              The final list of nodes used to run the workload is presented in workspace Status.
+            properties:
+              count:
+                default: 1
+                description: Count is the required number of GPU nodes.
+                type: integer
+              instanceType:
+                default: Standard_NC24ads_A100_v4
+                description: |-
+                  InstanceType specifies the GPU node SKU.
+                  This field defaults to "Standard_NC24ads_A100_v4" if not specified.
+                type: string
+              labelSelector:
+                description: LabelSelector specifies the required labels for the GPU
+                  nodes.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: |-
+                        A label selector requirement is a selector that contains values, a key, and an operator that
+                        relates the key and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: |-
+                            operator represents a key's relationship to a set of values.
+                            Valid operators are In, NotIn, Exists and DoesNotExist.
+                          type: string
+                        values:
+                          description: |-
+                            values is an array of string values. If the operator is In or NotIn,
+                            the values array must be non-empty. If the operator is Exists or DoesNotExist,
+                            the values array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                          x-kubernetes-list-type: atomic
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                    x-kubernetes-list-type: atomic
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: |-
+                      matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+                      map is equivalent to an element of matchExpressions, whose key field is "key", the
+                      operator is "In", and the values array contains only "value". The requirements are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              preferredNodes:
+                description: |-
+                  PreferredNodes is an optional node list specified by the user.
+                  If a node in the list does not have the required labels, it will be ignored.
+                items:
+                  type: string
+                type: array
+            required:
+            - labelSelector
+            type: object
+          status:
+            description: WorkspaceStatus defines the observed state of Workspace
+            properties:
+              conditions:
+                description: Conditions report the current conditions of the workspace.
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource.\n---\nThis struct is intended for
+                    direct use as an array at the field path .status.conditions.  For
+                    example,\n\n\n\ttype FooStatus struct{\n\t    // Represents the
+                    observations of a foo's current state.\n\t    // Known .status.conditions.type
+                    are: \"Available\", \"Progressing\", and \"Degraded\"\n\t    //
+                    +patchMergeKey=type\n\t    // +patchStrategy=merge\n\t    // +listType=map\n\t
+                    \   // +listMapKey=type\n\t    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+                    \   // other fields\n\t}"
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        ---
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+                        useful (see .node.status.conditions), the ability to deconflict is important.
+                        The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+              workerNodes:
+                description: WorkerNodes is the list of nodes chosen to run the workload
+                  based on the workspace resource requirement.
+                items:
+                  type: string
+                type: array
+            type: object
+          tuning:
+            properties:
+              config:
+                description: |-
+                  Config specifies the name of a custom ConfigMap that contains tuning arguments.
+                  If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+                  If not specified, a default Config is used based on the specified tuning method.
+                type: string
+              input:
+                description: Input describes the input used by the tuning method.
+                properties:
+                  image:
+                    description: |-
+                      The name of the image that contains the source data. The assumption is that the source data locates in the
+                      `data` directory in the image.
+                    type: string
+                  imagePullSecrets:
+                    description: ImagePullSecrets is a list of secret names in the
+                      same namespace used for pulling the data image.
+                    items:
+                      type: string
+                    type: array
+                  name:
+                    description: |-
+                      The name of the dataset. The same name will be used as a container name.
+                      It must be a valid DNS subdomain value,
+                    type: string
+                  urls:
+                    description: URLs specifies the links to the public data sources.
+                      E.g., files in a public github repository.
+                    items:
+                      type: string
+                    type: array
+                  volumeSource:
+                    description: The mounted volume that contains the data.
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
+              method:
+                description: Method specifies the Parameter-Efficient Fine-Tuning(PEFT)
+                  method, such as lora, qlora, used for the tuning.
+                type: string
+              output:
+                description: Output specified where to store the tuning output.
+                properties:
+                  image:
+                    description: Name of the image where the output data is pushed
+                      to.
+                    type: string
+                  imagePushSecret:
+                    description: |-
+                      ImagePushSecret is the name of the secret in the same namespace that contains the authentication
+                      information that is needed for running `docker push`.
+                    type: string
+                  volumeSource:
+                    description: The mounted volume that is used to save the output
+                      data.
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
+              preset:
+                description: Preset describes which model to load for tuning.
+                properties:
+                  accessMode:
+                    default: public
+                    description: |-
+                      AccessMode specifies whether the containerized model image is accessible via public registry
+                      or private registry. This field defaults to "public" if not specified.
+                      If this field is "private", user needs to provide the private image information in PresetOptions.
+                    enum:
+                    - public
+                    - private
+                    type: string
+                  name:
+                    description: Name of the supported models with preset configurations.
+                    type: string
+                  presetOptions:
+                    properties:
+                      image:
+                        description: Image is the name of the containerized model
+                          image.
+                        type: string
+                      imagePullSecrets:
+                        description: ImagePullSecrets is a list of secret names in
+                          the same namespace used for pulling the model image.
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                required:
+                - name
+                type: object
+            required:
+            - input
+            - output
+            type: object
+        type: object
+    served: true
     storage: true
     subresources:
       status: {}
diff --git a/charts/kaito/workspace/templates/deployment.yaml b/charts/kaito/workspace/templates/deployment.yaml
index 4c39375..d8c73dd 100644
--- a/charts/kaito/workspace/templates/deployment.yaml
+++ b/charts/kaito/workspace/templates/deployment.yaml
@@ -36,7 +36,7 @@ spec:
             - --feature-gates={{ include "utils.joinKeyValuePairs" .Values.featureGates }}
           env:
             - name: WEBHOOK_SERVICE
-              value: {{ include "kaito.fullname" . }}
+              value: {{ include "kaito.fullname" . }}-svc
             - name: WEBHOOK_PORT
               value: "{{ .Values.webhook.port }}"
             - name: SYSTEM_NAMESPACE
@@ -80,3 +80,4 @@ spec:
       tolerations:
         {{- toYaml . | nindent 8 }}
       {{- end }}
+      priorityClassName: system-cluster-critical
diff --git a/charts/kaito/workspace/templates/inference-params.yaml b/charts/kaito/workspace/templates/inference-params.yaml
index a7b970e..590307d 100644
--- a/charts/kaito/workspace/templates/inference-params.yaml
+++ b/charts/kaito/workspace/templates/inference-params.yaml
@@ -16,4 +16,5 @@ data:
       # max-seq-len-to-capture: 8192
       # num-scheduler-steps: 1
       # enable-chunked-prefill: false
-      # see https://docs.vllm.ai/en/stable/models/engine_args.html for more options.
\ No newline at end of file
+      # max-model-len: 2048
+      # see https://docs.vllm.ai/en/stable/serving/engine_args.html for more options.
\ No newline at end of file
diff --git a/charts/kaito/workspace/templates/nvidia-device-plugin-ds.yaml b/charts/kaito/workspace/templates/nvidia-device-plugin-ds.yaml
index 312ba65..ac3a481 100644
--- a/charts/kaito/workspace/templates/nvidia-device-plugin-ds.yaml
+++ b/charts/kaito/workspace/templates/nvidia-device-plugin-ds.yaml
@@ -44,9 +44,9 @@ spec:
           operator: "Equal"
           value: "gpu"
           effect: "NoSchedule"
-      priorityClassName: high-priority-nonpreempting
+      priorityClassName: "system-node-critical"
       containers:
-        - image: nvcr.io/nvidia/k8s-device-plugin:v0.16.2
+        - image: mcr.microsoft.com/oss/v2/nvidia/k8s-device-plugin:v0.17.0
           name: nvidia-device-plugin-ctr
           env:
             - name: FAIL_ON_INIT_ERROR
@@ -64,15 +64,3 @@ spec:
         - name: device-plugin
           hostPath:
             path: /var/lib/kubelet/device-plugins
----
-apiVersion: scheduling.k8s.io/v1
-kind: PriorityClass
-metadata:
-  name: high-priority-nonpreempting
-  namespace: {{ .Release.Namespace }}
-  labels:
-    {{- include "kaito.labels" . | nindent 4 }}
-value: 1000000
-preemptionPolicy: Never
-globalDefault: false
-description: "This priority class will not cause other pods to be preempted."
diff --git a/charts/kaito/workspace/templates/service.yaml b/charts/kaito/workspace/templates/service.yaml
index b8cb5de..5ffb12d 100644
--- a/charts/kaito/workspace/templates/service.yaml
+++ b/charts/kaito/workspace/templates/service.yaml
@@ -1,7 +1,7 @@
 apiVersion: v1
 kind: Service
 metadata:
-  name: {{ include "kaito.fullname" . }}
+  name: {{ include "kaito.fullname" . }}-svc
   namespace: {{ .Release.Namespace }}
   labels:
     {{- include "kaito.labels" . | nindent 4 }}
diff --git a/charts/kaito/workspace/templates/webhooks.yaml b/charts/kaito/workspace/templates/webhooks.yaml
index 9b50130..478352a 100644
--- a/charts/kaito/workspace/templates/webhooks.yaml
+++ b/charts/kaito/workspace/templates/webhooks.yaml
@@ -9,7 +9,7 @@ webhooks:
     admissionReviewVersions: ["v1"]
     clientConfig:
       service:
-        name: {{ include "kaito.fullname" . }}
+        name: {{ include "kaito.fullname" . }}-svc
         namespace: {{ .Release.Namespace }}
         port: {{ .Values.webhook.port }}
     failurePolicy: Fail
@@ -19,6 +19,7 @@ webhooks:
           - kaito.sh
         apiVersions:
           - v1alpha1
+          - v1beta1
         resources:
           - workspaces
         operations:
diff --git a/charts/kaito/workspace/values.yaml b/charts/kaito/workspace/values.yaml
index ca2cec6..653f969 100644
--- a/charts/kaito/workspace/values.yaml
+++ b/charts/kaito/workspace/values.yaml
@@ -5,7 +5,7 @@ replicaCount: 1
 image:
   repository: mcr.microsoft.com/aks/kaito/workspace
   pullPolicy: IfNotPresent
-  tag: 0.4.2
+  tag: 0.4.4
 imagePullSecrets: []
 podAnnotations: {}
 podSecurityContext:
@@ -30,6 +30,6 @@ resources:
 nodeSelector: {}
 tolerations: []
 affinity: {}
-# Values can be "azure" or "aws"
+# Values can be "azure" or "aws" or "arc"
 cloudProviderName: "azure"
 clusterName: "kaito"
diff --git a/cmd/workspace/main.go b/cmd/workspace/main.go
index fdb4868..3493fde 100644
--- a/cmd/workspace/main.go
+++ b/cmd/workspace/main.go
@@ -28,10 +28,11 @@ import (
 
 	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
 	// to ensure that exec-entrypoint and run can make use of them.
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/featuregates"
 	"github.com/kaito-project/kaito/pkg/k8sclient"
 	kaitoutils "github.com/kaito-project/kaito/pkg/utils"
+	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/nodeclaim"
 	"github.com/kaito-project/kaito/pkg/workspace/controllers"
 	"github.com/kaito-project/kaito/pkg/workspace/webhooks"
@@ -53,7 +54,7 @@ var (
 
 func init() {
 	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
-	utilruntime.Must(kaitov1alpha1.AddToScheme(scheme))
+	utilruntime.Must(kaitov1beta1.AddToScheme(scheme))
 	utilruntime.Must(kaitoutils.KarpenterSchemeBuilder.AddToScheme(scheme))
 	utilruntime.Must(azurev1alpha2.SchemeBuilder.AddToScheme(scheme))
 	utilruntime.Must(kaitoutils.AwsSchemeBuilder.AddToScheme(scheme))
@@ -161,9 +162,11 @@ func main() {
 		exitWithErrorFunc()
 	}
 
-	err = nodeclaim.CheckNodeClass(ctx, kClient)
-	if err != nil {
-		exitWithErrorFunc()
+	if featuregates.FeatureGates[consts.FeatureFlagEnsureNodeClass] {
+		err := nodeclaim.CheckNodeClass(ctx, kClient)
+		if err != nil {
+			exitWithErrorFunc()
+		}
 	}
 
 	klog.InfoS("starting manager")
diff --git a/cmd/workspace/models.go b/cmd/workspace/models.go
index c272dac..15eef61 100644
--- a/cmd/workspace/models.go
+++ b/cmd/workspace/models.go
@@ -3,6 +3,7 @@
 package main
 
 import (
+	_ "github.com/kaito-project/kaito/presets/workspace/models/deepseek"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/falcon"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/llama2"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/llama2chat"
diff --git a/codecov.yml b/codecov.yml
index e69de29..8520242 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -0,0 +1,23 @@
+comment:
+  layout: "header, diff, flags, components"  # show component info in the PR comment
+
+component_management:
+  default_rules:  # default rules that will be inherited by all components
+    statuses:
+      - type: project
+        target: auto
+        branches:
+          - "!main"
+  individual_components:
+    - component_id: module_workspace
+      name: workspace
+      paths:
+        - pkg/**
+    - component_id: module_presets
+      name: presets
+      paths:
+        - presets/**
+    - component_id: module_cmd
+      name: main
+      paths:
+        - cmd/**
\ No newline at end of file
diff --git a/config/crd/bases/kaito.sh_workspaces.yaml b/config/crd/bases/kaito.sh_workspaces.yaml
index 9fe21d1..973e178 100644
--- a/config/crd/bases/kaito.sh_workspaces.yaml
+++ b/config/crd/bases/kaito.sh_workspaces.yaml
@@ -400,6 +400,390 @@ spec:
             type: object
         type: object
     served: true
+    storage: false
+    subresources:
+      status: {}
+  - additionalPrinterColumns:
+    - jsonPath: .resource.instanceType
+      name: Instance
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="ResourceReady")].status
+      name: ResourceReady
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="InferenceReady")].status
+      name: InferenceReady
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="JobStarted")].status
+      name: JobStarted
+      type: string
+    - jsonPath: .status.conditions[?(@.type=="WorkspaceSucceeded")].status
+      name: WorkspaceSucceeded
+      type: string
+    - jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1beta1
+    schema:
+      openAPIV3Schema:
+        description: Workspace is the Schema for the workspaces API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          inference:
+            properties:
+              adapters:
+                description: |-
+                  Adapters are integrated into the base model for inference.
+                  Users can specify multiple adapters for the model and the respective weight of using each of them.
+                items:
+                  properties:
+                    source:
+                      description: Source describes where to obtain the adapter data.
+                      properties:
+                        image:
+                          description: |-
+                            The name of the image that contains the source data. The assumption is that the source data locates in the
+                            `data` directory in the image.
+                          type: string
+                        imagePullSecrets:
+                          description: ImagePullSecrets is a list of secret names
+                            in the same namespace used for pulling the data image.
+                          items:
+                            type: string
+                          type: array
+                        name:
+                          description: |-
+                            The name of the dataset. The same name will be used as a container name.
+                            It must be a valid DNS subdomain value,
+                          type: string
+                        urls:
+                          description: URLs specifies the links to the public data
+                            sources. E.g., files in a public github repository.
+                          items:
+                            type: string
+                          type: array
+                        volumeSource:
+                          description: The mounted volume that contains the data.
+                          x-kubernetes-preserve-unknown-fields: true
+                      type: object
+                    strength:
+                      description: |-
+                        Strength specifies the default multiplier for applying the adapter weights to the raw model weights.
+                        It is usually a float number between 0 and 1. It is defined as a string type to be language agnostic.
+                      type: string
+                  type: object
+                type: array
+              config:
+                description: |-
+                  Config specifies the name of a custom ConfigMap that contains inference arguments.
+                  If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+                type: string
+              preset:
+                description: Preset describes the base model that will be deployed
+                  with preset configurations.
+                properties:
+                  accessMode:
+                    default: public
+                    description: |-
+                      AccessMode specifies whether the containerized model image is accessible via public registry
+                      or private registry. This field defaults to "public" if not specified.
+                      If this field is "private", user needs to provide the private image information in PresetOptions.
+                    enum:
+                    - public
+                    - private
+                    type: string
+                  name:
+                    description: Name of the supported models with preset configurations.
+                    type: string
+                  presetOptions:
+                    properties:
+                      image:
+                        description: Image is the name of the containerized model
+                          image.
+                        type: string
+                      imagePullSecrets:
+                        description: ImagePullSecrets is a list of secret names in
+                          the same namespace used for pulling the model image.
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                required:
+                - name
+                type: object
+              template:
+                description: |-
+                  Template specifies the Pod template used to run the inference service. Users can specify custom Pod settings
+                  if the preset configurations cannot meet the requirements. Note that if Preset is specified, Template should not
+                  be specified and vice versa.
+                x-kubernetes-preserve-unknown-fields: true
+            type: object
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          resource:
+            description: |-
+              ResourceSpec describes the resource requirement of running the workload.
+              If the number of nodes in the cluster that meet the InstanceType and
+              LabelSelector requirements is small than the Count, controller
+              will provision new nodes before deploying the workload.
+              The final list of nodes used to run the workload is presented in workspace Status.
+            properties:
+              count:
+                default: 1
+                description: Count is the required number of GPU nodes.
+                type: integer
+              instanceType:
+                default: Standard_NC24ads_A100_v4
+                description: |-
+                  InstanceType specifies the GPU node SKU.
+                  This field defaults to "Standard_NC24ads_A100_v4" if not specified.
+                type: string
+              labelSelector:
+                description: LabelSelector specifies the required labels for the GPU
+                  nodes.
+                properties:
+                  matchExpressions:
+                    description: matchExpressions is a list of label selector requirements.
+                      The requirements are ANDed.
+                    items:
+                      description: |-
+                        A label selector requirement is a selector that contains values, a key, and an operator that
+                        relates the key and values.
+                      properties:
+                        key:
+                          description: key is the label key that the selector applies
+                            to.
+                          type: string
+                        operator:
+                          description: |-
+                            operator represents a key's relationship to a set of values.
+                            Valid operators are In, NotIn, Exists and DoesNotExist.
+                          type: string
+                        values:
+                          description: |-
+                            values is an array of string values. If the operator is In or NotIn,
+                            the values array must be non-empty. If the operator is Exists or DoesNotExist,
+                            the values array must be empty. This array is replaced during a strategic
+                            merge patch.
+                          items:
+                            type: string
+                          type: array
+                          x-kubernetes-list-type: atomic
+                      required:
+                      - key
+                      - operator
+                      type: object
+                    type: array
+                    x-kubernetes-list-type: atomic
+                  matchLabels:
+                    additionalProperties:
+                      type: string
+                    description: |-
+                      matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+                      map is equivalent to an element of matchExpressions, whose key field is "key", the
+                      operator is "In", and the values array contains only "value". The requirements are ANDed.
+                    type: object
+                type: object
+                x-kubernetes-map-type: atomic
+              preferredNodes:
+                description: |-
+                  PreferredNodes is an optional node list specified by the user.
+                  If a node in the list does not have the required labels, it will be ignored.
+                items:
+                  type: string
+                type: array
+            required:
+            - labelSelector
+            type: object
+          status:
+            description: WorkspaceStatus defines the observed state of Workspace
+            properties:
+              conditions:
+                description: Conditions report the current conditions of the workspace.
+                items:
+                  description: "Condition contains details for one aspect of the current
+                    state of this API Resource.\n---\nThis struct is intended for
+                    direct use as an array at the field path .status.conditions.  For
+                    example,\n\n\n\ttype FooStatus struct{\n\t    // Represents the
+                    observations of a foo's current state.\n\t    // Known .status.conditions.type
+                    are: \"Available\", \"Progressing\", and \"Degraded\"\n\t    //
+                    +patchMergeKey=type\n\t    // +patchStrategy=merge\n\t    // +listType=map\n\t
+                    \   // +listMapKey=type\n\t    Conditions []metav1.Condition `json:\"conditions,omitempty\"
+                    patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t
+                    \   // other fields\n\t}"
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        lastTransitionTime is the last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        message is a human readable message indicating details about the transition.
+                        This may be an empty string.
+                      maxLength: 32768
+                      type: string
+                    observedGeneration:
+                      description: |-
+                        observedGeneration represents the .metadata.generation that the condition was set based upon.
+                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+                        with respect to the current state of the instance.
+                      format: int64
+                      minimum: 0
+                      type: integer
+                    reason:
+                      description: |-
+                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
+                        Producers of specific condition types may define expected values and meanings for this field,
+                        and whether the values are considered a guaranteed API.
+                        The value should be a CamelCase string.
+                        This field may not be empty.
+                      maxLength: 1024
+                      minLength: 1
+                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      enum:
+                      - "True"
+                      - "False"
+                      - Unknown
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        ---
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be
+                        useful (see .node.status.conditions), the ability to deconflict is important.
+                        The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
+                      maxLength: 316
+                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - message
+                  - reason
+                  - status
+                  - type
+                  type: object
+                type: array
+              workerNodes:
+                description: WorkerNodes is the list of nodes chosen to run the workload
+                  based on the workspace resource requirement.
+                items:
+                  type: string
+                type: array
+            type: object
+          tuning:
+            properties:
+              config:
+                description: |-
+                  Config specifies the name of a custom ConfigMap that contains tuning arguments.
+                  If specified, the ConfigMap must be in the same namespace as the Workspace custom resource.
+                  If not specified, a default Config is used based on the specified tuning method.
+                type: string
+              input:
+                description: Input describes the input used by the tuning method.
+                properties:
+                  image:
+                    description: |-
+                      The name of the image that contains the source data. The assumption is that the source data locates in the
+                      `data` directory in the image.
+                    type: string
+                  imagePullSecrets:
+                    description: ImagePullSecrets is a list of secret names in the
+                      same namespace used for pulling the data image.
+                    items:
+                      type: string
+                    type: array
+                  name:
+                    description: |-
+                      The name of the dataset. The same name will be used as a container name.
+                      It must be a valid DNS subdomain value,
+                    type: string
+                  urls:
+                    description: URLs specifies the links to the public data sources.
+                      E.g., files in a public github repository.
+                    items:
+                      type: string
+                    type: array
+                  volumeSource:
+                    description: The mounted volume that contains the data.
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
+              method:
+                description: Method specifies the Parameter-Efficient Fine-Tuning(PEFT)
+                  method, such as lora, qlora, used for the tuning.
+                type: string
+              output:
+                description: Output specified where to store the tuning output.
+                properties:
+                  image:
+                    description: Name of the image where the output data is pushed
+                      to.
+                    type: string
+                  imagePushSecret:
+                    description: |-
+                      ImagePushSecret is the name of the secret in the same namespace that contains the authentication
+                      information that is needed for running `docker push`.
+                    type: string
+                  volumeSource:
+                    description: The mounted volume that is used to save the output
+                      data.
+                    x-kubernetes-preserve-unknown-fields: true
+                type: object
+              preset:
+                description: Preset describes which model to load for tuning.
+                properties:
+                  accessMode:
+                    default: public
+                    description: |-
+                      AccessMode specifies whether the containerized model image is accessible via public registry
+                      or private registry. This field defaults to "public" if not specified.
+                      If this field is "private", user needs to provide the private image information in PresetOptions.
+                    enum:
+                    - public
+                    - private
+                    type: string
+                  name:
+                    description: Name of the supported models with preset configurations.
+                    type: string
+                  presetOptions:
+                    properties:
+                      image:
+                        description: Image is the name of the containerized model
+                          image.
+                        type: string
+                      imagePullSecrets:
+                        description: ImagePullSecrets is a list of secret names in
+                          the same namespace used for pulling the model image.
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                required:
+                - name
+                type: object
+            required:
+            - input
+            - output
+            type: object
+        type: object
+    served: true
     storage: true
     subresources:
       status: {}
diff --git a/docker/presets/models/tfs/Dockerfile b/docker/presets/models/tfs/Dockerfile
index 63aa461..acedfed 100644
--- a/docker/presets/models/tfs/Dockerfile
+++ b/docker/presets/models/tfs/Dockerfile
@@ -14,6 +14,10 @@ COPY kaito/presets/workspace/dependencies/requirements.txt /workspace/requiremen
 
 RUN pip install --no-cache-dir -r /workspace/requirements.txt
 
+# Required for torch.compile
+RUN apt-get update -y && apt-get install gcc -y && \
+    apt-get clean && rm -rf /var/lib/apt/lists/*
+
 # 1. Huggingface transformers
 COPY kaito/presets/workspace/inference/${MODEL_TYPE}/inference_api.py \
     kaito/presets/workspace/tuning/${MODEL_TYPE}/cli.py \
diff --git a/docker/ragengine/Dockerfile b/docker/ragengine/Dockerfile
index a40724d..aad7e58 100644
--- a/docker/ragengine/Dockerfile
+++ b/docker/ragengine/Dockerfile
@@ -1,5 +1,5 @@
 # Build the manager binary
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.23 as builder
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24 as builder
 ARG TARGETOS
 ARG TARGETARCH
 
diff --git a/docker/workspace/Dockerfile b/docker/workspace/Dockerfile
index 738e0d6..b22fd34 100644
--- a/docker/workspace/Dockerfile
+++ b/docker/workspace/Dockerfile
@@ -1,5 +1,5 @@
 # Build the manager binary
-FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.23 as builder
+FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.24 as builder
 ARG TARGETOS
 ARG TARGETARCH
 
diff --git a/docs/How-to-use-kaito-in-aks-arc.md b/docs/How-to-use-kaito-in-aks-arc.md
deleted file mode 100644
index abc1226..0000000
--- a/docs/How-to-use-kaito-in-aks-arc.md
+++ /dev/null
@@ -1,150 +0,0 @@
-# Use Kaito in AKS Arc
-This article describes how to deploy AI models on AKS arc with AI toolchain operator (KAITO). 
-
-AKS Arc, or Azure Kubernetes Service (AKS) on Azure Arc, is a service that extends Azure management and governance capabilities to Kubernetes clusters running anywhere, whether on-premises, at the edge, or in other cloud environments. It allows you to manage and secure your Kubernetes clusters consistently across different environments using Azure tools and services.
-
-Follow the workflow below to enable this feature.
-
-## Supported Models
-Currently KAITO supports models such as Falcon, Phi2, Phi3, Llama2, Llama2Chat, Mistral. Please refer to KAITO’s [readme](https://github.com/kaito-project/kaito/blob/main/presets/README.md) file for the latest models. 
-
-## Prerequisite
-1.	Before you begin, please make sure you have the following details from your infrastructure administrator:
-    - An AKS Arc cluster that's up and running.
-    - We recommend using Linux machine for this feature.
-    - Your local kubectl environment configured to point to your AKS cluster.
-        - Run `az aksarc get-credentials --resource-group <ResourceGroupName> --name <ClusterName>  --admin` to download the kubeconfig file.
-2.	Make sure your Azure Local cluster is enabled with GPU, you can ask your infrastructure administrator to set it up for you. You also need to identify the right VM SKUs for your AKS cluster before creating the node pool. The instruction can be found at [use GPU for compute-intensive workloads](https://learn.microsoft.com/en-us/azure/aks/hybrid/deploy-gpu-node-pool).
-3.	Make sure the helm and kubectl are installed in your local machine.
-    - If you need to install or upgrade, please see instruction from [Install Helm](https://helm.sh/docs/intro/install/).
-    - If you need to install kubectl, please see instructions from [Install kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
-
-## Create a GPU Node Pool
-Available GPU sku can be found [here](https://learn.microsoft.com/en-us/azure/aks/hybrid/deploy-gpu-node-pool#supported-vm-sizes)
-<details>
-<summary><b>Using Azure Portal</b></summary>
-<div align="middle">
-  <img src="img/aksarc_nodepool_creation_portal.png" width=80% title="create nodepool from azure portal" alt="create nodepool from azure portal">
-</div>
-</details>
-
-<details>
-<summary><b>Using Az CLI</b></summary>
-<div align="middle">
-Run following Az command to provision node pool
-
-```bash
-az aksarc nodepool add --name "samplenodepool" --cluster-name "samplecluster" --resource-group "sample-rg" –node-vm-size "samplenodepoolsize" –os-type "Linux"
-```
-
-</div>
-</details>
-
-### Validation
-<details>
-<summary><b>Explain the get node command</b></summary>
-The node created will have random name in format moc-&lt;random&gt;, and will have the following label to reference its nodepool: msft.microsoft/nodepool-name = &lt;culsterName&gt;-&lt;randon&gt;-&lt;nodepoolName&gt;. 
-
-This command is to check the node whoes label can pass the regex and print its name
-</details>
-
-1.	After node pool creation command succeeds, you can confirm whether the GPU node is provisioned using following command, replace `<clusterName>` and `<nodepoolName>` with real value.
-```bash
-kubectl get nodes -o json | jq -r '.items[] | select(.metadata.labels["msft.microsoft/nodepool-name"] != null and (.metadata.labels["msft.microsoft/nodepool-name"] | tostring | test("^<culsterName>-.*-<nodepoolName>$"))) | .metadata.name'
-```
-
-2.	Please also ensure the node has allocatable GPU cores using command 
-```bash
-kubectl get node moc-l1i9uh0ksne -o yaml | grep -A 10 "allocatable:"
-```
-```
-  allocatable:
-    cpu: 31580m
-    ephemeral-storage: "95026644016"
-    hugepages-1Gi: "0"
-    hugepages-2Mi: "0"
-    memory: 121761176Ki
-    nvidia.com/gpu: "2"
-    pods: "110"
-  capacity:
-    cpu: "32"
-    ephemeral-storage: 103110508Ki
-```
-
-## Deploy KAITO via Helm
-
-> User does not need to install the gpu-provisioner as the node will be provisioned manually.
-1.	Git clone the KAITO repo to your local machine
-2.	Install KAITO operator using command 
-```bash
-helm install workspace ./charts/kaito/workspace --namespace kaito-workspace --create-namespace
-```
-
-## Deploy LLM Model
-<details>
-<summary><b>Explain the Yaml file</b></summary>
-If a user runs Kaito in an on-premise Kubernetes cluster where nodepool auto provision are unavailable, the GPU nodes can be <a href="#create-a-gpu-node-pool">pre-configured</a>.
-
-- the user needs to add the node names in the `preferredNodes` field in the `resource` spec. As a result, the Kaito controller will skip the steps for GPU node provisioning and use the prepared nodes to run the inference workload.
-
-
-Using the same method user can try all kaito functionalities, example can be found on /examples folder.
-</details>
-
-1.	Create a YAML file with the following template, make sure to replace the placeholders in curly braces with your own information. Please note, the PresetName can be found from the supported model file in KAITO’s github repo.
-```yaml
-apiVersion: kaito.sh/v1alpha1
-kind: Workspace
-metadata:
-  name: { YourDeploymentName }
-resource:
-  labelSelector:
-    matchLabels:
-      apps: { YourNodeLabel }
-  preferredNodes:
-  - { YourNodeName }
-inference:
-  preset:
-    name: { PresetName }
-
-```
-a sample yaml file can be 
-```yaml
-apiVersion: kaito.sh/v1alpha1
-kind: Workspace
-metadata:
-  name: workspace-falcon-7b
-resource:
-  labelSelector:
-    matchLabels:
-      apps: falcon-7b
-  preferredNodes: 
-  - moc-lmkq7webq9z
-inference:
-  preset:
-    name: falcon-7b-instruct
-```
-2.	You need to label your GPU node first, `Kubectl label node samplenode apps=YourNodeLabel` and then apply the YAML file
-`kubectl apply -f sampleyamlfile.yaml`
-
- 
-
-## Validate model deployment 
-1.	Validate the workspace using the command `kubectl get workspace`. Please also make sure both `ResourceReady` and `InferenceReady` fields are True before testing with the sample prompt.
-```
-NAME                           INSTANCE           RESOURCEREADY   INFERENCEREADY   JOBSTARTED   WORKSPACESUCCEEDED   AGE
-workspace-falcon-7b            Standard_NC6s_v3   True            True                          True                 4d
-```
-
-2.	You may test the model with a sample prompt: 
-```bash
-export CLUSTERIP=$(kubectl get svc workspace-falcon-7b -o jsonpath="{.spec.clusterIPs[0]}") 
-
-kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/chat -H "accept: application/json" -H "Content-Type: application/json" -d "{\"prompt\":\"<sample_prompt>\"}"
-```
-a sample output will be like
-```bash
-$ kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/chat -H "accept: application/json" -H "Content-Type: application/json" -d "{\"prompt\":\"write a poem\"}"
-If you don't see a command prompt, try pressing enter.
-{"Result":"write a poem about the first day of school, the last day of school, or a day in school.\nThe first day of school\nI wake at dawn\nand think of the new day\nas the sun rises\nI am excited\nthe first day of school\nI wake at dawn\nand think of the new day\nas the sun rises\nI am excited\nthe last day of school\nI wake at dawn\nand think of the last day\nas the sun rises\nI am sad\nthe last day of school\nI wake at dawn\nand think of the last day\nas the sun rises\nI am sad\na day in school\nthe first day of school\nI wake at dawn\nand think of the new day\nas the sun rises\nI am excited\nthe last day of school\nI wake at dawn\nand think of the last day\nas the sun rises\nI am sad\na day in school\nI walk down the stairs"}pod "curl" deleted
-```
\ No newline at end of file
diff --git a/docs/RAG/RAGEngine-installation.md b/docs/RAG/RAGEngine-installation.md
new file mode 100644
index 0000000..f03f87c
--- /dev/null
+++ b/docs/RAG/RAGEngine-installation.md
@@ -0,0 +1,27 @@
+# Kaito RAG Engine Installation 
+
+> Be sure you've cloned this repo and followed [kaito workspace installation](../installation.md)
+```bash
+helm install ragengine ./charts/kaito/ragengine --namespace kaito-ragengine --create-namespace
+```
+
+## Verify installation
+You can run the following commands to verify the installation of the controllers were successful.
+
+Check status of the Helm chart installations.
+
+```bash
+helm list -n kaito-ragengine
+```
+
+Check status of the `ragengine`.
+
+```bash
+kubectl describe deploy ragengine -n kaito-ragengine
+```
+
+## Clean up
+
+```bash
+helm uninstall kaito-ragengine
+```
\ No newline at end of file
diff --git a/docs/RAG/README.md b/docs/RAG/README.md
new file mode 100644
index 0000000..8c66b08
--- /dev/null
+++ b/docs/RAG/README.md
@@ -0,0 +1,58 @@
+# Kaito RAGEngine
+
+This document presents how to use the Kaito `ragengine` Custom Resource Definition (CRD) for retrieval-augumented generatoin workflow. By creating a RAGEngine resource, you can quickly stand up a service that indexes documents and queries them in conjunction with an existing LLM inference endpoint—no need to custom-build pipelines. This enables your large language model to answer questions based on your own private content.
+
+## Installation
+
+Please check the installation guidance [here](./RAGEngine-installation.md) for deployment using Azure CLI.
+
+## Usage
+
+### Prerequisite
+Before creating a RAGEngine, ensure you have an accessible model inference endpoint. This endpoint can be:
+
+1.	A model deployed through Kaito Workspace CRD (e.g., a local Hugging Face model, a vLLM instance, etc.).
+2.	An external API (e.g., Huggingface service or other REST-based LLM providers).
+
+### Define the RAGEngine
+Create a YAML manifest defining your RAGEngine. Key fields under spec include:
+
+Embedding: how to generate vector embeddings for your documents. You may choose remote or local (one must be left unset if you pick the other):
+
+```yaml
+embedding:  
+    local:
+      modelID: "BAAI/bge-small-en-v1.5" 
+```
+InferenceService: points to the LLM endpoint that RAGEngine will call for final text generation.
+```yaml
+inferenceService:
+  url: "<inference-url>/v1/completions"
+```
+Users also need to specify the GPU SKU used for inference in the `compute` spec. For example,
+
+```yaml
+apiVersion: kaito.sh/v1alpha1
+kind: RAGEngine
+metadata:
+  name: ragengine-start
+  annotations:
+      llm_model: "phi-3-mini-128k-instruct"
+spec:
+  compute:
+    instanceType: "Standard_NC6s_v3"  
+    labelSelector:
+      matchLabels:
+        apps: phi-3
+  embedding:  
+    local:
+      modelID: "BAAI/bge-small-en-v1.5"       
+  inferenceService:  
+    url: "<inference-url>/v1/completions" 
+```
+
+### Apply the Manifest
+After you create your YAML configuration, run:
+```sh
+kubectl apply -f examples/RAG/kaito_ragengine_phi_3.yaml
+```
\ No newline at end of file
diff --git a/docs/contributing/readme.md b/docs/contributing/readme.md
index ccbaa4f..256b585 100644
--- a/docs/contributing/readme.md
+++ b/docs/contributing/readme.md
@@ -13,3 +13,94 @@ For more information see the [Code of Conduct FAQ](https://opensource.microsoft.
 contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
 
 ## Development
+
+### Prerequisites
+
+- [Go 1.24](https://go.dev/doc/install) or later
+- [Tilt](https://tilt.dev/) for rapid, local development
+- [Docker](https://docs.docker.com/get-started/get-docker/) for building container images
+- Kubernetes cluster with Karpenter supported (e.g. AKS, EKS)
+
+### Getting Started
+
+#### 1. Fork and Clone the Repository
+
+```bash
+git clone git@github.com:<username>/kaito.git
+cd kaito
+```
+
+#### 2. Create `tilt-settings.yaml`
+
+Tilt streamlines Kubernetes development by automating image builds, deployments, and live updates. Instead of manually building and pushing images to a registry, Tilt handles this process and hot-reloads changes (Go binaries and CRD YAML files) in real-time.
+
+To configure Tilt, create a tilt-settings.yaml file in the repository root:
+
+```bash
+cat <<EOF > tilt-settings.yaml
+default_registry: <acr_name>.azurecr.io
+allowed_contexts:
+  - <context_name>
+cluster_name: <cluster_name>
+EOF
+```
+
+- `default_registry`: The container registry where Tilt pushes development images (e.g. `<acr_name>.azurecr.io`). Ensure you’re logged in with `docker login` and have push/pull permissions.
+if you want to use Azure Container Registry (ACR), set up a new ACR instance and attach it to your AKS cluster with these commands:
+
+```bash
+export ACR_NAME=<acr_name>
+export RESOURCE_GROUP=<resource_group>
+export CLUSTER_NAME=<cluster_name>
+
+az acr -n $ACR_NAME create --resource-group $RESOURCE_GROUP --sku Basic
+az acr login -n $ACR_NAME
+az aks update -n $CLUSTER_NAME -g $RESOURCE_GROUP --attach-acr $ACR_NAME
+```
+
+- `allowed_contexts`: The Kubernetes contexts Tilt is permitted to use. List your available contexts with `kubectl config get-contexts`.
+
+- `cluster_name`: The name of your Kubernetes cluster. If omitted, Tilt defaults to your current kubectl context.
+
+#### 3. Start Tilt
+
+To start Tilt, run the following command in the root of the repository:
+
+```bash
+tilt up
+```
+
+<details>
+<summary>Example Output</summary>
+
+<!-- markdown-link-check-disable -->
+
+```bash
+Tilt started on http://localhost:10350/
+v0.34.0, built 2025-03-11
+
+(space) to open the browser
+(s) to stream logs (--stream=true)
+(t) to open legacy terminal mode (--legacy=true)
+(ctrl-c) to exit
+Opening browser: http://localhost:10350/
+```
+
+<!-- markdown-link-check-enable -->
+
+</details>
+
+<br/>
+
+![Tilt UI](../img/tilt.png)
+
+Tilt automates the following steps during `tilt up`:
+
+<!-- markdown-link-check-disable -->
+1. **Start the Tilt Server**: Runs locally at http://localhost:10350/ and opens the Tilt UI in your browser (press space).
+<!-- markdown-link-check-enable -->
+2. **Generate and Build**: Creates CRD YAML files (`make generate manifests`) and compiles the controller manager Go binary (`go build`), saving it to `./tilt_bin`.
+3. **Build Development Image**: Packages the binary into a container image and tag it with `<default_registry>/kaito/workspace:tilt-<hash>`), then pushes it to your container registry.
+4. **Apply CRDs**: Deploys CRDs to the cluster with `kubectl apply --server-side -f charts/kaito/crds/`.
+5. **Deploy Resources**: Renders the Helm chart (`helm template charts/kaito/workspace`) and applies it to the cluster, including the controller manager, webhooks, RBAC, and NVIDIA device plugin resources.
+6. **Live Updates**: Detects changes to the controller manager source code, rebuilds the binary, updates the container, and restarts the controller manager within the container without re-deploying the pod - all in ~30 seconds.
diff --git a/docs/custom-model-integration/custom-deployment-template.yaml b/docs/custom-model-integration/custom-deployment-template.yaml
index fe9c2c4..35702ce 100644
--- a/docs/custom-model-integration/custom-deployment-template.yaml
+++ b/docs/custom-model-integration/custom-deployment-template.yaml
@@ -13,6 +13,26 @@ inference:
       containers:
       - name: custom-llm-container
         image: modelsregistry.azurecr.io/custom-llm:0.0.1 # Replace with the actual image name
+        livenessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /health
+            port: 5000
+            scheme: HTTP
+          initialDelaySeconds: 600
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
+        readinessProbe:
+          failureThreshold: 3
+          httpGet:
+            path: /health
+            port: 5000
+            scheme: HTTP
+          initialDelaySeconds: 30
+          periodSeconds: 10
+          successThreshold: 1
+          timeoutSeconds: 1
         command: ["accelerate"]
         args:
           - "launch"
diff --git a/docs/custom-model-integration/reference-image-deployment.yaml b/docs/custom-model-integration/reference-image-deployment.yaml
index 3a77dba..b825b0c 100644
--- a/docs/custom-model-integration/reference-image-deployment.yaml
+++ b/docs/custom-model-integration/reference-image-deployment.yaml
@@ -7,33 +7,69 @@ resource:
   labelSelector:
     matchLabels:
       apps: custom-llm
+#  preferredNodes:
+#    - <PREFERRED_NODE_HERE>
 inference:
-  template: 
+  template:
     spec:
       containers:
-      - name: custom-llm-container
-        image: ghcr.io/kaito-project/kaito/llm-reference-preset:latest
-        command: ["accelerate"]
-        args: 
-          - "launch"
-          - "--num_processes"
-          - "1"
-          - "--num_machines"
-          - "1"
-          - "--gpu_ids"
-          - "all"
-          - "inference_api.py"
-          - "--pipeline"
-          - "text-generation"
-          - "--trust_remote_code"
-          - "--allow_remote_files"
-          - "--pretrained_model_name_or_path"
-          - "<MODEL_ID>"  # Replace <MODEL_ID> with the specific HuggingFace model identifier
-          - "--torch_dtype"
-          - "float16"  # Set to "float16" for compatibility with V100 GPUs; use "bfloat16" for A100, H100 or newer GPUs
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
+        - name: custom-llm-container
+          image: ghcr.io/kaito-project/kaito/llm-reference-preset:latest # Kaito Reference Image
+          livenessProbe:
+            failureThreshold: 3
+            httpGet:
+              path: /health
+              port: 5000
+              scheme: HTTP
+            initialDelaySeconds: 600
+            periodSeconds: 10
+            successThreshold: 1
+            timeoutSeconds: 1
+          readinessProbe:
+            failureThreshold: 3
+            httpGet:
+              path: /health
+              port: 5000
+              scheme: HTTP
+            initialDelaySeconds: 30
+            periodSeconds: 10
+            successThreshold: 1
+            timeoutSeconds: 1
+          resources:
+            requests:
+              nvidia.com/gpu: 1  # Request 1 GPU; adjust as needed
+            limits:
+              nvidia.com/gpu: 1  # Optional: Limit to 1 GPU
+          command:
+            - "accelerate"
+          args:
+            - "launch"
+            - "--num_processes"
+            - "1"
+            - "--num_machines"
+            - "1"
+            - "--gpu_ids"
+            - "all"
+            - "inference_api.py"
+            - "--pipeline"
+            - "text-generation"
+            - "--trust_remote_code"
+            - "--allow_remote_files"
+            - "--pretrained_model_name_or_path"
+            - "<MODEL_ID>"  # Replace <MODEL_ID> with the specific HuggingFace model identifier
+            - "--torch_dtype"
+            - "float16"  # Set to "float16" for compatibility with V100 GPUs; use "bfloat16" for A100, H100 or newer GPUs
+          # env:
+          #   HF_TOKEN is required only for private or gated Hugging Face models
+          #   Uncomment and configure this block if needed
+          #   - name: HF_TOKEN
+          #     valueFrom:
+          #       secretKeyRef:
+          #         name: hf-token-secret     # Replace with your Kubernetes Secret name
+          #         key: HF_TOKEN             # Replace with the specific key holding the token
+          volumeMounts:
+            - name: dshm
+              mountPath: /dev/shm
       volumes:
       - name: dshm
         emptyDir:
diff --git a/docs/img/tilt.png b/docs/img/tilt.png
new file mode 100644
index 0000000..cd6c4ab
Binary files /dev/null and b/docs/img/tilt.png differ
diff --git a/docs/inference/Monitoring.md b/docs/inference/Monitoring.md
new file mode 100644
index 0000000..c297cb3
--- /dev/null
+++ b/docs/inference/Monitoring.md
@@ -0,0 +1,36 @@
+# Prometheus Metrics
+
+Inference service exposes Prometheus metrics for monitoring system stats.
+
+## vLLM runtime
+
+vLLM exposes Prometheus metrics at the `/metrics` endpoint. These metrics provide detailed insights into the system's performance, resource utilization, and request processing statistics. The following table lists all available metrics:
+
+vLLM version: 0.6.3
+
+| Category | Metric Name | Type | Description |
+|----------|-------------|------|-------------|
+| System Stats - Scheduler | `vllm:num_requests_running` | Gauge | Number of requests currently running on GPU |
+| System Stats - Scheduler | `vllm:num_requests_waiting` | Gauge | Number of requests waiting to be processed |
+| System Stats - Scheduler | `vllm:num_requests_swapped` | Gauge | Number of requests swapped to CPU |
+| System Stats - Cache | `vllm:gpu_cache_usage_perc` | Gauge | GPU KV-cache usage (1 = 100%) |
+| System Stats - Cache | `vllm:cpu_cache_usage_perc` | Gauge | CPU KV-cache usage (1 = 100%) |
+| System Stats - Cache | `vllm:cpu_prefix_cache_hit_rate` | Gauge | CPU prefix cache block hit rate |
+| System Stats - Cache | `vllm:gpu_prefix_cache_hit_rate` | Gauge | GPU prefix cache block hit rate |
+| Iteration Stats | `vllm:num_preemptions_total` | Counter | Cumulative number of preemption from the engine |
+| Iteration Stats | `vllm:prompt_tokens_total` | Counter | Number of prefill tokens processed |
+| Iteration Stats | `vllm:generation_tokens_total` | Counter | Number of generation tokens processed |
+| Iteration Stats | `vllm:time_to_first_token_seconds` | Histogram | Time to first token in seconds |
+| Iteration Stats | `vllm:time_per_output_token_seconds` | Histogram | Time per output token in seconds |
+| Request Stats | `vllm:e2e_request_latency_seconds` | Histogram | End to end request latency in seconds |
+| Request Stats | `vllm:request_prompt_tokens` | Histogram | Number of prefill tokens processed per request |
+| Request Stats | `vllm:request_generation_tokens` | Histogram | Number of generation tokens processed per request |
+| Request Stats | `vllm:request_params_n` | Histogram | The 'n' request parameter |
+| Request Stats | `vllm:request_success_total` | Counter | Count of successfully processed requests |
+| Speculative Decoding | `vllm:spec_decode_draft_acceptance_rate` | Gauge | Speculative token acceptance rate |
+| Speculative Decoding | `vllm:spec_decode_efficiency` | Gauge | Speculative decoding system efficiency |
+| Speculative Decoding | `vllm:spec_decode_num_accepted_tokens_total` | Counter | Number of accepted tokens |
+| Speculative Decoding | `vllm:spec_decode_num_draft_tokens_total` | Counter | Number of draft tokens |
+| Speculative Decoding | `vllm:spec_decode_num_emitted_tokens_total` | Counter | Number of emitted tokens |
+| Deprecated Metrics | `vllm:avg_prompt_throughput_toks_per_s` | Gauge | Average prefill throughput in tokens/s |
+| Deprecated Metrics | `vllm:avg_generation_throughput_toks_per_s` | Gauge | Average generation throughput in tokens/s |
diff --git a/docs/inference/README.md b/docs/inference/README.md
index 765012f..8c3c278 100644
--- a/docs/inference/README.md
+++ b/docs/inference/README.md
@@ -63,6 +63,49 @@ inference:
     name: "falcon-7b"
 ```
 
+### Inference with custom parameters
+
+Users can customize vLLM runtime parameters by creating a ConfigMap containing an `inference_config.yaml` file and referencing it in the workspace spec. For example:
+
+```yaml
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  namespace: myns
+  name: workspace-example
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: example
+inference:
+  preset:
+    name: "example-model"
+  config: "my-inference-params"  # Reference to ConfigMap name
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  namespace: myns
+  name: my-inference-params
+data:
+  inference_config.yaml: |
+    max_probe_steps: 6
+    vllm:
+      gpu-memory-utilization: 0.95  # Controls GPU memory usage (0.0-1.0)
+      tensor-parallel-size: 2        # Number of GPUs for tensor parallelism
+      max-model-len: 131072         # Maximum sequence length
+      swap-space: 4                 # CPU swap space in GB
+      cpu-offload-gb: 0             # Amount of GPU memory to offload to CPU
+```
+
+Key vLLM parameters include:
+- `gpu-memory-utilization`: Controls fraction of GPU memory allocated (between 0.0 and 1.0)
+- `tensor-parallel-size`: Number of GPUs to use for tensor parallelism
+- `max-model-len`: Maximum sequence length the model can handle
+
+For the complete list of vLLM parameters, refer to the [vLLM documentation](https://docs.vllm.ai/en/latest/serving/engine_args.html).
+
 ### Inference with LoRA adapters 
 
 Kaito also supports running the inference workload with LoRA adapters produced by [model fine-tuning jobs](../tuning/README.md). Users can specify one or more adapters in the `adapters` field of the `inference` spec. For example,
diff --git a/docs/installation.md b/docs/installation.md
index ea1d51b..b1f1133 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -8,6 +8,7 @@ Before you begin, ensure you have the following tools installed:
 - [Helm](https://helm.sh) to install this operator
 - [kubectl](https://kubernetes.io/docs/tasks/tools/) to view Kubernetes resources
 - [git](https://git-scm.com/downloads) to clone this repo locally
+- [yq](https://github.com/mikefarah/yq) to process yaml files
 - [jq](https://jqlang.github.io/jq/download) to process JSON files
 
 **Important Note**:
@@ -42,7 +43,7 @@ az aks install-cli
 Install the Workspace controller.
 
 ```bash
-export KAITO_WORKSPACE_VERSION=0.4.2
+export KAITO_WORKSPACE_VERSION=0.4.4
 
 helm install kaito-workspace  --set clusterName=$MY_CLUSTER --wait \
 https://github.com/kaito-project/kaito/raw/gh-pages/charts/kaito/workspace-$KAITO_WORKSPACE_VERSION.tgz --namespace kaito-workspace --create-namespace
@@ -76,12 +77,12 @@ az role assignment create --assignee $IDENTITY_PRINCIPAL_ID --scope /subscriptio
 #### Install helm charts
 
 **Important Note**:
-For kaito 0.4.2 and above, please use gpu-provisioner 0.3.1 or higher. For versions below kaito 0.4.2, please use gpu-provisioner 0.2.1.
+For kaito 0.4.2 and above, please use gpu-provisioner 0.3.2 or higher. For versions below kaito 0.4.2, please use gpu-provisioner 0.2.1.
 
 Install the Node provisioner controller.
 ```bash
 # get additional values for helm chart install
-export GPU_PROVISIONER_VERSION=0.3.1
+export GPU_PROVISIONER_VERSION=0.3.3
 
 curl -sO https://raw.githubusercontent.com/Azure/gpu-provisioner/main/hack/deploy/configure-helm-values.sh
 chmod +x ./configure-helm-values.sh && ./configure-helm-values.sh $MY_CLUSTER $RESOURCE_GROUP $IDENTITY_NAME
@@ -116,7 +117,7 @@ helm list -n gpu-provisioner
 Check status of the `workspace`.
 
 ```bash
-kubectl describe deploy workspace -n kaito-workspace
+kubectl describe deploy kaito-workspace -n kaito-workspace
 ```
 
 Check status of the `gpu-provisioner`.
@@ -137,6 +138,6 @@ kubectl logs --selector=app.kubernetes.io\/name=gpu-provisioner -n gpu-provision
 ## Clean up
 
 ```bash
-helm uninstall gpu-provisioner
-helm uninstall workspace
+helm uninstall gpu-provisioner -n gpu-provisioner
+helm uninstall kaito-workspace -n kaito-workspace
 ```
diff --git a/docs/proposals/20241212-phi4-instruct.md b/docs/proposals/20241212-phi4-instruct.md
new file mode 100644
index 0000000..db1cd2b
--- /dev/null
+++ b/docs/proposals/20241212-phi4-instruct.md
@@ -0,0 +1,50 @@
+---
+title: Proposal for support of Microsoft's Phi-4
+authors:
+  - "Suraj Deshmukh"
+reviewers:
+  - "Kaito contributor"
+creation-date: 2024-12-12
+last-updated: 2024-12-12
+status: provisional
+---
+
+# Title
+
+Add official support for Microsoft's Phi-4 models in Kaito.
+
+## Summary
+
+- **Model Description**: The `phi-4` model from Microsoft, released in December 2024, is a state-of-the-art language model designed for advanced reasoning and high-quality text generation. It is built on a blend of synthetic datasets, filtered public domain websites, and academic books, ensuring robust performance in various tasks. With 14.7B parameters, `phi-4` excels in memory and compute-constrained environments, offering precise instruction adherence and strong safety measures through supervised fine-tuning and direct preference optimization. This model is particularly suited for general-purpose AI applications requiring reasoning and logic. The knowledge cut-off date for `phi-4` is set to June 2024.
+- **Model Usage Statistics**: As of January 2025, `phi-4` has garnered over 88k downloads.
+- **Model License**: MIT License
+
+## Requirements
+
+The following table describes the basic model characteristics and the resource requirements of running it.
+
+| Field            | Notes                                    |
+|------------------|------------------------------------------|
+| Family name      | Phi-4                                    |
+| Type             | conversational                           |
+| Download site    | <https://huggingface.co/microsoft/phi-4> |
+| Version          | f957856cd926f9d681b14153374d755dd97e45ed |
+| Storage size     | 30 GB                                    |
+| GPU count        | 01 GPU                                   |
+| Total GPU memory | 30 GB                                    |
+| Per GPU memory   | 80 GB                                    |
+
+## Runtimes
+
+This section describes how to configure the runtime framework to support the inference calls.
+
+| Options               | Notes                                                              |
+|-----------------------|--------------------------------------------------------------------|
+| Runtime               | Huggingface Transformer & VLLM                                     |
+| Distributed Inference | False                                                              |
+| Custom configurations | Precision: BF16. Can run on one machine with 30+ GB of GPU memory. |
+
+# History
+
+- [x] 01/16/2025: Open proposal PR.
+- [ ] xx/xx/2025: Phi-4 Merged TBD
diff --git a/examples/RAG/kaito_ragengine_phi_3.yaml b/examples/RAG/kaito_ragengine_phi_3.yaml
new file mode 100644
index 0000000..909aa8c
--- /dev/null
+++ b/examples/RAG/kaito_ragengine_phi_3.yaml
@@ -0,0 +1,17 @@
+apiVersion: kaito.sh/v1alpha1
+kind: RAGEngine
+metadata:
+  name: ragengine-start
+  annotations:
+      llm_model: "phi-3-mini-128k-instruct"
+spec:
+  compute:
+    instanceType: "Standard_NC6s_v3"  
+    labelSelector:
+      matchLabels:
+        apps: phi-3
+  embedding:  
+    local:
+      modelID: "BAAI/bge-small-en-v1.5"       
+  inferenceService:  
+    url: "<inference-url>/v1/completions" 
\ No newline at end of file
diff --git a/examples/README.md b/examples/README.md
index e9becab..162276c 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,2 +1,28 @@
 # Notes:
-For **testing** purposes, users can add the `kaito.sh/enablelb: "True"` annotation to the workspace custom resource. As a result, a `loadbalancer` type service will be created for the inference service with a public IP being assigned. However, this is **NOT** recommended for production use. An [ingress controller](https://learn.microsoft.com/en-us/azure/aks/ingress-basic?tabs=azure-cli) is recommended to expose the service to public.
+
+### GPU Resource Requirement Bypass
+To bypass GPU memory requirement checks, annotate your workspace resource as follows:
+```yaml
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: my-workspace
+  annotations:
+    kaito.sh/bypass-resource-checks: "true"
+  # ... rest of the workspace spec
+```
+> Caution: Using this annotation may lead to degraded performance or out-of-memory errors if GPU resources are insufficient for your model. Warnings will be logged accordingly.
+
+---
+
+### Exposing Service via LoadBalancer (Testing Only)
+
+For **testing** purposes, you may annotate your workspace resource with `kaito.sh/enablelb: "true"` to automatically create a `LoadBalancer` type service with a public IP for the inference service:
+
+```yaml
+metadata:
+  annotations:
+    kaito.sh/enablelb: "true"
+```
+> Important: This approach is **NOT** recommended for production environments. For production scenarios, use an [Ingress Controller](https://learn.microsoft.com/en-us/azure/aks/ingress-basic?tabs=azure-cli) to safely expose the service.
+
diff --git a/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_llama_8b.yaml b/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_llama_8b.yaml
new file mode 100644
index 0000000..f6f576c
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_llama_8b.yaml
@@ -0,0 +1,15 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-deepseek-r1-distill-llama-8b
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: "deepseek-r1-distill-llama-8b"
+    
diff --git a/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml b/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml
new file mode 100644
index 0000000..696787a
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml
@@ -0,0 +1,15 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-deepseek-r1-distill-qwen-14b
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: "deepseek-r1-distill-qwen-14b"
+    
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_2.yaml b/examples/inference/arc-examples/kaito_workspace_phi_2.yaml
new file mode 100644
index 0000000..706572b
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_2.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-2
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: "phi-2"
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3.5-instruct.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3.5-instruct.yaml
new file mode 100644
index 0000000..9b36ba4
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3.5-instruct.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-5-mini
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3.5-mini-instruct
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3_medium_128k.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3_medium_128k.yaml
new file mode 100644
index 0000000..f26e953
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3_medium_128k.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-medium
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3-medium-128k-instruct
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3_medium_4k.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3_medium_4k.yaml
new file mode 100644
index 0000000..41266b4
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3_medium_4k.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-medium
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3-medium-4k-instruct
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3_mini_128k.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3_mini_128k.yaml
new file mode 100644
index 0000000..204d8eb
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3_mini_128k.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-mini
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3-mini-128k-instruct
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3_mini_4k.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3_mini_4k.yaml
new file mode 100644
index 0000000..79a3150
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3_mini_4k.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-mini
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3-mini-4k-instruct
diff --git a/examples/inference/arc-examples/kaito_workspace_phi_3_with_adapters.yaml b/examples/inference/arc-examples/kaito_workspace_phi_3_with_adapters.yaml
new file mode 100644
index 0000000..3624e95
--- /dev/null
+++ b/examples/inference/arc-examples/kaito_workspace_phi_3_with_adapters.yaml
@@ -0,0 +1,19 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-3-mini-adapter
+resource:
+  instanceType: <your-vm-sku>
+  preferredNodes:
+    - <your-arc-node-name>
+  labelSelector:
+    matchLabels:
+      apps: llm-inference
+inference:
+  preset:
+    name: phi-3-mini-128k-instruct
+  adapters:
+    - source:
+        name: "phi-3-adapter"
+        image: "ACR_REPO_HERE.azurecr.io/ADAPTER_HERE:0.0.1"
+      strength: "1.0"
diff --git a/examples/inference/kaito_workspace_custom_config.yaml b/examples/inference/kaito_workspace_custom_config.yaml
new file mode 100644
index 0000000..2c2c07f
--- /dev/null
+++ b/examples/inference/kaito_workspace_custom_config.yaml
@@ -0,0 +1,34 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-deepseek-r1-distill-qwen-14b
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: deepseek-r1-distill-qwen-14b
+inference:
+  preset:
+    name: "deepseek-r1-distill-qwen-14b"
+  config: "ds-inference-params"
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: ds-inference-params
+data:
+  inference_config.yaml: |
+    # Maximum number of steps to find the max available seq len fitting in the GPU memory.
+    max_probe_steps: 6
+
+    vllm:
+      cpu-offload-gb: 0
+      gpu-memory-utilization: 0.95
+      swap-space: 4
+      max-model-len: 131072
+      tensor-parallel-size: 2
+
+      max-seq-len-to-capture: 8192
+      num-scheduler-steps: 1
+      enable-chunked-prefill: true
+      # see https://docs.vllm.ai/en/latest/serving/engine_args.html for more options.
\ No newline at end of file
diff --git a/examples/inference/kaito_workspace_deepseek_r1_distill_llama_8b.yaml b/examples/inference/kaito_workspace_deepseek_r1_distill_llama_8b.yaml
new file mode 100644
index 0000000..d40cf57
--- /dev/null
+++ b/examples/inference/kaito_workspace_deepseek_r1_distill_llama_8b.yaml
@@ -0,0 +1,13 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-deepseek-r1-distill-llama-8b
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: deepseek-r1-distill-llama-8b
+inference:
+  preset:
+    name: "deepseek-r1-distill-llama-8b"
+    
diff --git a/examples/inference/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml b/examples/inference/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml
new file mode 100644
index 0000000..802cdc9
--- /dev/null
+++ b/examples/inference/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml
@@ -0,0 +1,13 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-deepseek-r1-distill-qwen-14b
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: deepseek-r1-distill-qwen-14b
+inference:
+  preset:
+    name: "deepseek-r1-distill-qwen-14b"
+    
diff --git a/examples/inference/kaito_workspace_phi_4.yaml b/examples/inference/kaito_workspace_phi_4.yaml
new file mode 100644
index 0000000..57ec07c
--- /dev/null
+++ b/examples/inference/kaito_workspace_phi_4.yaml
@@ -0,0 +1,12 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-4
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: phi-4
+inference:
+  preset:
+    name: phi-4 
\ No newline at end of file
diff --git a/examples/inference/kaito_workspace_phi_4_mini.yaml b/examples/inference/kaito_workspace_phi_4_mini.yaml
new file mode 100644
index 0000000..638d52f
--- /dev/null
+++ b/examples/inference/kaito_workspace_phi_4_mini.yaml
@@ -0,0 +1,12 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-phi-4-mini
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: phi-4
+inference:
+  preset:
+    name: phi-4-mini-instruct 
\ No newline at end of file
diff --git a/examples/inference/kaito_workspace_qwen_2.5_coder_32b-instruct.yaml b/examples/inference/kaito_workspace_qwen_2.5_coder_32b-instruct.yaml
new file mode 100644
index 0000000..b0dde32
--- /dev/null
+++ b/examples/inference/kaito_workspace_qwen_2.5_coder_32b-instruct.yaml
@@ -0,0 +1,12 @@
+apiVersion: kaito.sh/v1alpha1
+kind: Workspace
+metadata:
+  name: workspace-qwen-2.5-coder-32b
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"
+  labelSelector:
+    matchLabels:
+      apps: qwen
+inference:
+  preset:
+    name: qwen2.5-coder-32b-instruct
\ No newline at end of file
diff --git a/go.mod b/go.mod
index c51b116..7bcd88a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,17 +1,18 @@
 module github.com/kaito-project/kaito
 
-go 1.23.0
+go 1.24.0
 
-toolchain go1.23.2
+toolchain go1.24.1
 
 require (
 	github.com/Azure/karpenter-provider-azure v0.7.0
 	github.com/aws/karpenter-provider-aws v1.0.6
 	github.com/awslabs/operatorpkg v0.0.0-20240805231134-67d0acfb6306
+	github.com/distribution/reference v0.5.0
 	github.com/go-logr/logr v1.4.2
 	github.com/onsi/ginkgo/v2 v2.22.2
 	github.com/onsi/gomega v1.36.2
-	github.com/samber/lo v1.47.0
+	github.com/samber/lo v1.49.1
 	github.com/stretchr/testify v1.10.0
 	gopkg.in/yaml.v2 v2.4.0
 	gotest.tools v2.2.0+incompatible
@@ -72,6 +73,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+	github.com/opencontainers/go-digest v1.0.0 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
@@ -89,12 +91,12 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
-	golang.org/x/net v0.33.0 // indirect
+	golang.org/x/net v0.36.0 // indirect
 	golang.org/x/oauth2 v0.22.0 // indirect
-	golang.org/x/sync v0.10.0 // indirect
-	golang.org/x/sys v0.28.0 // indirect
-	golang.org/x/term v0.27.0 // indirect
-	golang.org/x/text v0.21.0 // indirect
+	golang.org/x/sync v0.11.0 // indirect
+	golang.org/x/sys v0.30.0 // indirect
+	golang.org/x/term v0.29.0 // indirect
+	golang.org/x/text v0.22.0 // indirect
 	golang.org/x/time v0.6.0 // indirect
 	golang.org/x/tools v0.28.0 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
@@ -129,6 +131,7 @@ replace (
 	k8s.io/component-helpers => k8s.io/component-helpers v0.30.1
 	k8s.io/controller-manager => k8s.io/controller-manager v0.30.1
 	k8s.io/cri-api => k8s.io/cri-api v0.30.1
+	k8s.io/cri-client => k8s.io/cri-client v0.30.1
 	k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.30.1
 	k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.30.1
 	k8s.io/endpointslice => k8s.io/endpointslice v0.30.1
diff --git a/go.sum b/go.sum
index 1555897..ca02508 100644
--- a/go.sum
+++ b/go.sum
@@ -148,6 +148,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
+github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -371,6 +373,8 @@ github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU
 github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
 github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
 github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
@@ -429,8 +433,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
-github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
+github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
+github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
 github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
 github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -497,8 +501,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
-golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
+golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -567,8 +571,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
 golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
-golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
+golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
+golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -589,8 +593,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
+golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -632,12 +636,12 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
+golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -645,8 +649,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/pkg/featuregates/featuregates.go b/pkg/featuregates/featuregates.go
index 015a2f0..9b5ddbf 100644
--- a/pkg/featuregates/featuregates.go
+++ b/pkg/featuregates/featuregates.go
@@ -13,9 +13,10 @@ import (
 )
 
 var (
-	// FeatureGates is a map that holds	the feature gates and their default values for Kaito.
+	// FeatureGates is a map that holds the feature gate names and their default values for Kaito.
 	FeatureGates = map[string]bool{
-		consts.FeatureFlagVLLM: true,
+		consts.FeatureFlagVLLM:            true,
+		consts.FeatureFlagEnsureNodeClass: false,
 		//	Add more feature gates here
 	}
 )
diff --git a/pkg/ragengine/controllers/preset-rag.go b/pkg/ragengine/controllers/preset-rag.go
index 3bdc006..49cf6fc 100644
--- a/pkg/ragengine/controllers/preset-rag.go
+++ b/pkg/ragengine/controllers/preset-rag.go
@@ -98,8 +98,8 @@ func CreatePresetRAG(ctx context.Context, ragEngineObj *v1alpha1.RAGEngine, revi
 
 	}
 	commands := utils.ShellCmd("python3 main.py")
-	// TODO: provide this image
-	image := "mcr.microsoft.com/aks/kaito/kaito-rag-service:0.0.1"
+
+	image := "aimodelsregistrytest.azurecr.io/kaito-rag-service:0.3.2" //TODO: Change to the mcr image when release
 
 	imagePullSecretRefs := []corev1.LocalObjectReference{}
 
diff --git a/pkg/ragengine/controllers/ragengine_controller.go b/pkg/ragengine/controllers/ragengine_controller.go
index c0f16e2..cb463c9 100644
--- a/pkg/ragengine/controllers/ragengine_controller.go
+++ b/pkg/ragengine/controllers/ragengine_controller.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/klog/v2"
 	"k8s.io/utils/clock"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -561,7 +562,7 @@ func (c *RAGEngineReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		For(&kaitov1alpha1.RAGEngine{}).
 		Owns(&appsv1.ControllerRevision{}).
 		Owns(&appsv1.Deployment{}).
-		Watches(&karpenterv1.NodeClaim{}, c.watchNodeClaims()). // watches for nodeClaim with labels indicating ragengine name.
+		Watches(&karpenterv1.NodeClaim{}, c.watchNodeClaims(), builder.WithPredicates(nodeclaim.NodeClaimPredicate)). // watches for nodeClaim with labels indicating ragengine name.
 		WithOptions(controller.Options{MaxConcurrentReconciles: 5})
 
 	return builder.Complete(c)
diff --git a/pkg/ragengine/manifests/manifests.go b/pkg/ragengine/manifests/manifests.go
index 541093b..2dba070 100644
--- a/pkg/ragengine/manifests/manifests.go
+++ b/pkg/ragengine/manifests/manifests.go
@@ -158,8 +158,15 @@ func RAGSetEnv(ragEngineObj *kaitov1alpha1.RAGEngine) []corev1.EnvVar {
 
 	if ragEngineObj.Spec.InferenceService.AccessSecret != "" {
 		accessSecretEnv := corev1.EnvVar{
-			Name:  "INFERENCE_ACCESS_SECRET",
-			Value: ragEngineObj.Spec.InferenceService.AccessSecret,
+			Name: "LLM_ACCESS_SECRET",
+			ValueFrom: &corev1.EnvVarSource{
+				SecretKeyRef: &corev1.SecretKeySelector{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: ragEngineObj.Spec.InferenceService.AccessSecret,
+					},
+					Key: "LLM_ACCESS_SECRET",
+				},
+			},
 		}
 		envs = append(envs, accessSecretEnv)
 	}
diff --git a/pkg/sku/arc_sku_handker.go b/pkg/sku/arc_sku_handker.go
new file mode 100644
index 0000000..d4392f2
--- /dev/null
+++ b/pkg/sku/arc_sku_handker.go
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package sku
+
+var _ CloudSKUHandler = &ArcSKUHandler{}
+
+type ArcSKUHandler struct {
+	supportedSKUs map[string]GPUConfig
+}
+
+func NewArcSKUHandler() *ArcSKUHandler {
+	return &ArcSKUHandler{
+		// The SKU including Azure Local official sku: https://learn.microsoft.com/en-us/azure/aks/aksarc/scale-requirements
+		supportedSKUs: map[string]GPUConfig{
+			"Standard_NK6":      {SKU: "Standard_NK6", GPUCount: 1, GPUMem: 8, GPUModel: "NVIDIA T4"},
+			"Standard_NK12":     {SKU: "Standard_NK12", GPUCount: 2, GPUMem: 16, GPUModel: "NVIDIA T4"},
+			"Standard_NC4_A2":   {SKU: "Standard_NC4_A2", GPUCount: 1, GPUMem: 16, GPUModel: "NVIDIA A2"},
+			"Standard_NC8_A2":   {SKU: "Standard_NC8_A2", GPUCount: 1, GPUMem: 16, GPUModel: "NVIDIA A2"},
+			"Standard_NC16_A2":  {SKU: "Standard_NC16_A2", GPUCount: 2, GPUMem: 48, GPUModel: "NVIDIA A2"},
+			"Standard_NC32_A2":  {SKU: "Standard_NC32_A2", GPUCount: 2, GPUMem: 48, GPUModel: "NVIDIA A2"},
+			"Standard_NC4_A16":  {SKU: "Standard_NC4_A16", GPUCount: 1, GPUMem: 16, GPUModel: "NVIDIA A16"},
+			"Standard_NC8_A16":  {SKU: "Standard_NC8_A16", GPUCount: 1, GPUMem: 16, GPUModel: "NVIDIA A16"},
+			"Standard_NC16_A16": {SKU: "Standard_NC16_A16", GPUCount: 2, GPUMem: 48, GPUModel: "NVIDIA A16"},
+			"Standard_NC32_A16": {SKU: "Standard_NC32_A16", GPUCount: 2, GPUMem: 48, GPUModel: "NVIDIA A16"},
+		},
+	}
+}
+
+func (a *ArcSKUHandler) GetSupportedSKUs() []string {
+	return GetMapKeys(a.supportedSKUs)
+}
+
+func (a *ArcSKUHandler) GetGPUConfigs() map[string]GPUConfig {
+	return a.supportedSKUs
+}
diff --git a/pkg/sku/cloud_sku_handler.go b/pkg/sku/cloud_sku_handler.go
index 7e5187b..0a6fe72 100644
--- a/pkg/sku/cloud_sku_handler.go
+++ b/pkg/sku/cloud_sku_handler.go
@@ -25,6 +25,8 @@ func GetCloudSKUHandler(cloud string) CloudSKUHandler {
 		return NewAzureSKUHandler()
 	case consts.AWSCloudName:
 		return NewAwsSKUHandler()
+	case consts.ArcCloudName:
+		return NewArcSKUHandler()
 	default:
 		return nil
 	}
diff --git a/pkg/utils/common-preset.go b/pkg/utils/common-preset.go
index c9f5f8d..10fc21e 100644
--- a/pkg/utils/common-preset.go
+++ b/pkg/utils/common-preset.go
@@ -3,6 +3,8 @@
 package utils
 
 import (
+	"fmt"
+
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -27,6 +29,45 @@ func ConfigResultsVolume(outputPath string) (corev1.Volume, corev1.VolumeMount)
 	return sharedWorkspaceVolume, sharedVolumeMount
 }
 
+func ConfigImagePullSecretVolume(nameSuffix string, imagePullSecrets []string) (corev1.Volume, corev1.VolumeMount) {
+	name := fmt.Sprintf("docker-config-%s", nameSuffix)
+
+	sources := make([]corev1.VolumeProjection, 0, len(imagePullSecrets))
+	for _, imagePullSecret := range imagePullSecrets {
+		volumeProjection := corev1.VolumeProjection{
+			Secret: &corev1.SecretProjection{
+				LocalObjectReference: corev1.LocalObjectReference{
+					Name: imagePullSecret,
+				},
+				Items: []corev1.KeyToPath{
+					{
+						Key:  ".dockerconfigjson",
+						Path: fmt.Sprintf("%s.json", imagePullSecret),
+					},
+				},
+			},
+		}
+
+		sources = append(sources, volumeProjection)
+	}
+
+	volume := corev1.Volume{
+		Name: name,
+		VolumeSource: corev1.VolumeSource{
+			Projected: &corev1.ProjectedVolumeSource{
+				Sources: sources,
+			},
+		},
+	}
+
+	volumeMount := corev1.VolumeMount{
+		Name:      name,
+		MountPath: fmt.Sprintf("/root/.docker/config.d/%s", nameSuffix),
+	}
+
+	return volume, volumeMount
+}
+
 func ConfigImagePushSecretVolume(imagePushSecret string) (corev1.Volume, corev1.VolumeMount) {
 	volume := corev1.Volume{
 		Name: "docker-config",
@@ -53,7 +94,7 @@ func ConfigImagePushSecretVolume(imagePushSecret string) (corev1.Volume, corev1.
 
 	volumeMount := corev1.VolumeMount{
 		Name:      "docker-config",
-		MountPath: "/tmp/.docker/config",
+		MountPath: "/root/.docker",
 	}
 
 	return volume, volumeMount
diff --git a/pkg/utils/consts/consts.go b/pkg/utils/consts/consts.go
index 68aab1c..1c1512e 100644
--- a/pkg/utils/consts/consts.go
+++ b/pkg/utils/consts/consts.go
@@ -13,6 +13,7 @@ const (
 	DefaultReleaseNamespaceEnvVar = "RELEASE_NAMESPACE"
 	AzureCloudName                = "azure"
 	AWSCloudName                  = "aws"
+	ArcCloudName                  = "arc"
 	GPUString                     = "gpu"
 	SKUString                     = "sku"
 	MaxRevisionHistoryLimit       = 10
@@ -20,7 +21,8 @@ const (
 	NvidiaGPU                     = "nvidia.com/gpu"
 
 	// Feature flags
-	FeatureFlagVLLM = "vLLM"
+	FeatureFlagVLLM            = "vLLM"
+	FeatureFlagEnsureNodeClass = "ensureNodeClass"
 
 	// Nodeclaim related consts
 	KaitoNodePoolName             = "kaito"
diff --git a/pkg/utils/nodeclaim/nodeclaim.go b/pkg/utils/nodeclaim/nodeclaim.go
index 3a40905..74f9906 100644
--- a/pkg/utils/nodeclaim/nodeclaim.go
+++ b/pkg/utils/nodeclaim/nodeclaim.go
@@ -10,6 +10,7 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"reflect"
 	"strings"
 	"time"
 
@@ -26,9 +27,12 @@ import (
 	"k8s.io/utils/clock"
 	"knative.dev/pkg/apis"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/event"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
 )
@@ -36,6 +40,36 @@ import (
 var (
 	// nodeClaimStatusTimeoutInterval is the interval to check the nodeClaim status.
 	nodeClaimStatusTimeoutInterval = 240 * time.Second
+
+	NodeClaimPredicate = predicate.Funcs{
+		CreateFunc: func(e event.CreateEvent) bool {
+			return true
+		},
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			oldNodeClaim, ok := e.ObjectOld.(*karpenterv1.NodeClaim)
+			if !ok {
+				return false
+			}
+
+			newNodeClaim, ok := e.ObjectNew.(*karpenterv1.NodeClaim)
+			if !ok {
+				return false
+			}
+
+			oldNodeClaimCopy := oldNodeClaim.DeepCopy()
+			newNodeClaimCopy := newNodeClaim.DeepCopy()
+
+			// if only nodeclaim.Status.LastPodEventTime is changed, skip update event
+			oldNodeClaimCopy.ResourceVersion = ""
+			oldNodeClaimCopy.Status.LastPodEventTime = metav1.Time{}
+			newNodeClaimCopy.ResourceVersion = ""
+			newNodeClaimCopy.Status.LastPodEventTime = metav1.Time{}
+			return !reflect.DeepEqual(oldNodeClaimCopy, newNodeClaimCopy)
+		},
+		DeleteFunc: func(e event.DeleteEvent) bool {
+			return true
+		},
+	}
 )
 
 // GenerateNodeClaimManifest generates a nodeClaim object from the given workspace or RAGEngine.
@@ -302,10 +336,10 @@ func ListNodeClaim(ctx context.Context, obj interface{}, kubeClient client.Clien
 
 	// Build label selector based on the type of the input object
 	switch o := obj.(type) {
-	case *kaitov1alpha1.Workspace:
+	case *kaitov1beta1.Workspace:
 		ls = labels.Set{
-			kaitov1alpha1.LabelWorkspaceName:      o.Name,
-			kaitov1alpha1.LabelWorkspaceNamespace: o.Namespace,
+			kaitov1beta1.LabelWorkspaceName:      o.Name,
+			kaitov1beta1.LabelWorkspaceNamespace: o.Namespace,
 		}
 	case *kaitov1alpha1.RAGEngine:
 		ls = labels.Set{
diff --git a/pkg/utils/resources/nodes.go b/pkg/utils/resources/nodes.go
index 091ac95..78e1020 100644
--- a/pkg/utils/resources/nodes.go
+++ b/pkg/utils/resources/nodes.go
@@ -14,6 +14,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 )
 
 const (
@@ -91,13 +92,13 @@ func CheckNvidiaPlugin(ctx context.Context, nodeObj *corev1.Node) bool {
 func ExtractObjFields(obj interface{}) (instanceType, namespace, name string, labelSelector *metav1.LabelSelector,
 	nameLabel, namespaceLabel string, err error) {
 	switch o := obj.(type) {
-	case *kaitov1alpha1.Workspace:
+	case *kaitov1beta1.Workspace:
 		instanceType = o.Resource.InstanceType
 		namespace = o.Namespace
 		name = o.Name
 		labelSelector = o.Resource.LabelSelector
-		nameLabel = kaitov1alpha1.LabelWorkspaceName
-		namespaceLabel = kaitov1alpha1.LabelWorkspaceNamespace
+		nameLabel = kaitov1beta1.LabelWorkspaceName
+		namespaceLabel = kaitov1beta1.LabelWorkspaceNamespace
 	case *kaitov1alpha1.RAGEngine:
 		instanceType = o.Spec.Compute.InstanceType
 		namespace = o.Namespace
diff --git a/pkg/workspace/controllers/workspace_controller.go b/pkg/workspace/controllers/workspace_controller.go
index 57a6c11..895a6d6 100644
--- a/pkg/workspace/controllers/workspace_controller.go
+++ b/pkg/workspace/controllers/workspace_controller.go
@@ -28,6 +28,7 @@ import (
 	"k8s.io/klog/v2"
 	"k8s.io/utils/clock"
 	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -35,7 +36,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/nodeclaim"
@@ -69,7 +70,7 @@ func NewWorkspaceReconciler(client client.Client, scheme *runtime.Scheme, log lo
 }
 
 func (c *WorkspaceReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+	workspaceObj := &kaitov1beta1.Workspace{}
 	if err := c.Client.Get(ctx, req.NamespacedName, workspaceObj); err != nil {
 		if !apierrors.IsNotFound(err) {
 			klog.ErrorS(err, "failed to get workspace", "workspace", req.Name)
@@ -106,7 +107,7 @@ func (c *WorkspaceReconciler) Reconcile(ctx context.Context, req reconcile.Reque
 	return result, nil
 }
 
-func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj *kaitov1beta1.Workspace) error {
 	if !controllerutil.ContainsFinalizer(workspaceObj, consts.WorkspaceFinalizer) {
 		patch := client.MergeFrom(workspaceObj.DeepCopy())
 		controllerutil.AddFinalizer(workspaceObj, consts.WorkspaceFinalizer)
@@ -118,11 +119,11 @@ func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj
 	return nil
 }
 
-func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *kaitov1alpha1.Workspace) (reconcile.Result, error) {
+func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (reconcile.Result, error) {
 	// Read ResourceSpec
 	err := c.applyWorkspaceResource(ctx, wObj)
 	if err != nil {
-		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
+		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
 			"workspaceFailed", err.Error()); updateErr != nil {
 			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return reconcile.Result{}, updateErr
@@ -136,7 +137,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 
 	if wObj.Tuning != nil {
 		if err = c.applyTuning(ctx, wObj); err != nil {
-			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
+			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
 				"workspaceFailed", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return reconcile.Result{}, updateErr
@@ -147,7 +148,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 		job := &batchv1.Job{}
 		if err = resources.GetResource(ctx, wObj.Name, wObj.Namespace, c.Client, job); err == nil {
 			if job.Status.Succeeded > 0 {
-				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionTrue,
+				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionTrue,
 					"workspaceSucceeded", "workspace succeeds"); updateErr != nil {
 					klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 					return reconcile.Result{}, updateErr
@@ -157,7 +158,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 				if job.Status.Ready != nil {
 					readyPod = *job.Status.Ready
 				}
-				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
+				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
 					"workspacePending", fmt.Sprintf("workspace has not completed, tuning job has %d active pod, %d ready pod", job.Status.Active, readyPod)); updateErr != nil {
 					klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 					return reconcile.Result{}, updateErr
@@ -169,7 +170,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 		}
 	} else if wObj.Inference != nil {
 		if err := c.ensureService(ctx, wObj); err != nil {
-			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
+			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
 				"workspaceFailed", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return reconcile.Result{}, updateErr
@@ -177,7 +178,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 			return reconcile.Result{}, err
 		}
 		if err = c.applyInference(ctx, wObj); err != nil {
-			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
+			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionFalse,
 				"workspaceFailed", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return reconcile.Result{}, updateErr
@@ -185,7 +186,7 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 			return reconcile.Result{}, err
 		}
 
-		if err = c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeSucceeded, metav1.ConditionTrue,
+		if err = c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeSucceeded, metav1.ConditionTrue,
 			"workspaceSucceeded", "workspace succeeds"); err != nil {
 			klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return reconcile.Result{}, err
@@ -195,9 +196,9 @@ func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *ka
 	return reconcile.Result{}, nil
 }
 
-func (c *WorkspaceReconciler) deleteWorkspace(ctx context.Context, wObj *kaitov1alpha1.Workspace) (reconcile.Result, error) {
+func (c *WorkspaceReconciler) deleteWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (reconcile.Result, error) {
 	klog.InfoS("deleteWorkspace", "workspace", klog.KObj(wObj))
-	err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeDeleting, metav1.ConditionTrue, "workspaceDeleted", "workspace is being deleted")
+	err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeDeleting, metav1.ConditionTrue, "workspaceDeleted", "workspace is being deleted")
 	if err != nil {
 		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 		return reconcile.Result{}, err
@@ -205,7 +206,7 @@ func (c *WorkspaceReconciler) deleteWorkspace(ctx context.Context, wObj *kaitov1
 
 	return c.garbageCollectWorkspace(ctx, wObj)
 }
-func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
 	currentHash := computeHash(wObj)
 	annotations := wObj.GetAnnotations()
 	if annotations == nil {
@@ -245,7 +246,7 @@ func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *
 				WorkspaceNameLabel: wObj.Name,
 			},
 			OwnerReferences: []metav1.OwnerReference{
-				*metav1.NewControllerRef(wObj, kaitov1alpha1.GroupVersion.WithKind("Workspace")),
+				*metav1.NewControllerRef(wObj, kaitov1beta1.GroupVersion.WithKind("Workspace")),
 			},
 		},
 		Revision: revisionNum,
@@ -264,7 +265,7 @@ func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *
 			if err := c.Create(ctx, newRevision); err != nil {
 				return fmt.Errorf("failed to create new ControllerRevision: %w", err)
 			} else {
-				annotations[kaitov1alpha1.WorkspaceRevisionAnnotation] = strconv.FormatInt(revisionNum, 10)
+				annotations[kaitov1beta1.WorkspaceRevisionAnnotation] = strconv.FormatInt(revisionNum, 10)
 			}
 
 			if len(revisions.Items) > consts.MaxRevisionHistoryLimit {
@@ -279,7 +280,7 @@ func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *
 		if controllerRevision.Annotations[WorkspaceHashAnnotation] != newRevision.Annotations[WorkspaceHashAnnotation] {
 			return fmt.Errorf("revision name conflicts, the hash values are different")
 		}
-		annotations[kaitov1alpha1.WorkspaceRevisionAnnotation] = strconv.FormatInt(controllerRevision.Revision, 10)
+		annotations[kaitov1beta1.WorkspaceRevisionAnnotation] = strconv.FormatInt(controllerRevision.Revision, 10)
 	}
 	annotations[WorkspaceHashAnnotation] = currentHash
 	wObj.SetAnnotations(annotations)
@@ -290,7 +291,7 @@ func (c *WorkspaceReconciler) syncControllerRevision(ctx context.Context, wObj *
 	return nil
 }
 
-func marshalSelectedFields(wObj *kaitov1alpha1.Workspace) ([]byte, error) {
+func marshalSelectedFields(wObj *kaitov1beta1.Workspace) ([]byte, error) {
 	partialMap := map[string]interface{}{
 		"resource":  wObj.Resource,
 		"inference": wObj.Inference,
@@ -305,7 +306,7 @@ func marshalSelectedFields(wObj *kaitov1alpha1.Workspace) ([]byte, error) {
 	return jsonData, nil
 }
 
-func computeHash(w *kaitov1alpha1.Workspace) string {
+func computeHash(w *kaitov1beta1.Workspace) string {
 	hasher := sha256.New()
 	encoder := json.NewEncoder(hasher)
 	encoder.Encode(w.Resource)
@@ -315,7 +316,7 @@ func computeHash(w *kaitov1alpha1.Workspace) string {
 }
 
 // applyWorkspaceResource applies workspace resource spec.
-func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
 	// Wait for pending nodeClaims if any before we decide whether to create new node or not.
 	if err := nodeclaim.WaitForPendingNodeClaims(ctx, wObj, c.Client); err != nil {
 		return err
@@ -334,7 +335,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 	if newNodesCount > 0 {
 		klog.InfoS("need to create more nodes", "NodeCount", newNodesCount)
 		if err := c.updateStatusConditionIfNotMatch(ctx, wObj,
-			kaitov1alpha1.ConditionTypeNodeClaimStatus, metav1.ConditionUnknown,
+			kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionUnknown,
 			"CreateNodeClaimPending", fmt.Sprintf("creating %d nodeClaims", newNodesCount)); err != nil {
 			klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return err
@@ -343,7 +344,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 		for i := 0; i < newNodesCount; i++ {
 			newNode, err := c.createAndValidateNode(ctx, wObj)
 			if err != nil {
-				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeResourceStatus, metav1.ConditionFalse,
+				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
 					"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
 					klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 					return updateErr
@@ -359,7 +360,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 		for i := range selectedNodes {
 			err = c.ensureNodePlugins(ctx, wObj, selectedNodes[i])
 			if err != nil {
-				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeResourceStatus, metav1.ConditionFalse,
+				if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
 					"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
 					klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 					return updateErr
@@ -370,7 +371,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 	}
 
 	if err = c.updateStatusConditionIfNotMatch(ctx, wObj,
-		kaitov1alpha1.ConditionTypeNodeClaimStatus, metav1.ConditionTrue,
+		kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionTrue,
 		"installNodePluginsSuccess", "nodeClaim plugins have been installed successfully"); err != nil {
 		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 		return err
@@ -379,7 +380,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 	// Add the valid nodes names to the WorkspaceStatus.WorkerNodes.
 	err = c.updateStatusNodeListIfNotMatch(ctx, wObj, selectedNodes)
 	if err != nil {
-		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeResourceStatus, metav1.ConditionFalse,
+		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
 			"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
 			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return updateErr
@@ -387,7 +388,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 		return err
 	}
 
-	if err = c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeResourceStatus, metav1.ConditionTrue,
+	if err = c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionTrue,
 		"workspaceResourceStatusSuccess", "workspace resource is ready"); err != nil {
 		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 		return err
@@ -396,7 +397,7 @@ func (c *WorkspaceReconciler) applyWorkspaceResource(ctx context.Context, wObj *
 	return nil
 }
 
-func (c *WorkspaceReconciler) getAllQualifiedNodes(ctx context.Context, wObj *kaitov1alpha1.Workspace) ([]*corev1.Node, error) {
+func (c *WorkspaceReconciler) getAllQualifiedNodes(ctx context.Context, wObj *kaitov1beta1.Workspace) ([]*corev1.Node, error) {
 	var qualifiedNodes []*corev1.Node
 
 	nodeList, err := resources.ListNodes(ctx, c.Client, wObj.Resource.LabelSelector.MatchLabels)
@@ -442,7 +443,7 @@ func (c *WorkspaceReconciler) getAllQualifiedNodes(ctx context.Context, wObj *ka
 }
 
 // createAndValidateNode creates a new node and validates status.
-func (c *WorkspaceReconciler) createAndValidateNode(ctx context.Context, wObj *kaitov1alpha1.Workspace) (*corev1.Node, error) {
+func (c *WorkspaceReconciler) createAndValidateNode(ctx context.Context, wObj *kaitov1beta1.Workspace) (*corev1.Node, error) {
 	var nodeOSDiskSize string
 	if wObj.Inference != nil && wObj.Inference.Preset != nil && wObj.Inference.Preset.Name != "" {
 		presetName := string(wObj.Inference.Preset.Name)
@@ -456,7 +457,7 @@ func (c *WorkspaceReconciler) createAndValidateNode(ctx context.Context, wObj *k
 	return c.CreateNodeClaim(ctx, wObj, nodeOSDiskSize)
 }
 
-func (c *WorkspaceReconciler) CreateNodeClaim(ctx context.Context, wObj *kaitov1alpha1.Workspace, nodeOSDiskSize string) (*corev1.Node, error) {
+func (c *WorkspaceReconciler) CreateNodeClaim(ctx context.Context, wObj *kaitov1beta1.Workspace, nodeOSDiskSize string) (*corev1.Node, error) {
 RetryWithDifferentName:
 	newNodeClaim := nodeclaim.GenerateNodeClaimManifest(ctx, nodeOSDiskSize, wObj)
 
@@ -467,7 +468,7 @@ RetryWithDifferentName:
 		} else {
 
 			klog.ErrorS(err, "failed to create nodeClaim", "nodeClaim", newNodeClaim.Name)
-			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
+			if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
 				"nodeClaimFailedCreation", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return nil, updateErr
@@ -479,7 +480,7 @@ RetryWithDifferentName:
 	// check nodeClaim status until it is ready
 	err := nodeclaim.CheckNodeClaimStatus(ctx, newNodeClaim, c.Client)
 	if err != nil {
-		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
+		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
 			"checkNodeClaimStatusFailed", err.Error()); updateErr != nil {
 			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return nil, updateErr
@@ -492,7 +493,7 @@ RetryWithDifferentName:
 }
 
 // ensureNodePlugins ensures node plugins are installed.
-func (c *WorkspaceReconciler) ensureNodePlugins(ctx context.Context, wObj *kaitov1alpha1.Workspace, nodeObj *corev1.Node) error {
+func (c *WorkspaceReconciler) ensureNodePlugins(ctx context.Context, wObj *kaitov1beta1.Workspace, nodeObj *corev1.Node) error {
 	timeClock := clock.RealClock{}
 	tick := timeClock.NewTicker(consts.NodePluginInstallTimeout)
 	defer tick.Stop()
@@ -509,7 +510,7 @@ func (c *WorkspaceReconciler) ensureNodePlugins(ctx context.Context, wObj *kaito
 				if err := resources.UpdateNodeWithLabel(ctx, nodeObj.Name, resources.LabelKeyNvidia, resources.LabelValueNvidia, c.Client); err != nil {
 					if apierrors.IsNotFound(err) {
 						klog.ErrorS(err, "nvidia plugin cannot be installed, node not found", "node", nodeObj.Name)
-						if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
+						if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.ConditionTypeNodeClaimStatus, metav1.ConditionFalse,
 							"checkNodeClaimStatusFailed", err.Error()); updateErr != nil {
 							klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 							return updateErr
@@ -525,7 +526,7 @@ func (c *WorkspaceReconciler) ensureNodePlugins(ctx context.Context, wObj *kaito
 }
 
 // getPresetName returns the preset name from wObj if available
-func getPresetName(wObj *kaitov1alpha1.Workspace) string {
+func getPresetName(wObj *kaitov1beta1.Workspace) string {
 	if wObj.Inference != nil && wObj.Inference.Preset != nil {
 		return string(wObj.Inference.Preset.Name)
 	}
@@ -535,12 +536,12 @@ func getPresetName(wObj *kaitov1alpha1.Workspace) string {
 	return ""
 }
 
-func (c *WorkspaceReconciler) ensureService(ctx context.Context, wObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) ensureService(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
 	serviceType := corev1.ServiceTypeClusterIP
 	wAnnotation := wObj.GetAnnotations()
 
 	if len(wAnnotation) != 0 {
-		val, found := wAnnotation[kaitov1alpha1.AnnotationEnableLB]
+		val, found := wAnnotation[kaitov1beta1.AnnotationEnableLB]
 		if found && val == "True" {
 			serviceType = corev1.ServiceTypeLoadBalancer
 		}
@@ -577,7 +578,7 @@ func (c *WorkspaceReconciler) ensureService(ctx context.Context, wObj *kaitov1al
 	return nil
 }
 
-func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
 	var err error
 	func() {
 		if wObj.Tuning.Preset != nil {
@@ -586,11 +587,11 @@ func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1alph
 
 			tuningParam := model.GetTuningParameters()
 			existingObj := &batchv1.Job{}
-			revisionNum := wObj.Annotations[kaitov1alpha1.WorkspaceRevisionAnnotation]
+			revisionNum := wObj.Annotations[kaitov1beta1.WorkspaceRevisionAnnotation]
 			if err = resources.GetResource(ctx, wObj.Name, wObj.Namespace, c.Client, existingObj); err == nil {
 				klog.InfoS("A tuning workload already exists for workspace", "workspace", klog.KObj(wObj))
 
-				if existingObj.Annotations[kaitov1alpha1.WorkspaceRevisionAnnotation] != revisionNum {
+				if existingObj.Annotations[kaitov1beta1.WorkspaceRevisionAnnotation] != revisionNum {
 					deletePolicy := metav1.DeletePropagationForeground
 					if err := c.Delete(ctx, existingObj, &client.DeleteOptions{
 						PropagationPolicy: &deletePolicy,
@@ -624,7 +625,7 @@ func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1alph
 	}()
 
 	if err != nil {
-		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeTuningJobStatus, metav1.ConditionFalse,
+		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeTuningJobStatus, metav1.ConditionFalse,
 			"WorkspaceTuningJobStatusFailed", err.Error()); updateErr != nil {
 			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return updateErr
@@ -632,7 +633,7 @@ func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1alph
 		return err
 	}
 
-	if err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeTuningJobStatus, metav1.ConditionTrue,
+	if err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeTuningJobStatus, metav1.ConditionTrue,
 		"WorkspaceTuningJobStatusStarted", "Tuning job has started"); err != nil {
 		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 		return err
@@ -642,7 +643,7 @@ func (c *WorkspaceReconciler) applyTuning(ctx context.Context, wObj *kaitov1alph
 }
 
 // applyInference applies inference spec.
-func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1alpha1.Workspace) error {
+func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
 	var err error
 	func() {
 		if wObj.Inference.Template != nil {
@@ -668,12 +669,12 @@ func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1a
 				existingObj = &appsv1.Deployment{}
 
 			}
-			revisionStr := wObj.Annotations[kaitov1alpha1.WorkspaceRevisionAnnotation]
+			revisionStr := wObj.Annotations[kaitov1beta1.WorkspaceRevisionAnnotation]
 			if err = resources.GetResource(ctx, wObj.Name, wObj.Namespace, c.Client, existingObj); err == nil {
 				klog.InfoS("An inference workload already exists for workspace", "workspace", klog.KObj(wObj))
 				if !model.SupportDistributedInference() {
 					deployment := existingObj.(*appsv1.Deployment)
-					if deployment.Annotations[kaitov1alpha1.WorkspaceRevisionAnnotation] != revisionStr {
+					if deployment.Annotations[kaitov1beta1.WorkspaceRevisionAnnotation] != revisionStr {
 						// TODO: respect updates to other fields
 						var volumes []corev1.Volume
 						var volumeMounts []corev1.VolumeMount
@@ -690,17 +691,17 @@ func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1a
 							volumes = append(volumes, adapterVolume)
 							volumeMounts = append(volumeMounts, adapterVolumeMount)
 						}
-						initContainers, envs := manifests.GenerateInitContainers(wObj, volumeMounts)
-						spec := &deployment.Spec
 
-						spec.Template.Spec.InitContainers = initContainers
-						spec.Template.Spec.Containers[0].Env = envs
-						spec.Template.Spec.Containers[0].VolumeMounts = volumeMounts
-						deployment.Annotations[kaitov1alpha1.WorkspaceRevisionAnnotation] = revisionStr
-						spec.Template.Spec.Volumes = volumes
+						pullerContainers, pullerEnvVars, pullerVolumes := manifests.GeneratePullerContainers(wObj, volumeMounts)
+						volumes = append(volumes, pullerVolumes...)
 
-						_, imagePullSecrets := inference.GetInferenceImageInfo(ctx, wObj, inferenceParam)
-						deployment.Spec.Template.Spec.ImagePullSecrets = imagePullSecrets
+						spec := &deployment.Spec.Template.Spec
+						spec.Containers[0].Env = pullerEnvVars
+						spec.Containers[0].VolumeMounts = volumeMounts
+						spec.InitContainers = pullerContainers
+						spec.Volumes = volumes
+
+						deployment.Annotations[kaitov1beta1.WorkspaceRevisionAnnotation] = revisionStr
 
 						if err := c.Update(ctx, deployment); err != nil {
 							return
@@ -725,7 +726,7 @@ func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1a
 	}()
 
 	if err != nil {
-		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeInferenceStatus, metav1.ConditionFalse,
+		if updateErr := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeInferenceStatus, metav1.ConditionFalse,
 			"WorkspaceInferenceStatusFailed", err.Error()); updateErr != nil {
 			klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 			return updateErr
@@ -734,7 +735,7 @@ func (c *WorkspaceReconciler) applyInference(ctx context.Context, wObj *kaitov1a
 		}
 	}
 
-	if err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1alpha1.WorkspaceConditionTypeInferenceStatus, metav1.ConditionTrue,
+	if err := c.updateStatusConditionIfNotMatch(ctx, wObj, kaitov1beta1.WorkspaceConditionTypeInferenceStatus, metav1.ConditionTrue,
 		"WorkspaceInferenceStatusSuccess", "Inference has been deployed successfully"); err != nil {
 		klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
 		return err
@@ -754,12 +755,12 @@ func (c *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	}
 
 	builder := ctrl.NewControllerManagedBy(mgr).
-		For(&kaitov1alpha1.Workspace{}).
+		For(&kaitov1beta1.Workspace{}).
 		Owns(&appsv1.ControllerRevision{}).
 		Owns(&appsv1.Deployment{}).
 		Owns(&appsv1.StatefulSet{}).
 		Owns(&batchv1.Job{}).
-		Watches(&karpenterv1.NodeClaim{}, c.watchNodeClaims()).
+		Watches(&karpenterv1.NodeClaim{}, c.watchNodeClaims(), builder.WithPredicates(nodeclaim.NodeClaimPredicate)).
 		WithOptions(controller.Options{MaxConcurrentReconciles: 5})
 
 	return builder.Complete(c)
@@ -770,11 +771,11 @@ func (c *WorkspaceReconciler) watchNodeClaims() handler.EventHandler {
 	return handler.EnqueueRequestsFromMapFunc(
 		func(ctx context.Context, o client.Object) []reconcile.Request {
 			nodeClaimObj := o.(*karpenterv1.NodeClaim)
-			name, ok := nodeClaimObj.Labels[kaitov1alpha1.LabelWorkspaceName]
+			name, ok := nodeClaimObj.Labels[kaitov1beta1.LabelWorkspaceName]
 			if !ok {
 				return nil
 			}
-			namespace, ok := nodeClaimObj.Labels[kaitov1alpha1.LabelWorkspaceNamespace]
+			namespace, ok := nodeClaimObj.Labels[kaitov1beta1.LabelWorkspaceNamespace]
 			if !ok {
 				return nil
 			}
diff --git a/pkg/workspace/controllers/workspace_gc_finalizer.go b/pkg/workspace/controllers/workspace_gc_finalizer.go
index a389cd3..fce8f7c 100644
--- a/pkg/workspace/controllers/workspace_gc_finalizer.go
+++ b/pkg/workspace/controllers/workspace_gc_finalizer.go
@@ -11,13 +11,13 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/nodeclaim"
 )
 
 // garbageCollectWorkspace remove finalizer associated with workspace object.
-func (c *WorkspaceReconciler) garbageCollectWorkspace(ctx context.Context, wObj *kaitov1alpha1.Workspace) (ctrl.Result, error) {
+func (c *WorkspaceReconciler) garbageCollectWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (ctrl.Result, error) {
 	klog.InfoS("garbageCollectWorkspace", "workspace", klog.KObj(wObj))
 
 	// Check if there are any nodeClaims associated with this workspace.
@@ -29,6 +29,7 @@ func (c *WorkspaceReconciler) garbageCollectWorkspace(ctx context.Context, wObj
 	// We should delete all the nodeClaims that are created by this workspace
 	for i := range ncList.Items {
 		if ncList.Items[i].DeletionTimestamp.IsZero() {
+			klog.InfoS("Deleting associated NodeClaim...", "nodeClaim", ncList.Items[i].Name)
 			if deleteErr := c.Delete(ctx, &ncList.Items[i], &client.DeleteOptions{}); deleteErr != nil {
 				klog.ErrorS(deleteErr, "failed to delete the nodeClaim", "nodeClaim", klog.KObj(&ncList.Items[i]))
 				return ctrl.Result{}, deleteErr
diff --git a/pkg/workspace/controllers/workspace_status.go b/pkg/workspace/controllers/workspace_status.go
index 33fdcce..08414d5 100644
--- a/pkg/workspace/controllers/workspace_status.go
+++ b/pkg/workspace/controllers/workspace_status.go
@@ -17,7 +17,7 @@ import (
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 )
 
 func (c *WorkspaceReconciler) updateWorkspaceStatus(ctx context.Context, name *client.ObjectKey, condition *metav1.Condition, workerNodes []string) error {
@@ -27,7 +27,7 @@ func (c *WorkspaceReconciler) updateWorkspaceStatus(ctx context.Context, name *c
 		},
 		func() error {
 			// Read the latest version to avoid update conflict.
-			wObj := &kaitov1alpha1.Workspace{}
+			wObj := &kaitov1beta1.Workspace{}
 			if err := c.Client.Get(ctx, *name, wObj); err != nil {
 				if !apierrors.IsNotFound(err) {
 					return err
@@ -44,7 +44,7 @@ func (c *WorkspaceReconciler) updateWorkspaceStatus(ctx context.Context, name *c
 		})
 }
 
-func (c *WorkspaceReconciler) updateStatusConditionIfNotMatch(ctx context.Context, wObj *kaitov1alpha1.Workspace, cType kaitov1alpha1.ConditionType,
+func (c *WorkspaceReconciler) updateStatusConditionIfNotMatch(ctx context.Context, wObj *kaitov1beta1.Workspace, cType kaitov1beta1.ConditionType,
 	cStatus metav1.ConditionStatus, cReason, cMessage string) error {
 	if curCondition := meta.FindStatusCondition(wObj.Status.Conditions, string(cType)); curCondition != nil {
 		if curCondition.Status == cStatus && curCondition.Reason == cReason && curCondition.Message == cMessage {
@@ -63,7 +63,7 @@ func (c *WorkspaceReconciler) updateStatusConditionIfNotMatch(ctx context.Contex
 	return c.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, &cObj, nil)
 }
 
-func (c *WorkspaceReconciler) updateStatusNodeListIfNotMatch(ctx context.Context, wObj *kaitov1alpha1.Workspace, validNodeList []*corev1.Node) error {
+func (c *WorkspaceReconciler) updateStatusNodeListIfNotMatch(ctx context.Context, wObj *kaitov1beta1.Workspace, validNodeList []*corev1.Node) error {
 	nodeNameList := lo.Map(validNodeList, func(v *corev1.Node, _ int) string {
 		return v.Name
 	})
diff --git a/pkg/workspace/image/README.md b/pkg/workspace/image/README.md
new file mode 100644
index 0000000..8a40f6d
--- /dev/null
+++ b/pkg/workspace/image/README.md
@@ -0,0 +1,31 @@
+# Kaito Adapter Image Format Specification
+
+This document describes the specification for Kaito-compatible adapter images. It assumes the reader is familiar with the [OCI Image Format Specification](https://github.com/opencontainers/image-spec).
+
+## Image Layout
+
+A Kaito-compatible adapter image consists of any OCI image with the file structure below. Files added to the image but not specified here are simply ignored. The image can use any base and have as many layers as needed.
+```
+/
+└── data
+    ├── adapter_config.json
+    └── adapter_model.safetensors
+```
+
+A minimal and conformant image can be created by building the following Dockerfile:
+
+```dockerfile
+FROM scratch
+COPY adapter_config.json       /data/
+COPY adapter_model.safetensors /data/
+```
+
+## Building Platform-Independent Images
+
+To build a platform-independent image compatible with Kaito, you can leverage [pusher.sh](pusher.sh). Simply run `./pusher.sh ${PATH_TO_DATA_DIRECTORY} ${IMAGE_REFERENCE}` to build and push a platform-independent image containing the files in the specified directory.
+
+The following example pushes `~/Documents/kaito/adapter/data` to `ghcr.io/kaito-project/kaito/adapter` with tag `1.2.3`:
+
+```bash
+./pusher.sh ~/Documents/kaito/adapter/data ghcr.io/kaito-project/kaito/adapter:1.2.3
+```
diff --git a/pkg/workspace/image/puller.go b/pkg/workspace/image/puller.go
new file mode 100644
index 0000000..2cf6e7f
--- /dev/null
+++ b/pkg/workspace/image/puller.go
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package image
+
+import (
+	"bytes"
+	_ "embed"
+	"log"
+	"text/template"
+
+	"github.com/distribution/reference"
+	corev1 "k8s.io/api/core/v1"
+)
+
+var (
+	//go:embed puller.sh
+	pullerSHTextData string
+
+	pullerSHTemplate *template.Template
+)
+
+func init() {
+	t, err := template.New("puller.sh").Option("missingkey=zero").Parse(pullerSHTextData)
+	if err != nil {
+		panic(err)
+	}
+
+	pullerSHTemplate = t
+}
+
+func renderPullerSH(imgRef string, volDir string) string {
+	normalizedImgRef, err := reference.ParseDockerRef(imgRef)
+	if err != nil {
+		log.Printf("failed to normalize image reference `%s`: %v", imgRef, err)
+	}
+	if normalizedImgRef != nil {
+		imgRef = normalizedImgRef.String()
+	}
+
+	data := map[string]string{
+		"imgRef": imgRef,
+		"volDir": volDir,
+	}
+
+	var buf bytes.Buffer
+	if err := pullerSHTemplate.Execute(&buf, data); err != nil {
+		panic(err)
+	}
+
+	return buf.String()
+}
+
+func NewPullerContainer(inputImage string, outputDirectory string) *corev1.Container {
+	return &corev1.Container{
+		Name:  "puller",
+		Image: "quay.io/skopeo/stable:v1.18.0-immutable",
+		Command: []string{
+			"/bin/sh",
+			"-c",
+		},
+		Args: []string{
+			renderPullerSH(inputImage, outputDirectory),
+		},
+	}
+}
diff --git a/pkg/workspace/image/puller.sh b/pkg/workspace/image/puller.sh
new file mode 100755
index 0000000..095a1a8
--- /dev/null
+++ b/pkg/workspace/image/puller.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+set -ex
+
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+TMPDIR="$(mktemp -d)"
+
+IMG_REF="${1}"
+[ ! -z "${IMG_REF}" ] || IMG_REF='{{ .imgRef }}'
+
+VOL_DIR="${2}"
+[ ! -z "${VOL_DIR}" ] || VOL_DIR='{{ .volDir }}'
+
+#{{`
+
+pull() {
+    LAYOUT_DIR="${TMPDIR}/layout"
+
+    local DOCKER_CONFIG_D="${HOME}/.docker/config.d"
+    [ ! -e "${DOCKER_CONFIG_D}" ] || local REGISTRY_AUTH_FILES="$(find -L "${DOCKER_CONFIG_D}" -type 'f' -printf '%p ')"
+
+    for REGISTRY_AUTH_FILE in '' ${REGISTRY_AUTH_FILES}
+    do
+        local SKOPEO_ARGS='--dest-decompress'
+        [ -z "${REGISTRY_AUTH_FILE}" ] || SKOPEO_ARGS="${SKOPEO_ARGS} --authfile=${REGISTRY_AUTH_FILE}"
+
+        skopeo copy ${SKOPEO_ARGS} "docker://${IMG_REF}" "dir:${LAYOUT_DIR}" || continue
+
+        local OK='1'
+        break
+    done
+
+    [ "${OK}" = '1' ]
+}
+
+expand() {
+    ROOTFS_DIR="${TMPDIR}/rootfs"
+    mkdir -p "${ROOTFS_DIR}"
+
+    local LAYER_DIFFS="$(skopeo inspect --format '{{ range .Layers }}{{ slice . 7 }} {{ end }}' "dir:${LAYOUT_DIR}")"
+    for LAYER_DIFF in ${LAYER_DIFFS}
+    do
+        local LAYER_PATH="${LAYOUT_DIR}/${LAYER_DIFF}"
+        tar x -f "${LAYER_PATH}" -C "${ROOTFS_DIR}"
+    done
+}
+
+relocate() {
+    local DATA_PATH="${ROOTFS_DIR}/data"
+    mv -f "${DATA_PATH}" "${VOL_DIR}"
+}
+
+#`}}
+
+pull
+expand
+relocate
diff --git a/pkg/workspace/image/pusher.go b/pkg/workspace/image/pusher.go
new file mode 100644
index 0000000..ab1f8b3
--- /dev/null
+++ b/pkg/workspace/image/pusher.go
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package image
+
+import (
+	"bytes"
+	_ "embed"
+	"encoding/json"
+	"log"
+	"path"
+	"text/template"
+
+	"github.com/distribution/reference"
+	corev1 "k8s.io/api/core/v1"
+)
+
+var (
+	//go:embed pusher.sh
+	pusherSHTextData string
+
+	pusherSHTemplate *template.Template
+)
+
+func init() {
+	t, err := template.New("pusher.sh").Option("missingkey=zero").Parse(pusherSHTextData)
+	if err != nil {
+		panic(err)
+	}
+
+	pusherSHTemplate = t
+}
+
+func renderPusherSH(volDir string, imgRef string, annotationsData map[string]map[string]string, sentinelPath *string) string {
+	normalizedImgRef, err := reference.ParseDockerRef(imgRef)
+	if err != nil {
+		log.Printf("failed to normalize image reference `%s`: %v", imgRef, err)
+	}
+	if normalizedImgRef != nil {
+		imgRef = normalizedImgRef.String()
+	}
+
+	data := map[string]string{
+		"volDir":          volDir,
+		"imgRef":          imgRef,
+		"annotationsData": "{}",
+		"sentinelPath":    path.Join(volDir, "fine_tuning_completed.txt"),
+	}
+
+	if annotationsData != nil {
+		annotationsData, err := json.Marshal(annotationsData)
+		if err != nil {
+			panic(err)
+		}
+
+		data["annotationsData"] = string(annotationsData)
+	}
+
+	if sentinelPath != nil {
+		data["sentinelPath"] = *sentinelPath
+	}
+
+	var buf bytes.Buffer
+	if err := pusherSHTemplate.Execute(&buf, data); err != nil {
+		panic(err)
+	}
+
+	return buf.String()
+}
+
+func NewPusherContainer(inputDirectory string, outputImage string, annotationsData map[string]map[string]string, sentinelPath *string) *corev1.Container {
+	return &corev1.Container{
+		Name:  "pusher",
+		Image: "ghcr.io/oras-project/oras:v1.2.2",
+		Command: []string{
+			"/bin/sh",
+			"-c",
+		},
+		Args: []string{
+			renderPusherSH(inputDirectory, outputImage, annotationsData, sentinelPath),
+		},
+	}
+}
diff --git a/pkg/workspace/image/pusher.sh b/pkg/workspace/image/pusher.sh
new file mode 100755
index 0000000..75820f5
--- /dev/null
+++ b/pkg/workspace/image/pusher.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+set -ex
+
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+TMPDIR="$(mktemp -d)"
+
+VOL_DIR="${1}"
+[ ! -z "${VOL_DIR}" ] || VOL_DIR='{{ .volDir }}'
+
+IMG_REF="${2}"
+[ ! -z "${IMG_REF}" ] || IMG_REF='{{ .imgRef }}'
+
+[ ! -z '{{ "" }}' ] || ANNOTATIONS_DATA='{{ .annotationsData }}'
+ANNOTATIONS_DATA="${ANNOTATIONS_DATA:-{}}"
+
+[ ! -z '{{ "" }}' ] || SENTINEL_PATH='{{ .sentinelPath }}'
+SENTINEL_PATH="${SENTINEL_PATH:-.}"
+
+#{{`
+
+wait() {
+    until [ -e "${SENTINEL_PATH}" ]
+    do
+        sleep 1
+    done
+}
+
+mklayer() {
+    local DATA_DIR="${TMPDIR}/data"
+    mkdir -p "${DATA_DIR}"
+
+    cp -R "${VOL_DIR}/adapter_config.json" "${VOL_DIR}/adapter_model.safetensors" "${DATA_DIR}"
+
+    local TAR_LAYER_PATH="${TMPDIR}/layer.tar"
+
+    tar c -f "${TAR_LAYER_PATH}" -C "$(dirname "${DATA_DIR}")" "$(basename "${DATA_DIR}")"
+
+    TAR_LAYER_DIFF="$(sha256sum "${TAR_LAYER_PATH}" | cut -d ' ' -f '1')"
+
+    gzip -9 "${TAR_LAYER_PATH}"
+
+    TGZ_LAYER_MIME='application/vnd.oci.image.layer.v1.tar+gzip'
+    TGZ_LAYER_PATH="${TAR_LAYER_PATH}.gz"
+}
+
+mkconfig() {
+    CONFIG_MIME='application/vnd.oci.image.config.v1+json'
+    CONFIG_PATH="${TMPDIR}/config.json"
+
+    printf '{"rootfs":{"diff_ids":["sha256:%s"]}}' "${TAR_LAYER_DIFF}" > "${CONFIG_PATH}"
+}
+
+mkannotations() {
+    ANNOTATIONS_PATH="${TMPDIR}/annotations.json"
+
+    printf '%s' "${ANNOTATIONS_DATA}" > "${ANNOTATIONS_PATH}"
+}
+
+mklayout() {
+    LAYOUT_REF="${TMPDIR}/layout:latest"
+
+    cd "$(dirname "${TGZ_LAYER_PATH}")"
+    oras push --disable-path-validation --annotation-file "${ANNOTATIONS_PATH}" --config "${CONFIG_PATH}:${CONFIG_MIME}" --oci-layout "${LAYOUT_REF}" "$(basename "${TGZ_LAYER_PATH}"):${TGZ_LAYER_MIME}"
+    cd -
+}
+
+push() {
+    oras cp --from-oci-layout "${LAYOUT_REF}" "${IMG_REF}"
+}
+
+resume() {
+    killall -SIGCHLD 'pause' 0</dev/null 1>&0 2>&0 || true
+}
+
+#`}}
+
+wait
+mklayer
+mkconfig
+mkannotations
+mklayout
+push
+resume
diff --git a/pkg/workspace/inference/preset-inference-types.go b/pkg/workspace/inference/preset-inference-types.go
index 205d0b4..a4d3dfb 100644
--- a/pkg/workspace/inference/preset-inference-types.go
+++ b/pkg/workspace/inference/preset-inference-types.go
@@ -54,8 +54,8 @@ var (
 		"gpu_ids":       DefaultGPUIds,
 	}
 
-	DefaultVLLMCommand         = "python3 /workspace/vllm/inference_api.py"
-	DefautTransformersMainFile = "/workspace/tfs/inference_api.py"
+	DefaultVLLMCommand          = "python3 /workspace/vllm/inference_api.py"
+	DefaultTransformersMainFile = "/workspace/tfs/inference_api.py"
 
 	DefaultImagePullSecrets = []corev1.LocalObjectReference{}
 )
diff --git a/pkg/workspace/inference/preset-inferences.go b/pkg/workspace/inference/preset-inferences.go
index ca4fcf2..bca56bf 100644
--- a/pkg/workspace/inference/preset-inferences.go
+++ b/pkg/workspace/inference/preset-inferences.go
@@ -14,7 +14,7 @@ import (
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
@@ -70,8 +70,8 @@ var (
 	}
 )
 
-func updateTorchParamsForDistributedInference(ctx context.Context, kubeClient client.Client, wObj *v1alpha1.Workspace, inferenceParam *model.PresetParam) error {
-	runtimeName := v1alpha1.GetWorkspaceRuntimeName(wObj)
+func updateTorchParamsForDistributedInference(ctx context.Context, kubeClient client.Client, wObj *v1beta1.Workspace, inferenceParam *model.PresetParam) error {
+	runtimeName := v1beta1.GetWorkspaceRuntimeName(wObj)
 	if runtimeName != model.RuntimeNameHuggingfaceTransformers {
 		return fmt.Errorf("distributed inference is not supported for runtime %s", runtimeName)
 	}
@@ -100,7 +100,7 @@ func updateTorchParamsForDistributedInference(ctx context.Context, kubeClient cl
 	return nil
 }
 
-func GetInferenceImageInfo(ctx context.Context, workspaceObj *v1alpha1.Workspace, presetObj *model.PresetParam) (string, []corev1.LocalObjectReference) {
+func GetInferenceImageInfo(ctx context.Context, workspaceObj *v1beta1.Workspace, presetObj *model.PresetParam) (string, []corev1.LocalObjectReference) {
 	imagePullSecretRefs := []corev1.LocalObjectReference{}
 	// Check if the workspace preset's access mode is private
 	if len(workspaceObj.Inference.Adapters) > 0 {
@@ -110,7 +110,7 @@ func GetInferenceImageInfo(ctx context.Context, workspaceObj *v1alpha1.Workspace
 			}
 		}
 	}
-	if string(workspaceObj.Inference.Preset.AccessMode) == string(v1alpha1.ModelImageAccessModePrivate) {
+	if string(workspaceObj.Inference.Preset.AccessMode) == string(v1beta1.ModelImageAccessModePrivate) {
 		imageName := workspaceObj.Inference.Preset.PresetOptions.Image
 		for _, secretName := range workspaceObj.Inference.Preset.PresetOptions.ImagePullSecrets {
 			imagePullSecretRefs = append(imagePullSecretRefs, corev1.LocalObjectReference{Name: secretName})
@@ -126,7 +126,7 @@ func GetInferenceImageInfo(ctx context.Context, workspaceObj *v1alpha1.Workspace
 	}
 }
 
-func CreatePresetInference(ctx context.Context, workspaceObj *v1alpha1.Workspace, revisionNum string,
+func CreatePresetInference(ctx context.Context, workspaceObj *v1beta1.Workspace, revisionNum string,
 	model model.Model, kubeClient client.Client) (client.Object, error) {
 	inferenceParam := model.GetInferenceParameters().DeepCopy()
 
@@ -136,7 +136,7 @@ func CreatePresetInference(ctx context.Context, workspaceObj *v1alpha1.Workspace
 			Namespace: workspaceObj.Namespace,
 		},
 		client.ObjectKey{
-			Name: v1alpha1.DefaultInferenceConfigTemplate,
+			Name: v1beta1.DefaultInferenceConfigTemplate,
 		},
 	)
 	if err != nil {
@@ -190,7 +190,7 @@ func CreatePresetInference(ctx context.Context, workspaceObj *v1alpha1.Workspace
 	}
 
 	// inference command
-	runtimeName := v1alpha1.GetWorkspaceRuntimeName(workspaceObj)
+	runtimeName := v1beta1.GetWorkspaceRuntimeName(workspaceObj)
 	commands := inferenceParam.GetInferenceCommand(runtimeName, skuNumGPUs, &cmVolumeMount)
 
 	image, imagePullSecrets := GetInferenceImageInfo(ctx, workspaceObj, inferenceParam)
diff --git a/pkg/workspace/inference/template_inference.go b/pkg/workspace/inference/template_inference.go
index 59b8504..4a37a58 100644
--- a/pkg/workspace/inference/template_inference.go
+++ b/pkg/workspace/inference/template_inference.go
@@ -7,12 +7,12 @@ import (
 
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/workspace/manifests"
 )
 
-func CreateTemplateInference(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, kubeClient client.Client) (client.Object, error) {
+func CreateTemplateInference(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, kubeClient client.Client) (client.Object, error) {
 	depObj := manifests.GenerateDeploymentManifestWithPodTemplate(ctx, workspaceObj, tolerations)
 	err := resources.CreateResource(ctx, client.Object(depObj), kubeClient)
 	if client.IgnoreAlreadyExists(err) != nil {
diff --git a/pkg/workspace/manifests/manifests.go b/pkg/workspace/manifests/manifests.go
index a4a4a97..350d4d5 100644
--- a/pkg/workspace/manifests/manifests.go
+++ b/pkg/workspace/manifests/manifests.go
@@ -6,6 +6,7 @@ package manifests
 import (
 	"context"
 	"fmt"
+	"path"
 
 	"github.com/samber/lo"
 	appsv1 "k8s.io/api/apps/v1"
@@ -15,15 +16,17 @@ import (
 	"k8s.io/apimachinery/pkg/util/intstr"
 	"k8s.io/utils/ptr"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/utils"
+	"github.com/kaito-project/kaito/pkg/workspace/image"
 )
 
 var controller = true
 
-func GenerateHeadlessServiceManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace) *corev1.Service {
+func GenerateHeadlessServiceManifest(ctx context.Context, workspaceObj *kaitov1beta1.Workspace) *corev1.Service {
 	serviceName := fmt.Sprintf("%s-headless", workspaceObj.Name)
 	selector := map[string]string{
-		kaitov1alpha1.LabelWorkspaceName: workspaceObj.Name,
+		kaitov1beta1.LabelWorkspaceName: workspaceObj.Name,
 	}
 
 	return &corev1.Service{
@@ -32,7 +35,7 @@ func GenerateHeadlessServiceManifest(ctx context.Context, workspaceObj *kaitov1a
 			Namespace: workspaceObj.Namespace,
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					UID:        workspaceObj.UID,
 					Name:       workspaceObj.Name,
@@ -56,9 +59,9 @@ func GenerateHeadlessServiceManifest(ctx context.Context, workspaceObj *kaitov1a
 	}
 }
 
-func GenerateServiceManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, serviceType corev1.ServiceType, isStatefulSet bool) *corev1.Service {
+func GenerateServiceManifest(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, serviceType corev1.ServiceType, isStatefulSet bool) *corev1.Service {
 	selector := map[string]string{
-		kaitov1alpha1.LabelWorkspaceName: workspaceObj.Name,
+		kaitov1beta1.LabelWorkspaceName: workspaceObj.Name,
 	}
 	// If statefulset, modify the selector to select the pod with index 0 as the endpoint
 	if isStatefulSet {
@@ -72,7 +75,7 @@ func GenerateServiceManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Wo
 			Namespace: workspaceObj.Namespace,
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					UID:        workspaceObj.UID,
 					Name:       workspaceObj.Name,
@@ -106,7 +109,7 @@ func GenerateServiceManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Wo
 	}
 }
 
-func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, imageName string,
+func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, imageName string,
 	imagePullSecretRefs []corev1.LocalObjectReference, replicas int, commands []string, containerPorts []corev1.ContainerPort,
 	livenessProbe, readinessProbe *corev1.Probe, resourceRequirements corev1.ResourceRequirements,
 	tolerations []corev1.Toleration, volumes []corev1.Volume, volumeMount []corev1.VolumeMount) *appsv1.StatefulSet {
@@ -121,11 +124,18 @@ func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1alpha
 	}
 
 	selector := map[string]string{
-		kaitov1alpha1.LabelWorkspaceName: workspaceObj.Name,
+		kaitov1beta1.LabelWorkspaceName: workspaceObj.Name,
 	}
 	labelselector := &v1.LabelSelector{
 		MatchLabels: selector,
 	}
+	// Add PYTORCH_CUDA_ALLOC_CONF environment variable
+	envVars := []corev1.EnvVar{
+		{
+			Name:  "PYTORCH_CUDA_ALLOC_CONF",
+			Value: "expandable_segments:True",
+		},
+	}
 
 	ss := &appsv1.StatefulSet{
 		ObjectMeta: v1.ObjectMeta{
@@ -133,7 +143,7 @@ func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1alpha
 			Namespace: workspaceObj.Namespace,
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					UID:        workspaceObj.UID,
 					Name:       workspaceObj.Name,
@@ -173,6 +183,7 @@ func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1alpha
 							ReadinessProbe: readinessProbe,
 							Ports:          containerPorts,
 							VolumeMounts:   volumeMount,
+							Env:            envVars,
 						},
 					},
 					Tolerations: tolerations,
@@ -185,16 +196,21 @@ func GenerateStatefulSetManifest(ctx context.Context, workspaceObj *kaitov1alpha
 	return ss
 }
 
-func GenerateTuningJobManifest(ctx context.Context, wObj *kaitov1alpha1.Workspace, revisionNum string, imageName string,
+func GenerateTuningJobManifest(ctx context.Context, wObj *kaitov1beta1.Workspace, revisionNum string, imageName string,
 	imagePullSecretRefs []corev1.LocalObjectReference, replicas int, commands []string, containerPorts []corev1.ContainerPort,
 	livenessProbe, readinessProbe *corev1.Probe, resourceRequirements corev1.ResourceRequirements, tolerations []corev1.Toleration,
 	initContainers []corev1.Container, sidecarContainers []corev1.Container, volumes []corev1.Volume, volumeMounts []corev1.VolumeMount,
 	envVars []corev1.EnvVar) *batchv1.Job {
 	labels := map[string]string{
-		kaitov1alpha1.LabelWorkspaceName: wObj.Name,
+		kaitov1beta1.LabelWorkspaceName: wObj.Name,
+	}
+
+	// TODO: make containers only mount the volumes they need
+
+	for i := range initContainers {
+		initContainers[i].VolumeMounts = append(initContainers[i].VolumeMounts, volumeMounts...)
 	}
 
-	// Add volume mounts to sidecar containers
 	for i := range sidecarContainers {
 		sidecarContainers[i].VolumeMounts = append(sidecarContainers[i].VolumeMounts, volumeMounts...)
 	}
@@ -225,11 +241,11 @@ func GenerateTuningJobManifest(ctx context.Context, wObj *kaitov1alpha1.Workspac
 			Namespace: wObj.Namespace,
 			Labels:    labels,
 			Annotations: map[string]string{
-				kaitov1alpha1.WorkspaceRevisionAnnotation: revisionNum,
+				kaitov1beta1.WorkspaceRevisionAnnotation: revisionNum,
 			},
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					Name:       wObj.Name,
 					UID:        wObj.UID,
@@ -244,19 +260,20 @@ func GenerateTuningJobManifest(ctx context.Context, wObj *kaitov1alpha1.Workspac
 					Labels: labels,
 				},
 				Spec: corev1.PodSpec{
-					InitContainers:   initContainers,
-					Containers:       containers,
-					RestartPolicy:    corev1.RestartPolicyNever,
-					Volumes:          volumes,
-					Tolerations:      tolerations,
-					ImagePullSecrets: imagePullSecretRefs,
+					InitContainers:        initContainers,
+					Containers:            containers,
+					RestartPolicy:         corev1.RestartPolicyNever,
+					ShareProcessNamespace: ptr.To(true),
+					Volumes:               volumes,
+					Tolerations:           tolerations,
+					ImagePullSecrets:      imagePullSecretRefs,
 				},
 			},
 		},
 	}
 }
 
-func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, revisionNum string, imageName string,
+func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, revisionNum string, imageName string,
 	imagePullSecretRefs []corev1.LocalObjectReference, replicas int, commands []string, containerPorts []corev1.ContainerPort,
 	livenessProbe, readinessProbe *corev1.Probe, resourceRequirements corev1.ResourceRequirements,
 	tolerations []corev1.Toleration, volumes []corev1.Volume, volumeMount []corev1.VolumeMount) *appsv1.Deployment {
@@ -271,16 +288,21 @@ func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1
 	}
 
 	selector := map[string]string{
-		kaitov1alpha1.LabelWorkspaceName: workspaceObj.Name,
+		kaitov1beta1.LabelWorkspaceName: workspaceObj.Name,
 	}
 	labelselector := &v1.LabelSelector{
 		MatchLabels: selector,
 	}
-	initContainers := []corev1.Container{}
 	envs := []corev1.EnvVar{}
-	if len(workspaceObj.Inference.Adapters) > 0 {
-		initContainers, envs = GenerateInitContainers(workspaceObj, volumeMount)
-	}
+	// Add PYTORCH_CUDA_ALLOC_CONF environment variable
+	envs = append(envs, corev1.EnvVar{
+		Name:  "PYTORCH_CUDA_ALLOC_CONF",
+		Value: "expandable_segments:True",
+	})
+
+	pullerContainers, pullerEnvVars, pullerVolumes := GeneratePullerContainers(workspaceObj, volumeMount)
+	envs = append(envs, pullerEnvVars...)
+	volumes = append(volumes, pullerVolumes...)
 
 	return &appsv1.Deployment{
 		ObjectMeta: v1.ObjectMeta{
@@ -288,7 +310,7 @@ func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1
 			Namespace: workspaceObj.Namespace,
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					UID:        workspaceObj.UID,
 					Name:       workspaceObj.Name,
@@ -296,7 +318,7 @@ func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1
 				},
 			},
 			Annotations: map[string]string{
-				kaitov1alpha1.WorkspaceRevisionAnnotation: revisionNum,
+				kaitov1beta1.WorkspaceRevisionAnnotation: revisionNum,
 			},
 		},
 		Spec: appsv1.DeploymentSpec{
@@ -332,7 +354,7 @@ func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1
 							},
 						},
 					},
-					InitContainers: initContainers,
+					InitContainers: pullerContainers,
 					Containers: []corev1.Container{
 						{
 							Name:           workspaceObj.Name,
@@ -354,31 +376,37 @@ func GenerateDeploymentManifest(ctx context.Context, workspaceObj *kaitov1alpha1
 	}
 }
 
-func GenerateInitContainers(wObj *kaitov1alpha1.Workspace, volumeMount []corev1.VolumeMount) ([]corev1.Container, []corev1.EnvVar) {
-	var initContainers []corev1.Container
-	var envs []corev1.EnvVar
-	if len(wObj.Inference.Adapters) > 0 {
-		for _, adapter := range wObj.Inference.Adapters {
-			initContainer := corev1.Container{
-				Name:            adapter.Source.Name,
-				Image:           adapter.Source.Image,
-				Command:         []string{"/bin/sh", "-c", fmt.Sprintf("mkdir -p /mnt/adapter/%s && cp -r /data/* /mnt/adapter/%s", adapter.Source.Name, adapter.Source.Name)},
-				VolumeMounts:    volumeMount,
-				ImagePullPolicy: corev1.PullAlways,
-			}
-			initContainers = append(initContainers, initContainer)
-			env := corev1.EnvVar{
-				Name:  adapter.Source.Name,
-				Value: *adapter.Strength,
-			}
-			envs = append(envs, env)
+func GeneratePullerContainers(wObj *kaitov1beta1.Workspace, volumeMounts []corev1.VolumeMount) ([]corev1.Container, []corev1.EnvVar, []corev1.Volume) {
+	size := len(wObj.Inference.Adapters)
+
+	initContainers := make([]corev1.Container, 0, size)
+	envVars := make([]corev1.EnvVar, 0, size)
+	volumes := make([]corev1.Volume, 0, size)
+
+	for _, adapter := range wObj.Inference.Adapters {
+		source := adapter.Source
+		sourceName := source.Name
+
+		volume, volumeMount := utils.ConfigImagePullSecretVolume(sourceName+"-inference-adapter", source.ImagePullSecrets)
+		volumes = append(volumes, volume)
+
+		envVar := corev1.EnvVar{
+			Name:  sourceName,
+			Value: *adapter.Strength,
 		}
+		envVars = append(envVars, envVar)
 
+		outputDirectory := path.Join("/mnt/adapter", sourceName)
+		pullerContainer := image.NewPullerContainer(source.Image, outputDirectory)
+		pullerContainer.Name += "-" + sourceName
+		pullerContainer.VolumeMounts = append(volumeMounts, volumeMount)
+		initContainers = append(initContainers, *pullerContainer)
 	}
-	return initContainers, envs
+
+	return initContainers, envVars, volumes
 }
 
-func GenerateDeploymentManifestWithPodTemplate(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, tolerations []corev1.Toleration) *appsv1.Deployment {
+func GenerateDeploymentManifestWithPodTemplate(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, tolerations []corev1.Toleration) *appsv1.Deployment {
 	nodeRequirements := make([]corev1.NodeSelectorRequirement, 0, len(workspaceObj.Resource.LabelSelector.MatchLabels))
 	for key, value := range workspaceObj.Resource.LabelSelector.MatchLabels {
 		nodeRequirements = append(nodeRequirements, corev1.NodeSelectorRequirement{
@@ -393,10 +421,10 @@ func GenerateDeploymentManifestWithPodTemplate(ctx context.Context, workspaceObj
 	if templateCopy.ObjectMeta.Labels == nil {
 		templateCopy.ObjectMeta.Labels = make(map[string]string)
 	}
-	templateCopy.ObjectMeta.Labels[kaitov1alpha1.LabelWorkspaceName] = workspaceObj.Name
+	templateCopy.ObjectMeta.Labels[kaitov1beta1.LabelWorkspaceName] = workspaceObj.Name
 	labelselector := &v1.LabelSelector{
 		MatchLabels: map[string]string{
-			kaitov1alpha1.LabelWorkspaceName: workspaceObj.Name,
+			kaitov1beta1.LabelWorkspaceName: workspaceObj.Name,
 		},
 	}
 	// Overwrite affinity
@@ -425,7 +453,7 @@ func GenerateDeploymentManifestWithPodTemplate(ctx context.Context, workspaceObj
 			Namespace: workspaceObj.Namespace,
 			OwnerReferences: []v1.OwnerReference{
 				{
-					APIVersion: kaitov1alpha1.GroupVersion.String(),
+					APIVersion: kaitov1beta1.GroupVersion.String(),
 					Kind:       "Workspace",
 					UID:        workspaceObj.UID,
 					Name:       workspaceObj.Name,
diff --git a/pkg/workspace/tuning/preset-tuning.go b/pkg/workspace/tuning/preset-tuning.go
index 3b608b5..ead8cbc 100644
--- a/pkg/workspace/tuning/preset-tuning.go
+++ b/pkg/workspace/tuning/preset-tuning.go
@@ -11,15 +11,15 @@ import (
 	"k8s.io/apimachinery/pkg/api/resource"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/klog/v2"
-	"k8s.io/utils/ptr"
 	"knative.dev/pkg/apis"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
+	"github.com/kaito-project/kaito/pkg/workspace/image"
 	"github.com/kaito-project/kaito/pkg/workspace/manifests"
 )
 
@@ -64,10 +64,10 @@ func getInstanceGPUCount(sku string) int {
 	return gpuConfig.GPUCount
 }
 
-func GetTuningImageInfo(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, presetObj *model.PresetParam) (string, []corev1.LocalObjectReference) {
+func GetTuningImageInfo(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, presetObj *model.PresetParam) (string, []corev1.LocalObjectReference) {
 	imagePullSecretRefs := []corev1.LocalObjectReference{}
 	// Check if the workspace preset's access mode is private
-	if string(workspaceObj.Tuning.Preset.AccessMode) == string(kaitov1alpha1.ModelImageAccessModePrivate) {
+	if string(workspaceObj.Tuning.Preset.AccessMode) == string(kaitov1beta1.ModelImageAccessModePrivate) {
 		imageName := workspaceObj.Tuning.Preset.PresetOptions.Image
 		for _, secretName := range workspaceObj.Tuning.Preset.PresetOptions.ImagePullSecrets {
 			imagePullSecretRefs = append(imagePullSecretRefs, corev1.LocalObjectReference{Name: secretName})
@@ -82,7 +82,7 @@ func GetTuningImageInfo(ctx context.Context, workspaceObj *kaitov1alpha1.Workspa
 	}
 }
 
-func GetDataSrcImageInfo(ctx context.Context, wObj *kaitov1alpha1.Workspace) (string, []corev1.LocalObjectReference) {
+func GetDataSrcImageInfo(ctx context.Context, wObj *kaitov1beta1.Workspace) (string, []corev1.LocalObjectReference) {
 	imagePullSecretRefs := make([]corev1.LocalObjectReference, len(wObj.Tuning.Input.ImagePullSecrets))
 	for i, secretName := range wObj.Tuning.Input.ImagePullSecrets {
 		imagePullSecretRefs[i] = corev1.LocalObjectReference{Name: secretName}
@@ -90,66 +90,6 @@ func GetDataSrcImageInfo(ctx context.Context, wObj *kaitov1alpha1.Workspace) (st
 	return wObj.Tuning.Input.Image, imagePullSecretRefs
 }
 
-func dockerSidecarScriptPushImage(outputDir, image string) string {
-	return fmt.Sprintf(`
-# Start the Docker daemon in the background with specific options for DinD
-dockerd &
-# Wait for the Docker daemon to be ready
-while ! docker info > /dev/null 2>&1; do
-  echo "Waiting for Docker daemon to start..."
-  sleep 1
-done
-echo 'Docker daemon started'
-
-PUSH_SUCCEEDED=false
-
-while true; do
-  FILE_PATH=$(find %s -name 'fine_tuning_completed.txt')
-  if [ ! -z "$FILE_PATH" ]; then
-    if [ "$PUSH_SUCCEEDED" = false ]; then
-      echo "FOUND TRAINING COMPLETED FILE at $FILE_PATH"
-
-      PARENT_DIR=$(dirname "$FILE_PATH")
-      echo "Parent directory is $PARENT_DIR"
-
-      TEMP_CONTEXT=$(mktemp -d)
-      cp "$PARENT_DIR/adapter_config.json" "$TEMP_CONTEXT/adapter_config.json"
-      cp -r "$PARENT_DIR/adapter_model.safetensors" "$TEMP_CONTEXT/adapter_model.safetensors"
-
-      # Create a minimal Dockerfile
-      echo 'FROM busybox:latest
-      RUN mkdir -p /data
-      ADD adapter_config.json /data/
-      ADD adapter_model.safetensors /data/' > "$TEMP_CONTEXT/Dockerfile"
-
-	  # Add symbolic link to read-only mounted config.json
-      mkdir -p /root/.docker
-	  ln -s /tmp/.docker/config/config.json /root/.docker/config.json
-
-      docker build -t %s "$TEMP_CONTEXT"
-      
-      while true; do
-        if docker push %s; then
-          echo "Upload complete"
-          # Cleanup: Remove the temporary directory
-          rm -rf "$TEMP_CONTEXT"
-          # Remove the file to prevent repeated builds
-          rm "$FILE_PATH"
-          PUSH_SUCCEEDED=true
-          # Signal completion
-          touch /tmp/upload_complete
-          exit 0
-        else
-          echo "Push failed, retrying in 30 seconds..."
-          sleep 30
-        fi
-      done
-    fi
-  fi
-  sleep 10  # Check every 10 seconds
-done`, outputDir, image, image)
-}
-
 // PrepareOutputDir ensures the output directory is within the base directory.
 func PrepareOutputDir(outputDir string) (string, error) {
 	if outputDir == "" {
@@ -187,7 +127,7 @@ func GetOutputDirFromTrainingArgs(trainingArgs map[string]runtime.RawExtension)
 
 // GetTrainingOutputDir retrieves and validates the output directory from the ConfigMap.
 func GetTrainingOutputDir(ctx context.Context, configMap *corev1.ConfigMap) (string, error) {
-	config, err := kaitov1alpha1.UnmarshalTrainingConfig(configMap)
+	config, err := kaitov1beta1.UnmarshalTrainingConfig(configMap)
 	if err != nil {
 		return "", err
 	}
@@ -210,7 +150,7 @@ func SetupTrainingOutputVolume(ctx context.Context, configMap *corev1.ConfigMap)
 	return resultsVolume, resultsVolumeMount, outputDir
 }
 
-func setupDefaultSharedVolumes(workspaceObj *kaitov1alpha1.Workspace, cmName string) ([]corev1.Volume, []corev1.VolumeMount) {
+func setupDefaultSharedVolumes(workspaceObj *kaitov1beta1.Workspace, cmName string) ([]corev1.Volume, []corev1.VolumeMount) {
 	var volumes []corev1.Volume
 	var volumeMounts []corev1.VolumeMount
 
@@ -231,14 +171,14 @@ func setupDefaultSharedVolumes(workspaceObj *kaitov1alpha1.Workspace, cmName str
 	return volumes, volumeMounts
 }
 
-func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, revisionNum string,
+func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, revisionNum string,
 	tuningObj *model.PresetParam, kubeClient client.Client) (client.Object, error) {
 
 	var defaultConfigName string
-	if workspaceObj.Tuning.Method == kaitov1alpha1.TuningMethodLora {
-		defaultConfigName = kaitov1alpha1.DefaultLoraConfigMapTemplate
-	} else if workspaceObj.Tuning.Method == kaitov1alpha1.TuningMethodQLora {
-		defaultConfigName = kaitov1alpha1.DefaultQloraConfigMapTemplate
+	if workspaceObj.Tuning.Method == kaitov1beta1.TuningMethodLora {
+		defaultConfigName = kaitov1beta1.DefaultLoraConfigMapTemplate
+	} else if workspaceObj.Tuning.Method == kaitov1beta1.TuningMethodQLora {
+		defaultConfigName = kaitov1beta1.DefaultQloraConfigMapTemplate
 	}
 	configVolume, err := resources.EnsureConfigOrCopyFromDefault(ctx, kubeClient,
 		client.ObjectKey{
@@ -251,28 +191,25 @@ func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1alpha1.Workspa
 		return nil, err
 	}
 
+	var imagePullSecrets []corev1.LocalObjectReference
 	var initContainers, sidecarContainers []corev1.Container
 	volumes, volumeMounts := setupDefaultSharedVolumes(workspaceObj, configVolume.Name)
 
+	// TODO: make containers only mount the volumes they need
+
 	// Add shared volume for training output
 	trainingOutputVolume, trainingOutputVolumeMount, outputDir := SetupTrainingOutputVolume(ctx, configVolume)
 	volumes = append(volumes, trainingOutputVolume)
 	volumeMounts = append(volumeMounts, trainingOutputVolumeMount)
 
-	initContainer, imagePullSecrets, dataSourceVolume, dataSourceVolumeMount, err := prepareDataSource(ctx, workspaceObj)
-	if err != nil {
-		return nil, err
-	}
-	volumes = append(volumes, dataSourceVolume)
-	volumeMounts = append(volumeMounts, dataSourceVolumeMount)
+	initContainer, dataSourceVolumes, dataSourceVolumeMounts := prepareDataSource(ctx, workspaceObj)
+	volumes = append(volumes, dataSourceVolumes...)
+	volumeMounts = append(volumeMounts, dataSourceVolumeMounts...)
 	if initContainer.Name != "" {
 		initContainers = append(initContainers, *initContainer)
 	}
 
-	sidecarContainer, imagePushSecret, dataDestVolume, dataDestVolumeMount, err := prepareDataDestination(ctx, workspaceObj, outputDir)
-	if err != nil {
-		return nil, err
-	}
+	sidecarContainer, imagePushSecret, dataDestVolume, dataDestVolumeMount := prepareDataDestination(ctx, workspaceObj, outputDir)
 	volumes = append(volumes, dataDestVolume)
 	volumeMounts = append(volumeMounts, dataDestVolumeMount)
 	if sidecarContainer != nil {
@@ -282,6 +219,13 @@ func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1alpha1.Workspa
 		imagePullSecrets = append(imagePullSecrets, *imagePushSecret)
 	}
 
+	pauseContainer := corev1.Container{
+		Name:            "pause",
+		Image:           "registry.k8s.io/pause:latest",
+		ImagePullPolicy: corev1.PullAlways,
+	}
+	sidecarContainers = append(sidecarContainers, pauseContainer)
+
 	modelCommand, err := prepareModelRunParameters(ctx, tuningObj)
 	if err != nil {
 		return nil, err
@@ -324,75 +268,56 @@ func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1alpha1.Workspa
 }
 
 // Now there are two options for data destination 1. HostPath - 2. Image
-func prepareDataDestination(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace, outputDir string) (*corev1.Container, *corev1.LocalObjectReference, corev1.Volume, corev1.VolumeMount, error) {
-	var sidecarContainer *corev1.Container
-	var volume corev1.Volume
-	var volumeMount corev1.VolumeMount
-	var imagePushSecret *corev1.LocalObjectReference
-	switch {
-	case workspaceObj.Tuning.Output.Image != "":
-		image, secret := workspaceObj.Tuning.Output.Image, workspaceObj.Tuning.Output.ImagePushSecret
-		imagePushSecret = &corev1.LocalObjectReference{Name: secret}
-		sidecarContainer, volume, volumeMount = handleImageDataDestination(ctx, outputDir, image, secret)
-		// TODO: Future PR include
-		//case workspaceObj.Tuning.Output.Volume != nil:
+func prepareDataDestination(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, inputDirectory string) (*corev1.Container, *corev1.LocalObjectReference, corev1.Volume, corev1.VolumeMount) {
+	tuning := workspaceObj.Tuning
+	output := tuning.Output
+
+	outputImage := output.Image
+	if outputImage == "" {
+		return nil, nil, corev1.Volume{}, corev1.VolumeMount{}
 	}
-	return sidecarContainer, imagePushSecret, volume, volumeMount, nil
-}
 
-func handleImageDataDestination(ctx context.Context, outputDir, image, imagePushSecret string) (*corev1.Container, corev1.Volume, corev1.VolumeMount) {
-	sidecarContainer := &corev1.Container{
-		Name:  "docker-sidecar",
-		Image: "docker:dind",
-		SecurityContext: &corev1.SecurityContext{
-			Privileged: ptr.To(true),
-		},
-		Command: []string{"/bin/sh", "-c"},
-		Args:    []string{dockerSidecarScriptPushImage(outputDir, image)},
+	var annotationsData map[string]map[string]string
+	if preset := tuning.Preset; preset != nil {
+		annotationsData = map[string]map[string]string{
+			"$manifest": {
+				"sh.kaito.model.name": string(preset.Name),
+			},
+		}
 	}
-	volume, volumeMount := utils.ConfigImagePushSecretVolume(imagePushSecret)
-	return sidecarContainer, volume, volumeMount
-}
 
-// Now there are three options for DataSource: 1. URL - 2. HostPath - 3. Image
-func prepareDataSource(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace) (*corev1.Container, []corev1.LocalObjectReference, corev1.Volume, corev1.VolumeMount, error) {
-	var initContainer *corev1.Container
-	var volume corev1.Volume
-	var volumeMount corev1.VolumeMount
-	var imagePullSecrets []corev1.LocalObjectReference
-	switch {
-	case workspaceObj.Tuning.Input.Image != "":
-		var image string
-		image, imagePullSecrets = GetDataSrcImageInfo(ctx, workspaceObj)
-		initContainer, volume, volumeMount = handleImageDataSource(ctx, image)
-	case len(workspaceObj.Tuning.Input.URLs) > 0:
-		initContainer, volume, volumeMount = handleURLDataSource(ctx, workspaceObj)
-		// TODO: Future PR include
-		// case workspaceObj.Tuning.Input.Volume != nil:
+	pusherContainer := image.NewPusherContainer(inputDirectory, outputImage, annotationsData, nil)
+
+	imagePushSecretRef := corev1.LocalObjectReference{
+		Name: output.ImagePushSecret,
 	}
-	return initContainer, imagePullSecrets, volume, volumeMount, nil
+
+	volume, volumeMount := utils.ConfigImagePushSecretVolume(imagePushSecretRef.Name)
+
+	return pusherContainer, &imagePushSecretRef, volume, volumeMount
 }
 
-func handleImageDataSource(ctx context.Context, image string) (*corev1.Container, corev1.Volume, corev1.VolumeMount) {
-	// Constructing a multistep command that lists, copies, and then lists the destination
-	command := "ls -la /data && cp -r /data/* " + utils.DefaultDataVolumePath + " && ls -la " + utils.DefaultDataVolumePath
-	initContainer := &corev1.Container{
-		Name:    "data-extractor",
-		Image:   image,
-		Command: []string{"sh", "-c", command},
-		VolumeMounts: []corev1.VolumeMount{
-			{
-				Name:      "data-volume",
-				MountPath: utils.DefaultDataVolumePath,
-			},
-		},
-	}
+// Now there are three options for DataSource: 1. URL - 2. HostPath - 3. Image
+func prepareDataSource(ctx context.Context, workspaceObj *kaitov1beta1.Workspace) (*corev1.Container, []corev1.Volume, []corev1.VolumeMount) {
+	input := workspaceObj.Tuning.Input
 
-	volume, volumeMount := utils.ConfigDataVolume(nil)
-	return initContainer, volume, volumeMount
+	switch {
+	case input.Image != "":
+		dataVolume, dataVolumeMount := utils.ConfigDataVolume(nil)
+		imagePullSecretVolume, imagePullSecretVolumeMount := utils.ConfigImagePullSecretVolume(input.Name+"-tuning-input", input.ImagePullSecrets)
+		pullerContainer := image.NewPullerContainer(input.Image, utils.DefaultDataVolumePath)
+		return pullerContainer, []corev1.Volume{imagePullSecretVolume, dataVolume}, []corev1.VolumeMount{imagePullSecretVolumeMount, dataVolumeMount}
+
+	case len(input.URLs) > 0:
+		initContainer, volume, volumeMount := handleURLDataSource(ctx, workspaceObj)
+		return initContainer, []corev1.Volume{volume}, []corev1.VolumeMount{volumeMount}
+
+	default:
+		return nil, nil, nil
+	}
 }
 
-func handleURLDataSource(ctx context.Context, workspaceObj *kaitov1alpha1.Workspace) (*corev1.Container, corev1.Volume, corev1.VolumeMount) {
+func handleURLDataSource(ctx context.Context, workspaceObj *kaitov1beta1.Workspace) (*corev1.Container, corev1.Volume, corev1.VolumeMount) {
 	initContainer := &corev1.Container{
 		Name:  "data-downloader",
 		Image: "curlimages/curl",
@@ -425,12 +350,6 @@ func handleURLDataSource(ctx context.Context, workspaceObj *kaitov1alpha1.Worksp
 			done
 			echo "All downloads completed successfully"
 		`},
-		VolumeMounts: []corev1.VolumeMount{
-			{
-				Name:      "data-volume",
-				MountPath: utils.DefaultDataVolumePath,
-			},
-		},
 		Env: []corev1.EnvVar{
 			{
 				Name:  "DATA_URLS",
@@ -455,7 +374,7 @@ func prepareModelRunParameters(ctx context.Context, tuningObj *model.PresetParam
 // accelerate launch <TORCH_PARAMS> baseCommand <MODEL_PARAMS>
 // and sets the GPU resources required for tuning.
 // Returns the command and resource configuration.
-func prepareTuningParameters(ctx context.Context, wObj *kaitov1alpha1.Workspace, modelCommand string,
+func prepareTuningParameters(ctx context.Context, wObj *kaitov1beta1.Workspace, modelCommand string,
 	tuningObj *model.PresetParam, skuNumGPUs string) ([]string, corev1.ResourceRequirements) {
 	hfParam := tuningObj.Transformers // Only support Huggingface for now
 	if hfParam.TorchRunParams == nil {
diff --git a/pkg/workspace/webhooks/webhooks.go b/pkg/workspace/webhooks/webhooks.go
index 9da123e..9601f62 100644
--- a/pkg/workspace/webhooks/webhooks.go
+++ b/pkg/workspace/webhooks/webhooks.go
@@ -14,6 +14,7 @@ import (
 	"knative.dev/pkg/webhook/resourcesemantics/validation"
 
 	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 )
 
 func NewWorkspaceWebhooks() []knativeinjection.ControllerConstructor {
@@ -35,4 +36,5 @@ func NewWorkspaceCRDValidationWebhook(ctx context.Context, _ configmap.Watcher)
 
 var WorkspaceResources = map[schema.GroupVersionKind]resourcesemantics.GenericCRD{
 	kaitov1alpha1.GroupVersion.WithKind("Workspace"): &kaitov1alpha1.Workspace{},
+	kaitov1beta1.GroupVersion.WithKind("Workspace"):  &kaitov1beta1.Workspace{},
 }
diff --git a/presets/ragengine/config.py b/presets/ragengine/config.py
index 8adde1f..4da749e 100644
--- a/presets/ragengine/config.py
+++ b/presets/ragengine/config.py
@@ -48,4 +48,4 @@ LLM_ACCESS_SECRET = os.getenv("LLM_ACCESS_SECRET", "default-access-secret")
 
 # Vector database configuration
 VECTOR_DB_IMPLEMENTATION = os.getenv("VECTOR_DB_IMPLEMENTATION", "faiss")
-VECTOR_DB_PERSIST_DIR = os.getenv("VECTOR_DB_PERSIST_DIR", "storage")
+DEFAULT_VECTOR_DB_PERSIST_DIR = os.getenv("DEFAULT_VECTOR_DB_PERSIST_DIR", "storage")
diff --git a/presets/ragengine/embedding/base.py b/presets/ragengine/embedding/base.py
index ee4d13d..97541ec 100644
--- a/presets/ragengine/embedding/base.py
+++ b/presets/ragengine/embedding/base.py
@@ -1,15 +1,18 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
 
+from typing import List
 from abc import ABC, abstractmethod
+from llama_index.core.embeddings import BaseEmbedding
+import asyncio
 
+class BaseEmbeddingModel(BaseEmbedding, ABC):
+    async def _aget_text_embedding(self, text: str) -> List[float]:
+        return await asyncio.to_thread(self._get_text_embedding, text)
+
+    async def _aget_query_embedding(self, query: str) -> List[float]:
+        return await asyncio.to_thread(self._get_query_embedding, query)
 
-class BaseEmbeddingModel(ABC):
-    @abstractmethod
-    def get_text_embedding(self, text: str):
-        """Returns the text embedding for a given input string."""
-        pass
-    
     @abstractmethod
     def get_embedding_dimension(self) -> int:
         """Returns the embedding dimension for the model."""
diff --git a/presets/ragengine/embedding/huggingface_local_embedding.py b/presets/ragengine/embedding/huggingface_local_embedding.py
index 1ae7a52..4a08bac 100644
--- a/presets/ragengine/embedding/huggingface_local_embedding.py
+++ b/presets/ragengine/embedding/huggingface_local_embedding.py
@@ -1,22 +1,12 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
-
+from typing import Any
 from llama_index.embeddings.huggingface import HuggingFaceEmbedding
-
 from .base import BaseEmbeddingModel
 
-
-class LocalHuggingFaceEmbedding(BaseEmbeddingModel):
-    def __init__(self, model_name: str):
-        self.model = HuggingFaceEmbedding(model_name=model_name) # TODO: Ensure/test loads on GPU (when available)
-
-    def get_text_embedding(self, text: str):
-        """Returns the text embedding for a given input string."""
-        return self.model.get_text_embedding(text)
-
+class LocalHuggingFaceEmbedding(HuggingFaceEmbedding, BaseEmbeddingModel):
     def get_embedding_dimension(self) -> int:
         """Infers the embedding dimension by making a local call to get the embedding of a dummy text."""
         dummy_input = "This is a dummy sentence."
         embedding = self.get_text_embedding(dummy_input)
-        
         return len(embedding)
\ No newline at end of file
diff --git a/presets/ragengine/embedding/remote_embedding.py b/presets/ragengine/embedding/remote_embedding.py
index b030071..542cef0 100644
--- a/presets/ragengine/embedding/remote_embedding.py
+++ b/presets/ragengine/embedding/remote_embedding.py
@@ -1,13 +1,12 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
-
+from typing import Any
 import requests
 import json
 from .base import BaseEmbeddingModel
 
-
 class RemoteEmbeddingModel(BaseEmbeddingModel):
-    def __init__(self, model_url: str, api_key: str):
+    def __init__(self, model_url: str, api_key: str, /, **data: Any):
         """
         Initialize the RemoteEmbeddingModel.
 
@@ -15,10 +14,11 @@ class RemoteEmbeddingModel(BaseEmbeddingModel):
             model_url (str): The URL of the embedding model API endpoint.
             api_key (str): The API key for accessing the API.
         """
+        super().__init__(**data)
         self.model_url = model_url
         self.api_key = api_key
 
-    def get_text_embedding(self, text: str):
+    def _get_text_embedding(self, text: str):
         """Returns the text embedding for a given input string."""
         headers = {
             "Authorization": f"Bearer {self.api_key}",
@@ -39,9 +39,12 @@ class RemoteEmbeddingModel(BaseEmbeddingModel):
         except requests.exceptions.RequestException as e:
             raise RuntimeError(f"Failed to get embedding from remote model: {e}")
 
+    def _get_query_embedding(self, query: str):
+        return self.get_text_embedding(query)
+
     def get_embedding_dimension(self) -> int:
         """Infers the embedding dimension by making a remote call to get the embedding of a dummy text."""
         dummy_input = "This is a dummy sentence."
-        embedding = self.get_text_embedding(dummy_input)
+        embedding = self._get_text_embedding(dummy_input)
         
         return len(embedding)
diff --git a/presets/ragengine/inference/inference.py b/presets/ragengine/inference/inference.py
index 7728c7a..479b472 100644
--- a/presets/ragengine/inference/inference.py
+++ b/presets/ragengine/inference/inference.py
@@ -1,9 +1,11 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
 
+from pydantic import PrivateAttr
 import logging
+import asyncio
+import httpx
 from typing import Any
-from dataclasses import field
 from llama_index.core.llms import CustomLLM, CompletionResponse, LLMMetadata, CompletionResponseGen
 from llama_index.llms.openai import OpenAI
 from llama_index.core.llms.callbacks import llm_completion_callback
@@ -11,6 +13,8 @@ import requests
 from requests.exceptions import HTTPError
 from urllib.parse import urlparse, urljoin
 from ragengine.config import LLM_INFERENCE_URL, LLM_ACCESS_SECRET #, LLM_RESPONSE_FIELD
+from fastapi import HTTPException
+import concurrent.futures
 
 # Configure logging
 logging.basicConfig(level=logging.INFO)
@@ -22,11 +26,21 @@ DEFAULT_HEADERS = {
     "Authorization": f"Bearer {LLM_ACCESS_SECRET}",
     "Content-Type": "application/json"
 }
+DEFAULT_HTTP_TIMEOUT = 300.0 # Seconds
+DEFAULT_HTTP_SUCCESS_CODE = 200
 
 class Inference(CustomLLM):
     params: dict = {}
     _default_model: str = None
+    _default_max_model_len: int = None
     _model_retrieval_attempted: bool = False
+    _async_http_client : httpx.AsyncClient = PrivateAttr(default=None)
+
+    async def _get_httpx_client(self):
+        """ Lazily initializes the HTTP client on first request. """
+        if self._async_http_client is None:
+            self._async_http_client = httpx.AsyncClient(timeout=DEFAULT_HTTP_TIMEOUT)
+        return self._async_http_client
 
     def set_params(self, params: dict) -> None:
         self.params = params
@@ -38,40 +52,70 @@ class Inference(CustomLLM):
     def stream_complete(self, prompt: str, **kwargs: Any) -> CompletionResponseGen:
         pass
 
+    def run_async_coroutine(self, coro):
+        with concurrent.futures.ThreadPoolExecutor() as executor:
+            # Submit a task that creates its own event loop using asyncio.run
+            future = executor.submit(asyncio.run, coro)
+            return future.result()
+
+    @llm_completion_callback()
+    def complete(self, prompt: str, formatted: bool = False, **kwargs: Any) -> CompletionResponse:
+        # Required implementation - Only called by LlamaIndex reranker because LLMRerank library doesn't use async call
+        try:
+            result = self.run_async_coroutine(self.acomplete(prompt, formatted=formatted, **kwargs))
+            if result.text == "Empty Response":
+                logger.error("LLMRerank Request returned an unparsable or invalid response")
+                raise HTTPException(status_code=422, detail="Rerank operation failed: Invalid response from LLM. This feature is experimental.")
+            return result
+        except HTTPException as http_exc:
+            # If it's already an HTTPException (e.g., 422), re-raise it as is
+            raise http_exc
+        except Exception as e:
+            logger.error(f"Unexpected exception in complete(): {e}")
+            raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
+
     @llm_completion_callback()
-    def complete(self, prompt: str, formatted: bool, **kwargs) -> CompletionResponse:
+    async def acomplete(self, prompt: str, formatted: bool = False, **kwargs: Any) -> CompletionResponse:
         try:
             if LLM_INFERENCE_URL.startswith(OPENAI_URL_PREFIX):
-                return self._openai_complete(prompt, **kwargs, **self.params)
+                return await self._async_openai_complete(prompt, **kwargs, **self.params)
             elif LLM_INFERENCE_URL.startswith(HUGGINGFACE_URL_PREFIX):
-                return self._huggingface_remote_complete(prompt, **kwargs, **self.params)
+                return await self._async_huggingface_remote_complete(prompt, **kwargs, **self.params)
             else:
-                return self._custom_api_complete(prompt, **kwargs, **self.params)
+                return await self._async_custom_api_complete(prompt, **kwargs, **self.params)
+        except HTTPException as http_exc:
+            raise http_exc
+        except Exception as e:
+            logger.error(f"Unexpected exception in acomplete(): {e}")
+            raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
         finally:
             # Clear params after the completion is done
             self.params = {}
 
-    def _openai_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
-        return OpenAI(api_key=LLM_ACCESS_SECRET, **kwargs).complete(prompt)
+    async def _async_openai_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
+        return await OpenAI(api_key=LLM_ACCESS_SECRET, **kwargs).acomplete(prompt)
 
-    def _huggingface_remote_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
-        return self._post_request(
-            {"messages": [{"role": "user", "content": prompt}]},
-            headers={"Authorization": f"Bearer {LLM_ACCESS_SECRET}"}
-        )
-
-    def _custom_api_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
-        model = kwargs.pop("model", self._get_default_model())
+    async def _async_huggingface_remote_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
+        data = {"prompt": prompt, **kwargs}
+        return await self._async_post_request(data, headers={"Authorization": f"Bearer {LLM_ACCESS_SECRET}", "Content-Type": "application/json"})
+    async def _async_custom_api_complete(self, prompt: str, **kwargs: Any) -> CompletionResponse:
+        model_name, model_max_len = self._get_default_model_info()
+        if kwargs.get("model"):
+            model_name = kwargs.pop("model")
         data = {"prompt": prompt, **kwargs}
-        if model:
-            data["model"] = model # Include the model only if it is not None
+        if model_name:
+            data["model"] = model_name # Include the model only if it is not None
+        if model_max_len and data.get("max_tokens"):
+            if data["max_tokens"] > model_max_len:
+                logger.error(f"Requested max_tokens ({data['max_tokens']}) exceeds model's max length ({model_max_len}).")
+                # vLLM will raise error ({"object":"error","message":"This model's maximum context length is 131072 tokens. However, you requested 500500500500505361 tokens (361 in the messages, 500500500500505000 in the completion). Please reduce the length of the messages or completion.","type":"BadRequestError","param":null,"code":400})
 
         # DEBUG: Call the debugging function
         # self._debug_curl_command(data)
         try:
-            return self._post_request(data, headers=DEFAULT_HEADERS)
+            return await self._async_post_request(data, headers=DEFAULT_HEADERS)
         except HTTPError as e:
-            if e.response.status_code == 400:
+            if not model_name and e.response.status_code == 400:
                 logger.warning(
                     f"Potential issue with 'model' parameter in API response. "
                     f"Response: {str(e)}. Attempting to update the model name as a mitigation..."
@@ -80,7 +124,7 @@ class Inference(CustomLLM):
                 if self._default_model:
                     logger.info(f"Default model '{self._default_model}' fetched successfully. Retrying request...")
                     data["model"] = self._default_model
-                    return self._post_request(data, headers=DEFAULT_HEADERS)
+                    return await self._async_post_request(data, headers=DEFAULT_HEADERS)
                 else:
                     logger.error("Failed to fetch a default model. Aborting retry.")
             raise  # Re-raise the exception if not recoverable
@@ -95,9 +139,9 @@ class Inference(CustomLLM):
         parsed = urlparse(LLM_INFERENCE_URL)
         return urljoin(f"{parsed.scheme}://{parsed.netloc}", "/v1/models")
 
-    def _fetch_default_model(self) -> str:
+    def _fetch_default_model_info(self) -> (str, int):
         """
-        Fetch the default model from the /v1/models endpoint.
+        Fetch the default model name and max_length from the /v1/models endpoint.
         """
         try:
             models_url = self._get_models_endpoint()
@@ -105,29 +149,45 @@ class Inference(CustomLLM):
             response.raise_for_status()  # Raise an exception for HTTP errors (includes 404)
 
             models = response.json().get("data", [])
-            return models[0].get("id") if models else None
+            if models:
+                return models[0].get("id", None), models[0].get("max_model_len", None)
+            return None, None
+
         except Exception as e:
             logger.error(f"Error fetching models from {models_url}: {e}. \"model\" parameter will not be included with inference call.")
             return None
 
-    def _get_default_model(self) -> str:
+    def _get_default_model_info(self) -> (str, int):
         """
         Returns the cached default model if available, otherwise fetches and caches it.
         """
         if not self._default_model and not self._model_retrieval_attempted:
             self._model_retrieval_attempted = True
-            self._default_model = self._fetch_default_model()
-        return self._default_model
+            self._default_model, self._default_max_model_len = self._fetch_default_model_info()
+        return self._default_model, self._default_max_model_len
 
-    def _post_request(self, data: dict, headers: dict) -> CompletionResponse:
+    async def _async_post_request(self, data: dict, headers: dict) -> CompletionResponse:
         try:
-            response = requests.post(LLM_INFERENCE_URL, json=data, headers=headers)
-            response.raise_for_status()  # Raise exception for HTTP errors
+            client = await self._get_httpx_client()
+            response = await client.post(LLM_INFERENCE_URL, json=data, headers=headers)
+            response.raise_for_status()  # Raise an exception for HTTP errors
             response_data = response.json()
+            # Check if the request was successful
+            if response.status_code == DEFAULT_HTTP_SUCCESS_CODE:
+                # OAI Spec returns array of choices, we choose the first one
+                if "choices" in response_data and response_data["choices"]:
+                    return CompletionResponse(text=response_data["choices"][0].get("text", ""))
+            # Return full response as text if no specific parsing is possible
             return CompletionResponse(text=str(response_data))
-        except requests.RequestException as e:
+        except httpx.HTTPStatusError as e:
+            logger.error(f"HTTP error {e.response.status_code} during POST request to {LLM_INFERENCE_URL}: {e.response.text}")
+            raise
+        except httpx.RequestError as e:
             logger.error(f"Error during POST request to {LLM_INFERENCE_URL}: {e}")
             raise
+        except Exception as e:
+            logger.error(f"Unexpected error during POST request: {e}")
+            raise
 
     def _debug_curl_command(self, data: dict) -> None:
         """
@@ -150,3 +210,8 @@ class Inference(CustomLLM):
     def metadata(self) -> LLMMetadata:
         """Get LLM metadata."""
         return LLMMetadata()
+
+    async def aclose(self):
+        """ Closes the HTTP client when shutting down. """
+        if self._async_http_client:
+            await self._async_http_client.aclose()
diff --git a/presets/ragengine/main.py b/presets/ragengine/main.py
index fd2d26e..f1c8f22 100644
--- a/presets/ragengine/main.py
+++ b/presets/ragengine/main.py
@@ -1,17 +1,19 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
 
-from typing import List
+from typing import List, Optional
 from vector_store_manager.manager import VectorStoreManager
 from embedding.huggingface_local_embedding import LocalHuggingFaceEmbedding
 from embedding.remote_embedding import RemoteEmbeddingModel
-from fastapi import FastAPI, HTTPException
+from fastapi import FastAPI, HTTPException, Query
 from models import (IndexRequest, ListDocumentsResponse,
                     QueryRequest, QueryResponse, DocumentResponse, HealthStatus)
 from vector_store.faiss_store import FaissVectorStoreHandler
 
 from ragengine.config import (REMOTE_EMBEDDING_URL, REMOTE_EMBEDDING_ACCESS_SECRET,
-                              EMBEDDING_SOURCE_TYPE, LOCAL_EMBEDDING_MODEL_ID)
+                              EMBEDDING_SOURCE_TYPE, LOCAL_EMBEDDING_MODEL_ID, DEFAULT_VECTOR_DB_PERSIST_DIR)
+from urllib.parse import unquote
+import os
 
 app = FastAPI()
 
@@ -30,10 +32,24 @@ vector_store_handler = FaissVectorStoreHandler(embedding_manager)
 # Initialize RAG operations
 rag_ops = VectorStoreManager(vector_store_handler)
 
-@app.get("/health", response_model=HealthStatus)
-async def health_check():
-    try:
+@app.get(
+    "/health",
+    response_model=HealthStatus,
+    summary="Health Check for RAG Engine",
+    description="""
+    Check the health status of the RAG Engine and its components.
 
+    ## Response Examples:
+    - **Success (200):**
+      ```json
+      {
+        "status": "Healthy",
+        "detail": null
+      }
+    """,
+)
+def health_check():
+    try:
         if embedding_manager is None:
             raise HTTPException(status_code=500, detail="Embedding manager not initialized")
         
@@ -45,10 +61,40 @@ async def health_check():
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
 
-@app.post("/index", response_model=List[DocumentResponse])
-async def index_documents(request: IndexRequest): # TODO: Research async/sync what to use (inference is calling)
+@app.post(
+    "/index",
+    response_model=List[DocumentResponse],
+    summary="Index Documents",
+    description="""
+    Add documents to an index or create a new index.
+
+    ## Request Example:
+    ```json
+    {
+      "index_name": "example_index",
+      "documents": [
+        {"text": "Sample document text.", "metadata": {"author": "John Doe", "category": "example"}}
+      ]
+    }
+    ```
+
+    ## Response Example:
+    ```json
+    [
+      {
+        "doc_id": "123456",
+        "text": "Sample document text.",
+        "hash_value": null,
+        "metadata": {"author": "John Doe", "category": "example"},
+        "is_truncated": false
+      }
+    ]
+    ```
+    """,
+)
+async def index_documents(request: IndexRequest):
     try:
-        doc_ids = rag_ops.index(request.index_name, request.documents)
+        doc_ids = await rag_ops.index(request.index_name, request.documents)
         documents = [
             DocumentResponse(doc_id=doc_id, text=doc.text, metadata=doc.metadata)
             for doc_id, doc in zip(doc_ids, request.documents)
@@ -57,14 +103,56 @@ async def index_documents(request: IndexRequest): # TODO: Research async/sync wh
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
 
-@app.post("/query", response_model=QueryResponse)
+@app.post(
+    "/query",
+    response_model=QueryResponse,
+    summary="Query an Index",
+    description="""
+    Query a specific index for documents and optionally rerank with an LLM.
+
+    ## Request Example:
+    ```json
+    {
+      "index_name": "example_index",
+      "query": "What is RAG?",
+      "top_k": 5,
+      "llm_params": {"temperature": 0.7, "max_tokens": 2048},
+      "rerank_params": {"top_n": 3}  # ⚠️ Experimental Feature
+    }
+    ```
+
+    ## Experimental Warning:
+    - The `rerank_params` option is **experimental** and may cause the query to fail.
+    - If `LLMRerank` produces an invalid or unparsable response, an **error will be raised**.
+    - Expected format:
+      ```
+      Answer:
+      Doc: 9, Relevance: 7
+      Doc: 3, Relevance: 4
+      Doc: 7, Relevance: 3
+      ```
+    - If reranking fails, the request will not return results and will instead raise an error.
+
+    ## Response Example:
+    ```json
+    {
+      "response": "...",
+      "source_nodes": [{"node_id": "node1", "text": "RAG explanation...", "score": 0.95, "metadata": {}}],
+      "metadata": {}
+    }
+    ```
+    """,
+)
 async def query_index(request: QueryRequest):
     try:
         llm_params = request.llm_params or {}  # Default to empty dict if no params provided
         rerank_params = request.rerank_params or {}  # Default to empty dict if no params provided
-        return rag_ops.query(
+        return await rag_ops.query(
             request.index_name, request.query, request.top_k, llm_params, rerank_params
         )
+    except HTTPException as http_exc:
+        # Preserve HTTP exceptions like 422 from reranker
+        raise http_exc
     except ValueError as ve:
         raise HTTPException(status_code=400, detail=str(ve))  # Validation issue
     except Exception as e:
@@ -72,14 +160,176 @@ async def query_index(request: QueryRequest):
             status_code=500, detail=f"An unexpected error occurred: {str(e)}"
         )
 
-@app.get("/indexed-documents", response_model=ListDocumentsResponse)
-async def list_all_indexed_documents():
+@app.get(
+    "/indexes",
+    response_model=List[str],
+    summary="List All Indexes",
+    description="""
+    Retrieve the names of all available indexes.
+
+    ## Response Example:
+    ```json
+    [
+      "example_index",
+      "test_index"
+    ]
+    ```
+    """,
+)
+def list_indexes():
     try:
-        documents = rag_ops.list_all_indexed_documents()
-        return ListDocumentsResponse(documents=documents)
+        return rag_ops.list_indexes()
     except Exception as e:
         raise HTTPException(status_code=500, detail=str(e))
 
+@app.get(
+    "/indexes/{index_name}/documents",
+    response_model=ListDocumentsResponse,
+    summary="List Documents in an Index",
+    description="""
+    Retrieve a paginated list of documents for a given index.
+
+    ## Request Example:
+    ```
+    GET /indexes/test_index/documents?limit=5&offset=5&max_text_length=500
+    ```
+
+    ## Response Example:
+    ```json
+    {
+      "documents": [
+        {
+          "doc_id": "123456",
+          "text": "Sample document text.",
+          "metadata": {"author": "John Doe"},
+          "is_truncated": true
+        },
+        {
+          "doc_id": "123457",
+          "text": "Another document text.",
+          "metadata": {"author": "Jane Doe"},
+          "is_truncated": false
+        }
+      ],
+      "count": 5
+    }
+    ```
+    """,
+)
+async def list_documents_in_index(
+    index_name: str,
+    limit: int = Query(10, ge=1, le=100, description="Maximum number of documents to return"),
+    offset: int = Query(0, ge=0, description="Starting point for the document list"),
+    max_text_length: Optional[int] = Query(
+        1000, 
+        ge=1, 
+        description="Maximum text length to return **per document**. This does not impose a limit on the total length of all documents returned."
+    ),
+):
+    """
+    Handles URL-encoded index names sent by the client.
+
+    Examples:
+    Raw Index Name    | URL-Encoded Form   | Decoded Form
+    ------------------|--------------------|--------------
+    my_index          | my_index          | my_index
+    my index          | my%20index        | my index
+    index/name        | index%2Fname      | index/name
+    """
+    try:
+        # Decode the index_name in case it was URL-encoded by the client
+        decoded_index_name = unquote(index_name)
+        documents = await rag_ops.list_documents_in_index(
+            index_name=decoded_index_name,
+            limit=limit,
+            offset=offset,
+            max_text_length=max_text_length
+        )
+
+        return ListDocumentsResponse(
+            documents=documents,
+            count=len(documents)
+        )
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=str(e))
+
+@app.post(
+    "/persist/{index_name}",
+    summary="Persist Index Data to Disk",
+    description="""
+    Persist the existing index data to disk at a specified location. This ensures that indexed data is saved.
+
+    ## Request Example:
+    ```
+    POST /persist/example_index?path=./custom_path
+    ```
+
+    If no path is provided, the index will be persisted in the default directory.
+
+    ## Response Example:
+    ```json
+    {
+      "message": "Successfully persisted index example_index to ./custom_path/example_index."
+    }
+    ```
+    """
+)
+async def persist_index(
+        index_name: str,
+        path: str = Query(DEFAULT_VECTOR_DB_PERSIST_DIR, description="Path where the index will be persisted")
+):  # TODO: Provide endpoint for loading existing index(es)
+    # TODO: Extend support for other vector databases/integrations besides FAISS
+    try:
+        # Append index to save path to prevent saving conflicts/overwriting
+        path = os.path.join(path, index_name) if path == DEFAULT_VECTOR_DB_PERSIST_DIR else path
+        await rag_ops.persist(index_name, path)
+        return {"message": f"Successfully persisted index {index_name} to {path}."}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"Persistence failed: {str(e)}")
+
+@app.post(
+    "/load/{index_name}",
+    summary="Load Index Data from Disk",
+    description="""
+    Load an existing index from disk at a specified location.
+
+    ## Request Example:
+    ```
+    POST /load/example_index?path=./custom_path/example_index
+    ```
+
+    If no path is provided, will attempt to load from the default directory.
+
+    ## Response Example:
+    ```json
+    {
+      "message": "Successfully loaded index example_index from ./custom_path/example_index."
+    }
+    ```
+    """
+)
+async def load_index(
+        index_name: str,
+        path: Optional[str] = Query(None, description="Path to load the index from"),
+        overwrite: bool = Query(False, description="Overwrite the existing index if it already exists")
+):
+    # If no path is provided, use the default directory joined with index_name.
+    if path is None:
+        path = os.path.join(DEFAULT_VECTOR_DB_PERSIST_DIR, index_name)
+    try:
+        await rag_ops.load(index_name, path, overwrite)
+        return {"message": f"Successfully loaded index {index_name} from {path}."}
+    except Exception as e:
+        raise HTTPException(status_code=500, detail=f"Loading failed: {str(e)}")
+
+@app.on_event("shutdown")
+async def shutdown_event():
+    """ Ensure the client is properly closed when the server shuts down. """
+    await rag_ops.shutdown()
+
 if __name__ == "__main__":
+    # DEBUG: Arize Phoenix
+    # import llama_index.core
+    # llama_index.core.set_global_handler("arize_phoenix")
     import uvicorn
-    uvicorn.run(app, host="0.0.0.0", port=5000)
+    uvicorn.run(app, host="0.0.0.0", port=5000, loop="asyncio")
diff --git a/presets/ragengine/models.py b/presets/ragengine/models.py
index a1e89a2..32f9f76 100644
--- a/presets/ragengine/models.py
+++ b/presets/ragengine/models.py
@@ -8,12 +8,18 @@ from pydantic import BaseModel, Field, model_validator
 
 class Document(BaseModel):
     text: str
-    metadata: Optional[dict] = {}
+    metadata: Optional[dict] = Field(default_factory=dict)
 
 class DocumentResponse(BaseModel):
     doc_id: str
     text: str
-    metadata: Optional[dict] = None
+    hash_value: Optional[str] = None
+    metadata: Optional[dict] = Field(default_factory=dict)
+    is_truncated: bool = False
+
+class ListDocumentsResponse(BaseModel):
+    documents: List[DocumentResponse] # List of DocumentResponses
+    count: int  # Number of documents in the current response
 
 class IndexRequest(BaseModel):
     index_name: str
@@ -22,7 +28,7 @@ class IndexRequest(BaseModel):
 class QueryRequest(BaseModel):
     index_name: str
     query: str
-    top_k: int = 10
+    top_k: int = 5
     # Accept a dictionary for our LLM parameters
     llm_params: Optional[Dict[str, Any]] = Field(
         default_factory=dict,
@@ -31,28 +37,25 @@ class QueryRequest(BaseModel):
     # Accept a dictionary for rerank parameters
     rerank_params: Optional[Dict[str, Any]] = Field(
         default_factory=dict,
-        description="Optional parameters for reranking, e.g., top_n, batch_size",
+        description="Experimental: Optional parameters for reranking. Only 'top_n' and 'choice_batch_size' are supported.",
     )
 
-    @model_validator(mode="before")
-    def validate_params(cls, values: Dict[str, Any]) -> Dict[str, Any]:
-        llm_params = values.get("llm_params", {})
-        rerank_params = values.get("rerank_params", {})
+    @model_validator(mode="after")
+    def validate_params(cls, values: "QueryRequest") -> "QueryRequest":
+        # Access fields as attributes instead of treating as a dictionary
+        llm_params = values.llm_params # Validate LLM parameters on vLLM side
+        rerank_params = values.rerank_params
+        top_k = values.top_k
 
-        # Validate LLM parameters
-        if "temperature" in llm_params and not (0.0 <= llm_params["temperature"] <= 1.0):
-            raise ValueError("Temperature must be between 0.0 and 1.0.")
-        # TODO: More LLM Param Validations here
         # Validate rerank parameters
-        top_k = values.get("top_k")
-        if "top_n" in rerank_params and rerank_params["top_n"] > top_k:
-            raise ValueError("Invalid configuration: 'top_n' for reranking cannot exceed 'top_k' from the RAG query.")
+        if "top_n" in rerank_params:
+            if not isinstance(rerank_params["top_n"], int):
+                raise ValueError("Invalid type: 'top_n' must be an integer.")
+            if rerank_params["top_n"] > top_k:
+                raise ValueError("Invalid configuration: 'top_n' for reranking cannot exceed 'top_k' from the RAG query.")
 
         return values
 
-class ListDocumentsResponse(BaseModel):
-    documents: Dict[str, Dict[str, Dict[str, str]]]
-
 # Define models for NodeWithScore, and QueryResponse
 class NodeWithScore(BaseModel):
     node_id: str
@@ -67,4 +70,4 @@ class QueryResponse(BaseModel):
 
 class HealthStatus(BaseModel):
     status: str
-    detail: Optional[str] = None 
\ No newline at end of file
+    detail: Optional[str] = None 
diff --git a/presets/ragengine/requirements-test.txt b/presets/ragengine/requirements-test.txt
new file mode 100644
index 0000000..53c8f6b
--- /dev/null
+++ b/presets/ragengine/requirements-test.txt
@@ -0,0 +1,7 @@
+# Common dependencies
+-r requirements.txt
+
+# Test dependencies
+pytest
+pytest-asyncio
+respx
diff --git a/presets/ragengine/requirements.txt b/presets/ragengine/requirements.txt
index a5b460c..2ec56e8 100644
--- a/presets/ragengine/requirements.txt
+++ b/presets/ragengine/requirements.txt
@@ -13,5 +13,6 @@ llama-index-vector-stores-faiss
 llama-index-vector-stores-chroma
 llama-index-vector-stores-azurecosmosmongo
 uvicorn
-# For UTs
-pytest
\ No newline at end of file
+asyncio
+aiorwlock
+httpx==0.27.0
diff --git a/presets/ragengine/vector_store/base.py b/presets/ragengine/vector_store/base.py
index fd45b9c..e493495 100644
--- a/presets/ragengine/vector_store/base.py
+++ b/presets/ragengine/vector_store/base.py
@@ -3,67 +3,86 @@
 
 import logging
 from abc import ABC, abstractmethod
-from typing import Dict, List
+from typing import Dict, List, Optional, Any
 import hashlib
 import os
+import asyncio
+from itertools import islice
 
 from llama_index.core import Document as LlamaDocument
 from llama_index.core.storage.index_store import SimpleIndexStore
-from llama_index.core import (StorageContext, VectorStoreIndex)
+from llama_index.core import (StorageContext, VectorStoreIndex, load_index_from_storage)
 from llama_index.core.postprocessor import LLMRerank  # Query with LLM Reranking
 
-from ragengine.models import Document
+from llama_index.vector_stores.faiss import FaissVectorStore
+
+from ragengine.models import Document, DocumentResponse
 from ragengine.embedding.base import BaseEmbeddingModel
 from ragengine.inference.inference import Inference
-from ragengine.config import (LLM_RERANKER_BATCH_SIZE, LLM_RERANKER_TOP_N, VECTOR_DB_PERSIST_DIR)
+from ragengine.config import (LLM_RERANKER_BATCH_SIZE, LLM_RERANKER_TOP_N)
+from fastapi import HTTPException
+
+from llama_index.core.storage.docstore import SimpleDocumentStore
+import aiorwlock
 
 # Configure logging
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger(__name__)
 
 class BaseVectorStore(ABC):
-    def __init__(self, embedding_manager: BaseEmbeddingModel):
-        self.embedding_manager = embedding_manager
-        self.embed_model = self.embedding_manager.model
+    def __init__(self, embed_model: BaseEmbeddingModel, use_rwlock: bool = False):
+        super().__init__()
+        self.llm = Inference()
+        self.embed_model = embed_model
         self.index_map = {}
         self.index_store = SimpleIndexStore()
-        self.llm = Inference()
+        # Use a reader/writer lock only if needed
+        self.use_rwlock = use_rwlock
+        self.rwlock = aiorwlock.RWLock() if self.use_rwlock else None
 
     @staticmethod
     def generate_doc_id(text: str) -> str:
         """Generates a unique document ID based on the hash of the document text."""
         return hashlib.sha256(text.encode('utf-8')).hexdigest()
 
-    def index_documents(self, index_name: str, documents: List[Document]) -> List[str]:
+    async def shutdown(self):
+        await self.llm.aclose()
+
+    async def index_documents(self, index_name: str, documents: List[Document]) -> List[str]:
         """Common indexing logic for all vector stores."""
         if index_name in self.index_map:
-            return self._append_documents_to_index(index_name, documents)
+            return await self._append_documents_to_index(index_name, documents)
         else:
-            return self._create_new_index(index_name, documents)
+            return await self._create_new_index(index_name, documents)
 
-    def _append_documents_to_index(self, index_name: str, documents: List[Document]) -> List[str]:
+    async def _append_documents_to_index(self, index_name: str, documents: List[Document]) -> List[str]:
         """Common logic for appending documents to existing index."""
         logger.info(f"Index {index_name} already exists. Appending documents to existing index.")
         indexed_doc_ids = set()
 
-        for doc in documents:
+        async def handle_document(doc: Document):
             doc_id = self.generate_doc_id(doc.text)
-            if not self.document_exists(index_name, doc, doc_id):
-                self.add_document_to_index(index_name, doc, doc_id)
+            if self.use_rwlock:
+                async with self.rwlock.reader_lock:
+                    retrieved_doc = await self.index_map[index_name].docstore.aget_ref_doc_info(doc_id)
+            else:
+                retrieved_doc = await self.index_map[index_name].docstore.aget_ref_doc_info(doc_id)
+            if not retrieved_doc:
+                await self.add_document_to_index(index_name, doc, doc_id)
                 indexed_doc_ids.add(doc_id)
             else:
                 logger.info(f"Document {doc_id} already exists in index {index_name}. Skipping.")
 
-        if indexed_doc_ids:
-            self._persist(index_name)
+        # Gather all coroutines for processing documents
+        await asyncio.gather(*(handle_document(doc) for doc in documents))
         return list(indexed_doc_ids)
     
     @abstractmethod
-    def _create_new_index(self, index_name: str, documents: List[Document]) -> List[str]:
+    async def _create_new_index(self, index_name: str, documents: List[Document]) -> List[str]:
         """Create a new index - implementation specific to each vector store."""
         pass
     
-    def _create_index_common(self, index_name: str, documents: List[Document], vector_store) -> List[str]:
+    async def _create_index_common(self, index_name: str, documents: List[Document], vector_store) -> List[str]:
         """Common logic for creating a new index with documents."""
         storage_context = StorageContext.from_defaults(vector_store=vector_store)
         llama_docs = []
@@ -76,18 +95,32 @@ class BaseVectorStore(ABC):
             indexed_doc_ids.add(doc_id)
 
         if llama_docs:
-            index = VectorStoreIndex.from_documents(
-                llama_docs,
-                storage_context=storage_context,
-                embed_model=self.embed_model,
-            )
-            index.set_index_id(index_name)
-            self.index_map[index_name] = index
-            self.index_store.add_index_struct(index.index_struct)
-            self._persist(index_name)
+            if self.use_rwlock:
+                async with self.rwlock.writer_lock:
+                    index = await asyncio.to_thread(
+                        VectorStoreIndex.from_documents,
+                        llama_docs,
+                        storage_context=storage_context,
+                        embed_model=self.embed_model,
+                        use_async=True,
+                    )
+                    index.set_index_id(index_name)
+                    self.index_map[index_name] = index
+                    self.index_store.add_index_struct(index.index_struct)
+            else:
+                index = await asyncio.to_thread(
+                    VectorStoreIndex.from_documents,
+                    llama_docs,
+                    storage_context=storage_context,
+                    embed_model=self.embed_model,
+                    use_async=True,
+                )
+                index.set_index_id(index_name)
+                self.index_map[index_name] = index
+                self.index_store.add_index_struct(index.index_struct)
         return list(indexed_doc_ids)
 
-    def query(self,
+    async def query(self,
               index_name: str,
               query: str,
               top_k: int,
@@ -136,7 +169,11 @@ class BaseVectorStore(ABC):
             similarity_top_k=top_k,
             node_postprocessors=node_postprocessors
         )
-        query_result = query_engine.query(query)
+        if self.use_rwlock:
+            async with self.rwlock.reader_lock:
+                query_result = await query_engine.aquery(query)
+        else:
+            query_result = await query_engine.aquery(query)
         return {
             "response": query_result.response,
             "source_nodes": [
@@ -151,48 +188,149 @@ class BaseVectorStore(ABC):
             "metadata": query_result.metadata,
         }
 
-    def add_document_to_index(self, index_name: str, document: Document, doc_id: str):
+    async def add_document_to_index(self, index_name: str, document: Document, doc_id: str):
         """Common logic for adding a single document."""
         if index_name not in self.index_map:
             raise ValueError(f"No such index: '{index_name}' exists.")
-        llama_doc = LlamaDocument(text=document.text, metadata=document.metadata, id_=doc_id)
-        self.index_map[index_name].insert(llama_doc)
+        llama_doc = LlamaDocument(id_=doc_id, text=document.text, metadata=document.metadata, excluded_llm_metadata_keys=[key for key in document.metadata.keys()])
 
-    def list_all_indexed_documents(self) -> Dict[str, Dict[str, Dict[str, str]]]:
-        """Common logic for listing all documents."""
-        return {
-            index_name: {
-                doc_info.ref_doc_id: {
-                    "text": doc_info.text, 
-                    "hash": doc_info.hash
-                } for _, doc_info in vector_store_index.docstore.docs.items()
+        if self.use_rwlock:
+            async with self.rwlock.writer_lock:
+                retrieved_doc = await self.index_map[index_name].docstore.aget_ref_doc_info(doc_id)
+                if retrieved_doc:
+                    logger.info(f"Document {doc_id} already exists in index {index_name} (double-check). Skipping insertion.")
+                    return
+                # Proceed with insertion only if the document is absent
+                await asyncio.to_thread(self.index_map[index_name].insert, llama_doc)
+        else:
+            await asyncio.to_thread(self.index_map[index_name].insert, llama_doc)
+
+    def list_indexes(self) -> List[str]:
+        return list(self.index_map.keys())
+
+    async def _process_document(self, doc_id: str, doc_stub, doc_store, max_text_length: Optional[int]):
+        """
+        Helper to process and format a single document.
+        """
+        try:
+            if isinstance(doc_store, SimpleDocumentStore):
+                text, hash_value = doc_stub.text, doc_stub.hash
+            else:
+                doc_info = await doc_store.aget_document(doc_id)
+                text, hash_value = doc_info.text, doc_info.hash
+
+            # Truncate if needed
+            is_truncated = bool(max_text_length and len(text) > max_text_length)
+            truncated_text = text[:max_text_length] if is_truncated else text
+
+            return {
+                "doc_id": doc_id,
+                "text": truncated_text,
+                "hash_value": hash_value,
+                "metadata": getattr(doc_stub, "metadata", {}),
+                "is_truncated": is_truncated,
             }
-            for index_name, vector_store_index in self.index_map.items()
-        }
+        except Exception as e:
+            logger.error(f"Error processing document {doc_id}: {str(e)}")
+            return None  # Explicitly return None for failed documents
+
+    async def list_documents_in_index(self, 
+            index_name: str, 
+            limit: int, 
+            offset: int, 
+            max_text_length: Optional[int] = None
+        ) -> List[Dict[str, Any]]:
+        """
+        Return a dictionary of document metadata for the given index.
+        """
+        vector_store_index = self.index_map.get(index_name)
+        if not vector_store_index:
+            raise ValueError(f"Index '{index_name}' not found.")
+        
+        doc_store = vector_store_index.docstore
+        docs_items = islice(doc_store.docs.items(), offset, offset + limit)
+
+        # Process documents concurrently, handling exceptions
+        docs = await asyncio.gather(
+            *(self._process_document(doc_id, doc_stub, doc_store, max_text_length) for doc_id, doc_stub in docs_items),
+            return_exceptions=True
+        )
 
-    def document_exists(self, index_name: str, doc: Document, doc_id: str) -> bool:
+        # Return list of valid documents
+        return [doc for doc in docs if isinstance(doc, dict)]
+
+    async def document_exists(self, index_name: str, doc: Document, doc_id: str) -> bool:
         """Common logic for checking document existence."""
         if index_name not in self.index_map:
             logger.warning(f"No such index: '{index_name}' exists in vector store.")
             return False
         return doc_id in self.index_map[index_name].ref_doc_info
 
-    def _persist_all(self):
-        """Common persistence logic."""
-        logger.info("Persisting all indexes.")
-        self.index_store.persist(os.path.join(VECTOR_DB_PERSIST_DIR, "store.json"))
-        for idx in self.index_store.index_structs():
-            self._persist(idx.index_id)
-
-    def _persist(self, index_name: str):
+    async def persist(self, index_name: str, path: str):
+        """Common persistence logic for individual index."""
+        if self.use_rwlock:
+            async with self.rwlock.writer_lock:
+                await self._persist_internal(index_name, path)
+        else:
+            await self._persist_internal(index_name, path)
+            
+    async def _persist_internal(self, index_name: str, path: str):
         """Common persistence logic for individual index."""
         try:
-            logger.info(f"Persisting index {index_name}.")
-            self.index_store.persist(os.path.join(VECTOR_DB_PERSIST_DIR, "store.json"))
-            assert index_name in self.index_map, f"No such index: '{index_name}' exists."
-            storage_context = self.index_map[index_name].storage_context
+            # Ensure the directory exists
+            os.makedirs(path, exist_ok=True)
+            if index_name not in self.index_map:
+                raise HTTPException(status_code=404, detail=f"No such index: '{index_name}' exists.")
+
             # Persist the specific index
-            storage_context.persist(persist_dir=os.path.join(VECTOR_DB_PERSIST_DIR, index_name))
+            storage_context = self.index_map[index_name].storage_context
+            await asyncio.to_thread(storage_context.persist, path)
             logger.info(f"Successfully persisted index {index_name}.")
         except Exception as e:
             logger.error(f"Failed to persist index {index_name}. Error: {str(e)}")
+            raise HTTPException(status_code=500, detail=f"Persistence failed: {str(e)}")
+
+    async def load(self, index_name: str, path: str, overwrite: bool):
+        """Common logic for loading an index."""
+        # Check path existence before acquiring any lock
+        if not os.path.exists(path):
+            raise HTTPException(status_code=404, detail=f"Path does not exist: {path}")
+        if self.use_rwlock:
+            async with self.rwlock.writer_lock:
+                await self._load_internal(index_name, path, overwrite)
+        else:
+            await self._load_internal(index_name, path, overwrite)
+
+    async def _load_internal(self, index_name: str, path: str, overwrite: bool):
+        """Common logic for loading an index."""
+        try:
+            if index_name in self.index_map and not overwrite:
+                raise HTTPException(
+                    status_code=409,
+                    detail=f"Index '{index_name}' already exists. Use a different name or delete the existing index first."
+                )
+
+            logger.info(f"Loading index {index_name} from {path}.")
+
+            try:
+                storage_context = StorageContext.from_defaults(persist_dir=path)
+            except UnicodeDecodeError as ude:
+                # Failed to load the index in the default json format, trying faissdb
+                faiss_vs = FaissVectorStore.from_persist_dir(persist_dir=path)
+                storage_context = StorageContext.from_defaults(persist_dir=path, vector_store=faiss_vs)
+            except Exception as e:
+                logger.error(f"Failed to load index '{index_name}'. Error: {str(e)}")
+                raise HTTPException(status_code=500, detail=f"Loading failed: {str(e)}")
+
+            logger.info(f"Loading index '{index_name}' using the workspace's embedding model.")
+            # Load the index using the workspace's embedding model, assuming all indices
+            # were created using the same embedding model currently in use.
+            loaded_index = await asyncio.to_thread(load_index_from_storage,
+                                                   storage_context,
+                                                   embed_model=self.embed_model,
+                                                   show_progress=True)
+            self.index_map[index_name] = loaded_index
+            logger.info(f"Successfully loaded index {index_name}.")
+        except Exception as e:
+            logger.error(f"Failed to load index {index_name}. Error: {str(e)}")
+            raise HTTPException(status_code=500, detail=f"Loading failed: {str(e)}")
\ No newline at end of file
diff --git a/presets/ragengine/vector_store/chromadb_store.py b/presets/ragengine/vector_store/chromadb_store.py
deleted file mode 100644
index 493c87c..0000000
--- a/presets/ragengine/vector_store/chromadb_store.py
+++ /dev/null
@@ -1,67 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT license.
-
-from typing import Dict, List
-from ragengine.models import Document
-import logging
-
-import chromadb
-import json
-from llama_index.vector_stores.chroma import ChromaVectorStore
-
-from .base import BaseVectorStore
-
-# Configure logging
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-class ChromaDBVectorStoreHandler(BaseVectorStore):
-    def __init__(self, embedding_manager):
-        super().__init__(embedding_manager)
-        self.chroma_client = chromadb.EphemeralClient()
-
-    def _create_new_index(self, index_name: str, documents: List[Document]) -> List[str]:
-        chroma_collection = self.chroma_client.create_collection(index_name)
-        vector_store = ChromaVectorStore(chroma_collection=chroma_collection)
-        return self._create_index_common(index_name, documents, vector_store)
-
-    def document_exists(self, index_name: str, doc: Document, doc_id: str) -> bool:
-        """ChromaDB for checking document existence."""
-        if index_name not in self.index_map:
-            logger.warning(f"No such index: '{index_name}' exists in vector store.")
-            return False
-        return doc.text in self.chroma_client.get_collection(name=index_name).get()["documents"]
-
-    def list_all_indexed_documents(self) -> Dict[str, Dict[str, Dict[str, str]]]:
-        indexed_docs = {} # Accumulate documents across all indexes
-        try:
-            for collection_name in self.chroma_client.list_collections():
-                collection_info = self.chroma_client.get_collection(collection_name).get()
-                for doc in zip(collection_info["ids"], collection_info["documents"], collection_info["metadatas"]):
-                    indexed_docs.setdefault(collection_name, {})[doc[0]] = {
-                        "text": doc[1],
-                        "metadata": json.dumps(doc[2]),
-                    }
-        except Exception as e:
-            print(f"Failed to get all collections in the ChromaDB instance: {e}")
-        return indexed_docs
-
-    def _clear_collection_and_indexes(self):
-        """Clears all collections and drops all indexes in the ChromaDB instance.
-
-        This method is primarily intended for testing purposes to ensure
-        a clean state between tests, preventing index and document conflicts.
-        """
-        try:
-           # Get all collections
-            collection_names = self.chroma_client.list_collections()
-
-            # Delete each collection
-            for collection_name in collection_names:
-                self.chroma_client.delete_collection(name=collection_name)
-                print(f"Collection '{collection_name}' has been deleted.")
-
-            print("All collections in the ChromaDB instance have been deleted.")
-        except Exception as e:
-            print(f"Failed to clear collections in the ChromaDB instance: {e}")
-    
\ No newline at end of file
diff --git a/presets/ragengine/vector_store/faiss_store.py b/presets/ragengine/vector_store/faiss_store.py
index 5e14d55..e76fece 100644
--- a/presets/ragengine/vector_store/faiss_store.py
+++ b/presets/ragengine/vector_store/faiss_store.py
@@ -6,15 +6,16 @@ from typing import List
 import faiss
 from llama_index.vector_stores.faiss import FaissVectorStore
 from ragengine.models import Document
+from ragengine.embedding.base import BaseEmbeddingModel
 from .base import BaseVectorStore
 
 
 class FaissVectorStoreHandler(BaseVectorStore):
-    def __init__(self, embedding_manager):
-        super().__init__(embedding_manager)
-        self.dimension = self.embedding_manager.get_embedding_dimension()
+    def __init__(self, embed_model: BaseEmbeddingModel):
+        super().__init__(embed_model, use_rwlock=True)
+        self.dimension = self.embed_model.get_embedding_dimension()
 
-    def _create_new_index(self, index_name: str, documents: List[Document]) -> List[str]:
+    async def _create_new_index(self, index_name: str, documents: List[Document]) -> List[str]:
         faiss_index = faiss.IndexFlatL2(self.dimension)
         vector_store = FaissVectorStore(faiss_index=faiss_index)
-        return self._create_index_common(index_name, documents, vector_store)
+        return await self._create_index_common(index_name, documents, vector_store)
diff --git a/presets/ragengine/vector_store_manager/manager.py b/presets/ragengine/vector_store_manager/manager.py
index 26467f9..b769986 100644
--- a/presets/ragengine/vector_store_manager/manager.py
+++ b/presets/ragengine/vector_store_manager/manager.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
 
-from typing import Dict, List
+from typing import Dict, List, Any
 
 from ragengine.models import Document
 from ragengine.vector_store.base import BaseVectorStore
@@ -10,11 +10,11 @@ class VectorStoreManager:
     def __init__(self, vector_store: BaseVectorStore):
         self.vector_store = vector_store
 
-    def index(self, index_name: str, documents: List[Document]) -> List[str]:
+    async def index(self, index_name: str, documents: List[Document]) -> List[str]:
         """Index new documents."""
-        return self.vector_store.index_documents(index_name, documents)
+        return await self.vector_store.index_documents(index_name, documents)
 
-    def query(self,
+    async def query(self,
               index_name: str,
               query: str,
               top_k: int,
@@ -22,8 +22,34 @@ class VectorStoreManager:
               rerank_params: dict
     ):
         """Query the indexed documents."""
-        return self.vector_store.query(index_name, query, top_k, llm_params, rerank_params)
+        return await self.vector_store.query(index_name, query, top_k, llm_params, rerank_params)
 
-    def list_all_indexed_documents(self) -> Dict[str, Dict[str, Dict[str, str]]]:
-        """List all documents."""
-        return self.vector_store.list_all_indexed_documents()
+    def list_indexes(self):
+        """List all indexes."""
+        return self.vector_store.list_indexes()
+
+    async def list_documents_in_index(self,
+            index_name: str,
+            limit: int,
+            offset: int,
+            max_text_length: int
+    ) -> List[Dict[str, Any]]:
+        """List all documents in index."""
+        return await self.vector_store.list_documents_in_index(
+            index_name,
+            limit,
+            offset,
+            max_text_length
+        )
+
+    async def persist(self, index_name: str, path: str) -> None:
+        """Persist existing index."""
+        return await self.vector_store.persist(index_name, path)
+
+    async def load(self, index_name: str, path: str, overwrite: bool) -> None:
+        """Load existing index."""
+        return await self.vector_store.load(index_name, path, overwrite)
+
+    async def shutdown(self):
+        """Shutdown the manager."""
+        await self.vector_store.shutdown()
diff --git a/presets/workspace/dependencies/requirements.txt b/presets/workspace/dependencies/requirements.txt
index 39a574e..3f4a135 100644
--- a/presets/workspace/dependencies/requirements.txt
+++ b/presets/workspace/dependencies/requirements.txt
@@ -1,11 +1,11 @@
 # Dependencies for TFS
 
 # Core Dependencies
-vllm==0.6.3
-transformers == 4.45.0
-torch==2.4.0
+vllm==0.8.2
+transformers == 4.49.0
+torch==2.6.0
 accelerate==1.0.0
-fastapi>=0.111.0,<0.112.0  # Allow patch updates
+fastapi==0.115.0
 pydantic>=2.9
 uvicorn[standard]>=0.29.0,<0.30.0  # Allow patch updates
 uvloop
@@ -17,10 +17,11 @@ jinja2>=3.1.0
 # Utility libraries
 datasets==2.19.1
 peft==0.11.1
-bitsandbytes==0.42.0
+bitsandbytes==0.45.3
 sentencepiece==0.2.0
 
 # Less critical, can be latest
 gputil
+setuptools
 psutil
 trl
diff --git a/presets/workspace/inference/chat_templates/deepseek-r1-distill-llama-8b.jinja b/presets/workspace/inference/chat_templates/deepseek-r1-distill-llama-8b.jinja
new file mode 100644
index 0000000..da1390e
--- /dev/null
+++ b/presets/workspace/inference/chat_templates/deepseek-r1-distill-llama-8b.jinja
@@ -0,0 +1,70 @@
+{% if not add_generation_prompt is defined %}
+    {% set add_generation_prompt = false %}
+{% endif %}
+
+{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}
+
+{%- for message in messages %}
+    {%- if message['role'] == 'system' %}
+        {% set ns.system_prompt = message['content'] %}
+    {%- endif %}
+{%- endfor %}
+
+{{ bos_token }}{{ ns.system_prompt }}
+
+{%- for message in messages %}
+    {%- if message['role'] == 'user' %}
+        {%- set ns.is_tool = false -%}
+        {{ '<｜User｜>' + message['content'] }}
+    {%- endif %}
+
+    {%- if message['role'] == 'assistant' and message['content'] is none %}
+        {%- set ns.is_tool = false -%}
+        {%- for tool in message['tool_calls'] %}
+            {%- if not ns.is_first %}
+                {{ '<｜Assistant｜><｜tool▁calls▁begin｜><｜tool▁call▁begin｜>' + tool['type'] +
+                '<｜tool▁sep｜>' + tool['function']['name'] + '\n' +
+                '```json\n' + tool['function']['arguments'] + '\n```' +
+                '<｜tool▁call▁end｜>' }}
+                {%- set ns.is_first = true -%}
+            {%- else %}
+                {{ '\n<｜tool▁call▁begin｜>' + tool['type'] + '<｜tool▁sep｜>' +
+                tool['function']['name'] + '\n' +
+                '```json\n' + tool['function']['arguments'] + '\n```' +
+                '<｜tool▁call▁end｜>' }}
+                {{ '<｜tool▁calls▁end｜><｜end▁of▁sentence｜>' }}
+            {%- endif %}
+        {%- endfor %}
+    {%- endif %}
+
+    {%- if message['role'] == 'assistant' and message['content'] is not none %}
+        {%- if ns.is_tool %}
+            {{ '<｜tool▁outputs▁end｜>' + message['content'] + '<｜end▁of▁sentence｜>' }}
+            {%- set ns.is_tool = false -%}
+        {%- else %}
+            {% set content = message['content'] %}
+            {% if '</think>' in content %}
+                {% set content = content.split('</think>')[-1] %}
+            {% endif %}
+            {{ '<｜Assistant｜>' + content + '<｜end▁of▁sentence｜>' }}
+        {%- endif %}
+    {%- endif %}
+
+    {%- if message['role'] == 'tool' %}
+        {%- set ns.is_tool = true -%}
+        {%- if ns.is_output_first %}
+            {{ '<｜tool▁outputs▁begin｜><｜tool▁output▁begin｜>' + message['content'] + '<｜tool▁output▁end｜>' }}
+            {%- set ns.is_output_first = false %}
+        {%- else %}
+            {{ '\n<｜tool▁output▁begin｜>' + message['content'] + '<｜tool▁output▁end｜>' }}
+        {%- endif %}
+    {%- endif %}
+{%- endfor -%}
+
+{% if ns.is_tool %}
+    {{ '<｜tool▁outputs▁end｜>' }}
+{% endif %}
+
+{% if add_generation_prompt and not ns.is_tool %}
+    {{ '<｜Assistant｜>' }}
+{% endif %}
diff --git a/presets/workspace/inference/chat_templates/deepseek-r1-distill-qwen-14b.jinja b/presets/workspace/inference/chat_templates/deepseek-r1-distill-qwen-14b.jinja
new file mode 100644
index 0000000..da1390e
--- /dev/null
+++ b/presets/workspace/inference/chat_templates/deepseek-r1-distill-qwen-14b.jinja
@@ -0,0 +1,70 @@
+{% if not add_generation_prompt is defined %}
+    {% set add_generation_prompt = false %}
+{% endif %}
+
+{% set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt='') %}
+
+{%- for message in messages %}
+    {%- if message['role'] == 'system' %}
+        {% set ns.system_prompt = message['content'] %}
+    {%- endif %}
+{%- endfor %}
+
+{{ bos_token }}{{ ns.system_prompt }}
+
+{%- for message in messages %}
+    {%- if message['role'] == 'user' %}
+        {%- set ns.is_tool = false -%}
+        {{ '<｜User｜>' + message['content'] }}
+    {%- endif %}
+
+    {%- if message['role'] == 'assistant' and message['content'] is none %}
+        {%- set ns.is_tool = false -%}
+        {%- for tool in message['tool_calls'] %}
+            {%- if not ns.is_first %}
+                {{ '<｜Assistant｜><｜tool▁calls▁begin｜><｜tool▁call▁begin｜>' + tool['type'] +
+                '<｜tool▁sep｜>' + tool['function']['name'] + '\n' +
+                '```json\n' + tool['function']['arguments'] + '\n```' +
+                '<｜tool▁call▁end｜>' }}
+                {%- set ns.is_first = true -%}
+            {%- else %}
+                {{ '\n<｜tool▁call▁begin｜>' + tool['type'] + '<｜tool▁sep｜>' +
+                tool['function']['name'] + '\n' +
+                '```json\n' + tool['function']['arguments'] + '\n```' +
+                '<｜tool▁call▁end｜>' }}
+                {{ '<｜tool▁calls▁end｜><｜end▁of▁sentence｜>' }}
+            {%- endif %}
+        {%- endfor %}
+    {%- endif %}
+
+    {%- if message['role'] == 'assistant' and message['content'] is not none %}
+        {%- if ns.is_tool %}
+            {{ '<｜tool▁outputs▁end｜>' + message['content'] + '<｜end▁of▁sentence｜>' }}
+            {%- set ns.is_tool = false -%}
+        {%- else %}
+            {% set content = message['content'] %}
+            {% if '</think>' in content %}
+                {% set content = content.split('</think>')[-1] %}
+            {% endif %}
+            {{ '<｜Assistant｜>' + content + '<｜end▁of▁sentence｜>' }}
+        {%- endif %}
+    {%- endif %}
+
+    {%- if message['role'] == 'tool' %}
+        {%- set ns.is_tool = true -%}
+        {%- if ns.is_output_first %}
+            {{ '<｜tool▁outputs▁begin｜><｜tool▁output▁begin｜>' + message['content'] + '<｜tool▁output▁end｜>' }}
+            {%- set ns.is_output_first = false %}
+        {%- else %}
+            {{ '\n<｜tool▁output▁begin｜>' + message['content'] + '<｜tool▁output▁end｜>' }}
+        {%- endif %}
+    {%- endif %}
+{%- endfor -%}
+
+{% if ns.is_tool %}
+    {{ '<｜tool▁outputs▁end｜>' }}
+{% endif %}
+
+{% if add_generation_prompt and not ns.is_tool %}
+    {{ '<｜Assistant｜>' }}
+{% endif %}
diff --git a/presets/workspace/inference/vllm/inference_api.py b/presets/workspace/inference/vllm/inference_api.py
index 61112a2..7827a1a 100644
--- a/presets/workspace/inference/vllm/inference_api.py
+++ b/presets/workspace/inference/vllm/inference_api.py
@@ -12,8 +12,8 @@ import uvloop
 import torch
 from vllm.utils import FlexibleArgumentParser
 import vllm.entrypoints.openai.api_server as api_server
-from vllm.entrypoints.openai.serving_engine import LoRAModulePath
-from vllm.engine.llm_engine import (LLMEngine, EngineArgs, EngineConfig)
+from vllm.entrypoints.openai.serving_models import LoRAModulePath
+from vllm.engine.llm_engine import (LLMEngine, EngineArgs, VllmConfig)
 from vllm.executor.executor_base import ExecutorBase
 
 # Initialize logger
@@ -38,7 +38,7 @@ class KAITOArgumentParser(argparse.ArgumentParser):
         # They should start with "kaito-" prefix to avoid conflict with vllm args
         self.add_argument("--kaito-adapters-dir", type=str, default="/mnt/adapter", help="Directory where adapters are stored in KAITO preset.")
         self.add_argument("--kaito-config-file", type=str, default="", help="Additional args for KAITO preset.")
-        self.add_argument("--kaito-max-probe-steps", type=int, default=6, help="Maximum number of steps to find the max available seq len fitting in the GPU memory.")
+        self.add_argument("--kaito-max-probe-steps", type=int, help="Maximum number of steps to find the max available seq len fitting in the GPU memory.")
 
     def _reset_vllm_defaults(self):
         local_rank = int(os.environ.get("LOCAL_RANK",
@@ -51,7 +51,7 @@ class KAITOArgumentParser(argparse.ArgumentParser):
         }
         self.vllm_parser.set_defaults(**server_default_args)
 
-        # See https://docs.vllm.ai/en/stable/models/engine_args.html for more args
+        # See https://docs.vllm.ai/en/stable/serving/engine_args.html for more args
         engine_default_args = {
             "model": "/workspace/vllm/weights",
             "cpu_offload_gb": 0,
@@ -110,10 +110,10 @@ class KaitoConfig:
 def load_lora_adapters(adapters_dir: str) -> Optional[LoRAModulePath]:
     lora_list: List[LoRAModulePath] = []
 
-    logger.info(f"Loading LoRA adapters from {adapters_dir}")
     if not os.path.exists(adapters_dir):
         return lora_list
 
+    logger.info(f"Loading LoRA adapters from {adapters_dir}")
     for adapter in os.listdir(adapters_dir):
         adapter_path = os.path.join(adapters_dir, adapter)
         if os.path.isdir(adapter_path):
@@ -121,34 +121,24 @@ def load_lora_adapters(adapters_dir: str) -> Optional[LoRAModulePath]:
 
     return lora_list
 
-def find_max_available_seq_len(engine_config: EngineConfig, max_probe_steps: int) -> int:
+def find_max_available_seq_len(vllm_config: VllmConfig, max_probe_steps: int) -> int:
     """
     Load model and run profiler to find max available seq len.
     """
-    # see https://github.com/vllm-project/vllm/blob/v0.6.3/vllm/engine/llm_engine.py#L335
-    executor_class = LLMEngine._get_executor_cls(engine_config)
-    executor = executor_class(
-        model_config=engine_config.model_config,
-        cache_config=engine_config.cache_config,
-        parallel_config=engine_config.parallel_config,
-        scheduler_config=engine_config.scheduler_config,
-        device_config=engine_config.device_config,
-        lora_config=engine_config.lora_config,
-        speculative_config=engine_config.speculative_config,
-        load_config=engine_config.load_config,
-        prompt_adapter_config=engine_config.prompt_adapter_config,
-        observability_config=engine_config.observability_config,
-    )
-
-    model_max_blocks = int(engine_config.model_config.max_model_len / engine_config.cache_config.block_size)
-    res = binary_search_with_limited_steps(model_max_blocks, max_probe_steps, lambda x: is_context_length_safe(executor, x))
+    executor_class = LLMEngine._get_executor_cls(vllm_config)
+    if vllm_config.scheduler_config.enable_chunked_prefill:
+        logger.info("Chunked Prefill is enabled, skip probing.")
+        return vllm_config.model_config.max_model_len
+    executor = executor_class(vllm_config=vllm_config)
+
+    res = binary_search_with_limited_steps(vllm_config.model_config.max_model_len, max_probe_steps, lambda x: is_context_length_safe(executor, x))
 
     # release memory
     del executor
     gc.collect()
     torch.cuda.empty_cache()
 
-    return engine_config.cache_config.block_size * res
+    return res
 
 def binary_search_with_limited_steps(upper: int, max_probe_steps: int, is_valid_fn: Callable[[int], bool]) -> int:
     """
@@ -180,20 +170,21 @@ def binary_search_with_limited_steps(upper: int, max_probe_steps: int, is_valid_
 
     return low
 
-def is_context_length_safe(executor: ExecutorBase, num_gpu_blocks: int) -> bool:
+def is_context_length_safe(executor: ExecutorBase, context_length: int) -> bool:
     """
     Check if the avilable gpu blocks is enough for the given num_gpu_blocks.
     """
-    context_length = executor.cache_config.block_size * num_gpu_blocks
+    # Profile memory usage with max_num_sequences sequences and the total
+    # number of tokens equal to max_num_batched_tokens.
     executor.scheduler_config.max_num_batched_tokens = context_length
-
     try:
         logger.info(f"Try to determine available gpu blocks for context length {context_length}")
-        # see https://github.com/vllm-project/vllm/blob/v0.6.3/vllm/engine/llm_engine.py#L477
+        # see https://github.com/vllm-project/vllm/blob/v0.7.2/vllm/engine/llm_engine.py#L416
         available_gpu_blocks, _ = executor.determine_num_available_blocks()
     except torch.OutOfMemoryError as e:
         return False    
 
+    num_gpu_blocks = context_length // executor.cache_config.block_size
     return available_gpu_blocks >= num_gpu_blocks
 
 def try_set_max_available_seq_len(args: argparse.Namespace):
@@ -201,6 +192,10 @@ def try_set_max_available_seq_len(args: argparse.Namespace):
         logger.info(f"max_model_len is set to {args.max_model_len}, skip probing.")
         return
 
+    if args.tensor_parallel_size > 1 or args.pipeline_parallel_size > 1:
+        logger.info("Multi-GPU serving is enabled, skip probing.")
+        return
+
     max_probe_steps = 0
     if args.kaito_max_probe_steps is not None:
         try:
@@ -215,13 +210,13 @@ def try_set_max_available_seq_len(args: argparse.Namespace):
     # read the model config from hf weights path.
     # vllm will perform different parser for different model architectures
     # and read it into a unified EngineConfig.
-    engine_config = engine_args.create_engine_config()
+    vllm_config = engine_args.create_engine_config()
 
-    max_model_len = engine_config.model_config.max_model_len
+    max_model_len = vllm_config.model_config.max_model_len
     available_seq_len = max_model_len
     logger.info("Try run profiler to find max available seq len")
-    available_seq_len = find_max_available_seq_len(engine_config, max_probe_steps)
-    # see https://github.com/vllm-project/vllm/blob/v0.6.3/vllm/worker/worker.py#L262
+    available_seq_len = find_max_available_seq_len(vllm_config, max_probe_steps)
+    # see https://github.com/vllm-project/vllm/blob/v0.7.2/vllm/worker/worker.py#L539
     if available_seq_len <= 0:
         raise ValueError("No available memory for the cache blocks. "
                         "Try increasing `gpu_memory_utilization` when "
diff --git a/presets/workspace/models/deepseek/README.md b/presets/workspace/models/deepseek/README.md
new file mode 100644
index 0000000..7bd1000
--- /dev/null
+++ b/presets/workspace/models/deepseek/README.md
@@ -0,0 +1,13 @@
+## Supported Models
+| Model name          |                        Model source                         |                                      | Kubernetes Workload | Distributed inference |
+|---------------------|:-----------------------------------------------------------:|:-------------------------------------------------------------------------------:|:-------------------:|:---------------------:|
+| deepseek-r1-distill-llama-8b  | [deepseek-r1-distill-llama-8b](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B)  | [link](../../../../examples/inference/kaito_workspace_deepseek_r1_distill_llama_8b.yaml)  |     Deployment      |         false         |
+| deepseek-r1-distill-qwen-14b  | [deepseek-r1-distill-qwen-14b](https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B)  | [link](../../../../examples/inference/kaito_workspace_deepseek_r1_distill_qwen_14b.yaml)  |     Deployment      |         false         |
+
+
+## Image Source
+- **Public**: Kaito maintainers manage the lifecycle of the inference service images that contain model weights. The images are available in Microsoft Container Registry (MCR).
+
+## Usage
+
+See [document](../../../../docs/inference/README.md).
diff --git a/presets/workspace/models/deepseek/model.go b/presets/workspace/models/deepseek/model.go
new file mode 100644
index 0000000..f0d2e8a
--- /dev/null
+++ b/presets/workspace/models/deepseek/model.go
@@ -0,0 +1,133 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+package deepseek
+
+import (
+	"time"
+
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/model"
+	"github.com/kaito-project/kaito/pkg/utils/plugin"
+	"github.com/kaito-project/kaito/pkg/workspace/inference"
+)
+
+func init() {
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetDeepSeekR1DistillLlama8BModel,
+		Instance: &deepseekA,
+	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetDeepSeekR1DistillQwen14BModel,
+		Instance: &deepseekB,
+	})
+}
+
+var (
+	PresetDeepSeekR1DistillLlama8BModel = "deepseek-r1-distill-llama-8b"
+	PresetDeepSeekR1DistillQwen14BModel = "deepseek-r1-distill-qwen-14b"
+
+	PresetDeepSeekTagMap = map[string]string{
+		"DeepSeekDistillLlama8B": "0.0.1",
+		"DeepSeekDistillQwen14B": "0.0.1",
+	}
+
+	baseCommandPresetDeepseekInference = "accelerate launch"
+	deepseekLlama8bRunParams           = map[string]string{
+		"torch_dtype": "bfloat16",
+		"pipeline":    "text-generation",
+	}
+	deepseekLlama8bRunParamsVLLM = map[string]string{
+		"dtype": "float16",
+	}
+	deepseekQwen14bRunParams = map[string]string{
+		"torch_dtype": "bfloat16",
+		"pipeline":    "text-generation",
+	}
+	deepseekQwen14bRunParamsVLLM = map[string]string{
+		"dtype": "float16",
+	}
+)
+
+var deepseekA llama8b
+
+type llama8b struct{}
+
+func (*llama8b) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "DeepSeek",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "50Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "14Gi",
+		PerGPUMemoryRequirement:   "0Gi", // We run DeepSeek using native vertical model parallel, no per GPU memory requirement.
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetDeepseekInference,
+				TorchRunParams:    inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    deepseekLlama8bRunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      PresetDeepSeekR1DistillLlama8BModel,
+				ModelRunParams: deepseekLlama8bRunParamsVLLM,
+			},
+			// vllm requires the model specification to be exactly divisible by
+			// the number of GPUs(tensor parallel level).
+			DisableTensorParallelism: true,
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetDeepSeekTagMap["DeepSeekDistillLlama8B"],
+	}
+}
+func (*llama8b) GetTuningParameters() *model.PresetParam {
+	return nil
+}
+func (*llama8b) SupportDistributedInference() bool {
+	return false
+}
+func (*llama8b) SupportTuning() bool {
+	return false
+}
+
+var deepseekB qwen14b
+
+type qwen14b struct{}
+
+func (*qwen14b) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "DeepSeek",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "50Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "25.7Gi",
+		PerGPUMemoryRequirement:   "0Gi", // We run DeepSeek using native vertical model parallel, no per GPU memory requirement.
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetDeepseekInference,
+				TorchRunParams:    inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    deepseekQwen14bRunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      PresetDeepSeekR1DistillQwen14BModel,
+				ModelRunParams: deepseekQwen14bRunParamsVLLM,
+			},
+			// vllm requires the model specification to be exactly divisible by
+			// the number of GPUs(tensor parallel level).
+			DisableTensorParallelism: true,
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetDeepSeekTagMap["DeepSeekDistillQwen14B"],
+	}
+}
+func (*qwen14b) GetTuningParameters() *model.PresetParam {
+	return nil
+}
+func (*qwen14b) SupportDistributedInference() bool {
+	return false
+}
+func (*qwen14b) SupportTuning() bool {
+	return false
+}
diff --git a/presets/workspace/models/falcon/model.go b/presets/workspace/models/falcon/model.go
index 34aac78..b805efb 100644
--- a/presets/workspace/models/falcon/model.go
+++ b/presets/workspace/models/falcon/model.go
@@ -5,7 +5,7 @@ package falcon
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -64,7 +64,7 @@ type falcon7b struct{}
 func (*falcon7b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "14Gi",
@@ -73,7 +73,7 @@ func (*falcon7b) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetFalconInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    falconRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -94,7 +94,7 @@ func (*falcon7b) GetInferenceParameters() *model.PresetParam {
 func (*falcon7b) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "16Gi",
@@ -126,7 +126,7 @@ type falcon7bInst struct{}
 func (*falcon7bInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "14Gi",
@@ -135,7 +135,7 @@ func (*falcon7bInst) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetFalconInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    falconRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -171,7 +171,7 @@ type falcon40b struct{}
 func (*falcon40b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "400",
 		GPUCountRequirement:       "2",
 		TotalGPUMemoryRequirement: "90Gi",
@@ -180,7 +180,7 @@ func (*falcon40b) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetFalconInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    falconRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -196,7 +196,7 @@ func (*falcon40b) GetInferenceParameters() *model.PresetParam {
 func (*falcon40b) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "2",
 		TotalGPUMemoryRequirement: "90Gi",
@@ -226,7 +226,7 @@ type falcon40bInst struct{}
 func (*falcon40bInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Falcon",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "400",
 		GPUCountRequirement:       "2",
 		TotalGPUMemoryRequirement: "90Gi",
@@ -235,7 +235,7 @@ func (*falcon40bInst) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetFalconInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    falconRunParams,
 			},
 			VLLM: model.VLLMParam{
diff --git a/presets/workspace/models/llama2/model.go b/presets/workspace/models/llama2/model.go
index a9fb5a2..5fe8ef3 100644
--- a/presets/workspace/models/llama2/model.go
+++ b/presets/workspace/models/llama2/model.go
@@ -5,7 +5,7 @@ package llama2
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -41,7 +41,7 @@ type llama2Text7b struct{}
 func (*llama2Text7b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "34Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "14Gi",
@@ -77,7 +77,7 @@ type llama2Text13b struct{}
 func (*llama2Text13b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "46Gi",
 		GPUCountRequirement:       "2",
 		TotalGPUMemoryRequirement: "30Gi",
@@ -113,7 +113,7 @@ type llama2Text70b struct{}
 func (*llama2Text70b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "158Gi",
 		GPUCountRequirement:       "8",
 		TotalGPUMemoryRequirement: "152Gi",
diff --git a/presets/workspace/models/llama2chat/model.go b/presets/workspace/models/llama2chat/model.go
index 431d04b..9c672cd 100644
--- a/presets/workspace/models/llama2chat/model.go
+++ b/presets/workspace/models/llama2chat/model.go
@@ -5,7 +5,7 @@ package llama2chat
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -41,7 +41,7 @@ type llama2Chat7b struct{}
 func (*llama2Chat7b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "34Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "16Gi",
@@ -77,7 +77,7 @@ type llama2Chat13b struct{}
 func (*llama2Chat13b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "46Gi",
 		GPUCountRequirement:       "2",
 		TotalGPUMemoryRequirement: "30Gi",
@@ -114,7 +114,7 @@ type llama2Chat70b struct{}
 func (*llama2Chat70b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "LLaMa2",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePrivate),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePrivate),
 		DiskStorageRequirement:    "158Gi",
 		GPUCountRequirement:       "8",
 		TotalGPUMemoryRequirement: "192Gi",
diff --git a/presets/workspace/models/mistral/model.go b/presets/workspace/models/mistral/model.go
index b3b8497..bc4b93f 100644
--- a/presets/workspace/models/mistral/model.go
+++ b/presets/workspace/models/mistral/model.go
@@ -5,7 +5,7 @@ package mistral
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -51,7 +51,7 @@ type mistral7b struct{}
 func (*mistral7b) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Mistral",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "100Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "14Gi",
@@ -61,7 +61,7 @@ func (*mistral7b) GetInferenceParameters() *model.PresetParam {
 				TorchRunParams:    inference.DefaultAccelerateParams,
 				ModelRunParams:    mistralRunParams,
 				BaseCommand:       baseCommandPresetMistralInference,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 			},
 			VLLM: model.VLLMParam{
 				BaseCommand:    inference.DefaultVLLMCommand,
@@ -77,7 +77,7 @@ func (*mistral7b) GetInferenceParameters() *model.PresetParam {
 func (*mistral7b) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Mistral",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "100Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "16Gi",
@@ -108,7 +108,7 @@ type mistral7bInst struct{}
 func (*mistral7bInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Mistral",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "100Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "16Gi",
@@ -118,7 +118,7 @@ func (*mistral7bInst) GetInferenceParameters() *model.PresetParam {
 				TorchRunParams:    inference.DefaultAccelerateParams,
 				ModelRunParams:    mistralRunParams,
 				BaseCommand:       baseCommandPresetMistralInference,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 			},
 			VLLM: model.VLLMParam{
 				BaseCommand:    inference.DefaultVLLMCommand,
diff --git a/presets/workspace/models/phi2/model.go b/presets/workspace/models/phi2/model.go
index bb7989d..c2d41a2 100644
--- a/presets/workspace/models/phi2/model.go
+++ b/presets/workspace/models/phi2/model.go
@@ -5,7 +5,7 @@ package phi2
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -43,7 +43,7 @@ type phi2 struct{}
 func (*phi2) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "12Gi",
@@ -53,7 +53,7 @@ func (*phi2) GetInferenceParameters() *model.PresetParam {
 				TorchRunParams:    inference.DefaultAccelerateParams,
 				ModelRunParams:    phiRunParams,
 				BaseCommand:       baseCommandPresetPhiInference,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 			},
 			VLLM: model.VLLMParam{
 				BaseCommand:    inference.DefaultVLLMCommand,
@@ -68,7 +68,7 @@ func (*phi2) GetInferenceParameters() *model.PresetParam {
 func (*phi2) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "16Gi",
diff --git a/presets/workspace/models/phi3/model.go b/presets/workspace/models/phi3/model.go
index c8c40e4..6d27fed 100644
--- a/presets/workspace/models/phi3/model.go
+++ b/presets/workspace/models/phi3/model.go
@@ -5,7 +5,7 @@ package phi3
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -68,7 +68,7 @@ type phi3Mini4KInst struct{}
 func (*phi3Mini4KInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "9Gi",
@@ -77,7 +77,7 @@ func (*phi3Mini4KInst) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetPhiInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    phiRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -93,7 +93,7 @@ func (*phi3Mini4KInst) GetInferenceParameters() *model.PresetParam {
 func (*phi3Mini4KInst) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "72Gi",
@@ -121,7 +121,7 @@ type phi3Mini128KInst struct{}
 func (*phi3Mini128KInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "9Gi",
@@ -130,7 +130,7 @@ func (*phi3Mini128KInst) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetPhiInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    phiRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -146,7 +146,7 @@ func (*phi3Mini128KInst) GetInferenceParameters() *model.PresetParam {
 func (*phi3Mini128KInst) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "72Gi",
@@ -172,7 +172,7 @@ type phi3_5MiniInst struct{}
 func (*phi3_5MiniInst) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3_5",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "8Gi",
@@ -181,7 +181,7 @@ func (*phi3_5MiniInst) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetPhiInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    phiRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -197,7 +197,7 @@ func (*phi3_5MiniInst) GetInferenceParameters() *model.PresetParam {
 func (*phi3_5MiniInst) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3_5",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "72Gi",
@@ -225,7 +225,7 @@ type Phi3Medium4kInstruct struct{}
 func (*Phi3Medium4kInstruct) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "28Gi",
@@ -234,7 +234,7 @@ func (*Phi3Medium4kInstruct) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetPhiInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    phiRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -250,7 +250,7 @@ func (*Phi3Medium4kInstruct) GetInferenceParameters() *model.PresetParam {
 func (*Phi3Medium4kInstruct) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "80Gi",
@@ -278,7 +278,7 @@ type Phi3Medium128kInstruct struct{}
 func (*Phi3Medium128kInstruct) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "28Gi",
@@ -287,7 +287,7 @@ func (*Phi3Medium128kInstruct) GetInferenceParameters() *model.PresetParam {
 			Transformers: model.HuggingfaceTransformersParam{
 				BaseCommand:       baseCommandPresetPhiInference,
 				TorchRunParams:    inference.DefaultAccelerateParams,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 				ModelRunParams:    phiRunParams,
 			},
 			VLLM: model.VLLMParam{
@@ -303,7 +303,7 @@ func (*Phi3Medium128kInstruct) GetInferenceParameters() *model.PresetParam {
 func (*Phi3Medium128kInstruct) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Phi3",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "50Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "80Gi",
diff --git a/presets/workspace/models/phi4/README.md b/presets/workspace/models/phi4/README.md
new file mode 100644
index 0000000..63b41cc
--- /dev/null
+++ b/presets/workspace/models/phi4/README.md
@@ -0,0 +1,12 @@
+## Supported Models
+| Model name                 |                               Model source                               |                               Sample workspace                                | Kubernetes Workload | Distributed inference |
+|----------------------------|:------------------------------------------------------------------------:|:-----------------------------------------------------------------------------:|:-------------------:|:---------------------:|
+| phi-4                      |   [microsoft](https://huggingface.co/microsoft/phi-4)                    |   [link](../../../../examples/inference/kaito_workspace_phi_4.yaml)           |     Deployment      |         false         |
+| phi-4-mini-instruct       |   [microsoft](https://huggingface.co/microsoft/phi-4-mini-instruct)     |   [link](../../../../examples/inference/kaito_workspace_phi_4_mini.yaml)     |     Deployment      |         false         |
+
+## Image Source
+- **Public**: Kaito maintainers manage the lifecycle of the inference service images that contain model weights. The images are available in Microsoft Container Registry (MCR).
+
+## Usage
+
+See [document](../../../../docs/inference/README.md).
diff --git a/presets/workspace/models/phi4/model.go b/presets/workspace/models/phi4/model.go
new file mode 100644
index 0000000..13ee0ec
--- /dev/null
+++ b/presets/workspace/models/phi4/model.go
@@ -0,0 +1,150 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+package phi4
+
+import (
+	"time"
+
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/model"
+	"github.com/kaito-project/kaito/pkg/utils/plugin"
+	"github.com/kaito-project/kaito/pkg/workspace/inference"
+)
+
+func init() {
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetPhi4Model,
+		Instance: &phi4A,
+	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetPhi4MiniInstructModel,
+		Instance: &phi4MiniB,
+	})
+}
+
+var (
+	PresetPhi4Model             = "phi-4"
+	PresetPhi4MiniInstructModel = "phi-4-mini-instruct"
+
+	PresetPhiTagMap = map[string]string{
+		"Phi4":             "0.1.0",
+		"Phi4MiniInstruct": "0.1.0",
+	}
+
+	baseCommandPresetPhiInference = "accelerate launch"
+	baseCommandPresetPhiTuning    = "cd /workspace/tfs/ && python3 metrics_server.py & accelerate launch"
+	phiRunParams                  = map[string]string{
+		"torch_dtype":       "auto",
+		"pipeline":          "text-generation",
+		"trust_remote_code": "",
+	}
+	phiRunParamsVLLM = map[string]string{
+		"dtype": "float16",
+	}
+)
+
+var phi4A phi4Model
+
+type phi4Model struct{}
+
+func (*phi4Model) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "Phi4",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "100Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "40Gi", // Requires at least A100 - TODO: Revisit for more accurate metric here
+		PerGPUMemoryRequirement:   "0Gi",  // We run Phi using native vertical model parallel, no per GPU memory requirement.
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetPhiInference,
+				TorchRunParams:    inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    phiRunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      "phi-4",
+				ModelRunParams: phiRunParamsVLLM,
+			},
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetPhiTagMap["Phi4"],
+	}
+}
+
+func (*phi4Model) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "Phi4",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "100Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "70Gi", // Requires at least A100 - TODO: Revisit for more accurate metric here
+		PerGPUMemoryRequirement:   "70Gi",
+		ReadinessTimeout:          time.Duration(30) * time.Minute,
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand: baseCommandPresetPhiTuning,
+			},
+		},
+		Tag: PresetPhiTagMap["Phi4"],
+	}
+}
+
+func (*phi4Model) SupportDistributedInference() bool { return false }
+func (*phi4Model) SupportTuning() bool {
+	return true
+}
+
+var phi4MiniB phi4MiniInstruct
+
+type phi4MiniInstruct struct{}
+
+func (*phi4MiniInstruct) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "Phi4",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "50Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "8Gi",
+		PerGPUMemoryRequirement:   "0Gi", // We run Phi using native vertical model parallel, no per GPU memory requirement.
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetPhiInference,
+				TorchRunParams:    inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    phiRunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      "phi-4-mini-instruct",
+				ModelRunParams: phiRunParamsVLLM,
+			},
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetPhiTagMap["Phi4MiniInstruct"],
+	}
+}
+
+func (*phi4MiniInstruct) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "Phi4",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "50Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "72Gi", // Requires at least A100 - TODO: Revisit for more accurate metric here
+		PerGPUMemoryRequirement:   "72Gi",
+		ReadinessTimeout:          time.Duration(30) * time.Minute,
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand: baseCommandPresetPhiTuning,
+			},
+		},
+		Tag: PresetPhiTagMap["Phi4MiniInstruct"],
+	}
+}
+
+func (*phi4MiniInstruct) SupportDistributedInference() bool { return false }
+func (*phi4MiniInstruct) SupportTuning() bool {
+	return true
+}
diff --git a/presets/workspace/models/qwen/README.md b/presets/workspace/models/qwen/README.md
index 9ca6aa5..69fc66f 100644
--- a/presets/workspace/models/qwen/README.md
+++ b/presets/workspace/models/qwen/README.md
@@ -2,6 +2,7 @@
 | Model name          |                              Model source                              |                                Sample workspace                                 | Kubernetes Workload | Distributed inference |
 |---------------------|:----------------------------------------------------------------------:|:-------------------------------------------------------------------------------:|:-------------------:|:---------------------:|
 | qwen2.5-coder-7b-instruct | [qwen](https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct) | [link](../../../../examples/inference/kaito_workspace_qwen_2.5_coder_7b-instruct.yaml) |     Deployment      |         false         |
+| qwen2.5-coder-32b-instruct | [qwen](https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct) | [link](../../../../examples/inference/kaito_workspace_qwen_2.5_coder_32b-instruct.yaml) |     Deployment      |         false         |
 
 ## Image Source
 - **Public**: Kaito maintainers manage the lifecycle of the inference service images that contain model weights. The images are available in Microsoft Container Registry (MCR).
diff --git a/presets/workspace/models/qwen/model.go b/presets/workspace/models/qwen/model.go
index 20a09df..105fa9d 100644
--- a/presets/workspace/models/qwen/model.go
+++ b/presets/workspace/models/qwen/model.go
@@ -5,7 +5,7 @@ package qwen
 import (
 	"time"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/workspace/inference"
@@ -16,13 +16,19 @@ func init() {
 		Name:     PresetQwen2_5Coder7BInstructModel,
 		Instance: &qwen2_5coder7bInst,
 	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetQwen2_5Coder32BInstructModel,
+		Instance: &qwen2_5coder32bInst,
+	})
 }
 
 var (
-	PresetQwen2_5Coder7BInstructModel = "qwen2.5-coder-7b-instruct"
+	PresetQwen2_5Coder7BInstructModel  = "qwen2.5-coder-7b-instruct"
+	PresetQwen2_5Coder32BInstructModel = "qwen2.5-coder-32b-instruct"
 
 	PresetTagMap = map[string]string{
-		"Qwen2.5-Coder-7B-Instruct": "0.0.1",
+		"Qwen2.5-Coder-7B-Instruct":  "0.0.1",
+		"Qwen2.5-Coder-32B-Instruct": "0.1.0",
 	}
 
 	baseCommandPresetQwenInference = "accelerate launch"
@@ -43,7 +49,7 @@ type qwen2_5Coder7BInstruct struct{}
 func (*qwen2_5Coder7BInstruct) GetInferenceParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "Qwen",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "100Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "24Gi",
@@ -53,7 +59,7 @@ func (*qwen2_5Coder7BInstruct) GetInferenceParameters() *model.PresetParam {
 				TorchRunParams:    inference.DefaultAccelerateParams,
 				ModelRunParams:    qwenRunParams,
 				BaseCommand:       baseCommandPresetQwenInference,
-				InferenceMainFile: inference.DefautTransformersMainFile,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
 			},
 			VLLM: model.VLLMParam{
 				BaseCommand:    inference.DefaultVLLMCommand,
@@ -69,7 +75,7 @@ func (*qwen2_5Coder7BInstruct) GetInferenceParameters() *model.PresetParam {
 func (*qwen2_5Coder7BInstruct) GetTuningParameters() *model.PresetParam {
 	return &model.PresetParam{
 		ModelFamilyName:           "qwen",
-		ImageAccessMode:           string(kaitov1alpha1.ModelImageAccessModePublic),
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
 		DiskStorageRequirement:    "100Gi",
 		GPUCountRequirement:       "1",
 		TotalGPUMemoryRequirement: "24Gi",
@@ -92,3 +98,58 @@ func (*qwen2_5Coder7BInstruct) SupportDistributedInference() bool {
 func (*qwen2_5Coder7BInstruct) SupportTuning() bool {
 	return true
 }
+
+var qwen2_5coder32bInst qwen2_5Coder32BInstruct
+
+type qwen2_5Coder32BInstruct struct{}
+
+func (*qwen2_5Coder32BInstruct) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "Qwen",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "120Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "70Gi", // Requires at least A100 - TODO: Revisit for more accurate metric
+		PerGPUMemoryRequirement:   "0Gi",  // We run qwen using native vertical model parallel, no per GPU memory requirement.
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				TorchRunParams:    inference.DefaultAccelerateParams,
+				ModelRunParams:    qwenRunParams,
+				BaseCommand:       baseCommandPresetQwenInference,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      PresetQwen2_5Coder32BInstructModel,
+				ModelRunParams: qwenRunParamsVLLM,
+			},
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetTagMap["Qwen2.5-Coder-32B-Instruct"],
+	}
+}
+
+func (*qwen2_5Coder32BInstruct) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ModelFamilyName:           "qwen",
+		ImageAccessMode:           string(kaitov1beta1.ModelImageAccessModePublic),
+		DiskStorageRequirement:    "120Gi",
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "140Gi", // Requires at least 2 A100 - TODO: Revisit for more accurate metric
+		PerGPUMemoryRequirement:   "70Gi",  // TODO: Revisit for more accurate metric
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand: baseCommandPresetQwenTuning,
+			},
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+		Tag:              PresetTagMap["Qwen2.5-Coder-32B-Instruct"],
+	}
+}
+
+func (*qwen2_5Coder32BInstruct) SupportDistributedInference() bool {
+	return false
+}
+func (*qwen2_5Coder32BInstruct) SupportTuning() bool {
+	return true
+}
diff --git a/presets/workspace/models/supported_models.yaml b/presets/workspace/models/supported_models.yaml
index eaa7dbb..37ea2c2 100644
--- a/presets/workspace/models/supported_models.yaml
+++ b/presets/workspace/models/supported_models.yaml
@@ -33,15 +33,16 @@ models:
   # Falcon
   - name: falcon-7b
     type: text-generation
-    version: https://huggingface.co/tiiuae/falcon-7b/commit/898df1396f35e447d5fe44e0a3ccaaaa69f30d36
+    version: https://huggingface.co/tiiuae/falcon-7b/commit/ec89142b67d748a1865ea4451372db8313ada0d8
     runtime: tfs
-    tag: 0.0.8
+    tag: 0.1.0
   - name: falcon-7b-instruct
     type: text-generation
-    version: https://huggingface.co/tiiuae/falcon-7b-instruct/commit/cf4b3c42ce2fdfe24f753f0f0d179202fea59c99
+    version: https://huggingface.co/tiiuae/falcon-7b-instruct/commit/8782b5c5d8c9290412416618f36a133653e85285
     runtime: tfs
-    tag: 0.0.8
+    tag: 0.1.0
     # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.8 - Support adapter and config file for VLLM runtime
     # 0.0.7 - Support VLLM runtime
     # 0.0.6 - Add Logging & Metrics Server
@@ -52,15 +53,16 @@ models:
     # 0.0.1 - Initial Release
   - name: falcon-40b
     type: text-generation
-    version: https://huggingface.co/tiiuae/falcon-40b/commit/4a70170c215b36a3cce4b4253f6d0612bb7d4146
+    version: https://huggingface.co/tiiuae/falcon-40b/commit/05ab2ee8d6b593bdbab17d728de5c028a7a94d83
     runtime: tfs
-    tag: 0.0.9
+    tag: 0.1.0
   - name: falcon-40b-instruct
     type: text-generation
     version: https://huggingface.co/tiiuae/falcon-40b-instruct/commit/ecb78d97ac356d098e79f0db222c9ce7c5d9ee5f
     runtime: tfs
-    tag: 0.0.9
+    tag: 0.1.0
     # Tag history for 40b models:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.9 - Support adapter and config file for VLLM runtime
     # 0.0.8 - Support VLLM runtime
     # 0.0.7 - Add Logging & Metrics Server
@@ -76,13 +78,14 @@ models:
     type: text-generation 
     version: https://huggingface.co/mistralai/Mistral-7B-v0.3/commit/d8cadc02ac76bd617a919d50b092e59d2d110aff
     runtime: tfs
-    tag: 0.0.9
+    tag: 0.1.0
   - name: mistral-7b-instruct
     type: text-generation
     version: https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3/commit/e0bc86c23ce5aae1db576c8cca6f06f1f73af2db
     runtime: tfs
-    tag: 0.0.9
+    tag: 0.1.0
     # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.9 - Support adapter and config file for VLLM runtime
     # 0.0.8 - Support VLLM runtime
     # 0.0.7 - Add Logging & Metrics Server
@@ -98,8 +101,9 @@ models:
     type: text-generation 
     version: https://huggingface.co/microsoft/phi-2/commit/ef382358ec9e382308935a992d908de099b64c23
     runtime: tfs
-    tag: 0.0.7
+    tag: 0.1.0
     # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.7 - Support adapter and config file for VLLM runtime
     # 0.0.6 - Support VLLM runtime
     # 0.0.5 - Add Logging & Metrics Server
@@ -113,23 +117,24 @@ models:
     type: text-generation 
     version: https://huggingface.co/microsoft/Phi-3-mini-4k-instruct/commit/0a67737cc96d2554230f90338b163bc6380a2a85
     runtime: tfs
-    tag: 0.0.4
+    tag: 0.1.0
   - name: phi-3-mini-128k-instruct
     type: text-generation 
-    version: https://huggingface.co/microsoft/Phi-3-mini-128k-instruct/commit/a90b62ae09941edff87a90ced39ba5807e6b2ade
+    version: https://huggingface.co/microsoft/Phi-3-mini-128k-instruct/commit/072cb7562cb8c4adf682a8e186aaafa49469eb5d
     runtime: tfs
-    tag: 0.0.4
+    tag: 0.1.0
   - name: phi-3-medium-4k-instruct
     type: text-generation
-    version: https://huggingface.co/microsoft/Phi-3-medium-4k-instruct/commit/ae004ae82eb6eddc32906dfacb1d6dfea8f91996
+    version: https://huggingface.co/microsoft/Phi-3-medium-4k-instruct/commit/b64223aaea6fbf273c0c8cd0801d5e732dce8897
     runtime: tfs
-    tag: 0.0.4
+    tag: 0.1.0
   - name: phi-3-medium-128k-instruct
     type: text-generation
     version: https://huggingface.co/microsoft/Phi-3-medium-128k-instruct/commit/fa7d2aa4f5ea69b2e36b20d050cdae79c9bfbb3f
     runtime: tfs
-    tag: 0.0.4
+    tag: 0.1.0
     # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.4 - Support adapter and config file for VLLM runtime
     # 0.0.3 - Support VLLM runtime
     # 0.0.2 - Add Logging & Metrics Server
@@ -137,17 +142,53 @@ models:
 
   - name: phi-3.5-mini-instruct
     type: text-generation
-    version: https://huggingface.co/microsoft/Phi-3.5-mini-instruct/commit/af0dfb8029e8a74545d0736d30cb6b58d2f0f3f0
+    version: https://huggingface.co/microsoft/Phi-3.5-mini-instruct/commit/3145e03a9fd4cdd7cd953c34d9bbf7ad606122ca
     runtime: tfs
-    tag: 0.0.2
+    tag: 0.1.0
     # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.2 - Support adapter and config file for VLLM runtime
     # 0.0.1 - New Model! Support VLLM Runtime
 
+  - name: phi-4-mini-instruct
+    type: text-generation
+    version: https://huggingface.co/microsoft/Phi-4-mini-instruct/commit/c0fb9e74abda11b496b7907a9c6c9009a7a0488f
+    runtime: tfs
+    tag: 0.1.0
+  - name: phi-4
+    type: text-generation
+    version: https://huggingface.co/microsoft/phi-4/commit/187ef0342fff0eb3333be9f00389385e95ef0b61
+    runtime: tfs
+    tag: 0.1.0
+  # Tag history:
+  # 0.1.0 - New Model!
+
+  # Qwen
   - name: qwen2.5-coder-7b-instruct
     type: text-generation
-    version: https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct/commit/0eb6b1ed2d0c4306bc637d09ecef51e59d3dfe05
+    version: https://huggingface.co/Qwen/Qwen2.5-Coder-7B-Instruct/commit/c03e6d358207e414f1eca0bb1891e29f1db0e242
+    runtime: tfs
+    tag: 0.1.0
+  - name: qwen2.5-coder-32b-instruct
+    type: text-generation
+    version: https://huggingface.co/Qwen/Qwen2.5-Coder-32B-Instruct/commit/381fc969f78efac66bc87ff7ddeadb7e73c218a7
+    runtime: tfs
+    tag: 0.1.0
+    # Tag history:
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
+    # 0.0.1 - New Model!
+
+  # Deepseek
+  - name: deepseek-r1-distill-qwen-14b
+    type: text-generation
+    version: https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B/commit/1df8507178afcc1bef68cd8c393f61a886323761
+    runtime: tfs
+    tag: 0.1.0
+  - name: deepseek-r1-distill-llama-8b
+    type: text-generation
+    version: https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B/commit/6a6f4aa4197940add57724a7707d069478df56b1
     runtime: tfs
-    tag: 0.0.1
+    tag: 0.1.0
     # Tag history:
-    # 0.0.1 - New Model!
\ No newline at end of file
+    # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
+    # 0.0.1 - New Model!
diff --git a/presets/workspace/tuning/text-generation/parser.py b/presets/workspace/tuning/text-generation/parser.py
index 4c07b68..519ceac 100644
--- a/presets/workspace/tuning/text-generation/parser.py
+++ b/presets/workspace/tuning/text-generation/parser.py
@@ -47,10 +47,25 @@ def filter_unsupported_init_args(dataclass_type, config_dict):
 # Function to parse a single section
 def parse_section(section_name, section_config):
     parser = HfArgumentParser((CONFIG_CLASS_MAP[section_name],))
-    # Flatten section_config to CLI-like arguments
-    cli_args = flatten_config_to_cli_args(section_config, prefix='')
-    # Parse the CLI-like arguments into a data class instance
-    return parser.parse_args_into_dataclasses(cli_args)[0]
+    try:
+        # Try parsing normally
+        cli_args = flatten_config_to_cli_args(section_config, prefix='')
+        instance = parser.parse_args_into_dataclasses(cli_args)[0]
+    except AttributeError as e:
+        if section_name == "DataCollator" and section_config.get("mlm", False):
+            print("Warning: Tokenizer does not have a mask token. Retrying with mlm=False.")
+            # Update the section_config to set mlm to False
+            section_config["mlm"] = False
+            # Regenerate the CLI arguments from the updated config
+            cli_args = flatten_config_to_cli_args(section_config, prefix='')
+            instance = parser.parse_args_into_dataclasses(cli_args)[0]
+        else:
+            # Reraise any other Error
+            raise e
+    except Exception as e:
+        raise e
+
+    return instance
 
 
 def parse_configs(config_yaml):
diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl
index d179b12..ec9ef95 100644
--- a/terraform/.terraform.lock.hcl
+++ b/terraform/.terraform.lock.hcl
@@ -2,41 +2,41 @@
 # Manual edits may be lost in future updates.
 
 provider "registry.terraform.io/hashicorp/azurerm" {
-  version     = "4.14.0"
-  constraints = "4.14.0"
+  version     = "4.16.0"
+  constraints = "4.16.0"
   hashes = [
-    "h1:FYZ9qh8i3X2gDmUTe1jJ/VzdSyjGjVmhBzv2R8D6CBo=",
-    "zh:05aaea16fc5f27b14d9fbad81654edf0638949ed3585576b2219c76a2bee095a",
-    "zh:065ce6ed16ba3fa7efcf77888ea582aead54e6a28f184c6701b73d71edd64bb0",
-    "zh:3c0cd17c249d18aa2e0120acb5f0c14810725158b379a67fec1331110e7c50df",
-    "zh:5a3ba3ffb2f1ce519fe3bf84a7296aa5862c437c70c62f0b0a5293bea9f2d01c",
-    "zh:7a8e9d72fa2714f4d567270b1761d4b4e788de7c15dada7db0cf0e29933185a2",
-    "zh:a11e190073f31c1238c15af29b9162e0f4564f6b0cd0310a3fa94102738450dc",
-    "zh:a5c004114410cc6dcb8fed584c9f3b84283b58025b0073a7e88d2bdb27840dfa",
-    "zh:a674a41db118e244eda7591e455d2ec338626664e0856e4125e909eb038f78db",
-    "zh:b5139010e4cbb2cb1a27c775610593c1c8063d3a7c82b00a65006509c434df2f",
-    "zh:cbb031223ccd8b099ac4d19b92641142f330b90f2fc6452843e445bae28f832c",
+    "h1:7e25Wr4cpUvlAcwL+9ZOeeA1xha84LqTZNviDaVQFlo=",
+    "zh:2035e461a94bd4180557a06f8e56f228a8a035608d0dac4d08e5870cf9265276",
+    "zh:3f15778a22ef1b9d0fa28670e5ea6ef1094b0be2533f43f350a2ef15d471b353",
+    "zh:4f1a4d03b008dd958bcd6bf82cf088fbaa9c121be2fd35e10e6b06c6e8f6aaa1",
+    "zh:5859f31c342364e849b4f8c437a46f33e927fa820244d0732b8d2ec74a95712d",
+    "zh:693d0f15512ca8c6b5e999b3a7551503feb06b408b3836bc6a6403e518b9ddab",
+    "zh:7f4912bec5b04f5156935292377c12484c13582151eb3c2555df409a7e5fb6e0",
+    "zh:bb9a509497f3a131c52fac32348919bf1b9e06c69a65f24607b03f7b56fb47b6",
+    "zh:c1b0c64e49ac591fd038ad71e71403ff71c07476e27e8da718c29f0028ea6d0d",
+    "zh:dd4ca432ee14eb0bb0cdc0bb463c8675b8ef02497be870a20d8dfee3e7fe52b3",
+    "zh:df58bb7fea984d2b11709567842ca4d55b3f24e187aa6be99e3677f55cbbe7da",
     "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
-    "zh:f7e7db1b94082a4ac3d4af3dabe7bbd335e1679305bf8e29d011f0ee440724ca",
+    "zh:f7fb37704da50c096f9c7c25e8a95fe73ce1d3c5aab0d616d506f07bc5cfcdd8",
   ]
 }
 
 provider "registry.terraform.io/hashicorp/helm" {
-  version     = "2.17.0"
-  constraints = "2.17.0"
+  version     = "3.0.0-pre1"
+  constraints = "3.0.0-pre1"
   hashes = [
-    "h1:K5FEjxvDnxb1JF1kG1xr8J3pNGxoaR3Z0IBG9Csm/Is=",
-    "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4",
-    "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7",
-    "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3",
-    "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c",
-    "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd",
-    "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940",
-    "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e",
-    "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930",
-    "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f",
-    "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654",
-    "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb",
+    "h1:f/06t5y9zfuSEOujJLhs/Pt1bbM6kf3HObeJtGZPbBk=",
+    "zh:1d5394356fb3c0bc7243d059a72ae51ef92912ce8fad1aec585df2fdceef8f7a",
+    "zh:485c0d69ae9efe0340e6b921237a9ceb39ada3db3758a23655289ca39111635a",
+    "zh:4c340a810f07ec4c28ea07e87588e53b7ec7d7306af47e402d66564396d9785a",
+    "zh:57f4b222826a91f26b3043c2ac9a43c5f52143cb6116b9a8a9068cd24412eb06",
+    "zh:64573d38158e6d9ce3476bf48a2c4ebd79da91d26ddf7836799e2d77a288a69e",
+    "zh:6606a3235b4f6e309d406732e039c362bd679e32cdfc33d2571967ff968dec58",
+    "zh:85f8f179c5e45dc8c94fe3379f681b4fe9411bdf8ce6bb9ba02987da6e9db1a8",
+    "zh:952eccb472d00634ff7392ca8739e72072b545e5cf0f3e6a21390fd490d1e70b",
+    "zh:a85c3d3e44966ab2f8d99e73c0a382bae3caa987dfde6fa0d8ae09455acb9450",
+    "zh:ae17cb8a34281dac138735d1185bae991e35f1b4ba8eee78e3893cd81e40d20c",
+    "zh:bfbdeef7735be5eabc301fecd964ab5c2ba1121e65a9dbed570cca5c8c5208c0",
     "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
   ]
 }
diff --git a/terraform/main.tf b/terraform/main.tf
index bcd91c3..8ff44d9 100644
--- a/terraform/main.tf
+++ b/terraform/main.tf
@@ -2,7 +2,7 @@ terraform {
   required_providers {
     azurerm = {
       source  = "hashicorp/azurerm"
-      version = "=4.14.0"
+      version = "=4.16.0"
     }
 
     random = {
@@ -17,7 +17,7 @@ terraform {
 
     helm = {
       source  = "hashicorp/helm"
-      version = "=2.17.0"
+      version = "=3.0.0-pre1"
     }
   }
 }
@@ -40,7 +40,7 @@ provider "kubernetes" {
 }
 
 provider "helm" {
-  kubernetes {
+  kubernetes = {
     host                   = azurerm_kubernetes_cluster.example.kube_config.0.host
     username               = azurerm_kubernetes_cluster.example.kube_config.0.username
     password               = azurerm_kubernetes_cluster.example.kube_config.0.password
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 0661e76..2596b08 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -6,13 +6,13 @@ variable "location" {
 
 variable "kaito_gpu_provisioner_version" {
   type        = string
-  default     = "0.2.1"
+  default     = "0.3.1"
   description = "kaito gpu provisioner version"
 }
 
 variable "kaito_workspace_version" {
   type        = string
-  default     = "0.4.1"
+  default     = "0.4.4"
   description = "kaito workspace version"
 }
 



Test patch:
diff --git a/api/v1alpha1/workspace_validation_test.go b/api/v1alpha1/workspace_validation_test.go
index 76dc47c..3763877 100644
--- a/api/v1alpha1/workspace_validation_test.go
+++ b/api/v1alpha1/workspace_validation_test.go
@@ -224,6 +224,7 @@ func TestResourceSpecValidateCreate(t *testing.T) {
 		modelPerGPUMemory   string
 		modelTotalGPUMemory string
 		preset              bool
+		presetNameOverride  string
 		errContent          string // Content expect error to include, if any
 		expectErrs          bool
 		validateTuning      bool // To indicate if we are testing tuning validation
@@ -362,6 +363,17 @@ func TestResourceSpecValidateCreate(t *testing.T) {
 			expectErrs:     true,
 			validateTuning: true,
 		},
+		{
+			name: "Invalid Preset Name",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC6s_v3",
+				Count:        pointerToInt(2),
+			},
+			errContent:         "",
+			preset:             true,
+			presetNameOverride: "Invalid-Preset-Name",
+			expectErrs:         false,
+		},
 	}
 
 	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
@@ -386,10 +398,15 @@ func TestResourceSpecValidateCreate(t *testing.T) {
 				var spec InferenceSpec
 
 				if tc.preset {
+					presetName := ModelName("test-validation")
+					if tc.presetNameOverride != "" {
+						presetName = ModelName(tc.presetNameOverride)
+					}
+
 					spec = InferenceSpec{
 						Preset: &PresetSpec{
 							PresetMeta: PresetMeta{
-								Name: ModelName("test-validation"),
+								Name: presetName,
 							},
 						},
 					}
@@ -403,7 +420,7 @@ func TestResourceSpecValidateCreate(t *testing.T) {
 				totalGPUMemoryRequirement = tc.modelTotalGPUMemory
 				perGPUMemoryRequirement = tc.modelPerGPUMemory
 
-				errs := tc.resourceSpec.validateCreateWithInference(&spec)
+				errs := tc.resourceSpec.validateCreateWithInference(&spec, false)
 				hasErrs := errs != nil
 				if hasErrs != tc.expectErrs {
 					t.Errorf("validateCreate() errors = %v, expectErrs %v", errs, tc.expectErrs)
@@ -542,7 +559,7 @@ func TestInferenceSpecValidateCreate(t *testing.T) {
 					},
 				},
 			},
-			errContent: "model is not registered",
+			errContent: "Unsupported inference preset name",
 			expectErrs: true,
 		},
 		{
@@ -1150,8 +1167,8 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 		{
 			name: "All fields valid",
 			tuningSpec: &TuningSpec{
-				Input:  &DataSource{Name: "valid-input", Image: "AZURE_ACR.azurecr.io/test:0.0.0"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
@@ -1161,8 +1178,8 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 		{
 			name: "Verify QLoRA Config",
 			tuningSpec: &TuningSpec{
-				Input:  &DataSource{Name: "valid-input", Image: "AZURE_ACR.azurecr.io/test:0.0.0"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodQLora,
 			},
@@ -1172,7 +1189,7 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 		{
 			name: "Missing Input",
 			tuningSpec: &TuningSpec{
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
@@ -1193,7 +1210,7 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 			name: "Missing Preset",
 			tuningSpec: &TuningSpec{
 				Input:  &DataSource{Name: "valid-input"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
 				Method: TuningMethodLora,
 			},
 			wantErr:   true,
@@ -1203,7 +1220,7 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 			name: "Invalid Preset",
 			tuningSpec: &TuningSpec{
 				Input:  &DataSource{Name: "valid-input"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("invalid-preset")}},
 				Method: TuningMethodLora,
 			},
@@ -1214,7 +1231,7 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 			name: "Invalid Method",
 			tuningSpec: &TuningSpec{
 				Input:  &DataSource{Name: "valid-input"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: "invalid-method",
 			},
@@ -1224,8 +1241,8 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 		{
 			name: "Invalid Input Source Casing",
 			tuningSpec: &TuningSpec{
-				Input:  &DataSource{Name: "valid-input", Image: "AZURE_ACR.azurecr.io/INPUT:0.0.0"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/output:0.0.0", ImagePushSecret: "secret"},
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/INPUT:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/output:0.0.0", ImagePushSecret: "secret"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
@@ -1235,8 +1252,8 @@ func TestTuningSpecValidateCreate(t *testing.T) {
 		{
 			name: "Invalid Output Destination Casing",
 			tuningSpec: &TuningSpec{
-				Input:  &DataSource{Name: "valid-input", Image: "AZURE_ACR.azurecr.io/input:0.0.0"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/OUTPUT:0.0.0", ImagePushSecret: "secret"},
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/input:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/OUTPUT:0.0.0", ImagePushSecret: "secret"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
@@ -1278,13 +1295,13 @@ func TestTuningSpecValidateUpdate(t *testing.T) {
 			name: "No changes",
 			oldTuning: &TuningSpec{
 				Input:  &DataSource{Name: "input1"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
 			newTuning: &TuningSpec{
 				Input:  &DataSource{Name: "input1"},
-				Output: &DataDestination{Image: "AZURE_ACR.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0"},
 				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
 				Method: TuningMethodLora,
 			},
@@ -1351,7 +1368,7 @@ func TestDataSourceValidateCreate(t *testing.T) {
 		{
 			name: "Volume specified only",
 			dataSource: &DataSource{
-				Image: "AZURE_ACR.azurecr.io/test:0.0.0",
+				Image: "kaito.azurecr.io/test:0.0.0",
 			},
 			wantErr: false,
 		},
@@ -1368,7 +1385,7 @@ func TestDataSourceValidateCreate(t *testing.T) {
 				Image:            "data-image:latest",
 				ImagePullSecrets: []string{"imagePushSecret"},
 			},
-			wantErr: true,
+			wantErr: false,
 		},
 		{
 			name: "Image without Tag Specified",
@@ -1376,7 +1393,16 @@ func TestDataSourceValidateCreate(t *testing.T) {
 				Image:            "aimodels.azurecr.io/data-image",
 				ImagePullSecrets: []string{"imagePushSecret"},
 			},
-			wantErr: true,
+			wantErr: false,
+		},
+		{
+			name: "Invalid image",
+			dataSource: &DataSource{
+				Image:            "!ValidImage",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr:  true,
+			errField: "invalid reference format",
 		},
 		{
 			name:       "None specified",
@@ -1455,6 +1481,16 @@ func TestDataSourceValidateUpdate(t *testing.T) {
 			wantErr:   true,
 			errFields: []string{"Name"},
 		},
+		{
+			name:      "Invalid image",
+			oldSource: &DataSource{},
+			newSource: &DataSource{
+				Image:            "!ValidImage",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr:   true,
+			errFields: []string{"invalid reference format"},
+		},
 	}
 
 	for _, tt := range tests {
@@ -1511,7 +1547,7 @@ func TestDataDestinationValidateCreate(t *testing.T) {
 				Image:           "data-image:latest",
 				ImagePushSecret: "imagePushSecret",
 			},
-			wantErr: true,
+			wantErr: false,
 		},
 		{
 			name: "Image without Tag Specified",
@@ -1519,7 +1555,16 @@ func TestDataDestinationValidateCreate(t *testing.T) {
 				Image:           "aimodels.azurecr.io/data-image",
 				ImagePushSecret: "imagePushSecret",
 			},
-			wantErr: true,
+			wantErr: false,
+		},
+		{
+			name: "Invalid image",
+			dataDestination: &DataDestination{
+				Image:           "!ValidImage",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr:  true,
+			errField: "invalid reference format",
 		},
 		// {
 		// 	name: "Both fields specified",
@@ -1570,6 +1615,16 @@ func TestDataDestinationValidateUpdate(t *testing.T) {
 			},
 			wantErr: false,
 		},
+		{
+			name:    "Invalid image",
+			oldDest: &DataDestination{},
+			newDest: &DataDestination{
+				Image:           "!ValidImage",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr:   true,
+			errFields: []string{"invalid reference format"},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/api/v1beta1/workspace_validation_test.go b/api/v1beta1/workspace_validation_test.go
new file mode 100644
index 0000000..9499699
--- /dev/null
+++ b/api/v1beta1/workspace_validation_test.go
@@ -0,0 +1,1648 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package v1beta1
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	"github.com/kaito-project/kaito/pkg/k8sclient"
+	"github.com/kaito-project/kaito/pkg/model"
+	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/utils/plugin"
+)
+
+const DefaultReleaseNamespace = "kaito-workspace"
+
+var ValidStrength string = "0.5"
+var InvalidStrength1 string = "invalid"
+var InvalidStrength2 string = "1.5"
+
+var gpuCountRequirement string
+var totalGPUMemoryRequirement string
+var perGPUMemoryRequirement string
+
+var invalidSourceName string
+
+func init() {
+	// Define a invalid source name longer than 253
+	for i := 0; i < 32; i++ {
+		invalidSourceName += "Adapter1"
+	}
+}
+
+type testModel struct{}
+
+func (*testModel) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		GPUCountRequirement:       gpuCountRequirement,
+		TotalGPUMemoryRequirement: totalGPUMemoryRequirement,
+		PerGPUMemoryRequirement:   perGPUMemoryRequirement,
+	}
+}
+func (*testModel) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		GPUCountRequirement:       gpuCountRequirement,
+		TotalGPUMemoryRequirement: totalGPUMemoryRequirement,
+		PerGPUMemoryRequirement:   perGPUMemoryRequirement,
+	}
+}
+func (*testModel) SupportDistributedInference() bool {
+	return false
+}
+func (*testModel) SupportTuning() bool {
+	return true
+}
+
+type testModelStatic struct{}
+
+func (*testModelStatic) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "16Gi",
+		PerGPUMemoryRequirement:   "16Gi",
+	}
+}
+func (*testModelStatic) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		GPUCountRequirement:       "1",
+		TotalGPUMemoryRequirement: "16Gi",
+		PerGPUMemoryRequirement:   "16Gi",
+	}
+}
+func (*testModelStatic) SupportDistributedInference() bool {
+	return false
+}
+func (*testModelStatic) SupportTuning() bool {
+	return true
+}
+
+type testModelPrivate struct{}
+
+func (*testModelPrivate) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ImageAccessMode:           string(ModelImageAccessModePrivate),
+		GPUCountRequirement:       gpuCountRequirement,
+		TotalGPUMemoryRequirement: totalGPUMemoryRequirement,
+		PerGPUMemoryRequirement:   perGPUMemoryRequirement,
+	}
+}
+func (*testModelPrivate) GetTuningParameters() *model.PresetParam {
+	return &model.PresetParam{
+		ImageAccessMode:           string(ModelImageAccessModePrivate),
+		GPUCountRequirement:       gpuCountRequirement,
+		TotalGPUMemoryRequirement: totalGPUMemoryRequirement,
+		PerGPUMemoryRequirement:   perGPUMemoryRequirement,
+	}
+}
+func (*testModelPrivate) SupportDistributedInference() bool {
+	return false
+}
+func (*testModelPrivate) SupportTuning() bool {
+	return true
+}
+
+func RegisterValidationTestModels() {
+	var test testModel
+	var testPrivate testModelPrivate
+	var testStatic testModelStatic
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     "test-validation",
+		Instance: &test,
+	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     "private-test-validation",
+		Instance: &testPrivate,
+	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     "test-validation-static",
+		Instance: &testStatic,
+	})
+}
+
+func pointerToInt(i int) *int {
+	return &i
+}
+
+func defaultConfigMapManifest() *v1.ConfigMap {
+	return &v1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      DefaultLoraConfigMapTemplate,
+			Namespace: DefaultReleaseNamespace, // Replace this with the appropriate namespace variable if dynamic
+		},
+		Data: map[string]string{
+			"training_config.yaml": `training_config:
+  ModelConfig:
+    torch_dtype: "bfloat16"
+    local_files_only: true
+    device_map: "auto"
+
+  QuantizationConfig:
+    load_in_4bit: false
+
+  LoraConfig:
+    r: 16
+    lora_alpha: 32
+    target_modules: "query_key_value"
+    lora_dropout: 0.05
+    bias: "none"
+
+  TrainingArguments:
+    output_dir: "output"
+    num_train_epochs: 4
+    auto_find_batch_size: true
+    ddp_find_unused_parameters: false
+    save_strategy: "epoch"
+
+  DatasetConfig:
+    shuffle_dataset: true
+    train_test_split: 1
+
+  DataCollator:
+    mlm: true`,
+		},
+	}
+}
+
+func qloraConfigMapManifest() *v1.ConfigMap {
+	return &v1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      DefaultQloraConfigMapTemplate,
+			Namespace: DefaultReleaseNamespace,
+		},
+		Data: map[string]string{
+			"training_config.yaml": `training_config:
+  ModelConfig:
+    torch_dtype: "bfloat16"
+    local_files_only: true
+    device_map: "auto"
+
+  QuantizationConfig:
+    load_in_4bit: true
+    bnb_4bit_quant_type: "nf4"
+    bnb_4bit_compute_dtype: "bfloat16"
+    bnb_4bit_use_double_quant: true
+
+  LoraConfig:
+    r: 16
+    lora_alpha: 32
+    target_modules: "query_key_value"
+    lora_dropout: 0.05
+    bias: "none"
+
+  TrainingArguments:
+    output_dir: "output"
+    num_train_epochs: 4
+    auto_find_batch_size: true
+    ddp_find_unused_parameters: false
+    save_strategy: "epoch"
+
+  DatasetConfig:
+    shuffle_dataset: true
+    train_test_split: 1
+
+  DataCollator:
+    mlm: true`,
+		},
+	}
+}
+
+func TestResourceSpecValidateCreate(t *testing.T) {
+	RegisterValidationTestModels()
+	tests := []struct {
+		name                string
+		resourceSpec        *ResourceSpec
+		modelGPUCount       string
+		modelPerGPUMemory   string
+		modelTotalGPUMemory string
+		preset              bool
+		presetNameOverride  string
+		errContent          string // Content expect error to include, if any
+		expectErrs          bool
+		validateTuning      bool // To indicate if we are testing tuning validation
+	}{
+		{
+			name: "Valid Resource",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_ND96asr_v4",
+				Count:        pointerToInt(1),
+			},
+			modelGPUCount:       "8",
+			modelPerGPUMemory:   "19Gi",
+			modelTotalGPUMemory: "152Gi",
+			preset:              true,
+			errContent:          "",
+			expectErrs:          false,
+			validateTuning:      false,
+		},
+		{
+			name: "Valid Resource - SKU Capacity == Model Requirement",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC12s_v3",
+				Count:        pointerToInt(1),
+			},
+			modelGPUCount:       "1",
+			modelPerGPUMemory:   "16Gi",
+			modelTotalGPUMemory: "16Gi",
+			preset:              true,
+			errContent:          "",
+			expectErrs:          false,
+			validateTuning:      false,
+		},
+		{
+			name: "Insufficient total GPU memory",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NV6",
+				Count:        pointerToInt(1),
+			},
+			modelGPUCount:       "1",
+			modelPerGPUMemory:   "0",
+			modelTotalGPUMemory: "14Gi",
+			preset:              true,
+			errContent:          "Insufficient total GPU memory",
+			expectErrs:          true,
+			validateTuning:      false,
+		},
+
+		{
+			name: "Insufficient number of GPUs",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC24ads_A100_v4",
+				Count:        pointerToInt(1),
+			},
+			modelGPUCount:       "2",
+			modelPerGPUMemory:   "15Gi",
+			modelTotalGPUMemory: "30Gi",
+			preset:              true,
+			errContent:          "Insufficient number of GPUs",
+			expectErrs:          true,
+			validateTuning:      false,
+		},
+		{
+			name: "Insufficient per GPU memory",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NV6",
+				Count:        pointerToInt(2),
+			},
+			modelGPUCount:       "1",
+			modelPerGPUMemory:   "15Gi",
+			modelTotalGPUMemory: "15Gi",
+			preset:              true,
+			errContent:          "Insufficient per GPU memory",
+			expectErrs:          true,
+			validateTuning:      false,
+		},
+
+		{
+			name: "Invalid SKU",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_invalid_sku",
+				Count:        pointerToInt(1),
+			},
+			errContent:     "Unsupported instance",
+			expectErrs:     true,
+			validateTuning: false,
+		},
+		{
+			name: "Only Template set",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NV12s_v3",
+				Count:        pointerToInt(1),
+			},
+			preset:         false,
+			errContent:     "",
+			expectErrs:     false,
+			validateTuning: false,
+		},
+		{
+			name: "N-Prefix SKU",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_Nsku",
+				Count:        pointerToInt(1),
+			},
+			errContent:     "",
+			expectErrs:     false,
+			validateTuning: false,
+		},
+
+		{
+			name: "D-Prefix SKU",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_Dsku",
+				Count:        pointerToInt(1),
+			},
+			errContent:     "",
+			expectErrs:     false,
+			validateTuning: false,
+		},
+		{
+			name: "Tuning validation with single node",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC6s_v3",
+				Count:        pointerToInt(1),
+			},
+			errContent:     "",
+			expectErrs:     false,
+			validateTuning: true,
+		},
+		{
+			name: "Tuning validation with multinode",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC6s_v3",
+				Count:        pointerToInt(2),
+			},
+			errContent:     "Tuning does not currently support multinode configurations",
+			expectErrs:     true,
+			validateTuning: true,
+		},
+		{
+			name: "Invalid Preset Name",
+			resourceSpec: &ResourceSpec{
+				InstanceType: "Standard_NC6s_v3",
+				Count:        pointerToInt(2),
+			},
+			errContent:         "",
+			preset:             true,
+			presetNameOverride: "Invalid-Preset-Name",
+			expectErrs:         false,
+		},
+	}
+
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			if tc.validateTuning {
+				tuningSpec := &TuningSpec{}
+				errs := tc.resourceSpec.validateCreateWithTuning(tuningSpec)
+				hasErrs := errs != nil
+				if hasErrs != tc.expectErrs {
+					t.Errorf("validateCreateWithTuning() errors = %v, expectErrs %v", errs, tc.expectErrs)
+				}
+
+				if hasErrs && tc.errContent != "" {
+					errMsg := errs.Error()
+					if !strings.Contains(errMsg, tc.errContent) {
+						t.Errorf("validateCreateWithTuning() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+					}
+				}
+			} else {
+				var spec InferenceSpec
+
+				if tc.preset {
+					presetName := ModelName("test-validation")
+					if tc.presetNameOverride != "" {
+						presetName = ModelName(tc.presetNameOverride)
+					}
+
+					spec = InferenceSpec{
+						Preset: &PresetSpec{
+							PresetMeta: PresetMeta{
+								Name: presetName,
+							},
+						},
+					}
+				} else {
+					spec = InferenceSpec{
+						Template: &v1.PodTemplateSpec{}, // Assuming a non-nil TemplateSpec implies it's set
+					}
+				}
+
+				gpuCountRequirement = tc.modelGPUCount
+				totalGPUMemoryRequirement = tc.modelTotalGPUMemory
+				perGPUMemoryRequirement = tc.modelPerGPUMemory
+
+				errs := tc.resourceSpec.validateCreateWithInference(&spec, false)
+				hasErrs := errs != nil
+				if hasErrs != tc.expectErrs {
+					t.Errorf("validateCreate() errors = %v, expectErrs %v", errs, tc.expectErrs)
+				}
+
+				// If there is an error and errContent is not empty, check that the error contains the expected content.
+				if hasErrs && tc.errContent != "" {
+					errMsg := errs.Error()
+					if !strings.Contains(errMsg, tc.errContent) {
+						t.Errorf("validateCreate() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestResourceSpecValidateUpdate(t *testing.T) {
+
+	tests := []struct {
+		name        string
+		newResource *ResourceSpec
+		oldResource *ResourceSpec
+		errContent  string // Content expected error to include, if any
+		expectErrs  bool
+	}{
+		{
+			name: "Immutable Count",
+			newResource: &ResourceSpec{
+				Count: pointerToInt(10),
+			},
+			oldResource: &ResourceSpec{
+				Count: pointerToInt(5),
+			},
+			errContent: "field is immutable",
+			expectErrs: true,
+		},
+		{
+			name: "Immutable InstanceType",
+			newResource: &ResourceSpec{
+				InstanceType: "new_type",
+			},
+			oldResource: &ResourceSpec{
+				InstanceType: "old_type",
+			},
+			errContent: "field is immutable",
+			expectErrs: true,
+		},
+		{
+			name: "Immutable LabelSelector",
+			newResource: &ResourceSpec{
+				LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key1": "value1"}},
+			},
+			oldResource: &ResourceSpec{
+				LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key2": "value2"}},
+			},
+			errContent: "field is immutable",
+			expectErrs: true,
+		},
+		{
+			name: "Valid Update",
+			newResource: &ResourceSpec{
+				Count:         pointerToInt(5),
+				InstanceType:  "same_type",
+				LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key": "value"}},
+			},
+			oldResource: &ResourceSpec{
+				Count:         pointerToInt(5),
+				InstanceType:  "same_type",
+				LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"key": "value"}},
+			},
+			errContent: "",
+			expectErrs: false,
+		},
+	}
+
+	// Run the tests
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			errs := tc.newResource.validateUpdate(tc.oldResource)
+			hasErrs := errs != nil
+			if hasErrs != tc.expectErrs {
+				t.Errorf("validateUpdate() errors = %v, expectErrs %v", errs, tc.expectErrs)
+			}
+
+			// If there is an error and errContent is not empty, check that the error contains the expected content.
+			if hasErrs && tc.errContent != "" {
+				errMsg := errs.Error()
+				if !strings.Contains(errMsg, tc.errContent) {
+					t.Errorf("validateUpdate() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+				}
+			}
+		})
+	}
+}
+
+func TestInferenceSpecValidateCreate(t *testing.T) {
+	RegisterValidationTestModels()
+	ctx := context.Background()
+
+	// Create fake client with default ConfigMap
+	scheme := runtime.NewScheme()
+	_ = v1.AddToScheme(scheme)
+	client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(
+		&v1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "valid-config",
+			},
+			Data: map[string]string{
+				"inference_config.yaml": "a: b",
+			},
+		},
+		&v1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "missing-key-config",
+			},
+			Data: map[string]string{
+				"other_key": "some value",
+			},
+		},
+	).Build()
+	k8sclient.SetGlobalClient(client)
+
+	tests := []struct {
+		name          string
+		inferenceSpec *InferenceSpec
+		errContent    string // Content expected error to include, if any
+		expectErrs    bool
+	}{
+		{
+			name: "Invalid Preset Name",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name: ModelName("Invalid-Preset-Name"),
+					},
+				},
+			},
+			errContent: "Unsupported inference preset name",
+			expectErrs: true,
+		},
+		{
+			name: "Only Template set",
+			inferenceSpec: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{}, // Assuming a non-nil TemplateSpec implies it's set
+			},
+			errContent: "",
+			expectErrs: false,
+		},
+		{
+			name:          "Preset and Template Unset",
+			inferenceSpec: &InferenceSpec{},
+			errContent:    "Preset or Template must be specified",
+			expectErrs:    true,
+		},
+		{
+			name: "Preset and Template Set",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name: ModelName("test-validation"),
+					},
+				},
+				Template: &v1.PodTemplateSpec{}, // Assuming a non-nil TemplateSpec implies it's set
+			},
+			errContent: "Preset and Template cannot be set at the same time",
+			expectErrs: true,
+		},
+		{
+			name: "Private Access Without Image",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name:       ModelName("test-validation"),
+						AccessMode: ModelImageAccessModePrivate,
+					},
+					PresetOptions: PresetOptions{},
+				},
+			},
+			errContent: "When AccessMode is private, an image must be provided",
+			expectErrs: true,
+		},
+		{
+			name: "Private Preset With Public AccessMode",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name: ModelName("private-test-validation"),
+					},
+					PresetOptions: PresetOptions{},
+				},
+			},
+			errContent: "This preset only supports private AccessMode, AccessMode must be private to continue",
+			expectErrs: true,
+		},
+		{
+			name: "Adapeters more than 10",
+			inferenceSpec: func() *InferenceSpec {
+				spec := &InferenceSpec{
+					Preset: &PresetSpec{
+						PresetMeta: PresetMeta{
+							Name:       ModelName("test-validation"),
+							AccessMode: ModelImageAccessModePublic,
+						},
+					},
+				}
+				for i := 1; i <= 11; i++ {
+					spec.Adapters = append(spec.Adapters, AdapterSpec{
+						Source: &DataSource{
+							Name:  fmt.Sprintf("Adapter-%d", i),
+							Image: fmt.Sprintf("fake.kaito.com/kaito-image:0.0.%d", i),
+						},
+						Strength: &ValidStrength,
+					})
+				}
+				return spec
+			}(),
+			errContent: "Number of Adapters exceeds the maximum limit, maximum of 10 allowed",
+			expectErrs: true,
+		},
+		{
+			name: "Adapeters names are duplicated",
+			inferenceSpec: func() *InferenceSpec {
+				spec := &InferenceSpec{
+					Preset: &PresetSpec{
+						PresetMeta: PresetMeta{
+							Name:       ModelName("test-validation"),
+							AccessMode: ModelImageAccessModePublic,
+						},
+					},
+				}
+				for i := 1; i <= 2; i++ {
+					spec.Adapters = append(spec.Adapters, AdapterSpec{
+						Source: &DataSource{
+							Name:  "Adapter",
+							Image: fmt.Sprintf("fake.kaito.com/kaito-image:0.0.%d", i),
+						},
+						Strength: &ValidStrength,
+					})
+				}
+				return spec
+			}(),
+			errContent: "",
+			expectErrs: true,
+		},
+		{
+			name: "Valid Preset",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name:       ModelName("test-validation"),
+						AccessMode: ModelImageAccessModePublic,
+					},
+				},
+			},
+			errContent: "Duplicate adapter source name found:",
+			expectErrs: false,
+		},
+		{
+			name: "Config specified but ConfigMap not found",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name:       ModelName("test-validation"),
+						AccessMode: ModelImageAccessModePublic,
+					},
+				},
+				Config: "nonexistent-config",
+			},
+			errContent: "ConfigMap 'nonexistent-config' specified in 'config' not found in namespace",
+			expectErrs: true,
+		},
+		{
+			name: "Config specified with valid ConfigMap",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name:       ModelName("test-validation"),
+						AccessMode: ModelImageAccessModePublic,
+					},
+				},
+				Config: "valid-config",
+			},
+			errContent: "",
+			expectErrs: false,
+		},
+		{
+			name: "ConfigMap missing required inference_config.yaml",
+			inferenceSpec: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name:       ModelName("test-validation"),
+						AccessMode: ModelImageAccessModePublic,
+					},
+				},
+				Config: "missing-key-config",
+			},
+			errContent: "missing field(s): inference_config.yaml in ConfigMap",
+			expectErrs: true,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			// If the test expects an error, setup defer function to catch the panic.
+			if tc.expectErrs {
+				defer func() {
+					if r := recover(); r != nil {
+						// Check if the recovered panic matches the expected error content.
+						if errContent, ok := r.(string); ok && strings.Contains(errContent, tc.errContent) {
+							return
+						}
+						t.Errorf("unexpected panic: %v", r)
+					}
+				}()
+			}
+			errs := tc.inferenceSpec.validateCreate(ctx, "")
+			hasErrs := errs != nil
+			if hasErrs != tc.expectErrs {
+				t.Errorf("validateCreate() errors = %v, expectErrs %v", errs, tc.expectErrs)
+			}
+
+			// If there is an error and errContent is not empty, check that the error contains the expected content.
+			if hasErrs && tc.errContent != "" {
+				errMsg := errs.Error()
+				if !strings.Contains(errMsg, tc.errContent) {
+					t.Errorf("validateCreate() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+				}
+			}
+		})
+	}
+}
+
+func TestAdapterSpecValidateCreateorUpdate(t *testing.T) {
+	RegisterValidationTestModels()
+	tests := []struct {
+		name        string
+		adapterSpec *AdapterSpec
+		errContent  string // Content expected error to include, if any
+		expectErrs  bool
+	}{
+		{
+			name: "Missing Source",
+			adapterSpec: &AdapterSpec{
+				Strength: &ValidStrength,
+			},
+			errContent: "Source",
+			expectErrs: true,
+		},
+		{
+			name: "Missing Source Name",
+			adapterSpec: &AdapterSpec{
+				Source: &DataSource{
+					Image: "fake.kaito.com/kaito-image:0.0.1",
+				},
+				Strength: &ValidStrength,
+			},
+			errContent: "Name of Adapter field must be specified",
+			expectErrs: true,
+		},
+		{
+			name: "Invalid Strength, not a number",
+			adapterSpec: &AdapterSpec{
+				Source: &DataSource{
+					Name:  "Adapter-1",
+					Image: "fake.kaito.com/kaito-image:0.0.1",
+				},
+				Strength: &InvalidStrength1,
+			},
+			errContent: "Invalid strength value for Adapter 'Adapter-1'",
+			expectErrs: true,
+		},
+		{
+			name: "Invalid Strength, larger than 1",
+			adapterSpec: &AdapterSpec{
+				Source: &DataSource{
+					Name:  "Adapter-1",
+					Image: "fake.kaito.com/kaito-image:0.0.1",
+				},
+				Strength: &InvalidStrength2,
+			},
+			errContent: "Strength value for Adapter 'Adapter-1' must be between 0 and 1",
+			expectErrs: true,
+		},
+		{
+			name: "Invalid Source Name, longer than 253",
+			adapterSpec: &AdapterSpec{
+				Source: &DataSource{
+					Name:  invalidSourceName,
+					Image: "fake.kaito.com/kaito-image:0.0.1",
+				},
+				Strength: &ValidStrength,
+			},
+			errContent: "invalid value",
+			expectErrs: true,
+		},
+		{
+			name: "Valid Adapter",
+			adapterSpec: &AdapterSpec{
+				Source: &DataSource{
+					Name:  "adapter-1",
+					Image: "fake.kaito.com/kaito-image:0.0.1",
+				},
+			},
+			errContent: "",
+			expectErrs: false,
+		},
+	}
+
+	// Run the tests
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			errs := tc.adapterSpec.validateCreateorUpdate()
+			hasErrs := errs != nil
+			if hasErrs != tc.expectErrs {
+				t.Errorf("validateUpdate() errors = %v, expectErrs %v", errs, tc.expectErrs)
+			}
+
+			// If there is an error and errContent is not empty, check that the error contains the expected content.
+			if hasErrs && tc.errContent != "" {
+				errMsg := errs.Error()
+				if !strings.Contains(errMsg, tc.errContent) {
+					t.Errorf("validateUpdate() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+				}
+			}
+		})
+	}
+}
+
+func TestInferenceSpecValidateUpdate(t *testing.T) {
+	tests := []struct {
+		name         string
+		newInference *InferenceSpec
+		oldInference *InferenceSpec
+		errContent   string // Content expected error to include, if any
+		expectErrs   bool
+	}{
+		{
+			name: "Preset Immutable",
+			newInference: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name: ModelName("new-preset"),
+					},
+				},
+			},
+			oldInference: &InferenceSpec{
+				Preset: &PresetSpec{
+					PresetMeta: PresetMeta{
+						Name: ModelName("old-preset"),
+					},
+				},
+			},
+			errContent: "field is immutable",
+			expectErrs: true,
+		},
+		{
+			name: "Template Unset",
+			newInference: &InferenceSpec{
+				Template: nil,
+			},
+			oldInference: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{},
+			},
+			errContent: "field cannot be unset/set if it was set/unset",
+			expectErrs: true,
+		},
+		{
+			name: "Template Set",
+			newInference: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{},
+			},
+			oldInference: &InferenceSpec{
+				Template: nil,
+			},
+			errContent: "field cannot be unset/set if it was set/unset",
+			expectErrs: true,
+		},
+		{
+			name: "Template Set",
+			newInference: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{},
+				Adapters: []AdapterSpec{
+					{
+						Source: &DataSource{
+							Name:  "Adapter-1",
+							Image: "fake.kaito.com/kaito-image:0.0.1",
+						},
+					},
+					{
+						Source: &DataSource{
+							Name:  "Adapter-1",
+							Image: "fake.kaito.com/kaito-image:0.0.6",
+						},
+					},
+				},
+			},
+			oldInference: &InferenceSpec{
+				Template: nil,
+			},
+			errContent: "field cannot be unset/set if it was set/unset",
+			expectErrs: true,
+		},
+		{
+			name: "Valid Update",
+			newInference: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{},
+			},
+			oldInference: &InferenceSpec{
+				Template: &v1.PodTemplateSpec{},
+			},
+			errContent: "",
+			expectErrs: false,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			errs := tc.newInference.validateUpdate(tc.oldInference)
+			hasErrs := errs != nil
+			if hasErrs != tc.expectErrs {
+				t.Errorf("validateUpdate() errors = %v, expectErrs %v", errs, tc.expectErrs)
+			}
+
+			// If there is an error and errContent is not empty, check that the error contains the expected content.
+			if hasErrs && tc.errContent != "" {
+				errMsg := errs.Error()
+				if !strings.Contains(errMsg, tc.errContent) {
+					t.Errorf("validateUpdate() error message = %v, expected to contain = %v", errMsg, tc.errContent)
+				}
+			}
+		})
+	}
+}
+
+func TestWorkspaceValidateCreate(t *testing.T) {
+	tests := []struct {
+		name      string
+		workspace *Workspace
+		wantErr   bool
+		errField  string
+	}{
+		{
+			name:      "Neither Inference nor Tuning specified",
+			workspace: &Workspace{},
+			wantErr:   true,
+			errField:  "neither",
+		},
+		{
+			name: "Both Inference and Tuning specified",
+			workspace: &Workspace{
+				Inference: &InferenceSpec{},
+				Tuning:    &TuningSpec{},
+			},
+			wantErr:  true,
+			errField: "both",
+		},
+		{
+			name: "Only Inference specified",
+			workspace: &Workspace{
+				Inference: &InferenceSpec{},
+			},
+			wantErr:  false,
+			errField: "",
+		},
+		{
+			name: "Only Tuning specified",
+			workspace: &Workspace{
+				Tuning: &TuningSpec{Input: &DataSource{}},
+			},
+			wantErr:  false,
+			errField: "",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.workspace.validateCreate()
+			if (errs != nil) != tt.wantErr {
+				t.Errorf("validateCreate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+			if errs != nil && !strings.Contains(errs.Error(), tt.errField) {
+				t.Errorf("validateCreate() expected error to contain field %s, but got %s", tt.errField, errs.Error())
+			}
+		})
+	}
+}
+
+func TestWorkspaceValidateName(t *testing.T) {
+	testWorkspace := &Workspace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "testWorkspace",
+			Namespace: "kaito",
+		},
+		Resource: ResourceSpec{
+			InstanceType: "Standard_NC12s_v3",
+			Count:        pointerToInt(1),
+		},
+		Inference: &InferenceSpec{
+			Preset: &PresetSpec{
+				PresetMeta: PresetMeta{
+					Name: ModelName("test-validation-static"),
+				},
+			},
+		},
+	}
+	RegisterValidationTestModels()
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+	tests := []struct {
+		name          string
+		workspaceName string
+		wantErr       bool
+		errField      string
+	}{
+		{
+			name:          "Valid name",
+			workspaceName: "valid-name",
+			wantErr:       false,
+		},
+		{
+			name:          "Name with invdalid characters",
+			workspaceName: "phi-3.5-mini",
+			wantErr:       true,
+			errField:      "name",
+		},
+		{
+			name:          "Name start with invalid character",
+			workspaceName: "-mini",
+			wantErr:       true,
+			errField:      "name",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			workspace := testWorkspace.DeepCopy()
+			workspace.Name = tt.workspaceName
+			errs := workspace.Validate(context.Background())
+			if (errs != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+			if errs != nil && !strings.Contains(errs.Error(), tt.errField) {
+				t.Errorf("Validate() expected error to contain field %s, but got %s", tt.errField, errs.Error())
+			}
+		})
+	}
+}
+
+func TestWorkspaceValidateUpdate(t *testing.T) {
+	tests := []struct {
+		name         string
+		oldWorkspace *Workspace
+		newWorkspace *Workspace
+		expectErrs   bool
+		errFields    []string // Fields we expect to have errors
+	}{
+		{
+			name:         "Inference toggled on",
+			oldWorkspace: &Workspace{},
+			newWorkspace: &Workspace{
+				Inference: &InferenceSpec{},
+			},
+			expectErrs: true,
+			errFields:  []string{"inference"},
+		},
+		{
+			name: "Inference toggled off",
+			oldWorkspace: &Workspace{
+				Inference: &InferenceSpec{Preset: &PresetSpec{}},
+			},
+			newWorkspace: &Workspace{},
+			expectErrs:   true,
+			errFields:    []string{"inference"},
+		},
+		{
+			name:         "Tuning toggled on",
+			oldWorkspace: &Workspace{},
+			newWorkspace: &Workspace{
+				Tuning: &TuningSpec{Input: &DataSource{}},
+			},
+			expectErrs: true,
+			errFields:  []string{"tuning"},
+		},
+		{
+			name: "Tuning toggled off",
+			oldWorkspace: &Workspace{
+				Tuning: &TuningSpec{Input: &DataSource{}},
+			},
+			newWorkspace: &Workspace{},
+			expectErrs:   true,
+			errFields:    []string{"tuning"},
+		},
+		{
+			name: "No toggling",
+			oldWorkspace: &Workspace{
+				Tuning: &TuningSpec{Input: &DataSource{}},
+			},
+			newWorkspace: &Workspace{
+				Tuning: &TuningSpec{Input: &DataSource{}},
+			},
+			expectErrs: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.newWorkspace.validateUpdate(tt.oldWorkspace)
+			hasErrs := errs != nil
+
+			if hasErrs != tt.expectErrs {
+				t.Errorf("validateUpdate() errors = %v, expectErrs %v", errs, tt.expectErrs)
+			}
+
+			if hasErrs {
+				for _, field := range tt.errFields {
+					if !strings.Contains(errs.Error(), field) {
+						t.Errorf("validateUpdate() expected errors to contain field %s, but got %s", field, errs.Error())
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestTuningSpecValidateCreate(t *testing.T) {
+	RegisterValidationTestModels()
+	// Set ReleaseNamespace Env
+	t.Setenv(consts.DefaultReleaseNamespaceEnvVar, DefaultReleaseNamespace)
+
+	// Create fake client with default ConfigMap
+	scheme := runtime.NewScheme()
+	_ = v1.AddToScheme(scheme)
+	client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(defaultConfigMapManifest(), qloraConfigMapManifest()).Build()
+	k8sclient.SetGlobalClient(client)
+	// Include client in ctx
+	ctx := context.Background()
+
+	tests := []struct {
+		name       string
+		tuningSpec *TuningSpec
+		wantErr    bool
+		errFields  []string // Fields we expect to have errors
+	}{
+		{
+			name: "All fields valid",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   false,
+			errFields: nil,
+		},
+		{
+			name: "Verify QLoRA Config",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/test:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: "secret"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodQLora,
+			},
+			wantErr:   false,
+			errFields: nil,
+		},
+		{
+			name: "Missing Input",
+			tuningSpec: &TuningSpec{
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"Input"},
+		},
+		{
+			name: "Missing Output",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"Output"},
+		},
+		{
+			name: "Missing Preset",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"Preset"},
+		},
+		{
+			name: "Invalid Preset",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("invalid-preset")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"presetName"},
+		},
+		{
+			name: "Invalid Method",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0", ImagePushSecret: ""},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: "invalid-method",
+			},
+			wantErr:   true,
+			errFields: []string{"Method"},
+		},
+		{
+			name: "Invalid Input Source Casing",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/INPUT:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/output:0.0.0", ImagePushSecret: "secret"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"Image"},
+		},
+		{
+			name: "Invalid Output Destination Casing",
+			tuningSpec: &TuningSpec{
+				Input:  &DataSource{Name: "valid-input", Image: "kaito.azurecr.io/input:0.0.0"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/OUTPUT:0.0.0", ImagePushSecret: "secret"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			wantErr:   true,
+			errFields: []string{"Image"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.tuningSpec.validateCreate(ctx, "WORKSPACE_NAMESPACE")
+			hasErrs := errs != nil
+
+			if hasErrs != tt.wantErr {
+				t.Errorf("validateCreate() errors = %v, wantErr %v", errs, tt.wantErr)
+			}
+
+			if hasErrs {
+				for _, field := range tt.errFields {
+					if !strings.Contains(errs.Error(), field) {
+						t.Errorf("validateCreate() expected errors to contain field %s, but got %s", field, errs.Error())
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestTuningSpecValidateUpdate(t *testing.T) {
+	RegisterValidationTestModels()
+	tests := []struct {
+		name       string
+		oldTuning  *TuningSpec
+		newTuning  *TuningSpec
+		expectErrs bool
+		errFields  []string // Fields we expect to have errors
+	}{
+		{
+			name: "No changes",
+			oldTuning: &TuningSpec{
+				Input:  &DataSource{Name: "input1"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			newTuning: &TuningSpec{
+				Input:  &DataSource{Name: "input1"},
+				Output: &DataDestination{Image: "kaito.azurecr.io/test:0.0.0"},
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+				Method: TuningMethodLora,
+			},
+			expectErrs: false,
+		},
+		{
+			name: "Preset changed",
+			oldTuning: &TuningSpec{
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("test-validation")}},
+			},
+			newTuning: &TuningSpec{
+				Preset: &PresetSpec{PresetMeta: PresetMeta{Name: ModelName("invalid-preset")}},
+			},
+			expectErrs: true,
+			errFields:  []string{"Preset"},
+		},
+		{
+			name: "Method changed",
+			oldTuning: &TuningSpec{
+				Method: TuningMethodLora,
+			},
+			newTuning: &TuningSpec{
+				Method: TuningMethodQLora,
+			},
+			expectErrs: true,
+			errFields:  []string{"Method"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.newTuning.validateUpdate(tt.oldTuning)
+			hasErrs := errs != nil
+
+			if hasErrs != tt.expectErrs {
+				t.Errorf("validateUpdate() errors = %v, expectErrs %v", errs, tt.expectErrs)
+			}
+
+			if hasErrs {
+				for _, field := range tt.errFields {
+					if !strings.Contains(errs.Error(), field) {
+						t.Errorf("validateUpdate() expected errors to contain field %s, but got %s", field, errs.Error())
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestDataSourceValidateCreate(t *testing.T) {
+	tests := []struct {
+		name       string
+		dataSource *DataSource
+		wantErr    bool
+		errField   string // The field we expect to have an error on
+	}{
+		{
+			name: "URLs specified only",
+			dataSource: &DataSource{
+				URLs: []string{"http://example.com/data"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Volume specified only",
+			dataSource: &DataSource{
+				Image: "kaito.azurecr.io/test:0.0.0",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Image specified only",
+			dataSource: &DataSource{
+				Image: "aimodels.azurecr.io/data-image:latest",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Image without URL Specified",
+			dataSource: &DataSource{
+				Image:            "data-image:latest",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Image without Tag Specified",
+			dataSource: &DataSource{
+				Image:            "aimodels.azurecr.io/data-image",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Invalid image",
+			dataSource: &DataSource{
+				Image:            "!ValidImage",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr:  true,
+			errField: "invalid reference format",
+		},
+		{
+			name:       "None specified",
+			dataSource: &DataSource{},
+			wantErr:    true,
+			errField:   "Exactly one of URLs, Volume, or Image must be specified",
+		},
+		// {
+		// 	name: "URLs and Volume specified",
+		// 	dataSource: &DataSource{
+		// 		URLs:   []string{"http://example.com/data"},
+		// 		Volume: &v1.VolumeSource{},
+		// 	},
+		// 	wantErr:  true,
+		// 	errField: "Exactly one of URLs, Volume, or Image must be specified",
+		// },
+		{
+			name: "All fields specified",
+			dataSource: &DataSource{
+				URLs:  []string{"http://example.com/data"},
+				Image: "aimodels.azurecr.io/data-image:latest",
+			},
+			wantErr:  true,
+			errField: "Exactly one of URLs, Volume, or Image must be specified",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.dataSource.validateCreate()
+			hasErrs := errs != nil
+
+			if hasErrs != tt.wantErr {
+				t.Errorf("validateCreate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+
+			if hasErrs && tt.errField != "" && !strings.Contains(errs.Error(), tt.errField) {
+				t.Errorf("validateCreate() expected error to contain %s, but got %s", tt.errField, errs.Error())
+			}
+		})
+	}
+}
+
+func TestDataSourceValidateUpdate(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldSource *DataSource
+		newSource *DataSource
+		wantErr   bool
+		errFields []string // Fields we expect to have errors
+	}{
+		{
+			name: "No changes",
+			oldSource: &DataSource{
+				URLs: []string{"http://example.com/data1", "http://example.com/data2"},
+				// Volume:           &v1.VolumeSource{},
+				Image:            "data-image:latest",
+				ImagePullSecrets: []string{"secret1", "secret2"},
+			},
+			newSource: &DataSource{
+				URLs: []string{"http://example.com/data2", "http://example.com/data1"}, // Note the different order, should not matter
+				// Volume:           &v1.VolumeSource{},
+				Image:            "data-image:latest",
+				ImagePullSecrets: []string{"secret2", "secret1"}, // Note the different order, should not matter
+			},
+			wantErr: false,
+		},
+		{
+			name: "Name changed",
+			oldSource: &DataSource{
+				Name: "original-dataset",
+			},
+			newSource: &DataSource{
+				Name: "new-dataset",
+			},
+			wantErr:   true,
+			errFields: []string{"Name"},
+		},
+		{
+			name:      "Invalid image",
+			oldSource: &DataSource{},
+			newSource: &DataSource{
+				Image:            "!ValidImage",
+				ImagePullSecrets: []string{"imagePushSecret"},
+			},
+			wantErr:   true,
+			errFields: []string{"invalid reference format"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.newSource.validateUpdate(tt.oldSource, true)
+			hasErrs := errs != nil
+
+			if hasErrs != tt.wantErr {
+				t.Errorf("validateUpdate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+
+			if hasErrs {
+				for _, field := range tt.errFields {
+					if !strings.Contains(errs.Error(), field) {
+						t.Errorf("validateUpdate() expected errors to contain field %s, but got %s", field, errs.Error())
+					}
+				}
+			}
+		})
+	}
+}
+
+func TestDataDestinationValidateCreate(t *testing.T) {
+	tests := []struct {
+		name            string
+		dataDestination *DataDestination
+		wantErr         bool
+		errField        string // The field we expect to have an error on
+	}{
+		{
+			name:            "No fields specified",
+			dataDestination: &DataDestination{},
+			wantErr:         true,
+			errField:        "At least one of Volume or Image must be specified",
+		},
+		// {
+		// 	name: "Volume specified only",
+		// 	dataDestination: &DataDestination{
+		// 		Volume: &v1.VolumeSource{},
+		// 	},
+		// 	wantErr: false,
+		// },
+		{
+			name: "Image specified only",
+			dataDestination: &DataDestination{
+				Image:           "aimodels.azurecr.io/data-image:latest",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Image without URL Specified",
+			dataDestination: &DataDestination{
+				Image:           "data-image:latest",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Image without Tag Specified",
+			dataDestination: &DataDestination{
+				Image:           "aimodels.azurecr.io/data-image",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr: false,
+		},
+		{
+			name: "Invalid image",
+			dataDestination: &DataDestination{
+				Image:           "!ValidImage",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr:  true,
+			errField: "invalid reference format",
+		},
+		// {
+		// 	name: "Both fields specified",
+		// 	dataDestination: &DataDestination{
+		// 		Volume:          &v1.VolumeSource{},
+		// 		Image:           "aimodels.azurecr.io/data-image:latest",
+		// 		ImagePushSecret: "imagePushSecret",
+		// 	},
+		// 	wantErr: false,
+		// },
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.dataDestination.validateCreate()
+			hasErrs := errs != nil
+
+			if hasErrs != tt.wantErr {
+				t.Errorf("validateCreate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+
+			if hasErrs && tt.errField != "" && !strings.Contains(errs.Error(), tt.errField) {
+				t.Errorf("validateCreate() expected error to contain %s, but got %s", tt.errField, errs.Error())
+			}
+		})
+	}
+}
+
+func TestDataDestinationValidateUpdate(t *testing.T) {
+	tests := []struct {
+		name      string
+		oldDest   *DataDestination
+		newDest   *DataDestination
+		wantErr   bool
+		errFields []string // Fields we expect to have errors
+	}{
+		{
+			name: "No changes",
+			oldDest: &DataDestination{
+				// Volume:          &v1.VolumeSource{},
+				Image:           "old-image:latest",
+				ImagePushSecret: "old-secret",
+			},
+			newDest: &DataDestination{
+				// Volume:          &v1.VolumeSource{},
+				Image:           "old-image:latest",
+				ImagePushSecret: "old-secret",
+			},
+			wantErr: false,
+		},
+		{
+			name:    "Invalid image",
+			oldDest: &DataDestination{},
+			newDest: &DataDestination{
+				Image:           "!ValidImage",
+				ImagePushSecret: "imagePushSecret",
+			},
+			wantErr:   true,
+			errFields: []string{"invalid reference format"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			errs := tt.newDest.validateUpdate()
+			hasErrs := errs != nil
+
+			if hasErrs != tt.wantErr {
+				t.Errorf("validateUpdate() error = %v, wantErr %v", errs, tt.wantErr)
+			}
+
+			if hasErrs {
+				for _, field := range tt.errFields {
+					if !strings.Contains(errs.Error(), field) {
+						t.Errorf("validateUpdate() expected errors to contain field %s, but got %s", field, errs.Error())
+					}
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/featuregates/featuregates_test.go b/pkg/featuregates/featuregates_test.go
index 0fde9ac..3b74999 100644
--- a/pkg/featuregates/featuregates_test.go
+++ b/pkg/featuregates/featuregates_test.go
@@ -13,19 +13,22 @@ func TestParseFeatureGates(t *testing.T) {
 		name          string
 		featureGates  string
 		expectedError bool
-		expectedValue string
+		targetFeature string
+		expectedValue bool
 	}{
 		{
-			name:          "WithValidEnableFeatureGates",
+			name:          "WithValidEnableFeatureGates-vLLM",
 			featureGates:  "vLLM=true",
 			expectedError: false,
-			expectedValue: "true",
+			targetFeature: "vLLM",
+			expectedValue: true,
 		},
 		{
-			name:          "WithDuplicateFeatureGates",
+			name:          "WithDuplicateFeatureGates-vLLM",
 			featureGates:  "vLLM=false,vLLM=true",
 			expectedError: false,
-			expectedValue: "true", // Apply the last value.
+			targetFeature: "vLLM",
+			expectedValue: true, // Apply the last value.
 		},
 		{
 			name:          "WithInvalidFeatureGates",
@@ -38,10 +41,25 @@ func TestParseFeatureGates(t *testing.T) {
 			expectedError: true,
 		},
 		{
-			name:          "WithValidDisableFeatureGates",
+			name:          "WithValidDisableFeatureGates-vLLM",
 			featureGates:  "vLLM=false",
 			expectedError: false,
-			expectedValue: "false",
+			targetFeature: "vLLM",
+			expectedValue: false,
+		},
+		{
+			name:          "WithValidEnableFeatureGates-ensureNodeClass",
+			featureGates:  "ensureNodeClass=true",
+			expectedError: false,
+			targetFeature: "ensureNodeClass",
+			expectedValue: true,
+		},
+		{
+			name:          "WithValidDisableFeatureGates-ensureNodeClass",
+			featureGates:  "ensureNodeClass=false",
+			expectedError: false,
+			targetFeature: "ensureNodeClass",
+			expectedValue: false,
 		},
 		{
 			name:          "WithEmptyFeatureGates",
@@ -57,6 +75,9 @@ func TestParseFeatureGates(t *testing.T) {
 				assert.Check(t, err != nil, "expected error but got nil")
 			} else {
 				assert.NilError(t, err)
+				if tt.targetFeature != "" && FeatureGates[tt.targetFeature] != tt.expectedValue {
+					t.Errorf("feature gate test %s fails", tt.name)
+				}
 			}
 		})
 	}
diff --git a/pkg/ragengine/controllers/preset-rag_test.go b/pkg/ragengine/controllers/preset-rag_test.go
index 0e42d79..7ba31a0 100644
--- a/pkg/ragengine/controllers/preset-rag_test.go
+++ b/pkg/ragengine/controllers/preset-rag_test.go
@@ -31,7 +31,7 @@ func TestCreatePresetRAG(t *testing.T) {
 				c.On("Create", mock.IsType(context.TODO()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
 			},
 			expectedCmd:   "/bin/sh -c python3 main.py",
-			expectedImage: "mcr.microsoft.com/aks/kaito/kaito-rag-service:0.0.1",
+			expectedImage: "aimodelsregistrytest.azurecr.io/kaito-rag-service:0.3.2", //TODO: Change to the mcr image when release
 		},
 	}
 
diff --git a/pkg/utils/nodeclaim/nodeclaim_test.go b/pkg/utils/nodeclaim/nodeclaim_test.go
index 9b6f120..cd9990d 100644
--- a/pkg/utils/nodeclaim/nodeclaim_test.go
+++ b/pkg/utils/nodeclaim/nodeclaim_test.go
@@ -19,7 +19,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
@@ -186,8 +186,8 @@ func TestGenerateNodeClaimManifest(t *testing.T) {
 
 		assert.Check(t, nodeClaim != nil, "NodeClaim must not be nil")
 		assert.Equal(t, nodeClaim.Namespace, mockWorkspace.Namespace, "NodeClaim must have same namespace as workspace")
-		assert.Equal(t, nodeClaim.Labels[kaitov1alpha1.LabelWorkspaceName], mockWorkspace.Name, "label must have same workspace name as workspace")
-		assert.Equal(t, nodeClaim.Labels[kaitov1alpha1.LabelWorkspaceNamespace], mockWorkspace.Namespace, "label must have same workspace namespace as workspace")
+		assert.Equal(t, nodeClaim.Labels[kaitov1beta1.LabelWorkspaceName], mockWorkspace.Name, "label must have same workspace name as workspace")
+		assert.Equal(t, nodeClaim.Labels[kaitov1beta1.LabelWorkspaceNamespace], mockWorkspace.Namespace, "label must have same workspace namespace as workspace")
 		assert.Equal(t, nodeClaim.Labels[consts.LabelNodePool], consts.KaitoNodePoolName, "label must have same labels as workspace label selector")
 		assert.Equal(t, nodeClaim.Annotations[karpenterv1.DoNotDisruptAnnotationKey], "true", "label must have do not disrupt annotation")
 		assert.Equal(t, len(nodeClaim.Spec.Requirements), 4, " NodeClaim must have 4 NodeSelector Requirements")
@@ -205,8 +205,8 @@ func TestGenerateNodeClaimManifest(t *testing.T) {
 
 		assert.Check(t, nodeClaim != nil, "NodeClaim must not be nil")
 		assert.Equal(t, nodeClaim.Namespace, mockWorkspace.Namespace, "NodeClaim must have same namespace as workspace")
-		assert.Equal(t, nodeClaim.Labels[kaitov1alpha1.LabelWorkspaceName], mockWorkspace.Name, "label must have same workspace name as workspace")
-		assert.Equal(t, nodeClaim.Labels[kaitov1alpha1.LabelWorkspaceNamespace], mockWorkspace.Namespace, "label must have same workspace namespace as workspace")
+		assert.Equal(t, nodeClaim.Labels[kaitov1beta1.LabelWorkspaceName], mockWorkspace.Name, "label must have same workspace name as workspace")
+		assert.Equal(t, nodeClaim.Labels[kaitov1beta1.LabelWorkspaceNamespace], mockWorkspace.Namespace, "label must have same workspace namespace as workspace")
 		assert.Equal(t, nodeClaim.Labels[consts.LabelNodePool], consts.KaitoNodePoolName, "label must have same labels as workspace label selector")
 		assert.Equal(t, nodeClaim.Annotations[karpenterv1.DoNotDisruptAnnotationKey], "true", "label must have do not disrupt annotation")
 		assert.Equal(t, len(nodeClaim.Spec.Requirements), 4, " NodeClaim must have 4 NodeSelector Requirements")
diff --git a/pkg/utils/test/testUtils.go b/pkg/utils/test/testUtils.go
index f0f9781..7f9a9f9 100644
--- a/pkg/utils/test/testUtils.go
+++ b/pkg/utils/test/testUtils.go
@@ -14,6 +14,7 @@ import (
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 )
 
@@ -26,15 +27,15 @@ const (
 var ValidStrength string = "0.5"
 
 var (
-	MockWorkspaceDistributedModel = &v1alpha1.Workspace{
+	MockWorkspaceDistributedModel = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
 			Annotations: map[string]string{
-				v1alpha1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
+				v1beta1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -43,20 +44,20 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-distributed-model",
 				},
 			},
 		},
 	}
-	MockWorkspaceWithPreferredNodes = &v1alpha1.Workspace{
+	MockWorkspaceWithPreferredNodes = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -66,20 +67,20 @@ var (
 			},
 			PreferredNodes: []string{"node-p1", "node-p2"},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-distributed-model",
 				},
 			},
 		},
 	}
-	MockWorkspaceCustomModel = &v1alpha1.Workspace{
+	MockWorkspaceCustomModel = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testCustomWorkspace",
 			Namespace: "kaito",
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -88,7 +89,7 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
+		Inference: &v1beta1.InferenceSpec{
 			Template: &corev1.PodTemplateSpec{
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
@@ -148,15 +149,15 @@ var (
 )
 
 var (
-	MockWorkspaceWithPreset = &v1alpha1.Workspace{
+	MockWorkspaceWithPreset = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
 			Annotations: map[string]string{
-				v1alpha1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
+				v1beta1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -165,20 +166,20 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
 		},
 	}
-	MockWorkspaceWithPresetVLLM = &v1alpha1.Workspace{
+	MockWorkspaceWithPresetVLLM = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -187,9 +188,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
@@ -282,7 +283,7 @@ var (
 var MockRAGEngineWithPresetHash = "14485768c1b67a529a71e3c87d9f2e6c1ed747534dea07e268e93475a5e21e"
 
 var (
-	MockWorkspaceWithDeleteOldCR = v1alpha1.Workspace{
+	MockWorkspaceWithDeleteOldCR = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
@@ -291,7 +292,7 @@ var (
 				"workspace.kaito.io/revision": "1",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -300,9 +301,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model-DeleteOldCR", // presetMeta name is changed
 				},
 			},
@@ -340,7 +341,7 @@ var (
 )
 
 var (
-	MockWorkspaceFailToCreateCR = v1alpha1.Workspace{
+	MockWorkspaceFailToCreateCR = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace-failedtocreateCR",
 			Namespace: "kaito",
@@ -348,7 +349,7 @@ var (
 				"workspace.kaito.io/revision": "1",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -357,9 +358,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
@@ -390,7 +391,7 @@ var (
 )
 
 var (
-	MockWorkspaceSuccessful = v1alpha1.Workspace{
+	MockWorkspaceSuccessful = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace-successful",
 			Namespace: "kaito",
@@ -398,7 +399,7 @@ var (
 				"workspace.kaito.io/revision": "0",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -407,9 +408,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
@@ -440,7 +441,7 @@ var (
 )
 
 var (
-	MockWorkspaceWithComputeHash = v1alpha1.Workspace{
+	MockWorkspaceWithComputeHash = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
@@ -449,7 +450,7 @@ var (
 				"workspace.kaito.io/revision": "1",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -458,9 +459,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
@@ -492,7 +493,7 @@ var (
 )
 
 var (
-	MockWorkspaceUpdateCR = v1alpha1.Workspace{
+	MockWorkspaceUpdateCR = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
@@ -501,7 +502,7 @@ var (
 				"workspace.kaito.io/revision": "1",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -510,9 +511,9 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
@@ -521,7 +522,7 @@ var (
 )
 
 var (
-	MockWorkspaceWithUpdatedDeployment = v1alpha1.Workspace{
+	MockWorkspaceWithUpdatedDeployment = v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
@@ -530,7 +531,7 @@ var (
 				"workspace.kaito.io/revision": "1",
 			},
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -539,15 +540,15 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
-			Preset: &v1alpha1.PresetSpec{
-				PresetMeta: v1alpha1.PresetMeta{
+		Inference: &v1beta1.InferenceSpec{
+			Preset: &v1beta1.PresetSpec{
+				PresetMeta: v1beta1.PresetMeta{
 					Name: "test-model",
 				},
 			},
-			Adapters: []v1alpha1.AdapterSpec{
+			Adapters: []v1beta1.AdapterSpec{
 				{
-					Source: &v1alpha1.DataSource{
+					Source: &v1beta1.DataSource{
 						Name:  "Adapter-1",
 						Image: "fake.kaito.com/kaito-image:0.0.1",
 					},
@@ -587,7 +588,7 @@ var (
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        "testWorkspace",
 			Namespace:   "kaito",
-			Annotations: map[string]string{v1alpha1.WorkspaceRevisionAnnotation: "1"},
+			Annotations: map[string]string{v1beta1.WorkspaceRevisionAnnotation: "1"},
 		},
 		Spec: appsv1.DeploymentSpec{
 			Replicas: &numRep,
@@ -728,12 +729,12 @@ var MockRAGDeploymentUpdated = appsv1.Deployment{
 }
 
 var (
-	MockWorkspaceWithInferenceTemplate = &v1alpha1.Workspace{
+	MockWorkspaceWithInferenceTemplate = &v1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "testWorkspace",
 			Namespace: "kaito",
 		},
-		Resource: v1alpha1.ResourceSpec{
+		Resource: v1beta1.ResourceSpec{
 			Count:        &gpuNodeCount,
 			InstanceType: "Standard_NC12s_v3",
 			LabelSelector: &metav1.LabelSelector{
@@ -742,7 +743,7 @@ var (
 				},
 			},
 		},
-		Inference: &v1alpha1.InferenceSpec{
+		Inference: &v1beta1.InferenceSpec{
 			Template: &corev1.PodTemplateSpec{},
 		},
 	}
@@ -841,57 +842,57 @@ var (
 )
 
 var (
-	Adapters1 = []v1alpha1.AdapterSpec{
+	Adapters1 = []v1beta1.AdapterSpec{
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-1",
 				Image: "fake.kaito.com/kaito-image:0.0.1",
 			},
 			Strength: &ValidStrength,
 		},
 	}
-	Adapters2 = []v1alpha1.AdapterSpec{
+	Adapters2 = []v1beta1.AdapterSpec{
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-1",
 				Image: "fake.kaito.com/kaito-image:0.0.1",
 			},
 			Strength: &ValidStrength,
 		},
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-2",
 				Image: "fake.kaito.com/kaito-image:0.0.2",
 			},
 			Strength: &ValidStrength,
 		},
 	}
-	Adapters3 = []v1alpha1.AdapterSpec{
+	Adapters3 = []v1beta1.AdapterSpec{
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-2",
 				Image: "fake.kaito.com/kaito-image:0.0.2",
 			},
 			Strength: &ValidStrength,
 		},
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-1",
 				Image: "fake.kaito.com/kaito-image:0.0.1",
 			},
 			Strength: &ValidStrength,
 		},
 	}
-	Adapters4 = []v1alpha1.AdapterSpec{
+	Adapters4 = []v1beta1.AdapterSpec{
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-1",
 				Image: "fake.kaito.com/kaito-image:0.0.1",
 			},
 			Strength: &ValidStrength,
 		},
 		{
-			Source: &v1alpha1.DataSource{
+			Source: &v1beta1.DataSource{
 				Name:  "Adapter-3",
 				Image: "fake.kaito.com/kaito-image:0.0.3",
 			},
diff --git a/pkg/workspace/controllers/workspace_controller_test.go b/pkg/workspace/controllers/workspace_controller_test.go
index 5a6976e..a09a83f 100644
--- a/pkg/workspace/controllers/workspace_controller_test.go
+++ b/pkg/workspace/controllers/workspace_controller_test.go
@@ -27,7 +27,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/test"
@@ -318,7 +318,7 @@ func TestCreateAndValidateNodeClaimNode(t *testing.T) {
 		callMocks           func(c *test.MockClient)
 		cloudProvider       string
 		nodeClaimConditions []status.Condition
-		workspace           v1alpha1.Workspace
+		workspace           v1beta1.Workspace
 		expectedError       error
 	}{
 		"Node is not created because nodeClaim creation fails": {
@@ -327,8 +327,8 @@ func TestCreateAndValidateNodeClaimNode(t *testing.T) {
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&azurev1alpha2.AKSNodeClass{}), mock.Anything).Return(nil)
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&karpenterv1.NodeClaim{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			cloudProvider: consts.AzureCloudName,
 			nodeClaimConditions: []status.Condition{
@@ -401,7 +401,7 @@ func TestEnsureService(t *testing.T) {
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
 		expectedError error
-		workspace     *v1alpha1.Workspace
+		workspace     *v1beta1.Workspace
 	}{
 		"Existing service is found for workspace": {
 			callMocks: func(c *test.MockClient) {
@@ -462,15 +462,15 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 	test.RegisterTestModel()
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
-		workspace     v1alpha1.Workspace
+		workspace     v1beta1.Workspace
 		expectedError error
 	}{
 		"Fail to get inference because associated workload with workspace cannot be retrieved": {
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&appsv1.StatefulSet{}), mock.Anything).Return(errors.New("Failed to get resource"))
 
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceDistributedModel,
 			expectedError: errors.New("Failed to get resource"),
@@ -490,8 +490,8 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&corev1.Service{}), mock.Anything).Return(nil)
 
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceWithPreset,
 			expectedError: nil,
@@ -508,8 +508,8 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 					c.CreateOrUpdateObjectInMap(depObj)
 				})
 
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceDistributedModel,
 			expectedError: nil,
@@ -527,8 +527,8 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 
 				c.On("Update", mock.IsType(context.Background()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
 
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceWithPreset,
 			expectedError: nil,
@@ -561,14 +561,14 @@ func TestApplyInferenceWithPreset(t *testing.T) {
 func TestApplyInferenceWithTemplate(t *testing.T) {
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
-		workspace     v1alpha1.Workspace
+		workspace     v1beta1.Workspace
 		expectedError error
 	}{
 		"Fail to apply inference from workspace template": {
 			callMocks: func(c *test.MockClient) {
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(errors.New("Failed to create deployment"))
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceWithInferenceTemplate,
 			expectedError: errors.New("Failed to create deployment"),
@@ -577,8 +577,8 @@ func TestApplyInferenceWithTemplate(t *testing.T) {
 			callMocks: func(c *test.MockClient) {
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
 				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 			},
 			workspace:     *test.MockWorkspaceWithInferenceTemplate,
 			expectedError: nil,
@@ -627,7 +627,7 @@ func TestGetAllQualifiedNodes(t *testing.T) {
 
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
-		workspace     *v1alpha1.Workspace
+		workspace     *v1beta1.Workspace
 		expectedError error
 		expectedNodes []string
 	}{
@@ -766,7 +766,7 @@ func TestApplyWorkspaceResource(t *testing.T) {
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
 		expectedError error
-		workspace     v1alpha1.Workspace
+		workspace     v1beta1.Workspace
 	}{
 		"Fail to apply workspace because associated nodeClaim cannot be retrieved": {
 			callMocks: func(c *test.MockClient) {
@@ -814,8 +814,8 @@ func TestApplyWorkspaceResource(t *testing.T) {
 
 				c.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 
 			},
 			workspace:     *test.MockWorkspaceDistributedModel,
@@ -860,7 +860,7 @@ func TestApplyWorkspaceResource(t *testing.T) {
 func TestUpdateControllerRevision1(t *testing.T) {
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
-		workspace     v1alpha1.Workspace
+		workspace     v1beta1.Workspace
 		expectedError error
 		verifyCalls   func(c *test.MockClient)
 	}{
@@ -880,7 +880,7 @@ func TestUpdateControllerRevision1(t *testing.T) {
 						}
 					}).
 					Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Return(nil)
 			},
 			workspace:     test.MockWorkspaceWithComputeHash,
@@ -900,7 +900,7 @@ func TestUpdateControllerRevision1(t *testing.T) {
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).Return(errors.New("failed to create ControllerRevision"))
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).
 					Return(apierrors.NewNotFound(appsv1.Resource("ControllerRevision"), test.MockWorkspaceFailToCreateCR.Name))
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Return(nil)
 			},
 			workspace:     test.MockWorkspaceFailToCreateCR,
@@ -920,7 +920,7 @@ func TestUpdateControllerRevision1(t *testing.T) {
 				c.On("Create", mock.IsType(context.Background()), mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).Return(nil)
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).
 					Return(apierrors.NewNotFound(appsv1.Resource("ControllerRevision"), test.MockWorkspaceFailToCreateCR.Name))
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Return(nil)
 			},
 			workspace:     test.MockWorkspaceSuccessful,
@@ -961,7 +961,7 @@ func TestUpdateControllerRevision1(t *testing.T) {
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).
 					Return(apierrors.NewNotFound(appsv1.Resource("ControllerRevision"), test.MockWorkspaceFailToCreateCR.Name))
 				c.On("Delete", mock.IsType(context.Background()), mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Return(nil)
 			},
 			workspace:     test.MockWorkspaceWithDeleteOldCR,
@@ -1002,7 +1002,7 @@ func TestUpdateControllerRevision1(t *testing.T) {
 				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).
 					Return(apierrors.NewNotFound(appsv1.Resource("ControllerRevision"), test.MockWorkspaceFailToCreateCR.Name))
 				c.On("Delete", mock.IsType(context.Background()), mock.IsType(&appsv1.ControllerRevision{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).
 					Return(fmt.Errorf("failed to update Workspace annotations"))
 			},
 			workspace:     test.MockWorkspaceUpdateCR,
diff --git a/pkg/workspace/controllers/workspace_gc_finalizer_test.go b/pkg/workspace/controllers/workspace_gc_finalizer_test.go
index fabe843..9ed910c 100644
--- a/pkg/workspace/controllers/workspace_gc_finalizer_test.go
+++ b/pkg/workspace/controllers/workspace_gc_finalizer_test.go
@@ -10,7 +10,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
@@ -22,18 +22,18 @@ func TestGarbageCollectWorkspace(t *testing.T) {
 	}{
 		"Fails to delete workspace because associated nodeClaims cannot be retrieved": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(errors.New("failed to list nodeClaims"))
 			},
 			expectedError: errors.New("failed to list nodeClaims"),
 		},
 		"Fails to delete workspace because associated nodeClaims cannot be deleted": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 
 				nodeClaimList := test.MockNodeClaimList
 				relevantMap := c.CreateMapWithType(nodeClaimList)
@@ -52,9 +52,9 @@ func TestGarbageCollectWorkspace(t *testing.T) {
 		},
 		"Delete workspace with associated nodeClaim objects because finalizer cannot be removed from workspace": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(errors.New("failed to update workspace"))
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(errors.New("failed to update workspace"))
 
 				nodeClaimList := test.MockNodeClaimList
 				relevantMap := c.CreateMapWithType(nodeClaimList)
@@ -72,9 +72,9 @@ func TestGarbageCollectWorkspace(t *testing.T) {
 		},
 		"Successfully deletes workspace with associated nodeClaim objects and removes finalizer associated with workspace": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 
 				c.On("List", mock.IsType(context.Background()), mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(nil)
 
@@ -94,9 +94,9 @@ func TestGarbageCollectWorkspace(t *testing.T) {
 		},
 		"Delete workspace with nodeClaim objects because finalizer cannot be removed from workspace": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(errors.New("failed to update workspace"))
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(errors.New("failed to update workspace"))
 
 				nodeClaimList := test.MockNodeClaimList
 				relevantNodeClaimsMap := c.CreateMapWithType(nodeClaimList)
@@ -114,9 +114,9 @@ func TestGarbageCollectWorkspace(t *testing.T) {
 		},
 		"Successfully deletes workspace with machine and nodeClaim objects and removes finalizer associated with workspace": {
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
-				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1alpha1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
+				c.On("Update", mock.IsType(context.Background()), mock.IsType(&v1beta1.Workspace{}), mock.Anything).Return(nil)
 
 				nodeClaimList := test.MockNodeClaimList
 				relevantNodeClaimsMap := c.CreateMapWithType(nodeClaimList)
diff --git a/pkg/workspace/controllers/workspace_status_test.go b/pkg/workspace/controllers/workspace_status_test.go
index 1ec551f..b3bbb3e 100644
--- a/pkg/workspace/controllers/workspace_status_test.go
+++ b/pkg/workspace/controllers/workspace_status_test.go
@@ -15,7 +15,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
@@ -36,8 +36,8 @@ func TestUpdateWorkspaceStatus(t *testing.T) {
 		}
 		workerNodes := []string{"node1", "node2"}
 
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
-		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
 
 		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
 		assert.Nil(t, err)
@@ -59,7 +59,7 @@ func TestUpdateWorkspaceStatus(t *testing.T) {
 		}
 		workerNodes := []string{"node1", "node2"}
 
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(errors.New("Get operation failed"))
+		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(errors.New("Get operation failed"))
 
 		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
 		assert.NotNil(t, err)
@@ -81,7 +81,7 @@ func TestUpdateWorkspaceStatus(t *testing.T) {
 		}
 		workerNodes := []string{"node1", "node2"}
 
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{}, "workspace"))
+		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{}, "workspace"))
 
 		err := reconciler.updateWorkspaceStatus(ctx, &client.ObjectKey{Name: workspace.Name, Namespace: workspace.Namespace}, &condition, workerNodes)
 		assert.Nil(t, err)
@@ -97,7 +97,7 @@ func TestUpdateStatusConditionIfNotMatch(t *testing.T) {
 		}
 		ctx := context.Background()
 		workspace := test.MockWorkspaceDistributedModel
-		conditionType := kaitov1alpha1.ConditionType("TestCondition")
+		conditionType := kaitov1beta1.ConditionType("TestCondition")
 		conditionStatus := metav1.ConditionStatus("True")
 		conditionReason := "TestReason"
 		conditionMessage := "TestMessage"
@@ -123,7 +123,7 @@ func TestUpdateStatusConditionIfNotMatch(t *testing.T) {
 		}
 		ctx := context.Background()
 		workspace := test.MockWorkspaceDistributedModel
-		conditionType := kaitov1alpha1.ConditionType("TestCondition")
+		conditionType := kaitov1beta1.ConditionType("TestCondition")
 		conditionStatus := metav1.ConditionStatus("True")
 		conditionReason := "TestReason"
 		conditionMessage := "TestMessage"
@@ -136,8 +136,8 @@ func TestUpdateStatusConditionIfNotMatch(t *testing.T) {
 				Message: "DifferentMessage",
 			},
 		}
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
-		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
 
 		err := reconciler.updateStatusConditionIfNotMatch(ctx, workspace, conditionType, conditionStatus, conditionReason, conditionMessage)
 		assert.Nil(t, err)
@@ -151,7 +151,7 @@ func TestUpdateStatusConditionIfNotMatch(t *testing.T) {
 		}
 		ctx := context.Background()
 		workspace := test.MockWorkspaceDistributedModel
-		conditionType := kaitov1alpha1.ConditionType("TestCondition")
+		conditionType := kaitov1beta1.ConditionType("TestCondition")
 		conditionStatus := metav1.ConditionStatus("True")
 		conditionReason := "TestReason"
 		conditionMessage := "TestMessage"
@@ -164,8 +164,8 @@ func TestUpdateStatusConditionIfNotMatch(t *testing.T) {
 				Message: conditionMessage,
 			},
 		}
-		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
-		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1alpha1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.On("Get", mock.IsType(context.Background()), mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
+		mockClient.StatusMock.On("Update", mock.IsType(context.Background()), mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil)
 
 		err := reconciler.updateStatusConditionIfNotMatch(ctx, workspace, conditionType, conditionStatus, conditionReason, conditionMessage)
 		assert.Nil(t, err)
diff --git a/pkg/workspace/image/puller_test.go b/pkg/workspace/image/puller_test.go
new file mode 100644
index 0000000..8cb1769
--- /dev/null
+++ b/pkg/workspace/image/puller_test.go
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package image
+
+import (
+	"text/template"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/apimachinery/pkg/util/rand"
+)
+
+var _ = Describe("Puller", func() {
+	Context("embed", func() {
+		It("initializes the puller text data", func() {
+			Expect(pullerSHTextData).NotTo(BeEmpty())
+		})
+
+		It("uses sh as the interpreter", func() {
+			Expect(pullerSHTextData).To(HavePrefix("#!/bin/sh\n"))
+		})
+	})
+
+	Context("init", func() {
+		It("instantiates the puller template", func() {
+			Expect(pusherSHTemplate).NotTo(BeNil())
+		})
+	})
+
+	Context("renderPullerSH", func() {
+		It("renders the puller script", func() {
+			var (
+				imgRef = "docker.io/library/scratch:" + rand.String(8)
+				volDir = "/tmp/" + rand.String(8)
+			)
+
+			ret := renderPullerSH(imgRef, volDir)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='` + imgRef + `'` + "\n"))
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${VOL_DIR}" ] || VOL_DIR='` + volDir + `'` + "\n"))
+		})
+
+		It("normalizes the image reference", func() {
+			var (
+				imgRef = "scratch-" + rand.String(8)
+				volDir = "/tmp/" + rand.String(8)
+			)
+
+			ret := renderPullerSH(imgRef, volDir)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='docker.io/library/` + imgRef + `:latest'` + "\n"))
+		})
+
+		It("falls back to original image reference when unable to normalize", func() {
+			var (
+				imgRef = "!" + rand.String(8)
+				volDir = "/tmp/" + rand.String(8)
+			)
+
+			ret := renderPullerSH(imgRef, volDir)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='` + imgRef + `'` + "\n"))
+		})
+
+		It("panics when the template is bad", func() {
+			var (
+				imgRef = "docker.io/library/scratch:" + rand.String(8)
+				volDir = "/tmp/" + rand.String(8)
+			)
+
+			pullerSHTemplate0 := pullerSHTemplate
+			defer func() {
+				pullerSHTemplate = pullerSHTemplate0
+			}()
+
+			pullerSHTemplate = &template.Template{}
+
+			f := func() {
+				ret := renderPullerSH(imgRef, volDir)
+				Expect(ret).To(BeEmpty())
+			}
+			Expect(f).To(PanicWith(MatchError(`template: : "" is an incomplete or empty template`)))
+		})
+	})
+
+	Context("NewPullerContainer", func() {
+		It("returns the expected container", func() {
+			var (
+				imgRef = "docker.io/library/scratch:" + rand.String(8)
+				volDir = "/tmp/" + rand.String(8)
+			)
+
+			pullerSH := renderPullerSH(imgRef, volDir)
+
+			ret := NewPullerContainer(imgRef, volDir)
+			Expect(ret).NotTo(BeNil())
+			Expect(ret.Name).To(Equal("puller"))
+			Expect(ret.Image).To(Equal("quay.io/skopeo/stable:v1.18.0-immutable"))
+			Expect(ret.Command).To(Equal([]string{"/bin/sh", "-c"}))
+			Expect(ret.Args).To(Equal([]string{pullerSH}))
+		})
+	})
+})
diff --git a/pkg/workspace/image/pusher_test.go b/pkg/workspace/image/pusher_test.go
new file mode 100644
index 0000000..34c2a69
--- /dev/null
+++ b/pkg/workspace/image/pusher_test.go
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package image
+
+import (
+	"encoding/json"
+	"text/template"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"k8s.io/apimachinery/pkg/util/rand"
+)
+
+var _ = Describe("Pusher", func() {
+	Context("embed", func() {
+		It("initializes the pusher text data", func() {
+			Expect(pusherSHTextData).NotTo(BeEmpty())
+		})
+
+		It("uses sh as the interpreter", func() {
+			Expect(pusherSHTextData).To(HavePrefix("#!/bin/sh\n"))
+		})
+	})
+
+	Context("init", func() {
+		It("instantiates the pusher template", func() {
+			Expect(pusherSHTemplate).NotTo(BeNil())
+		})
+	})
+
+	Context("renderPusherSH", func() {
+		It("renders the pusher script", func() {
+			var (
+				volDir          = "/tmp/" + rand.String(8)
+				imgRef          = "docker.io/library/scratch:" + rand.String(8)
+				annotationsData = map[string]map[string]string{"$config": {"hello": "world"}, "$manifest": {"foo": "bar"}}
+				sentinelPath    = "/tmp/" + rand.String(8)
+			)
+
+			bytes, err := json.Marshal(annotationsData)
+			Expect(err).NotTo(HaveOccurred())
+
+			ret := renderPusherSH(volDir, imgRef, annotationsData, &sentinelPath)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${VOL_DIR}" ] || VOL_DIR='` + volDir + `'` + "\n"))
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='` + imgRef + `'` + "\n"))
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z '' ] || ANNOTATIONS_DATA='` + string(bytes) + `'` + "\n"))
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z '' ] || SENTINEL_PATH='` + sentinelPath + `'` + "\n"))
+		})
+
+		It("normalizes the image reference", func() {
+			var (
+				volDir = "/tmp/" + rand.String(8)
+				imgRef = "scratch-" + rand.String(8)
+			)
+
+			ret := renderPusherSH(volDir, imgRef, nil, nil)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='docker.io/library/` + imgRef + `:latest'` + "\n"))
+		})
+
+		It("falls back to original image reference when unable to normalize", func() {
+			var (
+				volDir = "/tmp/" + rand.String(8)
+				imgRef = "^" + rand.String(8)
+			)
+
+			ret := renderPusherSH(volDir, imgRef, nil, nil)
+			Expect(ret).To(ContainSubstring("\n" + `[ ! -z "${IMG_REF}" ] || IMG_REF='` + imgRef + `'` + "\n"))
+		})
+
+		It("panics when the template is bad", func() {
+			var (
+				volDir = "/tmp/" + rand.String(8)
+				imgRef = "docker.io/library/scratch:" + rand.String(8)
+			)
+
+			pusherSHTemplate0 := pusherSHTemplate
+			defer func() {
+				pusherSHTemplate = pusherSHTemplate0
+			}()
+
+			pusherSHTemplate = &template.Template{}
+
+			f := func() {
+				ret := renderPusherSH(volDir, imgRef, nil, nil)
+				Expect(ret).To(BeEmpty())
+			}
+			Expect(f).To(PanicWith(MatchError(`template: : "" is an incomplete or empty template`)))
+		})
+	})
+
+	Context("NewPusherContainer", func() {
+		It("returns the expected container", func() {
+			var (
+				volDir = "/tmp/" + rand.String(8)
+				imgRef = "docker.io/library/scratch:" + rand.String(8)
+			)
+
+			pusherSH := renderPusherSH(volDir, imgRef, nil, nil)
+
+			ret := NewPusherContainer(volDir, imgRef, nil, nil)
+			Expect(ret).NotTo(BeNil())
+			Expect(ret.Name).To(Equal("pusher"))
+			Expect(ret.Image).To(Equal("ghcr.io/oras-project/oras:v1.2.2"))
+			Expect(ret.Command).To(Equal([]string{"/bin/sh", "-c"}))
+			Expect(ret.Args).To(Equal([]string{pusherSH}))
+		})
+	})
+})
diff --git a/pkg/workspace/image/suite_test.go b/pkg/workspace/image/suite_test.go
new file mode 100644
index 0000000..f601338
--- /dev/null
+++ b/pkg/workspace/image/suite_test.go
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package image
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestImage(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Image Suite")
+}
diff --git a/pkg/workspace/inference/preset-inferences_test.go b/pkg/workspace/inference/preset-inferences_test.go
index 4d224a4..a66bf4e 100644
--- a/pkg/workspace/inference/preset-inferences_test.go
+++ b/pkg/workspace/inference/preset-inferences_test.go
@@ -14,7 +14,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/utils/test"
@@ -25,7 +25,7 @@ var ValidStrength string = "0.5"
 func TestCreatePresetInference(t *testing.T) {
 	test.RegisterTestModel()
 	testcases := map[string]struct {
-		workspace      *v1alpha1.Workspace
+		workspace      *v1beta1.Workspace
 		nodeCount      int
 		modelName      string
 		callMocks      func(c *test.MockClient)
@@ -134,9 +134,9 @@ func TestCreatePresetInference(t *testing.T) {
 			workspace.Resource.Count = &tc.nodeCount
 			expectedSecrets := []string{"fake-secret"}
 			if tc.hasAdapters {
-				workspace.Inference.Adapters = []v1alpha1.AdapterSpec{
+				workspace.Inference.Adapters = []v1beta1.AdapterSpec{
 					{
-						Source: &v1alpha1.DataSource{
+						Source: &v1beta1.DataSource{
 							Name:             "Adapter-1",
 							Image:            "fake.kaito.com/kaito-image:0.0.1",
 							ImagePullSecrets: expectedSecrets,
diff --git a/pkg/workspace/manifests/manifests_test.go b/pkg/workspace/manifests/manifests_test.go
index 174c4f7..bfa39b5 100644
--- a/pkg/workspace/manifests/manifests_test.go
+++ b/pkg/workspace/manifests/manifests_test.go
@@ -11,7 +11,7 @@ import (
 
 	v1 "k8s.io/api/core/v1"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
@@ -40,7 +40,7 @@ func TestGenerateStatefulSetManifest(t *testing.T) {
 		}
 
 		appSelector := map[string]string{
-			v1alpha1.LabelWorkspaceName: workspace.Name,
+			v1beta1.LabelWorkspaceName: workspace.Name,
 		}
 
 		if !reflect.DeepEqual(appSelector, obj.Spec.Selector.MatchLabels) {
@@ -80,7 +80,7 @@ func TestGenerateDeploymentManifest(t *testing.T) {
 		)
 
 		appSelector := map[string]string{
-			v1alpha1.LabelWorkspaceName: workspace.Name,
+			v1beta1.LabelWorkspaceName: workspace.Name,
 		}
 
 		if !reflect.DeepEqual(appSelector, obj.Spec.Selector.MatchLabels) {
@@ -108,7 +108,7 @@ func TestGenerateDeploymentManifestWithPodTemplate(t *testing.T) {
 		obj := GenerateDeploymentManifestWithPodTemplate(context.TODO(), workspace, nil)
 
 		appSelector := map[string]string{
-			v1alpha1.LabelWorkspaceName: workspace.Name,
+			v1beta1.LabelWorkspaceName: workspace.Name,
 		}
 
 		if !reflect.DeepEqual(appSelector, obj.Spec.Selector.MatchLabels) {
@@ -146,7 +146,7 @@ func TestGenerateServiceManifest(t *testing.T) {
 			obj := GenerateServiceManifest(context.TODO(), workspace, v1.ServiceTypeClusterIP, isStatefulSet)
 
 			svcSelector := map[string]string{
-				v1alpha1.LabelWorkspaceName: workspace.Name,
+				v1beta1.LabelWorkspaceName: workspace.Name,
 			}
 			if isStatefulSet {
 				svcSelector["statefulset.kubernetes.io/pod-name"] = fmt.Sprintf("%s-0", workspace.Name)
@@ -165,7 +165,7 @@ func TestGenerateHeadlessServiceManifest(t *testing.T) {
 		obj := GenerateHeadlessServiceManifest(context.TODO(), workspace)
 
 		svcSelector := map[string]string{
-			v1alpha1.LabelWorkspaceName: workspace.Name,
+			v1beta1.LabelWorkspaceName: workspace.Name,
 		}
 		if !reflect.DeepEqual(svcSelector, obj.Spec.Selector) {
 			t.Errorf("svc selector is wrong")
diff --git a/pkg/workspace/tuning/preset-tuning_test.go b/pkg/workspace/tuning/preset-tuning_test.go
index 0ec6856..bffa67b 100644
--- a/pkg/workspace/tuning/preset-tuning_test.go
+++ b/pkg/workspace/tuning/preset-tuning_test.go
@@ -11,12 +11,12 @@ import (
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
-	"k8s.io/utils/ptr"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/workspace/image"
 )
 
 func normalize(s string) string {
@@ -55,16 +55,16 @@ func TestGetInstanceGPUCount(t *testing.T) {
 func TestGetTuningImageInfo(t *testing.T) {
 	testcases := map[string]struct {
 		registryName string
-		wObj         *kaitov1alpha1.Workspace
+		wObj         *kaitov1beta1.Workspace
 		presetObj    *model.PresetParam
 		expected     string
 	}{
 		"Valid Registry and Parameters": {
 			registryName: "testregistry",
-			wObj: &kaitov1alpha1.Workspace{
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Preset: &kaitov1alpha1.PresetSpec{
-						PresetMeta: kaitov1alpha1.PresetMeta{
+			wObj: &kaitov1beta1.Workspace{
+				Tuning: &kaitov1beta1.TuningSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
 							Name: "testpreset",
 						},
 					},
@@ -77,10 +77,10 @@ func TestGetTuningImageInfo(t *testing.T) {
 		},
 		"Empty Registry Name": {
 			registryName: "",
-			wObj: &kaitov1alpha1.Workspace{
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Preset: &kaitov1alpha1.PresetSpec{
-						PresetMeta: kaitov1alpha1.PresetMeta{
+			wObj: &kaitov1beta1.Workspace{
+				Tuning: &kaitov1beta1.TuningSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
 							Name: "testpreset",
 						},
 					},
@@ -105,14 +105,14 @@ func TestGetTuningImageInfo(t *testing.T) {
 
 func TestGetDataSrcImageInfo(t *testing.T) {
 	testcases := map[string]struct {
-		wObj            *kaitov1alpha1.Workspace
+		wObj            *kaitov1beta1.Workspace
 		expectedImage   string
 		expectedSecrets []corev1.LocalObjectReference
 	}{
 		"Multiple Image Pull Secrets": {
-			wObj: &kaitov1alpha1.Workspace{
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Input: &kaitov1alpha1.DataSource{
+			wObj: &kaitov1beta1.Workspace{
+				Tuning: &kaitov1beta1.TuningSpec{
+					Input: &kaitov1beta1.DataSource{
 						Image:            "kaito/data-source",
 						ImagePullSecrets: []string{"secret1", "secret2"},
 					},
@@ -125,9 +125,9 @@ func TestGetDataSrcImageInfo(t *testing.T) {
 			},
 		},
 		"No Image Pull Secrets": {
-			wObj: &kaitov1alpha1.Workspace{
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Input: &kaitov1alpha1.DataSource{
+			wObj: &kaitov1beta1.Workspace{
+				Tuning: &kaitov1beta1.TuningSpec{
+					Input: &kaitov1beta1.DataSource{
 						Image: "kaito/data-source",
 					},
 				},
@@ -220,48 +220,9 @@ training_config:
 	}
 }
 
-func TestHandleImageDataSource(t *testing.T) {
-	testcases := map[string]struct {
-		workspaceObj              *kaitov1alpha1.Workspace
-		expectedInitContainerName string
-		expectedVolumeName        string
-		expectedVolumeMountPath   string
-	}{
-		"Handle Image Data Source": {
-			workspaceObj: &kaitov1alpha1.Workspace{
-				Resource: kaitov1alpha1.ResourceSpec{
-					Count: ptr.To(1),
-				},
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Input: &kaitov1alpha1.DataSource{
-						Image: "data-image",
-					},
-				},
-			},
-			expectedInitContainerName: "data-extractor",
-			expectedVolumeName:        "data-volume",
-			expectedVolumeMountPath:   "/mnt/data",
-		},
-	}
-
-	for name, tc := range testcases {
-		t.Run(name, func(t *testing.T) {
-			initContainer, volume, volumeMount := handleImageDataSource(context.Background(), tc.workspaceObj.Tuning.Input.Image)
-
-			assert.Equal(t, tc.expectedInitContainerName, initContainer.Name)
-			assert.Equal(t, tc.workspaceObj.Tuning.Input.Image, initContainer.Image)
-			assert.Contains(t, initContainer.Command[2], "cp -r /data/* /mnt/data")
-
-			assert.Equal(t, tc.expectedVolumeName, volume.Name)
-
-			assert.Equal(t, tc.expectedVolumeMountPath, volumeMount.MountPath)
-		})
-	}
-}
-
 func TestHandleURLDataSource(t *testing.T) {
 	testcases := map[string]struct {
-		workspaceObj              *kaitov1alpha1.Workspace
+		workspaceObj              *kaitov1beta1.Workspace
 		expectedInitContainerName string
 		expectedImage             string
 		expectedCommands          string
@@ -269,9 +230,9 @@ func TestHandleURLDataSource(t *testing.T) {
 		expectedVolumeMountPath   string
 	}{
 		"Handle URL Data Source": {
-			workspaceObj: &kaitov1alpha1.Workspace{
-				Tuning: &kaitov1alpha1.TuningSpec{
-					Input: &kaitov1alpha1.DataSource{
+			workspaceObj: &kaitov1beta1.Workspace{
+				Tuning: &kaitov1beta1.TuningSpec{
+					Input: &kaitov1beta1.DataSource{
 						URLs: []string{"http://example.com/data1.zip", "http://example.com/data2.zip"},
 					},
 				},
@@ -304,15 +265,15 @@ func TestPrepareTuningParameters(t *testing.T) {
 
 	testcases := map[string]struct {
 		name                 string
-		workspaceObj         *kaitov1alpha1.Workspace
+		workspaceObj         *kaitov1beta1.Workspace
 		modelCommand         string
 		tuningObj            *model.PresetParam
 		expectedCommands     []string
 		expectedRequirements corev1.ResourceRequirements
 	}{
 		"Basic Tuning Parameters Setup": {
-			workspaceObj: &kaitov1alpha1.Workspace{
-				Resource: kaitov1alpha1.ResourceSpec{
+			workspaceObj: &kaitov1beta1.Workspace{
+				Resource: kaitov1beta1.ResourceSpec{
 					InstanceType: "gpu-instance-type",
 				},
 			},
@@ -351,40 +312,101 @@ func TestPrepareTuningParameters(t *testing.T) {
 	}
 }
 
+func TestPrepareDataDestination_ImageDestination(t *testing.T) {
+	ctx := context.TODO()
+
+	workspaceObj := &kaitov1beta1.Workspace{
+		Tuning: &kaitov1beta1.TuningSpec{
+			Output: &kaitov1beta1.DataDestination{
+				Image:           "custom/data-loader-image",
+				ImagePushSecret: "image-push-secret",
+			},
+		},
+	}
+
+	expectedImagePushSecret := corev1.LocalObjectReference{
+		Name: workspaceObj.Tuning.Output.ImagePushSecret,
+	}
+
+	expectedVolume, expectedVolumeMount := utils.ConfigImagePushSecretVolume(expectedImagePushSecret.Name)
+
+	outDir := "/mnt/results"
+
+	expectedSidecarContainer := image.NewPusherContainer(outDir, workspaceObj.Tuning.Output.Image, nil, nil)
+
+	sidecarContainer, imagePushSecret, volume, volumeMount := prepareDataDestination(ctx, workspaceObj, outDir)
+
+	// Assertions
+	assert.Equal(t, expectedSidecarContainer, sidecarContainer)
+	assert.Equal(t, expectedVolume, volume)
+	assert.Equal(t, expectedVolumeMount, volumeMount)
+	assert.Equal(t, expectedImagePushSecret, *imagePushSecret)
+}
+
 func TestPrepareDataSource_ImageSource(t *testing.T) {
 	ctx := context.TODO()
 
-	workspaceObj := &kaitov1alpha1.Workspace{
-		Tuning: &kaitov1alpha1.TuningSpec{
-			Input: &kaitov1alpha1.DataSource{
+	workspaceObj := &kaitov1beta1.Workspace{
+		Tuning: &kaitov1beta1.TuningSpec{
+			Input: &kaitov1beta1.DataSource{
+				Name:  "some-name",
 				Image: "custom/data-loader-image",
+				ImagePullSecrets: []string{
+					"image-pull-secret-name",
+				},
 			},
 		},
 	}
 
 	// Expected outputs from mocked functions
-	expectedVolume := corev1.Volume{
-		Name: "data-volume",
-		VolumeSource: corev1.VolumeSource{
-			EmptyDir: &corev1.EmptyDirVolumeSource{}, // Assume we expect an EmptyDir
+	expectedInitContainer := image.NewPullerContainer(workspaceObj.Tuning.Input.Image, "/mnt/data")
+
+	expectedVolumes := []corev1.Volume{
+		{
+			Name: "docker-config-some-name-tuning-input",
+			VolumeSource: corev1.VolumeSource{
+				Projected: &corev1.ProjectedVolumeSource{
+					Sources: []corev1.VolumeProjection{
+						{
+							Secret: &corev1.SecretProjection{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: "image-pull-secret-name",
+								},
+								Items: []corev1.KeyToPath{
+									{
+										Key:  ".dockerconfigjson",
+										Path: "image-pull-secret-name.json",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			Name: "data-volume",
+			VolumeSource: corev1.VolumeSource{
+				EmptyDir: &corev1.EmptyDirVolumeSource{},
+			},
 		},
 	}
 
-	expectedVolumeMount := corev1.VolumeMount{Name: "data-volume", MountPath: "/mnt/data"}
-	expectedImagePullSecrets := []corev1.LocalObjectReference{}
-	expectedInitContainer := &corev1.Container{
-		Name:         "data-extractor",
-		Image:        "custom/data-loader-image",
-		Command:      []string{"sh", "-c", "ls -la /data && cp -r /data/* /mnt/data && ls -la /mnt/data"},
-		VolumeMounts: []corev1.VolumeMount{expectedVolumeMount},
+	expectedVolumeMounts := []corev1.VolumeMount{
+		{
+			Name:      "docker-config-some-name-tuning-input",
+			MountPath: "/root/.docker/config.d/some-name-tuning-input",
+		},
+		{
+			Name:      "data-volume",
+			MountPath: "/mnt/data",
+		},
 	}
 
-	initContainer, imagePullSecrets, volume, volumeMount, err := prepareDataSource(ctx, workspaceObj)
+	initContainer, volumes, volumeMounts := prepareDataSource(ctx, workspaceObj)
 
 	// Assertions
-	assert.NoError(t, err)
 	assert.Equal(t, expectedInitContainer, initContainer)
-	assert.Equal(t, expectedVolume, volume)
-	assert.Equal(t, expectedVolumeMount, volumeMount)
-	assert.Equal(t, expectedImagePullSecrets, imagePullSecrets)
+	assert.Equal(t, expectedVolumes, volumes)
+	assert.Equal(t, expectedVolumeMounts, volumeMounts)
 }
diff --git a/presets/ragengine/tests/api/conftest.py b/presets/ragengine/tests/api/conftest.py
index 1576962..bc9ffb0 100644
--- a/presets/ragengine/tests/api/conftest.py
+++ b/presets/ragengine/tests/api/conftest.py
@@ -7,3 +7,6 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../.
 os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # Force CPU-only execution for testing
 os.environ["OMP_NUM_THREADS"] = "1" # Force single-threaded for testing to prevent segfault while loading embedding model
 os.environ["MKL_NUM_THREADS"] = "1"  # Force MKL to use a single thread
+
+import nest_asyncio
+nest_asyncio.apply()
diff --git a/presets/ragengine/tests/api/test_main.py b/presets/ragengine/tests/api/test_main.py
index 102936d..92ef6a3 100644
--- a/presets/ragengine/tests/api/test_main.py
+++ b/presets/ragengine/tests/api/test_main.py
@@ -1,24 +1,42 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT license.
 
-from unittest.mock import patch
+from unittest.mock import patch, ANY
 
 from llama_index.core.storage.index_store import SimpleIndexStore
 
 from ragengine.main import app, vector_store_handler, rag_ops
-from fastapi.testclient import TestClient
+from ragengine.config import DEFAULT_VECTOR_DB_PERSIST_DIR
+
+import os
+
 import pytest
+import pytest_asyncio
+import asyncio
+import httpx
+import respx
 
 AUTO_GEN_DOC_ID_LEN = 64
 
-client = TestClient(app)
+@pytest_asyncio.fixture
+async def async_client():
+    """Use an async HTTP client to interact with FastAPI app."""
+    async with httpx.AsyncClient(app=app, base_url="http://localhost") as client:
+        yield client
+
+@pytest.fixture(scope="session")
+def event_loop():
+    loop = asyncio.new_event_loop()
+    yield loop
+    loop.close()
 
 @pytest.fixture(autouse=True)
 def clear_index():
     vector_store_handler.index_map.clear()
     vector_store_handler.index_store = SimpleIndexStore()
 
-def test_index_documents_success():
+@pytest.mark.asyncio
+async def test_index_documents_success(async_client):
     request_data = {
         "index_name": "test_index",
         "documents": [
@@ -27,7 +45,7 @@ def test_index_documents_success():
         ]
     }
 
-    response = client.post("/index", json=request_data)
+    response = await async_client.post("/index", json=request_data)
     assert response.status_code == 200
     doc1, doc2 = response.json()
     assert (doc1["text"] == "This is a test document")
@@ -38,14 +56,21 @@ def test_index_documents_success():
     assert len(doc2["doc_id"]) == AUTO_GEN_DOC_ID_LEN
     assert not doc2["metadata"]
 
-@patch('requests.post')
-def test_query_index_success(mock_post):
-    # Define Mock Response for Custom Inference API
-    mock_response = {
-        "result": "This is the completion from the API"
+@pytest.mark.asyncio
+@respx.mock
+@patch("requests.get")  # Mock the requests.get call for fetching model metadata
+async def test_query_index_success(mock_get, async_client):
+    # Mock the response for the default model fetch
+    mock_get.return_value.status_code = 200
+    mock_get.return_value.json.return_value = {
+        "data": [{"id": "mock-model", "max_model_len": 2048}]
     }
-    mock_post.return_value.json.return_value = mock_response
-    # Index
+
+    # Mock HTTPX response for Custom Inference API
+    mock_response = {"result": "This is the completion from the API"}
+    respx.post("http://localhost:5000/v1/completions").mock(return_value=httpx.Response(200, json=mock_response))
+
+    # Index Request
     request_data = {
         "index_name": "test_index",
         "documents": [
@@ -54,10 +79,10 @@ def test_query_index_success(mock_post):
         ]
     }
 
-    response = client.post("/index", json=request_data)
+    response = await async_client.post("/index", json=request_data)
     assert response.status_code == 200
 
-    # Query
+    # Query Request
     request_data = {
         "index_name": "test_index",
         "query": "test query",
@@ -65,18 +90,24 @@ def test_query_index_success(mock_post):
         "llm_params": {"temperature": 0.7}
     }
 
-    response = client.post("/query", json=request_data)
+    response = await async_client.post("/query", json=request_data)
     assert response.status_code == 200
     assert response.json()["response"] == "{'result': 'This is the completion from the API'}"
     assert len(response.json()["source_nodes"]) == 1
     assert response.json()["source_nodes"][0]["text"] == "This is a test document"
     assert response.json()["source_nodes"][0]["score"] == pytest.approx(0.5354418754577637, rel=1e-6)
     assert response.json()["source_nodes"][0]["metadata"] == {}
-    assert mock_post.call_count == 1
 
+    # Ensure HTTPX was called once
+    assert respx.calls.call_count == 1
+
+    # Ensure the model fetch was called once
+    mock_get.assert_called_once_with("http://localhost:5000/v1/models", headers=ANY)
 
-@patch('requests.post')
-def test_reranker_and_query_with_index(mock_post):
+@pytest.mark.asyncio
+@respx.mock
+@patch("requests.get")  # Mock the requests.get call for fetching model metadata
+async def test_reranker_and_query_with_index(mock_get, async_client):
     """
     Test reranker and query functionality with indexed documents.
 
@@ -102,12 +133,22 @@ def test_reranker_and_query_with_index(mock_post):
     Doc: 3, Relevance: 4
     Doc: 7, Relevance: 3
     """
-    # Mock responses for the reranker and query API calls
-    reranker_mock_response = "Doc: 4, Relevance: 10\nDoc: 5, Relevance: 10"
-    query_mock_response = {"result": "This is the completion from the API"}
-    mock_http_responses = [reranker_mock_response, query_mock_response]
+    # Mock the response for the default model fetch
+    mock_get.return_value.status_code = 200
+    mock_get.return_value.json.return_value = {
+        "data": [{"id": "mock-model", "max_model_len": 2048}]
+    }
+
+    # Mock HTTPX response for reranker and query API calls
+    reranker_mock_response = {"choices": [{"text": "Doc: 4, Relevance: 10\nDoc: 5, Relevance: 10"}]}
+    query_mock_response = {"choices": [{"text": "his is the completion from the API"}]}
+
+    # Mock reranker response
+    respx.post("http://localhost:5000/v1/completions", content__contains="A list of documents is shown below") \
+        .mock(return_value=httpx.Response(200, json=reranker_mock_response))
 
-    mock_post.return_value.json.side_effect = mock_http_responses
+    # Mock query response
+    respx.post("http://localhost:5000/v1/completions").mock(return_value=httpx.Response(200, json=query_mock_response))
 
     # Define input documents for indexing
     documents = [
@@ -126,26 +167,25 @@ def test_reranker_and_query_with_index(mock_post):
     }
 
     # Perform indexing
-    response = client.post("/index", json=index_request_payload)
+    response = await async_client.post("/index", json=index_request_payload)
     assert response.status_code == 200
 
     # Query request payload with reranking
-    top_n = len(reranker_mock_response.split("\n"))  # Extract top_n from mock reranker response
+    top_n = 2  # The number of relevant docs returned in reranker response
     query_request_payload = {
         "index_name": "test_index",
         "query": "what is the capital of france?",
         "top_k": 5,
-        "llm_params": {"temperature": 0.7},
+        "llm_params": {"temperature": 0, "max_tokens": 2000},
         "rerank_params": {"top_n": top_n}
     }
 
     # Perform query
-    response = client.post("/query", json=query_request_payload)
+    response = await async_client.post("/query", json=query_request_payload)
     assert response.status_code == 200
     query_response = response.json()
 
     # Validate query response
-    assert query_response["response"] == str(query_mock_response)
     assert len(query_response["source_nodes"]) == top_n
 
     # Validate each source node in the query response
@@ -162,10 +202,69 @@ def test_reranker_and_query_with_index(mock_post):
         assert actual_node["score"] == expected_node["score"]
         assert actual_node["metadata"] == expected_node["metadata"]
 
-    # Verify the number of mock API calls
-    assert mock_post.call_count == len(mock_http_responses)
+    # Ensure HTTPX requests were made
+    assert respx.calls.call_count == 2  # One for rerank, one for query completion
+
+@pytest.mark.asyncio
+@respx.mock
+@patch("requests.get")  # Mock the requests.get call for fetching model metadata
+async def test_reranker_failed_and_query_with_index(mock_get, async_client):
+    """
+    Test a failed reranker request with query.
+    """
+    # Mock the response for the default model fetch
+    mock_get.return_value.status_code = 200
+    mock_get.return_value.json.return_value = {
+        "data": [{"id": "mock-model", "max_model_len": 2048}]
+    }
+
+    # Mock HTTPX response for reranker and query API calls
+    reranker_mock_response = {"choices": [{"text": "Empty Response"}]}
 
-def test_query_index_failure():
+    # Mock reranker response
+    respx.post("http://localhost:5000/v1/completions", content__contains="A list of documents is shown below") \
+        .mock(return_value=httpx.Response(200, json=reranker_mock_response))
+
+    # Define input documents for indexing
+    documents = [
+        "The capital of France is great.",
+        "The capital of France is huge.",
+        "The capital of France is beautiful.",
+        "Have you ever visited Paris? It is a beautiful city where you can eat delicious food and see the Eiffel Tower. I really enjoyed all the cities in France, but its capital with the Eiffel Tower is my favorite city.",
+        "I really enjoyed my trip to Paris, France. The city is beautiful and the food is delicious. I would love to visit again. "
+        "Such a great capital city."
+    ]
+
+    # Indexing request payload
+    index_request_payload = {
+        "index_name": "test_index",
+        "documents": [{"text": doc.strip()} for doc in documents]
+    }
+
+    # Perform indexing
+    response = await async_client.post("/index", json=index_request_payload)
+    assert response.status_code == 200
+
+    # Query request payload with reranking
+    top_n = 2  # The number of relevant docs returned in reranker response
+    query_request_payload = {
+        "index_name": "test_index",
+        "query": "what is the capital of france?",
+        "top_k": 5,
+        "llm_params": {"temperature": 0, "max_tokens": 2000},
+        "rerank_params": {"top_n": top_n}
+    }
+
+    # Perform query
+    response = await async_client.post("/query", json=query_request_payload)
+    assert response.status_code == 422
+    assert response.content == b'{"detail":"Rerank operation failed: Invalid response from LLM. This feature is experimental."}'
+
+    # Ensure HTTPX requests were made
+    assert respx.calls.call_count == 1  # One for rerank
+
+@pytest.mark.asyncio
+async def test_query_index_failure(async_client):
     # Prepare request data for querying.
     request_data = {
         "index_name": "non_existent_index",  # Use an index name that doesn't exist
@@ -174,36 +273,101 @@ def test_query_index_failure():
         "llm_params": {"temperature": 0.7}
     }
 
-    response = client.post("/query", json=request_data)
+    response = await async_client.post("/query", json=request_data)
     assert response.status_code == 400
     assert response.json()["detail"] == "No such index: 'non_existent_index' exists."
 
+@pytest.mark.asyncio
+async def test_list_documents_in_index_success(async_client):
+    index_name = "test_index"
 
-def test_list_all_indexed_documents_success():
-    response = client.get("/indexed-documents")
-    assert response.status_code == 200
-    assert response.json() == {'documents': {}}
+    # Ensure no documents are present initially
+    response = await async_client.get(f"/indexes/{index_name}/documents")
+
+    assert response.status_code == 500
+    assert response.json() == {'detail': "Index 'test_index' not found."}
 
     request_data = {
-        "index_name": "test_index",
+        "index_name": index_name,
         "documents": [
             {"text": "This is a test document"},
             {"text": "Another test document"}
         ]
     }
 
-    response = client.post("/index", json=request_data)
+    response = await async_client.post("/index", json=request_data)
     assert response.status_code == 200
 
-    response = client.get("/indexed-documents")
+    # Retrieve documents for the specific index
+    response = await async_client.get(f"/indexes/{index_name}/documents")
     assert response.status_code == 200
-    assert "test_index" in response.json()["documents"]
-    response_idx = response.json()["documents"]["test_index"]
-    assert len(response_idx) == 2 # Two Documents Indexed
-    assert ({item["text"] for item in response_idx.values()}
+    response_json = response.json()
+
+    # Ensure documents exist correctly in the specific index
+    assert response_json["count"] == 2
+    assert len(response_json["documents"]) == 2
+    assert ({item["text"] for item in response_json["documents"]}
             == {item["text"] for item in request_data["documents"]})
 
 
+@pytest.mark.asyncio
+async def test_persist_documents(async_client):
+    index_name = "test_index"
+
+    # Ensure no documents are present initially
+    response = await async_client.get(f"/indexes/{index_name}/documents")
+
+    assert response.status_code == 500
+    assert response.json() == {'detail': "Index 'test_index' not found."}
+
+    request_data = {
+        "index_name": index_name,
+        "documents": [
+            {"text": "This is a test document"},
+            {"text": "Another test document"}
+        ]
+    }
+
+    response = await async_client.post("/index", json=request_data)
+    assert response.status_code == 200
+
+    # Persist documents for the specific index
+    response = await async_client.post(f"/persist/{index_name}")
+    assert response.status_code == 200
+    response_json = response.json()
+    assert response_json == {"message": f"Successfully persisted index {index_name} to {DEFAULT_VECTOR_DB_PERSIST_DIR}/{index_name}."}
+    assert os.path.exists(os.path.join(DEFAULT_VECTOR_DB_PERSIST_DIR, index_name))
+
+    # Persist documents for the specific index at a custom path
+    custom_path = "./custom_test_path"
+    response = await async_client.post(f"/persist/{index_name}?path={custom_path}")
+    assert response.status_code == 200
+    response_json = response.json()
+    assert response_json == {"message": f"Successfully persisted index {index_name} to {custom_path}."}
+    assert os.path.exists(custom_path)
+
+@pytest.mark.asyncio
+async def test_load_documents(async_client):
+    index_name = "test_index"
+    response = await async_client.post(f"/load/{index_name}?path={DEFAULT_VECTOR_DB_PERSIST_DIR}/{index_name}")
+
+    assert response.status_code == 200
+    assert response.json() == {'message': 'Successfully loaded index test_index from storage/test_index.'}
+
+    response = await async_client.get(f"/indexes")
+    assert response.status_code == 200
+    assert response.json() == [index_name]
+
+    response = await async_client.get(f"/indexes/test_index/documents")
+    assert response.status_code == 200
+    response_data = response.json()
+
+    assert response_data["count"] == 2
+    assert len(response_data["documents"]) == 2
+    assert response_data["documents"][0]["text"] == "This is a test document"
+    assert response_data["documents"][1]["text"] == "Another test document"
+
+
 """
 Example of a live query test. This test is currently commented out as it requires a valid 
 INFERENCE_URL in config.py. To run the test, ensure that a valid INFERENCE_URL is provided. 
diff --git a/presets/ragengine/tests/vector_store/conftest.py b/presets/ragengine/tests/vector_store/conftest.py
index 1576962..338e142 100644
--- a/presets/ragengine/tests/vector_store/conftest.py
+++ b/presets/ragengine/tests/vector_store/conftest.py
@@ -3,6 +3,7 @@
 
 import sys
 import os
+
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
 os.environ["CUDA_VISIBLE_DEVICES"] = "-1" # Force CPU-only execution for testing
 os.environ["OMP_NUM_THREADS"] = "1" # Force single-threaded for testing to prevent segfault while loading embedding model
diff --git a/presets/ragengine/tests/vector_store/test_base_store.py b/presets/ragengine/tests/vector_store/test_base_store.py
index 9f55bad..49b17e6 100644
--- a/presets/ragengine/tests/vector_store/test_base_store.py
+++ b/presets/ragengine/tests/vector_store/test_base_store.py
@@ -10,7 +10,9 @@ from ragengine.vector_store.base import BaseVectorStore
 from ragengine.models import Document
 from ragengine.embedding.huggingface_local_embedding import LocalHuggingFaceEmbedding
 from ragengine.config import (LOCAL_EMBEDDING_MODEL_ID, LLM_INFERENCE_URL,
-                              LLM_ACCESS_SECRET, VECTOR_DB_PERSIST_DIR)
+                              LLM_ACCESS_SECRET, DEFAULT_VECTOR_DB_PERSIST_DIR)
+import httpx
+import respx
 
 class BaseVectorStoreTest(ABC):
     """Base class for vector store tests that defines the test structure."""
@@ -31,20 +33,22 @@ class BaseVectorStoreTest(ABC):
         """Override this in implementation-specific test classes."""
         pass
 
-    def test_index_documents(self, vector_store_manager):
+    @pytest.mark.asyncio
+    async def test_index_documents(self, vector_store_manager):
         first_doc_text, second_doc_text = "First document", "Second document"
         documents = [
             Document(text=first_doc_text, metadata={"type": "text"}),
             Document(text=second_doc_text, metadata={"type": "text"})
         ]
         
-        doc_ids = vector_store_manager.index_documents("test_index", documents)
+        doc_ids = await vector_store_manager.index_documents("test_index", documents)
         
         assert len(doc_ids) == 2
         assert set(doc_ids) == {BaseVectorStore.generate_doc_id(first_doc_text),
                                 BaseVectorStore.generate_doc_id(second_doc_text)}
 
-    def test_index_documents_isolation(self, vector_store_manager):
+    @pytest.mark.asyncio
+    async def test_index_documents_isolation(self, vector_store_manager):
         documents1 = [
             Document(text="First document in index1", metadata={"type": "text"}),
         ]
@@ -54,67 +58,103 @@ class BaseVectorStoreTest(ABC):
 
         # Index documents in separate indices
         index_name_1, index_name_2 = "index1", "index2"
-        vector_store_manager.index_documents(index_name_1, documents1)
-        vector_store_manager.index_documents(index_name_2, documents2)
+        await vector_store_manager.index_documents(index_name_1, documents1)
+        await vector_store_manager.index_documents(index_name_2, documents2)
 
         # Call the backend-specific check method
-        self.check_indexed_documents(vector_store_manager)
+        await self.check_indexed_documents(vector_store_manager)
 
     @abstractmethod
     def check_indexed_documents(self, vector_store_manager):
         """Abstract method to check indexed documents in backend-specific format."""
         pass
 
-    @patch('requests.post')
-    def test_query_documents(self, mock_post, vector_store_manager):
-        mock_response = {
-            "result": "This is the completion from the API"
+    @pytest.mark.asyncio
+    @respx.mock
+    @patch("requests.get")
+    async def test_query_documents(self, mock_get, vector_store_manager):
+        mock_get.return_value.status_code = 200
+        mock_get.return_value.json.return_value = {
+            "data": [{"id": "mock-model", "max_model_len": 2048}]
         }
-        mock_post.return_value.json.return_value = mock_response
+
+        mock_response = {"result": "This is the completion from the API"}
+        respx.post(LLM_INFERENCE_URL).mock(return_value=httpx.Response(200, json=mock_response))
 
         documents = [
             Document(text="First document", metadata={"type": "text"}),
             Document(text="Second document", metadata={"type": "text"})
         ]
-        vector_store_manager.index_documents("test_index", documents)
+        await vector_store_manager.index_documents("test_index", documents)
 
         params = {"temperature": 0.7}
-        query_result = vector_store_manager.query("test_index", "First", top_k=1,
+        query_result = await vector_store_manager.query("test_index", "First", top_k=1,
                                                   llm_params=params, rerank_params={})
 
         assert query_result is not None
         assert query_result["response"] == "{'result': 'This is the completion from the API'}"
         assert query_result["source_nodes"][0]["text"] == "First document"
         assert query_result["source_nodes"][0]["score"] == pytest.approx(self.expected_query_score, rel=1e-6)
+        assert respx.calls.call_count == 1  # Ensure only one LLM inference request was made
 
-        mock_post.assert_called_once_with(
-            LLM_INFERENCE_URL,
-            json={"prompt": "Context information is below.\n---------------------\ntype: text\n\nFirst document\n---------------------\nGiven the context information and not prior knowledge, answer the query.\nQuery: First\nAnswer: ", 'temperature': 0.7},
-            headers={"Authorization": f"Bearer {LLM_ACCESS_SECRET}", 'Content-Type': 'application/json'}
-        )
-
-    def test_add_document(self, vector_store_manager):
+    @pytest.mark.asyncio
+    async def test_add_document(self, vector_store_manager):
         documents = [Document(text="Third document", metadata={"type": "text"})]
-        vector_store_manager.index_documents("test_index", documents)
+        await vector_store_manager.index_documents("test_index", documents)
 
         new_document = [Document(text="Fourth document", metadata={"type": "text"})]
-        vector_store_manager.index_documents("test_index", new_document)
+        await vector_store_manager.index_documents("test_index", new_document)
 
-        assert vector_store_manager.document_exists("test_index", new_document[0],
+        assert await vector_store_manager.document_exists("test_index", new_document[0],
                                                     BaseVectorStore.generate_doc_id("Fourth document"))
 
-    def test_persist_index_1(self, vector_store_manager):
+    @pytest.mark.asyncio
+    async def test_persist_index(self, vector_store_manager):
         documents = [Document(text="Test document", metadata={"type": "text"})]
-        vector_store_manager.index_documents("test_index", documents)
-        vector_store_manager._persist("test_index")
-        assert os.path.exists(VECTOR_DB_PERSIST_DIR)
+        await vector_store_manager.index_documents("test_index", documents)
+        await vector_store_manager.persist("test_index", DEFAULT_VECTOR_DB_PERSIST_DIR)
+        assert os.path.exists(DEFAULT_VECTOR_DB_PERSIST_DIR)
+
+    @pytest.mark.asyncio
+    async def test_list_documents_in_index(self, vector_store_manager):
+        """Test various list document scenarios with different limit and offset values."""
+        index_name = "test_index"
+        # Create multiple documents
+        documents = [
+            Document(text=f"Document {i}", metadata={"type": "text"})
+            for i in range(10)
+        ]
+        
+        await vector_store_manager.index_documents(index_name, documents)
 
-    def test_persist_index_2(self, vector_store_manager):
-        documents = [Document(text="Test document", metadata={"type": "text"})]
-        vector_store_manager.index_documents("test_index", documents)
+        # 1. Offset 0, Limit 5 (Basic Case)
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=5, offset=0)
+        assert len(result) == 5
+
+        # 2. Offset 5, Limit 5 (Next Batch)
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=5, offset=5)
+        assert len(result) == 5
+
+        # 3. Offset at max (Empty Case)
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=5, offset=10)
+        assert index_name not in result or len(result) == 0
+
+        # 4. Limit larger than available docs
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=15, offset=0)
+        assert len(result) == 10  # Should return only available docs
+
+        # 5. Limit exactly matches available docs
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=10, offset=0)
+        assert len(result) == 10
+
+        # 6. Limit of 1 (Single-Doc Retrieval)
+        result = await vector_store_manager.list_documents_in_index(index_name, limit=1, offset=0)
+        assert len(result) == 1
 
-        documents = [Document(text="Another Test document", metadata={"type": "text"})]
-        vector_store_manager.index_documents("another_test_index", documents)
+        # 7. max_text_length truncation check
+        truncated_result = await vector_store_manager.list_documents_in_index(index_name, limit=1, offset=0, max_text_length=5)
+        assert len(next(iter(truncated_result))['text']) == 5  # Ensure truncation
 
-        vector_store_manager._persist_all()
-        assert os.path.exists(VECTOR_DB_PERSIST_DIR)
+        # 8. max_text_length is None (Full text should return)
+        full_text_result = await vector_store_manager.list_documents_in_index(index_name, limit=1, offset=0, max_text_length=None)
+        assert "Document" in next(iter(full_text_result))['text']  # Ensure no truncation
diff --git a/presets/ragengine/tests/vector_store/test_chromadb_store.py b/presets/ragengine/tests/vector_store/test_chromadb_store.py
deleted file mode 100644
index e0ca36d..0000000
--- a/presets/ragengine/tests/vector_store/test_chromadb_store.py
+++ /dev/null
@@ -1,33 +0,0 @@
-
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT license.
-
-import pytest
-import os
-
-from tempfile import TemporaryDirectory
-from ragengine.tests.vector_store.test_base_store import BaseVectorStoreTest
-from ragengine.vector_store.chromadb_store import ChromaDBVectorStoreHandler
-
-class TestChromaDBVectorStore(BaseVectorStoreTest):
-    """Test implementation for ChromaDB vector store."""
-    
-    @pytest.fixture
-    def vector_store_manager(self, init_embed_manager):
-        with TemporaryDirectory() as temp_dir:
-            print(f"Saving temporary test storage at: {temp_dir}")
-            os.environ['PERSIST_DIR'] = temp_dir
-            manager = ChromaDBVectorStoreHandler(init_embed_manager)
-            manager._clear_collection_and_indexes()
-            yield manager
-
-    def check_indexed_documents(self, vector_store_manager):
-        indexed_docs = vector_store_manager.list_all_indexed_documents()
-        assert len(indexed_docs) == 2
-        assert list(indexed_docs["index1"].values())[0]["text"] == "First document in index1"
-        assert list(indexed_docs["index2"].values())[0]["text"] == "First document in index2"
-
-    @property
-    def expected_query_score(self):
-        """Override this in implementation-specific test classes."""
-        return 0.5601649858735368
diff --git a/presets/ragengine/tests/vector_store/test_faiss_store.py b/presets/ragengine/tests/vector_store/test_faiss_store.py
index 40a3749..0b07513 100644
--- a/presets/ragengine/tests/vector_store/test_faiss_store.py
+++ b/presets/ragengine/tests/vector_store/test_faiss_store.py
@@ -18,18 +18,43 @@ class TestFaissVectorStore(BaseVectorStoreTest):
             os.environ['PERSIST_DIR'] = temp_dir
             yield FaissVectorStoreHandler(init_embed_manager)
 
-    def check_indexed_documents(self, vector_store_manager):
-        expected_output = {
-            'index1': {"87117028123498eb7d757b1507aa3e840c63294f94c27cb5ec83c939dedb32fd": {
-                'hash': '1e64a170be48c45efeaa8667ab35919106da0489ec99a11d0029f2842db133aa',
-                'text': 'First document in index1'
-            }},
-            'index2': {"49b198c0e126a99e1975f17b564756c25b4ad691a57eda583e232fd9bee6de91": {
-                'hash': 'a222f875b83ce8b6eb72b3cae278b620de9bcc7c6b73222424d3ce979d1a463b',
-                'text': 'First document in index2'
-            }}
-        }
-        assert vector_store_manager.list_all_indexed_documents() == expected_output
+    @pytest.mark.asyncio
+    async def check_indexed_documents(self, vector_store_manager):
+        expected_output_1 = [
+            {
+                'hash_value': '1e64a170be48c45efeaa8667ab35919106da0489ec99a11d0029f2842db133aa',
+                'text': 'First document in index1',
+                'is_truncated': False,
+                'metadata': {
+                    'type': 'text',
+                },
+            }
+        ]
+
+        expected_output_2 = [
+            {
+                'hash_value': 'a222f875b83ce8b6eb72b3cae278b620de9bcc7c6b73222424d3ce979d1a463b',
+                'text': 'First document in index2',
+                'is_truncated': False,
+                'metadata': {
+                    'type': 'text',
+                },
+            }
+        ]
+
+        for index, expected_output in zip(["index1", "index2"], [expected_output_1, expected_output_2]):
+            response = await vector_store_manager.list_documents_in_index(
+                index,
+                limit=10,
+                offset=0,
+                max_text_length=1000
+            )
+
+            # Remove "doc_id" from each document in the specified index
+            def remove_doc_id(data: list) -> list:
+                return [{k: v for k, v in doc.items() if k != "doc_id"} for doc in data]
+
+            assert remove_doc_id(response) == expected_output
 
     @property
     def expected_query_score(self):
diff --git a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct-service.yaml b/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct-service.yaml
deleted file mode 100644
index fc35793..0000000
--- a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: falcon-40b-instruct
-spec:
-  selector:
-    app: falcon
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_hf.yaml b/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_hf.yaml
deleted file mode 100644
index a440438..0000000
--- a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_hf.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-40b-instruct
-spec:
-  progressDeadlineSeconds: 1800
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-40b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon40bins
diff --git a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_vllm.yaml b/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_vllm.yaml
deleted file mode 100644
index 7b40cba..0000000
--- a/presets/workspace/test/manifests/falcon-40b-instruct/falcon-40b-instruct_vllm.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-40b-instruct
-spec:
-  progressDeadlineSeconds: 1800
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-40b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype bfloat16 --chat-template /workspace/chat_templates/falcon-instruct.jinja --tensor-parallel-size 2
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon40bins
diff --git a/presets/workspace/test/manifests/falcon-40b/falcon-40b-service.yaml b/presets/workspace/test/manifests/falcon-40b/falcon-40b-service.yaml
deleted file mode 100644
index 80ab4b5..0000000
--- a/presets/workspace/test/manifests/falcon-40b/falcon-40b-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: falcon-40b
-spec:
-  selector:
-    app: falcon
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/falcon-40b/falcon-40b_hf.yaml b/presets/workspace/test/manifests/falcon-40b/falcon-40b_hf.yaml
deleted file mode 100644
index 514d12e..0000000
--- a/presets/workspace/test/manifests/falcon-40b/falcon-40b_hf.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-40b
-spec:
-  progressDeadlineSeconds: 1800
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-40b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon40b
diff --git a/presets/workspace/test/manifests/falcon-40b/falcon-40b_vllm.yaml b/presets/workspace/test/manifests/falcon-40b/falcon-40b_vllm.yaml
deleted file mode 100644
index 7e74ac7..0000000
--- a/presets/workspace/test/manifests/falcon-40b/falcon-40b_vllm.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-40b
-spec:
-  progressDeadlineSeconds: 1800
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-40b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype bfloat16 --chat-template /workspace/chat_templates/falcon-instruct.jinja --tensor-parallel-size 2
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon40b
diff --git a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct-service.yaml b/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct-service.yaml
deleted file mode 100644
index 2f27d46..0000000
--- a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: falcon-7b-instruct
-spec:
-  selector:
-    app: falcon
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_hf.yaml b/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_hf.yaml
deleted file mode 100644
index 1b2092b..0000000
--- a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_hf.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-7b-instruct
-spec:
-  progressDeadlineSeconds: 1200
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon7binst
diff --git a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_vllm.yaml b/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_vllm.yaml
deleted file mode 100644
index 4019d64..0000000
--- a/presets/workspace/test/manifests/falcon-7b-instruct/falcon-7b-instruct_vllm.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-7b-instruct
-spec:
-  progressDeadlineSeconds: 1200
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --chat-template /workspace/chat_templates/falcon-instruct.jinja
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon7binst
diff --git a/presets/workspace/test/manifests/falcon-7b/falcon-7b-service.yaml b/presets/workspace/test/manifests/falcon-7b/falcon-7b-service.yaml
deleted file mode 100644
index 595e839..0000000
--- a/presets/workspace/test/manifests/falcon-7b/falcon-7b-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: falcon-7b
-spec:
-  selector:
-    app: falcon
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/falcon-7b/falcon-7b_hf.yaml b/presets/workspace/test/manifests/falcon-7b/falcon-7b_hf.yaml
deleted file mode 100644
index 56a775f..0000000
--- a/presets/workspace/test/manifests/falcon-7b/falcon-7b_hf.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-7b
-spec:
-  progressDeadlineSeconds: 1200
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-7b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon7b
diff --git a/presets/workspace/test/manifests/falcon-7b/falcon-7b_vllm.yaml b/presets/workspace/test/manifests/falcon-7b/falcon-7b_vllm.yaml
deleted file mode 100644
index bceb145..0000000
--- a/presets/workspace/test/manifests/falcon-7b/falcon-7b_vllm.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-7b
-spec:
-  progressDeadlineSeconds: 1200
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-      - name: falcon-container
-        image: REPO_HERE.azurecr.io/falcon-7b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --chat-template /workspace/chat_templates/falcon-instruct.jinja
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: falcon7b
diff --git a/presets/workspace/test/manifests/inference-tmpl/manifest.yaml b/presets/workspace/test/manifests/inference-tmpl/manifest.yaml
new file mode 100644
index 0000000..ad9c347
--- /dev/null
+++ b/presets/workspace/test/manifests/inference-tmpl/manifest.yaml
@@ -0,0 +1,100 @@
+deployment:
+  apiVersion: apps/v1
+  kind: Deployment
+  metadata:
+    name: WORKLOAD_NAME
+  spec:
+    progressDeadlineSeconds: 3600
+    replicas: 1
+    selector:
+      matchLabels:
+        app: WORKLOAD_NAME
+    template:
+      metadata:
+        labels:
+          app: WORKLOAD_NAME
+      spec:
+        containers:
+        - name: WORKLOAD_NAME-container
+          image: REPO/MODEL_NAME:TAG
+          command:
+            - /bin/sh
+            - -c
+            - RUNTIME_COMMAND
+          resources:
+            requests:
+              nvidia.com/gpu: GPU_COUNT
+            limits:
+              nvidia.com/gpu: GPU_COUNT
+          livenessProbe:
+            httpGet:
+              path: /health
+              port: 5000
+            initialDelaySeconds: 600 # 10 Min
+            periodSeconds: 10
+          readinessProbe:
+            httpGet:
+              path: /health
+              port: 5000
+            initialDelaySeconds: 30
+            periodSeconds: 10
+          volumeMounts:
+          - name: dshm
+            mountPath: /dev/shm
+          - name: config-volume
+            mountPath: /mnt/config
+        volumes:
+        - name: dshm
+          emptyDir:
+            medium: Memory
+        - name: config-volume
+          configMap:
+            defaultMode: 420
+            name: testing-inference-params
+        tolerations:
+        - effect: NoSchedule
+          key: sku
+          operator: Equal
+          value: gpu
+        - effect: NoSchedule
+          key: nvidia.com/gpu
+          operator: Exists
+        nodeSelector:
+          pool: NODE_POOL
+
+config:
+  apiVersion: v1
+  kind: ConfigMap
+  metadata:
+    name: testing-inference-params
+  data:
+    inference_config.yaml: |
+      # Maximum number of steps to find the max available seq len fitting in the GPU memory.
+      max_probe_steps: 6
+
+      vllm:
+        cpu-offload-gb: 0
+        gpu-memory-utilization: 0.9
+        swap-space: 4
+        served-model-name: test
+        dtype: float16
+
+        # max-seq-len-to-capture: 8192
+        # num-scheduler-steps: 1
+        # enable-chunked-prefill: false
+        # see https://docs.vllm.ai/en/stable/models/engine_args.html for more options.
+
+service:
+  apiVersion: v1
+  kind: Service
+  metadata:
+    name: WORKLOAD_NAME
+  spec:
+    ports:
+    - port: 5000
+      targetPort: 5000
+      protocol: TCP
+      name: http
+    selector:
+      app: WORKLOAD_NAME
+    type: ClusterIP
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat-service.yaml b/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat-service.yaml
deleted file mode 100644
index 58720a9..0000000
--- a/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: llama-2-13b-chat
-spec:
-  selector:
-    app: llama
-    statefulset.kubernetes.io/pod-name: llama-2-13b-chat-0
-  ports:
-    - name: http
-      protocol: TCP
-      port: 80
-      targetPort: 5000
-    - name: torchrun
-      protocol: TCP
-      port: 29500
-      targetPort: 29500
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat.yaml b/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat.yaml
index 2520ddb..6389695 100644
--- a/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat.yaml
+++ b/presets/workspace/test/manifests/llama-2-13b-chat/llama-2-13b-chat.yaml
@@ -25,14 +25,14 @@ spec:
             topologyKey: "kubernetes.io/hostname"
       containers:
         - name: llama-container
-          image: REPO_HERE.azurecr.io/llama-2-13b-chat:TAG_HERE # Placeholder that will be replaced
-          env:
-          - name: MASTER_ADDR
-            value: "MASTER_ADDR_HERE"  # Placeholder that will be replaced
+          image: REPO/llama-2-13b-chat:TAG # Placeholder that will be replaced
           command:
             - /bin/sh
             - -c
             - |
+              apt-get update
+              apt-get install -y dnsutils
+              MASTER_ADDR=$(dig +short llama-2-13b-chat.default.svc.cluster.local)
               echo "MASTER_ADDR: $MASTER_ADDR"
               NODE_RANK=$(echo $HOSTNAME | grep -o '[^-]*$')
               cd /workspace/llama/llama-2 && torchrun --nnodes 2 --nproc_per_node 1 --node_rank $NODE_RANK --master_addr $MASTER_ADDR --master_port 29500 inference_api.py
@@ -70,3 +70,23 @@ spec:
           operator: Exists
       nodeSelector:
         pool: llama213bcha
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: llama-2-13b-chat
+spec:
+  selector:
+    app: llama
+    statefulset.kubernetes.io/pod-name: llama-2-13b-chat-0
+  ports:
+    - name: http
+      protocol: TCP
+      port: 80
+      targetPort: 5000
+    - name: torchrun
+      protocol: TCP
+      port: 29500
+      targetPort: 29500
+  type: ClusterIP
+  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-13b/llama-2-13b-service.yaml b/presets/workspace/test/manifests/llama-2-13b/llama-2-13b-service.yaml
deleted file mode 100644
index f43826a..0000000
--- a/presets/workspace/test/manifests/llama-2-13b/llama-2-13b-service.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: llama-2-13b
-spec:
-  selector:
-    app: llama
-    statefulset.kubernetes.io/pod-name: llama-2-13b-0
-  ports:
-    - name: http
-      protocol: TCP
-      port: 80
-      targetPort: 5000
-    - name: torchrun
-      protocol: TCP
-      port: 29500
-      targetPort: 29500
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-13b/llama-2-13b.yaml b/presets/workspace/test/manifests/llama-2-13b/llama-2-13b.yaml
index 3bc5f72..943f206 100644
--- a/presets/workspace/test/manifests/llama-2-13b/llama-2-13b.yaml
+++ b/presets/workspace/test/manifests/llama-2-13b/llama-2-13b.yaml
@@ -25,14 +25,14 @@ spec:
             topologyKey: "kubernetes.io/hostname"
       containers:
         - name: llama-container
-          image: REPO_HERE.azurecr.io/llama-2-13b:TAG_HERE # Placeholder that will be replaced
-          env:
-          - name: MASTER_ADDR
-            value: "MASTER_ADDR_HERE"  # Placeholder that will be replaced
+          image: REPO/llama-2-13b:TAG # Placeholder that will be replaced
           command:
             - /bin/sh
             - -c
             - |
+              apt-get update
+              apt-get install -y dnsutils
+              MASTER_ADDR=$(dig +short llama-2-13b.default.svc.cluster.local)
               echo "MASTER_ADDR: $MASTER_ADDR"
               NODE_RANK=$(echo $HOSTNAME | grep -o '[^-]*$')
               cd /workspace/llama/llama-2 && torchrun --nnodes 2 --nproc_per_node 1 --node_rank $NODE_RANK --master-addr $MASTER_ADDR --master-port 29500 inference_api.py
@@ -70,3 +70,23 @@ spec:
           operator: Exists
       nodeSelector:
         pool: llama213b
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: llama-2-13b
+spec:
+  selector:
+    app: llama
+    statefulset.kubernetes.io/pod-name: llama-2-13b-0
+  ports:
+    - name: http
+      protocol: TCP
+      port: 80
+      targetPort: 5000
+    - name: torchrun
+      protocol: TCP
+      port: 29500
+      targetPort: 29500
+  type: ClusterIP
+  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat-service.yaml b/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat-service.yaml
deleted file mode 100644
index 99fc789..0000000
--- a/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat-service.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: llama-2-7b-chat
-spec:
-  selector:
-    app: llama
-    statefulset.kubernetes.io/pod-name: llama-2-7b-chat-0
-  ports:
-    - protocol: TCP
-      port: 80
-      targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat.yaml b/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat.yaml
index af895fb..fb1babc 100644
--- a/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat.yaml
+++ b/presets/workspace/test/manifests/llama-2-7b-chat/llama-2-7b-chat.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
         - name: llama-container
-          image: REPO_HERE.azurecr.io/llama-2-7b-chat:TAG_HERE
+          image: REPO/llama-2-7b-chat:TAG
           command:
             - /bin/sh
             - -c
@@ -54,3 +54,18 @@ spec:
           operator: Exists
       nodeSelector:
         pool: llama27bchat
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: llama-2-7b-chat
+spec:
+  selector:
+    app: llama
+    statefulset.kubernetes.io/pod-name: llama-2-7b-chat-0
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 5000
+  type: ClusterIP
+  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-7b/llama-2-7b-service.yaml b/presets/workspace/test/manifests/llama-2-7b/llama-2-7b-service.yaml
deleted file mode 100644
index d8dfb84..0000000
--- a/presets/workspace/test/manifests/llama-2-7b/llama-2-7b-service.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: llama-2-7b
-spec:
-  selector:
-    app: llama
-    statefulset.kubernetes.io/pod-name: llama-2-7b-0
-  ports:
-    - protocol: TCP
-      port: 80
-      targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/llama-2-7b/llama-2-7b.yaml b/presets/workspace/test/manifests/llama-2-7b/llama-2-7b.yaml
index 265b3c1..1179d03 100644
--- a/presets/workspace/test/manifests/llama-2-7b/llama-2-7b.yaml
+++ b/presets/workspace/test/manifests/llama-2-7b/llama-2-7b.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
         - name: llama-container
-          image: REPO_HERE.azurecr.io/llama-2-7b:TAG_HERE
+          image: REPO/llama-2-7b:TAG
           command:
             - /bin/sh
             - -c
@@ -54,3 +54,18 @@ spec:
           operator: Exists
       nodeSelector:
         pool: llama27b
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: llama-2-7b
+spec:
+  selector:
+    app: llama
+    statefulset.kubernetes.io/pod-name: llama-2-7b-0
+  ports:
+    - protocol: TCP
+      port: 80
+      targetPort: 5000
+  type: ClusterIP
+  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct-service.yaml b/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct-service.yaml
deleted file mode 100644
index 9462774..0000000
--- a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: mistral-7b-instruct
-spec:
-  selector:
-    app: mistral
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_hf.yaml b/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_hf.yaml
deleted file mode 100644
index 7517968..0000000
--- a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: mistral-7b-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: mistral
-  template:
-    metadata:
-      labels:
-        app: mistral
-    spec:
-      containers:
-      - name: mistral-instruct-container
-        image: REPO_HERE.azurecr.io/mistral-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: mistral7bins
diff --git a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_vllm.yaml b/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_vllm.yaml
deleted file mode 100644
index 939d6c7..0000000
--- a/presets/workspace/test/manifests/mistral-7b-instruct/mistral-7b-instruct_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: mistral-7b-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: mistral
-  template:
-    metadata:
-      labels:
-        app: mistral
-    spec:
-      containers:
-      - name: mistral-instruct-container
-        image: REPO_HERE.azurecr.io/mistral-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --chat-template /workspace/chat_templates/mistral-instruct.jinja
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: mistral7bins
diff --git a/presets/workspace/test/manifests/mistral-7b/mistral-7b-service.yaml b/presets/workspace/test/manifests/mistral-7b/mistral-7b-service.yaml
deleted file mode 100644
index 90ba3ec..0000000
--- a/presets/workspace/test/manifests/mistral-7b/mistral-7b-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: mistral-7b
-spec:
-  selector:
-    app: mistral
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/mistral-7b/mistral-7b_hf.yaml b/presets/workspace/test/manifests/mistral-7b/mistral-7b_hf.yaml
deleted file mode 100644
index 3eff559..0000000
--- a/presets/workspace/test/manifests/mistral-7b/mistral-7b_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: mistral-7b
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: mistral
-  template:
-    metadata:
-      labels:
-        app: mistral
-    spec:
-      containers:
-      - name: mistral-container
-        image: REPO_HERE.azurecr.io/mistral-7b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: mistral7b
diff --git a/presets/workspace/test/manifests/mistral-7b/mistral-7b_vllm.yaml b/presets/workspace/test/manifests/mistral-7b/mistral-7b_vllm.yaml
deleted file mode 100644
index 2bd9453..0000000
--- a/presets/workspace/test/manifests/mistral-7b/mistral-7b_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: mistral-7b
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: mistral
-  template:
-    metadata:
-      labels:
-        app: mistral
-    spec:
-      containers:
-      - name: mistral-container
-        image: REPO_HERE.azurecr.io/mistral-7b:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --chat-template /workspace/chat_templates/mistral-instruct.jinja
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: mistral7b
diff --git a/presets/workspace/test/manifests/phi-2/phi-2-service.yaml b/presets/workspace/test/manifests/phi-2/phi-2-service.yaml
deleted file mode 100644
index d0f99f9..0000000
--- a/presets/workspace/test/manifests/phi-2/phi-2-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-2
-spec:
-  selector:
-    app: phi-2
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-2/phi-2_hf.yaml b/presets/workspace/test/manifests/phi-2/phi-2_hf.yaml
deleted file mode 100644
index cbc6f94..0000000
--- a/presets/workspace/test/manifests/phi-2/phi-2_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-2
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-2
-  template:
-    metadata:
-      labels:
-        app: phi-2
-    spec:
-      containers:
-      - name: phi-2-container
-        image: REPO_HERE.azurecr.io/phi-2:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi2
diff --git a/presets/workspace/test/manifests/phi-2/phi-2_vllm.yaml b/presets/workspace/test/manifests/phi-2/phi-2_vllm.yaml
deleted file mode 100644
index e77f212..0000000
--- a/presets/workspace/test/manifests/phi-2/phi-2_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-2
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-2
-  template:
-    metadata:
-      labels:
-        app: phi-2
-    spec:
-      containers:
-      - name: phi-2-container
-        image: REPO_HERE.azurecr.io/phi-2:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi2
diff --git a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct-service.yaml
deleted file mode 100644
index bab354e..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-medium-128k-instruct
-spec:
-  selector:
-    app: phi-3-medium-128k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_hf.yaml b/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_hf.yaml
deleted file mode 100644
index 0adb122..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-medium-128k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-medium-128k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-medium-128k-instruct
-    spec:
-      containers:
-      - name: phi-3-medium-128k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-medium-128k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3medium12
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_vllm.yaml b/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_vllm.yaml
deleted file mode 100644
index 5b93bde..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-128k-instruct/phi-3-medium-128k-instruct_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-medium-128k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-medium-128k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-medium-128k-instruct
-    spec:
-      containers:
-      - name: phi-3-medium-128k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-medium-128k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --max-model-len 1024 --tensor-parallel-size 2
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3medium12
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct-service.yaml
deleted file mode 100644
index 6071050..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-medium-4k-instruct
-spec:
-  selector:
-    app: phi-3-medium-4k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_hf.yaml b/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_hf.yaml
deleted file mode 100644
index 1d0d64e..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-medium-4k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-medium-4k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-medium-4k-instruct
-    spec:
-      containers:
-      - name: phi-3-medium-4k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-medium-4k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3medium4k
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_vllm.yaml b/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_vllm.yaml
deleted file mode 100644
index 3bdce80..0000000
--- a/presets/workspace/test/manifests/phi-3-medium-4k-instruct/phi-3-medium-4k-instruct_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-medium-4k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-medium-4k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-medium-4k-instruct
-    spec:
-      containers:
-      - name: phi-3-medium-4k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-medium-4k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --tensor-parallel-size 2
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3medium4k
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct-service.yaml
deleted file mode 100644
index ef86aef..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-mini-128k-instruct
-spec:
-  selector:
-    app: phi-3-mini-128k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_hf.yaml b/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_hf.yaml
deleted file mode 100644
index cf88980..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-mini-128k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-mini-128k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-mini-128k-instruct
-    spec:
-      containers:
-      - name: phi-3-mini-128k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-mini-128k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3mini128k
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_vllm.yaml b/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_vllm.yaml
deleted file mode 100644
index f719bf9..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-128k-instruct/phi-3-mini-128k-instruct_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-mini-128k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-mini-128k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-mini-128k-instruct
-    spec:
-      containers:
-      - name: phi-3-mini-128k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-mini-128k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3mini128k
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct-service.yaml
deleted file mode 100644
index 0063f24..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-mini-4k-instruct
-spec:
-  selector:
-    app: phi-3-mini-4k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_hf.yaml b/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_hf.yaml
deleted file mode 100644
index 1d7069a..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-mini-4k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-mini-4k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-mini-4k-instruct
-    spec:
-      containers:
-      - name: phi-3-mini-4k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-mini-4k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3mini4kin
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_vllm.yaml b/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_vllm.yaml
deleted file mode 100644
index 8d12756..0000000
--- a/presets/workspace/test/manifests/phi-3-mini-4k-instruct/phi-3-mini-4k-instruct_vllm.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-mini-4k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-mini-4k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-mini-4k-instruct
-    spec:
-      containers:
-      - name: phi-3-mini-4k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-mini-4k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3mini4kin
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct-service.yaml
deleted file mode 100644
index a28bac0..0000000
--- a/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-small-128k-instruct
-spec:
-  selector:
-    app: phi-3-small-128k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct.yaml b/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct.yaml
deleted file mode 100644
index 1827155..0000000
--- a/presets/workspace/test/manifests/phi-3-small-128k-instruct/phi-3-small-128k-instruct.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-small-128k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-small-128k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-small-128k-instruct
-    spec:
-      containers:
-      - name: phi-3-small-128k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-small-128k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3small128
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct-service.yaml b/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct-service.yaml
deleted file mode 100644
index 17e031f..0000000
--- a/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: phi-3-small-8k-instruct
-spec:
-  selector:
-    app: phi-3-small-8k-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct.yaml b/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct.yaml
deleted file mode 100644
index 1f515cc..0000000
--- a/presets/workspace/test/manifests/phi-3-small-8k-instruct/phi-3-small-8k-instruct.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: phi-3-small-8k-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: phi-3-small-8k-instruct
-  template:
-    metadata:
-      labels:
-        app: phi-3-small-8k-instruct
-    spec:
-      containers:
-      - name: phi-3-small-8k-instruct-container
-        image: REPO_HERE.azurecr.io/phi-3-small-8k-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 1
-          limits:
-            nvidia.com/gpu: 1  # Requesting 1 GPU
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: phi3small8ki
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct-service.yaml b/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct-service.yaml
deleted file mode 100644
index 73637c9..0000000
--- a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct-service.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
-  name: qwen2-5-coder-7b-instruct
-spec:
-  selector:
-    app: qwen2-5-coder-7b-instruct
-  ports:
-  - protocol: TCP
-    port: 80
-    targetPort: 5000
-  type: ClusterIP
-  publishNotReadyAddresses: true
diff --git a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_hf.yaml b/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_hf.yaml
deleted file mode 100644
index e92d906..0000000
--- a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_hf.yaml
+++ /dev/null
@@ -1,55 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: qwen2-5-coder-7b-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: qwen2-5-coder-7b-instruct
-  template:
-    metadata:
-      labels:
-        app: qwen2-5-coder-7b-instruct
-    spec:
-      containers:
-      - name: qwen2-5-coder-7b-instruct-container
-        image: REPO_HERE.azurecr.io/qwen2.5-coder-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype auto --trust_remote_code
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: qwen25coder7
\ No newline at end of file
diff --git a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_vllm.yaml b/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_vllm.yaml
deleted file mode 100644
index 4c1e725..0000000
--- a/presets/workspace/test/manifests/qwen2-5-coder-7b-instruct/qwen2-5-coder-7b-instruct_vllm.yaml
+++ /dev/null
@@ -1,83 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: qwen2-5-coder-7b-instruct
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: qwen2-5-coder-7b-instruct
-  template:
-    metadata:
-      labels:
-        app: qwen2-5-coder-7b-instruct
-    spec:
-      containers:
-      - name: qwen2-5-coder-7b-instruct-container
-        image: REPO_HERE.azurecr.io/qwen2.5-coder-7b-instruct:TAG_HERE
-        command:
-          - /bin/sh
-          - -c
-          - python3 /workspace/vllm/inference_api.py --kaito-config-file /mnt/config/inference_config.yaml
-        resources:
-          requests:
-            nvidia.com/gpu: 2
-          limits:
-            nvidia.com/gpu: 2
-        livenessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 600 # 10 Min
-          periodSeconds: 10
-        readinessProbe:
-          httpGet:
-            path: /health
-            port: 5000
-          initialDelaySeconds: 30
-          periodSeconds: 10
-        volumeMounts:
-        - name: dshm
-          mountPath: /dev/shm
-        - mountPath: /mnt/config
-          name: config-volume
-      volumes:
-      - name: dshm
-        emptyDir:
-          medium: Memory
-      - configMap:
-          defaultMode: 420
-          name: qwen2-5-coder-7b-inference-params
-        name: config-volume
-      tolerations:
-      - effect: NoSchedule
-        key: sku
-        operator: Equal
-        value: gpu
-      - effect: NoSchedule
-        key: nvidia.com/gpu
-        operator: Exists
-      nodeSelector:
-        pool: qwen25coder7
----
-apiVersion: v1
-kind: ConfigMap
-metadata:
-  name: qwen2-5-coder-7b-inference-params
-data:
-  inference_config.yaml: |
-    # Maximum number of steps to find the max available seq len fitting in the GPU memory.
-    max_probe_steps: 6
-
-    vllm:
-      cpu-offload-gb: 0
-      gpu-memory-utilization: 0.95
-      swap-space: 4
-      served-model-name: test
-      dtype: float16
-      tensor-parallel-size: 2
-
-      # max-seq-len-to-capture: 8192
-      # num-scheduler-steps: 1
-      # enable-chunked-prefill: false
-      # see https://docs.vllm.ai/en/stable/models/engine_args.html for more options.
diff --git a/presets/workspace/test/manifests/tuning/falcon/falcon-7b.yaml b/presets/workspace/test/manifests/tuning/falcon/falcon-7b.yaml
deleted file mode 100644
index 7852bf0..0000000
--- a/presets/workspace/test/manifests/tuning/falcon/falcon-7b.yaml
+++ /dev/null
@@ -1,103 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
-  name: falcon-7b-tuning
-spec:
-  replicas: 1
-  selector:
-    matchLabels:
-      app: falcon
-  template:
-    metadata:
-      labels:
-        app: falcon
-    spec:
-      containers:
-        - name: falcon-container
-          image: aimodelsregistrytest.azurecr.io/tuning-falcon-7b:0.0.1
-          command: ["/bin/sh", "-c", "sleep infinity"]
-          resources:
-            requests:
-              nvidia.com/gpu: 2
-            limits:
-              nvidia.com/gpu: 2  # Requesting 2 GPUs
-          volumeMounts:
-            - name: dshm
-              mountPath: /dev/shm
-            - name: workspace
-              mountPath: /workspace
-
-        - name: docker-sidecar
-          image: docker:dind
-          securityContext:
-            privileged: true  # Allows container to manage its own containers
-          volumeMounts:
-            - name: workspace
-              mountPath: /workspace
-          env:
-            - name: ACR_USERNAME
-              value: "{{ACR_USERNAME}}"
-            - name: ACR_PASSWORD
-              value: "{{ACR_PASSWORD}}"
-            - name: TAG
-              value: "{{TAG}}"
-          command: ["/bin/sh"]
-          args:
-            - -c
-            - |
-              # Start the Docker daemon in the background with specific options for DinD
-              dockerd &
-              # Wait for the Docker daemon to be ready
-              while ! docker info > /dev/null 2>&1; do
-                echo "Waiting for Docker daemon to start..."
-                sleep 1
-              done
-              echo 'Docker daemon started'
-
-              while true; do
-                FILE_PATH=$(find /workspace/tfs -name 'fine_tuning_completed.txt')
-                if [ ! -z "$FILE_PATH" ]; then
-                  echo "FOUND TRAINING COMPLETED FILE at $FILE_PATH"
-
-                  PARENT_DIR=$(dirname "$FILE_PATH")
-                  echo "Parent directory is $PARENT_DIR"
-
-                  TEMP_CONTEXT=$(mktemp -d)
-                  cp "$PARENT_DIR/adapter_config.json" "$TEMP_CONTEXT/adapter_config.json"
-                  cp -r "$PARENT_DIR/adapter_model.safetensors" "$TEMP_CONTEXT/adapter_model.safetensors"
-
-                  # Create a minimal Dockerfile
-                  echo 'FROM scratch
-                  ADD adapter_config.json /
-                  ADD adapter_model.safetensors /' > "$TEMP_CONTEXT/Dockerfile"
-              
-                  # Login to Docker registry
-                  echo $ACR_PASSWORD | docker login $ACR_USERNAME.azurecr.io -u $ACR_USERNAME --password-stdin
-
-                  docker build -t $ACR_USERNAME.azurecr.io/adapter-falcon-7b:$TAG "$TEMP_CONTEXT"
-                  docker push $ACR_USERNAME.azurecr.io/adapter-falcon-7b:$TAG
-
-                  # Cleanup: Remove the temporary directory
-                  rm -rf "$TEMP_CONTEXT"
-
-                  # Remove the file to prevent repeated builds, or handle as needed
-                  # rm "$FILE_PATH"
-                fi
-                sleep 10  # Check every 10 seconds
-              done
-
-      volumes:
-        - name: dshm
-          emptyDir:
-            medium: Memory
-        - name: workspace
-          emptyDir: {}
-
-      tolerations:
-        - effect: NoSchedule
-          key: sku
-          operator: Equal
-          value: gpu
-        - effect: NoSchedule
-          key: nvidia.com/gpu
-          operator: Exists
diff --git a/presets/workspace/test/tuning/tuning-job.yaml b/presets/workspace/test/manifests/tuning/tuning.yaml
similarity index 100%
rename from presets/workspace/test/tuning/tuning-job.yaml
rename to presets/workspace/test/manifests/tuning/tuning.yaml
diff --git a/presets/workspace/test/scripts/README.md b/presets/workspace/test/scripts/README.md
new file mode 100644
index 0000000..aaa49ca
--- /dev/null
+++ b/presets/workspace/test/scripts/README.md
@@ -0,0 +1,82 @@
+# Manifest Generation Script
+
+This script generates Kubernetes manifests for different model deployments based on templates and configurations.
+
+## Overview
+
+The script (`generate_manifests.py`) combines:
+- Base templates from `presets/workspace/test/manifests/inference-tmpl/manifest.yaml`
+- Model configurations from `.github/e2e-preset-configs.json`
+- Predefined manifests from `presets/workspace/test/manifests/<model_name>/<model_name>.yaml`
+to generate deployment/statefulset and service manifests for each model.
+
+## Usage
+
+```bash
+# Print manifests to stdout
+python generate_manifests.py <model_name> <runtime> [--repo REPO] [--tag TAG]
+```
+
+### Parameters:
+- `model_name`: Name of the model (e.g., "mistral-7b", "falcon-40b")
+- `runtime`: Runtime to use ("hf" for Hugging Face or "vllm" for vLLM)
+- `--repo`: (Optional) Repository name to replace REPO in the template
+- `--tag`: (Optional) Tag to replace TAG in the template
+
+### Example:
+```bash
+# Generate manifest for Mistral 7B with vLLM runtime
+python generate_manifests.py mistral-7b vllm
+
+# Generate manifest for Falcon 40B with Hugging Face runtime and custom repo/tag
+python generate_manifests.py falcon-40b hf --repo myregistry.azurecr.io --tag v1.0.0
+```
+
+## Configuration Files
+
+### Template (presets/workspace/test/manifests/inference-tmpl/manifest.yaml)
+Contains base templates for:
+- Deployment
+- Service
+- ConfigMap
+
+Placeholders:
+- MODEL_NAME
+- RUNTIME_COMMAND
+- GPU_COUNT
+- NODE_POOL
+- NODE_COUNT
+- REPO (can be overridden with --repo flag)
+- TAG (can be overridden with --tag flag)
+
+### Model Configurations
+Located at `.github/e2e-preset-configs.json`
+- Contains configurations for all supported models
+- Each model configuration includes:
+  - Basic info (name, node count, VM size, etc.)
+  - Node pool specification
+  - Runtime-specific configurations
+
+## Example Configuration
+
+```json
+{
+  "name": "mistral-7b",
+  "node-count": 1,
+  "node-vm-size": "Standard_NC6s_v3",
+  "node-osdisk-size": 100,
+  "OSS": true,
+  "loads_adapter": false,
+  "node_pool": "mistral7b",
+  "runtimes": {
+    "hf": {
+      "command": "accelerate launch --num_processes 1 --num_machines 1 --machine_rank 0 --gpu_ids all /workspace/tfs/inference_api.py --pipeline text-generation --torch_dtype bfloat16",
+      "gpu_count": 1
+    },
+    "vllm": {
+      "command": "python3 /workspace/vllm/inference_api.py --served-model-name test --dtype float16 --chat-template /workspace/chat_templates/mistral-instruct.jinja",
+      "gpu_count": 1
+    }
+  }
+}
+```
diff --git a/presets/workspace/test/scripts/generate_manifests.py b/presets/workspace/test/scripts/generate_manifests.py
new file mode 100644
index 0000000..defc2d8
--- /dev/null
+++ b/presets/workspace/test/scripts/generate_manifests.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+import json
+import yaml
+import sys
+import os
+import argparse
+from pathlib import Path
+
+def get_project_root():
+    # Get the directory containing the current script
+    current_dir = Path(__file__).resolve().parent
+    # Go up 4 levels to reach project root (from presets/workspace/test/scripts)
+    return current_dir.parents[3]
+
+def load_json_config():
+    project_root = get_project_root()
+    config_path = project_root / '.github' / 'e2e-preset-configs.json'
+    with open(config_path, 'r') as f:
+        data = json.load(f)
+        return data['matrix']['image']  # Return the array of model configs
+
+def load_template():
+    project_root = get_project_root()
+    template_path = project_root / 'presets' / 'workspace' / 'test' / 'manifests' / 'inference-tmpl' / 'manifest.yaml'
+    with open(template_path, 'r') as f:
+        return yaml.safe_load(f)
+
+def check_predefined_manifest(model_name):
+    project_root = get_project_root()
+    manifest_path = project_root / 'presets' / 'workspace' / 'test' / 'manifests' / f'{model_name}' / f'{model_name}.yaml'
+    if not os.path.exists(manifest_path):
+        return (None, False)
+    with open(manifest_path, 'r') as f:
+        return (f.read(), True)
+
+def process_model(model_name, runtime, repo=None, tag=None):
+    configs = load_json_config()
+    model_config = next((m for m in configs if m['name'] == model_name), None)
+
+    if not model_config:
+        print(f"Model {model_name} not found in configs", file=sys.stderr)
+        sys.exit(1)
+
+    predefined_manifest, predefined_manifest_exists = check_predefined_manifest(model_name)
+    if predefined_manifest_exists:
+        return process_predefined_manifest(model_name, runtime, predefined_manifest, repo, tag)
+
+    if runtime not in model_config.get('runtimes', {}):
+        print(f"Runtime {runtime} not configured for model {model_name}", file=sys.stderr)
+        sys.exit(1)
+
+    runtime_config = model_config['runtimes'][runtime]
+    workload_name = model_config.get('workload', model_name)
+
+    templates = load_template()
+    manifest_str = yaml.dump(templates['deployment'])
+
+    # Replace placeholders in template
+    manifest_str = (
+        manifest_str
+        .replace('WORKLOAD_NAME', workload_name)
+        .replace('MODEL_NAME', model_name)
+        .replace('RUNTIME_COMMAND', runtime_config['command'])
+        .replace('GPU_COUNT', str(runtime_config['gpu_count']))
+        .replace('NODE_POOL', model_config['node_pool'])
+        .replace('NODE_COUNT', str(model_config['node-count']))
+    )
+
+    # Replace repo and tag if provided
+    if repo:
+        manifest_str = manifest_str.replace('REPO', repo)
+    if tag:
+        manifest_str = manifest_str.replace('TAG', tag)
+
+    # Parse the template string back into YAML
+    manifest = yaml.safe_load(manifest_str)
+
+    # Generate service manifest
+    service_template = templates['service']
+    service_str = yaml.dump(service_template)
+    service_str = service_str.replace('WORKLOAD_NAME', workload_name)
+    service_manifest = yaml.safe_load(service_str)
+
+    # Generate config manifest
+    config_template = templates['config']
+    config_str = yaml.dump(config_template)
+    config_manifest = yaml.safe_load(config_str)
+
+    # Print manifests to stdout
+    yaml.dump(manifest, sys.stdout, default_flow_style=False)
+    print('---')  # Document separator
+    yaml.dump(service_manifest, sys.stdout, default_flow_style=False)
+    print('---')  # Document separator
+    yaml.dump(config_manifest, sys.stdout, default_flow_style=False)
+
+def process_predefined_manifest(model_name, runtime, predefined_manifest, repo=None, tag=None):
+    if repo:
+        predefined_manifest = predefined_manifest.replace('REPO', repo)
+    if tag:
+        predefined_manifest = predefined_manifest.replace('TAG', tag)
+
+    print(predefined_manifest)
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description='Process model template with optional repo and tag.')
+    parser.add_argument('model_name', help='Name of the model')
+    parser.add_argument('runtime', help='Runtime to use')
+    parser.add_argument('--repo', help='Repository name to use instead of REPO')
+    parser.add_argument('--tag', help='Tag to use instead of TAG')
+
+    args = parser.parse_args()
+    process_model(args.model_name, args.runtime, args.repo, args.tag)
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index 0facad6..0fb754b 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -94,6 +94,14 @@ var _ = BeforeSuite(func() {
 		},
 	})
 	Expect(err).NotTo(HaveOccurred())
+
+	loadTestEnvVars()
+
+	err = copySecretToNamespace(aiModelsRegistrySecret, namespaceName)
+	Expect(err).NotTo(HaveOccurred())
+
+	err = copySecretToNamespace(e2eACRSecret, namespaceName)
+	Expect(err).NotTo(HaveOccurred())
 })
 
 var _ = AfterSuite(func() {
diff --git a/test/e2e/inference_with_adapters_test.go b/test/e2e/inference_with_adapters_test.go
index 11e2632..7fd33f4 100644
--- a/test/e2e/inference_with_adapters_test.go
+++ b/test/e2e/inference_with_adapters_test.go
@@ -16,7 +16,8 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/workspace/image"
 	"github.com/kaito-project/kaito/test/e2e/utils"
 )
 
@@ -27,22 +28,22 @@ var fullImageName1 = utils.GetEnv("E2E_ACR_REGISTRY") + "/" + imageName1 + ":0.0
 var imageName2 = "e2e-adapter2"
 var fullImageName2 = utils.GetEnv("E2E_ACR_REGISTRY") + "/" + imageName2 + ":0.0.1"
 
-var validAdapters1 = []kaitov1alpha1.AdapterSpec{
+var validAdapters1 = []kaitov1beta1.AdapterSpec{
 	{
-		Source: &kaitov1alpha1.DataSource{
+		Source: &kaitov1beta1.DataSource{
 			Name:  imageName1,
 			Image: fullImageName1,
 			ImagePullSecrets: []string{
-				utils.GetEnv("AI_MODELS_REGISTRY_SECRET"),
+				utils.GetEnv("E2E_ACR_REGISTRY_SECRET"),
 			},
 		},
 		Strength: &DefaultStrength,
 	},
 }
 
-var validAdapters2 = []kaitov1alpha1.AdapterSpec{
+var validAdapters2 = []kaitov1beta1.AdapterSpec{
 	{
-		Source: &kaitov1alpha1.DataSource{
+		Source: &kaitov1beta1.DataSource{
 			Name:  imageName2,
 			Image: fullImageName2,
 			ImagePullSecrets: []string{
@@ -53,21 +54,23 @@ var validAdapters2 = []kaitov1alpha1.AdapterSpec{
 	},
 }
 
+var baseInitContainer = image.NewPullerContainer("", "")
+
 var expectedInitContainers1 = []corev1.Container{
 	{
-		Name:  imageName1,
-		Image: fullImageName1,
+		Name:  baseInitContainer.Name + "-" + imageName1,
+		Image: baseInitContainer.Image,
 	},
 }
 
 var expectedInitContainers2 = []corev1.Container{
 	{
-		Name:  imageName2,
-		Image: fullImageName2,
+		Name:  baseInitContainer.Name + "-" + imageName2,
+		Image: baseInitContainer.Image,
 	},
 }
 
-func validateInitContainers(workspaceObj *kaitov1alpha1.Workspace, expectedInitContainers []corev1.Container) {
+func validateInitContainers(workspaceObj *kaitov1beta1.Workspace, expectedInitContainers []corev1.Container) {
 	By("Checking the InitContainers", func() {
 		Eventually(func() bool {
 			var err error
@@ -101,7 +104,7 @@ func validateInitContainers(workspaceObj *kaitov1alpha1.Workspace, expectedInitC
 	})
 }
 
-func validateImagePullSecrets(workspaceObj *kaitov1alpha1.Workspace, expectedImagePullSecrets []string) {
+func validateImagePullSecrets(workspaceObj *kaitov1beta1.Workspace, expectedImagePullSecrets []string) {
 	By("Checking the ImagePullSecrets", func() {
 		Eventually(func() bool {
 			var err error
@@ -130,7 +133,7 @@ func validateImagePullSecrets(workspaceObj *kaitov1alpha1.Workspace, expectedIma
 	})
 }
 
-func validateAdapterAdded(workspaceObj *kaitov1alpha1.Workspace, deploymentName string, adapterName string) {
+func validateAdapterAdded(workspaceObj *kaitov1beta1.Workspace, deploymentName string, adapterName string) {
 	By("Checking the Adapters", func() {
 		Eventually(func() bool {
 			coreClient, err := utils.GetK8sClientset()
@@ -160,7 +163,8 @@ func validateAdapterAdded(workspaceObj *kaitov1alpha1.Workspace, deploymentName
 	})
 }
 
-func validateAdapterLoadedInVLLM(workspaceObj *kaitov1alpha1.Workspace, deploymentName string, adapterName string) {
+func validateAdapterLoadedInVLLM(workspaceObj *kaitov1beta1.Workspace, adapterName string) {
+	deploymentName := workspaceObj.Name
 	execOption := corev1.PodExecOptions{
 		Command:   []string{"bash", "-c", "apt-get update && apt-get install curl -y; curl -s 127.0.0.1:5000/v1/models | grep " + adapterName},
 		Container: deploymentName,
@@ -216,7 +220,7 @@ var _ = Describe("Workspace Preset", func() {
 		}
 	})
 
-	It("should create a falcon workspace with adapter, and update the workspace with another adapter", func() {
+	It("should create a falcon workspace with adapter, and update the workspace with another adapter", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		workspaceObj := createCustomWorkspaceWithAdapter(numOfNode, validAdapters1)
 
diff --git a/test/e2e/preset_test.go b/test/e2e/preset_test.go
index 17ebe05..48d2b9c 100644
--- a/test/e2e/preset_test.go
+++ b/test/e2e/preset_test.go
@@ -4,8 +4,8 @@
 package e2e
 
 import (
+	"context"
 	"fmt"
-	"log"
 	"math/rand"
 	"os"
 	"strconv"
@@ -17,25 +17,28 @@ import (
 	"github.com/samber/lo"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/test/e2e/utils"
 )
 
 const (
-	PresetLlama2AChat            = "llama-2-7b-chat"
-	PresetLlama2BChat            = "llama-2-13b-chat"
-	PresetFalcon7BModel          = "falcon-7b"
-	PresetFalcon40BModel         = "falcon-40b"
-	PresetMistral7BInstructModel = "mistral-7b-instruct"
-	PresetQwen2_5Coder7BModel    = "qwen2.5-coder-7b-instruct"
-	PresetPhi2Model              = "phi-2"
-	PresetPhi3Mini128kModel      = "phi-3-mini-128k-instruct"
-	WorkspaceHashAnnotation      = "workspace.kaito.io/hash"
+	PresetLlama2AChat                   = "llama-2-7b-chat"
+	PresetLlama2BChat                   = "llama-2-13b-chat"
+	PresetFalcon7BModel                 = "falcon-7b"
+	PresetFalcon40BModel                = "falcon-40b"
+	PresetMistral7BInstructModel        = "mistral-7b-instruct"
+	PresetQwen2_5Coder7BModel           = "qwen2.5-coder-7b-instruct"
+	PresetPhi2Model                     = "phi-2"
+	PresetPhi3Mini128kModel             = "phi-3-mini-128k-instruct"
+	PresetDeepSeekR1DistillLlama8BModel = "deepseek-r1-distill-llama-8b"
+	PresetDeepSeekR1DistillQwen14BModel = "deepseek-r1-distill-qwen-14b"
+	WorkspaceHashAnnotation             = "workspace.kaito.io/hash"
 	// WorkspaceRevisionAnnotation represents the revision number of the workload managed by the workspace
 	WorkspaceRevisionAnnotation = "workspace.kaito.io/revision"
 )
@@ -79,21 +82,21 @@ func loadModelVersions() {
 	}
 }
 
-func createCustomWorkspaceWithAdapter(numOfNode int, validAdapters []kaitov1alpha1.AdapterSpec) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createCustomWorkspaceWithAdapter(numOfNode int, validAdapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace with adapter", func() {
 		uniqueID := fmt.Sprint("preset-falcon-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "", numOfNode, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "custom-preset-e2e-test-falcon"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, validAdapters)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, validAdapters)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func updateCustomWorkspaceWithAdapter(workspaceObj *kaitov1alpha1.Workspace, validAdapters []kaitov1alpha1.AdapterSpec) *kaitov1alpha1.Workspace {
+func updateCustomWorkspaceWithAdapter(workspaceObj *kaitov1beta1.Workspace, validAdapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
 	By("Updating a workspace with adapter", func() {
 		workspaceObj.Inference.Adapters = validAdapters
 
@@ -115,78 +118,78 @@ func updateCustomWorkspaceWithAdapter(workspaceObj *kaitov1alpha1.Workspace, val
 	return workspaceObj
 }
 
-func createFalconWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createFalconWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Falcon 7B preset public mode", func() {
 		uniqueID := fmt.Sprint("preset-falcon-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "", numOfNode, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-falcon"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createMistralWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createMistralWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Mistral 7B preset public mode", func() {
 		uniqueID := fmt.Sprint("preset-mistral-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "", numOfNode, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-mistral"},
-			}, nil, PresetMistral7BInstructModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetMistral7BInstructModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createPhi2WorkspaceWithPresetPublicMode(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createPhi2WorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Phi 2 preset public mode", func() {
 		uniqueID := fmt.Sprint("preset-phi2-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-phi-2"},
-			}, nil, PresetPhi2Model, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetPhi2Model, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createLlama7BWorkspaceWithPresetPrivateMode(registry, registrySecret, imageVersion string, numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createLlama7BWorkspaceWithPresetPrivateMode(registry, registrySecret, imageVersion string, numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Llama 7B Chat preset private mode", func() {
 		uniqueID := fmt.Sprint("preset-llama-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, fmt.Sprintf("%s/%s:%s", registry, PresetLlama2AChat, imageVersion),
 			numOfNode, "Standard_NC12s_v3", &metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "private-preset-e2e-test-llama-2-7b"},
-			}, nil, PresetLlama2AChat, kaitov1alpha1.ModelImageAccessModePrivate, []string{registrySecret}, nil, nil)
+			}, nil, PresetLlama2AChat, kaitov1beta1.ModelImageAccessModePrivate, []string{registrySecret}, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createLlama13BWorkspaceWithPresetPrivateMode(registry, registrySecret, imageVersion string, numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createLlama13BWorkspaceWithPresetPrivateMode(registry, registrySecret, imageVersion string, numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Llama 13B Chat preset private mode", func() {
 		uniqueID := fmt.Sprint("preset-llama-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, fmt.Sprintf("%s/%s:%s", registry, PresetLlama2BChat, imageVersion),
 			numOfNode, "Standard_NC12s_v3", &metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "private-preset-e2e-test-llama-2-13b"},
-			}, nil, PresetLlama2BChat, kaitov1alpha1.ModelImageAccessModePrivate, []string{registrySecret}, nil, nil)
+			}, nil, PresetLlama2BChat, kaitov1beta1.ModelImageAccessModePrivate, []string{registrySecret}, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createCustomWorkspaceWithPresetCustomMode(imageName string, numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createCustomWorkspaceWithPresetCustomMode(imageName string, numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with custom workspace mode", func() {
 		uniqueID := fmt.Sprint("preset-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "",
@@ -199,14 +202,14 @@ func createCustomWorkspaceWithPresetCustomMode(imageName string, numOfNode int)
 	return workspaceObj
 }
 
-func createPhi3WorkspaceWithPresetPublicMode(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createPhi3WorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Phi-3-mini-128k-instruct preset public mode", func() {
 		uniqueID := fmt.Sprint("preset-phi3-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "",
 			numOfNode, "Standard_NC6s_v3", &metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-phi-3-mini-128k-instruct"},
-			}, nil, PresetPhi3Mini128kModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetPhi3Mini128kModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
@@ -240,8 +243,8 @@ func createAndValidateConfigMap(configMap *v1.ConfigMap) {
 	})
 }
 
-func createPhi3TuningWorkspaceWithPresetPublicMode(configMapName string, numOfNode int) (*kaitov1alpha1.Workspace, string, string) {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createPhi3TuningWorkspaceWithPresetPublicMode(configMapName string, numOfNode int) (*kaitov1beta1.Workspace, string, string) {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	e2eOutputImageName := fmt.Sprintf("adapter-%s-e2e-test", PresetPhi3Mini128kModel)
 	e2eOutputImageTag := utils.GenerateRandomString()
 	outputRegistryUrl := fmt.Sprintf("%s.azurecr.io/%s:%s", azureClusterName, e2eOutputImageName, e2eOutputImageTag)
@@ -251,14 +254,14 @@ func createPhi3TuningWorkspaceWithPresetPublicMode(configMapName string, numOfNo
 		workspaceObj = utils.GenerateE2ETuningWorkspaceManifest(uniqueID, namespaceName, "",
 			fullDatasetImageName1, outputRegistryUrl, numOfNode, "Standard_NC6s_v3", &metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-tuning-falcon"},
-			}, nil, PresetPhi3Mini128kModel, kaitov1alpha1.ModelImageAccessModePublic, []string{e2eACRSecret}, configMapName)
+			}, nil, PresetPhi3Mini128kModel, kaitov1beta1.ModelImageAccessModePublic, []string{e2eACRSecret}, configMapName)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj, uniqueID, outputRegistryUrl
 }
 
-func createAndValidateWorkspace(workspaceObj *kaitov1alpha1.Workspace) {
+func createAndValidateWorkspace(workspaceObj *kaitov1beta1.Workspace) {
 	By("Creating workspace", func() {
 		Eventually(func() error {
 			return utils.TestingCluster.KubeClient.Create(ctx, workspaceObj, &client.CreateOptions{})
@@ -275,15 +278,15 @@ func createAndValidateWorkspace(workspaceObj *kaitov1alpha1.Workspace) {
 	})
 }
 
-func updatePhi3TuningWorkspaceWithPresetPublicMode(workspaceObj *kaitov1alpha1.Workspace, datasetImageName string) (*kaitov1alpha1.Workspace, string) {
+func updatePhi3TuningWorkspaceWithPresetPublicMode(workspaceObj *kaitov1beta1.Workspace, datasetImageName string) (*kaitov1beta1.Workspace, string) {
 	e2eOutputImageName := fmt.Sprintf("adapter-%s-e2e-test2", PresetPhi3Mini128kModel)
 	e2eOutputImageTag := utils.GenerateRandomString()
 	outputRegistryUrl := fmt.Sprintf("%s.azurecr.io/%s:%s", azureClusterName, e2eOutputImageName, e2eOutputImageTag)
 	By("Updating a workspace Tuning CR with Phi-3 preset public mode. The update includes the tuning input and output configurations for the workspace.", func() {
-		workspaceObj.Tuning.Input = &kaitov1alpha1.DataSource{
+		workspaceObj.Tuning.Input = &kaitov1beta1.DataSource{
 			Image: datasetImageName,
 		}
-		workspaceObj.Tuning.Output = &kaitov1alpha1.DataDestination{
+		workspaceObj.Tuning.Output = &kaitov1beta1.DataDestination{
 			Image:           outputRegistryUrl,
 			ImagePushSecret: e2eACRSecret,
 		}
@@ -292,7 +295,7 @@ func updatePhi3TuningWorkspaceWithPresetPublicMode(workspaceObj *kaitov1alpha1.W
 	return workspaceObj, outputRegistryUrl
 }
 
-func updateAndValidateWorkspace(workspaceObj *kaitov1alpha1.Workspace) {
+func updateAndValidateWorkspace(workspaceObj *kaitov1beta1.Workspace) {
 	By("Creating workspace", func() {
 		Eventually(func() error {
 			return utils.TestingCluster.KubeClient.Update(ctx, workspaceObj)
@@ -335,7 +338,7 @@ func copySecretToNamespace(secretName, targetNamespace string) error {
 }
 
 // validateResourceStatus validates resource status
-func validateResourceStatus(workspaceObj *kaitov1alpha1.Workspace) {
+func validateResourceStatus(workspaceObj *kaitov1beta1.Workspace) {
 	By("Checking the resource status", func() {
 		Eventually(func() bool {
 			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
@@ -348,7 +351,7 @@ func validateResourceStatus(workspaceObj *kaitov1alpha1.Workspace) {
 			}
 
 			_, conditionFound := lo.Find(workspaceObj.Status.Conditions, func(condition metav1.Condition) bool {
-				return condition.Type == string(kaitov1alpha1.ConditionTypeResourceStatus) &&
+				return condition.Type == string(kaitov1beta1.ConditionTypeResourceStatus) &&
 					condition.Status == metav1.ConditionTrue
 			})
 			return conditionFound
@@ -356,7 +359,7 @@ func validateResourceStatus(workspaceObj *kaitov1alpha1.Workspace) {
 	})
 }
 
-func validateAssociatedService(workspaceObj *kaitov1alpha1.Workspace) {
+func validateAssociatedService(workspaceObj *kaitov1beta1.Workspace) {
 	serviceName := workspaceObj.Name
 	serviceNamespace := workspaceObj.Namespace
 
@@ -385,7 +388,7 @@ func validateAssociatedService(workspaceObj *kaitov1alpha1.Workspace) {
 }
 
 // validateInferenceResource validates inference deployment
-func validateInferenceResource(workspaceObj *kaitov1alpha1.Workspace, expectedReplicas int32, isStatefulSet bool) {
+func validateInferenceResource(workspaceObj *kaitov1beta1.Workspace, expectedReplicas int32, isStatefulSet bool) {
 	By("Checking the inference resource", func() {
 		Eventually(func() bool {
 			var err error
@@ -433,7 +436,7 @@ func validateInferenceResource(workspaceObj *kaitov1alpha1.Workspace, expectedRe
 }
 
 // validateRevision validates the annotations of the workspace and the workload, as well as the corresponding controller revision
-func validateRevision(workspaceObj *kaitov1alpha1.Workspace, revisionStr string) {
+func validateRevision(workspaceObj *kaitov1beta1.Workspace, revisionStr string) {
 	By("Checking the revisions of the resources", func() {
 		Eventually(func() bool {
 			var isWorkloadAnnotationCorrect bool
@@ -483,7 +486,7 @@ func validateRevision(workspaceObj *kaitov1alpha1.Workspace, revisionStr string)
 }
 
 // validateTuningResource validates tuning deployment
-func validateTuningResource(workspaceObj *kaitov1alpha1.Workspace) {
+func validateTuningResource(workspaceObj *kaitov1beta1.Workspace) {
 	By("Checking the tuning resource", func() {
 		Eventually(func() bool {
 			var err error
@@ -522,70 +525,81 @@ func validateTuningResource(workspaceObj *kaitov1alpha1.Workspace) {
 	})
 }
 
-func validateTuningJobInputOutput(workspaceObj *kaitov1alpha1.Workspace, inputImage string, output string) {
+func validateTuningJobInputOutput(workspaceObj *kaitov1beta1.Workspace, inputImage string, outputImage string) {
 	By("Checking the tuning input and output", func() {
 		Eventually(func() bool {
-			var err error
-
-			job := &batchv1.Job{}
-			err = utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
-				Namespace: workspaceObj.Namespace,
-				Name:      workspaceObj.Name,
-			}, job)
+			var job batchv1.Job
+			if err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKeyFromObject(workspaceObj), &job); err != nil {
+				if client.IgnoreNotFound(err) == nil {
+					GinkgoWriter.Printf("Job not found: %v\n", err)
+					return false
+				}
 
-			if err != nil {
-				GinkgoWriter.Printf("Error fetching resource: %v\n", err)
-				return false
+				Expect(err).NotTo(HaveOccurred())
 			}
 
-			image := job.Spec.Template.Spec.InitContainers[0].Image
-
-			expectedString1 := "docker build -t " + output
-			expectedString2 := "if docker push " + output + "; then"
+			var pullerContainer *v1.Container
+			for _, container := range job.Spec.Template.Spec.InitContainers {
+				if strings.HasPrefix(container.Name, "puller") {
+					pullerContainer = &container
+					break
+				}
+			}
 
-			var sidecarContainer v1.Container
+			pullerSH := pullerContainer.Args[0]
+			if !strings.Contains(pullerSH, fmt.Sprintf("\n[ ! -z \"${IMG_REF}\" ] || IMG_REF='%s'\n", inputImage)) {
+				GinkgoWriter.Printf("Unexpected pullerSH: %s\n", pullerSH)
+				return false
+			}
 
+			var pusherContainer *v1.Container
 			for _, container := range job.Spec.Template.Spec.Containers {
-				if container.Name == "docker-sidecar" {
-					sidecarContainer = container
+				if strings.HasPrefix(container.Name, "pusher") {
+					pusherContainer = &container
 					break
 				}
 			}
-			return image == inputImage && strings.Contains(sidecarContainer.Args[0], expectedString1) && strings.Contains(sidecarContainer.Args[0], expectedString2)
+
+			pusherSH := pusherContainer.Args[0]
+			if !strings.Contains(pusherSH, fmt.Sprintf("\n[ ! -z \"${IMG_REF}\" ] || IMG_REF='%s'\n", outputImage)) {
+				GinkgoWriter.Printf("Unexpected pusherSH: %s\n", pullerSH)
+				return false
+			}
+
+			return true
 		}, 10*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for Tuning resource to be ready")
 	})
 }
 
-func validateACRTuningResultsUploaded(workspaceObj *kaitov1alpha1.Workspace, jobName string) {
+func validateACRTuningResultsUploaded(workspaceObj *kaitov1beta1.Workspace, jobName string) {
 	coreClient, err := utils.GetK8sClientset()
 	if err != nil {
-		log.Fatalf("Failed to create core client: %v", err)
-	}
-	namespace := workspaceObj.Namespace
-	podName, err := utils.GetPodNameForJob(coreClient, namespace, jobName)
-	if err != nil {
-		log.Fatalf("Failed to get pod name for job %s: %v", jobName, err)
+		Fail(fmt.Sprintf("Failed to create core client: %v", err))
 	}
 
 	for {
-		logs, err := utils.GetPodLogs(coreClient, namespace, podName, "docker-sidecar")
+		job, err := coreClient.BatchV1().Jobs(workspaceObj.Namespace).Get(ctx, jobName, metav1.GetOptions{})
 		if err != nil {
-			log.Printf("Failed to get logs from pod %s: %v", podName, err)
-			time.Sleep(10 * time.Second)
+			Fail(fmt.Sprintf("Failed to get job %s: %v", jobName, err))
+		}
+
+		if job.Status.CompletionTime.IsZero() {
+			time.Sleep(10 * time.Second) // Poll every 10 seconds
 			continue
 		}
 
-		if strings.Contains(logs, "Upload complete") {
-			fmt.Println("Upload complete")
+		if job.Status.Succeeded == 0 {
+			Fail("Job did not succeed")
 			break
 		}
 
-		time.Sleep(10 * time.Second) // Poll every 10 seconds
+		GinkgoWriter.Println("Upload complete")
+		break
 	}
 }
 
 // validateWorkspaceReadiness validates workspace readiness
-func validateWorkspaceReadiness(workspaceObj *kaitov1alpha1.Workspace) {
+func validateWorkspaceReadiness(workspaceObj *kaitov1beta1.Workspace) {
 	By("Checking the workspace status is ready", func() {
 		Eventually(func() bool {
 			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
@@ -598,7 +612,7 @@ func validateWorkspaceReadiness(workspaceObj *kaitov1alpha1.Workspace) {
 			}
 
 			_, conditionFound := lo.Find(workspaceObj.Status.Conditions, func(condition metav1.Condition) bool {
-				return condition.Type == string(kaitov1alpha1.WorkspaceConditionTypeSucceeded) &&
+				return condition.Type == string(kaitov1beta1.WorkspaceConditionTypeSucceeded) &&
 					condition.Status == metav1.ConditionTrue
 			})
 			return conditionFound
@@ -606,7 +620,94 @@ func validateWorkspaceReadiness(workspaceObj *kaitov1alpha1.Workspace) {
 	})
 }
 
-func cleanupResources(workspaceObj *kaitov1alpha1.Workspace) {
+func validateModelsEndpoint(workspaceObj *kaitov1beta1.Workspace) {
+	deploymentName := workspaceObj.Name
+	modelName := workspaceObj.Inference.Preset.Name
+	expectedModelID := fmt.Sprintf(`"id":"%s"`, modelName)
+	execOption := corev1.PodExecOptions{
+		Command:   []string{"bash", "-c", fmt.Sprintf(`apt-get update && apt-get install curl -y; curl -s -X GET http://%s.%s.svc.cluster.local:80/v1/models | grep -e '%s'`, workspaceObj.Name, workspaceObj.Namespace, expectedModelID)},
+		Container: deploymentName,
+		Stdout:    true,
+		Stderr:    true,
+	}
+
+	By("Validating the /v1/models endpoint", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+
+			namespace := workspaceObj.Namespace
+			podName, err := utils.GetPodNameForDeployment(coreClient, namespace, deploymentName)
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get pod name for deployment %s: %v\n", deploymentName, err)
+				return false
+			}
+
+			k8sConfig, err := utils.GetK8sConfig()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get k8s config: %v\n", err)
+				return false
+			}
+
+			ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
+			defer cancel()
+			_, err = utils.ExecSync(ctx, k8sConfig, coreClient, namespace, podName, execOption)
+			if err != nil {
+				GinkgoWriter.Printf("validate command fails: %v\n", err)
+				return false
+			}
+			return true
+		}, 5*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for /v1/models endpoint to be ready")
+	})
+}
+
+func validateCompletionsEndpoint(workspaceObj *kaitov1beta1.Workspace) {
+	deploymentName := workspaceObj.Name
+	expectedCompletion := `"object":"text_completion"`
+	execOption := corev1.PodExecOptions{
+		Command:   []string{"bash", "-c", fmt.Sprintf(`apt-get update && apt-get install curl -y; curl -s -X POST -H "Content-Type: application/json" -d '{"model":"%s","prompt":"What is Kubernetes?","max_tokens":7,"temperature":0}' http://%s.%s.svc.cluster.local:80/v1/completions | grep -e '%s'`, workspaceObj.Inference.Preset.Name, workspaceObj.Name, workspaceObj.Namespace, expectedCompletion)},
+		Container: deploymentName,
+		Stdout:    true,
+		Stderr:    true,
+	}
+
+	By("Validating the /v1/completions endpoint", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+
+			namespace := workspaceObj.Namespace
+			podName, err := utils.GetPodNameForDeployment(coreClient, namespace, deploymentName)
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get pod name for deployment %s: %v\n", deploymentName, err)
+				return false
+			}
+
+			k8sConfig, err := utils.GetK8sConfig()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get k8s config: %v\n", err)
+				return false
+			}
+
+			ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
+			defer cancel()
+			_, err = utils.ExecSync(ctx, k8sConfig, coreClient, namespace, podName, execOption)
+			if err != nil {
+				GinkgoWriter.Printf("validate command fails: %v\n", err)
+				return false
+			}
+			return true
+		}, 5*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for /v1/completions endpoint to be ready")
+	})
+}
+
+func cleanupResources(workspaceObj *kaitov1beta1.Workspace) {
 	By("Cleaning up resources", func() {
 		if !CurrentSpecReport().Failed() {
 			// delete workspace
@@ -618,7 +719,7 @@ func cleanupResources(workspaceObj *kaitov1alpha1.Workspace) {
 	})
 }
 
-func deleteWorkspace(workspaceObj *kaitov1alpha1.Workspace) error {
+func deleteWorkspace(workspaceObj *kaitov1beta1.Workspace) error {
 	By("Deleting workspace", func() {
 		Eventually(func() error {
 			// Check if the workspace exists
@@ -781,7 +882,7 @@ var _ = Describe("Workspace Preset", func() {
 		validateWorkspaceReadiness(workspaceObj)
 	})
 
-	It("should create a custom template workspace successfully", func() {
+	It("should create a custom template workspace successfully", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		imageName := "nginx:latest"
 		workspaceObj := createCustomWorkspaceWithPresetCustomMode(imageName, numOfNode)
@@ -799,7 +900,7 @@ var _ = Describe("Workspace Preset", func() {
 		validateWorkspaceReadiness(workspaceObj)
 	})
 
-	It("should create a Phi-3-mini-128k-instruct workspace with preset public mode successfully", func() {
+	It("should create a Phi-3-mini-128k-instruct workspace with preset public mode successfully", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		workspaceObj := createPhi3WorkspaceWithPresetPublicMode(numOfNode)
 
@@ -819,12 +920,8 @@ var _ = Describe("Workspace Preset", func() {
 		validateWorkspaceReadiness(workspaceObj)
 	})
 
-	It("should create a workspace for tuning successfully, and update the workspace with another dataset and output image", func() {
+	It("should create a workspace for tuning successfully, and update the workspace with another dataset and output image", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
-		err := copySecretToNamespace(e2eACRSecret, namespaceName)
-		if err != nil {
-			log.Fatalf("Error copying secret: %v", err)
-		}
 		configMap := createCustomTuningConfigMapForE2E()
 		workspaceObj, jobName, outputRegistryUrl1 := createPhi3TuningWorkspaceWithPresetPublicMode(configMap.Name, numOfNode)
 
@@ -862,16 +959,16 @@ var _ = Describe("Workspace Preset", func() {
 
 })
 
-func validateCreateNode(workspaceObj *kaitov1alpha1.Workspace, numOfNode int) {
+func validateCreateNode(workspaceObj *kaitov1beta1.Workspace, numOfNode int) {
 	utils.ValidateNodeClaimCreation(ctx, workspaceObj, numOfNode)
 }
 
 // validateInferenceConfig validates that the inference config exists and contains data
-func validateInferenceConfig(workspaceObj *kaitov1alpha1.Workspace) {
+func validateInferenceConfig(workspaceObj *kaitov1beta1.Workspace) {
 	By("Checking the inference config exists", func() {
 		Eventually(func() bool {
 			configMap := &v1.ConfigMap{}
-			configName := kaitov1alpha1.DefaultInferenceConfigTemplate
+			configName := kaitov1beta1.DefaultInferenceConfigTemplate
 			if workspaceObj.Inference.Config != "" {
 				configName = workspaceObj.Inference.Config
 			}
diff --git a/test/e2e/preset_vllm_test.go b/test/e2e/preset_vllm_test.go
index b64e981..e575003 100644
--- a/test/e2e/preset_vllm_test.go
+++ b/test/e2e/preset_vllm_test.go
@@ -1,3 +1,5 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
 package e2e
 
 import (
@@ -8,7 +10,7 @@ import (
 	. "github.com/onsi/ginkgo/v2"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/test/e2e/utils"
 )
 
@@ -27,6 +29,53 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		}
 	})
 
+	It("should create a deepseek-distilled-llama-8b workspace with preset public mode successfully", func() {
+		numOfNode := 1
+		workspaceObj := createDeepSeekLlama8BWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
+	})
+
+	It("should create a deepseek-distilled-qwen-14b workspace with preset public mode successfully", func() {
+		if !runLlama13B {
+			Skip("Skipping deepseek-distilled-qwen-14b workspace test")
+		}
+		numOfNode := 1
+		workspaceObj := createDeepSeekQwen14BWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
+	})
+
 	It("should create a falcon workspace with preset public mode successfully", func() {
 		numOfNode := 1
 		workspaceObj := createFalconWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
@@ -45,6 +94,8 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 	})
 
 	It("should create a mistral workspace with preset public mode successfully", func() {
@@ -65,6 +116,8 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 	})
 
 	It("should create a Phi-2 workspace with preset public mode successfully", func() {
@@ -85,9 +138,11 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 	})
 
-	It("should create a Phi-3-mini-128k-instruct workspace with preset public mode successfully", func() {
+	It("should create a Phi-3-mini-128k-instruct workspace with preset public mode successfully", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		workspaceObj := createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfNode)
 
@@ -105,9 +160,11 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 	})
 
-	It("should create a qwen2.5 coder workspace with preset public mode and 2 gpu successfully", func() {
+	It("should create a qwen2.5 coder workspace with preset public mode and 2 gpu successfully", utils.GinkgoLabelFastCheck, func() {
 		// single node with 2 gpu
 		numOfNode := 1
 		workspaceObj := createQwen2_5WorkspaceWithPresetPublicModeAndVLLMAndMultiGPU(numOfNode)
@@ -126,9 +183,11 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 	})
 
-	It("should create a falcon workspace with adapter successfully", func() {
+	It("should create a falcon workspace with adapter successfully", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		workspaceObj := createFalconWorkspaceWithAdapterAndVLLM(numOfNode, validAdapters2)
 
@@ -146,91 +205,121 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateInferenceResource(workspaceObj, int32(numOfNode), false)
 
 		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
 
 		validateInitContainers(workspaceObj, expectedInitContainers2)
 
-		validateAdapterLoadedInVLLM(workspaceObj, workspaceObj.Name, imageName2)
+		validateAdapterLoadedInVLLM(workspaceObj, imageName2)
 	})
 })
 
-func createFalconWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createDeepSeekLlama8BWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
+	By("Creating a workspace CR with DeepSeek Distilled Llama 8B preset public mode and vLLM", func() {
+		uniqueID := fmt.Sprint("preset-deepseek-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC24ads_A100_v4",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-deepseek-llama-vllm"},
+			}, nil, PresetDeepSeekR1DistillLlama8BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+	return workspaceObj
+}
+
+func createDeepSeekQwen14BWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
+	By("Creating a workspace CR with DeepSeek Distilled Qwen 14B preset public mode and vLLM", func() {
+		uniqueID := fmt.Sprint("preset-deepseek-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC24ads_A100_v4",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-deepseek-qwen-vllm"},
+			}, nil, PresetDeepSeekR1DistillQwen14BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+	return workspaceObj
+}
+
+func createFalconWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Falcon 7B preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-falcon-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-falcon-vllm"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createFalconWorkspaceWithAdapterAndVLLM(numOfNode int, validAdapters []kaitov1alpha1.AdapterSpec) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createFalconWorkspaceWithAdapterAndVLLM(numOfNode int, validAdapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Falcon 7B preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-falcon-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-falcon-adapter-vllm"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, validAdapters)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, validAdapters)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createMistralWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createMistralWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Mistral 7B preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-mistral-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-mistral-vllm"},
-			}, nil, PresetMistral7BInstructModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetMistral7BInstructModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createPhi2WorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createPhi2WorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Phi 2 preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-phi2-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-phi-2-vllm"},
-			}, nil, PresetPhi2Model, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetPhi2Model, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Phi-3-mini-128k-instruct preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-phi3-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC6s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-phi-3-mini-128k-instruct-vllm"},
-			}, nil, PresetPhi3Mini128kModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetPhi3Mini128kModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
 	return workspaceObj
 }
 
-func createQwen2_5WorkspaceWithPresetPublicModeAndVLLMAndMultiGPU(numOfNode int) *kaitov1alpha1.Workspace {
-	workspaceObj := &kaitov1alpha1.Workspace{}
+func createQwen2_5WorkspaceWithPresetPublicModeAndVLLMAndMultiGPU(numOfNode int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
 	By("Creating a workspace CR with Qwen2.5 Coder 7B preset public mode and vLLM", func() {
 		uniqueID := fmt.Sprint("preset-qwen-2gpu-", rand.Intn(1000))
 		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-qwen-2gpu-vllm"},
-			}, nil, PresetQwen2_5Coder7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetQwen2_5Coder7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		createAndValidateWorkspace(workspaceObj)
 	})
diff --git a/test/e2e/utils/cluster.go b/test/e2e/utils/cluster.go
index a858ec8..202cd65 100644
--- a/test/e2e/utils/cluster.go
+++ b/test/e2e/utils/cluster.go
@@ -15,6 +15,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 
 	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	kaitoutils "github.com/kaito-project/kaito/pkg/utils"
 )
 
@@ -44,6 +45,7 @@ func NewCluster(scheme *runtime.Scheme) *Cluster {
 func GetClusterClient(cluster *Cluster) {
 	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
 	utilruntime.Must(kaitov1alpha1.AddToScheme(scheme))
+	utilruntime.Must(kaitov1beta1.AddToScheme(scheme))
 	utilruntime.Must(kaitoutils.KarpenterSchemeBuilder.AddToScheme(scheme))
 	utilruntime.Must(azurev1alpha2.SchemeBuilder.AddToScheme(scheme))
 	utilruntime.Must(kaitoutils.AwsSchemeBuilder.AddToScheme(scheme))
diff --git a/test/e2e/utils/ginkgo.go b/test/e2e/utils/ginkgo.go
new file mode 100644
index 0000000..a0bcb7c
--- /dev/null
+++ b/test/e2e/utils/ginkgo.go
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package utils
+
+import g "github.com/onsi/ginkgo/v2"
+
+var (
+	GinkgoLabelFastCheck = g.Label("FastCheck")
+)
diff --git a/test/e2e/utils/nodeclaim.go b/test/e2e/utils/nodeclaim.go
index 4055ff4..f62541f 100644
--- a/test/e2e/utils/nodeclaim.go
+++ b/test/e2e/utils/nodeclaim.go
@@ -18,11 +18,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
-	"github.com/kaito-project/kaito/api/v1alpha1"
+	"github.com/kaito-project/kaito/api/v1beta1"
 )
 
 // ValidateNodeClaimCreation Logic to validate the nodeClaim creation.
-func ValidateNodeClaimCreation(ctx context.Context, workspaceObj *v1alpha1.Workspace, expectedCount int) {
+func ValidateNodeClaimCreation(ctx context.Context, workspaceObj *v1beta1.Workspace, expectedCount int) {
 	ginkgo.By("Checking nodeClaim created by the workspace CR", func() {
 		gomega.Eventually(func() bool {
 			nodeClaimList, err := GetAllValidNodeClaims(ctx, workspaceObj)
@@ -49,11 +49,11 @@ func ValidateNodeClaimCreation(ctx context.Context, workspaceObj *v1alpha1.Works
 }
 
 // GetAllValidNodeClaims get all valid nodeClaims.
-func GetAllValidNodeClaims(ctx context.Context, workspaceObj *v1alpha1.Workspace) (*karpenterv1.NodeClaimList, error) {
+func GetAllValidNodeClaims(ctx context.Context, workspaceObj *v1beta1.Workspace) (*karpenterv1.NodeClaimList, error) {
 	nodeClaimList := &karpenterv1.NodeClaimList{}
 	ls := labels.Set{
-		v1alpha1.LabelWorkspaceName:      workspaceObj.Name,
-		v1alpha1.LabelWorkspaceNamespace: workspaceObj.Namespace,
+		v1beta1.LabelWorkspaceName:      workspaceObj.Name,
+		v1beta1.LabelWorkspaceNamespace: workspaceObj.Namespace,
 	}
 
 	err := TestingCluster.KubeClient.List(ctx, nodeClaimList, &client.MatchingLabelsSelector{Selector: ls.AsSelector()})
diff --git a/test/e2e/utils/utils.go b/test/e2e/utils/utils.go
index bda2657..493c658 100644
--- a/test/e2e/utils/utils.go
+++ b/test/e2e/utils/utils.go
@@ -27,12 +27,12 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/remotecommand"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/pkg/model"
 )
 
 const (
-	InferenceModeCustomTemplate kaitov1alpha1.ModelImageAccessMode = "customTemplate"
+	InferenceModeCustomTemplate kaitov1beta1.ModelImageAccessMode = "customTemplate"
 
 	ExampleDatasetURL = "https://huggingface.co/datasets/philschmid/dolly-15k-oai-style/resolve/main/data/train-00000-of-00001-54e3756291ca09c6.parquet?download=true"
 )
@@ -180,7 +180,7 @@ func ExecSync(ctx context.Context, config *rest.Config, coreClient *kubernetes.C
 		Stderr: &stderr,
 	})
 	if err != nil {
-		return "", fmt.Errorf("failed to execute command: %w, stderr: %q", err, stderr.String())
+		return "", fmt.Errorf("failed to execute command %v: %w, stderr: %q", options.Command, err, stderr.String())
 	}
 
 	if stderr.Len() > 0 {
@@ -257,19 +257,19 @@ func ExtractModelVersion(configs map[string]interface{}) (map[string]string, err
 }
 
 func GenerateInferenceWorkspaceManifest(name, namespace, imageName string, resourceCount int, instanceType string,
-	labelSelector *metav1.LabelSelector, preferredNodes []string, presetName kaitov1alpha1.ModelName,
-	accessMode kaitov1alpha1.ModelImageAccessMode, imagePullSecret []string,
-	podTemplate *corev1.PodTemplateSpec, adapters []kaitov1alpha1.AdapterSpec) *kaitov1alpha1.Workspace {
+	labelSelector *metav1.LabelSelector, preferredNodes []string, presetName kaitov1beta1.ModelName,
+	accessMode kaitov1beta1.ModelImageAccessMode, imagePullSecret []string,
+	podTemplate *corev1.PodTemplateSpec, adapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
 
-	workspace := &kaitov1alpha1.Workspace{
+	workspace := &kaitov1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
 			Namespace: namespace,
 			Annotations: map[string]string{
-				kaitov1alpha1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
+				kaitov1beta1.AnnotationWorkspaceRuntime: string(model.RuntimeNameHuggingfaceTransformers),
 			},
 		},
-		Resource: kaitov1alpha1.ResourceSpec{
+		Resource: kaitov1beta1.ResourceSpec{
 			Count:          lo.ToPtr(resourceCount),
 			InstanceType:   instanceType,
 			LabelSelector:  labelSelector,
@@ -277,15 +277,15 @@ func GenerateInferenceWorkspaceManifest(name, namespace, imageName string, resou
 		},
 	}
 
-	var workspaceInference kaitov1alpha1.InferenceSpec
-	if accessMode == kaitov1alpha1.ModelImageAccessModePublic ||
-		accessMode == kaitov1alpha1.ModelImageAccessModePrivate {
-		workspaceInference.Preset = &kaitov1alpha1.PresetSpec{
-			PresetMeta: kaitov1alpha1.PresetMeta{
+	var workspaceInference kaitov1beta1.InferenceSpec
+	if accessMode == kaitov1beta1.ModelImageAccessModePublic ||
+		accessMode == kaitov1beta1.ModelImageAccessModePrivate {
+		workspaceInference.Preset = &kaitov1beta1.PresetSpec{
+			PresetMeta: kaitov1beta1.PresetMeta{
 				Name:       presetName,
 				AccessMode: accessMode,
 			},
-			PresetOptions: kaitov1alpha1.PresetOptions{
+			PresetOptions: kaitov1beta1.PresetOptions{
 				Image:            imageName,
 				ImagePullSecrets: imagePullSecret,
 			},
@@ -306,35 +306,35 @@ func GenerateInferenceWorkspaceManifest(name, namespace, imageName string, resou
 }
 
 func GenerateInferenceWorkspaceManifestWithVLLM(name, namespace, imageName string, resourceCount int, instanceType string,
-	labelSelector *metav1.LabelSelector, preferredNodes []string, presetName kaitov1alpha1.ModelName,
-	accessMode kaitov1alpha1.ModelImageAccessMode, imagePullSecret []string,
-	podTemplate *corev1.PodTemplateSpec, adapters []kaitov1alpha1.AdapterSpec) *kaitov1alpha1.Workspace {
+	labelSelector *metav1.LabelSelector, preferredNodes []string, presetName kaitov1beta1.ModelName,
+	accessMode kaitov1beta1.ModelImageAccessMode, imagePullSecret []string,
+	podTemplate *corev1.PodTemplateSpec, adapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
 	workspace := GenerateInferenceWorkspaceManifest(name, namespace, imageName, resourceCount, instanceType,
 		labelSelector, preferredNodes, presetName, accessMode, imagePullSecret, podTemplate, adapters)
 
 	if workspace.Annotations == nil {
 		workspace.Annotations = make(map[string]string)
 	}
-	workspace.Annotations[kaitov1alpha1.AnnotationWorkspaceRuntime] = string(model.RuntimeNameVLLM)
+	workspace.Annotations[kaitov1beta1.AnnotationWorkspaceRuntime] = string(model.RuntimeNameVLLM)
 	return workspace
 }
 
 func GenerateTuningWorkspaceManifest(name, namespace, imageName string, resourceCount int, instanceType string,
-	labelSelector *metav1.LabelSelector, preferredNodes []string, input *kaitov1alpha1.DataSource,
-	output *kaitov1alpha1.DataDestination, preset *kaitov1alpha1.PresetSpec, method kaitov1alpha1.TuningMethod) *kaitov1alpha1.Workspace {
+	labelSelector *metav1.LabelSelector, preferredNodes []string, input *kaitov1beta1.DataSource,
+	output *kaitov1beta1.DataDestination, preset *kaitov1beta1.PresetSpec, method kaitov1beta1.TuningMethod) *kaitov1beta1.Workspace {
 
-	workspace := &kaitov1alpha1.Workspace{
+	workspace := &kaitov1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
 			Namespace: namespace,
 		},
-		Resource: kaitov1alpha1.ResourceSpec{
+		Resource: kaitov1beta1.ResourceSpec{
 			Count:          lo.ToPtr(resourceCount),
 			InstanceType:   instanceType,
 			LabelSelector:  labelSelector,
 			PreferredNodes: preferredNodes,
 		},
-		Tuning: &kaitov1alpha1.TuningSpec{
+		Tuning: &kaitov1beta1.TuningSpec{
 			Method: method,
 			Input:  input,
 			Output: output,
@@ -347,14 +347,14 @@ func GenerateTuningWorkspaceManifest(name, namespace, imageName string, resource
 
 func GenerateE2ETuningWorkspaceManifest(name, namespace, imageName, datasetImageName, outputRegistry string,
 	resourceCount int, instanceType string, labelSelector *metav1.LabelSelector,
-	preferredNodes []string, presetName kaitov1alpha1.ModelName, accessMode kaitov1alpha1.ModelImageAccessMode,
-	imagePullSecret []string, customConfigMapName string) *kaitov1alpha1.Workspace {
-	workspace := &kaitov1alpha1.Workspace{
+	preferredNodes []string, presetName kaitov1beta1.ModelName, accessMode kaitov1beta1.ModelImageAccessMode,
+	imagePullSecret []string, customConfigMapName string) *kaitov1beta1.Workspace {
+	workspace := &kaitov1beta1.Workspace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      name,
 			Namespace: namespace,
 		},
-		Resource: kaitov1alpha1.ResourceSpec{
+		Resource: kaitov1beta1.ResourceSpec{
 			Count:          lo.ToPtr(resourceCount),
 			InstanceType:   instanceType,
 			LabelSelector:  labelSelector,
@@ -362,15 +362,15 @@ func GenerateE2ETuningWorkspaceManifest(name, namespace, imageName, datasetImage
 		},
 	}
 
-	var workspaceTuning kaitov1alpha1.TuningSpec
-	if accessMode == kaitov1alpha1.ModelImageAccessModePublic ||
-		accessMode == kaitov1alpha1.ModelImageAccessModePrivate {
-		workspaceTuning.Preset = &kaitov1alpha1.PresetSpec{
-			PresetMeta: kaitov1alpha1.PresetMeta{
+	var workspaceTuning kaitov1beta1.TuningSpec
+	if accessMode == kaitov1beta1.ModelImageAccessModePublic ||
+		accessMode == kaitov1beta1.ModelImageAccessModePrivate {
+		workspaceTuning.Preset = &kaitov1beta1.PresetSpec{
+			PresetMeta: kaitov1beta1.PresetMeta{
 				Name:       presetName,
 				AccessMode: accessMode,
 			},
-			PresetOptions: kaitov1alpha1.PresetOptions{
+			PresetOptions: kaitov1beta1.PresetOptions{
 				Image:            imageName,
 				ImagePullSecrets: imagePullSecret,
 			},
@@ -378,11 +378,12 @@ func GenerateE2ETuningWorkspaceManifest(name, namespace, imageName, datasetImage
 	}
 
 	workspace.Tuning = &workspaceTuning
-	workspace.Tuning.Method = kaitov1alpha1.TuningMethodQLora
-	workspace.Tuning.Input = &kaitov1alpha1.DataSource{
-		Image: datasetImageName,
+	workspace.Tuning.Method = kaitov1beta1.TuningMethodQLora
+	workspace.Tuning.Input = &kaitov1beta1.DataSource{
+		Image:            datasetImageName,
+		ImagePullSecrets: imagePullSecret,
 	}
-	workspace.Tuning.Output = &kaitov1alpha1.DataDestination{
+	workspace.Tuning.Output = &kaitov1beta1.DataDestination{
 		Image:           outputRegistry,
 		ImagePushSecret: imagePullSecret[0],
 	}
diff --git a/test/e2e/webhook_test.go b/test/e2e/webhook_test.go
index e0b4e3c..dc415e8 100644
--- a/test/e2e/webhook_test.go
+++ b/test/e2e/webhook_test.go
@@ -13,28 +13,28 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
-	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
 	"github.com/kaito-project/kaito/test/e2e/utils"
 )
 
 var (
 	testModelImage            = utils.GetEnv("AI_MODELS_REGISTRY") + "/e2e-test:0.0.1"
-	testDataSourceConfig      = &kaitov1alpha1.DataSource{Name: PresetFalcon7BModel, Image: testModelImage}
-	testDataDestinationConfig = &kaitov1alpha1.DataDestination{Image: testModelImage, ImagePushSecret: utils.GetEnv("AI_MODELS_REGISTRY_SECRET")}
+	testDataSourceConfig      = &kaitov1beta1.DataSource{Name: PresetFalcon7BModel, Image: testModelImage}
+	testDataDestinationConfig = &kaitov1beta1.DataDestination{Image: testModelImage, ImagePushSecret: utils.GetEnv("AI_MODELS_REGISTRY_SECRET")}
 
-	initialPresetSpec = &kaitov1alpha1.PresetSpec{PresetMeta: kaitov1alpha1.PresetMeta{Name: PresetFalcon7BModel}}
-	updatedPresetSpec = &kaitov1alpha1.PresetSpec{PresetMeta: kaitov1alpha1.PresetMeta{Name: PresetFalcon40BModel}}
+	initialPresetSpec = &kaitov1beta1.PresetSpec{PresetMeta: kaitov1beta1.PresetMeta{Name: PresetFalcon7BModel}}
+	updatedPresetSpec = &kaitov1beta1.PresetSpec{PresetMeta: kaitov1beta1.PresetMeta{Name: PresetFalcon40BModel}}
 
-	initialTuningMethod     = kaitov1alpha1.TuningMethodLora
-	alternativeTuningMethod = kaitov1alpha1.TuningMethodQLora
+	initialTuningMethod     = kaitov1beta1.TuningMethodLora
+	alternativeTuningMethod = kaitov1beta1.TuningMethodQLora
 )
 
-var _ = Describe("Workspace Validation Webhook", func() {
+var _ = Describe("Workspace Validation Webhook", utils.GinkgoLabelFastCheck, func() {
 	It("should validate the workspace resource spec at creation ", func() {
 		workspaceObj := utils.GenerateInferenceWorkspaceManifest(fmt.Sprint("webhook-", rand.Intn(1000)), namespaceName, "", 1, "Standard_Bad",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "webhook-e2e-test"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		By("Creating a workspace with invalid instancetype", func() {
 			// Create workspace
@@ -49,7 +49,7 @@ var _ = Describe("Workspace Validation Webhook", func() {
 		workspaceObj := utils.GenerateInferenceWorkspaceManifest(fmt.Sprint("webhook-", rand.Intn(1000)), namespaceName, "", 1, "Standard_NC6",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "webhook-e2e-test"},
-			}, nil, "invalid-name", kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, "invalid-name", kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		By("Creating a workspace with invalid preset name", func() {
 			// Create workspace
@@ -112,7 +112,7 @@ var _ = Describe("Workspace Validation Webhook", func() {
 		workspaceObj := utils.GenerateInferenceWorkspaceManifest(fmt.Sprint("webhook-", rand.Intn(1000)), namespaceName, "", 1, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "webhook-e2e-test"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		By("Creating a valid workspace", func() {
 			// Create workspace
@@ -196,7 +196,7 @@ var _ = Describe("Workspace Validation Webhook", func() {
 		workspaceObj := utils.GenerateInferenceWorkspaceManifest(fmt.Sprint("webhook-", rand.Intn(1000)), namespaceName, "", 1, "Standard_NC12s_v3",
 			&metav1.LabelSelector{
 				MatchLabels: map[string]string{"kaito-workspace": "webhook-e2e-test"},
-			}, nil, PresetFalcon7BModel, kaitov1alpha1.ModelImageAccessModePublic, nil, nil, nil)
+			}, nil, PresetFalcon7BModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
 
 		By("Creating a valid workspace", func() {
 			// Create workspace
diff --git a/test/rage2e/e2e_test.go b/test/rage2e/e2e_test.go
new file mode 100644
index 0000000..046a2fb
--- /dev/null
+++ b/test/rage2e/e2e_test.go
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+package e2e
+
+import (
+	"context"
+	"fmt"
+	"math/rand"
+	"os"
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/kaito-project/kaito/test/e2e/utils"
+)
+
+var (
+	ctx                 = context.Background()
+	namespaceName       = fmt.Sprint(utils.E2eNamespace, rand.Intn(100))
+	nodeProvisionerName = os.Getenv("TEST_SUITE")
+)
+
+var _ = BeforeSuite(func() {
+	utils.GetClusterClient(utils.TestingCluster)
+
+	namespaceName = fmt.Sprintf("%s-%d", namespaceName, GinkgoParallelProcess())
+	GinkgoWriter.Printf("Namespace %q for e2e tests\n", namespaceName)
+
+	kaitoNamespace := os.Getenv("KAITO_NAMESPACE")
+
+	if nodeProvisionerName == "azkarpenter" {
+		karpenterNamespace := os.Getenv("KARPENTER_NAMESPACE")
+		//check karpenter deployment is up and running
+		karpenterDeployment := &v1.Deployment{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "karpenter",
+				Namespace: karpenterNamespace,
+			},
+		}
+
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: karpenterDeployment.Namespace,
+				Name:      karpenterDeployment.Name,
+			}, karpenterDeployment, &client.GetOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to wait for	karpenter deployment")
+	}
+
+	if nodeProvisionerName == "gpuprovisioner" {
+		gpuNamespace := os.Getenv("GPU_PROVISIONER_NAMESPACE")
+		//check gpu-provisioner deployment is up and running
+		gpuProvisionerDeployment := &v1.Deployment{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "gpu-provisioner",
+				Namespace: gpuNamespace,
+			},
+		}
+
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: gpuProvisionerDeployment.Namespace,
+				Name:      gpuProvisionerDeployment.Name,
+			}, gpuProvisionerDeployment, &client.GetOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to wait for	gpu-provisioner deployment")
+	}
+
+	//check kaito-workspace deployment is up and running
+
+	kaitoWorkspaceDeployment := &v1.Deployment{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "kaito-workspace",
+			Namespace: kaitoNamespace,
+		},
+	}
+
+	Eventually(func() error {
+		return utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+			Namespace: kaitoWorkspaceDeployment.Namespace,
+			Name:      kaitoWorkspaceDeployment.Name,
+		}, kaitoWorkspaceDeployment, &client.GetOptions{})
+	}, utils.PollTimeout, utils.PollInterval).Should(Succeed(), "Failed to wait for	kaito-workspace deployment")
+
+	// create testing namespace
+	err := utils.TestingCluster.KubeClient.Create(context.TODO(), &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: namespaceName,
+		},
+	})
+	Expect(err).NotTo(HaveOccurred())
+})
+
+var _ = AfterSuite(func() {
+	// delete testing namespace
+	Eventually(func() error {
+		return utils.TestingCluster.KubeClient.Delete(ctx, &corev1.Namespace{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: namespaceName,
+			},
+		}, &client.DeleteOptions{})
+	}, utils.PollTimeout, utils.PollInterval).Should(Succeed(), "Failed to delete namespace for e2e")
+
+})
+
+func RunE2ETests(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "AI Toolchain Operator E2E Test Suite")
+}
+
+func TestE2E(t *testing.T) {
+	RunE2ETests(t)
+}
+
+func TestMain(m *testing.M) {
+	os.Exit(m.Run())
+}
diff --git a/test/rage2e/rag_test.go b/test/rage2e/rag_test.go
new file mode 100644
index 0000000..b2a7085
--- /dev/null
+++ b/test/rage2e/rag_test.go
@@ -0,0 +1,719 @@
+package e2e
+
+import (
+	"fmt"
+	"math/rand"
+	"os"
+	"strings"
+	"time"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/samber/lo"
+	appsv1 "k8s.io/api/apps/v1"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	kaitov1alpha1 "github.com/kaito-project/kaito/api/v1alpha1"
+	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/test/e2e/utils"
+)
+
+const (
+	PresetPhi3Mini128kModel = "phi-3-mini-128k-instruct"
+)
+
+func loadTestEnvVars() {
+	// Required for Llama models
+	aiModelsRegistry = utils.GetEnv("AI_MODELS_REGISTRY")
+	aiModelsRegistrySecret = utils.GetEnv("AI_MODELS_REGISTRY_SECRET")
+	// Currently required for uploading fine-tuning results
+	e2eACRSecret = utils.GetEnv("E2E_ACR_REGISTRY_SECRET")
+	supportedModelsYamlPath = utils.GetEnv("SUPPORTED_MODELS_YAML_PATH")
+	azureClusterName = utils.GetEnv("AZURE_CLUSTER_NAME")
+}
+
+func loadModelVersions() {
+	// Load stable model versions
+	configs, err := utils.GetModelConfigInfo(supportedModelsYamlPath)
+	if err != nil {
+		fmt.Printf("Failed to load model configs: %v\n", err)
+		os.Exit(1)
+	}
+
+	modelInfo, err = utils.ExtractModelVersion(configs)
+	if err != nil {
+		fmt.Printf("Failed to extract stable model versions: %v\n", err)
+		os.Exit(1)
+	}
+}
+
+var aiModelsRegistry string
+var aiModelsRegistrySecret string
+var e2eACRSecret string
+var supportedModelsYamlPath string
+var modelInfo map[string]string
+var azureClusterName string
+
+var _ = Describe("RAGEngine", func() {
+	BeforeEach(func() {
+		loadTestEnvVars()
+		loadModelVersions()
+	})
+
+	AfterEach(func() {
+		if CurrentSpecReport().Failed() {
+			utils.PrintPodLogsOnFailure(namespaceName, "")     // The Preset Pod
+			utils.PrintPodLogsOnFailure("kaito-workspace", "") // The Kaito Workspace Pod
+			utils.PrintPodLogsOnFailure("kaito-ragengine", "") // The Kaito ragengine Pod
+			utils.PrintPodLogsOnFailure("gpu-provisioner", "") // The gpu-provisioner Pod
+			Fail("Fail threshold reached")
+		}
+	})
+
+	It("should create RAG with localembedding and huggingface API successfully", func() {
+		numOfReplica := 1
+
+		createAndValidateSecret()
+		ragengineObj := createLocalEmbeddingHFURLRAGEngine()
+
+		defer cleanupResources(nil, ragengineObj)
+
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.ConditionTypeResourceStatus), "ragengineObj resource status to be ready")
+		validateAssociatedService(ragengineObj.ObjectMeta)
+		validateInferenceandRAGResource(ragengineObj.ObjectMeta, int32(numOfReplica), false)
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.RAGEngineConditionTypeSucceeded), "ragengine to be ready")
+
+		err := createAndValidateIndexPod(ragengineObj)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate IndexPod")
+
+		searchQuerySuccess := "\\n\\nKaito is an operator that is designed to automate the AI/ML model inference or tuning workload in a Kubernetes cluster."
+		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, true)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryPod")
+
+		persistLogSuccess := "Successfully persisted index kaito"
+		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate PersistPod")
+
+		loadLogSuccess := "Successfully loaded index kaito"
+		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate LoadPod")
+	})
+
+	It("should create RAG with localembedding and kaito VLLM workspace successfully", func() {
+		numOfReplica := 1
+		workspaceObj := createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfReplica)
+
+		time.Sleep(30 * time.Second)
+
+		validateWorkspaceResourceStatus(workspaceObj)
+
+		validateAssociatedService(workspaceObj.ObjectMeta)
+
+		validateInferenceandRAGResource(workspaceObj.ObjectMeta, int32(numOfReplica), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+
+		serviceName := workspaceObj.Name
+		serviceNamespace := workspaceObj.Namespace
+		service := &v1.Service{}
+
+		_ = utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+			Namespace: serviceNamespace,
+			Name:      serviceName,
+		}, service)
+
+		clusterIP := service.Spec.ClusterIP
+
+		ragengineObj := createLocalEmbeddingKaitoVLLMRAGEngine(clusterIP)
+
+		defer cleanupResources(workspaceObj, ragengineObj)
+
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.ConditionTypeResourceStatus), "ragengineObj resource status to be ready")
+		validateAssociatedService(ragengineObj.ObjectMeta)
+		validateInferenceandRAGResource(ragengineObj.ObjectMeta, int32(numOfReplica), false)
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.RAGEngineConditionTypeSucceeded), "ragengine to be ready")
+
+		err := createAndValidateIndexPod(ragengineObj)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate IndexPod")
+		searchQuerySuccess := "\\nKaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster.\\n\\n\\n"
+		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, false)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryPod")
+
+		persistLogSuccess := "Successfully persisted index kaito"
+		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate PersistPod")
+
+		loadLogSuccess := "Successfully loaded index kaito"
+		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess)
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate LoadPod")
+	})
+
+})
+
+func createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfReplica int) *kaitov1beta1.Workspace {
+	workspaceObj := &kaitov1beta1.Workspace{}
+	By("Creating a workspace CR with Phi-3-mini-128k-instruct preset public mode and vLLM", func() {
+		uniqueID := fmt.Sprint("preset-phi3-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfReplica, "Standard_NC6s_v3",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "rag-e2e-test-phi-3-mini-128k-instruct-vllm"},
+			}, nil, PresetPhi3Mini128kModel, kaitov1beta1.ModelImageAccessModePublic, nil, nil, nil)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+	return workspaceObj
+}
+
+func createAndValidateWorkspace(workspaceObj *kaitov1beta1.Workspace) {
+	By("Creating workspace", func() {
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, workspaceObj, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create workspace %s", workspaceObj.Name)
+	})
+}
+
+func createAndValidateRAGEngine(ragEngineObj *kaitov1alpha1.RAGEngine) {
+	By("Creating ragEngine", func() {
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, ragEngineObj, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create ragEngine   %s", ragEngineObj.Name)
+
+		By("Validating ragEngine creation", func() {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragEngineObj.Namespace,
+				Name:      ragEngineObj.Name,
+			}, ragEngineObj, &client.GetOptions{})
+			Expect(err).NotTo(HaveOccurred())
+		})
+	})
+}
+
+func GenerateLocalEmbeddingRAGEngineManifest(name, namespace, instanceType, embeddingModelID string, labelSelector *metav1.LabelSelector, inferenceSpec *kaitov1alpha1.InferenceServiceSpec) *kaitov1alpha1.RAGEngine {
+	return &kaitov1alpha1.RAGEngine{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: &kaitov1alpha1.RAGEngineSpec{
+			Compute: &kaitov1alpha1.ResourceSpec{
+				InstanceType:  instanceType,
+				LabelSelector: labelSelector,
+			},
+			Embedding: &kaitov1alpha1.EmbeddingSpec{
+				Local: &kaitov1alpha1.LocalEmbeddingSpec{
+					ModelID: embeddingModelID,
+				},
+			},
+			InferenceService: inferenceSpec,
+		},
+	}
+}
+
+// validateWorkspaceReadiness validates workspace readiness
+func validateWorkspaceReadiness(workspaceObj *kaitov1beta1.Workspace) {
+	By("Checking the workspace status is ready", func() {
+		Eventually(func() bool {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj, &client.GetOptions{})
+
+			if err != nil {
+				return false
+			}
+
+			_, conditionFound := lo.Find(workspaceObj.Status.Conditions, func(condition metav1.Condition) bool {
+				return condition.Type == string(kaitov1beta1.WorkspaceConditionTypeSucceeded) &&
+					condition.Status == metav1.ConditionTrue
+			})
+			return conditionFound
+		}, 10*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for workspace to be ready")
+	})
+}
+
+func createLocalEmbeddingKaitoVLLMRAGEngine(baseURL string) *kaitov1alpha1.RAGEngine {
+	ragEngineObj := &kaitov1alpha1.RAGEngine{}
+	serviceURL := fmt.Sprintf("http://%s/v1/completions", baseURL)
+	By("Creating RAG with localembedding and kaito vllm inference", func() {
+		uniqueID := fmt.Sprint("rag-", rand.Intn(1000))
+		ragEngineObj = GenerateLocalEmbeddingRAGEngineManifest(uniqueID, namespaceName, "Standard_NC24s_v3", "BAAI/bge-small-en-v1.5",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"apps": "phi-3"},
+			},
+			&kaitov1alpha1.InferenceServiceSpec{
+				URL: serviceURL,
+			},
+		)
+
+		createAndValidateRAGEngine(ragEngineObj)
+	})
+	return ragEngineObj
+}
+
+func createLocalEmbeddingHFURLRAGEngine() *kaitov1alpha1.RAGEngine {
+	ragEngineObj := &kaitov1alpha1.RAGEngine{}
+	hfURL := "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta/v1/completions"
+	By("Creating RAG with localembedding and huggingface API", func() {
+		uniqueID := fmt.Sprint("rag-", rand.Intn(1000))
+		ragEngineObj = GenerateLocalEmbeddingRAGEngineManifest(uniqueID, namespaceName, "Standard_NC12s_v3", "BAAI/bge-small-en-v1.5",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"apps": "phi-3"},
+			},
+			&kaitov1alpha1.InferenceServiceSpec{
+				URL:          hfURL,
+				AccessSecret: "huggingface-token",
+			},
+		)
+
+		createAndValidateRAGEngine(ragEngineObj)
+	})
+	return ragEngineObj
+}
+
+func cleanupResources(
+	workspaceObj *kaitov1beta1.Workspace,
+	ragengineObj *kaitov1alpha1.RAGEngine,
+) {
+	By("Cleaning up resources", func() {
+		if !CurrentSpecReport().Failed() {
+			err := deleteRAGEngine(ragengineObj)
+			Expect(err).NotTo(HaveOccurred(), "Failed to delete RAGEngine")
+
+			if workspaceObj != nil {
+				err = deleteWorkspace(workspaceObj)
+				Expect(err).NotTo(HaveOccurred(), "Failed to delete Workspace")
+			}
+		} else {
+			if ragengineObj != nil {
+				GinkgoWriter.Printf("Test failed, keep Workspace %s and RAGEngine %s\n",
+					workspaceObj.Name, ragengineObj.Name)
+			} else {
+				GinkgoWriter.Printf("Test failed, keep Workspace %s\n", workspaceObj.Name)
+			}
+		}
+	})
+}
+
+// validateWorkspacResourceStatus validates resource status
+func validateWorkspaceResourceStatus(workspaceObj *kaitov1beta1.Workspace) {
+	By("Checking the resource status", func() {
+		Eventually(func() bool {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj, &client.GetOptions{})
+
+			if err != nil {
+				return false
+			}
+
+			_, conditionFound := lo.Find(workspaceObj.Status.Conditions, func(condition metav1.Condition) bool {
+				return condition.Type == string(kaitov1alpha1.ConditionTypeResourceStatus) &&
+					condition.Status == metav1.ConditionTrue
+			})
+			return conditionFound
+		}, 25*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for workspace resource status to be ready")
+	})
+}
+
+func deleteRAGEngine(ragengineObj *kaitov1alpha1.RAGEngine) error {
+	By("Deleting ragengineObj", func() {
+		Eventually(func() error {
+			// Check if the workspace exists
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragengineObj.Namespace,
+				Name:      ragengineObj.Name,
+			}, ragengineObj)
+
+			if errors.IsNotFound(err) {
+				GinkgoWriter.Printf("RAGEngine %s does not exist, no need to delete\n", ragengineObj.Name)
+				return nil
+			}
+			if err != nil {
+				return fmt.Errorf("error checking if ragengine %s exists: %v", ragengineObj.Name, err)
+			}
+
+			err = utils.TestingCluster.KubeClient.Delete(ctx, ragengineObj, &client.DeleteOptions{})
+			if err != nil {
+				return fmt.Errorf("failed to delete ragengine %s: %v", ragengineObj.Name, err)
+			}
+			return nil
+		}, utils.PollTimeout, utils.PollInterval).Should(Succeed(), "Failed to delete ragengine")
+	})
+
+	return nil
+}
+
+// validateInferenceResource validates inference deployment
+func validateInferenceandRAGResource(objectMeta metav1.ObjectMeta, expectedReplicas int32, isStatefulSet bool) {
+	By("Checking the inference resource", func() {
+		Eventually(func() bool {
+			var err error
+			var readyReplicas int32
+
+			if isStatefulSet {
+				sts := &appsv1.StatefulSet{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      objectMeta.Name,
+						Namespace: objectMeta.Namespace,
+					},
+				}
+				err = utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+					Namespace: objectMeta.Namespace,
+					Name:      objectMeta.Name,
+				}, sts)
+				readyReplicas = sts.Status.ReadyReplicas
+
+			} else {
+				dep := &appsv1.Deployment{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      objectMeta.Name,
+						Namespace: objectMeta.Namespace,
+					},
+				}
+				err = utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+					Namespace: objectMeta.Namespace,
+					Name:      objectMeta.Name,
+				}, dep)
+				readyReplicas = dep.Status.ReadyReplicas
+			}
+
+			if err != nil {
+				GinkgoWriter.Printf("Error fetching resource: %v\n", err)
+				return false
+			}
+
+			if readyReplicas == expectedReplicas {
+				return true
+			}
+
+			return false
+		}, 20*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for inference resource to be ready")
+	})
+}
+
+func validateAssociatedService(objectMeta metav1.ObjectMeta) {
+	serviceName := objectMeta.Name
+	serviceNamespace := objectMeta.Namespace
+
+	By(fmt.Sprintf("Checking for service %s in namespace %s", serviceName, serviceNamespace), func() {
+		service := &v1.Service{}
+
+		Eventually(func() bool {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: serviceNamespace,
+				Name:      serviceName,
+			}, service)
+
+			if err != nil {
+				if errors.IsNotFound(err) {
+					GinkgoWriter.Printf("Service %s not found in namespace %s\n", serviceName, serviceNamespace)
+				} else {
+					GinkgoWriter.Printf("Error fetching service %s in namespace %s: %v\n", serviceName, serviceNamespace, err)
+				}
+				return false
+			}
+
+			GinkgoWriter.Printf("Found service: %s in namespace %s\n", serviceName, serviceNamespace)
+			return true
+		}, 10*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for service to be created")
+	})
+}
+
+// validateRAGEngineReadiness validates ragengine conditions
+func validateRAGEngineCondition(ragengineObj *kaitov1alpha1.RAGEngine, conditionType string, description string) {
+	By(fmt.Sprintf("Checking %s", description), func() {
+		Eventually(func() bool {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragengineObj.Namespace,
+				Name:      ragengineObj.Name,
+			}, ragengineObj, &client.GetOptions{})
+			if err != nil {
+				return false
+			}
+			_, conditionFound := lo.Find(ragengineObj.Status.Conditions, func(condition metav1.Condition) bool {
+				return condition.Type == conditionType &&
+					condition.Status == metav1.ConditionTrue
+			})
+			return conditionFound
+		}, 10*time.Minute, utils.PollInterval).Should(BeTrue(), fmt.Sprintf("Failed to wait for %s", description))
+	})
+}
+
+func deleteWorkspace(workspaceObj *kaitov1beta1.Workspace) error {
+	By("Deleting workspace", func() {
+		Eventually(func() error {
+			// Check if the workspace exists
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj)
+
+			if errors.IsNotFound(err) {
+				GinkgoWriter.Printf("Workspace %s does not exist, no need to delete\n", workspaceObj.Name)
+				return nil
+			}
+			if err != nil {
+				return fmt.Errorf("error checking if workspace %s exists: %v", workspaceObj.Name, err)
+			}
+
+			err = utils.TestingCluster.KubeClient.Delete(ctx, workspaceObj, &client.DeleteOptions{})
+			if err != nil {
+				return fmt.Errorf("failed to delete workspace %s: %v", workspaceObj.Name, err)
+			}
+			return nil
+		}, utils.PollTimeout, utils.PollInterval).Should(Succeed(), "Failed to delete workspace")
+	})
+
+	return nil
+}
+
+func createAndValidateIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) error {
+	podName := "index-pod"
+	By("Creating index pod", func() {
+		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/index \
+-H "Content-Type: application/json" \
+-d '{
+    "index_name": "kaito",
+    "documents": [
+        {
+            "text": "Kaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster",
+            "metadata": {"author": "kaito", "category": "kaito"}
+        }
+    ]
+}'`
+		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, pod, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create index pod")
+	})
+
+	By("Checking the index logs", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+
+			logs, err := utils.GetPodLogs(coreClient, ragengineObj.Namespace, podName, "")
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get logs from pod %s: %v\n", podName, err)
+				return false
+			}
+
+			return strings.Contains(logs, "Kaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster")
+		}, 4*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for index logs to be ready")
+	})
+
+	return nil
+}
+
+func createAndValidateQueryPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSearchQueries string, remote bool) error {
+	podName := "query-pod"
+	By("Creating query pod", func() {
+		var curlCommand string
+		// Note: Request without model specified should still succeed with vLLM. As model name is dynamically fetched.
+		if remote {
+			curlCommand = `curl -X POST ` + ragengineObj.Name + `:80/query \
+-H "Content-Type: application/json" \
+-d '{
+	"index_name": "kaito",
+    "query": "what is kaito?",
+    "llm_params": {
+      "max_tokens": 50,
+      "temperature": 0
+    }
+}'`
+		} else {
+			curlCommand = `curl -X POST ` + ragengineObj.Name + `:80/query \
+-H "Content-Type: application/json" \
+-d '{
+	"index_name": "kaito",
+    "model": "phi-3-mini-128k-instruct",
+    "query": "what is kaito?",
+    "llm_params": {
+      "max_tokens": 50,
+      "temperature": 0
+    }
+}'`
+		}
+		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, pod, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create query pod")
+	})
+
+	By("Waiting for query pod to be running", func() {
+		Eventually(func() bool {
+			pod := &v1.Pod{}
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragengineObj.Namespace,
+				Name:      podName,
+			}, pod)
+			if err != nil {
+				return false
+			}
+			return pod.Status.Phase == v1.PodRunning || pod.Status.Phase == v1.PodSucceeded
+		}, 5*time.Minute, utils.PollInterval).Should(BeTrue(), "Query pod did not reach Running or Succeeded state")
+	})
+
+	By("Checking the query logs", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+			logs, err := utils.GetPodLogs(coreClient, ragengineObj.Namespace, podName, "")
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get logs from pod %s: %v\n", podName, err)
+				return false
+			}
+			return strings.Contains(logs, expectedSearchQueries)
+		}, 4*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for query logs to be ready")
+	})
+
+	return nil
+}
+
+func createAndValidatePersistPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedPersistResult string) error {
+	podName := "persist-pod"
+	By("Creating Persist pod", func() {
+		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/persist/kaito`
+		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, pod, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create persist pod")
+	})
+
+	By("Waiting for persist pod to be running", func() {
+		Eventually(func() bool {
+			pod := &v1.Pod{}
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragengineObj.Namespace,
+				Name:      podName,
+			}, pod)
+			if err != nil {
+				return false
+			}
+			return pod.Status.Phase == v1.PodRunning || pod.Status.Phase == v1.PodSucceeded
+		}, 5*time.Minute, utils.PollInterval).Should(BeTrue(), "Persist pod did not reach Running or Succeeded state")
+	})
+
+	By("Checking the persist logs", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+			logs, err := utils.GetPodLogs(coreClient, ragengineObj.Namespace, podName, "")
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get logs from pod %s: %v\n", podName, err)
+				return false
+			}
+			return strings.Contains(logs, expectedPersistResult)
+		}, 4*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for persist logs to be ready")
+	})
+
+	return nil
+}
+
+func createAndValidateLoadPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedLoadResult string) error {
+	podName := "load-pod"
+	By("Creating Load Pod", func() {
+		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/load/kaito?overwrite=True`
+		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, pod, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create load pod")
+	})
+	// Wait for the pod to be running before attempting to fetch logs.
+	By("Waiting for load pod to be running", func() {
+		Eventually(func() bool {
+			pod := &v1.Pod{}
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: ragengineObj.Namespace,
+				Name:      podName,
+			}, pod)
+			if err != nil {
+				return false
+			}
+			return pod.Status.Phase == v1.PodRunning || pod.Status.Phase == v1.PodSucceeded
+		}, 5*time.Minute, utils.PollInterval).Should(BeTrue(), "Load pod did not reach Running or Succeeded state")
+	})
+	By("Checking the load logs", func() {
+		Eventually(func() bool {
+			coreClient, err := utils.GetK8sClientset()
+			if err != nil {
+				GinkgoWriter.Printf("Failed to create core client: %v\n", err)
+				return false
+			}
+			logs, err := utils.GetPodLogs(coreClient, ragengineObj.Namespace, podName, "")
+			if err != nil {
+				GinkgoWriter.Printf("Failed to get logs from pod %s: %v\n", podName, err)
+				return false
+			}
+			return strings.Contains(logs, expectedLoadResult)
+		}, 4*time.Minute, utils.PollInterval).Should(BeTrue(), "Failed to wait for load logs to be ready")
+	})
+	return nil
+}
+
+func GenerateCURLPodManifest(podName, curlCommand, namespace string) *v1.Pod {
+	return &v1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      podName,
+			Namespace: namespace,
+		},
+		Spec: v1.PodSpec{
+			RestartPolicy: v1.RestartPolicyNever,
+			Containers: []v1.Container{
+				{
+					Name:    "curl",
+					Image:   "curlimages/curl:latest",
+					Command: []string{"/bin/sh", "-c"},
+					Args:    []string{curlCommand},
+				},
+			},
+		},
+	}
+}
+
+func createAndValidateSecret() {
+	hfToken := os.Getenv("HF_TOKEN")
+	secret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "huggingface-token",
+			Namespace: namespaceName,
+		},
+		Data: map[string][]byte{
+			"LLM_ACCESS_SECRET": []byte(hfToken),
+		},
+		Type: v1.SecretTypeOpaque,
+	}
+	By("Creating secret", func() {
+		Eventually(func() error {
+			return utils.TestingCluster.KubeClient.Create(ctx, secret, &client.CreateOptions{})
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to create secret   %s", secret.Name)
+
+		By("Validating secret creation", func() {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: secret.Namespace,
+				Name:      secret.Name,
+			}, secret, &client.GetOptions{})
+			Expect(err).NotTo(HaveOccurred())
+		})
+	})
+}