Repository: kaito-project/kaito

Issue:
KAITO should not default instanceType
currently, kaito defaults `instanceType` to `Standard_NC24ads_A100_v4`. not only this is very Azure specific, it is also going to be sunset sooner or later

Discussion:
@kaito-pr-agent: ### :bulb: Auto-generated documentation-based answer:
Currently, the latest release of KAITO is v0.5.0, released on July 2nd, 2025. This version includes Retrieval Augmented Generation (RAG) support using LlamaIndex and FAISS.

#### Relevant Sources:

> - https://github.com/kaito-project/kaito/blob/main/README.md


Solution patch:
diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml
index 46c2e45..cd10815 100644
--- a/.github/workflows/trivy.yaml
+++ b/.github/workflows/trivy.yaml
@@ -22,6 +22,10 @@ jobs:
             image: test/kaito-rag-service:v0.0.1
             registry: test
             build_flags: "--target dependencies"
+          - target: docker-build-kaito-base
+            image: test/kaito-base:v0.0.1
+            registry: test
+            build_flags: "--target dependencies"
 
     steps:
 
diff --git a/Makefile b/Makefile
index 623939c..87055b9 100644
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,13 @@ GPU_PROVISIONER_VERSION ?= 0.3.7
 RAGENGINE_IMG_NAME ?= ragengine
 IMG_TAG ?= $(subst v,,$(VERSION))
 
+# injection variables
+INJECTION_ROOT := github.com/kaito-project/kaito/pkg/version
+BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
+GIT_VERSION := $(shell git describe --tags --always --dirty)
+LDFLAGS := -X '$(INJECTION_ROOT).Version=$(GIT_VERSION)' \
+		   -X '$(INJECTION_ROOT).BuildDate=$(BUILD_DATE)'
+
 ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
 BIN_DIR := $(abspath $(ROOT_DIR)/bin)
 
@@ -265,7 +272,7 @@ create-eks-cluster: ## Create an EKS cluster.
 BUILDX_BUILDER_NAME ?= img-builder
 OUTPUT_TYPE ?= type=registry
 QEMU_VERSION ?= 7.2.0-1
-ARCH ?= amd64,arm64
+ARCH ?= amd64
 BUILDKIT_VERSION ?= v0.18.1
 
 RAGENGINE_IMAGE_NAME ?= ragengine
@@ -275,6 +282,9 @@ RAGENGINE_SERVICE_IMG_NAME ?= kaito-rag-service
 RAGENGINE_SERVICE_IMG_TAG ?= v0.0.1
 RAGENGINE_SERVICE_APP_VERSION := $(subst v,,$(RAGENGINE_SERVICE_IMG_TAG))
 
+KAITO_BASE_IMG_NAME ?= kaito-base
+KAITO_BASE_IMG_TAG ?= v0.0.1
+
 E2E_IMAGE_NAME ?= kaito-e2e
 E2E_IMAGE_TAG ?= v0.0.1
 
@@ -293,6 +303,8 @@ docker-build-workspace: docker-buildx ## Build Docker image for workspace.
 		--file ./docker/workspace/Dockerfile \
 		--output=$(OUTPUT_TYPE) \
 		--platform="linux/$(ARCH)" \
+		--build-arg VERSION=$(GIT_VERSION) \
+		--build-arg BUILD_DATE=$(BUILD_DATE) \
 		--pull \
 		$(BUILD_FLAGS) \
 		--tag $(REGISTRY)/$(IMG_NAME):$(IMG_TAG) .
@@ -303,11 +315,25 @@ docker-build-ragengine: docker-buildx ## Build Docker image for RAG Engine.
 		--file ./docker/ragengine/Dockerfile \
 		--output=$(OUTPUT_TYPE) \
 		--platform="linux/$(ARCH)" \
+		--build-arg VERSION=$(GIT_VERSION) \
+		--build-arg BUILD_DATE=$(BUILD_DATE) \
 		--pull \
 		$(BUILD_FLAGS) \
 		--tag $(REGISTRY)/$(RAGENGINE_IMAGE_NAME):$(IMG_TAG) .
 
-.PHONY: docker-build-rag-service
+.PHONY: docker-build-kaito-base
+docker-build-kaito-base: docker-buildx ## Build Docker image for KAITO base.
+	docker buildx build \
+        --build-arg VERSION=$(KAITO_BASE_IMG_TAG) \
+		--build-arg MODEL_TYPE=text-generation \
+        --platform="linux/$(ARCH)" \
+        --output=$(OUTPUT_TYPE) \
+        --file ./docker/presets/models/tfs/Dockerfile \
+        --pull \
+		$(BUILD_FLAGS) \
+        --tag $(REGISTRY)/$(KAITO_BASE_IMG_NAME):$(KAITO_BASE_IMG_TAG) .
+
+.PHONY: docker-build-ragservice
 docker-build-ragservice: docker-buildx ## Build Docker image for RAG Engine service.
 	docker buildx build \
         --build-arg VERSION=$(RAGENGINE_SERVICE_APP_VERSION) \
@@ -490,11 +516,11 @@ aws-karpenter-helm: ## Install AWS Karpenter Helm chart and set AWS env vars and
 
 .PHONY: build-workspace
 build-workspace: manifests generate fmt vet ## Build manager binary.
-	go build -o bin/workspace-manager cmd/workspace/*.go
+	go build -ldflags "$(LDFLAGS)" -o bin/workspace-manager cmd/workspace/*.go
 
 .PHONY: run-workspace
 run-workspace: manifests generate fmt vet ## Run a controller from your host.
-	go run ./cmd/workspace/main.go
+	go run -ldflags "$(LDFLAGS)" ./cmd/workspace/main.go
 
 .PHONY: localbin
 localbin: $(LOCALBIN) ## Create folder for installing local binaries.
@@ -511,11 +537,11 @@ $(LOCALBIN): ## Create folder for installing local binaries.
 
 .PHONY: build-ragengine
 build-ragengine: manifests generate fmt vet ## Build RAG Engine binary.
-	go build -o bin/rag-engine-manager cmd/ragengine/*.go
+	go build -ldflags "$(LDFLAGS)" -o bin/rag-engine-manager cmd/ragengine/*.go
 
 .PHONY: run-ragengine
 run-ragengine: manifests generate fmt vet ## Run RAG Engine controller from command line.
-	go run ./cmd/ragengine/main.go
+	go run -ldflags "$(LDFLAGS)" ./cmd/ragengine/main.go
 
 
 ## --------------------------------------
@@ -560,6 +586,10 @@ vet: ## Run go vet against code.
 .PHONY: lint
 lint: $(GOLANGCI_LINT) ## Run golangci-lint against code.
 	$(GOLANGCI_LINT) run -v
+	
+.PHONY: lint-fix
+lint-fix: $(GOLANGCI_LINT) ## Run golangci-lint against code.
+	$(GOLANGCI_LINT) run --fix
 
 .PHONY: fmt
 fmt: ## Run go fmt against code.
diff --git a/api/v1beta1/condition_types.go b/api/v1beta1/condition_types.go
index 839ccd3..8eee817 100644
--- a/api/v1beta1/condition_types.go
+++ b/api/v1beta1/condition_types.go
@@ -20,9 +20,6 @@ const (
 	// ConditionTypeNodeClaimStatus is the state when checking nodeClaim status.
 	ConditionTypeNodeClaimStatus = ConditionType("NodeClaimsReady")
 
-	// ConditionTypeNodePluginStatus is the state when checking nodePlugin status.
-	ConditionTypeNodePluginStatus = ConditionType("NodePluginsReady")
-
 	// ConditionTypeNodeStatus is the state when checking node status.
 	ConditionTypeNodeStatus = ConditionType("NodesReady")
 
diff --git a/api/v1beta1/workspace_types.go b/api/v1beta1/workspace_types.go
index 9265548..f8a597b 100644
--- a/api/v1beta1/workspace_types.go
+++ b/api/v1beta1/workspace_types.go
@@ -40,7 +40,6 @@ type ResourceSpec struct {
 	LabelSelector *metav1.LabelSelector `json:"labelSelector"`
 
 	// Deprecated: PreferredNodes is deprecated in v1beta1 and will be removed in a future version.
-	// 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.
 	// The controller will use the `InstanceType` to create the remaining nodes.
 	// +optional
diff --git a/api/v1beta1/workspace_validation.go b/api/v1beta1/workspace_validation.go
index d0d6ca7..61af597 100644
--- a/api/v1beta1/workspace_validation.go
+++ b/api/v1beta1/workspace_validation.go
@@ -364,7 +364,7 @@ func (r *ResourceSpec) validateCreateWithInference(inference *InferenceSpec, byp
 
 			for _, node := range nodeList.Items {
 				// Try to get GPU configuration from nvidia.com labels first
-				gpuConfig, err := utils.GetGPUConfigFromNvidiaLabels(&node)
+				gpuConfig, err := utils.GetGPUConfigFromNodeLabels(&node)
 				if err != nil {
 					errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("Failed to get GPU config from nvidia labels on node %s: %v", node.Name, err)))
 					return errs
diff --git a/charts/kaito/workspace/Chart.lock b/charts/kaito/workspace/Chart.lock
index 6b329b7..ed051fe 100644
--- a/charts/kaito/workspace/Chart.lock
+++ b/charts/kaito/workspace/Chart.lock
@@ -4,9 +4,9 @@ dependencies:
   version: 2.16.3
 - name: gpu-feature-discovery
   repository: https://nvidia.github.io/k8s-device-plugin
-  version: 0.18.0
+  version: 0.17.4
 - name: local-csi-driver
   repository: oci://localcsidriver.azurecr.io/acstor/charts
   version: 0.2.6
-digest: sha256:82a17788b188dead73501eac49fc06b1000efd373fd8a745bd0fb83f7dee02a3
-generated: "2025-10-28T15:13:17.399314887+08:00"
+digest: sha256:4aabdecbb5270a8c092f1de1a1b6d0e57b435d154642b130e4b7b148b350b31a
+generated: "2025-10-28T16:49:29.412762132-04:00"
diff --git a/charts/kaito/workspace/Chart.yaml b/charts/kaito/workspace/Chart.yaml
index ef72b67..21ac685 100644
--- a/charts/kaito/workspace/Chart.yaml
+++ b/charts/kaito/workspace/Chart.yaml
@@ -38,7 +38,7 @@ dependencies:
   repository: https://fluxcd-community.github.io/helm-charts
   condition: featureGates.gatewayAPIInferenceExtension
 - name: gpu-feature-discovery
-  version: 0.18.0
+  version: 0.17.4
   repository: https://nvidia.github.io/k8s-device-plugin
   condition: featureGates.disableNodeAutoProvisioning # Only install GFD when NAP is disabled.
 - name: local-csi-driver
diff --git a/charts/kaito/workspace/charts/gpu-feature-discovery-0.17.4.tgz b/charts/kaito/workspace/charts/gpu-feature-discovery-0.17.4.tgz
new file mode 100644
index 0000000..e6fdfd5
Binary files /dev/null and b/charts/kaito/workspace/charts/gpu-feature-discovery-0.17.4.tgz differ
diff --git a/charts/kaito/workspace/charts/gpu-feature-discovery-0.18.0.tgz b/charts/kaito/workspace/charts/gpu-feature-discovery-0.18.0.tgz
deleted file mode 100644
index 49a0d1a..0000000
Binary files a/charts/kaito/workspace/charts/gpu-feature-discovery-0.18.0.tgz and /dev/null differ
diff --git a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
index 90328d1..f3b0465 100644
--- a/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
+++ b/charts/kaito/workspace/crds/kaito.sh_workspaces.yaml
@@ -610,7 +610,6 @@ spec:
               preferredNodes:
                 description: |-
                   Deprecated: PreferredNodes is deprecated in v1beta1 and will be removed in a future version.
-                  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.
                   The controller will use the `InstanceType` to create the remaining nodes.
                 items:
diff --git a/charts/kaito/workspace/templates/supported-models-configmap.yaml b/charts/kaito/workspace/templates/supported-models-configmap.yaml
index 93b8038..a85d31b 100644
--- a/charts/kaito/workspace/templates/supported-models-configmap.yaml
+++ b/charts/kaito/workspace/templates/supported-models-configmap.yaml
@@ -9,7 +9,7 @@ data:
       - name: base
         type: text-generation
         runtime: tfs
-        tag: 0.0.8
+        tag: 0.0.9
       - name: llama-3.1-8b-instruct
         type: text-generation
         version: https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct/commit/0e9e39f249a16976918f6564b8830bc894c89659
diff --git a/cmd/ragengine/main.go b/cmd/ragengine/main.go
index 94e2926..493bbd7 100644
--- a/cmd/ragengine/main.go
+++ b/cmd/ragengine/main.go
@@ -16,6 +16,7 @@ package main
 import (
 	"context"
 	"flag"
+	"fmt"
 	"os"
 	"os/signal"
 	"strconv"
@@ -28,6 +29,7 @@ import (
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
+	"k8s.io/client-go/rest"
 	"k8s.io/klog/v2"
 	"knative.dev/pkg/injection/sharedmain"
 	"knative.dev/pkg/webhook"
@@ -45,6 +47,7 @@ import (
 	"github.com/kaito-project/kaito/pkg/ragengine/controllers"
 	"github.com/kaito-project/kaito/pkg/ragengine/webhooks"
 	kaitoutils "github.com/kaito-project/kaito/pkg/utils"
+	"github.com/kaito-project/kaito/pkg/version"
 )
 
 const (
@@ -55,6 +58,8 @@ const (
 var (
 	scheme = runtime.NewScheme()
 
+	ragengineController = fmt.Sprintf("kaito-ragengine/%s", version.Version)
+
 	exitWithErrorFunc = func() {
 		klog.Flush()
 		os.Exit(1)
@@ -78,25 +83,41 @@ func main() {
 	var enableWebhook bool
 	var probeAddr string
 	var featureGates string
+	var kubeClientQPS int = 30
+	var kubeClientBurst int = 50
+	var printVersionAndExit bool
 	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
 	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+	flag.IntVar(&kubeClientQPS, "kube-client-qps", kubeClientQPS, "the rate of qps to kube-apiserver.")
+	flag.IntVar(&kubeClientBurst, "kube-client-burst", kubeClientBurst, "the max allowed burst of queries to the kube-apiserver.")
 	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
 		"Enable leader election for controller manager. "+
 			"Enabling this will ensure there is only one active controller manager.")
 	flag.BoolVar(&enableWebhook, "webhook", true,
 		"Enable webhook for controller manager. Default is true.")
 	flag.StringVar(&featureGates, "feature-gates", "vLLM=true", "Enable Kaito feature gates. Default,	vLLM=true.")
+	flag.BoolVar(&printVersionAndExit, "version", false, "Print version and exit.")
 	opts := zap.Options{
 		Development: true,
 	}
 	opts.BindFlags(flag.CommandLine)
 	flag.Parse()
 
+	if printVersionAndExit {
+		fmt.Println(version.VersionInfo())
+		os.Exit(0)
+	}
+	klog.Info("version: ", version.VersionInfo())
+
 	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
 
 	ctx := withShutdownSignal(context.Background())
 
-	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+	cfg := ctrl.GetConfigOrDie()
+	cfg.UserAgent = ragengineController
+	setRestConfig(cfg, kubeClientQPS, kubeClientBurst)
+
+	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
 		Scheme: scheme,
 		Metrics: metricsserver.Options{
 			BindAddress: metricsAddr,
@@ -191,3 +212,12 @@ func withShutdownSignal(ctx context.Context) context.Context {
 	}()
 	return nctx
 }
+
+func setRestConfig(c *rest.Config, kubeClientQPS, kubeClientBurst int) {
+	if kubeClientQPS > 0 {
+		c.QPS = float32(kubeClientQPS)
+	}
+	if kubeClientBurst > 0 {
+		c.Burst = kubeClientBurst
+	}
+}
diff --git a/cmd/workspace/main.go b/cmd/workspace/main.go
index fb474a8..95bbd8b 100644
--- a/cmd/workspace/main.go
+++ b/cmd/workspace/main.go
@@ -16,6 +16,7 @@ package main
 import (
 	"context"
 	"flag"
+	"fmt"
 	"os"
 	"os/signal"
 	"strconv"
@@ -30,6 +31,7 @@ import (
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
+	"k8s.io/client-go/rest"
 	"k8s.io/klog/v2"
 	"knative.dev/pkg/injection/sharedmain"
 	"knative.dev/pkg/webhook"
@@ -48,6 +50,7 @@ import (
 	"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/version"
 	"github.com/kaito-project/kaito/pkg/workspace/controllers"
 	"github.com/kaito-project/kaito/pkg/workspace/controllers/garbagecollect"
 	"github.com/kaito-project/kaito/pkg/workspace/controllers/inferenceset"
@@ -62,6 +65,8 @@ const (
 var (
 	scheme = runtime.NewScheme()
 
+	workspaceController = fmt.Sprintf("kaito-workspace/%s", version.Version)
+
 	exitWithErrorFunc = func() {
 		klog.Flush()
 		os.Exit(1)
@@ -88,20 +93,32 @@ func main() {
 	var enableWebhook bool
 	var probeAddr string
 	var featureGates string
+	var kubeClientQPS int = 30
+	var kubeClientBurst int = 50
+	var printVersionAndExit bool
 	flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
 	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+	flag.IntVar(&kubeClientQPS, "kube-client-qps", kubeClientQPS, "the rate of qps to kube-apiserver.")
+	flag.IntVar(&kubeClientBurst, "kube-client-burst", kubeClientBurst, "the max allowed burst of queries to the kube-apiserver.")
 	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
 		"Enable leader election for controller manager. "+
 			"Enabling this will ensure there is only one active controller manager.")
 	flag.BoolVar(&enableWebhook, "webhook", true,
 		"Enable webhook for controller manager. Default is true.")
 	flag.StringVar(&featureGates, "feature-gates", "vLLM=true,disableNodeAutoProvisioning=false", "Enable Kaito feature gates. Default: vLLM=true,disableNodeAutoProvisioning=false.")
+	flag.BoolVar(&printVersionAndExit, "version", false, "Print version and exit.")
 	opts := zap.Options{
 		Development: true,
 	}
 	opts.BindFlags(flag.CommandLine)
 	flag.Parse()
 
+	if printVersionAndExit {
+		fmt.Println(version.VersionInfo())
+		os.Exit(0)
+	}
+	klog.Info("version: ", version.VersionInfo())
+
 	if err := featuregates.ParseAndValidateFeatureGates(featureGates); err != nil {
 		klog.ErrorS(err, "unable to set `feature-gates` flag")
 		exitWithErrorFunc()
@@ -111,7 +128,11 @@ func main() {
 
 	ctx := withShutdownSignal(context.Background())
 
-	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+	cfg := ctrl.GetConfigOrDie()
+	cfg.UserAgent = workspaceController
+	setRestConfig(cfg, kubeClientQPS, kubeClientBurst)
+
+	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
 		Scheme: scheme,
 		Metrics: metricsserver.Options{
 			BindAddress: metricsAddr,
@@ -230,3 +251,12 @@ func withShutdownSignal(ctx context.Context) context.Context {
 	}()
 	return nctx
 }
+
+func setRestConfig(c *rest.Config, kubeClientQPS, kubeClientBurst int) {
+	if kubeClientQPS > 0 {
+		c.QPS = float32(kubeClientQPS)
+	}
+	if kubeClientBurst > 0 {
+		c.Burst = kubeClientBurst
+	}
+}
diff --git a/config/crd/bases/kaito.sh_workspaces.yaml b/config/crd/bases/kaito.sh_workspaces.yaml
index 90328d1..f3b0465 100644
--- a/config/crd/bases/kaito.sh_workspaces.yaml
+++ b/config/crd/bases/kaito.sh_workspaces.yaml
@@ -610,7 +610,6 @@ spec:
               preferredNodes:
                 description: |-
                   Deprecated: PreferredNodes is deprecated in v1beta1 and will be removed in a future version.
-                  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.
                   The controller will use the `InstanceType` to create the remaining nodes.
                 items:
diff --git a/docker/presets/models/tfs/Dockerfile b/docker/presets/models/tfs/Dockerfile
index 1f54b33..2b9cfe6 100644
--- a/docker/presets/models/tfs/Dockerfile
+++ b/docker/presets/models/tfs/Dockerfile
@@ -6,6 +6,8 @@ ARG VERSION
 # https://docs.ray.io/en/latest/cluster/usage-stats.html
 ENV RAY_USAGE_STATS_ENABLED="1"
 
+RUN pip3 install --upgrade pip
+
 # Set the working directory
 WORKDIR /workspace
 
diff --git a/docker/ragengine/Dockerfile b/docker/ragengine/Dockerfile
index 9a43768..f576bee 100644
--- a/docker/ragengine/Dockerfile
+++ b/docker/ragengine/Dockerfile
@@ -17,6 +17,9 @@ RUN \
 
 FROM dependencies AS builder
 
+ARG VERSION
+ARG BUILD_DATE
+
 # Copy the go source
 COPY cmd/ cmd/
 COPY api/ api/
@@ -29,7 +32,8 @@ COPY pkg/ pkg/
 # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
 RUN --mount=type=cache,target=${GOCACHE} \
     --mount=type=cache,id=ragengine-controller,sharing=locked,target=/go/pkg/mod \
-    CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o manager cmd/ragengine/*.go
+    CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on \
+    go build -ldflags "-X 'github.com/kaito-project/kaito/pkg/version.Version=${VERSION}' -X 'github.com/kaito-project/kaito/pkg/version.BuildDate=${BUILD_DATE}'" -a -o manager cmd/ragengine/*.go
 
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
diff --git a/docker/ragengine/service/Dockerfile b/docker/ragengine/service/Dockerfile
index 97334c4..ac3ff30 100644
--- a/docker/ragengine/service/Dockerfile
+++ b/docker/ragengine/service/Dockerfile
@@ -11,6 +11,8 @@ RUN apt-get update && apt-get upgrade -y \
     && apt-get clean \
     && rm -rf /var/lib/apt/lists/*
 
+RUN pip3 install --upgrade pip
+
 FROM dependencies AS base
 
 ARG VERSION
diff --git a/docker/skopeo/Dockerfile b/docker/skopeo/Dockerfile
index 7d714a3..46675d9 100644
--- a/docker/skopeo/Dockerfile
+++ b/docker/skopeo/Dockerfile
@@ -22,6 +22,7 @@ targets:
             - ${VERSION}-${REVISION}.azl3
       runtime:
         bash: {}
+        findutils: {}
         tar: {}
 
 artifacts:
diff --git a/docker/workspace/Dockerfile b/docker/workspace/Dockerfile
index 0131f75..57806a0 100644
--- a/docker/workspace/Dockerfile
+++ b/docker/workspace/Dockerfile
@@ -17,6 +17,9 @@ RUN \
 
 FROM dependencies as builder
 
+ARG VERSION
+ARG BUILD_DATE
+
 # Copy the go source
 COPY cmd/ cmd/
 COPY api/ api/
@@ -30,7 +33,8 @@ COPY presets/ presets/
 # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
 RUN --mount=type=cache,target=${GOCACHE} \
     --mount=type=cache,id=kaito-controller,sharing=locked,target=/go/pkg/mod \
-    CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on go build -a -o manager cmd/workspace/*.go
+    CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} GO111MODULE=on \
+    go build -ldflags "-X 'github.com/kaito-project/kaito/pkg/version.Version=${VERSION}' -X 'github.com/kaito-project/kaito/pkg/version.BuildDate=${BUILD_DATE}'" -a -o manager cmd/workspace/*.go
 
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
diff --git a/pkg/ragengine/controllers/preset_rag.go b/pkg/ragengine/controllers/preset_rag.go
index 608fe21..916814f 100644
--- a/pkg/ragengine/controllers/preset_rag.go
+++ b/pkg/ragengine/controllers/preset_rag.go
@@ -116,22 +116,13 @@ func CreatePresetRAG(ctx context.Context, ragEngineObj *v1alpha1.RAGEngine, revi
 	var resourceReq corev1.ResourceRequirements
 
 	if ragEngineObj.Spec.Embedding.Local != nil {
-		var skuNumGPUs int
-		var gpuConfig *sku.GPUConfig = nil
+		var gpuConfig *sku.GPUConfig
 		var err error
-		if len(ragEngineObj.Spec.Compute.PreferredNodes) == 0 {
-			gpuConfig, _ = utils.GetGPUConfigBySKU(ragEngineObj.Spec.Compute.InstanceType)
-		}
-		if gpuConfig != nil {
-			skuNumGPUs = gpuConfig.GPUCount
-		} else {
-			gpuConfig, err = utils.TryGetGPUConfigFromNode(ctx, kubeClient, ragEngineObj.Status.WorkerNodes)
-			if err != nil {
-				skuNumGPUs = 0
-			} else if gpuConfig != nil {
-				skuNumGPUs = gpuConfig.GPUCount
-			}
+		gpuConfig, err = utils.GetGPUConfigBySKU(ragEngineObj.Spec.Compute.InstanceType)
+		if err != nil {
+			return nil, err
 		}
+		skuNumGPUs := gpuConfig.GPUCount
 
 		resourceReq = corev1.ResourceRequirements{
 			Requests: corev1.ResourceList{
diff --git a/pkg/ragengine/controllers/ragengine_controller.go b/pkg/ragengine/controllers/ragengine_controller.go
index c4f8b75..b8fefb1 100644
--- a/pkg/ragengine/controllers/ragengine_controller.go
+++ b/pkg/ragengine/controllers/ragengine_controller.go
@@ -389,7 +389,11 @@ func (c *RAGEngineReconciler) applyRAGEngineResource(ctx context.Context, ragEng
 	}
 
 	// Ensure all gpu plugins are running successfully.
-	knownGPUConfig, _ := utils.GetGPUConfigBySKU(ragEngineObj.Spec.Compute.InstanceType)
+	knownGPUConfig, err := utils.GetGPUConfigBySKU(ragEngineObj.Spec.Compute.InstanceType)
+	if err != nil {
+		return err
+	}
+
 	if len(ragEngineObj.Spec.Compute.PreferredNodes) == 0 && knownGPUConfig != nil {
 		for i := range selectedNodes {
 			err = c.ensureNodePlugins(ctx, ragEngineObj, selectedNodes[i])
diff --git a/pkg/utils/common.go b/pkg/utils/common.go
index 2c4d162..a090a10 100644
--- a/pkg/utils/common.go
+++ b/pkg/utils/common.go
@@ -14,7 +14,6 @@
 package utils
 
 import (
-	"context"
 	"fmt"
 	"net/url"
 	"os"
@@ -31,7 +30,6 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/apimachinery/pkg/types"
 	"knative.dev/pkg/apis"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterapis "sigs.k8s.io/karpenter/pkg/apis"
@@ -182,52 +180,16 @@ func GetGPUConfigBySKU(instanceType string) (*sku.GPUConfig, error) {
 		return nil, apis.ErrInvalidValue(fmt.Sprintf("Failed to get SKU handler: %v", err), "sku")
 	}
 
-	return skuHandler.GetGPUConfigBySKU(instanceType), nil
-}
-
-// TODO: Remove this function in favor of TryGetGPUConfigFromNodes in a follow up to integrate BYO in the controller.
-func TryGetGPUConfigFromNode(ctx context.Context, kubeClient client.Client, workerNodes []string) (*sku.GPUConfig, error) {
-	skuGPUCount, err := FetchGPUCountFromNodes(ctx, kubeClient, workerNodes)
-	if err != nil || skuGPUCount == 0 {
-		return nil, fmt.Errorf("failed to fetch GPU count from nodes: %w", err)
-	}
-
-	return &sku.GPUConfig{
-		SKU:      "unknown", // SKU is not available from nodes
-		GPUCount: skuGPUCount,
-		GPUModel: "unknown", // GPU model is not available from nodes
-	}, nil
-}
-
-func TryGetGPUConfigFromNodes(ctx context.Context, workerNodes []*corev1.Node) (*sku.GPUConfig, error) {
-	if len(workerNodes) == 0 {
-		return nil, fmt.Errorf("no worker nodes found")
-	}
-
-	// Get the first node to extract GPU configuration
-	for _, node := range workerNodes {
-		// Try to get GPU configuration from nvidia.com labels first
-		if gpuConfig, err := GetGPUConfigFromNvidiaLabels(node); err == nil {
-			return gpuConfig, nil
-		}
-	}
-
-	// Fallback to the previous implementation using only GPU count
-	skuGPUCount := GetPerNodeGPUCountFromNodes(workerNodes)
-	if skuGPUCount == 0 {
-		return nil, fmt.Errorf("failed to fetch GPU count from nodes")
+	config := skuHandler.GetGPUConfigBySKU(instanceType)
+	if config == nil {
+		return nil, apis.ErrInvalidValue(fmt.Sprintf("Unsupported SKU '%s' for cloud provider", instanceType), "sku")
 	}
 
-	return &sku.GPUConfig{
-		SKU:       "unknown", // SKU is not available from nodes
-		GPUCount:  skuGPUCount,
-		GPUModel:  "unknown", // GPU model is not available from nodes
-		GPUMemGiB: 0,         // GPU memory is not available from capacity
-	}, nil
+	return config, nil
 }
 
-// GetGPUConfigFromNvidiaLabels extracts GPU configuration from nvidia.com labels on a node
-func GetGPUConfigFromNvidiaLabels(node *corev1.Node) (*sku.GPUConfig, error) {
+// GetGPUConfigFromNodeLabels extracts GPU configuration from nvidia.com labels on a node
+func GetGPUConfigFromNodeLabels(node *corev1.Node) (*sku.GPUConfig, error) {
 	gpuProduct, hasGPUProduct := node.Labels[consts.NvidiaGPUProduct]
 	gpuCountStr, hasGPUCount := node.Labels[consts.NvidiaGPUCount]
 	gpuMemoryStr, hasGPUMemory := node.Labels[consts.NvidiaGPUMemory]
@@ -259,34 +221,6 @@ func GetGPUConfigFromNvidiaLabels(node *corev1.Node) (*sku.GPUConfig, error) {
 	}, nil
 }
 
-// FetchGPUCountFromNodes retrieves the GPU count from the given node names.
-func FetchGPUCountFromNodes(ctx context.Context, kubeClient client.Client, nodeNames []string) (int, error) {
-	if len(nodeNames) == 0 {
-		return 0, fmt.Errorf("no worker nodes found in the workspace")
-	}
-
-	allNodes := []*corev1.Node{}
-	for _, nodeName := range nodeNames {
-		node := &corev1.Node{}
-		if err := kubeClient.Get(ctx, types.NamespacedName{Name: nodeName}, node); err != nil { // Note: nodes don't have a namespace here.
-			return 0, fmt.Errorf("failed to get node %s: %w", nodeName, err)
-		}
-		allNodes = append(allNodes, node)
-	}
-
-	return GetPerNodeGPUCountFromNodes(allNodes), nil
-}
-
-func GetPerNodeGPUCountFromNodes(nodes []*corev1.Node) int {
-	for _, node := range nodes {
-		gpuCount, exists := node.Status.Capacity[consts.NvidiaGPU]
-		if exists && gpuCount.String() != "" {
-			return int(gpuCount.Value())
-		}
-	}
-	return 0
-}
-
 func ExtractAndValidateRepoName(image string) error {
 	// Extract repository name (part after the last / and before the colon :)
 	// For example given image: modelsregistry.azurecr.io/ADAPTER_HERE:0.0.1
diff --git a/pkg/utils/nodeclaim/nodeclaim.go b/pkg/utils/nodeclaim/nodeclaim.go
index aa79f1d..9497c25 100644
--- a/pkg/utils/nodeclaim/nodeclaim.go
+++ b/pkg/utils/nodeclaim/nodeclaim.go
@@ -28,7 +28,7 @@ import (
 	awsv1 "github.com/aws/karpenter-provider-aws/pkg/apis/v1"
 	"github.com/awslabs/operatorpkg/status"
 	"github.com/samber/lo"
-	v1 "k8s.io/api/core/v1"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -170,39 +170,39 @@ func GenerateNodeClaimManifest(storageRequirement string, obj client.Object) *ka
 				Kind:  nodeClassRefKind,
 				Group: nodeClassRefGroup,
 			},
-			Taints: []v1.Taint{
+			Taints: []corev1.Taint{
 				{
 					Key:    consts.SKUString,
 					Value:  consts.GPUString,
-					Effect: v1.TaintEffectNoSchedule,
+					Effect: corev1.TaintEffectNoSchedule,
 				},
 			},
 			Requirements: []karpenterv1.NodeSelectorRequirementWithMinValues{
 				{
-					NodeSelectorRequirement: v1.NodeSelectorRequirement{
+					NodeSelectorRequirement: corev1.NodeSelectorRequirement{
 						Key:      consts.LabelNodePool,
-						Operator: v1.NodeSelectorOpIn,
+						Operator: corev1.NodeSelectorOpIn,
 						Values:   []string{consts.KaitoNodePoolName},
 					},
 				},
 				{
-					NodeSelectorRequirement: v1.NodeSelectorRequirement{
-						Key:      v1.LabelInstanceTypeStable,
-						Operator: v1.NodeSelectorOpIn,
+					NodeSelectorRequirement: corev1.NodeSelectorRequirement{
+						Key:      corev1.LabelInstanceTypeStable,
+						Operator: corev1.NodeSelectorOpIn,
 						Values:   []string{instanceType},
 					},
 				},
 				{
-					NodeSelectorRequirement: v1.NodeSelectorRequirement{
-						Key:      v1.LabelOSStable,
-						Operator: v1.NodeSelectorOpIn,
+					NodeSelectorRequirement: corev1.NodeSelectorRequirement{
+						Key:      corev1.LabelOSStable,
+						Operator: corev1.NodeSelectorOpIn,
 						Values:   []string{"linux"},
 					},
 				},
 			},
 			Resources: karpenterv1.ResourceRequirements{
-				Requests: v1.ResourceList{
-					v1.ResourceStorage: resource.MustParse(storageRequirement),
+				Requests: corev1.ResourceList{
+					corev1.ResourceStorage: resource.MustParse(storageRequirement),
 				},
 			},
 		},
@@ -210,9 +210,9 @@ func GenerateNodeClaimManifest(storageRequirement string, obj client.Object) *ka
 
 	if cloudName == consts.AzureCloudName {
 		nodeSelector := karpenterv1.NodeSelectorRequirementWithMinValues{
-			NodeSelectorRequirement: v1.NodeSelectorRequirement{
+			NodeSelectorRequirement: corev1.NodeSelectorRequirement{
 				Key:      azurev1beta1.LabelSKUName,
-				Operator: v1.NodeSelectorOpIn,
+				Operator: corev1.NodeSelectorOpIn,
 				Values:   []string{instanceType},
 			},
 		}
@@ -221,9 +221,9 @@ func GenerateNodeClaimManifest(storageRequirement string, obj client.Object) *ka
 
 	if cloudName == consts.AWSCloudName {
 		nodeSelector := karpenterv1.NodeSelectorRequirementWithMinValues{
-			NodeSelectorRequirement: v1.NodeSelectorRequirement{
+			NodeSelectorRequirement: corev1.NodeSelectorRequirement{
 				Key:      "karpenter.k8s.aws/instance-gpu-count",
-				Operator: v1.NodeSelectorOpGt,
+				Operator: corev1.NodeSelectorOpGt,
 				Values:   []string{"0"},
 			},
 		}
@@ -337,8 +337,8 @@ func WaitForPendingNodeClaims(ctx context.Context, obj client.Object, kubeClient
 	for i := range nodeClaims.Items {
 		// check if the nodeClaim being created has the requested instance type
 		_, nodeClaimInstanceType := lo.Find(nodeClaims.Items[i].Spec.Requirements, func(requirement karpenterv1.NodeSelectorRequirementWithMinValues) bool {
-			return requirement.Key == v1.LabelInstanceTypeStable &&
-				requirement.Operator == v1.NodeSelectorOpIn &&
+			return requirement.Key == corev1.LabelInstanceTypeStable &&
+				requirement.Operator == corev1.NodeSelectorOpIn &&
 				lo.Contains(requirement.Values, instanceType)
 		})
 		if nodeClaimInstanceType {
@@ -473,33 +473,6 @@ func CheckNodeClass(ctx context.Context, kClient client.Client) error {
 	return nil
 }
 
-// ResolveReadyNodesAndTargetNodeClaimCount returns all ready nodes and the number of target NodeClaims(nodes) for the given workspace
-func ResolveReadyNodesAndTargetNodeClaimCount(ctx context.Context, c client.Client, wObj *kaitov1beta1.Workspace) ([]string, int, error) {
-	availableBYONodes, readyNodes, err := resources.GetBYOAndReadyNodes(ctx, c, wObj)
-	if err != nil {
-		return nil, 0, fmt.Errorf("failed to get available BYO nodes: %w", err)
-	}
-
-	targetNodeCount := int(wObj.Status.TargetNodeCount)
-
-	// Calculate the number of target NodeClaims(nodes) (target - BYO nodes)
-	targetNodeClaimCount := max(0, targetNodeCount-len(availableBYONodes))
-
-	klog.InfoS("Resolved node counts for workspace",
-		"workspace", klog.KObj(wObj),
-		"targetNodeCount", targetNodeCount,
-		"availableBYONodes", len(availableBYONodes),
-		"targetNodeClaimCount", targetNodeClaimCount,
-		"autoProvisioningDisabled", featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning])
-
-	// if node provision is disabled, NodeClaims(nodes) are not needed.
-	if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
-		targetNodeClaimCount = 0
-	}
-
-	return readyNodes, targetNodeClaimCount, nil
-}
-
 // IsNodeClaimReadyNotDeleting checks if a NodeClaim is in ready state and not being deleted
 func IsNodeClaimReadyNotDeleting(nodeClaim *karpenterv1.NodeClaim) bool {
 	if !nodeClaim.DeletionTimestamp.IsZero() {
diff --git a/pkg/utils/resources/nodes.go b/pkg/utils/resources/nodes.go
index bb10a14..d6da40a 100644
--- a/pkg/utils/resources/nodes.go
+++ b/pkg/utils/resources/nodes.go
@@ -20,14 +20,11 @@ import (
 	"github.com/samber/lo"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/sets"
 	"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"
-	"github.com/kaito-project/kaito/pkg/featuregates"
-	"github.com/kaito-project/kaito/pkg/utils/consts"
 )
 
 const (
@@ -124,8 +121,8 @@ func ExtractObjFields(obj client.Object) (instanceType, namespace, name string,
 	return
 }
 
-// GetBYOAndReadyNodes finds all BYO nodes and ready nodes that match the workspace's label selector
-func GetBYOAndReadyNodes(ctx context.Context, c client.Client, wObj *kaitov1beta1.Workspace) ([]*corev1.Node, []string, error) {
+// GetReadyNodes finds all ready nodes that match the workspace's label selector
+func GetReadyNodes(ctx context.Context, c client.Client, wObj *kaitov1beta1.Workspace) ([]*corev1.Node, error) {
 	var matchLabels client.MatchingLabels
 	if wObj.Resource.LabelSelector != nil {
 		matchLabels = wObj.Resource.LabelSelector.MatchLabels
@@ -133,44 +130,29 @@ func GetBYOAndReadyNodes(ctx context.Context, c client.Client, wObj *kaitov1beta
 
 	nodeList, err := ListNodes(ctx, c, matchLabels)
 	if err != nil {
-		return nil, nil, err
+		return nil, err
 	}
 
-	//nolint:staticcheck // SA1019
-	preferredNodeSet := sets.New(wObj.Resource.PreferredNodes...)
-
-	availableBYONodes := make([]*corev1.Node, 0, len(nodeList.Items))
-	readyNodes := make([]string, 0, len(nodeList.Items))
+	readyNodes := make([]*corev1.Node, 0, len(nodeList.Items))
 	for i := range nodeList.Items {
 		node := &nodeList.Items[i]
 
 		if !NodeIsReadyAndNotDeleting(node) {
-			klog.V(4).InfoS("BYO node is not ready, skipping",
+			klog.V(4).InfoS("Node is not ready, skipping",
 				"node", node.Name,
 				"workspace", klog.KObj(wObj))
 			continue
 		} else {
-			readyNodes = append(readyNodes, node.Name)
+			readyNodes = append(readyNodes, node)
 		}
 
-		if !featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
-			// If preferred nodes are specified, only include those nodes
-			if preferredNodeSet.Has(node.Name) {
-				availableBYONodes = append(availableBYONodes, node)
-			}
-		} else {
-			// If node auto-provisioning is disabled, include all ready nodes
-			availableBYONodes = append(availableBYONodes, node)
-		}
 	}
 
-	klog.V(4).InfoS("Found available BYO nodes",
+	klog.V(4).InfoS("Found ready nodes",
 		"workspace", klog.KObj(wObj),
-		//nolint:staticcheck // SA1019
-		"preferredNodesSpecified", len(wObj.Resource.PreferredNodes),
-		"availableBYONodes", len(availableBYONodes))
+		"readyNodes", len(readyNodes))
 
-	return availableBYONodes, readyNodes, nil
+	return readyNodes, nil
 }
 
 func NodeIsReadyAndNotDeleting(node *corev1.Node) bool {
diff --git a/pkg/version/version.go b/pkg/version/version.go
new file mode 100644
index 0000000..e746e4d
--- /dev/null
+++ b/pkg/version/version.go
@@ -0,0 +1,30 @@
+// Copyright (c) KAITO authors.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package version
+
+import (
+	"fmt"
+	"runtime"
+)
+
+var (
+	// Build-time variables injected via -ldflags
+	Version   = "dev"             // Will be replaced with actual version (e.g., git tag)
+	BuildDate = "unknown"         // Will be replaced with actual build timestamp
+	GoVersion = runtime.Version() // Keep as runtime value - this is appropriate
+)
+
+func VersionInfo() string {
+	return fmt.Sprintf("%s (Build Date: %s, Go Version: %s)", Version, BuildDate, GoVersion)
+}
diff --git a/pkg/workspace/controllers/workspace_controller.go b/pkg/workspace/controllers/workspace_controller.go
index 29eb1b2..7010ea6 100644
--- a/pkg/workspace/controllers/workspace_controller.go
+++ b/pkg/workspace/controllers/workspace_controller.go
@@ -144,45 +144,80 @@ func (c *WorkspaceReconciler) ensureFinalizer(ctx context.Context, workspaceObj
 	return nil
 }
 
-func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (reconcile.Result, error) {
-	workspaceKey := client.ObjectKeyFromObject(wObj).String()
-	if !c.expectations.SatisfiedExpectations(c.Log, workspaceKey) {
-		klog.V(4).InfoS("Waiting for NodeClaim expectations to be satisfied",
-			"workspace", workspaceKey)
-		return reconcile.Result{}, nil
+func (c *WorkspaceReconciler) reconcileNodes(ctx context.Context, wObj *kaitov1beta1.Workspace) (*reconcile.Result, error) {
+	defer func() {
+		if err := c.nodeResourceManager.SetResourceReadyConditionByStatus(ctx, wObj); err != nil {
+			klog.ErrorS(err, "failed to update resource status", "workspace", klog.KObj(wObj))
+		}
+	}()
+
+	nodeList, err := resources.ListNodes(ctx, c.Client, wObj.Resource.LabelSelector.MatchLabels)
+	if err != nil {
+		return &reconcile.Result{}, err
+	}
+	matchingNodes := []*corev1.Node{}
+	for i := range nodeList.Items {
+		node := &nodeList.Items[i]
+		matchingNodes = append(matchingNodes, node)
 	}
 
-	// diff node claims
-	addedNodeClaimsCount, existingNodeClaims, readyNodes, err := c.nodeClaimManager.CheckNodeClaims(ctx, wObj)
+	readyNodes, err := resources.GetReadyNodes(ctx, c.Client, wObj)
 	if err != nil {
-		return reconcile.Result{}, err
+		return &reconcile.Result{}, fmt.Errorf("failed to list ready nodes: %w", err)
 	}
 
-	// create nodeclaims
-	if err := c.nodeClaimManager.CreateUpNodeClaims(ctx, wObj, addedNodeClaimsCount); err != nil {
-		return reconcile.Result{}, err
+	existingNodeClaims := []*karpenterv1.NodeClaim{}
+	if !featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+		// diff node claims
+		var numNodeClaimsToCreate int
+		var err error
+		numNodeClaimsToCreate, existingNodeClaims, err = c.nodeClaimManager.CheckNodeClaims(ctx, wObj, readyNodes)
+		klog.Info("NodeClaims to create", "count", numNodeClaimsToCreate, "workspace", klog.KObj(wObj))
+		if err != nil {
+			return &reconcile.Result{}, err
+		}
+
+		// create nodeclaims
+		if err := c.nodeClaimManager.CreateUpNodeClaims(ctx, wObj, numNodeClaimsToCreate); err != nil {
+			return &reconcile.Result{}, err
+		}
+
+		// check nodeclaims meet the target count
+		if ready, err := c.nodeClaimManager.EnsureNodeClaimsReady(ctx, wObj, readyNodes, existingNodeClaims); err != nil {
+			return &reconcile.Result{}, err
+		} else if !ready {
+			// Not enough ready nodeclaims, requeue and wait for next reconcile.
+			return &reconcile.Result{}, nil
+		}
+
+		// check node plugins ready
+		if ready, err := c.nodeResourceManager.CheckIfNodePluginsReady(ctx, wObj, existingNodeClaims); err != nil {
+			return &reconcile.Result{}, err
+		} else if !ready {
+			// The node resource changes can not trigger workspace controller reconcile, so we need to requeue reconcile when don't proceed because of node resource not ready.
+			return &reconcile.Result{RequeueAfter: 2 * time.Second}, nil
+		}
 	}
 
-	// check nodeclaims meet the target count
-	if ready, err := c.nodeClaimManager.AreNodeClaimsReady(ctx, wObj, existingNodeClaims); err != nil {
-		return reconcile.Result{}, err
-	} else if !ready {
-		// Not enough ready nodeclaims, requeue and wait for next reconcile
+	// Check if selected nodes are ready in both NAP and BYO scenarios.
+	_, err = c.nodeResourceManager.EnsureNodesReady(ctx, wObj, matchingNodes, existingNodeClaims)
+
+	return nil, err
+}
+
+func (c *WorkspaceReconciler) addOrUpdateWorkspace(ctx context.Context, wObj *kaitov1beta1.Workspace) (reconcile.Result, error) {
+	workspaceKey := client.ObjectKeyFromObject(wObj).String()
+	if !c.expectations.SatisfiedExpectations(c.Log, workspaceKey) {
+		klog.V(4).InfoS("Waiting for NodeClaim expectations to be satisfied",
+			"workspace", workspaceKey)
 		return reconcile.Result{}, nil
 	}
 
-	// check node plugins ready
-	if ready, err := c.nodeResourceManager.AreNodePluginsReady(ctx, wObj, existingNodeClaims); err != nil {
-		return reconcile.Result{}, err
-	} else if !ready {
-		// The node resource changes can not trigger workspace controller reconcile, so we need to requeue reconcile when don't proceed because of node resource not ready.
-		return reconcile.Result{RequeueAfter: 2 * time.Second}, nil
+	if result, err := c.reconcileNodes(ctx, wObj); err != nil || result != nil {
+		return *result, err
 	}
 
-	// update worker nodes in status
-	if err := c.nodeResourceManager.UpdateWorkerNodesInStatus(ctx, wObj, readyNodes); err != nil {
-		return reconcile.Result{}, err
-	}
+	var err error
 
 	if wObj.Tuning != nil {
 		if err = c.applyTuning(ctx, wObj); err != nil {
diff --git a/pkg/workspace/estimator/advancednodesestimator/estimator.go b/pkg/workspace/estimator/advancednodesestimator/estimator.go
index 2c68761..2ac664c 100644
--- a/pkg/workspace/estimator/advancednodesestimator/estimator.go
+++ b/pkg/workspace/estimator/advancednodesestimator/estimator.go
@@ -23,6 +23,8 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
+	"github.com/kaito-project/kaito/pkg/sku"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
@@ -52,12 +54,26 @@ func (c *AdvancedNodesEstimator) EstimateNodeCount(ctx context.Context, workspac
 	presetName := string(workspace.Inference.Preset.Name)
 	model := plugin.KaitoModelRegister.MustGet(presetName)
 
-	gpuConfig, err := utils.GetGPUConfigBySKU(workspace.Resource.InstanceType)
-	if err != nil {
-		return 0, fmt.Errorf("failed to get GPU config for instance type %s: %w", workspace.Resource.InstanceType, err)
-	}
-	if gpuConfig == nil {
-		return 0, fmt.Errorf("GPU config is nil for instance type %s", workspace.Resource.InstanceType)
+	var gpuConfig *sku.GPUConfig
+	var err error
+
+	if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+		if readyNodes, err := resources.GetReadyNodes(ctx, client, workspace); err != nil {
+			return 0, fmt.Errorf("failed to list ready nodes: %w", err)
+		} else if len(readyNodes) == 0 {
+			return 0, fmt.Errorf("no ready nodes found, unable to determine GPU configuration")
+		} else {
+			gpuConfig, err = utils.GetGPUConfigFromNodeLabels(readyNodes[0])
+			if err != nil {
+				return 0, fmt.Errorf("failed to get GPU config from existing nodes: %w", err)
+			}
+		}
+	} else {
+		// NAP is enabled - instanceType is required and must be valid
+		gpuConfig, err = utils.GetGPUConfigBySKU(workspace.Resource.InstanceType)
+		if err != nil {
+			return 0, fmt.Errorf("failed to get GPU config for instance type %s: %w", workspace.Resource.InstanceType, err)
+		}
 	}
 
 	// Start with the user-requested node count (default is 1)
diff --git a/pkg/workspace/estimator/basicnodesestimator/estimator.go b/pkg/workspace/estimator/basicnodesestimator/estimator.go
index 15dcfea..2738fc6 100644
--- a/pkg/workspace/estimator/basicnodesestimator/estimator.go
+++ b/pkg/workspace/estimator/basicnodesestimator/estimator.go
@@ -22,9 +22,12 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
+	"github.com/kaito-project/kaito/pkg/sku"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
+	"github.com/kaito-project/kaito/pkg/utils/resources"
 )
 
 // BasicNodesEstimator calculates node count based on SKU memory and model memory requirement
@@ -50,12 +53,29 @@ func (e *BasicNodesEstimator) EstimateNodeCount(ctx context.Context, wObj *kaito
 	presetName := string(wObj.Inference.Preset.Name)
 	model := plugin.KaitoModelRegister.MustGet(presetName)
 
-	gpuConfig, err := utils.GetGPUConfigBySKU(wObj.Resource.InstanceType)
-	if err != nil {
-		return 0, fmt.Errorf("failed to get GPU config for instance type %s: %w", wObj.Resource.InstanceType, err)
-	}
-	if gpuConfig == nil {
-		return 0, fmt.Errorf("GPU config is nil for instance type %s", wObj.Resource.InstanceType)
+	// Import featuregates and consts for NAP check
+	var gpuConfig *sku.GPUConfig
+	var err error
+
+	if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+		// NAP is disabled (BYO scenario) - instanceType is optional
+		// Try to get GPU config from existing nodes
+		if readyNodes, err := resources.GetReadyNodes(ctx, client, wObj); err != nil {
+			return 0, fmt.Errorf("failed to list ready nodes: %w", err)
+		} else if len(readyNodes) == 0 {
+			return 0, fmt.Errorf("no ready nodes found, unable to determine GPU configuration")
+		} else {
+			gpuConfig, err = utils.GetGPUConfigFromNodeLabels(readyNodes[0])
+			if err != nil {
+				return 0, fmt.Errorf("failed to get GPU config from existing nodes: %w", err)
+			}
+		}
+	} else {
+		// NAP is enabled - instanceType is required and must be valid
+		gpuConfig, err = utils.GetGPUConfigBySKU(wObj.Resource.InstanceType)
+		if err != nil {
+			return 0, fmt.Errorf("failed to get GPU config for instance type %s: %w", wObj.Resource.InstanceType, err)
+		}
 	}
 
 	// Start with the user-requested node count (default is 1)
diff --git a/pkg/workspace/image/puller.go b/pkg/workspace/image/puller.go
index 5e71661..7ac0bfe 100644
--- a/pkg/workspace/image/puller.go
+++ b/pkg/workspace/image/puller.go
@@ -64,7 +64,7 @@ func renderPullerSH(imgRef string, volDir string) string {
 func NewPullerContainer(inputImage string, outputDirectory string) *corev1.Container {
 	return &corev1.Container{
 		Name:  "puller",
-		Image: "quay.io/skopeo/stable:v1.18.0-immutable",
+		Image: "mcr.microsoft.com/aks/skopeo:1.14.4-6",
 		Command: []string{
 			"/bin/sh",
 			"-c",
diff --git a/pkg/workspace/inference/preset_inferences.go b/pkg/workspace/inference/preset_inferences.go
index d615a9d..d99eefb 100644
--- a/pkg/workspace/inference/preset_inferences.go
+++ b/pkg/workspace/inference/preset_inferences.go
@@ -29,6 +29,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
 	pkgmodel "github.com/kaito-project/kaito/pkg/model"
 	"github.com/kaito-project/kaito/pkg/sku"
 	"github.com/kaito-project/kaito/pkg/utils"
@@ -136,13 +137,16 @@ func GeneratePresetInference(ctx context.Context, workspaceObj *v1beta1.Workspac
 		Model:      model,
 	}
 
-	gpuConfig := getGPUConfig(gctx)
+	gpuConfig, err := getGPUConfig(gctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get GPU config: %w", err)
+	}
 
 	// Set the target node count for the inference workload
 	numNodes := int(workspaceObj.Status.TargetNodeCount)
 
 	podOpts := []generator.TypedManifestModifier[generator.WorkspaceGeneratorContext, corev1.PodSpec]{
-		GenerateInferencePodSpec(&gpuConfig, numNodes),
+		GenerateInferencePodSpec(gpuConfig, numNodes),
 		SetModelDownloadInfo,
 		SetAdapterPuller,
 	}
@@ -156,7 +160,7 @@ func GeneratePresetInference(ctx context.Context, workspaceObj *v1beta1.Workspac
 			manifests.GenerateStatefulSetManifest(revisionNum, numNodes),
 		}
 
-		if checkIfNVMeAvailable(ctx, &gpuConfig, kubeClient) {
+		if checkIfNVMeAvailable(ctx, gpuConfig, kubeClient) {
 			ssOpts = append(ssOpts, manifests.AddStatefulSetVolumeClaimTemplates(GenerateModelWeightsCacheVolume(ctx, workspaceObj, model)))
 		} else {
 			podOpts = append(podOpts, SetDefaultModelWeightsVolume)
@@ -184,30 +188,27 @@ func GeneratePresetInference(ctx context.Context, workspaceObj *v1beta1.Workspac
 	}
 }
 
-func getGPUConfig(ctx *generator.WorkspaceGeneratorContext) sku.GPUConfig {
-	var gpuConfig *sku.GPUConfig
-	var err error
-	// 1. try to get GPU config from known sku if instanceType is set
-	//nolint:staticcheck // SA1019
-	if len(ctx.Workspace.Resource.PreferredNodes) == 0 {
-		gpuConfig, _ = utils.GetGPUConfigBySKU(ctx.Workspace.Resource.InstanceType)
-		if gpuConfig != nil {
-			return *gpuConfig
+func getGPUConfig(ctx *generator.WorkspaceGeneratorContext) (*sku.GPUConfig, error) {
+	if featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+		// NAP is disabled (BYO scenario) - prefer to get GPU config from matching nodes with nvidia.com labels
+		// Only try to find matching nodes if we have a labelSelector and if WorkerNodes is not already populated
+		readyNodes, err := resources.GetReadyNodes(ctx.Ctx, ctx.KubeClient, ctx.Workspace)
+		if err != nil {
+			return nil, fmt.Errorf("failed to list ready nodes: %w", err)
+		}
+		if len(readyNodes) == 0 {
+			return nil, fmt.Errorf("no ready nodes found matching the workspace's label selector")
 		}
-	}
 
-	// 2. try to get GPU config from the node status
-	gpuConfig, err = utils.TryGetGPUConfigFromNode(ctx.Ctx, ctx.KubeClient, ctx.Workspace.Status.WorkerNodes)
-	if err == nil {
-		return *gpuConfig
-	}
+		return utils.GetGPUConfigFromNodeLabels(readyNodes[0])
+	} else {
+		// NAP is enabled - try to get GPU config from known SKU
+		gpuConfig, err := utils.GetGPUConfigBySKU(ctx.Workspace.Resource.InstanceType)
+		if err != nil {
+			return nil, err
+		}
 
-	// 3. if both above methods fail, use the default GPU count requirement from the model
-	//    FIXME: assume gpu nodes are provided here. cpu inference should not go through this path.
-	defaultNumGPU := resource.MustParse(ctx.Model.GetInferenceParameters().GPUCountRequirement)
-	skuNumGPUs := int(defaultNumGPU.Value())
-	return sku.GPUConfig{
-		GPUCount: skuNumGPUs,
+		return gpuConfig, nil
 	}
 }
 
diff --git a/pkg/workspace/resource/node.go b/pkg/workspace/resource/node.go
index 9af92dc..b0bd13b 100644
--- a/pkg/workspace/resource/node.go
+++ b/pkg/workspace/resource/node.go
@@ -20,13 +20,16 @@ import (
 	"sort"
 
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/klog/v2"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
 	"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/utils/workspace"
 )
@@ -41,27 +44,29 @@ func NewNodeManager(c client.Client) *NodeManager {
 	}
 }
 
-// AreNodePluginsReady is used for ensuring node label(accelerator:nvidia) and GPU capacity on all auto-provisioned nodes for the workspace.
-func (c *NodeManager) AreNodePluginsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
+// CheckIfNodePluginsReady is used for ensuring node label(accelerator:nvidia) and GPU capacity on all auto-provisioned nodes for the workspace.
+// It also updates the Node status condition accordingly.
+func (c *NodeManager) CheckIfNodePluginsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
 	// ensure Nvidia device plugins are ready for the workspace when instance type is known.
 	knownGPUConfig, _ := utils.GetGPUConfigBySKU(wObj.Resource.InstanceType)
 	if knownGPUConfig != nil {
 		if areReady, err := c.checkNodePlugin(ctx, wObj, existingNodeClaims); err != nil {
-			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"nodePluginIsNotReady", err.Error()); updateErr != nil {
+			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeStatus, metav1.ConditionFalse,
+				"NodePluginsNotReady", err.Error()); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return false, updateErr
 			}
 			return false, err
 		} else if !areReady {
-			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"nodePluginIsNotReady", "waiting all node plugins to be ready"); updateErr != nil {
+			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeStatus, metav1.ConditionFalse,
+				"NodePluginsNotReady", "waiting all node plugins to be ready"); updateErr != nil {
 				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
 				return false, updateErr
 			}
 			return false, nil
 		}
 	}
+
 	return true, nil
 }
 
@@ -71,6 +76,7 @@ func (c *NodeManager) checkNodePlugin(ctx context.Context, wObj *kaitov1beta1.Wo
 	if err != nil {
 		return false, fmt.Errorf("failed to get ready nodes from nodeClaims: %w", err)
 	} else if len(nodes) != len(existingNodeClaims) {
+		klog.Infof("node plugins not ready, # nodes (%d) is not equal to # nodeClaims (%d) for workspace %s/%s", len(nodes), len(existingNodeClaims), wObj.Namespace, wObj.Name)
 		return false, nil
 	}
 
@@ -89,14 +95,18 @@ func (c *NodeManager) checkNodePlugin(ctx context.Context, wObj *kaitov1beta1.Wo
 
 		gpuCapacity := node.Status.Capacity[resources.CapacityNvidiaGPU]
 		if gpuCapacity.IsZero() {
+			klog.Infof("node plugins not ready, %s does not have GPU capacity for workspace %s/%s", node.Name, wObj.Namespace, wObj.Name)
 			return false, nil
 		}
 
 		if node.Labels[corev1.LabelInstanceTypeStable] != wObj.Resource.InstanceType {
+			klog.Infof("node plugins not ready, %s instance type label %s does not match workspace instance type %s", node.Name, node.Labels[corev1.LabelInstanceTypeStable], wObj.Resource.InstanceType)
 			return false, nil
 		}
 	}
 
+	klog.Infof("all node plugins are ready for workspace %s/%s", wObj.Namespace, wObj.Name)
+
 	return true, nil
 }
 
@@ -127,28 +137,132 @@ func (c *NodeManager) getReadyNodesFromNodeClaims(ctx context.Context, wObj *kai
 	return nodes, nil
 }
 
+// EnsureNodesReady is used for checking the number of ready nodes meet the target node count. Updates the status condition accordingly.
+func (c *NodeManager) EnsureNodesReady(ctx context.Context, wObj *kaitov1beta1.Workspace, matchingNodes []*corev1.Node, nodeClaims []*karpenterv1.NodeClaim) (ready bool, retErr error) {
+	targetNodeCount := int(wObj.Status.TargetNodeCount)
+	readyCount := 0
+
+	for _, node := range matchingNodes {
+		if resources.NodeIsReadyAndNotDeleting(node) {
+			readyCount++
+		}
+
+		if !featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+			// If NAP is enabled, ensure the nodes have the instance type label set correctly and that it matches the workspace instance type.
+			instanceType, ok := node.Labels[corev1.LabelInstanceTypeStable]
+			var message string
+			if !ok {
+				message = fmt.Sprintf("Node %s is missing required instance type label", node.Name)
+			} else if instanceType != wObj.Resource.InstanceType {
+				message = fmt.Sprintf("Node %s instance type label %s does not match workspace instance type %s", node.Name, instanceType, wObj.Resource.InstanceType)
+			}
+
+			if message != "" {
+				if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeStatus, metav1.ConditionFalse,
+					"NodeNotReady", message); updateErr != nil {
+					return false, fmt.Errorf("failed to update Node status condition(NodeNotReady): %w", updateErr)
+				}
+			}
+		}
+	}
+
+	defer func() {
+		// Update the list of nodes, but don't block setting the condition on it.
+		if err := c.UpdateWorkerNodesInStatus(ctx, wObj, matchingNodes); err != nil {
+			klog.Error("failed to update worker nodes in workspace status", "workspace", klog.KObj(wObj), "error", err)
+			retErr = err
+		}
+	}()
+
+	if readyCount >= targetNodeCount { // Enough nodes are ready.
+		// If NAP is enabled, ensure node plugins are ready.
+		if !featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] {
+			ready, err := c.CheckIfNodePluginsReady(ctx, wObj, nodeClaims)
+			if err != nil {
+				return false, fmt.Errorf("failed to check node plugin readiness: %w", err)
+			}
+			if !ready {
+				return false, nil
+			}
+		}
+
+		// Enough Nodes are ready, mark condition as true.
+		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeStatus, metav1.ConditionTrue,
+			"NodesReady", fmt.Sprintf("Enough Nodes are ready (TargetNodes: %d, CurrentReadyNodes: %d, SelectedNodes: %d)", targetNodeCount, readyCount, len(matchingNodes))); updateErr != nil {
+			klog.ErrorS(updateErr, "failed to update Node status condition NodesReady to true", "workspace", klog.KObj(wObj))
+			return false, fmt.Errorf("failed to update Node status condition(NodesReady): %w", updateErr)
+		}
+		return true, nil
+	} else {
+		klog.InfoS("Not enough Nodes are ready for workspace", "workspace", client.ObjectKeyFromObject(wObj).String(),
+			"targetNodes", targetNodeCount, "currentReadyNodes", readyCount)
+		if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeNodeStatus, metav1.ConditionFalse,
+			"NodeNotReady", fmt.Sprintf("Not enough Nodes are ready (TargetNodes: %d, CurrentReadyNodes: %d, SelectedNodes: %d)", targetNodeCount, readyCount, len(matchingNodes))); updateErr != nil {
+			klog.ErrorS(updateErr, "failed to update Node status condition NodesReady to false", "workspace", klog.KObj(wObj))
+			return false, fmt.Errorf("failed to update Node status condition(NodeNotReady): %w", updateErr)
+		}
+
+		return false, nil
+	}
+}
+
+// VerifyOwnedConditions checks the owned conditions, and if any one is false, it sets the condition to false with the reason and message of the first false condition found.
+func (c *NodeManager) VerifyOwnedConditions(ctx context.Context, wObj *kaitov1beta1.Workspace, condition kaitov1beta1.ConditionType, conditionTypes []kaitov1beta1.ConditionType) (bool, error) {
+	for _, cType := range conditionTypes {
+		ownedCondition := meta.FindStatusCondition(wObj.Status.Conditions, string(cType))
+		if ownedCondition == nil {
+			continue // Condition not found, skip to the next one
+		} else if ownedCondition.Status != metav1.ConditionTrue {
+			// Set the owned condition to false with the reason and message of the first false condition found.
+			if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, condition, metav1.ConditionFalse, ownedCondition.Reason, ownedCondition.Message); err != nil {
+				klog.ErrorS(err, "failed to update workspace status", "workspace", klog.KObj(wObj))
+				return false, err
+			}
+			return false, nil
+		}
+	}
+
+	return true, nil
+}
+
+// SetResourceReadyConditionByStatus updates the status of the resource ready condition based on the statuses of owned conditions.
+func (c *NodeManager) SetResourceReadyConditionByStatus(ctx context.Context, wObj *kaitov1beta1.Workspace) error {
+	ownedConditions := []kaitov1beta1.ConditionType{
+		kaitov1beta1.ConditionTypeNodeClaimStatus,
+		kaitov1beta1.ConditionTypeNodeStatus,
+	}
+
+	// If any owned condition is false, set the resource condition to false and return.
+	if conditionTrue, err := c.VerifyOwnedConditions(ctx, wObj, kaitov1beta1.ConditionTypeResourceStatus, ownedConditions); err != nil {
+		return err
+	} else if conditionTrue {
+		// All owned conditions are true, set the resource condition to true.
+		if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, 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
+		}
+	}
+
+	return nil
+}
+
 // UpdateWorkerNodesInStatus updates the worker nodes list in workspace status.
-func (c *NodeManager) UpdateWorkerNodesInStatus(ctx context.Context, wObj *kaitov1beta1.Workspace, workerNodes []string) error {
-	sort.Strings(workerNodes)
-	if !reflect.DeepEqual(wObj.Status.WorkerNodes, workerNodes) {
+func (c *NodeManager) UpdateWorkerNodesInStatus(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodes []*corev1.Node) error {
+	nodeNames := make([]string, 0, len(readyNodes))
+	for _, node := range readyNodes {
+		nodeNames = append(nodeNames, node.Name)
+	}
+	sort.Strings(nodeNames)
+
+	if !reflect.DeepEqual(wObj.Status.WorkerNodes, nodeNames) {
 		if err := workspace.UpdateWorkspaceStatus(ctx, c.Client, &client.ObjectKey{Name: wObj.Name, Namespace: wObj.Namespace}, func(status *kaitov1beta1.WorkspaceStatus) error {
-			status.WorkerNodes = workerNodes
+			status.WorkerNodes = nodeNames
 			return nil
 		}); err != nil {
-			if updateErr := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, wObj, kaitov1beta1.ConditionTypeResourceStatus, metav1.ConditionFalse,
-				"workspaceResourceStatusFailed", err.Error()); updateErr != nil {
-				klog.ErrorS(updateErr, "failed to update workspace status", "workspace", klog.KObj(wObj))
-				return updateErr
-			}
 			return err
 		}
 	}
 
-	if err := workspace.UpdateStatusConditionIfNotMatch(ctx, c.Client, 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
-	}
-
 	return nil
 }
diff --git a/pkg/workspace/resource/node_claim.go b/pkg/workspace/resource/node_claim.go
index ccafd3f..d230274 100644
--- a/pkg/workspace/resource/node_claim.go
+++ b/pkg/workspace/resource/node_claim.go
@@ -17,6 +17,7 @@ import (
 	"context"
 	"fmt"
 
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/tools/record"
 	"k8s.io/client-go/util/retry"
@@ -26,6 +27,7 @@ import (
 
 	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"
 	"github.com/kaito-project/kaito/pkg/utils/plugin"
 	"github.com/kaito-project/kaito/pkg/utils/workspace"
@@ -47,23 +49,31 @@ func NewNodeClaimManager(c client.Client, recorder record.EventRecorder, expecta
 	}
 }
 
-// CheckNodeClaims checks the current state of NodeClaims with the target count recorded in the workspace.Status.
-func (c *NodeClaimManager) CheckNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace) (int, []*karpenterv1.NodeClaim, []string, error) {
-	var addedNodeClaimsCount int
+// GetNumNodeClaimsNeeded calculates how many NodeClaims are needed to meet the target node count for the workspace.
+func (c *NodeClaimManager) GetNumNodeClaimsNeeded(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodes []*corev1.Node) int {
+	targetNodeCount := int(wObj.Status.TargetNodeCount)
 
-	// Calculate the number of NodeClaims required (target - BYO nodes)
-	readyNodes, targetNodeClaimsCount, err := nodeclaim.ResolveReadyNodesAndTargetNodeClaimCount(ctx, c.Client, wObj)
-	if err != nil {
-		return addedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get required NodeClaims: %w", err)
+	// Count ready nodes that do NOT have a corresponding NodeClaim
+	readyNodesWithoutNodeClaim := 0
+
+	for _, node := range readyNodes {
+		if _, ok := node.Labels[consts.LabelNodePool]; !ok {
+			readyNodesWithoutNodeClaim++
+		}
 	}
 
+	// Calculate how many NodeClaims we need, including those already provisioned.
+	targetNodeClaimCount := max(0, targetNodeCount-readyNodesWithoutNodeClaim)
+
+	return targetNodeClaimCount
+}
+
+// CheckNodeClaims checks the current state of NodeClaims for the given workspace and determines how many additional NodeClaims need to be created to meet the target node count.
+func (c *NodeClaimManager) CheckNodeClaims(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodes []*corev1.Node) (int, []*karpenterv1.NodeClaim, error) {
+	// We don't care in this case if the ready nodes come from NodeClaims, meaning ready nodes could come from BYO if the right size and properly labeled.
 	ncList, err := nodeclaim.ListNodeClaim(ctx, wObj, c.Client)
 	if err != nil {
-		return addedNodeClaimsCount, nil, nil, fmt.Errorf("failed to get existing NodeClaims: %w", err)
-	}
-
-	if targetNodeClaimsCount > len(ncList.Items) {
-		addedNodeClaimsCount = targetNodeClaimsCount - len(ncList.Items)
+		return 0, nil, fmt.Errorf("failed to get existing NodeClaims: %w", err)
 	}
 
 	nodeClaims := make([]*karpenterv1.NodeClaim, 0, len(ncList.Items))
@@ -71,7 +81,18 @@ func (c *NodeClaimManager) CheckNodeClaims(ctx context.Context, wObj *kaitov1bet
 		nodeClaims = append(nodeClaims, &ncList.Items[i])
 	}
 
-	return addedNodeClaimsCount, nodeClaims, readyNodes, nil
+	klog.InfoS("Existing NodeClaims fetched", "count", len(nodeClaims), "workspace", klog.KObj(wObj))
+
+	// Calculate the total number of NodeClaims needed.
+	numNodeClaimsNeeded := c.GetNumNodeClaimsNeeded(ctx, wObj, readyNodes)
+	klog.InfoS("NodeClaims needed calculation", "needed", numNodeClaimsNeeded, "workspace", klog.KObj(wObj))
+
+	// Then, the number of NodeClaims to create is the difference between the total number needed and number of existing NodeClaims.
+	numNodeClaimsToCreate := max(0, numNodeClaimsNeeded-len(nodeClaims))
+
+	klog.InfoS("Number of NodeClaims to create", "count", numNodeClaimsToCreate)
+
+	return numNodeClaimsToCreate, nodeClaims, nil
 }
 
 // CreateUpNodeClaims creates a specified number of NodeClaims as defined by nodesToCreate for the given workspace.
@@ -116,13 +137,9 @@ func (c *NodeClaimManager) CreateUpNodeClaims(ctx context.Context, wObj *kaitov1
 	return nil
 }
 
-// AreNodeClaimsReady is used for checking the number of ready nodeclaims(isNodeClaimReadyNotDeleting) meet the target NodeClaim count needed
-func (c *NodeClaimManager) AreNodeClaimsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
-	// Calculate the actual number of NodeClaims needed (if BYO, numtargetNodes == 0)
-	_, targetNodeClaimCount, err := nodeclaim.ResolveReadyNodesAndTargetNodeClaimCount(ctx, c.Client, wObj)
-	if err != nil {
-		return false, fmt.Errorf("failed to resolve target NodeClaim count: %w", err)
-	}
+// EnsureNodeClaimsReady is used for checking the number of ready nodeclaims(isNodeClaimReadyNotDeleting) meet the target NodeClaim count needed. Updates the
+func (c *NodeClaimManager) EnsureNodeClaimsReady(ctx context.Context, wObj *kaitov1beta1.Workspace, readyNodes []*corev1.Node, existingNodeClaims []*karpenterv1.NodeClaim) (bool, error) {
+	targetNodeClaimCount := c.GetNumNodeClaimsNeeded(ctx, wObj, readyNodes)
 
 	readyCount := 0
 	for _, claim := range existingNodeClaims {
diff --git a/pkg/workspace/tuning/preset_tuning.go b/pkg/workspace/tuning/preset_tuning.go
index 931cebc..3797270 100644
--- a/pkg/workspace/tuning/preset_tuning.go
+++ b/pkg/workspace/tuning/preset_tuning.go
@@ -145,18 +145,11 @@ func SetupTrainingOutputVolume(ctx context.Context, configMap *corev1.ConfigMap,
 func CreatePresetTuning(ctx context.Context, workspaceObj *kaitov1beta1.Workspace, revisionNum string,
 	model pkgmodel.Model, kubeClient client.Client) (client.Object, error) {
 
-	var skuNumGPUs int
 	gpuConfig, err := utils.GetGPUConfigBySKU(workspaceObj.Resource.InstanceType)
 	if err != nil {
-		gpuConfig, err = utils.TryGetGPUConfigFromNode(ctx, kubeClient, workspaceObj.Status.WorkerNodes)
-		if err != nil {
-			defaultNumGPU := resource.MustParse(model.GetTuningParameters().GPUCountRequirement)
-			skuNumGPUs = int(defaultNumGPU.Value())
-		}
-	}
-	if gpuConfig != nil {
-		skuNumGPUs = gpuConfig.GPUCount
+		return nil, err
 	}
+	skuNumGPUs := gpuConfig.GPUCount
 
 	gctx := &generator.WorkspaceGeneratorContext{
 		Ctx:        ctx,
diff --git a/presets/ragengine/main.py b/presets/ragengine/main.py
index fef6656..a46faaa 100644
--- a/presets/ragengine/main.py
+++ b/presets/ragengine/main.py
@@ -28,8 +28,6 @@ from models import (
     HealthStatus,
     IndexRequest,
     ListDocumentsResponse,
-    QueryRequest,
-    QueryResponse,
     UpdateDocumentRequest,
     UpdateDocumentResponse,
 )
@@ -55,7 +53,6 @@ from ragengine.metrics.prometheus_metrics import (
     e2e_request_latency_seconds,
     e2e_request_total,
     num_requests_running,
-    rag_avg_source_score,
     rag_chat_latency,
     rag_chat_requests_total,
     rag_delete_latency,
@@ -72,23 +69,21 @@ from ragengine.metrics.prometheus_metrics import (
     rag_indexes_update_document_requests_total,
     rag_load_latency,
     rag_load_requests_total,
-    rag_lowest_source_score,
     rag_persist_latency,
     rag_persist_requests_total,
-    rag_query_latency,
-    rag_query_requests_total,
 )
 
 # Import Prometheus client for metrics collection
 
 
-app = FastAPI()
+app = FastAPI(
+    title="KAITO RAG Engine",
+)
 
 
 @app.middleware("http")
 async def track_requests(request: Request, call_next):
     tracked_paths = [
-        "/query",
         "/index",
         "/indexes",
         "/persist",
@@ -137,7 +132,7 @@ vector_store_handler = FaissVectorStoreHandler(embedding_manager)
 rag_ops = VectorStoreManager(vector_store_handler)
 
 
-@app.get("/metrics", tags=["Monitoring"])
+@app.get("/metrics", operation_id="get_metrics", tags=["Monitoring"])
 async def metrics():
     """
     Expose Prometheus metrics.
@@ -153,6 +148,8 @@ async def metrics():
 
 @app.get(
     "/health",
+    operation_id="get_health",
+    tags=["Monitoring"],
     response_model=HealthStatus,
     summary="Health Check for RAG Engine",
     description="""
@@ -187,6 +184,8 @@ def health_check():
 
 @app.post(
     "/index",
+    operation_id="create_index",
+    tags=["Index"],
     response_model=list[Document],
     summary="Index Documents",
     description="""
@@ -240,97 +239,10 @@ async def index_documents(request: IndexRequest):
         )
 
 
-@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?",
-      "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": [{"doc_id": "doc1", "node_id": "node1", "text": "RAG explanation...", "score": 0.95, "metadata": {}}],
-      "metadata": {}
-    }
-    ```
-    """,
-)
-async def query_index(request: QueryRequest):
-    start_time = time.perf_counter()
-    status = STATUS_FAILURE  # Default status
-
-    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
-
-        result_dict = await rag_ops.query(
-            request.index_name, request.query, request.top_k, llm_params, rerank_params
-        )
-
-        result = QueryResponse(
-            response=result_dict["response"],
-            source_nodes=result_dict["source_nodes"],
-            metadata=result_dict.get("metadata", {}),
-        )
-
-        # Record source retrieval quality metrics
-        if result.source_nodes and result.response:
-            lowest_score_node = min(result.source_nodes, key=lambda x: x.score)
-            rag_lowest_source_score.observe(lowest_score_node.score)
-
-            scores = [node.score for node in result.source_nodes]
-            avg_score = sum(scores) / len(scores)
-            rag_avg_source_score.observe(avg_score)
-
-        status = STATUS_SUCCESS
-        return result
-    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:
-        raise HTTPException(
-            status_code=500, detail=f"An unexpected error occurred: {str(e)}"
-        )
-    finally:
-        # Record metrics once in finally block
-        rag_query_requests_total.labels(status=status).inc()
-        rag_query_latency.labels(status=status).observe(
-            time.perf_counter() - start_time
-        )
-
-
 @app.post(
     "/v1/chat/completions",
+    operation_id="chat",
+    tags=["Chat"],
     response_model=ChatCompletionResponse,
     summary="OpenAI-Compatible Chat Completions API",
     description="""
@@ -402,6 +314,8 @@ async def chat_completions(request: dict):
 
 @app.get(
     "/indexes",
+    operation_id="list_indexes",
+    tags=["Index"],
     response_model=list[str],
     summary="List All Indexes",
     description="""
@@ -436,6 +350,8 @@ def list_indexes():
 
 @app.get(
     "/indexes/{index_name}/documents",
+    operation_id="list_documents_in_index",
+    tags=["Index"],
     response_model=ListDocumentsResponse,
     summary="List Documents in an Index",
     description="""
@@ -533,6 +449,8 @@ async def list_documents_in_index(
 
 @app.post(
     "/indexes/{index_name}/documents",
+    operation_id="update_documents_in_index",
+    tags=["Index"],
     response_model=UpdateDocumentResponse,
     summary="Update documents in an Index",
     description="""
@@ -581,6 +499,8 @@ async def update_documents_in_index(
 
 @app.post(
     "/indexes/{index_name}/documents/delete",
+    operation_id="delete_documents_in_index",
+    tags=["Index"],
     response_model=DeleteDocumentResponse,
     summary="Delete documents in an Index",
     description="""
@@ -628,6 +548,8 @@ async def delete_documents_in_index(
 
 @app.post(
     "/persist/{index_name}",
+    operation_id="persist_index",
+    tags=["Index"],
     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.
@@ -681,6 +603,8 @@ async def persist_index(
 
 @app.post(
     "/load/{index_name}",
+    operation_id="load_index",
+    tags=["Index"],
     summary="Load Index Data from Disk",
     description="""
     Load an existing index from disk at a specified location.
@@ -730,6 +654,8 @@ async def load_index(
 
 @app.delete(
     "/indexes/{index_name}",
+    operation_id="delete_index",
+    tags=["Index"],
     summary="Delete the Index",
     description="""
     Delete an existing index
diff --git a/presets/ragengine/metrics/prometheus_metrics.py b/presets/ragengine/metrics/prometheus_metrics.py
index 8a862e3..37bf6b6 100644
--- a/presets/ragengine/metrics/prometheus_metrics.py
+++ b/presets/ragengine/metrics/prometheus_metrics.py
@@ -35,18 +35,6 @@ rag_embedding_requests_total = Counter(
     labelnames=[STATUS_LABEL, MODE_LABEL],
 )
 
-# Query API metrics
-rag_query_latency = Histogram(
-    "rag_query_latency_seconds",
-    "Time to call '/query' API in seconds",
-    labelnames=[STATUS_LABEL],
-)
-rag_query_requests_total = Counter(
-    "rag_query_requests_total",
-    "Count of successful/failed calling '/query' requests",
-    labelnames=[STATUS_LABEL],
-)
-
 # Chat API metrics
 rag_chat_latency = Histogram(
     "rag_chat_latency_seconds",
diff --git a/presets/ragengine/models.py b/presets/ragengine/models.py
index ccb97c3..fc945c7 100644
--- a/presets/ragengine/models.py
+++ b/presets/ragengine/models.py
@@ -12,13 +12,11 @@
 # limitations under the License.
 
 
-from typing import Any
-
 from llama_index.core.base.llms.types import ChatMessage, MessageRole
 from openai.types.chat import (
     ChatCompletion,
 )
-from pydantic import BaseModel, Field, model_validator
+from pydantic import BaseModel, Field
 
 
 class Document(BaseModel):
@@ -59,39 +57,7 @@ class DeleteDocumentResponse(BaseModel):
     not_found_doc_ids: list[str]
 
 
-class QueryRequest(BaseModel):
-    index_name: str
-    query: str
-    top_k: int = 5
-    # Accept a dictionary for our LLM parameters
-    llm_params: dict[str, Any] | None = Field(
-        default_factory=dict,
-        description="Optional parameters for the language model, e.g., temperature, top_p",
-    )
-    # Accept a dictionary for rerank parameters
-    rerank_params: dict[str, Any] | None = Field(
-        default_factory=dict,
-        description="Experimental: Optional parameters for reranking. Only 'top_n' and 'choice_batch_size' are supported.",
-    )
-
-    @model_validator(mode="after")
-    def validate_params(self) -> "QueryRequest":
-        # Access fields as attributes
-        rerank_params = self.rerank_params
-        top_k = self.top_k
-
-        # Validate rerank parameters
-        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 self
-
-
-# Define models for NodeWithScore, and QueryResponse
+# Define models for NodeWithScore
 class NodeWithScore(BaseModel):
     doc_id: str
     node_id: str
@@ -100,12 +66,6 @@ class NodeWithScore(BaseModel):
     metadata: dict | None = None
 
 
-class QueryResponse(BaseModel):
-    response: str
-    source_nodes: list[NodeWithScore]
-    metadata: dict | None = None
-
-
 class HealthStatus(BaseModel):
     status: str
     detail: str | None = None
diff --git a/presets/ragengine/vector_store/base.py b/presets/ragengine/vector_store/base.py
index b69b279..cd306dc 100644
--- a/presets/ragengine/vector_store/base.py
+++ b/presets/ragengine/vector_store/base.py
@@ -28,15 +28,12 @@ from llama_index.core import Document as LlamaDocument
 from llama_index.core import StorageContext, VectorStoreIndex, load_index_from_storage
 from llama_index.core.base.llms.types import MessageRole
 from llama_index.core.chat_engine.types import ChatMode
-from llama_index.core.postprocessor import LLMRerank  # Query with LLM Reranking
 from llama_index.core.storage.docstore import SimpleDocumentStore
 from llama_index.vector_stores.faiss import FaissMapVectorStore
 from openai.types.chat import ChatCompletionContentPartTextParam, CompletionCreateParams
 from pydantic import ValidationError
 
 from ragengine.config import (
-    LLM_RERANKER_BATCH_SIZE,
-    LLM_RERANKER_TOP_N,
     RAG_DEFAULT_CONTEXT_TOKEN_FILL_RATIO,
     RAG_DOCUMENT_NODE_TOKEN_APPROXIMATION,
     RAG_SIMILARITY_THRESHOLD,
@@ -168,84 +165,6 @@ class BaseVectorStore(ABC):
                 self.index_map[index_name] = index
         return indexed_doc_ids
 
-    async def query(
-        self,
-        index_name: str,
-        query: str,
-        top_k: int,
-        llm_params: dict,
-        rerank_params: dict,
-    ):
-        """
-        Query the indexed documents
-
-        Args:
-            index_name (str): Name of the index to query
-            query (str): Query string
-            top_k (int): Number of initial top results to retrieve
-            llm_params (dict): Optional parameters for the language model
-            rerank_params (dict): Optional configuration for reranking
-                - 'top_n' (int): Number of top documents to return after reranking
-                - 'choice_batch_size' (int):  Number of documents to process in each batch
-
-        Returns:
-            dict: A dictionary containing the response and source nodes.
-        """
-        if index_name not in self.index_map:
-            raise HTTPException(
-                status_code=404, detail=f"No such index: '{index_name}' exists."
-            )
-
-        node_postprocessors = []
-        if rerank_params:
-            # Set default reranking parameters and merge with provided params
-            default_rerank_params = {
-                "choice_batch_size": LLM_RERANKER_BATCH_SIZE,  # Default batch size
-                "top_n": min(
-                    LLM_RERANKER_TOP_N, top_k
-                ),  # Limit top_n to top_k by default
-            }
-            rerank_params = {**default_rerank_params, **rerank_params}
-
-            # Add LLMRerank to postprocessors
-            node_postprocessors.append(
-                LLMRerank(
-                    llm=self.llm,
-                    choice_batch_size=rerank_params["choice_batch_size"],
-                    top_n=rerank_params["top_n"],
-                )
-            )
-
-        query_engine = self.index_map[index_name].as_chat_engine(
-            llm=self.llm,
-            similarity_top_k=top_k,
-            node_postprocessors=node_postprocessors,
-            chat_mode=ChatMode.CONDENSE_PLUS_CONTEXT,
-            verbose=True,
-        )
-
-        if self.use_rwlock:
-            async with self.rwlock.reader_lock:
-                self.llm.set_params(llm_params)
-                query_result = await query_engine.achat(query)
-        else:
-            self.llm.set_params(llm_params)
-            query_result = await query_engine.achat(query)
-        return {
-            "response": query_result.response,
-            "source_nodes": [
-                {
-                    "doc_id": source_node.node.ref_doc_id,
-                    "node_id": source_node.node_id,
-                    "text": source_node.text,
-                    "score": source_node.score,
-                    "metadata": source_node.metadata,
-                }
-                for source_node in query_result.source_nodes
-            ],
-            "metadata": query_result.metadata,
-        }
-
     async def chat_completion(self, request: dict) -> ChatCompletionResponse:
         """
         Create a chat completion based on the provided request.
diff --git a/presets/ragengine/vector_store_manager/manager.py b/presets/ragengine/vector_store_manager/manager.py
index 5832be7..d2a9f13 100644
--- a/presets/ragengine/vector_store_manager/manager.py
+++ b/presets/ragengine/vector_store_manager/manager.py
@@ -24,19 +24,6 @@ class VectorStoreManager:
         """Index new documents."""
         return await self.vector_store.index_documents(index_name, documents)
 
-    async def query(
-        self,
-        index_name: str,
-        query: str,
-        top_k: int,
-        llm_params: dict,
-        rerank_params: dict,
-    ):
-        """Query the indexed documents."""
-        return await self.vector_store.query(
-            index_name, query, top_k, llm_params, rerank_params
-        )
-
     async def chat_completion(self, request: dict):
         """Chat completion using the vector store."""
         return await self.vector_store.chat_completion(request)
diff --git a/presets/workspace/models/supported_models.yaml b/presets/workspace/models/supported_models.yaml
index e51a04a..4c6cd06 100644
--- a/presets/workspace/models/supported_models.yaml
+++ b/presets/workspace/models/supported_models.yaml
@@ -3,8 +3,9 @@ models:
   - name: base
     type: text-generation
     runtime: tfs
-    tag: 0.0.8
+    tag: 0.0.9
     # Tag history:
+    # 0.0.9 - bump pip to 25.3
     # 0.0.8 - Fix multi-node-health-check issue
     # 0.0.7 - bump lmcache to 0.3.5
     # 0.0.6 - bump vLLM to 0.10.1.1, transformers to 4.55.2, torch to 2.7.1, lmcache to 0.3.3, ray to 2.48.0, accelerate to 1.3.0
diff --git a/rag_benchmark_docs/RAG_BENCHMARK_DOCS_GUIDE.md b/rag_benchmark_docs/RAG_BENCHMARK_DOCS_GUIDE.md
new file mode 100644
index 0000000..8385e01
--- /dev/null
+++ b/rag_benchmark_docs/RAG_BENCHMARK_DOCS_GUIDE.md
@@ -0,0 +1,353 @@
+# RAG Benchmark Testing Guide (Document Q&A)
+
+## Overview
+
+This benchmark tool helps you **quantitatively measure** how much RAG improves over pure LLM responses for **document-based question answering**. It generates test questions from your indexed documents, compares answers from both systems, and produces detailed performance reports.
+
+> **Scope**: This tool is designed for benchmarking RAG on **pre-indexed documents**. It works with any content type (PDFs, reports, manuals, code, etc.) as long as the documents are already indexed in your RAG system.
+
+> **Important**: Users must manually create and index documents in their RAG system before running this benchmark. The tool does NOT handle document indexing.
+
+**Use Cases:**
+- Evaluate if RAG is worth implementing for document Q&A use cases
+- Measure RAG performance improvement on your specific indexed content (usually 100-300%+)
+- Compare different RAG configurations for document retrieval
+- Generate quantitative reports for stakeholders
+
+---
+
+## Quick Start
+
+**Prerequisites:**
+1. Index your documents in the RAG system first
+2. Note the index name you created
+
+```bash
+python rag_benchmark_docs.py \
+  --index-name my_docs_index \
+  --rag-url http://localhost:5000 \
+  --llm-url http://your-llm-api.com \
+  --judge-url http://your-llm-api.com \
+  --llm-model "deepseek-v3.1" \
+  --judge-model "deepseek-v3.1" \
+  --llm-api-key "your-api-key" \
+  --judge-api-key "your-api-key"
+```
+
+---
+
+## How It Works
+
+### Architecture Flow
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│  STEP 1: Query Existing Index                                          │
+└─────────────────────────────────────────────────────────────────────────┘
+                              │
+                   [User provides --index-name]
+                              │
+                              ▼
+                   ┌────────────────────────┐
+                   │   Validate Index       │
+                   │   Exists in RAG System │
+                   └────────────┬───────────┘
+                                │
+┌─────────────────────────────────────────────────────────────────────────┐
+│  STEP 2: Generate Test Questions                                       │
+└─────────────────────────────────────────────────────────────────────────┘
+                                │
+                   Retrieve Indexed Nodes via API
+                                │
+                                ▼
+                   ┌────────────────────────┐
+                   │  Smart Node Sampling   │
+                   │  (20 nodes selected)   │
+                   └────────────┬───────────┘
+                                │
+                                ▼
+                   ┌────────────────────────┐
+                   │ LLM Generates Q&A      │
+                   └────────────┬───────────┘
+                                │
+               ┌────────────────┴────────────────┐
+               ▼                                 ▼
+    ┌─────────────────────┐          ┌─────────────────────┐
+    │ 10 Closed Questions │          │ 10 Open Questions   │
+    │ (Factual)           │          │ (Analytical)        │
+    │ Score: 0/5/10       │          │ Score: 0-10         │
+    └─────────────────────┘          └─────────────────────┘
+                                │
+┌─────────────────────────────────────────────────────────────────────────┐
+│  STEP 3: Run Benchmark Tests (20 questions × 2 systems)                │
+└─────────────────────────────────────────────────────────────────────────┘
+                                │
+              ┌─────────────────┴─────────────────┐
+              ▼                                   ▼
+   ┌──────────────────────┐          ┌──────────────────────┐
+   │   RAG System         │          │   Pure LLM           │
+   │                      │          │                      │
+   │ • Search index       │          │ • No context         │
+   │ • Retrieve context   │          │ • Direct query       │
+   │ • Generate answer    │          │ • Generate answer    │
+   └──────────┬───────────┘          └──────────┬───────────┘
+              │                                   │
+              └─────────────┬─────────────────────┘
+                            │
+┌─────────────────────────────────────────────────────────────────────────┐
+│  STEP 4: Evaluate with LLM Judge                                       │
+└─────────────────────────────────────────────────────────────────────────┘
+                            │
+                            ▼
+                   ┌────────────────────────┐
+                   │  LLM Judge Scores      │
+                   │  Each Answer: 0-10     │
+                   │                        │
+                   │  40 total evaluations  │
+                   │  (20 RAG + 20 LLM)     │
+                   └────────────┬───────────┘
+                                │
+┌─────────────────────────────────────────────────────────────────────────┐
+│  STEP 5: Generate Report                                               │
+└─────────────────────────────────────────────────────────────────────────┘
+                                │
+                                ▼
+                   ┌────────────────────────┐
+                   │  Performance Metrics   │
+                   │                        │
+                   │ • RAG avg score        │
+                   │ • LLM avg score        │
+                   │ • Improvement %        │
+                   │ • Token efficiency     │
+                   └────────────────────────┘
+```
+
+---
+
+## Detailed Process
+
+### Step 1: Use Existing Index
+- **Index Required**: You must provide `--index-name` pointing to a pre-indexed collection in your RAG system
+- **Pre-requisite**: Documents must be indexed beforehand using your RAG system's indexing API/tool
+- **Flexibility**: Works with any content type (documents, code, web pages, etc.)
+
+### Step 2: Test Question Generation
+1. **Retrieve nodes** from RAG index via API
+2. **Filter** content-rich nodes (min 200 chars, 30 words)
+3. **Smart sampling**: 
+   - If ≤20 nodes: distribute questions evenly
+   - If >20 nodes: randomly select 20
+4. **Generate Q&A pairs** using LLM:
+   - 10 closed-ended (factual, specific answers)
+   - 10 open-ended (comprehension, analysis)
+   - Ground truth answers included
+
+### Step 3: Comparative Testing
+For each question, both systems generate answers:
+
+**RAG System:**
+- Performs vector similarity search
+- Retrieves relevant context chunks
+- Passes context + question to LLM
+- Generates contextually-aware answer
+
+**Pure LLM:**
+- No document access
+- Relies solely on pre-trained knowledge
+- Often lacks specific details
+- May hallucinate or be outdated
+
+### Step 4: Evaluation
+**LLM as Judge** evaluates each answer:
+
+**Closed Questions (0/5/10 scoring):**
+- 10 = Completely correct, all facts match
+- 5 = Partially correct, missing details
+- 0 = Wrong or irrelevant
+
+**Open Questions (0-10 gradient):**
+- Accuracy (3 pts)
+- Completeness (3 pts)
+- Understanding (2 pts)
+- Relevance (2 pts)
+
+### Step 5: Results Analysis
+Reports include:
+- Average scores (RAG vs LLM)
+- Performance improvement percentage
+- Category breakdown (closed vs open)
+- Token usage efficiency
+- Detailed Q&A with scores
+
+---
+
+## Output Files
+
+All results saved to `benchmark_results/` directory:
+
+```
+benchmark_results/
+├── questions_20251020_043345.json    # Generated test questions
+├── results_20251020_043345.json      # Detailed answers & scores
+├── report_20251020_043345.json       # JSON metrics
+└── report_20251020_043345.txt        # Human-readable report
+```
+
+### Sample Report Output
+
+```
+RAG vs LLM Benchmark Report
+==================================================
+
+Total Questions: 20
+
+Average Scores (0-10):
+  RAG Overall: [Score]
+  LLM Overall: [Score]
+  Performance Improvement: [Percentage]
+
+Closed Questions (0/5/10 scoring - factual accuracy):
+  RAG: [Score]
+  LLM: [Score]
+
+Open Questions (0-10 scoring - comprehensive evaluation):
+  RAG: [Score]
+  LLM: [Score]
+
+Token Usage:
+  RAG: [Count] tokens
+  LLM: [Count] tokens
+  Efficiency: [Percentage] fewer/more tokens with RAG
+```
+
+---
+
+## Configuration Options
+
+```bash
+python rag_benchmark_docs.py [OPTIONS]
+
+Required:
+  --index-name NAME       Name of existing RAG index (REQUIRED)
+
+Service URLs:
+  --rag-url URL           RAG service endpoint (default: http://localhost:5000)
+  --llm-url URL           Pure LLM API endpoint (default: http://localhost:8081)
+  --judge-url URL         Judge LLM API endpoint (default: http://localhost:8082)
+
+Model Configuration:
+  --llm-model NAME        Model name for LLM/RAG service (default: gpt-4)
+  --judge-model NAME      Model name for Judge LLM (default: gpt-4)
+
+Authentication:
+  --llm-api-key KEY       API key for LLM service (or env: LLM_API_KEY)
+  --judge-api-key KEY     API key for Judge LLM (or env: JUDGE_API_KEY)
+
+Test Configuration:
+  --closed-questions N    Number of closed questions (default: 10)
+  --open-questions N      Number of open questions (default: 10)
+  --output-dir DIR        Results directory (default: benchmark_results)
+```
+
+---
+
+## Requirements
+
+### Prerequisites
+1. **Indexed Documents**: You must have already indexed your documents in the RAG system
+2. **RAG Service**: Running RAG instance accessible via HTTP API
+3. **LLM API**: Compatible with OpenAI Chat Completion format
+
+### Python Dependencies
+- **Python 3.8+** with dependencies:
+  ```bash
+  pip install requests tqdm
+  ```
+
+### Indexing Your Documents (Before Running Benchmark)
+
+This tool does NOT handle document indexing. You must index documents beforehand using your RAG system's tools. Example workflow:
+
+```bash
+# Example 1: Using RAG API directly
+curl -X POST http://localhost:5000/index \
+  -H "Content-Type: application/json" \
+  -d '{
+    "index_name": "my_docs",
+    "documents": [
+      {"text": "Your document content...", "metadata": {...}}
+    ]
+  }'
+
+# Example 2: Using a custom indexing script
+python index_documents.py --input docs/ --index-name my_docs
+
+# Then run benchmark with the index name
+python rag_benchmark_docs.py --index-name my_docs
+```
+
+---
+
+## Tips for Best Results
+
+1. **Index quality matters**: Ensure your documents are properly chunked and indexed before benchmarking
+2. **Use representative content**: Test with indexed content similar to production use cases
+3. **Sufficient content**: Indexes with 20+ rich content nodes work best
+4. **Quality over quantity**: 20 well-crafted questions > 100 random ones
+5. **Consistent LLM**: Use same model for question generation and judging (via `--llm-model` and `--judge-model`)
+6. **Review questions**: Check `questions_*.json` to ensure quality
+7. **Model selection**: Configure model names explicitly if not using default "gpt-4"
+
+---
+
+## Expected Performance
+
+Based on typical benchmarks:
+
+| Metric | RAG | Pure LLM | Notes |
+|--------|-----|----------|-------|
+| **Closed Questions** | Higher | Lower | RAG has document access |
+| **Open Questions** | Higher | Moderate | RAG provides context |
+| **Token Usage** | Variable | Variable | Depends on retrieval |
+| **Accuracy** | High | Limited | Document-specific knowledge |
+
+**Key Finding**: RAG typically excels at factual accuracy (closed questions) where pure LLM lacks specific document knowledge.
+
+---
+
+## Troubleshooting
+
+**Q: Getting "index not found" errors?**
+- Verify the index name exists in your RAG system
+- Check index name spelling matches exactly
+- Ensure documents are properly indexed before running benchmark
+
+**Q: Getting "Connection refused" errors?**
+- Ensure RAG service is running on specified port
+- Check LLM API endpoints are accessible
+- Verify firewall/network settings
+
+**Q: Getting "No documents found in index" errors?**
+- Your index may be empty or have no content-rich nodes
+- Re-index your documents with proper content
+- Check that indexing process completed successfully
+
+**Q: Questions seem low quality?**
+- Index may have insufficient content-rich nodes (need 20+ nodes with 200+ chars)
+- Try indexing more comprehensive documents
+- Ensure documents have meaningful text content
+
+**Q: Scores seem biased?**
+- Use different LLM for judging vs generation
+- Configure models explicitly with `--llm-model` and `--judge-model`
+- Review scoring prompts for clarity
+
+**Q: Process is slow?**
+- Expected: ~10-15 minutes for 20 questions
+- Reduce question count for faster testing: `--closed-questions 5 --open-questions 5`
+- Each question requires 4 LLM calls (2 answers + 2 scores)
+
+**Q: Token counts seem wrong?**
+- Tool now extracts real token usage from API responses
+- If API doesn't return token data, it falls back to estimation (with warning in logs)
+- Check that your LLM API returns usage data in OpenAI format
\ No newline at end of file
diff --git a/rag_benchmark_docs/RAG_BENCHMARK_DOCS_README.md b/rag_benchmark_docs/RAG_BENCHMARK_DOCS_README.md
new file mode 100644
index 0000000..51f81a5
--- /dev/null
+++ b/rag_benchmark_docs/RAG_BENCHMARK_DOCS_README.md
@@ -0,0 +1,106 @@
+# RAG Benchmark Tool (Document Testing)
+
+Quick benchmark testing to measure RAG performance improvement over pure LLM **for document-based Q&A**.
+
+> **Note**: This tool benchmarks RAG performance on **pre-indexed documents**. Works with any content type (documents, code, web pages, etc.) as long as it's indexed in your RAG system.
+
+> **Important**: You must manually index your documents in the RAG system **BEFORE** running this benchmark. This tool does NOT handle indexing.
+
+## 🚀 Quick Start
+
+**Step 1: Index your documents first** (using your RAG system's tools)
+
+**Step 2: Run benchmark with the index name**
+```bash
+python rag_benchmark_docs.py \
+  --index-name my_docs_index \
+  --rag-url http://localhost:5000 \
+  --llm-url http://your-llm-api.com \
+  --judge-url http://your-llm-api.com \
+  --llm-model "deepseek-v3.1" \
+  --judge-model "deepseek-v3.1" \
+  --llm-api-key "your-api-key" \
+  --judge-api-key "your-api-key"
+```
+
+## 📊 What You Get
+
+```
+RAG vs LLM Benchmark Report
+==================================================
+
+Total Questions: 20
+
+Average Scores (0-10):
+  RAG Overall: [Higher score]        ← Your RAG system
+  LLM Overall: [Lower score]         ← Pure LLM baseline
+  Performance Improvement: [Percentage]
+
+Closed Questions (factual accuracy):
+  RAG: [Higher - has access to documents]
+  LLM: [Lower - no document context]
+
+Open Questions (comprehensive):
+  RAG: [Higher - contextually aware]
+  LLM: [Lower - general knowledge only]
+
+Token Usage:
+  RAG: [Typically fewer tokens]
+  LLM: [More tokens]
+  Efficiency: [RAG usually more efficient]
+```
+
+## 📖 Documentation
+
+- **[RAG_BENCHMARK_DOCS_GUIDE.md](./RAG_BENCHMARK_DOCS_GUIDE.md)** - Complete guide with architecture, configuration options, and troubleshooting
+- **[benchmark_results/](../benchmark_results/)** - Results output directory
+
+## 🎯 Key Features
+
+- ✅ **Pre-indexed content**: Works with any indexed content in your RAG system
+- ✅ **Smart sampling**: Automatically selects representative content from index
+- ✅ **Dual question types**: 10 factual + 10 analytical questions
+- ✅ **LLM-as-Judge**: Automated scoring with ground truth comparison
+- ✅ **Detailed reports**: JSON + human-readable formats
+- ✅ **Real token tracking**: Extracts actual token usage from API responses
+- ✅ **Configurable models**: Specify any LLM model via `--llm-model` and `--judge-model`
+
+## 📦 Requirements
+
+```bash
+pip install requests tqdm
+```
+
+- **Pre-indexed documents** in your RAG system (you must index them first!)
+- Running RAG service with accessible index
+- LLM API access (OpenAI-compatible format)
+
+## ⏱️ Runtime
+
+- ~10-15 minutes for 20 questions
+- 4 LLM calls per question (2 answers + 2 scores)
+- Adjustable with `--closed-questions` and `--open-questions` flags
+
+## 🎓 Example Use Cases
+
+1. **Prove ROI**: Show stakeholders quantitative RAG improvements
+2. **A/B Testing**: Compare different RAG configurations or models
+3. **Quality Control**: Ensure RAG maintains performance over time
+4. **Documentation**: Generate reports for compliance/audits
+5. **Model Comparison**: Test different LLM models with the same indexed content
+
+## 📋 Workflow
+
+```
+1. Index your documents (manually, using your RAG system)
+   ↓
+2. Note the index name
+   ↓
+3. Run: python rag_benchmark_docs.py --index-name <name>
+   ↓
+4. Get detailed performance report
+```
+
+---
+
+**Ready to benchmark?** Start with the [Quick Start](#-quick-start) above or read the [full guide](./RAG_BENCHMARK_DOCS_GUIDE.md) for advanced options.
diff --git a/rag_benchmark_docs/README.md b/rag_benchmark_docs/README.md
new file mode 100644
index 0000000..6326c0e
--- /dev/null
+++ b/rag_benchmark_docs/README.md
@@ -0,0 +1,26 @@
+# RAG Benchmark for Documents
+
+This folder contains tools to benchmark RAG performance on **document-based Q&A** tasks.
+
+> **Note**: This is specifically for testing RAG on text documents (PDFs, reports, manuals). Code-related RAG benchmarking uses a different tool.
+
+## 📁 Files
+
+- **`rag_benchmark_docs.py`** - Main benchmark script
+- **`RAG_BENCHMARK_DOCS_README.md`** - Quick start guide
+- **`RAG_BENCHMARK_DOCS_GUIDE.md`** - Complete documentation
+
+## 🚀 Quick Start
+
+Read `RAG_BENCHMARK_DOCS_README.md` to get started in 2 minutes.
+
+## 📊 What This Tests
+
+- **Document retrieval accuracy**: How well RAG finds relevant context
+- **Answer quality**: RAG vs pure LLM on factual questions
+- **Comprehension**: RAG vs pure LLM on analytical questions
+- **Token efficiency**: Cost comparison
+
+
+
+Results are saved to `../benchmark_results/` directory.
diff --git a/rag_benchmark_docs/rag_benchmark_docs.py b/rag_benchmark_docs/rag_benchmark_docs.py
new file mode 100644
index 0000000..711845a
--- /dev/null
+++ b/rag_benchmark_docs/rag_benchmark_docs.py
@@ -0,0 +1,822 @@
+# Copyright (c) KAITO authors.
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#!/usr/bin/env python3
+"""
+RAG vs LLM Benchmark Test
+
+Tests RAG performance improvement over pure LLM using pre-indexed documents
+
+Workflow:
+1. Query existing RAG index (user must index documents beforehand)
+2. Retrieve document nodes from RAG index
+3. Generate test questions (10 closed + 10 open by default)
+4. Test both RAG and pure LLM on same questions
+5. Judge answers and generate report
+
+Question Types:
+- Closed: Factual questions with definitive answers (scored 0/5/10)
+- Open: Complex questions requiring comprehensive answers (scored 0-10)
+
+Output:
+- questions_{timestamp}.json - Generated test questions
+- results_{timestamp}.json - Detailed test results
+- report_{timestamp}.json - Performance metrics (JSON)
+- report_{timestamp}.txt - Human-readable report
+
+Example:
+    python rag_benchmark_docs.py \\
+        --index-name my_docs_index \\
+        --rag-url http://localhost:5000 \\
+        --llm-url http://localhost:8081 \\
+        --judge-url http://localhost:8082 \\
+        --closed-questions 10 \\
+        --open-questions 10 \\
+        --output-dir benchmark_results
+"""
+
+import argparse
+import json
+import logging
+import os
+import random
+import time
+from dataclasses import asdict, dataclass
+from pathlib import Path
+
+import requests
+from tqdm import tqdm
+
+# Configure logging
+logging.basicConfig(
+    level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
+)
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class TestQuestion:
+    """Test question with ground truth answer"""
+
+    id: int
+    question: str
+    answer: str
+    question_type: str  # "closed" or "open"
+
+
+@dataclass
+class TestResult:
+    """Result for a single test question"""
+
+    question_id: int
+    rag_answer: str
+    llm_answer: str
+    rag_score: float
+    llm_score: float
+    rag_tokens: int
+    llm_tokens: int
+
+
+@dataclass
+class BenchmarkReport:
+    """Final benchmark report"""
+
+    total_questions: int
+    rag_avg_score: float
+    llm_avg_score: float
+    rag_closed_avg: float
+    llm_closed_avg: float
+    rag_open_avg: float
+    llm_open_avg: float
+    performance_improvement: float
+    rag_total_tokens: int
+    llm_total_tokens: int
+    token_efficiency: float
+
+
+class RAGBenchmark:
+    """Main benchmark testing class"""
+
+    def __init__(
+        self,
+        rag_url: str,
+        llm_url: str,
+        judge_llm_url: str,
+        llm_model: str = "gpt-4",
+        judge_model: str = "gpt-4",
+        llm_api_key: str | None = None,
+        judge_api_key: str | None = None,
+    ):
+        """
+        Initialize benchmark with service URLs
+
+        Args:
+            rag_url: RAGEngine service URL (e.g., http://localhost:8080)
+            llm_url: Pure LLM service URL (e.g., http://localhost:8081)
+            judge_llm_url: LLM Judge service URL for scoring
+            llm_model: Model name for LLM service (default: gpt-4)
+            judge_model: Model name for Judge LLM (default: gpt-4)
+            llm_api_key: API key for LLM service
+            judge_api_key: API key for Judge LLM service
+        """
+        self.rag_url = rag_url.rstrip("/")
+        self.llm_url = llm_url.rstrip("/")
+        self.judge_llm_url = judge_llm_url.rstrip("/")
+        self.llm_model = llm_model
+        self.judge_model = judge_model
+        self.llm_api_key = llm_api_key or os.getenv("LLM_API_KEY")
+        self.judge_api_key = judge_api_key or os.getenv("JUDGE_API_KEY")
+        self.index_name = None
+        self.test_questions: list[TestQuestion] = []
+        self.test_results: list[TestResult] = []
+
+        # Prepare headers for authenticated requests
+        self.llm_headers = self._prepare_headers(self.llm_api_key)
+        self.judge_headers = self._prepare_headers(self.judge_api_key)
+
+    def _prepare_headers(self, api_key: str | None) -> dict[str, str]:
+        """Prepare headers with API key if provided"""
+        headers = {"Content-Type": "application/json"}
+        if api_key:
+            headers["Authorization"] = f"Bearer {api_key}"
+        return headers
+
+    def is_rich_node(self, text: str) -> bool:
+        """
+        Check if node has rich content based on quality criteria
+
+        Args:
+            text: Node text content
+
+        Returns:
+            True if node meets quality standards
+        """
+        if not text or not text.strip():
+            return False
+
+        text = text.strip()
+        char_count = len(text)
+        words = text.split()
+        word_count = len(words)
+
+        # Apply quality criteria
+        if char_count < 200:
+            return False
+        if word_count < 30:
+            return False
+        return not (word_count > 0 and (char_count / word_count) >= 15)
+
+    def generate_test_questions(
+        self, num_closed: int = 10, num_open: int = 10
+    ) -> list[TestQuestion]:
+        """
+        Generate test questions from indexed documents using smart sampling strategy
+        Retrieves document nodes from RAG index via API
+
+        Args:
+            num_closed: Number of closed-ended questions
+            num_open: Number of open-ended questions
+        """
+        total_questions = num_closed + num_open
+        logger.info(
+            f"Generating {num_closed} closed and {num_open} open questions from index '{self.index_name}'"
+        )
+
+        # Step 1: Retrieve all document nodes from RAG index (with pagination)
+        logger.info("Retrieving document nodes from RAG index...")
+        all_nodes = []
+        offset = 0
+        limit = 100  # API max limit
+
+        while True:
+            response = requests.get(
+                f"{self.rag_url}/indexes/{self.index_name}/documents",
+                params={"limit": limit, "offset": offset},
+            )
+
+            if response.status_code != 200:
+                raise Exception(
+                    f"Failed to retrieve documents from index: {response.text}"
+                )
+
+            batch = response.json().get("documents", [])
+            if not batch:
+                break  # No more documents
+
+            all_nodes.extend(batch)
+            offset += len(batch)
+
+            if len(batch) < limit:
+                break  # Last page
+
+        logger.info(f"Retrieved {len(all_nodes)} nodes from index")
+
+        if len(all_nodes) == 0:
+            raise Exception(f"No documents found in index '{self.index_name}'")
+
+        # Step 2: Filter content-rich nodes
+        rich_nodes = [n for n in all_nodes if self.is_rich_node(n.get("text", ""))]
+        logger.info(
+            f"Found {len(rich_nodes)} content-rich nodes out of {len(all_nodes)} total nodes"
+        )
+
+        if len(rich_nodes) == 0:
+            raise Exception("No content-rich nodes found in index")
+
+        # Step 3: Determine node-question assignments
+        node_assignments = []  # List of (node, num_questions)
+
+        if len(rich_nodes) <= total_questions:
+            # Case A: N ≤ 20 - distribute questions across all rich nodes
+            base_questions_per_node = total_questions // len(rich_nodes)
+            remainder = total_questions % len(rich_nodes)
+
+            # Assign base questions to all nodes
+            for node in rich_nodes:
+                node_assignments.append((node, base_questions_per_node))
+
+            # Randomly distribute remainder
+            if remainder > 0:
+                random_indices = random.sample(range(len(node_assignments)), remainder)
+                for idx in random_indices:
+                    node, num_q = node_assignments[idx]
+                    node_assignments[idx] = (node, num_q + 1)
+
+            logger.info(
+                f"Case A: {len(rich_nodes)} nodes ≤ {total_questions} questions"
+            )
+            logger.info(
+                f"Base: {base_questions_per_node} q/node, Remainder: {remainder} extra questions"
+            )
+        else:
+            # Case B: N > 20 - randomly select 20 nodes
+            selected_nodes = random.sample(rich_nodes, total_questions)
+            node_assignments = [(node, 1) for node in selected_nodes]
+            logger.info(
+                f"Case B: Randomly selected {total_questions} nodes from {len(rich_nodes)} rich nodes"
+            )
+
+        # Step 4: Randomly assign question types
+        # Create list of question types [0=closed, 1=open]
+        question_types = [0] * num_closed + [1] * num_open
+        random.shuffle(question_types)
+
+        # Flatten node assignments to match with question types
+        flattened_assignments = []
+        for node, num_q in node_assignments:
+            for _ in range(num_q):
+                flattened_assignments.append(node)
+
+        # Pair each node assignment with question type
+        node_type_pairs = list(zip(flattened_assignments, question_types))
+        random.shuffle(node_type_pairs)  # Additional shuffle for randomness
+
+        logger.info(f"Created {len(node_type_pairs)} node-question-type assignments")
+
+        # Step 5: Generate questions for each node
+        question_id = 0
+        for node, q_type in tqdm(node_type_pairs, desc="Generating questions"):
+            question_type_name = "closed" if q_type == 0 else "open"
+            node_text = node.get("text", "")
+
+            # Generate single question for this node
+            prompt = f"""Based on this short text excerpt, generate exactly 1 {"closed-ended (factual, specific answer)" if q_type == 0 else "open-ended (comprehension, analysis)"} question with its answer.
+
+IMPORTANT: This is a short excerpt from a larger document. Ask about specific facts/concepts mentioned in THIS excerpt only.
+
+Text excerpt:
+{node_text[:2000]}
+
+Format your response as JSON:
+{{
+    "question": "...",
+    "answer": "..."
+}}
+"""
+
+            try:
+                response = requests.post(
+                    f"{self.judge_llm_url}/v1/chat/completions",
+                    headers=self.judge_headers,
+                    json={
+                        "model": self.judge_model,
+                        "messages": [{"role": "user", "content": prompt}],
+                        "temperature": 0,
+                        "max_tokens": 10000,
+                    },
+                    timeout=60,
+                )
+
+                if response.status_code != 200:
+                    logger.error(f"Failed to generate question: {response.text}")
+                    continue
+
+                result = response.json()
+                message = result["choices"][0]["message"]
+                content = message.get("content", "") or message.get(
+                    "reasoning_content", ""
+                )
+
+                if not content:
+                    logger.error("No content in response")
+                    continue
+
+                # Parse JSON response
+                import re
+
+                try:
+                    question_data = json.loads(content)
+                except json.JSONDecodeError:
+                    # Fallback: try to extract JSON
+                    json_match = re.search(r"\{.*\}", content, re.DOTALL)
+                    if json_match:
+                        try:
+                            question_data = json.loads(json_match.group())
+                        except json.JSONDecodeError:
+                            logger.error("Failed to parse JSON")
+                            continue
+                    else:
+                        logger.error("No JSON found")
+                        continue
+
+                # Add question
+                self.test_questions.append(
+                    TestQuestion(
+                        id=question_id,
+                        question=question_data.get("question", ""),
+                        answer=question_data.get("answer", ""),
+                        question_type=question_type_name,
+                    )
+                )
+                question_id += 1
+
+                # Small delay to avoid rate limiting
+                time.sleep(0.3)
+
+            except Exception as e:
+                logger.error(f"Error generating question: {e}")
+                continue
+
+        logger.info(f"Successfully generated {len(self.test_questions)} test questions")
+
+        # Verify we have the right distribution
+        closed_count = sum(
+            1 for q in self.test_questions if q.question_type == "closed"
+        )
+        open_count = sum(1 for q in self.test_questions if q.question_type == "open")
+        logger.info(
+            f"Final distribution: {closed_count} closed, {open_count} open questions"
+        )
+
+        return self.test_questions
+
+    def query_rag(self, question: str) -> tuple[str, int]:
+        """
+        Query RAG system with a question
+
+        Returns:
+            (answer, token_count)
+        """
+        # Query RAG system - modeled after rag_solution.py success
+        response = requests.post(
+            f"{self.rag_url}/v1/chat/completions",
+            headers={"Content-Type": "application/json"},
+            json={
+                "model": self.llm_model,
+                "messages": [
+                    {
+                        "role": "system",
+                        "content": "You are a helpful assistant. Answer based on the provided document.",
+                    },
+                    {"role": "user", "content": question},
+                ],
+                "index_name": self.index_name,
+                "context_token_ratio": 0.7,
+                "temperature": 0,
+                "max_tokens": 10000,
+            },
+            timeout=60,
+        )
+
+        if response.status_code != 200:
+            raise Exception(f"RAG query failed: {response.text}")
+
+        result = response.json()
+        message = result["choices"][0]["message"]
+        answer = message.get("content", "") or message.get("reasoning_content", "")
+
+        if not answer:
+            raise Exception(f"No content in RAG response: {message}")
+
+        # Extract real token usage from RAG API response
+        usage = result.get("usage", {})
+        tokens = usage.get("total_tokens", 0)
+
+        if tokens == 0:
+            # Fallback to estimation if API doesn't return usage
+            logger.warning("No token usage in RAG response, using estimation")
+            tokens = int(len(answer.split()) * 1.3 + len(question.split()) * 1.3)
+
+        return answer, tokens
+
+    def query_llm(self, question: str) -> tuple[str, int]:
+        """
+        Query pure LLM with a question
+
+        Returns:
+            (answer, token_count)
+        """
+        response = requests.post(
+            f"{self.llm_url}/v1/chat/completions",
+            headers=self.llm_headers,
+            json={
+                "model": self.llm_model,
+                "messages": [{"role": "user", "content": question}],
+                "temperature": 0,
+                "max_tokens": 10000,
+            },
+            timeout=3000,  # Very long timeout for LLM with reasoning
+        )
+
+        if response.status_code != 200:
+            raise Exception(f"LLM query failed: {response.text}")
+
+        result = response.json()
+        message = result["choices"][0]["message"]
+        answer = message.get("content", "") or message.get("reasoning_content", "")
+
+        if not answer:
+            raise Exception(f"No content in LLM response: {message}")
+
+        # Extract real token usage from LLM API response
+        usage = result.get("usage", {})
+        tokens = usage.get("total_tokens", 0)
+
+        if tokens == 0:
+            # Fallback to estimation if API doesn't return usage
+            logger.warning("No token usage in LLM response, using estimation")
+            tokens = int(len(answer.split()) * 1.3 + len(question.split()) * 1.3)
+
+        return answer, tokens
+
+    def score_answer(
+        self, question: str, ground_truth: str, answer: str, question_type: str
+    ) -> float:
+        """
+        Use LLM Judge to score an answer against ground truth
+
+        Args:
+            question: The test question
+            ground_truth: Expected answer
+            answer: Provided answer
+            question_type: "closed" or "open"
+
+        Returns:
+            Score from 0-10
+        """
+        if question_type == "closed":
+            # Strict binary-like scoring for closed-ended questions
+            prompt = f"""Score this factual answer using STRICT criteria:
+            - Give 10 points if the answer is COMPLETELY CORRECT with all key facts matching
+            - Give 5 points if the answer is PARTIALLY CORRECT (has some correct facts but missing important details)
+            - Give 0 points if the answer is WRONG or completely misses the point
+            
+            Question: {question}
+            Ground Truth Answer: {ground_truth}
+            Provided Answer: {answer}
+            
+            This is a CLOSED-ENDED factual question. Be strict - if the core fact is wrong, give 0.
+            Respond with only a number: 0, 5, or 10."""
+        else:
+            # Gradient scoring for open-ended questions
+            prompt = f"""Score this analytical answer on a scale of 0-10 considering:
+            - Accuracy (3 points): Are the facts and concepts correct?
+            - Completeness (3 points): Does it cover the main points from the ground truth?
+            - Understanding (2 points): Does it show comprehension of the topic?
+            - Relevance (2 points): Does it directly address the question?
+            
+            Question: {question}
+            Ground Truth Answer: {ground_truth}
+            Provided Answer: {answer}
+            
+            This is an OPEN-ENDED analytical question. Provide a nuanced score from 0-10.
+            Respond with only a number from 0 to 10."""
+
+        response = requests.post(
+            f"{self.judge_llm_url}/v1/chat/completions",
+            headers=self.judge_headers,
+            json={
+                "model": self.judge_model,
+                "messages": [{"role": "user", "content": prompt}],
+                "temperature": 0,
+                "max_tokens": 10000,  # Allow full reasoning output
+            },
+            timeout=180,
+        )
+
+        if response.status_code != 200:
+            logger.error(f"Scoring failed: {response.text}")
+            return 0.0
+
+        result = response.json()
+        message = result["choices"][0]["message"]
+        score_text = (
+            message.get("content", "") or message.get("reasoning_content", "")
+        ).strip()
+
+        if not score_text:
+            logger.error(f"No content in Judge LLM scoring response: {message}")
+            return 0.0
+
+        # Extract numeric score from response (handle reasoning models that output verbose text)
+        import re
+
+        # Strategy 1: Look for explicit score patterns like "Score: 8" or "Final score: 10"
+        score_patterns = [
+            r"(?:final\s*)?score\s*[:=]\s*(\d+(?:\.\d+)?)",  # "Score: 8" or "Final score: 10"
+            r"(\d+(?:\.\d+)?)\s*(?:out of|/)\s*10",  # "8 out of 10" or "8/10"
+            r"(?:rating|score).*?(\d+(?:\.\d+)?)",  # "rating is 8" or "score is 8"
+        ]
+
+        for pattern in score_patterns:
+            matches = re.findall(pattern, score_text, re.IGNORECASE)
+            if matches:
+                try:
+                    score = float(matches[-1])  # Use the last match
+                    logger.info(f"Extracted score {score} from pattern: {pattern}")
+                    # For closed questions, enforce 0/5/10 scoring
+                    if question_type == "closed":
+                        if score >= 8:
+                            return 10.0
+                        elif score >= 3:
+                            return 5.0
+                        else:
+                            return 0.0
+                    # For open questions, clamp to 0-10 range
+                    return min(max(score, 0.0), 10.0)
+                except (ValueError, IndexError):
+                    continue  # Try next pattern
+
+        # Strategy 2: Fallback - extract all numbers and use the last one in valid range
+        all_numbers = re.findall(r"\b(\d+(?:\.\d+)?)\b", score_text)
+        for num in reversed(all_numbers):  # Check from end to start
+            try:
+                score = float(num)
+                if 0 <= score <= 10:  # Must be valid score range
+                    logger.info(
+                        f"Extracted score {score} from fallback (last valid number)"
+                    )
+                    if question_type == "closed":
+                        if score >= 8:
+                            return 10.0
+                        elif score >= 3:
+                            return 5.0
+                        else:
+                            return 0.0
+                    return min(max(score, 0.0), 10.0)
+            except ValueError:
+                continue
+
+        logger.error(f"Could not extract valid score from: {score_text[:300]}")
+        return 0.0
+
+    def run_benchmark(self) -> BenchmarkReport:
+        """Run the full benchmark test"""
+        logger.info("Starting benchmark tests")
+
+        for question in tqdm(self.test_questions, desc="Testing questions"):
+            # Query both systems
+            rag_answer, rag_tokens = self.query_rag(question.question)
+            llm_answer, llm_tokens = self.query_llm(question.question)
+
+            # Score both answers with question type awareness
+            rag_score = self.score_answer(
+                question.question, question.answer, rag_answer, question.question_type
+            )
+            llm_score = self.score_answer(
+                question.question, question.answer, llm_answer, question.question_type
+            )
+
+            # Store results
+            result = TestResult(
+                question_id=question.id,
+                rag_answer=rag_answer,
+                llm_answer=llm_answer,
+                rag_score=rag_score,
+                llm_score=llm_score,
+                rag_tokens=rag_tokens,
+                llm_tokens=llm_tokens,
+            )
+            self.test_results.append(result)
+
+            # Small delay to avoid rate limiting
+            time.sleep(0.5)
+
+        return self.generate_report()
+
+    def generate_report(self) -> BenchmarkReport:
+        """Generate final benchmark report"""
+        # Separate closed and open questions
+        closed_results = [
+            r
+            for r in self.test_results
+            if self.test_questions[r.question_id].question_type == "closed"
+        ]
+        open_results = [
+            r
+            for r in self.test_results
+            if self.test_questions[r.question_id].question_type == "open"
+        ]
+
+        # Calculate averages
+        rag_avg = sum(r.rag_score for r in self.test_results) / len(self.test_results)
+        llm_avg = sum(r.llm_score for r in self.test_results) / len(self.test_results)
+
+        rag_closed_avg = (
+            sum(r.rag_score for r in closed_results) / len(closed_results)
+            if closed_results
+            else 0
+        )
+        llm_closed_avg = (
+            sum(r.llm_score for r in closed_results) / len(closed_results)
+            if closed_results
+            else 0
+        )
+
+        rag_open_avg = (
+            sum(r.rag_score for r in open_results) / len(open_results)
+            if open_results
+            else 0
+        )
+        llm_open_avg = (
+            sum(r.llm_score for r in open_results) / len(open_results)
+            if open_results
+            else 0
+        )
+
+        # Token usage
+        rag_tokens = sum(r.rag_tokens for r in self.test_results)
+        llm_tokens = sum(r.llm_tokens for r in self.test_results)
+
+        # Performance improvement
+        improvement = ((rag_avg - llm_avg) / llm_avg * 100) if llm_avg > 0 else 0
+        token_efficiency = (
+            ((llm_tokens - rag_tokens) / llm_tokens * 100) if llm_tokens > 0 else 0
+        )
+
+        report = BenchmarkReport(
+            total_questions=len(self.test_questions),
+            rag_avg_score=rag_avg,
+            llm_avg_score=llm_avg,
+            rag_closed_avg=rag_closed_avg,
+            llm_closed_avg=llm_closed_avg,
+            rag_open_avg=rag_open_avg,
+            llm_open_avg=llm_open_avg,
+            performance_improvement=improvement,
+            rag_total_tokens=rag_tokens,
+            llm_total_tokens=llm_tokens,
+            token_efficiency=token_efficiency,
+        )
+
+        return report
+
+    def save_results(self, output_dir: str = "benchmark_results"):
+        """Save all results to files"""
+        Path(output_dir).mkdir(exist_ok=True)
+        timestamp = time.strftime("%Y%m%d_%H%M%S")
+
+        # Save test questions
+        with open(f"{output_dir}/questions_{timestamp}.json", "w") as f:
+            json.dump([asdict(q) for q in self.test_questions], f, indent=2)
+
+        # Save test results
+        with open(f"{output_dir}/results_{timestamp}.json", "w") as f:
+            json.dump([asdict(r) for r in self.test_results], f, indent=2)
+
+        # Save report
+        report = self.generate_report()
+        with open(f"{output_dir}/report_{timestamp}.json", "w") as f:
+            json.dump(asdict(report), f, indent=2)
+
+        # Save human-readable report
+        with open(f"{output_dir}/report_{timestamp}.txt", "w") as f:
+            f.write("RAG vs LLM Benchmark Report\n")
+            f.write("=" * 50 + "\n\n")
+            f.write(f"Total Questions: {report.total_questions}\n\n")
+            f.write("Average Scores (0-10):\n")
+            f.write(f"  RAG Overall: {report.rag_avg_score:.2f}\n")
+            f.write(f"  LLM Overall: {report.llm_avg_score:.2f}\n")
+            f.write(
+                f"  Performance Improvement: {report.performance_improvement:.1f}%\n\n"
+            )
+            f.write("Closed Questions (0/5/10 scoring - factual accuracy):\n")
+            f.write(f"  RAG: {report.rag_closed_avg:.2f}\n")
+            f.write(f"  LLM: {report.llm_closed_avg:.2f}\n\n")
+            f.write("Open Questions (0-10 scoring - comprehensive evaluation):\n")
+            f.write(f"  RAG: {report.rag_open_avg:.2f}\n")
+            f.write(f"  LLM: {report.llm_open_avg:.2f}\n\n")
+            f.write("Token Usage:\n")
+            f.write(f"  RAG: {report.rag_total_tokens:,} tokens\n")
+            f.write(f"  LLM: {report.llm_total_tokens:,} tokens\n")
+            f.write(
+                f"  Efficiency: {report.token_efficiency:.1f}% {'fewer' if report.token_efficiency > 0 else 'more'} tokens with RAG\n"
+            )
+
+        logger.info(f"Results saved to {output_dir}")
+
+
+def main():
+    """Main execution function"""
+    parser = argparse.ArgumentParser(description="RAG vs LLM Benchmark Testing")
+    parser.add_argument(
+        "--index-name", required=True, help="Existing RAG index name (required)"
+    )
+    parser.add_argument(
+        "--rag-url", default="http://localhost:5000", help="RAG service URL"
+    )
+    parser.add_argument(
+        "--llm-url", default="http://localhost:8081", help="LLM service URL"
+    )
+    parser.add_argument(
+        "--judge-url", default="http://localhost:8082", help="Judge LLM service URL"
+    )
+    parser.add_argument(
+        "--llm-model",
+        default="gpt-4",
+        help="Model name for LLM/RAG service (default: gpt-4)",
+    )
+    parser.add_argument(
+        "--judge-model",
+        default="gpt-4",
+        help="Model name for Judge LLM (default: gpt-4)",
+    )
+    parser.add_argument(
+        "--llm-api-key", help="API key for LLM service (or set LLM_API_KEY env var)"
+    )
+    parser.add_argument(
+        "--judge-api-key", help="API key for Judge LLM (or set JUDGE_API_KEY env var)"
+    )
+    parser.add_argument(
+        "--closed-questions", type=int, default=10, help="Number of closed questions"
+    )
+    parser.add_argument(
+        "--open-questions", type=int, default=10, help="Number of open questions"
+    )
+    parser.add_argument(
+        "--output-dir", default="benchmark_results", help="Output directory"
+    )
+
+    args = parser.parse_args()
+
+    # Initialize benchmark with API keys and models
+    benchmark = RAGBenchmark(
+        args.rag_url,
+        args.llm_url,
+        args.judge_url,
+        args.llm_model,
+        args.judge_model,
+        args.llm_api_key,
+        args.judge_api_key,
+    )
+
+    try:
+        # Phase 1: Use existing index
+        logger.info(f"Using existing index: {args.index_name}")
+        benchmark.index_name = args.index_name
+
+        # Phase 2: Generate test questions (fetch nodes from RAG index via API)
+        benchmark.generate_test_questions(args.closed_questions, args.open_questions)
+
+        # Phase 3: Run benchmark
+        report = benchmark.run_benchmark()
+
+        # Phase 4: Save results
+        benchmark.save_results(args.output_dir)
+
+        # Print summary
+        print("\n" + "=" * 50)
+        print("BENCHMARK COMPLETE")
+        print("=" * 50)
+        print(f"RAG Average Score: {report.rag_avg_score:.2f}/10")
+        print(f"LLM Average Score: {report.llm_avg_score:.2f}/10")
+        print(f"Performance Improvement: {report.performance_improvement:.1f}%")
+        print(f"Results saved to: {args.output_dir}")
+
+        return 0
+
+    except Exception as e:
+        logger.error(f"Benchmark failed: {e}")
+        return 1
+
+
+if __name__ == "__main__":
+    exit(main())
diff --git a/website/docs/example_rag_client.py b/website/docs/example_rag_client.py
index e2a5760..8584913 100644
--- a/website/docs/example_rag_client.py
+++ b/website/docs/example_rag_client.py
@@ -41,27 +41,6 @@ class KAITORAGClient:
         resp.raise_for_status()
         return resp.json()
 
-    def query(self, index_name, query, llm_temperature, llm_max_tokens, top_k=5):
-        """
-        Query the RAGEngine.
-        query: str, the query text
-        top_k: int, number of results to return
-        metadata: optional dict, additional metadata for the query
-        """
-        url = f"{self.base_url}/query"
-        payload = {
-            "index_name": index_name,
-            "query": query,
-            "top_k": top_k,
-            "llm_params": {
-                "temperature": llm_temperature,
-                "max_tokens": llm_max_tokens,
-            },
-        }
-        resp = requests.post(url, json=payload, headers=self.headers)
-        resp.raise_for_status()
-        return resp.json()
-
     def update_documents(self, index_name, documents):
         """
         Update a document in the RAGEngine.
diff --git a/website/docs/rag-api.md b/website/docs/rag-api.md
index 41cbdd3..6019559 100644
--- a/website/docs/rag-api.md
+++ b/website/docs/rag-api.md
@@ -281,60 +281,6 @@ DELETE /indexes/rag_index
 
 Use this endpoint to permanently remove an index and all its data when it is no longer needed.
 
-## Query Index
-
-To query a specific index for relevant documents, use the `/query` API route. This endpoint accepts a POST request with the index name, query string, and optional parameters for result count, and LLM generation.
-
-### Query Index Request
-
-```json
-POST /query
-{
-  "index_name": "rag_index",
-  "query": "What is RAG?",
-  "top_k": 5,
-  "llm_params": {
-    "temperature": 0.7,
-    "max_tokens": 2048
-  }
-}
-```
-
-- `index_name`: The name of the index to query.
-- `query`: The query string.
-- `top_k`: (optional) Number of top documents to retrieve (default: 5).
-- `llm_params`: (optional) Parameters for LLM-based generation (e.g., temperature, max_tokens).
-
-### Query Index Response
-
-```json
-{
-  "response": "Retrieval Augmented Generation (RAG) is an architecture that augments the capabilities of a Large Language Model...",
-  "source_nodes": [
-    {
-      "doc_id": "123456",
-      "node_id": "2853a565-8c1f-4982-acaa-a0ab52691435",
-      "text": "Retrieval Augmented Generation (RAG) is an architecture that augments the capabilities of a Large Language Model...",
-      "score": 0.95,
-      "metadata": {
-        "author": "kaito",
-      }
-    }
-  ],
-  "metadata": {
-    "2853a565-8c1f-4982-acaa-a0ab52691435": {
-      "author": "kaito",
-    }
-  }
-}
-```
-
-- `response`: The generated answer or summary from the LLM (if enabled).
-- `source_nodes`: List of source nodes with their text, score, and metadata.
-- `metadata`: Additional metadata about the query or response.
-
-Use this endpoint to retrieve relevant information from your indexed documents and optionally generate answers using an LLM.
-
 ## OpenAI-Compatible Chat Completions
 
 The RAGEngine provides an OpenAI-compatible chat completions endpoint at `/v1/chat/completions`. This endpoint allows you to use RAG capabilities with the familiar OpenAI API format, making it easy to integrate with existing applications that use OpenAI's chat completions.



Test patch:
diff --git a/pkg/ragengine/controllers/preset_rag_test.go b/pkg/ragengine/controllers/preset_rag_test.go
index d24453b..8963fa7 100644
--- a/pkg/ragengine/controllers/preset_rag_test.go
+++ b/pkg/ragengine/controllers/preset_rag_test.go
@@ -120,8 +120,8 @@ func TestGPUConfigLogic(t *testing.T) {
 				c.On("Create", mock.IsType(context.TODO()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
 			},
 			ragEngine:        test.MockRAGEngineWithPresetPreferredCPUNodes,
-			expectedGPUReq:   int64(0),
-			expectedLimitReq: int64(0),
+			expectedGPUReq:   int64(1), // Standard_NC24ads_A100_v4 has 1 GPU
+			expectedLimitReq: int64(1), // Standard_NC24ads_A100_v4 has 1 GPU
 		},
 	}
 
diff --git a/pkg/ragengine/controllers/ragengine_controller_test.go b/pkg/ragengine/controllers/ragengine_controller_test.go
index 11c0fb1..ac3a932 100644
--- a/pkg/ragengine/controllers/ragengine_controller_test.go
+++ b/pkg/ragengine/controllers/ragengine_controller_test.go
@@ -48,6 +48,7 @@ import (
 
 func TestApplyRAGEngineResource(t *testing.T) {
 	test.RegisterTestModel()
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
 	testcases := map[string]struct {
 		callMocks     func(c *test.MockClient)
 		expectedError error
@@ -700,6 +701,7 @@ func TestEnsureService(t *testing.T) {
 
 func TestReconcile(t *testing.T) {
 	test.RegisterTestModel()
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
 	mockRAGEngineDistributedModel0Node := test.MockRAGEngine
 	mockRAGEngineDistributedModel0Node.Spec.InferenceService = &v1alpha1.InferenceServiceSpec{
 		URL: "http://example.com/inference",
diff --git a/pkg/utils/common_test.go b/pkg/utils/common_test.go
index f8ba985..b75e170 100644
--- a/pkg/utils/common_test.go
+++ b/pkg/utils/common_test.go
@@ -14,7 +14,6 @@
 package utils
 
 import (
-	"context"
 	"strings"
 	"testing"
 
@@ -22,121 +21,12 @@ import (
 	"github.com/stretchr/testify/require"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/client-go/kubernetes/scheme"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
 	"github.com/kaito-project/kaito/pkg/sku"
 )
 
-func TestFetchGPUCountFromNodes(t *testing.T) {
-	node1 := &corev1.Node{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "node-1",
-		},
-		Status: corev1.NodeStatus{
-			Capacity: corev1.ResourceList{
-				"nvidia.com/gpu": resource.MustParse("2"),
-			},
-		},
-	}
-
-	node2 := &corev1.Node{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "node-2",
-		},
-		Status: corev1.NodeStatus{
-			Capacity: corev1.ResourceList{
-				"nvidia.com/gpu": resource.MustParse("4"),
-			},
-		},
-	}
-
-	node3 := &corev1.Node{
-		ObjectMeta: metav1.ObjectMeta{
-			Name: "node-3",
-		},
-		Status: corev1.NodeStatus{
-			Capacity: corev1.ResourceList{},
-		},
-	}
-
-	tests := []struct {
-		name        string
-		nodeNames   []string
-		nodes       []runtime.Object
-		expectedGPU int
-		expectErr   bool
-		expectedErr string
-	}{
-		{
-			name:        "Single Node with GPU",
-			nodeNames:   []string{"node-1"},
-			nodes:       []runtime.Object{node1},
-			expectedGPU: 2,
-		},
-		{
-			name:        "Multiple Nodes with GPU",
-			nodeNames:   []string{"node-1", "node-2"},
-			nodes:       []runtime.Object{node1, node2},
-			expectedGPU: 2,
-		},
-		{
-			name:        "Node without GPU",
-			nodeNames:   []string{"node-3"},
-			nodes:       []runtime.Object{node3},
-			expectedGPU: 0,
-		},
-		{
-			name:        "No Worker Nodes",
-			nodeNames:   []string{},
-			nodes:       []runtime.Object{},
-			expectedGPU: 0,
-			expectErr:   true,
-			expectedErr: "no worker nodes found in the workspace",
-		},
-		{
-			name:        "Node not found",
-			nodeNames:   []string{"non-existent-node"},
-			nodes:       []runtime.Object{node1, node2},
-			expectedGPU: 0,
-			expectErr:   true,
-			expectedErr: "failed to get node non-existent-node: nodes \"non-existent-node\" not found",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			// Set up the fake client with the indexer
-			s := scheme.Scheme
-			s.AddKnownTypes(corev1.SchemeGroupVersion, &corev1.Node{}, &corev1.NodeList{})
-
-			// Build the fake client with the indexer
-			kubeClient := fake.NewClientBuilder().
-				WithScheme(s).
-				WithRuntimeObjects(tt.nodes...).
-				Build()
-
-			// Call the function
-			gpuCount, err := FetchGPUCountFromNodes(context.TODO(), kubeClient, tt.nodeNames)
-
-			// Check the error
-			if tt.expectErr {
-				require.Error(t, err)
-				assert.Equal(t, tt.expectedErr, err.Error())
-			} else {
-				require.NoError(t, err)
-			}
-
-			// Check the GPU count
-			assert.Equal(t, tt.expectedGPU, gpuCount)
-		})
-	}
-}
-
 func TestParseHuggingFaceModelVersion(t *testing.T) {
 	tests := []struct {
 		name             string
@@ -625,7 +515,7 @@ func TestGetGPUConfigFromNvidiaLabels(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := GetGPUConfigFromNvidiaLabels(tt.node)
+			got, err := GetGPUConfigFromNodeLabels(tt.node)
 			if tt.wantErr {
 				assert.Error(t, err)
 				assert.Nil(t, got)
diff --git a/pkg/utils/nodeclaim/nodeclaim_test.go b/pkg/utils/nodeclaim/nodeclaim_test.go
index 6201810..d357a77 100644
--- a/pkg/utils/nodeclaim/nodeclaim_test.go
+++ b/pkg/utils/nodeclaim/nodeclaim_test.go
@@ -31,7 +31,6 @@ import (
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
-	"github.com/kaito-project/kaito/pkg/featuregates"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
@@ -309,328 +308,3 @@ func TestCreateKarpenterNodeClass(t *testing.T) {
 		assert.Error(t, err, "create failed")
 	})
 }
-
-func TestResolveReadyNodesAndTargetNodeClaimCount(t *testing.T) {
-	testcases := []struct {
-		name                      string
-		workspace                 *kaitov1beta1.Workspace
-		mockBYONodes              []*corev1.Node
-		mockReadyNodes            []string
-		mockError                 error
-		featureGateDisabled       bool
-		expectedReadyNodes        []string
-		expectedRequiredNodeCount int
-		expectedError             string
-	}{
-		{
-			name: "successful_with_inference_status",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 3,
-				},
-			},
-			mockBYONodes:              []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "byo-node"}}},
-			mockReadyNodes:            []string{"node1", "node2", "byo-node"},
-			mockError:                 nil,
-			featureGateDisabled:       false,
-			expectedReadyNodes:        []string{"node1", "node2", "byo-node"},
-			expectedRequiredNodeCount: 2, // target=3, BYO=1, required=2
-			expectedError:             "",
-		},
-		{
-			name: "byo_nodes_exceed_target_count",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 2,
-				},
-			},
-			mockBYONodes: []*corev1.Node{
-				{ObjectMeta: metav1.ObjectMeta{Name: "byo-node-1"}},
-				{ObjectMeta: metav1.ObjectMeta{Name: "byo-node-2"}},
-				{ObjectMeta: metav1.ObjectMeta{Name: "byo-node-3"}},
-			},
-			mockReadyNodes:            []string{"byo-node-1", "byo-node-2", "byo-node-3"},
-			mockError:                 nil,
-			featureGateDisabled:       false,
-			expectedReadyNodes:        []string{"byo-node-1", "byo-node-2", "byo-node-3"},
-			expectedRequiredNodeCount: 0, // target=2, BYO=3, max(0, 2-3)=0
-			expectedError:             "",
-		},
-		{
-			name: "feature_gate_disabled_auto_provisioning",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 5,
-				},
-			},
-			mockBYONodes:              []*corev1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "byo-node"}}},
-			mockReadyNodes:            []string{"node1", "byo-node"},
-			mockError:                 nil,
-			featureGateDisabled:       true,
-			expectedReadyNodes:        []string{"node1", "byo-node"},
-			expectedRequiredNodeCount: 0, // feature gate disabled, always 0
-			expectedError:             "",
-		},
-		{
-			name: "get_byo_and_ready_nodes_error",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-			},
-			mockBYONodes:              nil,
-			mockReadyNodes:            nil,
-			mockError:                 errors.New("failed to list nodes"),
-			featureGateDisabled:       false,
-			expectedReadyNodes:        nil,
-			expectedRequiredNodeCount: 0,
-			expectedError:             "failed to get available BYO nodes: failed to list nodes",
-		},
-	}
-
-	for _, tc := range testcases {
-		t.Run(tc.name, func(t *testing.T) {
-			// Set up feature gate
-			originalFeatureGate := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
-			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = tc.featureGateDisabled
-			defer func() {
-				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalFeatureGate
-			}()
-
-			mockClient := test.NewClient()
-
-			// Mock the actual client.List call that resources.GetBYOAndReadyNodes makes
-			if tc.mockError != nil {
-				mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Return(tc.mockError)
-			} else {
-				// Create mock nodes that will simulate the behavior
-				var nodeItems []corev1.Node
-
-				// Add ready nodes to simulate what GetBYOAndReadyNodes finds
-				for _, nodeName := range tc.mockReadyNodes {
-					node := corev1.Node{
-						ObjectMeta: metav1.ObjectMeta{
-							Name: nodeName,
-							Labels: map[string]string{
-								"workload": "gpu", // Match the LabelSelector
-							},
-						},
-						Status: corev1.NodeStatus{
-							Conditions: []corev1.NodeCondition{
-								{
-									Type:   corev1.NodeReady,
-									Status: corev1.ConditionTrue,
-								},
-							},
-						},
-					}
-					nodeItems = append(nodeItems, node)
-				}
-
-				// Add BYO nodes to preferred nodes list if any
-				if len(tc.mockBYONodes) > 0 {
-					preferredNodes := make([]string, len(tc.mockBYONodes))
-					for i, byoNode := range tc.mockBYONodes {
-						preferredNodes[i] = byoNode.Name
-					}
-					//nolint:staticcheck // SA1019
-					tc.workspace.Resource.PreferredNodes = preferredNodes
-				}
-
-				nodeList := &corev1.NodeList{Items: nodeItems}
-				mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-			}
-
-			readyNodes, requiredNodeClaimCount, err := ResolveReadyNodesAndTargetNodeClaimCount(context.Background(), mockClient, tc.workspace)
-
-			if tc.expectedError != "" {
-				assert.Check(t, err != nil, "Expected an error")
-				assert.Check(t, err.Error() == tc.expectedError, "Expected error message: %s, got: %s", tc.expectedError, err.Error())
-				return
-			}
-
-			assert.Check(t, err == nil, "Expected no error, got: %v", err)
-			assert.DeepEqual(t, readyNodes, tc.expectedReadyNodes)
-			assert.Equal(t, requiredNodeClaimCount, tc.expectedRequiredNodeCount, "Expected required node count: %d, got: %d", tc.expectedRequiredNodeCount, requiredNodeClaimCount)
-
-			mockClient.AssertExpectations(t)
-		})
-	}
-}
-
-func TestResolveReadyNodesAndTargetNodeClaimCount_PreferredNodes(t *testing.T) {
-	testcases := map[string]struct {
-		name                      string
-		workspace                 *kaitov1beta1.Workspace
-		mockBYONodes              []*corev1.Node
-		mockReadyNodes            []string
-		expectedReadyNodes        []string
-		expectedRequiredNodeCount int
-		expectedError             string
-	}{
-		"preferred_node_exists_should_not_create_nodeclaim": {
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-					PreferredNodes: []string{"preferred-node-1"},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 1, // Need 1 node total
-				},
-			},
-			mockBYONodes: []*corev1.Node{{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "preferred-node-1",
-					Labels: map[string]string{
-						"workload": "gpu", // Must match LabelSelector
-					},
-				},
-				Status: corev1.NodeStatus{
-					Conditions: []corev1.NodeCondition{{
-						Type:   corev1.NodeReady,
-						Status: corev1.ConditionTrue,
-					}},
-				},
-			}}, // 1 preferred node available with matching labels
-			mockReadyNodes:            []string{"preferred-node-1"},
-			expectedReadyNodes:        []string{"preferred-node-1"},
-			expectedRequiredNodeCount: 0, // target=1, BYO=1, max(0, 1-1)=0 - NO NODECLAIM NEEDED
-			expectedError:             "",
-		},
-		"preferred_node_not_found_should_create_nodeclaim": {
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-					PreferredNodes: []string{"preferred-node-1"},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 1, // Need 1 node total
-				},
-			},
-			mockBYONodes:              []*corev1.Node{}, // No preferred node available
-			mockReadyNodes:            []string{},
-			expectedReadyNodes:        []string{},
-			expectedRequiredNodeCount: 1, // target=1, BYO=0, max(0, 1-0)=1 - NODECLAIM NEEDED
-			expectedError:             "",
-		},
-		"multiple_preferred_nodes_with_partial_availability": {
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType: "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{
-						MatchLabels: map[string]string{
-							"workload": "gpu",
-						},
-					},
-					PreferredNodes: []string{"preferred-node-1", "preferred-node-2", "preferred-node-3"},
-				},
-				Inference: &kaitov1beta1.InferenceSpec{},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 3, // Need 3 nodes total
-				},
-			},
-			mockBYONodes: []*corev1.Node{{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "preferred-node-1",
-					Labels: map[string]string{
-						"workload": "gpu", // Must match LabelSelector
-					},
-				},
-				Status: corev1.NodeStatus{
-					Conditions: []corev1.NodeCondition{{
-						Type:   corev1.NodeReady,
-						Status: corev1.ConditionTrue,
-					}},
-				},
-			}}, // Only 1 of 3 preferred nodes available with matching labels
-			mockReadyNodes:            []string{"preferred-node-1"},
-			expectedReadyNodes:        []string{"preferred-node-1"},
-			expectedRequiredNodeCount: 2, // target=3, BYO=1, max(0, 3-1)=2 - 2 NODECLAIMS NEEDED
-			expectedError:             "",
-		},
-	}
-
-	for testName, tc := range testcases {
-		t.Run(testName, func(t *testing.T) {
-			mockClient := test.NewClient()
-
-			// Mock the List call for nodes
-			mockClient.On("List", mock.IsType(context.Background()), mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-				nodeList := args.Get(1).(*corev1.NodeList)
-				nodeList.Items = make([]corev1.Node, len(tc.mockBYONodes))
-				for i, node := range tc.mockBYONodes {
-					nodeList.Items[i] = *node
-				}
-			}).Return(nil)
-
-			readyNodes, requiredNodeClaimCount, err := ResolveReadyNodesAndTargetNodeClaimCount(context.Background(), mockClient, tc.workspace)
-
-			if tc.expectedError != "" {
-				assert.Check(t, err != nil, "Expected an error")
-				assert.Check(t, err.Error() == tc.expectedError, "Expected error message: %s, got: %s", tc.expectedError, err.Error())
-				return
-			}
-
-			assert.Check(t, err == nil, "Expected no error, got: %v", err)
-			assert.DeepEqual(t, readyNodes, tc.expectedReadyNodes)
-			assert.Equal(t, requiredNodeClaimCount, tc.expectedRequiredNodeCount, "Expected required node count: %d, got: %d", tc.expectedRequiredNodeCount, requiredNodeClaimCount)
-
-			mockClient.AssertExpectations(t)
-		})
-	}
-}
diff --git a/pkg/utils/resources/nodes_test.go b/pkg/utils/resources/nodes_test.go
index d19e649..e1d7303 100644
--- a/pkg/utils/resources/nodes_test.go
+++ b/pkg/utils/resources/nodes_test.go
@@ -362,14 +362,13 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 		expectedReadyNodeNames        []string
 	}{
 		{
-			name: "successful_retrieval_with_preferred_nodes",
+			name: "successful_retrieval_all_matching_nodes",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"ready-node-1", "ready-node-2"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -381,25 +380,24 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 				Items: []corev1.Node{
 					createMockNode("ready-node-1", true, false, map[string]string{"workload": "gpu"}),
 					createMockNode("ready-node-2", true, false, map[string]string{"workload": "gpu"}),
-					createMockNode("not-preferred-node", true, false, map[string]string{"workload": "gpu"}),
+					createMockNode("ready-node-3", true, false, map[string]string{"workload": "gpu"}),
 				},
 			},
 			disableNodeAutoProvisioning:   false,
-			expectedAvailableBYONodes:     2,
+			expectedAvailableBYONodes:     3,
 			expectedReadyNodes:            3,
 			expectedError:                 "",
-			expectedAvailableBYONodeNames: []string{"ready-node-1", "ready-node-2"},
-			expectedReadyNodeNames:        []string{"ready-node-1", "ready-node-2", "not-preferred-node"},
+			expectedAvailableBYONodeNames: []string{"ready-node-1", "ready-node-2", "ready-node-3"},
+			expectedReadyNodeNames:        []string{"ready-node-1", "ready-node-2", "ready-node-3"},
 		},
 		{
-			name: "disable_node_auto_provisioning_ignores_preferred_nodes",
+			name: "nap_disabled_includes_all_matching_nodes",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"ready-node-1"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -428,7 +426,6 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"not-ready-node", "ready-node"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -457,7 +454,6 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"deleting-node", "ready-node"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -486,7 +482,6 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"node-1"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -525,14 +520,13 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 			expectedError:             "failed to list nodes",
 		},
 		{
-			name: "no_preferred_nodes_with_auto_provisioning_enabled",
+			name: "nap_enabled_includes_all_matching_nodes",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{}, // Empty preferred nodes
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -547,21 +541,20 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 				},
 			},
 			disableNodeAutoProvisioning:   false,
-			expectedAvailableBYONodes:     0, // No nodes added to BYO because they're not in preferred list
-			expectedReadyNodes:            2, // But they're counted as ready
+			expectedAvailableBYONodes:     2, // All matching nodes are included
+			expectedReadyNodes:            2,
 			expectedError:                 "",
-			expectedAvailableBYONodeNames: []string{},
+			expectedAvailableBYONodeNames: []string{"ready-node-1", "ready-node-2"},
 			expectedReadyNodeNames:        []string{"ready-node-1", "ready-node-2"},
 		},
 		{
-			name: "mixed_ready_and_not_ready_preferred_nodes",
+			name: "mixed_ready_not_ready_and_deleting_nodes",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
 					Namespace: "default",
 				},
 				Resource: kaitov1beta1.ResourceSpec{
-					PreferredNodes: []string{"ready-node", "not-ready-node", "deleting-node"},
 					LabelSelector: &metav1.LabelSelector{
 						MatchLabels: map[string]string{
 							"workload": "gpu",
@@ -605,7 +598,8 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 				}).Return(nil)
 			}
 
-			availableBYONodes, readyNodes, err := GetBYOAndReadyNodes(context.Background(), mockClient, tc.workspace)
+			readyNodes, err := GetReadyNodes(context.Background(), mockClient, tc.workspace)
+			availableBYONodes := readyNodes
 
 			if tc.expectedError != "" {
 				assert.Check(t, err != nil, "Expected an error")
@@ -624,7 +618,11 @@ func TestGetBYOAndReadyNodes(t *testing.T) {
 			}
 
 			assert.DeepEqual(t, actualBYONodeNames, tc.expectedAvailableBYONodeNames)
-			assert.DeepEqual(t, readyNodes, tc.expectedReadyNodeNames)
+			actualReadyNodeNames := make([]string, len(readyNodes))
+			for i, node := range readyNodes {
+				actualReadyNodeNames[i] = node.Name
+			}
+			assert.DeepEqual(t, actualReadyNodeNames, tc.expectedReadyNodeNames)
 
 			mockClient.AssertExpectations(t)
 		})
diff --git a/pkg/utils/test/test_utils.go b/pkg/utils/test/test_utils.go
index a9afceb..03fda34 100644
--- a/pkg/utils/test/test_utils.go
+++ b/pkg/utils/test/test_utils.go
@@ -141,7 +141,8 @@ var (
 		},
 		Spec: &v1alpha1.RAGEngineSpec{
 			Compute: &v1alpha1.ResourceSpec{
-				Count: &gpuNodeCount,
+				Count:        &gpuNodeCount,
+				InstanceType: "Standard_NC12s_v3",
 				LabelSelector: &metav1.LabelSelector{
 					MatchLabels: map[string]string{
 						corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
diff --git a/pkg/workspace/estimator/advancednodesestimator/estimator_test.go b/pkg/workspace/estimator/advancednodesestimator/estimator_test.go
index 78f6a89..d51aa21 100644
--- a/pkg/workspace/estimator/advancednodesestimator/estimator_test.go
+++ b/pkg/workspace/estimator/advancednodesestimator/estimator_test.go
@@ -18,12 +18,17 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
@@ -124,7 +129,7 @@ func TestAdvancedNodesEstimator_EstimateNodeCount(t *testing.T) {
 			expectedError: false,
 		},
 		{
-			name: "Should return error for invalid instance type",
+			name: "Should return error for invalid instance type when NAP enabled",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
@@ -144,10 +149,10 @@ func TestAdvancedNodesEstimator_EstimateNodeCount(t *testing.T) {
 			},
 			expectedCount: 0,
 			expectedError: true,
-			errorContains: "GPU config is nil for instance type",
+			errorContains: "failed to get GPU config for instance type Invalid_Instance_Type",
 		},
 		{
-			name: "Should calculate optimal node count when GPU memory allows optimization",
+			name: "Should optimize node count with valid instance type when NAP enabled",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
@@ -160,12 +165,12 @@ func TestAdvancedNodesEstimator_EstimateNodeCount(t *testing.T) {
 				Inference: &kaitov1beta1.InferenceSpec{
 					Preset: &kaitov1beta1.PresetSpec{
 						PresetMeta: kaitov1beta1.PresetMeta{
-							Name: "test-model", // 8Gi requirement
+							Name: "test-model", // 8Gi requirement, easily fits in single A100
 						},
 					},
 				},
 			},
-			expectedCount: 1, // Should optimize to 1 node (8Gi easily fits in 80GB GPU)
+			expectedCount: 1, // Should optimize to 1 node despite user requesting 4
 			expectedError: false,
 		},
 		{
@@ -216,6 +221,13 @@ func TestAdvancedNodesEstimator_EstimateNodeCount(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			// Ensure NAP is enabled (default behavior) for these tests
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
 			count, err := calculator.EstimateNodeCount(ctx, tt.workspace, nil)
 
 			if tt.expectedError {
@@ -232,6 +244,208 @@ func TestAdvancedNodesEstimator_EstimateNodeCount(t *testing.T) {
 	}
 }
 
+func TestAdvancedNodesEstimator_EstimateNodeCount_BYO(t *testing.T) {
+	// Set the cloud provider environment variable for SKU lookup
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+
+	ctx := context.Background()
+	calculator := &AdvancedNodesEstimator{}
+
+	tests := []struct {
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		setupMocks    func(*test.MockClient)
+		expectedCount int32
+		expectedError bool
+		errorContains string
+	}{
+		{
+			name: "Should return error when no ready nodes found (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Invalid_Instance_Type", // Instance type is optional for BYO
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock empty ready nodes list
+				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "no ready nodes found, unable to determine GPU configuration",
+		},
+		{
+			name: "Should return error when GetReadyNodes fails (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					// No InstanceType - should get config from existing nodes
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock GetReadyNodes to fail
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(assert.AnError)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "failed to list ready nodes",
+		},
+		{
+			name: "Should use GPU config from ready nodes (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					// No InstanceType specified - should get config from existing nodes
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock ready node with GPU labels
+				readyNode := corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "byo-gpu-node",
+						Labels: map[string]string{
+							"node.kubernetes.io/instance-type": "Standard_NC96ads_A100_v4",
+							"kubernetes.azure.com/accelerator": "nvidia-tesla-a100",
+							"nvidia.com/gpu.product":           "Tesla-A100-SXM4-80GB",
+							"nvidia.com/gpu.count":             "4",
+							"nvidia.com/gpu.memory":            "81920", // 80GB in MiB
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+						Capacity: corev1.ResourceList{
+							resources.CapacityNvidiaGPU: resource.MustParse("4"), // 4 A100 GPUs
+						},
+					},
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{readyNode}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 1, // Should work with BYO node configuration
+			expectedError: false,
+		},
+		{
+			name: "Should return error when GetGPUConfigFromNodeLabels fails (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock ready node without proper GPU labels
+				readyNode := corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "cpu-only-node",
+						Labels: map[string]string{
+							"node.kubernetes.io/instance-type": "Standard_D4s_v3", // CPU-only instance
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{readyNode}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "failed to get GPU config from existing nodes",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Save original feature gate value and enable BYO mode
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = true
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
+			// Create a mock client for BYO scenarios
+			mockClient := test.NewClient()
+			if tt.setupMocks != nil {
+				tt.setupMocks(mockClient)
+			}
+
+			count, err := calculator.EstimateNodeCount(ctx, tt.workspace, mockClient)
+
+			if tt.expectedError {
+				require.Error(t, err)
+				if tt.errorContains != "" {
+					assert.Contains(t, err.Error(), tt.errorContains)
+				}
+				assert.Equal(t, tt.expectedCount, count)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedCount, count)
+			}
+
+			mockClient.AssertExpectations(t)
+		})
+	}
+}
+
 func TestAdvancedNodesEstimator_EstimateNodeCount_Falcon7B(t *testing.T) {
 	// Set the cloud provider environment variable for SKU lookup
 	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
@@ -316,6 +530,13 @@ func TestAdvancedNodesEstimator_EstimateNodeCount_Falcon7B(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			// Ensure NAP is enabled for these tests
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
 			count, err := calculator.EstimateNodeCount(ctx, tt.workspace, nil)
 
 			if tt.expectedError {
@@ -372,6 +593,13 @@ func TestAdvancedNodesEstimator_EstimateNodeCount_Qwen25Coder32B(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			// Ensure NAP is enabled for these tests
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
 			count, err := calculator.EstimateNodeCount(ctx, tt.workspace, nil)
 
 			if tt.expectedError {
diff --git a/pkg/workspace/estimator/basicnodesestimator/estimator_test.go b/pkg/workspace/estimator/basicnodesestimator/estimator_test.go
index 8a2d58f..691bf9f 100644
--- a/pkg/workspace/estimator/basicnodesestimator/estimator_test.go
+++ b/pkg/workspace/estimator/basicnodesestimator/estimator_test.go
@@ -18,12 +18,17 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/utils/ptr"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
@@ -45,11 +50,12 @@ func TestBasicNodesEstimator_EstimateNodeCount(t *testing.T) {
 	estimator := &BasicNodesEstimator{}
 
 	tests := []struct {
-		name          string
-		workspace     *kaitov1beta1.Workspace
-		expectedCount int32
-		expectedError bool
-		errorContains string
+		name                        string
+		workspace                   *kaitov1beta1.Workspace
+		disableNodeAutoProvisioning bool
+		expectedCount               int32
+		expectedError               bool
+		errorContains               string
 	}{
 		{
 			name: "Should return resource count when inference is nil",
@@ -124,7 +130,7 @@ func TestBasicNodesEstimator_EstimateNodeCount(t *testing.T) {
 			expectedError: false,
 		},
 		{
-			name: "Should return error for invalid instance type",
+			name: "Should return error for invalid instance type (NAP enabled by default)",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
@@ -144,10 +150,10 @@ func TestBasicNodesEstimator_EstimateNodeCount(t *testing.T) {
 			},
 			expectedCount: 0,
 			expectedError: true,
-			errorContains: "GPU config is nil for instance type",
+			errorContains: "failed to get GPU config for instance type Invalid_Instance_Type",
 		},
 		{
-			name: "Should calculate optimal node count when GPU memory allows optimization",
+			name: "Should optimize node count with valid instance type (NAP enabled by default)",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name:      "test-workspace",
@@ -155,17 +161,17 @@ func TestBasicNodesEstimator_EstimateNodeCount(t *testing.T) {
 				},
 				Resource: kaitov1beta1.ResourceSpec{
 					Count:        ptr.To(4),                  // User requests 4 nodes
-					InstanceType: "Standard_NC96ads_A100_v4", // Has large GPU memory (320GB)
+					InstanceType: "Standard_NC96ads_A100_v4", // Large instance type with plenty of GPU memory
 				},
 				Inference: &kaitov1beta1.InferenceSpec{
 					Preset: &kaitov1beta1.PresetSpec{
 						PresetMeta: kaitov1beta1.PresetMeta{
-							Name: "test-model", // 8Gi requirement
+							Name: "test-model", // 8Gi requirement (small model)
 						},
 					},
 				},
 			},
-			expectedCount: 1, // Should calculate that 1 node is sufficient
+			expectedCount: 1, // Should optimize to 1 node since A100 has enough memory for 8Gi model
 			expectedError: false,
 		},
 		{
@@ -216,6 +222,13 @@ func TestBasicNodesEstimator_EstimateNodeCount(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			// Ensure NAP is enabled (default behavior) for these tests
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
 			count, err := estimator.EstimateNodeCount(ctx, tt.workspace, nil)
 
 			if tt.expectedError {
@@ -241,6 +254,13 @@ func TestBasicNodesEstimator_EstimateNodeCount_GPUMemoryCalculation(t *testing.T
 
 	// Test case for detailed GPU memory calculation verification
 	t.Run("Should calculate correct minimum nodes based on GPU memory requirements", func(t *testing.T) {
+		// Ensure NAP is enabled for this test
+		originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+		featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+		defer func() {
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+		}()
+
 		// Use a model that requires significant GPU memory (64Gi)
 		workspace := &kaitov1beta1.Workspace{
 			ObjectMeta: metav1.ObjectMeta{
@@ -270,6 +290,212 @@ func TestBasicNodesEstimator_EstimateNodeCount_GPUMemoryCalculation(t *testing.T
 	})
 }
 
+func TestBasicNodesEstimator_EstimateNodeCount_BYO(t *testing.T) {
+	// Set the cloud provider environment variable for SKU lookup
+	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+
+	ctx := context.Background()
+	estimator := &BasicNodesEstimator{}
+
+	tests := []struct {
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		setupMocks    func(*test.MockClient)
+		expectedCount int32
+		expectedError bool
+		errorContains string
+	}{
+		{
+			name: "Should return error when no ready nodes found (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					Count:        ptr.To(1),
+					InstanceType: "Standard_NC6s_v3", // Instance type is optional for BYO
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock empty ready nodes list
+				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "no ready nodes found, unable to determine GPU configuration",
+		},
+		{
+			name: "Should return error when GetReadyNodes fails (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					Count: ptr.To(1),
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock GetReadyNodes to fail
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(assert.AnError)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "failed to list ready nodes",
+		},
+		{
+			name: "Should use GPU config from ready nodes (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					Count: ptr.To(2),
+					// No InstanceType specified - should get config from existing nodes
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock ready node with GPU labels
+				readyNode := corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "byo-gpu-node",
+						Labels: map[string]string{
+							"node.kubernetes.io/instance-type": "Standard_NC96ads_A100_v4",
+							"kubernetes.azure.com/accelerator": "nvidia-tesla-a100",
+							"nvidia.com/gpu.product":           "Tesla-A100-SXM4-80GB",
+							"nvidia.com/gpu.count":             "4",
+							"nvidia.com/gpu.memory":            "81920", // 80GB in MiB
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+						Capacity: corev1.ResourceList{
+							resources.CapacityNvidiaGPU: resource.MustParse("4"), // 4 A100 GPUs
+						},
+					},
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{readyNode}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 1, // Should optimize from 2 to 1 node based on A100 memory
+			expectedError: false,
+		},
+		{
+			name: "Should return error when GetGPUConfigFromNodeLabels fails (NAP disabled)",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test-workspace",
+					Namespace: "default",
+				},
+				Resource: kaitov1beta1.ResourceSpec{
+					Count: ptr.To(1),
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "test-model",
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock ready node without proper GPU labels
+				readyNode := corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "cpu-only-node",
+						Labels: map[string]string{
+							"node.kubernetes.io/instance-type": "Standard_D4s_v3", // CPU-only instance
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				}
+				nodeList := &corev1.NodeList{Items: []corev1.Node{readyNode}}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+			},
+			expectedCount: 0,
+			expectedError: true,
+			errorContains: "failed to get GPU config from existing nodes",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Enable BYO mode (NAP disabled)
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = true
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
+			// Create a mock client
+			mockClient := test.NewClient()
+			if tt.setupMocks != nil {
+				tt.setupMocks(mockClient)
+			}
+
+			count, err := estimator.EstimateNodeCount(ctx, tt.workspace, mockClient)
+
+			if tt.expectedError {
+				require.Error(t, err)
+				if tt.errorContains != "" {
+					assert.Contains(t, err.Error(), tt.errorContains)
+				}
+				assert.Equal(t, tt.expectedCount, count)
+			} else {
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedCount, count)
+			}
+
+			mockClient.AssertExpectations(t)
+		})
+	}
+}
+
 func TestBasicNodesEstimator_EstimateNodeCount_EdgeCases(t *testing.T) {
 	// Set the cloud provider environment variable for SKU lookup
 	t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
@@ -278,6 +504,13 @@ func TestBasicNodesEstimator_EstimateNodeCount_EdgeCases(t *testing.T) {
 	estimator := &BasicNodesEstimator{}
 
 	t.Run("Should handle case when nodeCountPerReplica is zero", func(t *testing.T) {
+		// Ensure NAP is enabled for this test
+		originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+		featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+		defer func() {
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+		}()
+
 		// This test covers the new logic where the condition is:
 		// if minimumNodes < nodeCountPerReplica { nodeCountPerReplica = minimumNodes }
 		// When nodeCountPerReplica is 0, minimumNodes will not be less than 0,
@@ -310,6 +543,13 @@ func TestBasicNodesEstimator_EstimateNodeCount_EdgeCases(t *testing.T) {
 	})
 
 	t.Run("Should return minimum nodes when it's smaller than nodeCountPerReplica", func(t *testing.T) {
+		// Ensure NAP is enabled for this test
+		originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+		featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+		defer func() {
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+		}()
+
 		workspace := &kaitov1beta1.Workspace{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "test-workspace",
@@ -338,6 +578,13 @@ func TestBasicNodesEstimator_EstimateNodeCount_EdgeCases(t *testing.T) {
 	})
 
 	t.Run("Should return nodeCountPerReplica when minimumNodes is higher", func(t *testing.T) {
+		// Ensure NAP is enabled for this test
+		originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+		featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = false
+		defer func() {
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+		}()
+
 		workspace := &kaitov1beta1.Workspace{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "test-workspace",
diff --git a/pkg/workspace/image/puller_test.go b/pkg/workspace/image/puller_test.go
index 5ce0ed4..37ca47b 100644
--- a/pkg/workspace/image/puller_test.go
+++ b/pkg/workspace/image/puller_test.go
@@ -103,7 +103,7 @@ var _ = Describe("Puller", func() {
 			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.Image).To(Equal("mcr.microsoft.com/aks/skopeo:1.14.4-6"))
 			Expect(ret.Command).To(Equal([]string{"/bin/sh", "-c"}))
 			Expect(ret.Args).To(Equal([]string{pullerSH}))
 		})
diff --git a/pkg/workspace/inference/preset_inferences_test.go b/pkg/workspace/inference/preset_inferences_test.go
index a80fd2d..189b1b9 100644
--- a/pkg/workspace/inference/preset_inferences_test.go
+++ b/pkg/workspace/inference/preset_inferences_test.go
@@ -24,11 +24,10 @@ import (
 	"github.com/stretchr/testify/mock"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
 	"github.com/kaito-project/kaito/pkg/sku"
 	"github.com/kaito-project/kaito/pkg/utils"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
@@ -63,6 +62,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-model:1.0.0",
@@ -78,6 +81,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-no-tensor-parallel-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-no-tensor-parallel-model:1.0.0",
@@ -93,6 +100,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-no-lora-support-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-no-lora-support-model:1.0.0",
@@ -108,6 +119,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-model:1.0.0",
@@ -126,6 +141,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-model:1.0.0",
@@ -141,6 +160,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:           "Deployment",
 			expectedModelImage: "test-registry/kaito-test-model:1.0.0",
@@ -159,6 +182,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model-download-a100",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:    "Deployment",
 			expectedCmd: `/bin/sh -c python3 /workspace/vllm/inference_api.py --gpu-memory-utilization=0.84 --max-model-len=2048 --tensor-parallel-size=2 --model=test-repo/test-model-a100 --code-revision=test-revision --download-dir=/workspace/weights --kaito-config-file=/mnt/config/inference_config.yaml`,
@@ -182,6 +209,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Service{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:    "StatefulSet",
 			expectedCmd: `/bin/sh -c if [ "${POD_INDEX}" = "0" ]; then  --ray_cluster_size=3 --ray_port=6379; python3 /workspace/vllm/inference_api.py --model=test-repo/test-model --code-revision=test-revision --download-dir=/workspace/weights --gpu-memory-utilization=0.84 --max-model-len=2048 --kaito-config-file=/mnt/config/inference_config.yaml --pipeline-parallel-size=3 --tensor-parallel-size=2; else  --ray_address=testWorkspace-0.testWorkspace-headless.kaito.svc.cluster.local --ray_port=6379; fi`,
@@ -214,6 +245,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Service{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.Anything, mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:    "StatefulSet",
 			expectedCmd: `/bin/sh -c if [ "${POD_INDEX}" = "0" ]; then  --ray_cluster_size=3 --ray_port=6379; python3 /workspace/vllm/inference_api.py --model=test-repo/test-model --code-revision=test-revision --download-dir=/workspace/weights --gpu-memory-utilization=0.84 --max-model-len=2048 --kaito-config-file=/mnt/config/inference_config.yaml --pipeline-parallel-size=3 --tensor-parallel-size=2; else  --ray_address=testWorkspace-0.testWorkspace-headless.kaito.svc.cluster.local --ray_port=6379; fi`,
@@ -243,6 +278,10 @@ func TestGeneratePresetInference(t *testing.T) {
 			modelName: "test-model-download",
 			callMocks: func(c *test.MockClient) {
 				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.ConfigMap{}), mock.Anything).Return(nil)
+				// Mock node get for TryGetGPUConfigFromNode
+				c.On("Get", mock.IsType(context.TODO()), mock.Anything, mock.IsType(&corev1.Node{}), mock.Anything).Return(nil)
+				// Mock node list for BYO node discovery
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(nil)
 			},
 			workload:    "Deployment",
 			expectedCmd: "/bin/sh -c accelerate launch /workspace/tfs/inference_api.py --pretrained_model_name_or_path=test-repo/test-model --revision=test-revision",
@@ -273,6 +312,9 @@ func TestGeneratePresetInference(t *testing.T) {
 			//nolint:staticcheck //SA1019: deprecate Resource.Count field
 			workspace.Resource.Count = &tc.nodeCount
 
+			// Set WorkerNodes to avoid node listing in tests (for BYO scenarios)
+			workspace.Status.WorkerNodes = []string{"test-node-1"}
+
 			// Set the Status.Inference.TargetNodeCount for proper node count calculation
 			if workspace.Inference != nil {
 				nodeCount, err := estimator.EstimateNodeCount(t.Context(), workspace, mockClient)
@@ -493,10 +535,12 @@ func TestGetGPUConfig(t *testing.T) {
 	test.RegisterTestModel()
 
 	testcases := map[string]struct {
-		workspace      *v1beta1.Workspace
-		model          string
-		callMocks      func(c *test.MockClient)
-		expectedConfig sku.GPUConfig
+		workspace                   *v1beta1.Workspace
+		model                       string
+		callMocks                   func(c *test.MockClient)
+		disableNodeAutoProvisioning bool
+		expectedConfig              *sku.GPUConfig
+		expectedErr                 string
 	}{
 		"SKU path - get config from instanceType": {
 			workspace: &v1beta1.Workspace{
@@ -510,20 +554,26 @@ func TestGetGPUConfig(t *testing.T) {
 			callMocks: func(c *test.MockClient) {
 				// No need for mock expectations here
 			},
-			expectedConfig: sku.GPUConfig{
+			disableNodeAutoProvisioning: false,
+			expectedConfig: &sku.GPUConfig{
 				SKU:             "Standard_NC24ads_A100_v4",
 				GPUCount:        1,
 				GPUMemGiB:       80,
 				GPUModel:        "NVIDIA A100",
 				NVMeDiskEnabled: true,
 			},
+			expectedErr: "",
 		},
 		"Node status path - get config from node": {
 			workspace: &v1beta1.Workspace{
 				Resource: v1beta1.ResourceSpec{
 					InstanceType:   "unknown-instance-type",
 					PreferredNodes: []string{},
-					LabelSelector:  &metav1.LabelSelector{},
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"test-label-key": "test-label-value",
+						},
+					},
 				},
 				Status: v1beta1.WorkspaceStatus{
 					WorkerNodes: []string{"gpu-node-1"},
@@ -534,28 +584,40 @@ func TestGetGPUConfig(t *testing.T) {
 				node := &corev1.Node{
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "gpu-node-1",
+						// TODO: add nvidia.com labels for GPU product, count, and memory
+						Labels: map[string]string{
+							// TODO: use the variable names for nvidia.com labels instead of the string literals.
+							consts.NvidiaGPUProduct: "NVIDIA-A10-24Q",
+							consts.NvidiaGPUCount:   "2",
+							consts.NvidiaGPUMemory:  "24512", // in MiB
+							"test-label-key":        "test-label-value",
+						},
 					},
 					Status: corev1.NodeStatus{
-						Capacity: corev1.ResourceList{
-							consts.NvidiaGPU: *resource.NewQuantity(2, resource.DecimalSI),
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
 						},
 					},
 				}
-				c.On("Get", mock.Anything, mock.MatchedBy(func(key client.ObjectKey) bool {
-					return key.Name == "gpu-node-1"
-				}), mock.IsType(&corev1.Node{}), mock.Anything).
+
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).
 					Run(func(args mock.Arguments) {
-						nodeArg := args.Get(2).(*corev1.Node)
-						*nodeArg = *node
+						nodeListArg := args.Get(1).(*corev1.NodeList)
+						nodeListArg.Items = []corev1.Node{*node}
 					}).Return(nil)
 			},
-			expectedConfig: sku.GPUConfig{
+			disableNodeAutoProvisioning: true,
+			expectedConfig: &sku.GPUConfig{
 				SKU:      "unknown",
 				GPUCount: 2,
-				GPUModel: "unknown",
+				GPUModel: "NVIDIA-A10-24Q",
 			},
+			expectedErr: "",
 		},
-		"Fallback path when node fetch fails": {
+		"Return error when node list fails": {
 			workspace: &v1beta1.Workspace{
 				Resource: v1beta1.ResourceSpec{
 					InstanceType:   "unknown-instance-type",
@@ -568,20 +630,17 @@ func TestGetGPUConfig(t *testing.T) {
 			},
 			model: "test-model",
 			callMocks: func(c *test.MockClient) {
-				c.On("Get", mock.Anything, mock.MatchedBy(func(key client.ObjectKey) bool {
-					return key.Name == "gpu-node-1"
-				}), mock.IsType(&corev1.Node{}), mock.Anything).Return(fmt.Errorf("no nodes found"))
-			},
-			expectedConfig: sku.GPUConfig{
-				GPUCount: 1, // From test-model's GPUCountRequirement
+				c.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(fmt.Errorf("internal server error"))
 			},
+			disableNodeAutoProvisioning: true,
+			expectedConfig:              nil,
+			expectedErr:                 "failed to list ready nodes: internal server error",
 		},
-		"PreferredNodes path - bypass SKU path": {
+		"Return error when SKU look up fails": {
 			workspace: &v1beta1.Workspace{
 				Resource: v1beta1.ResourceSpec{
-					InstanceType:   "Standard_NC24ads_A100_v4",
-					PreferredNodes: []string{"preferred-node"},
-					LabelSelector:  &metav1.LabelSelector{},
+					InstanceType:  "unknown-instance-type",
+					LabelSelector: &metav1.LabelSelector{},
 				},
 				Status: v1beta1.WorkspaceStatus{
 					WorkerNodes: []string{"gpu-node-1"},
@@ -589,34 +648,21 @@ func TestGetGPUConfig(t *testing.T) {
 			},
 			model: "test-model",
 			callMocks: func(c *test.MockClient) {
-				node := &corev1.Node{
-					ObjectMeta: metav1.ObjectMeta{
-						Name: "gpu-node-1",
-					},
-					Status: corev1.NodeStatus{
-						Capacity: corev1.ResourceList{
-							consts.NvidiaGPU: *resource.NewQuantity(2, resource.DecimalSI),
-						},
-					},
-				}
-				c.On("Get", mock.Anything, mock.MatchedBy(func(key client.ObjectKey) bool {
-					return key.Name == "gpu-node-1"
-				}), mock.IsType(&corev1.Node{}), mock.Anything).
-					Run(func(args mock.Arguments) {
-						nodeArg := args.Get(2).(*corev1.Node)
-						*nodeArg = *node
-					}).Return(nil)
-			},
-			expectedConfig: sku.GPUConfig{
-				SKU:      "unknown",
-				GPUCount: 2,
-				GPUModel: "unknown",
 			},
+			disableNodeAutoProvisioning: false,
+			expectedConfig:              nil,
+			expectedErr:                 "invalid value: Unsupported SKU 'unknown-instance-type' for cloud provider: sku",
 		},
 	}
 
 	for k, tc := range testcases {
 		t.Run(k, func(t *testing.T) {
+			originalFeatureGate := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = tc.disableNodeAutoProvisioning
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalFeatureGate
+			}()
+
 			t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
 
 			mockClient := test.NewClient()
@@ -631,7 +677,23 @@ func TestGetGPUConfig(t *testing.T) {
 				KubeClient: mockClient,
 			}
 
-			config := getGPUConfig(gctx)
+			config, err := getGPUConfig(gctx)
+			if tc.expectedErr == "" {
+				if err != nil {
+					t.Errorf("Expected no error but got error: '%v'", err)
+					return
+				}
+			} else {
+				if err == nil {
+					t.Errorf("Expected error '%v' but got no error", tc.expectedErr)
+				} else {
+					if err.Error() != tc.expectedErr {
+						t.Errorf("Expected error to be '%v', got '%v'", tc.expectedErr, err)
+					}
+				}
+
+				return
+			}
 
 			// Check each field against expected values
 			if tc.expectedConfig.SKU != "" && config.SKU != tc.expectedConfig.SKU {
diff --git a/pkg/workspace/manifests/manifests_test.go b/pkg/workspace/manifests/manifests_test.go
index 0a1bb47..430a7e1 100644
--- a/pkg/workspace/manifests/manifests_test.go
+++ b/pkg/workspace/manifests/manifests_test.go
@@ -185,7 +185,7 @@ func TestGeneratePullerContainers(t *testing.T) {
 				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)
+					assert.Equal(t, "mcr.microsoft.com/aks/skopeo:1.14.4-6", c.Image)
 					if assert.Len(t, c.Args, 1) {
 						assert.Contains(t, c.Args[0], "/mnt/adapter/adapterA")
 					}
diff --git a/pkg/workspace/resource/node_claim_test.go b/pkg/workspace/resource/node_claim_test.go
index 6c4634e..1d4c316 100644
--- a/pkg/workspace/resource/node_claim_test.go
+++ b/pkg/workspace/resource/node_claim_test.go
@@ -40,50 +40,24 @@ func TestCheckNodeClaims(t *testing.T) {
 	testCases := []struct {
 		name                       string
 		workspace                  *kaitov1beta1.Workspace
+		readyNodes                 []*corev1.Node
 		setupMocks                 func(*test.MockClient)
 		expectedAddedCount         int
 		expectedExistingNodeClaims int
 		expectedError              string
 		featureFlagValue           bool
 	}{
-		{
-			name: "get required node claims fails",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					LabelSelector: &metav1.LabelSelector{},
-				},
-			},
-			setupMocks: func(mockClient *test.MockClient) {
-				// Mock GetRequiredNodeClaimsCount to fail (mock node list to fail)
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Return(errors.New("list nodes failed"))
-			},
-			expectedAddedCount:         0,
-			expectedExistingNodeClaims: 0,
-			expectedError:              "failed to get required NodeClaims",
-			featureFlagValue:           false,
-		},
 		{
 			name: "get existing node claims fails",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
-					LabelSelector:  &metav1.LabelSelector{},
-					PreferredNodes: []string{},
-				},
-				Status: kaitov1beta1.WorkspaceStatus{
-					TargetNodeCount: 2,
+					LabelSelector: &metav1.LabelSelector{},
 				},
 			},
+			readyNodes: []*corev1.Node{}, // Empty ready nodes
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock node list to succeed (for GetRequiredNodeClaimsCount)
-				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-
-				// Mock NodeClaim list to fail (for GetExistingNodeClaims)
+				// Mock NodeClaim list to fail
 				mockClient.On("List", mock.Anything, mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Return(errors.New("list nodeclaims failed"))
 			},
 			expectedAddedCount:         0,
@@ -104,14 +78,8 @@ func TestCheckNodeClaims(t *testing.T) {
 					TargetNodeCount: 3, // Target 3 nodes, no BYO = need 3 NodeClaims, have 1 = add 2
 				},
 			},
+			readyNodes: []*corev1.Node{}, // Empty ready nodes - no ready nodes yet
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock empty node list (no BYO nodes)
-				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-
 				// Mock existing NodeClaim list with 1 NodeClaim
 				nodeClaim := &karpenterv1.NodeClaim{
 					ObjectMeta: metav1.ObjectMeta{
@@ -128,7 +96,7 @@ func TestCheckNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedAddedCount:         2, // Required 3, have 1 = add 2
+			expectedAddedCount:         2, // Required 3, have 0 ready nodes without NodeClaim = need 3 NodeClaims
 			expectedExistingNodeClaims: 1,
 			expectedError:              "",
 			featureFlagValue:           false,
@@ -146,14 +114,8 @@ func TestCheckNodeClaims(t *testing.T) {
 					TargetNodeCount: 2, // Target 2, no BYO = need 2 NodeClaims, have 2 = perfect match
 				},
 			},
+			readyNodes: []*corev1.Node{}, // Empty ready nodes - NodeClaims exist but not ready yet
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock empty node list (no BYO nodes)
-				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-
 				// Mock existing NodeClaim list with 2 NodeClaims (matches target)
 				nodeClaims := []karpenterv1.NodeClaim{
 					{
@@ -181,13 +143,13 @@ func TestCheckNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedAddedCount:         0,
+			expectedAddedCount:         0, // Target 2, have 0 ready nodes without NodeClaim = need 2 NodeClaims
 			expectedExistingNodeClaims: 2,
 			expectedError:              "",
 			featureFlagValue:           false,
 		},
 		{
-			name: "BYO mode no node claims needed",
+			name: "empty nodeclaim list with target nodes",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
@@ -198,14 +160,8 @@ func TestCheckNodeClaims(t *testing.T) {
 					TargetNodeCount: 1,
 				},
 			},
+			readyNodes: []*corev1.Node{}, // Empty ready nodes
 			setupMocks: func(mockClient *test.MockClient) {
-				// Mock empty node list (for ResolveReadyNodesAndRequiredNodeCount -> GetBYOAndReadyNodes -> ListNodes)
-				nodeList := &corev1.NodeList{Items: []corev1.Node{}}
-				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
-					nl := args.Get(1).(*corev1.NodeList)
-					*nl = *nodeList
-				}).Return(nil)
-
 				// Mock empty NodeClaim list
 				nodeClaimList := &karpenterv1.NodeClaimList{Items: []karpenterv1.NodeClaim{}}
 				mockClient.On("List", mock.Anything, mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Run(func(args mock.Arguments) {
@@ -213,10 +169,75 @@ func TestCheckNodeClaims(t *testing.T) {
 					*ncl = *nodeClaimList
 				}).Return(nil)
 			},
-			expectedAddedCount:         0,
+			expectedAddedCount:         1, // Target 1, have 0 = add 1
 			expectedExistingNodeClaims: 0,
 			expectedError:              "",
-			featureFlagValue:           true, // BYO mode
+			featureFlagValue:           false,
+		},
+		{
+			name: "mixed scenario: BYO nodes and target nodes",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					LabelSelector:  &metav1.LabelSelector{},
+					PreferredNodes: []string{},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 3, // Target 3: have 1 BYO ready node = need 2 more NodeClaims
+				},
+			},
+			readyNodes: []*corev1.Node{
+				// 1 ready BYO node (does not have karpenter.sh/nodepool label)
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "byo-ready-node-1",
+						Labels: map[string]string{
+							// No karpenter.sh/nodepool label - this is a BYO node
+							"node.kubernetes.io/instance-type": "Standard_NC24ads_A100_v4",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				},
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock 2 NodeClaims: 1 that has a ready node, 1 that is still pending
+				nodeClaims := []karpenterv1.NodeClaim{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "nodeclaim-ready", // This one has a ready node
+							Labels: map[string]string{
+								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
+								kaitov1beta1.LabelWorkspaceNamespace: "default",
+							},
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "nodeclaim-pending", // This one is still pending
+							Labels: map[string]string{
+								kaitov1beta1.LabelWorkspaceName:      "test-workspace",
+								kaitov1beta1.LabelWorkspaceNamespace: "default",
+							},
+						},
+					},
+				}
+				nodeClaimList := &karpenterv1.NodeClaimList{Items: nodeClaims}
+				mockClient.On("List", mock.Anything, mock.IsType(&karpenterv1.NodeClaimList{}), mock.Anything).Run(func(args mock.Arguments) {
+					ncl := args.Get(1).(*karpenterv1.NodeClaimList)
+					*ncl = *nodeClaimList
+				}).Return(nil)
+			},
+			expectedAddedCount:         0, // Target 3, have 1 BYO ready node = need 2 more NodeClaims
+			expectedExistingNodeClaims: 2,
+			expectedError:              "",
+			featureFlagValue:           false,
 		},
 	}
 
@@ -242,7 +263,7 @@ func TestCheckNodeClaims(t *testing.T) {
 			}
 
 			// Execute the function under test
-			addedCount, existingNodeClaims, _, err := manager.CheckNodeClaims(context.Background(), tc.workspace)
+			addedCount, existingNodeClaims, err := manager.CheckNodeClaims(context.Background(), tc.workspace, tc.readyNodes)
 
 			// Assertions
 			assert.Equal(t, tc.expectedAddedCount, addedCount, "Added count mismatch")
@@ -494,6 +515,119 @@ func TestCreateNodeClaims(t *testing.T) {
 	}
 }
 
+func TestSetNodeClaimsReadyCondition_SetsToTrue(t *testing.T) {
+	// Helper function to create NodeClaim with ready condition
+	createNodeClaim := func(name string, isReady bool, isDeleting bool) *karpenterv1.NodeClaim {
+		nodeClaim := &karpenterv1.NodeClaim{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: name,
+			},
+			Status: karpenterv1.NodeClaimStatus{
+				NodeName: "test-node-" + name,
+			},
+		}
+
+		if isDeleting {
+			now := metav1.Now()
+			nodeClaim.DeletionTimestamp = &now
+		}
+
+		if isReady {
+			nodeClaim.Status.Conditions = []status.Condition{
+				{
+					Type:   "Ready",
+					Status: metav1.ConditionTrue,
+				},
+			}
+		} else {
+			nodeClaim.Status.Conditions = []status.Condition{
+				{
+					Type:   "Ready",
+					Status: metav1.ConditionFalse,
+				},
+			}
+		}
+
+		return nodeClaim
+	}
+
+	// Define test cases specifically for verifying condition is set to True
+	testCases := []struct {
+		name               string
+		workspace          *kaitov1beta1.Workspace
+		existingNodeClaims []*karpenterv1.NodeClaim
+		setupMocks         func(*test.MockClient)
+		expectedReady      bool
+		expectedError      string
+	}{
+		{
+			name: "Should set NodeClaimsReady condition to true when enough ready node claims",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 2,
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				createNodeClaim("nodeclaim-1", true, false),
+				createNodeClaim("nodeclaim-2", true, false),
+			},
+			setupMocks: func(mockClient *test.MockClient) {
+				// Mock workspace Get and Status update calls
+				setupWorkspaceStatusMock(mockClient, &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Status:     kaitov1beta1.WorkspaceStatus{TargetNodeCount: 2},
+				}, nil)
+
+				// Specifically verify the condition is set to True with correct reason
+				mockClient.StatusMock.On("Update", mock.Anything, mock.MatchedBy(func(ws *kaitov1beta1.Workspace) bool {
+					// Find the NodeClaimStatus condition and verify it's set to True
+					for _, condition := range ws.Status.Conditions {
+						if condition.Type == string(kaitov1beta1.ConditionTypeNodeClaimStatus) {
+							return condition.Status == metav1.ConditionTrue && condition.Reason == "NodeClaimsReady"
+						}
+					}
+					return false
+				}), mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: true,
+			expectedError: "",
+		},
+	}
+
+	// Run all test cases using a for loop
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			// Set up mocks
+			mockClient := test.NewClient()
+			mockRecorder := record.NewFakeRecorder(100)
+			expectations := utils.NewControllerExpectations()
+			manager := NewNodeClaimManager(mockClient, mockRecorder, expectations)
+
+			// Set up test-specific mocks
+			if tc.setupMocks != nil {
+				tc.setupMocks(mockClient)
+			}
+
+			// Execute the function under test
+			ready, err := manager.EnsureNodeClaimsReady(context.Background(), tc.workspace, []*corev1.Node{}, tc.existingNodeClaims)
+
+			// Assertions
+			assert.Equal(t, tc.expectedReady, ready, "Ready state mismatch")
+
+			if tc.expectedError != "" {
+				assert.Error(t, err, "Expected error but got none")
+				assert.Contains(t, err.Error(), tc.expectedError, "Error message mismatch")
+			} else {
+				assert.NoError(t, err, "Expected no error but got: %v", err)
+			}
+
+			// Verify mock expectations
+			mockClient.AssertExpectations(t)
+		})
+	}
+}
+
 func TestAreNodeClaimsReady(t *testing.T) {
 	// Helper function to create NodeClaim with ready condition
 	createNodeClaim := func(name string, isReady bool, isDeleting bool) *karpenterv1.NodeClaim {
@@ -686,7 +820,7 @@ func TestAreNodeClaimsReady(t *testing.T) {
 			}
 
 			// Execute the function under test
-			ready, err := manager.AreNodeClaimsReady(context.Background(), tc.workspace, tc.existingNodeClaims)
+			ready, err := manager.EnsureNodeClaimsReady(context.Background(), tc.workspace, []*corev1.Node{}, tc.existingNodeClaims)
 
 			// Assertions
 			assert.Equal(t, tc.expectedReady, ready, "Ready state mismatch")
@@ -703,3 +837,66 @@ func TestAreNodeClaimsReady(t *testing.T) {
 		})
 	}
 }
+
+func TestDetermineNodeOSDiskSize(t *testing.T) {
+	tests := []struct {
+		name             string
+		workspace        *kaitov1beta1.Workspace
+		expectedDiskSize string
+	}{
+		{
+			name: "Should return default disk size when workspace has no inference spec",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			expectedDiskSize: "1024Gi",
+		},
+		{
+			name: "Should return default disk size when inference spec has no preset",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					// No preset specified
+				},
+			},
+			expectedDiskSize: "1024Gi",
+		},
+		{
+			name: "Should return default disk size when preset name is empty",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Inference: &kaitov1beta1.InferenceSpec{
+					Preset: &kaitov1beta1.PresetSpec{
+						PresetMeta: kaitov1beta1.PresetMeta{
+							Name: "", // Empty preset name
+						},
+					},
+				},
+			},
+			expectedDiskSize: "1024Gi",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			mockRecorder := record.NewFakeRecorder(100)
+			expectations := utils.NewControllerExpectations()
+			manager := NewNodeClaimManager(mockClient, mockRecorder, expectations)
+
+			diskSize := manager.determineNodeOSDiskSize(tt.workspace)
+
+			// When no preset is specified, we expect the default
+			assert.Equal(t, tt.expectedDiskSize, diskSize)
+		})
+	}
+}
diff --git a/pkg/workspace/resource/node_test.go b/pkg/workspace/resource/node_test.go
index 7dd9971..207b678 100644
--- a/pkg/workspace/resource/node_test.go
+++ b/pkg/workspace/resource/node_test.go
@@ -17,6 +17,7 @@ import (
 	"context"
 	"errors"
 	"os"
+	"reflect"
 	"testing"
 
 	"github.com/awslabs/operatorpkg/status"
@@ -30,6 +31,8 @@ import (
 	karpenterv1 "sigs.k8s.io/karpenter/pkg/apis/v1"
 
 	kaitov1beta1 "github.com/kaito-project/kaito/api/v1beta1"
+	"github.com/kaito-project/kaito/pkg/featuregates"
+	"github.com/kaito-project/kaito/pkg/utils/consts"
 	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
@@ -43,7 +46,227 @@ func TestNewNodeResourceManager(t *testing.T) {
 	assert.Equal(t, mockClient, manager.Client)
 }
 
-func TestAreNodePluginsReady(t *testing.T) {
+func TestSetNodePluginsReadyCondition_SetsToTrue(t *testing.T) {
+	tests := []struct {
+		name               string
+		workspace          *kaitov1beta1.Workspace
+		existingNodeClaims []*karpenterv1.NodeClaim
+		setup              func(*test.MockClient)
+		expectedReady      bool
+		expectedError      bool
+	}{
+		{
+			name: "Should set NodePluginsReady condition to true when all plugins are ready",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3", // GPU instance type
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						Conditions: []status.Condition{
+							{Type: "Ready", Status: metav1.ConditionTrue},
+						},
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a ready node with GPU capacity and correct labels
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+						Labels: map[string]string{
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
+							resources.LabelKeyNvidia:       resources.LabelValueNvidia,
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+						Capacity: corev1.ResourceList{
+							resources.CapacityNvidiaGPU: resource.MustParse("1"),
+						},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+
+				// Create workspace object in the mock object map for Get calls
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+
+				// Mock status update for workspace condition update
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: true,
+			expectedError: false,
+		},
+		{
+			name: "Should set NodePluginsReady condition to true for non-GPU instance type",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_D2s_v3", // Non-GPU instance type
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{},
+			setup: func(mockClient *test.MockClient) {
+				// Create workspace object in the mock object map for status updates
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+
+				// Mock status update for NodePluginsReady condition
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: true,
+			expectedError: false,
+		},
+		{
+			name: "Should set NodePluginsReady condition to false when checkNodePlugin fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3", // GPU instance type
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						Conditions: []status.Condition{
+							{Type: "Ready", Status: metav1.ConditionTrue},
+						},
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create workspace object in the mock object map for Get calls
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+
+				// Don't create the node in ObjectMap to simulate Get failure
+				// Mock Get to return an error for node operations
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("node get failed"))
+
+				// Mock status update for error condition
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: false,
+			expectedError: true,
+		},
+		{
+			name: "Should set NodePluginsReady condition to false when nodes are not ready",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3", // GPU instance type
+					LabelSelector: &metav1.LabelSelector{},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{},
+				},
+			},
+			existingNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						Conditions: []status.Condition{
+							{Type: "Ready", Status: metav1.ConditionTrue},
+						},
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a node with zero GPU capacity (device plugins not ready)
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+						Labels: map[string]string{
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
+							resources.LabelKeyNvidia:       resources.LabelValueNvidia,
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+						Capacity: corev1.ResourceList{
+							resources.CapacityNvidiaGPU: resource.MustParse("0"),
+						},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+
+				// Create workspace object in the mock object map for Get calls
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+
+				// Mock status update for NodePluginsNotReady condition
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: false,
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			// set env CLOUD_PROVIDER to "azure" for AzureSKUHandler
+			os.Setenv("CLOUD_PROVIDER", "azure")
+			defer os.Unsetenv("CLOUD_PROVIDER")
+
+			manager := NewNodeManager(mockClient)
+			ready, err := manager.CheckIfNodePluginsReady(context.Background(), tt.workspace, tt.existingNodeClaims)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.expectedReady, ready)
+		})
+	}
+}
+
+func TestSetNodePluginsReadyCondition_AdditionalCases(t *testing.T) {
 	tests := []struct {
 		name               string
 		workspace          *kaitov1beta1.Workspace
@@ -66,11 +289,18 @@ func TestAreNodePluginsReady(t *testing.T) {
 			},
 			existingNodeClaims: []*karpenterv1.NodeClaim{},
 			setup: func(mockClient *test.MockClient) {
-				// Mock Get for workspace status update
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
-
-				// Mock status update for worker nodes and resource status
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				// For non-GPU instance types, the function should set NodePluginsReady to true directly
+				// Create workspace object in the mock object map for Get calls
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+					Resource: kaitov1beta1.ResourceSpec{
+						InstanceType:  "Standard_D2s_v3", // Non-GPU instance type
+						LabelSelector: &metav1.LabelSelector{},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: true,
 			expectedError: false,
@@ -121,12 +351,13 @@ func TestAreNodePluginsReady(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 
-				// Mock Get for node and workspace status update
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				// Node and workspace exist in ObjectMap for status updates
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 
 				// Mock status update
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: true,
 			expectedError: false,
@@ -152,11 +383,11 @@ func TestAreNodePluginsReady(t *testing.T) {
 				},
 			},
 			setup: func(mockClient *test.MockClient) {
-				// Mock Get for workspace status update
+				// Mock failing node Get - this should fail when getting the node, not from ObjectMap
 				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("node get failed"))
 
 				// Mock status update for error condition
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: false,
 			expectedError: true,
@@ -173,7 +404,7 @@ func TestAreNodePluginsReady(t *testing.T) {
 			defer os.Unsetenv("CLOUD_PROVIDER")
 
 			manager := NewNodeManager(mockClient)
-			ready, err := manager.AreNodePluginsReady(context.Background(), tt.workspace, tt.existingNodeClaims)
+			ready, err := manager.CheckIfNodePluginsReady(context.Background(), tt.workspace, tt.existingNodeClaims)
 
 			assert.Equal(t, tt.expectedError, err != nil)
 			assert.Equal(t, tt.expectedReady, ready)
@@ -209,11 +440,11 @@ func TestCheckNodePlugin(t *testing.T) {
 				},
 			},
 			setup: func(mockClient *test.MockClient) {
-				// Mock failing node Get
+				// Mock failing node Get - this should fail when getting the node, not from ObjectMap
 				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(errors.New("node get failed"))
 
 				// Mock status update for error condition
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: false,
 			expectedError: true,
@@ -257,10 +488,10 @@ func TestCheckNodePlugin(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 
 				// Mock node update
-				mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: true,
 			expectedError: false,
@@ -305,10 +536,16 @@ func TestCheckNodePlugin(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+
+				// Create workspace object in the mock object map for status updates
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
 
 				// Mock status update for GPU capacity not ready
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: false,
 			expectedError: false,
@@ -353,7 +590,7 @@ func TestCheckNodePlugin(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: false,
 			expectedError: false,
@@ -398,7 +635,7 @@ func TestCheckNodePlugin(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: true,
 			expectedError: false,
@@ -435,15 +672,22 @@ func TestCheckNodePlugin(t *testing.T) {
 					},
 				}
 				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 
-				// Get returns nil to indicate node exists
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				// Node exists in ObjectMap
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 
 				// Simulate Update failing when adding the label
 				mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("update failed"))
 
+				// Create workspace object in the mock object map for status updates
+				workspace := &kaitov1beta1.Workspace{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				}
+				mockClient.CreateOrUpdateObjectInMap(workspace)
+
 				// Expect status update attempt for error condition
-				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
 			expectedReady: false,
 			expectedError: true,
@@ -468,150 +712,737 @@ func TestCheckNodePlugin(t *testing.T) {
 	}
 }
 
-func TestGetReadyNodesFromNodeClaims(t *testing.T) {
+func TestSetNodesReadyCondition_SetsToTrue(t *testing.T) {
 	tests := []struct {
-		name            string
-		workspace       *kaitov1beta1.Workspace
-		readyNodeClaims []*karpenterv1.NodeClaim
-		setup           func(*test.MockClient)
-		expectedNodes   int
-		expectedError   bool
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		nodes         []*corev1.Node
+		nodeClaims    []*karpenterv1.NodeClaim
+		setup         func(*test.MockClient)
+		expectedReady bool
+		expectedError bool
 	}{
 		{
-			name: "Should return error when NodeClaim without assigned node",
+			name: "Should set NodesReady condition to true when enough nodes are ready",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 2,
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			nodes: []*corev1.Node{
 				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "", // No node assigned
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload": "test",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
 					},
 				},
-			},
-			setup: func(mockClient *test.MockClient) {
-				// No setup needed - will skip NodeClaim without NodeName
-			},
-			expectedNodes: 0,
-			expectedError: false,
-		},
-		{
-			name: "Should return error when fail to get node",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
-				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
-				},
-			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
 				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "nonexistent-node",
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-2",
+						Labels: map[string]string{
+							"workload": "test",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
 					},
 				},
 			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
 			setup: func(mockClient *test.MockClient) {
-				// Mock Get to return NotFound error
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{Resource: "nodes"}, "nonexistent-node"))
+				// Mock status update calls - verify condition is set to True
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+
+				// Mock the condition update to verify it's set to True with "NodesReady" reason
+				mockClient.StatusMock.On("Update", mock.Anything, mock.MatchedBy(func(ws *kaitov1beta1.Workspace) bool {
+					// Find the NodeStatus condition and verify it's set to True
+					for _, condition := range ws.Status.Conditions {
+						if condition.Type == string(kaitov1beta1.ConditionTypeNodeStatus) {
+							return condition.Status == metav1.ConditionTrue && condition.Reason == "NodesReady"
+						}
+					}
+					return false
+				}), mock.Anything).Return(nil).Maybe()
+
+				// Mock worker nodes update
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
 			},
-			expectedNodes: 0,
-			expectedError: true,
+			expectedReady: true,
+			expectedError: false,
 		},
-		{
-			name: "Should return error when node is not ready",
-			workspace: &kaitov1beta1.Workspace{
-				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			// Default to empty slices if not specified
+			nodes := tt.nodes
+			if nodes == nil {
+				nodes = []*corev1.Node{}
+			}
+			nodeClaims := tt.nodeClaims
+			if nodeClaims == nil {
+				nodeClaims = []*karpenterv1.NodeClaim{}
+			}
+
+			manager := NewNodeManager(mockClient)
+			ready, err := manager.EnsureNodesReady(context.Background(), tt.workspace, nodes, nodeClaims)
+
+			assert.Equal(t, tt.expectedReady, ready)
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestEnsureNodesReady(t *testing.T) {
+	tests := []struct {
+		name                        string
+		workspace                   *kaitov1beta1.Workspace
+		nodes                       []*corev1.Node
+		nodeClaims                  []*karpenterv1.NodeClaim
+		setup                       func(*test.MockClient)
+		disableNodeAutoProvisioning bool
+		expectedReady               bool
+		expectedError               bool
+	}{
+		{
+			name: "Should return true when enough nodes are ready",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 2,
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			nodes: []*corev1.Node{
 				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "test-node",
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload": "test",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
+					},
+				},
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-2",
+						Labels: map[string]string{
+							"workload": "test",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
 					},
 				},
 			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
 			setup: func(mockClient *test.MockClient) {
-				// Create a node that is not ready
-				node := &corev1.Node{
+				// Mock status update calls
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: true,
+			expectedError: false,
+		},
+		{
+			name: "Should return false when not enough nodes are ready",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 3,
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Only 1 ready node, but target is 3
+				nodes := []corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "node-1",
+							Labels: map[string]string{
+								"workload": "test",
+							},
+						},
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+							},
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "node-2",
+							Labels: map[string]string{
+								"workload": "test",
+							},
+						},
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{Type: corev1.NodeReady, Status: corev1.ConditionFalse},
+							},
+						},
+					},
+				}
+
+				nodeList := &corev1.NodeList{Items: nodes}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+
+				// Mock status update calls
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: false,
+			expectedError: false,
+		},
+		{
+			name: "Should exclude nodes being deleted",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 2,
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				now := metav1.Now()
+				nodes := []corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "node-1",
+							Labels: map[string]string{
+								"workload": "test",
+							},
+						},
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+							},
+						},
+					},
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "node-2",
+							Labels: map[string]string{
+								"workload": "test",
+							},
+							DeletionTimestamp: &now, // Being deleted
+						},
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+							},
+						},
+					},
+				}
+
+				nodeList := &corev1.NodeList{Items: nodes}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+
+				// Mock status update calls
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedReady: false, // Only 1 ready node (excluding deleting), but target is 2
+			expectedError: false,
+		},
+		{
+			name: "Should return error when UpdateWorkerNodesInStatus fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
+				},
+			},
+			nodes: []*corev1.Node{
+				{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-node",
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload": "test",
+						},
 					},
 					Status: corev1.NodeStatus{
 						Conditions: []corev1.NodeCondition{
-							{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionFalse,
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
+					},
+				},
+			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
+			setup: func(mockClient *test.MockClient) {
+				// Mock successful condition update
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
+				// Mock UpdateWorkerNodesInStatus to fail (this happens in the defer block)
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("worker nodes update failed")).Maybe()
+			},
+			disableNodeAutoProvisioning: true, // If NAP is disabled, we don't want to fail on updating the status due to the missing instance type label as that check should be skipped.
+			expectedReady:               true, // Function succeeds initially
+			expectedError:               true, // But returns error due to UpdateWorkerNodesInStatus failing in defer
+		},
+		{
+			name: "Should return error when status update fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				nodes := []corev1.Node{
+					{
+						ObjectMeta: metav1.ObjectMeta{
+							Name: "node-1",
+							Labels: map[string]string{
+								"workload": "test",
+							},
+						},
+						Status: corev1.NodeStatus{
+							Conditions: []corev1.NodeCondition{
+								{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
 							},
 						},
 					},
 				}
-				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+
+				nodeList := &corev1.NodeList{Items: nodes}
+				mockClient.On("List", mock.Anything, mock.IsType(&corev1.NodeList{}), mock.Anything).Run(func(args mock.Arguments) {
+					nl := args.Get(1).(*corev1.NodeList)
+					*nl = *nodeList
+				}).Return(nil)
+
+				// Mock status update calls - fail the condition update
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("status update failed")).Once()
+				// Mock successful worker nodes status update
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
 			},
-			expectedNodes: 0,
-			expectedError: false,
+			expectedReady: false,
+			expectedError: true,
 		},
 		{
-			name: "Should return ready node with matching instance type",
+			name: "NAP enabled - Should succeed but log warning when node missing instance type label",
 			workspace: &kaitov1beta1.Workspace{
 				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
 				Resource: kaitov1beta1.ResourceSpec{
-					InstanceType:  "Standard_NC12s_v3",
-					LabelSelector: &metav1.LabelSelector{},
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
 				},
 			},
-			readyNodeClaims: []*karpenterv1.NodeClaim{
+			nodes: []*corev1.Node{
 				{
-					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
-					Status: karpenterv1.NodeClaimStatus{
-						NodeName: "test-node",
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload": "test",
+							// Missing instance type label - logged but doesn't fail
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
 					},
 				},
 			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
 			setup: func(mockClient *test.MockClient) {
-				// Create a ready node with matching instance type
-				node := &corev1.Node{
+				// Mock status update calls - multiple updates expected (false then true)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			disableNodeAutoProvisioning: false, // NAP enabled
+			expectedReady:               true,  // Current behavior: succeeds despite warning
+			expectedError:               false,
+		},
+		{
+			name: "NAP enabled - Should succeed but log warning when node has mismatched instance type label",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
+				},
+			},
+			nodes: []*corev1.Node{
+				{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-node",
+						Name: "node-1",
 						Labels: map[string]string{
-							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
+							"workload":                     "test",
+							corev1.LabelInstanceTypeStable: "Standard_NC6s_v3", // Wrong instance type - logged but doesn't fail
 						},
 					},
 					Status: corev1.NodeStatus{
 						Conditions: []corev1.NodeCondition{
-							{
-								Type:   corev1.NodeReady,
-								Status: corev1.ConditionTrue,
-							},
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
 						},
 					},
-				}
-				mockClient.CreateOrUpdateObjectInMap(node)
-				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
+				},
 			},
-			expectedNodes: 1,
-			expectedError: false,
+			nodeClaims: []*karpenterv1.NodeClaim{},
+			setup: func(mockClient *test.MockClient) {
+				// Mock status update calls - multiple updates expected (false then true)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			disableNodeAutoProvisioning: false, // NAP enabled
+			expectedReady:               true,  // Current behavior: succeeds despite warning
+			expectedError:               false,
+		},
+		{
+			name: "NAP enabled - Should succeed when node has correct instance type label",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
+				},
+			},
+			nodes: []*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload":                     "test",
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3", // Correct instance type
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
+					},
+				},
+			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
+			setup: func(mockClient *test.MockClient) {
+				// Mock status update calls for success condition
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			disableNodeAutoProvisioning: false, // NAP enabled
+			expectedReady:               true,
+			expectedError:               false,
+		},
+		{
+			name: "NAP disabled - Should succeed even when node missing instance type label",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType: "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{
+						MatchLabels: map[string]string{
+							"workload": "test",
+						},
+					},
+				},
+				Status: kaitov1beta1.WorkspaceStatus{
+					TargetNodeCount: 1,
+				},
+			},
+			nodes: []*corev1.Node{
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "node-1",
+						Labels: map[string]string{
+							"workload": "test",
+							// Missing instance type label - should be OK when NAP is disabled
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{Type: corev1.NodeReady, Status: corev1.ConditionTrue},
+						},
+					},
+				},
+			},
+			nodeClaims: []*karpenterv1.NodeClaim{},
+			setup: func(mockClient *test.MockClient) {
+				// Mock status update calls for success condition
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			disableNodeAutoProvisioning: true, // NAP disabled
+			expectedReady:               true,
+			expectedError:               false,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			// Setup feature gate
+			originalValue := featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning]
+			featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = tt.disableNodeAutoProvisioning
+			defer func() {
+				featuregates.FeatureGates[consts.FeatureFlagDisableNodeAutoProvisioning] = originalValue
+			}()
+
 			mockClient := test.NewClient()
 			tt.setup(mockClient)
 
-			manager := NewNodeManager(mockClient)
-			nodes, err := manager.getReadyNodesFromNodeClaims(context.Background(), tt.workspace, tt.readyNodeClaims)
+			// Default to empty slices if not specified
+			nodes := tt.nodes
+			if nodes == nil {
+				nodes = []*corev1.Node{}
+			}
+			nodeClaims := tt.nodeClaims
+			if nodeClaims == nil {
+				nodeClaims = []*karpenterv1.NodeClaim{}
+			}
+
+			manager := NewNodeManager(mockClient)
+			ready, err := manager.EnsureNodesReady(context.Background(), tt.workspace, nodes, nodeClaims)
+
+			assert.Equal(t, tt.expectedReady, ready)
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestGetReadyNodesFromNodeClaims(t *testing.T) {
+	tests := []struct {
+		name            string
+		workspace       *kaitov1beta1.Workspace
+		readyNodeClaims []*karpenterv1.NodeClaim
+		setup           func(*test.MockClient)
+		expectedNodes   int
+		expectedError   bool
+	}{
+		{
+			name: "Should return error when NodeClaim without assigned node",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			readyNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "", // No node assigned
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// No setup needed - will skip NodeClaim without NodeName
+			},
+			expectedNodes: 0,
+			expectedError: false,
+		},
+		{
+			name: "Should return error when fail to get node",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			readyNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "nonexistent-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Mock Get to return NotFound error
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(apierrors.NewNotFound(schema.GroupResource{Resource: "nodes"}, "nonexistent-node"))
+			},
+			expectedNodes: 0,
+			expectedError: true,
+		},
+		{
+			name: "Should return error when node is not ready",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			readyNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a node that is not ready
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionFalse,
+							},
+						},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedNodes: 0,
+			expectedError: false,
+		},
+		{
+			name: "Should return ready node with matching instance type",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Resource: kaitov1beta1.ResourceSpec{
+					InstanceType:  "Standard_NC12s_v3",
+					LabelSelector: &metav1.LabelSelector{},
+				},
+			},
+			readyNodeClaims: []*karpenterv1.NodeClaim{
+				{
+					ObjectMeta: metav1.ObjectMeta{Name: "test-nodeclaim"},
+					Status: karpenterv1.NodeClaimStatus{
+						NodeName: "test-node",
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				// Create a ready node with matching instance type
+				node := &corev1.Node{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-node",
+						Labels: map[string]string{
+							corev1.LabelInstanceTypeStable: "Standard_NC12s_v3",
+						},
+					},
+					Status: corev1.NodeStatus{
+						Conditions: []corev1.NodeCondition{
+							{
+								Type:   corev1.NodeReady,
+								Status: corev1.ConditionTrue,
+							},
+						},
+					},
+				}
+				mockClient.CreateOrUpdateObjectInMap(node)
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
+			},
+			expectedNodes: 1,
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			manager := NewNodeManager(mockClient)
+			nodes, err := manager.getReadyNodesFromNodeClaims(context.Background(), tt.workspace, tt.readyNodeClaims)
 
 			assert.Len(t, nodes, tt.expectedNodes)
 			if tt.expectedError {
@@ -622,3 +1453,479 @@ func TestGetReadyNodesFromNodeClaims(t *testing.T) {
 		})
 	}
 }
+
+func TestPropagateOwnedConditions(t *testing.T) {
+	tests := []struct {
+		name              string
+		workspace         *kaitov1beta1.Workspace
+		condition         kaitov1beta1.ConditionType
+		conditionTypes    []kaitov1beta1.ConditionType
+		setup             func(*test.MockClient)
+		expectedError     bool
+		expectedCondition metav1.ConditionStatus
+	}{
+		{
+			name: "Should set condition to false when first owned condition is false",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:    string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status:  metav1.ConditionFalse,
+							Reason:  "NodeClaimNotReady",
+							Message: "Not enough NodeClaims are ready",
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			condition: kaitov1beta1.ConditionTypeResourceStatus,
+			conditionTypes: []kaitov1beta1.ConditionType{
+				kaitov1beta1.ConditionTypeNodeClaimStatus,
+				kaitov1beta1.ConditionTypeNodeStatus,
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError:     false,
+			expectedCondition: metav1.ConditionFalse,
+		},
+		{
+			name: "Should not update condition when all owned conditions are true",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			condition: kaitov1beta1.ConditionTypeResourceStatus,
+			conditionTypes: []kaitov1beta1.ConditionType{
+				kaitov1beta1.ConditionTypeNodeClaimStatus,
+				kaitov1beta1.ConditionTypeNodeStatus,
+			},
+			setup:         func(mockClient *test.MockClient) {},
+			expectedError: false,
+		},
+		{
+			name: "Should handle missing owned conditions gracefully",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			condition: kaitov1beta1.ConditionTypeResourceStatus,
+			conditionTypes: []kaitov1beta1.ConditionType{
+				kaitov1beta1.ConditionTypeNodeClaimStatus, // Missing
+				kaitov1beta1.ConditionTypeNodeStatus,
+			},
+			setup:         func(mockClient *test.MockClient) {},
+			expectedError: false,
+		},
+		{
+			name: "Should return error when status update fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:    string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status:  metav1.ConditionFalse,
+							Reason:  "NodeClaimNotReady",
+							Message: "Not enough NodeClaims are ready",
+						},
+					},
+				},
+			},
+			condition: kaitov1beta1.ConditionTypeResourceStatus,
+			conditionTypes: []kaitov1beta1.ConditionType{
+				kaitov1beta1.ConditionTypeNodeClaimStatus,
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("status update failed"))
+			},
+			expectedError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			manager := NewNodeManager(mockClient)
+			_, err := manager.VerifyOwnedConditions(context.Background(), tt.workspace, tt.condition, tt.conditionTypes)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestSetResourceReadyCondition_SetsToTrue(t *testing.T) {
+	tests := []struct {
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		setup         func(*test.MockClient)
+		expectedError bool
+	}{
+		{
+			name: "Should set ResourceReady condition to true when all owned conditions are true",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+
+				// Mock status update - verify ResourceReady condition is set to True
+				mockClient.StatusMock.On("Update", mock.Anything, mock.MatchedBy(func(ws *kaitov1beta1.Workspace) bool {
+					// Find the ResourceStatus condition and verify it's set to True
+					for _, condition := range ws.Status.Conditions {
+						if condition.Type == string(kaitov1beta1.ConditionTypeResourceStatus) {
+							return condition.Status == metav1.ConditionTrue && condition.Reason == "workspaceResourceStatusSuccess"
+						}
+					}
+					return false
+				}), mock.Anything).Return(nil)
+			},
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			manager := NewNodeManager(mockClient)
+			err := manager.SetResourceReadyConditionByStatus(context.Background(), tt.workspace)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestSetResourceReadyCondition(t *testing.T) {
+	tests := []struct {
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		setup         func(*test.MockClient)
+		expectedError bool
+	}{
+		{
+			name: "Should set resource condition to true when all owned conditions are true",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should set resource condition to false when NodeClaim condition is false",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:    string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status:  metav1.ConditionFalse,
+							Reason:  "NodeClaimNotReady",
+							Message: "Not enough NodeClaims are ready",
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should set resource condition to false when Node condition is false",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:    string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status:  metav1.ConditionFalse,
+							Reason:  "NodeNotReady",
+							Message: "Not enough nodes are ready",
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should set resource condition to false when NodePlugin condition is false",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should return error when propagateOwnedConditions fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:    string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status:  metav1.ConditionFalse,
+							Reason:  "NodeNotReady",
+							Message: "Not enough nodes are ready",
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("status update failed"))
+			},
+			expectedError: true,
+		},
+		{
+			name: "Should return error when final status update fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					Conditions: []metav1.Condition{
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeClaimStatus),
+							Status: metav1.ConditionTrue,
+						},
+						{
+							Type:   string(kaitov1beta1.ConditionTypeNodeStatus),
+							Status: metav1.ConditionTrue,
+						},
+					},
+				},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("final status update failed"))
+			},
+			expectedError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			manager := NewNodeManager(mockClient)
+			err := manager.SetResourceReadyConditionByStatus(context.Background(), tt.workspace)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestUpdateWorkerNodesInStatus(t *testing.T) {
+	tests := []struct {
+		name          string
+		workspace     *kaitov1beta1.Workspace
+		readyNodes    []corev1.Node
+		setup         func(*test.MockClient)
+		expectedError bool
+	}{
+		{
+			name: "Should update worker nodes when list has changed",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{"old-node-1"},
+				},
+			},
+			readyNodes: []corev1.Node{
+				{ObjectMeta: metav1.ObjectMeta{Name: "new-node-1"}},
+				{ObjectMeta: metav1.ObjectMeta{Name: "new-node-2"}},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should not update when worker nodes list is unchanged",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{"node-1", "node-2"},
+				},
+			},
+			readyNodes: []corev1.Node{
+				{ObjectMeta: metav1.ObjectMeta{Name: "node-1"}},
+				{ObjectMeta: metav1.ObjectMeta{Name: "node-2"}},
+			},
+			setup:         func(mockClient *test.MockClient) {},
+			expectedError: false,
+		},
+		{
+			name: "Should handle empty node list",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{"old-node-1"},
+				},
+			},
+			readyNodes: []corev1.Node{},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe().Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe().Maybe()
+			},
+			expectedError: false,
+		},
+		{
+			name: "Should return error when status update fails",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{"old-node-1"},
+				},
+			},
+			readyNodes: []corev1.Node{
+				{ObjectMeta: metav1.ObjectMeta{Name: "new-node-1"}},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				mockClient.StatusMock.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(errors.New("status update failed"))
+			},
+			expectedError: true,
+		},
+		{
+			name: "Should sort node names alphabetically",
+			workspace: &kaitov1beta1.Workspace{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-workspace", Namespace: "default"},
+				Status: kaitov1beta1.WorkspaceStatus{
+					WorkerNodes: []string{},
+				},
+			},
+			readyNodes: []corev1.Node{
+				{ObjectMeta: metav1.ObjectMeta{Name: "node-z"}},
+				{ObjectMeta: metav1.ObjectMeta{Name: "node-a"}},
+				{ObjectMeta: metav1.ObjectMeta{Name: "node-m"}},
+			},
+			setup: func(mockClient *test.MockClient) {
+				mockClient.On("Get", mock.Anything, mock.Anything, mock.IsType(&kaitov1beta1.Workspace{}), mock.Anything).Return(nil).Maybe()
+				// Expect the update to be called with sorted node names
+				mockClient.StatusMock.On("Update", mock.Anything, mock.MatchedBy(func(ws *kaitov1beta1.Workspace) bool {
+					expectedNodes := []string{"node-a", "node-m", "node-z"}
+					return reflect.DeepEqual(ws.Status.WorkerNodes, expectedNodes)
+				}), mock.Anything).Return(nil)
+			},
+			expectedError: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mockClient := test.NewClient()
+			tt.setup(mockClient)
+
+			manager := NewNodeManager(mockClient)
+			// Convert []Node to []*Node
+			nodePointers := make([]*corev1.Node, len(tt.readyNodes))
+			for i := range tt.readyNodes {
+				nodePointers[i] = &tt.readyNodes[i]
+			}
+			err := manager.UpdateWorkerNodesInStatus(context.Background(), tt.workspace, nodePointers)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
diff --git a/presets/ragengine/tests/api/test_main.py b/presets/ragengine/tests/api/test_main.py
index 9f837d1..039d7a4 100644
--- a/presets/ragengine/tests/api/test_main.py
+++ b/presets/ragengine/tests/api/test_main.py
@@ -15,12 +15,14 @@
 import json
 import os
 import re
+import time
 from unittest.mock import patch
 
 import httpx
 import pytest
 import respx
 
+import ragengine
 from ragengine.config import DEFAULT_VECTOR_DB_PERSIST_DIR
 
 AUTO_GEN_DOC_ID_LEN = 64
@@ -63,78 +65,17 @@ async def test_index_documents_success(async_client):
 @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 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": [
-            {"text": "This is a test document"},
-            {"text": "Another test document"},
-        ],
-    }
-
-    response = await async_client.post("/index", json=request_data)
-    assert response.status_code == 200
-
-    # Query Request
-    request_data = {
-        "index_name": "test_index",
-        "query": "test query",
-        "top_k": 1,
-        "llm_params": {"temperature": 0.7},
-    }
-
-    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
+async def test_document_update_success(mock_get, async_client, monkeypatch):
+    monkeypatch.setattr(
+        ragengine.config,
+        "LLM_INFERENCE_URL",
+        "http://localhost:5000/v1/chat/completions",
     )
-    assert response.json()["source_nodes"][0]["metadata"] == {}
-
-    response = await async_client.get("/metrics")
-    assert response.status_code == 200
-    assert (
-        len(
-            re.findall(
-                r'rag_index_requests_total{status="success"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-    assert (
-        len(
-            re.findall(
-                r'rag_query_requests_total{status="success"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
+    monkeypatch.setattr(
+        ragengine.inference.inference,
+        "LLM_INFERENCE_URL",
+        "http://localhost:5000/v1/chat/completions",
     )
-
-
-@pytest.mark.asyncio
-@respx.mock
-@patch("requests.get")  # Mock the requests.get call for fetching model metadata
-async def test_document_update_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 = {
@@ -142,8 +83,24 @@ async def test_document_update_success(mock_get, async_client):
     }
 
     # 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(
+    mock_response = {
+        "id": "chatcmpl-test123",
+        "object": "chat.completion",
+        "created": int(time.time()),
+        "model": "mock-model",
+        "choices": [
+            {
+                "index": 0,
+                "message": {
+                    "role": "assistant",
+                    "content": "This is a helpful response about the test document.",
+                },
+                "finish_reason": "stop",
+            }
+        ],
+        "usage": {"prompt_tokens": 25, "completion_tokens": 12, "total_tokens": 37},
+    }
+    respx.post("http://localhost:5000/v1/chat/completions").mock(
         return_value=httpx.Response(200, json=mock_response)
     )
 
@@ -183,21 +140,22 @@ async def test_document_update_success(mock_get, async_client):
     )
     assert response.json()["unchanged_documents"][0]["text"] == doc1["text"]
 
-    # Query Request
-    request_data = {
+    # Test chat completion request
+    chat_request = {
         "index_name": "test_update_index",
-        "query": "updates test query",
-        "top_k": 1,
-        "llm_params": {"temperature": 0.7},
+        "model": "mock-model",
+        "messages": [{"role": "user", "content": "updates test query"}],
+        "temperature": 0.7,
+        "max_tokens": 50,
     }
 
-    response = await async_client.post("/query", json=request_data)
+    response = await async_client.post("/v1/chat/completions", json=chat_request)
+    print(response.json())
     assert response.status_code == 200
-    assert (
-        response.json()["response"]
-        == "{'result': 'This is the completion from the API'}"
-    )
-    assert len(response.json()["source_nodes"]) == 1
+
+    response_data = response.json()
+    assert "source_nodes" in response_data
+    assert len(response_data["source_nodes"]) == 2
     assert (
         response.json()["source_nodes"][0]["text"] == "This is an updated test document"
     )
@@ -205,7 +163,6 @@ async def test_document_update_success(mock_get, async_client):
         0.48061275482177734, rel=1e-6
     )
     assert response.json()["source_nodes"][0]["metadata"] == {}
-
     assert respx.calls.call_count == 1
 
     response = await async_client.get("/metrics")
@@ -222,7 +179,7 @@ async def test_document_update_success(mock_get, async_client):
     assert (
         len(
             re.findall(
-                r'rag_query_requests_total{status="success"} ([1-9]\d*).0',
+                r'rag_chat_requests_total{status="success"} ([1-9]\d*).0',
                 response.text,
             )
         )
@@ -302,257 +259,6 @@ async def test_document_delete_success(async_client):
     )
 
 
-@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.
-
-    This test ensures the following:
-    1. The custom reranker returns a relevance-sorted list of documents.
-    2. The query response matches the expected format and contains the correct top results.
-
-    Template for reranker input:
-    A list of documents is shown below. Each document has a number next to it along with a summary of the document.
-    A question is also provided. Respond with the numbers of the documents you should consult to answer the question,
-    in order of relevance, as well as the relevance score. The relevance score is a number from 1-10 based on how
-    relevant you think the document is to the question. Do not include any documents that are not relevant.
-
-    Example format:
-    Document 1: <summary of document 1>
-    Document 2: <summary of document 2>
-    ...
-    Document 10: <summary of document 10>
-
-    Question: <question>
-    Answer:
-    Doc: 9, Relevance: 7
-    Doc: 3, Relevance: 4
-    Doc: 7, Relevance: 3
-    """
-    # 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 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 = [
-        "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
-    index_response = response.json()
-
-    # 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 == 200
-    query_response = response.json()
-
-    # Validate query response
-    assert len(query_response["source_nodes"]) == top_n
-
-    # Validate each source node in the query response
-    expected_source_nodes = [
-        {
-            "text": "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.",
-            "score": 10.0,
-            "metadata": {},
-            "doc_id": index_response[3]["doc_id"],
-        },
-        {
-            "text": "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.",
-            "score": 10.0,
-            "metadata": {},
-            "doc_id": index_response[4]["doc_id"],
-        },
-    ]
-    for i, expected_node in enumerate(expected_source_nodes):
-        actual_node = query_response["source_nodes"][i]
-        assert actual_node["text"] == expected_node["text"]
-        assert actual_node["score"] == expected_node["score"]
-        assert actual_node["metadata"] == expected_node["metadata"]
-        assert actual_node["doc_id"] == expected_node["doc_id"]
-
-    # Ensure HTTPX requests were made
-    assert respx.calls.call_count == 2  # One for rerank, one for query completion
-
-    response = await async_client.get("/metrics")
-    assert response.status_code == 200
-    assert (
-        len(
-            re.findall(
-                r'rag_index_requests_total{status="success"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-    assert (
-        len(
-            re.findall(
-                r'rag_query_requests_total{status="success"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-
-
-@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"}]}
-
-    # 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
-
-    response = await async_client.get("/metrics")
-    assert response.status_code == 200
-    assert (
-        len(
-            re.findall(
-                r'rag_index_requests_total{status="success"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-    assert (
-        len(
-            re.findall(
-                r'rag_query_requests_total{status="failure"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-
-
-@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
-        "query": "test query",
-        "top_k": 1,
-        "llm_params": {"temperature": 0.7},
-    }
-
-    response = await async_client.post("/query", json=request_data)
-    assert response.status_code == 404
-    assert response.json()["detail"] == "No such index: 'non_existent_index' exists."
-
-    response = await async_client.get("/metrics")
-    assert response.status_code == 200
-    assert (
-        len(
-            re.findall(
-                r'rag_query_requests_total{status="failure"} ([1-9]\d*).0',
-                response.text,
-            )
-        )
-        == 1
-    )
-
-
 @pytest.mark.asyncio
 async def test_list_documents_in_index_success(async_client):
     index_name = "test_index"
@@ -861,36 +567,3 @@ async def test_delete_index(async_client):
         )
         == 1
     )
-
-
-"""
-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. 
-Upon execution, RAG results should be observed.
-
-def test_live_query_test():
-    # Index
-    request_data = {
-        "index_name": "test_index",
-        "documents": [
-            {"text": "Polar bear – can lift 450Kg (approximately 0.7 times their body weight) \
-                Adult male polar bears can grow to be anywhere between 300 and 700kg"},
-            {"text": "Giraffes are the tallest mammals and are well-adapted to living in trees. \
-                They have few predators as adults."}
-        ]
-    }
-
-    response = client.post("/index", json=request_data)
-    assert response.status_code == 200
-
-    # Query
-    request_data = {
-        "index_name": "test_index",
-        "query": "What is the strongest bear?",
-        "top_k": 1,
-        "llm_params": {"temperature": 0.7}
-    }
-
-    response = client.post("/query", json=request_data)
-    assert response.status_code == 200
-"""
diff --git a/presets/ragengine/tests/vector_store/test_base_store.py b/presets/ragengine/tests/vector_store/test_base_store.py
index 60f3d82..e4b437d 100644
--- a/presets/ragengine/tests/vector_store/test_base_store.py
+++ b/presets/ragengine/tests/vector_store/test_base_store.py
@@ -89,47 +89,6 @@ class BaseVectorStoreTest(ABC):
         """Abstract method to check indexed documents in backend-specific format."""
         pass
 
-    @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_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"}),
-        ]
-        index_doc_resp = await vector_store_manager.index_documents(
-            "test_index", documents
-        )
-
-        params = {"temperature": 0.7}
-        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 query_result["source_nodes"][0]["doc_id"] == index_doc_resp[0]
-        assert (
-            respx.calls.call_count == 1
-        )  # Ensure only one LLM inference request was made
-
     @pytest.mark.asyncio
     @respx.mock
     @patch("requests.get")
@@ -429,15 +388,20 @@ func main() {}"""
             ids[0],
         )
 
-        # Check if the document was updated
-        result = await vector_store_manager.query(
-            "test_index",
-            "Updated Fifth document",
-            top_k=1,
-            llm_params={},
-            rerank_params={},
+        chat_results = await vector_store_manager.chat_completion(
+            {
+                "index_name": "test_index",
+                "model": "mock-model",
+                "messages": [
+                    {"role": "user", "content": "What is the first document?"}
+                ],
+                "temperature": 0.7,
+                "max_tokens": 100,
+            }
         )
-        assert result["source_nodes"][0]["text"] == "Updated Fifth document"
+
+        assert chat_results.source_nodes is not None
+        assert chat_results.source_nodes[0].text == "Updated Fifth document"
 
         # Check documents not found case
         result = await vector_store_manager.update_documents(
diff --git a/test/e2e/preset_test.go b/test/e2e/preset_test.go
index 97000a8..db8c208 100644
--- a/test/e2e/preset_test.go
+++ b/test/e2e/preset_test.go
@@ -108,21 +108,30 @@ func createCustomWorkspaceWithAdapter(numOfNode int, validAdapters []kaitov1beta
 
 func updateCustomWorkspaceWithAdapter(workspaceObj *kaitov1beta1.Workspace, validAdapters []kaitov1beta1.AdapterSpec) *kaitov1beta1.Workspace {
 	By("Updating a workspace with adapter", func() {
-		workspaceObj.Inference.Adapters = validAdapters
+		Eventually(func() error {
+			// Get fresh copy before each update attempt
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj, &client.GetOptions{})
+			if err != nil {
+				return err
+			}
 
-		By("Updating workspace", func() {
-			Eventually(func() error {
-				return utils.TestingCluster.KubeClient.Update(ctx, workspaceObj)
-			}, utils.PollTimeout, utils.PollInterval).
-				Should(Succeed(), "Failed to update workspace %s", workspaceObj.Name)
+			// Apply changes to fresh copy
+			workspaceObj.Inference.Adapters = validAdapters
 
-			By("Validating workspace update", func() {
-				err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
-					Namespace: workspaceObj.Namespace,
-					Name:      workspaceObj.Name,
-				}, workspaceObj, &client.GetOptions{})
-				Expect(err).NotTo(HaveOccurred())
-			})
+			// Attempt update with fresh copy
+			return utils.TestingCluster.KubeClient.Update(ctx, workspaceObj)
+		}, utils.PollTimeout, utils.PollInterval).
+			Should(Succeed(), "Failed to update workspace %s", workspaceObj.Name)
+
+		By("Validating workspace update", func() {
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj, &client.GetOptions{})
+			Expect(err).NotTo(HaveOccurred())
 		})
 	})
 	return workspaceObj
@@ -371,13 +380,31 @@ func updatePhi3TuningWorkspaceWithPresetPublicMode(workspaceObj *kaitov1beta1.Wo
 }
 
 func updateAndValidateWorkspace(workspaceObj *kaitov1beta1.Workspace) {
-	By("Creating workspace", func() {
+	By("Updating workspace", func() {
+		// Capture the desired changes before we start the retry loop
+		desiredTuningInput := workspaceObj.Tuning.Input
+		desiredTuningOutput := workspaceObj.Tuning.Output
+
 		Eventually(func() error {
+			// Get fresh copy before each update attempt
+			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+				Namespace: workspaceObj.Namespace,
+				Name:      workspaceObj.Name,
+			}, workspaceObj, &client.GetOptions{})
+			if err != nil {
+				return err
+			}
+
+			// Reapply desired changes to fresh copy
+			workspaceObj.Tuning.Input = desiredTuningInput
+			workspaceObj.Tuning.Output = desiredTuningOutput
+
+			// Attempt update with fresh copy
 			return utils.TestingCluster.KubeClient.Update(ctx, workspaceObj)
 		}, utils.PollTimeout, utils.PollInterval).
-			Should(Succeed(), "Failed to create workspace %s", workspaceObj.Name)
+			Should(Succeed(), "Failed to update workspace %s", workspaceObj.Name)
 
-		By("Validating workspace creation", func() {
+		By("Validating workspace update", func() {
 			err := utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
 				Namespace: workspaceObj.Namespace,
 				Name:      workspaceObj.Name,
diff --git a/test/rage2e/rag_test.go b/test/rage2e/rag_test.go
index 46c448d..895e95c 100644
--- a/test/rage2e/rag_test.go
+++ b/test/rage2e/rag_test.go
@@ -111,9 +111,6 @@ var _ = Describe("RAGEngine", func() {
 		docID := indexDoc["doc_id"].(string)
 
 		searchQuerySuccess := "Kaito is an operator that automates 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")
-
 		err = createAndValidateQueryChatMessagesPod(ragengineObj, searchQuerySuccess, true)
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryChatMessagesPod")
 
@@ -176,10 +173,6 @@ var _ = Describe("RAGEngine", func() {
 		Expect(indexDoc["text"]).NotTo(BeNil(), "Index document text should not be nil")
 		docID := indexDoc["doc_id"].(string)
 
-		searchQuerySuccess := "Kaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster"
-		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")
@@ -243,10 +236,6 @@ var _ = Describe("RAGEngine", func() {
 		Expect(indexDoc["text"]).NotTo(BeNil(), "Index document text should not be nil")
 		docID := indexDoc["doc_id"].(string)
 
-		searchQuerySuccess := "Kaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster"
-		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")
@@ -809,46 +798,6 @@ func createAndValidateDeleteIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) erro
 	return err
 }
 
-func createAndValidateQueryPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSearchQueries string, remote bool) error {
-	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.ObjectMeta.Name + `:80/query \
--H "Content-Type: application/json" \
--d '{
-	"index_name": "kaito",
-	"model": "HuggingFaceH4/zephyr-7b-beta",
-    "query": "what is kaito?",
-    "llm_params": {
-      "max_tokens": 50,
-      "temperature": 0
-    }
-}'`
-	} else {
-		curlCommand = `curl -X POST ` + ragengineObj.ObjectMeta.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
-    }
-}'`
-	}
-	opts := PodValidationOptions{
-		PodName:            fmt.Sprintf("query-pod-%s", utils.GenerateRandomString()),
-		CurlCommand:        curlCommand,
-		Namespace:          ragengineObj.ObjectMeta.Namespace,
-		ExpectedLogContent: expectedSearchQueries,
-		WaitForRunning:     true,
-		ParseJSONResponse:  false,
-	}
-	_, err := createAndValidateAPIPod(ragengineObj, opts)
-	return err
-}
-
 func createAndValidateQueryChatMessagesPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSearchQueries string, remote bool) error {
 	var curlCommand string
 	// Note: Request without model specified should still succeed with vLLM. As model name is dynamically fetched.