Repository: kaito-project/kaito

Issue:
Unable to run a model with an adapter
**Describe the bug**

Unable to run a kaito workspace with an adapter

**Steps To Reproduce**
1. Created my own GPU nodes and added `apps: gpu` label
2. Downloaded `adapter_config.json` `adapter_model.bin` from https://huggingface.co/tomar840/falcon-7b-math-reasoning/tree/main
3. Built a container image with this Dockerfile:

```Dockerfile
FROM busybox:latest

RUN mkdir -p /data
COPY adapter_config.json /data/
COPY adapter_model.bin /data/
```
4. Deploy this workspace yaml

```sh
cat <<EOF | kubectl apply -f -
apiVersion: kaito.sh/v1beta1
kind: Workspace
metadata:
  name: workspace-falcon-7b-instruct-adapter
  annotations:
    kaito.sh/runtime: "transformers"
resource:
  preferredNodes:
    - aks-gpunodes-23967602-vmss000000
  labelSelector:
    matchLabels:
      apps: gpu
inference:
  preset:
    name: "falcon-7b-instruct"
  adapters:
    - source:
        name: "falcon-7b-adapter"
        image:  "ritazh/falcon-7b-math-reasoning-adapter:latest"
      strength: "0.2"
EOF
```
Getting this error from workspace status:

> 'Deployment.apps "workspace-falcon-7b-instruct-adapter" is invalid: [spec.template.spec.initContainers[1].volumeMounts[1].name:       Required value, spec.template.spec.initContainers[1].volumeMounts[1].name: Not       found: "", spec.template.spec.initContainers[1].volumeMounts[1].mountPath: Required       value]'

**Expected behavior**

**Logs**

**Environment**

- Kubernetes version (use `kubectl version`): 1.32.6
- OS (e.g: `cat /etc/os-release`):
- Install tools: kaito v0.6.0
- Others:

**Additional context**

Discussion:

Solution patch:
diff --git a/.github/workflows/e2e-preset-test.yaml b/.github/workflows/e2e-preset-test.yaml
deleted file mode 100644
index faada82..0000000
--- a/.github/workflows/e2e-preset-test.yaml
+++ /dev/null
@@ -1,386 +0,0 @@
-name: E2E Preset Test
-
-on:
-    workflow_run:
-        workflows: ["Build and Push Preset Models"]
-        types:
-            - completed
-    workflow_dispatch:
-        inputs:
-            test-on-vllm:
-                type: boolean
-                default: false
-                description: "Test on VLLM runtime"
-
-env:
-    GO_VERSION: "1.24"
-    RUNTIME: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.test-on-vllm == 'true') && 'vllm' || 'hf' }}
-
-permissions:
-    id-token: write
-    contents: read
-
-jobs:
-  determine-models:
-    if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
-    runs-on: ubuntu-latest
-    environment: preset-env
-    outputs:
-      matrix: ${{ steps.affected_models.outputs.matrix }}
-      is_matrix_empty: ${{ steps.check_matrix_empty.outputs.is_empty }}
-      full_matrix: ${{ steps.images.outputs.full_matrix }}
-    steps:
-      - name: Checkout
-        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
-        with:
-          submodules: true
-          fetch-depth: 0
-
-      - name: Download crane
-        run: |
-            if ! command -v crane &> /dev/null; then
-                wget -q https://github.com/google/go-containerregistry/releases/latest/download/go-containerregistry_Linux_x86_64.tar.gz -O /tmp/crane.tar.gz
-                tar -xzf /tmp/crane.tar.gz -C /tmp crane
-                chmod +x /tmp/crane
-                sudo mv /tmp/crane /usr/local/bin/crane
-            fi
-
-      - name: Determine Affected Models
-        id: affected_models
-        run: python3 .github/determine_missing_preset_images.py
-
-      - name: Print Determined Models
-        run: |
-            echo "Output from determine_models: ${{ steps.affected_models.outputs.matrix }}"
-
-      - name: Check if Matrix is Empty
-        id: check_matrix_empty
-        run: |
-            if [ "${{ steps.affected_models.outputs.matrix }}" == "[]" ] || [ -z "${{ steps.affected_models.outputs.matrix }}" ]; then
-                echo "is_empty=true" >> $GITHUB_OUTPUT
-            else
-                echo "is_empty=false" >> $GITHUB_OUTPUT
-            fi
-
-      - name: Add Config info for Testing
-        if: steps.check_matrix_empty.outputs.is_empty == 'false'
-        id: images
-        run: |
-            # Read the additional configurations from e2e-preset-configs.json
-            CONFIGS=$(cat .github/e2e-preset-configs.json | jq -c '.matrix.image')
-
-            echo "CONFIGS:"
-            echo $CONFIGS
-
-            ORIGINAL_MATRIX='${{ steps.affected_models.outputs.matrix }}'
-            # Check if "base" exists and add test cases for base image
-            BASE_IMAGE_TESTS=$(echo "$ORIGINAL_MATRIX" | jq -c '
-                map(select(.name == "base")) |
-                if length > 0 then
-                    .[0] as $base |
-                    [
-                        ($base | .name = "vllm-distributed" | . + {"image-name": "base"})
-                        # example to add new case
-                        # ($base | .name = "test2" | . + {"image-name": "base"})
-                    ]
-                else
-                    []
-                end
-            ')
-
-            echo "BASE_IMAGE_TESTS:"
-            echo $BASE_IMAGE_TESTS
-
-            # Combine original matrix with test items
-            EXTENDED_MATRIX=$(echo "$ORIGINAL_MATRIX" | jq --argjson new "$BASE_IMAGE_TESTS" -c '. + $new')
-
-            # Pseudocode for combining matrices
-            # COMBINED_MATRIX = []
-            # for model in MATRIX:
-            #     for config in CONFIGS:
-            #         if config['name'] == model['name']:
-            #             combined = {**model, **config}
-            #             COMBINED_MATRIX.append(combined)
-            #             break
-            COMBINED_MATRIX=$(echo "$EXTENDED_MATRIX" | jq --argjson configs "$CONFIGS" -c '
-                map(. as $model | $configs[] | select(.name == $model.name) | $model + .)
-            ')
-
-            # Filter out items with A100 VMs
-            # FIXME: add it back when A100 is available
-            COMBINED_MATRIX=$(echo "$COMBINED_MATRIX" | jq -c '
-                map(select(."node-vm-size" | contains("A100") | not))
-            ')
-
-            echo "full_matrix=$COMBINED_MATRIX" >> $GITHUB_OUTPUT
-
-      - name: Print Combined Matrix
-        if: steps.check_matrix_empty.outputs.is_empty == 'false'
-        run: |
-            echo "Combined Matrix:"
-            echo '${{ steps.images.outputs.full_matrix }}'
-
-  e2e-preset-tests:
-    needs: determine-models
-    if: needs.determine-models.outputs.is_matrix_empty == 'false' && (github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success')
-    runs-on: ubuntu-latest
-    environment: preset-env
-    strategy:
-      fail-fast: false
-      max-parallel: 5
-      matrix:
-        # Ex matrix element:
-        # {"name":"falcon-40b","type":"text-generation","version":"#",
-        # "runtime":"tfs","tag":"0.0.1","node-count":1,
-        # "node-vm-size":"Standard_NC96ads_A100_v4", "node-osdisk-size":400}
-        model: ${{fromJson(needs.determine-models.outputs.full_matrix)}}
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v5.0.0
-        with:
-            submodules: true
-            fetch-depth: 0
-
-      - name: Set OSS Flag
-        run: echo "MODEL_IS_OSS=${{ matrix.model.OSS }}" >> $GITHUB_ENV
-
-      - name: 'Az CLI login'
-        uses: azure/login@v2.3.0
-        with:
-            client-id: ${{ secrets.AZURE_CLIENT_ID }}
-            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
-            allow-no-subscriptions: true
-
-      - name: 'Set ACR Subscription'
-        run: az account set --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}}
-
-      - name: Check if Image exists in ACR
-        id: check_image
-        run: |
-            ACR_NAME=${{ secrets.ACR_AMRT_USERNAME }}
-            IMAGE_NAME=${{ matrix.model.image-name || matrix.model.name }}
-            TAG=${{ matrix.model.tag }}
-
-            # Use '|| true' to prevent script from exiting with an error if the repository is not found
-            TAGS=$(az acr repository show-tags -n $ACR_NAME --repository $IMAGE_NAME --output tsv || true)
-
-            if [[ -z "$TAGS" ]]; then
-                echo "Image $IMAGE_NAME:$TAG or repository not found in $ACR_NAME."
-                echo "IMAGE_EXISTS=false" >> $GITHUB_OUTPUT
-            else
-                if echo "$TAGS" | grep -q "^$TAG$"; then
-                    echo "IMAGE_EXISTS=true" >> $GITHUB_OUTPUT
-                else
-                    echo "IMAGE_EXISTS=false" >> $GITHUB_OUTPUT
-                    echo "Image $IMAGE_NAME:$TAG not found in $ACR_NAME."
-                fi
-            fi
-
-      - name: Exit if Image for testing does not exist
-        if: steps.check_image.outputs.IMAGE_EXISTS == 'false'
-        run: |
-            echo "Image doesn't exist in ACR, remember to build image for testing first (preset-image-build)"
-            exit 1
-
-      - name: Set up kubectl context
-        run: |
-          az aks get-credentials --resource-group llm-test --name GitRunner
-
-      - name: Get testing workload
-        id: workload
-        run: |
-            NODEPOOL_NAME=${{ matrix.model.node_pool }}
-            echo "NODEPOOL_NAME=$NODEPOOL_NAME" >> $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=${{ matrix.model.resource-type || '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.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.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.workload.outputs.NODEPOOL_NAME }} \
-                    --node-taints sku=gpu:NoSchedule
-            else
-                NODEPOOL_STATE=$(az aks nodepool show \
-                                --name ${{ steps.workload.outputs.NODEPOOL_NAME }} \
-                                --cluster-name GitRunner \
-                                --resource-group llm-test \
-                                --query 'provisioningState' -o tsv)
-                echo "NODEPOOL_STATE: $NODEPOOL_STATE"
-                if [ "$NODEPOOL_STATE" != "Succeeded" ]; then
-                    echo "Nodepool exists but is not in a Succeeded state. Please check manually."
-                    exit 1
-                else
-                    echo "Nodepool already exists and is in a running state."
-                fi
-            fi
-
-      - name: Create workload
-        run: |
-            kubectl apply -f ${{steps.workload.outputs.WORKLOAD_FILE}}
-
-      - name: Wait for Resource to be ready
-        run: |
-            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: |
-            POD_NAME=$(kubectl get pods -l app=${{steps.workload.outputs.WORKLOAD_NAME}} -o jsonpath="{.items[0].metadata.name}")
-            kubectl logs $POD_NAME | grep "Adapter added:" | grep "${{ matrix.model.expected_adapter }}" || (echo "Adapter not loaded or incorrect adapter loaded" && exit 1)
-
-      - name: Install testing commands
-        run: |
-            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.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 }}"
-
-            # Retry logic - attempt up to 3 times
-            MAX_ATTEMPTS=3
-            ATTEMPT=1
-
-            while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
-                echo "Attempt $ATTEMPT of $MAX_ATTEMPTS"
-
-                if [[ "${{ env.RUNTIME }}" == *"vllm"* ]]; then
-                    if 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" \
-                    -d '{
-                        "model": "test",
-                        "messages": [
-                        {
-                            "role": "system",
-                            "content": "You are a helpful assistant."
-                        },
-                        {
-                            "role": "user",
-                            "content": "Hello!"
-                        }
-                        ]
-                        }' \
-                    http://localhost:5000/v1/chat/completions; then
-                        echo "Inference test succeeded"
-                        break
-                    fi
-                else
-                    if 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" \
-                    -d '{
-                        "prompt":"Girafatron is obsessed with giraffes, the most glorious animal on the face of this Earth. Giraftron believes all other animals are irrelevant when compared to the glorious majesty of the giraffe.\nDaniel: Hello, Girafatron!\nGirafatron:",
-                        "return_full_text": false,
-                        "clean_up_tokenization_spaces": false,
-                        "prefix": null,
-                        "handle_long_generation": null,
-                        "generate_kwargs": {
-                                "max_length":200,
-                                "min_length":0,
-                                "do_sample":true,
-                                "early_stopping":false,
-                                "num_beams":1,
-                                "num_beam_groups":1,
-                                "diversity_penalty":0.0,
-                                "temperature":1.0,
-                                "top_k":10,
-                                "top_p":1,
-                                "typical_p":1,
-                                "repetition_penalty":1,
-                                "length_penalty":1,
-                                "no_repeat_ngram_size":0,
-                                "encoder_no_repeat_ngram_size":0,
-                                "bad_words_ids":null,
-                                "num_return_sequences":1,
-                                "output_scores":false,
-                                "return_dict_in_generate":false,
-                                "forced_bos_token_id":null,
-                                "forced_eos_token_id":null,
-                                "remove_invalid_values":null
-                            }
-                        }' \
-                    http://localhost:5000/chat; then
-                        echo "Inference test succeeded"
-                        break
-                    fi
-                fi
-
-                if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
-                    echo "Test failed, retrying in 5 seconds..."
-                    sleep 5
-                else
-                    echo "Test failed after $MAX_ATTEMPTS attempts"
-                    exit 1
-                fi
-
-                ATTEMPT=$((ATTEMPT + 1))
-            done
-
-      - name: Cleanup
-        if: always()
-        run: |
-            # Only proceed if RESOURCE_TYPE is set (else resource wasn't created)
-            if [ -n "${{ steps.workload.outputs.RESOURCE_TYPE }}" ]; then
-                # Use RESOURCE_TYPE from the previous step
-                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
-                    kubectl logs $RESOURCE_TYPE/${{steps.workload.outputs.WORKLOAD_NAME}}
-                    kubectl delete $RESOURCE_TYPE ${{steps.workload.outputs.WORKLOAD_NAME}}
-                fi
-            fi
-
-            # Check and Delete K8s Service if it exists
-            if kubectl get svc ${{steps.workload.outputs.WORKLOAD_NAME}} > /dev/null 2>&1; then
-                kubectl delete svc ${{steps.workload.outputs.WORKLOAD_NAME}}
-            fi
-
-            # Check and Delete AKS Nodepool if it exists
-            if [ -n "${{ steps.workload.outputs.NODEPOOL_NAME }}" ]; then
-                NODEPOOL_EXIST=$(az aks nodepool show \
-                                --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.workload.outputs.NODEPOOL_NAME }} \
-                    --cluster-name GitRunner \
-                    --resource-group llm-test
-                fi
-            fi
diff --git a/.github/workflows/e2e-preset-tuning-test.yaml b/.github/workflows/e2e-preset-tuning-test.yaml
deleted file mode 100644
index 9ecb4f3..0000000
--- a/.github/workflows/e2e-preset-tuning-test.yaml
+++ /dev/null
@@ -1,135 +0,0 @@
-name: E2E Preset tuning Test
-
-on:
-    workflow_run:
-        workflows: ["Build and Push Preset Models"]
-        types:
-            - completed
-    workflow_dispatch: {}
-
-env:
-    GO_VERSION: "1.24"
-
-permissions:
-    id-token: write
-    contents: read
-
-jobs:
-  e2e-preset-tuning-tests:
-    if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
-    runs-on: ubuntu-latest
-    environment: preset-env
-    steps:
-      - name: Checkout
-        uses: actions/checkout@v5.0.0
-        with:
-            submodules: true
-            fetch-depth: 0
-
-      - name: 'Az CLI login'
-        uses: azure/login@v2.3.0
-        with:
-            client-id: ${{ secrets.AZURE_CLIENT_ID }}
-            tenant-id: ${{ secrets.AZURE_TENANT_ID }}
-            allow-no-subscriptions: true
-
-      - name: 'Set ACR Subscription'
-        run: az account set --subscription ${{secrets.AZURE_SUBSCRIPTION_ID}}
-
-      - name: Set up kubectl context
-        run: |
-          az aks get-credentials --resource-group llm-test --name GitRunner
-
-      - name: Get test meta
-        id: get_test_meta
-        run: |
-            CONFIG=$(jq -c '.matrix.image[] | select(.name == "tuning")' .github/e2e-preset-configs.json)
-
-            echo "TAG=0.2.0" >> $GITHUB_OUTPUT
-            for row in $(echo "${CONFIG}" | jq -r 'to_entries|map("\(.key)=\(.value|tostring)")|.[]'); do
-                echo "${row}" >> $GITHUB_OUTPUT
-            done
-
-      - name: Create Nodepool
-        run: |
-            NODEPOOL_EXIST=$(az aks nodepool show \
-                            --name ${{ steps.get_test_meta.outputs.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_test_meta.outputs.name }} \
-                    --cluster-name GitRunner \
-                    --resource-group llm-test \
-                    --node-count ${{ steps.get_test_meta.outputs.node-count }} \
-                    --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
-            else
-                NODEPOOL_STATE=$(az aks nodepool show \
-                                --name ${{ steps.get_test_meta.outputs.name }} \
-                                --cluster-name GitRunner \
-                                --resource-group llm-test \
-                                --query 'provisioningState' -o tsv)
-                echo "NODEPOOL_STATE: $NODEPOOL_STATE"
-                if [ "$NODEPOOL_STATE" != "Succeeded" ]; then
-                    echo "Nodepool exists but is not in a Succeeded state. Please check manually."
-                    exit 1
-                else
-                    echo "Nodepool already exists and is in a running state."
-                fi
-            fi
-
-      - name: Replace repo and Deploy Resource to K8s
-        run: |
-            sed -i "s/REPO_HERE/${{ secrets.ACR_AMRT_USERNAME }}/g" presets/workspace/test/manifests/tuning/tuning.yaml
-            sed -i "s/TAG_HERE/${{ steps.get_test_meta.outputs.TAG }}/g" presets/workspace/test/manifests/tuning/tuning.yaml
-            kubectl apply -f presets/workspace/test/manifests/tuning/tuning.yaml
-
-      - name: Wait for tuning job to be ready
-        shell: bash {0}
-        run: |
-            retval_complete=1
-            retval_failed=1
-            count=0
-            max_retries=60
-            while [[ $retval_complete -ne 0 ]] && [[ $retval_failed -ne 0 ]] && [[ $count -lt $max_retries ]]; do
-                sleep 10
-                output=$(kubectl wait --for=condition=failed job/tuning-example --timeout=0 2>&1)
-                retval_failed=$?
-                output=$(kubectl wait --for=condition=complete job/tuning-example --timeout=0 2>&1)
-                retval_complete=$?
-                count=$((count + 1))
-            done
-
-            if [ $retval_failed -eq 0 ]; then
-                echo "Job failed. Please check logs."
-                exit 1
-            elif [ $retval_complete -ne 0 ]; then
-                echo "Job timeout."
-                exit 1
-            else
-                echo "Job succeeded."
-            fi
-      - name: Cleanup
-        if: always()
-        run: |
-            kubectl delete --wait=true -f presets/workspace/test/manifests/tuning/tuning.yaml
-
-            # Check and Delete AKS Nodepool if it exists
-            NODEPOOL_EXIST=$(az aks nodepool show \
-                            --name ${{ steps.get_test_meta.outputs.name }} \
-                            --cluster-name GitRunner \
-                            --resource-group llm-test \
-                            --query 'name' -o tsv || echo "")
-
-            if [ -n "$NODEPOOL_EXIST" ]; then
-                echo "deleting nodepool"
-                az aks nodepool delete \
-                --name ${{ steps.get_test_meta.outputs.name }} \
-                --cluster-name GitRunner \
-                --resource-group llm-test
-            fi
diff --git a/pkg/workspace/manifests/manifests.go b/pkg/workspace/manifests/manifests.go
index c40b797..0f4f95c 100644
--- a/pkg/workspace/manifests/manifests.go
+++ b/pkg/workspace/manifests/manifests.go
@@ -265,11 +265,15 @@ func GeneratePullerContainers(wObj *kaitov1beta1.Workspace, volumeMounts []corev
 		source := adapter.Source
 		sourceName := source.Name
 
-		var volumeMount corev1.VolumeMount
-		var volume corev1.Volume
+		outputDirectory := path.Join("/mnt/adapter", sourceName)
+		pullerContainer := image.NewPullerContainer(source.Image, outputDirectory)
+		pullerContainer.Name += "-" + sourceName
+		pullerContainer.VolumeMounts = volumeMounts
+
 		if len(source.ImagePullSecrets) > 0 {
-			volume, volumeMount = utils.ConfigImagePullSecretVolume(sourceName+"-inference-adapter", source.ImagePullSecrets)
+			volume, volumeMount := utils.ConfigImagePullSecretVolume(sourceName+"-inference-adapter", source.ImagePullSecrets)
 			volumes = append(volumes, volume)
+			pullerContainer.VolumeMounts = append(pullerContainer.VolumeMounts, volumeMount)
 		}
 
 		if adapter.Strength != nil {
@@ -280,10 +284,6 @@ func GeneratePullerContainers(wObj *kaitov1beta1.Workspace, volumeMounts []corev
 			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)
 	}
 



Test patch:
diff --git a/pkg/workspace/manifests/manifests_test.go b/pkg/workspace/manifests/manifests_test.go
index c24e46f..c98f2b2 100644
--- a/pkg/workspace/manifests/manifests_test.go
+++ b/pkg/workspace/manifests/manifests_test.go
@@ -15,6 +15,7 @@ package manifests
 
 import (
 	"encoding/json"
+	"strings"
 	"testing"
 
 	helmv2 "github.com/fluxcd/helm-controller/api/v2"
@@ -131,3 +132,135 @@ func TestGenerateInferencePoolHelmRelease(t *testing.T) {
 		})
 	}
 }
+
+func TestGeneratePullerContainers(t *testing.T) {
+	base := test.MockWorkspaceWithPreset.DeepCopy()
+	base.Name = "puller-ws"
+	base.Namespace = "kaito"
+
+	strength := func(s string) *string { return &s }
+
+	volumeMounts := []corev1.VolumeMount{{Name: "shared", MountPath: "/mnt/shared"}}
+
+	tests := []struct {
+		name               string
+		adapters           []kaitov1beta1.AdapterSpec
+		volumeMounts       []corev1.VolumeMount
+		expectedContainers int
+		expectedEnvVars    map[string]string // name -> value
+		expectedVolumes    int
+		verify             func(t *testing.T, containers []corev1.Container, envVars []corev1.EnvVar, volumes []corev1.Volume)
+	}{
+		{
+			name:               "no adapters",
+			adapters:           nil,
+			volumeMounts:       volumeMounts,
+			expectedContainers: 0,
+			expectedEnvVars:    map[string]string{},
+			expectedVolumes:    0,
+		},
+		{
+			name: "single adapter with strength and secrets",
+			adapters: []kaitov1beta1.AdapterSpec{
+				{
+					Source: &kaitov1beta1.DataSource{
+						Name:             "adapterA",
+						Image:            "docker.io/library/alpine:latest",
+						ImagePullSecrets: []string{"secretA", "secretB"},
+					},
+					Strength: strength("0.5"),
+				},
+			},
+			volumeMounts:       volumeMounts,
+			expectedContainers: 1,
+			expectedEnvVars:    map[string]string{"adapterA": "0.5"},
+			expectedVolumes:    1,
+			verify: func(t *testing.T, containers []corev1.Container, envVars []corev1.EnvVar, volumes []corev1.Volume) {
+				if assert.Len(t, containers, 1) {
+					c := containers[0]
+					assert.Equal(t, "puller-adapterA", c.Name)
+					assert.Equal(t, "quay.io/skopeo/stable:v1.18.0-immutable", c.Image)
+					if assert.Len(t, c.Args, 1) {
+						assert.Contains(t, c.Args[0], "/mnt/adapter/adapterA")
+					}
+					// base volumeMount + secret volumeMount
+					assert.GreaterOrEqual(t, len(c.VolumeMounts), 1)
+					assert.Equal(t, volumeMounts[0], c.VolumeMounts[0])
+					// secret volume mount appended
+					assert.Equal(t, "docker-config-adapterA-inference-adapter", c.VolumeMounts[len(c.VolumeMounts)-1].Name)
+					assert.Equal(t, "/root/.docker/config.d/adapterA-inference-adapter", c.VolumeMounts[len(c.VolumeMounts)-1].MountPath)
+				}
+				if assert.Len(t, volumes, 1) {
+					v := volumes[0]
+					assert.Equal(t, "docker-config-adapterA-inference-adapter", v.Name)
+					if assert.NotNil(t, v.VolumeSource.Projected) {
+						assert.Len(t, v.VolumeSource.Projected.Sources, 2)
+					}
+				}
+			},
+		},
+		{
+			name: "multiple adapters mixed",
+			adapters: []kaitov1beta1.AdapterSpec{
+				{
+					Source: &kaitov1beta1.DataSource{
+						Name:  "adapter1",
+						Image: "docker.io/library/busybox:latest",
+					},
+					Strength: strength("0.7"),
+				},
+				{
+					Source: &kaitov1beta1.DataSource{
+						Name:  "adapter2",
+						Image: "docker.io/library/alpine:3.19",
+					},
+				},
+			},
+			volumeMounts:       volumeMounts,
+			expectedContainers: 2,
+			expectedEnvVars:    map[string]string{"adapter1": "0.7"},
+			expectedVolumes:    0,
+			verify: func(t *testing.T, containers []corev1.Container, envVars []corev1.EnvVar, volumes []corev1.Volume) {
+				// verify ordering & fields
+				names := []string{"puller-adapter1", "puller-adapter2"}
+				gotNames := []string{containers[0].Name, containers[1].Name}
+				assert.Equal(t, names, gotNames)
+				for _, c := range containers {
+					if assert.Len(t, c.Args, 1) {
+						// ensure path for corresponding adapter is inside script
+						parts := strings.Split(c.Name, "-")
+						adapterName := parts[len(parts)-1]
+						assert.Contains(t, c.Args[0], "/mnt/adapter/"+adapterName)
+					}
+					assert.Equal(t, volumeMounts[0], c.VolumeMounts[0])
+				}
+			},
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			w := base.DeepCopy()
+			if w.Inference == nil {
+				w.Inference = &kaitov1beta1.InferenceSpec{}
+			}
+			w.Inference.Adapters = tc.adapters
+
+			containers, envVars, volumes := GeneratePullerContainers(w, tc.volumeMounts)
+
+			assert.Len(t, containers, tc.expectedContainers)
+			assert.Len(t, volumes, tc.expectedVolumes)
+
+			// build map for env var assertions
+			envMap := make(map[string]string, len(envVars))
+			for _, e := range envVars {
+				envMap[e.Name] = e.Value
+			}
+			assert.Equal(t, tc.expectedEnvVars, envMap)
+
+			if tc.verify != nil {
+				tc.verify(t, containers, envVars, volumes)
+			}
+		})
+	}
+}