Repository: kaito-project/kaito

Issue:
kaito cli
cli that makes it easy to interact with kaito and models

Hints from issue comments:
@chewong: https://github.com/kaito-project/kaito-kubectl-plugin

Solution patch:
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 786b343..35081b5 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -60,6 +60,7 @@ jobs:
       env:
         ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }}
         ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
+        ALGOLIA_SITE_VERIFICATION: ${{ secrets.ALGOLIA_SITE_VERIFICATION }}
 
     - name: Deploy to GitHub Pages
       uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0
diff --git a/.github/workflows/ragengine-e2e-workflow.yml b/.github/workflows/ragengine-e2e-workflow.yml
index 4e965d6..0834339 100644
--- a/.github/workflows/ragengine-e2e-workflow.yml
+++ b/.github/workflows/ragengine-e2e-workflow.yml
@@ -120,6 +120,21 @@ jobs:
           AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
           RAGENGINE_SERVICE_IMG_TAG: ${{ env.VERSION }}
 
+      - name: Create node pool for RAG workloads
+        uses: azure/CLI@v2.1.0
+        with:
+          inlineScript: |
+            az aks nodepool add \
+              --resource-group "${{ env.CLUSTER_NAME }}" \
+              --cluster-name "${{ env.CLUSTER_NAME }}" \
+              --name ragpool \
+              -s Standard_D8_v3 \
+              -c 1 \
+              --labels apps=phi-3
+        env:
+          AZURE_RESOURCE_GROUP: ${{ env.CLUSTER_NAME }}
+          AZURE_CLUSTER_NAME: ${{ env.CLUSTER_NAME }}
+
       - name: Run e2e test
         run: |
           AI_MODELS_REGISTRY=$E2E_ACR_AMRT_USERNAME.azurecr.io \
diff --git a/README.md b/README.md
index ab0ea61..a17d93c 100644
--- a/README.md
+++ b/README.md
@@ -113,12 +113,12 @@ $ kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -s
 }
 
 # make an inference call using the model id (phi-3.5-mini-instruct) from previous step
-$ kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/completions \
+$ kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/chat/completions \
   -H "Content-Type: application/json" \
   -d '{
     "model": "phi-3.5-mini-instruct",
-    "prompt": "What is kubernetes?",
-    "max_tokens": 7,
+    "messages": [{"role": "user", "content": "What is kubernetes?"}],
+    "max_tokens": 50,
     "temperature": 0
   }'
 ```
diff --git a/api/v1alpha1/ragengine_types.go b/api/v1alpha1/ragengine_types.go
index f60b982..827ebc5 100644
--- a/api/v1alpha1/ragengine_types.go
+++ b/api/v1alpha1/ragengine_types.go
@@ -99,10 +99,11 @@ type RAGEngineStatus struct {
 // RAGEngine is the Schema for the ragengine API
 // +kubebuilder:object:root=true
 // +kubebuilder:subresource:status
-// +kubebuilder:resource:path=ragengines,scope=Namespaced,categories=ragengine
+// +kubebuilder:resource:path=ragengines,scope=Namespaced,categories=ragengine,shortName=rag
 // +kubebuilder:storageversion
 // +kubebuilder:printcolumn:name="Instance",type="string",JSONPath=".spec.compute.instanceType",description=""
 // +kubebuilder:printcolumn:name="ResourceReady",type="string",JSONPath=".status.conditions[?(@.type==\"ResourceReady\")].status",description=""
+// +kubebuilder:printcolumn:name="ServiceReady",type="string",JSONPath=".status.conditions[?(@.type==\"ServiceReady\")].status",description=""
 // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
 type RAGEngine struct {
 	metav1.TypeMeta   `json:",inline"`
diff --git a/charts/kaito/ragengine/crds/kaito.sh_ragengines.yaml b/charts/kaito/ragengine/crds/kaito.sh_ragengines.yaml
index ff7e1cc..72c6a4d 100644
--- a/charts/kaito/ragengine/crds/kaito.sh_ragengines.yaml
+++ b/charts/kaito/ragengine/crds/kaito.sh_ragengines.yaml
@@ -13,6 +13,8 @@ spec:
     kind: RAGEngine
     listKind: RAGEngineList
     plural: ragengines
+    shortNames:
+    - rag
     singular: ragengine
   scope: Namespaced
   versions:
@@ -23,6 +25,9 @@ spec:
     - jsonPath: .status.conditions[?(@.type=="ResourceReady")].status
       name: ResourceReady
       type: string
+    - jsonPath: .status.conditions[?(@.type=="ServiceReady")].status
+      name: ServiceReady
+      type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
diff --git a/config/crd/bases/kaito.sh_ragengines.yaml b/config/crd/bases/kaito.sh_ragengines.yaml
index ff7e1cc..72c6a4d 100644
--- a/config/crd/bases/kaito.sh_ragengines.yaml
+++ b/config/crd/bases/kaito.sh_ragengines.yaml
@@ -13,6 +13,8 @@ spec:
     kind: RAGEngine
     listKind: RAGEngineList
     plural: ragengines
+    shortNames:
+    - rag
     singular: ragengine
   scope: Namespaced
   versions:
@@ -23,6 +25,9 @@ spec:
     - jsonPath: .status.conditions[?(@.type=="ResourceReady")].status
       name: ResourceReady
       type: string
+    - jsonPath: .status.conditions[?(@.type=="ServiceReady")].status
+      name: ServiceReady
+      type: string
     - jsonPath: .metadata.creationTimestamp
       name: Age
       type: date
diff --git a/pkg/ragengine/controllers/preset-rag.go b/pkg/ragengine/controllers/preset-rag.go
index ed1cf0c..608fe21 100644
--- a/pkg/ragengine/controllers/preset-rag.go
+++ b/pkg/ragengine/controllers/preset-rag.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/kaito-project/kaito/api/v1alpha1"
 	"github.com/kaito-project/kaito/pkg/ragengine/manifests"
+	"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/resources"
@@ -116,15 +117,20 @@ func CreatePresetRAG(ctx context.Context, ragEngineObj *v1alpha1.RAGEngine, revi
 
 	if ragEngineObj.Spec.Embedding.Local != nil {
 		var skuNumGPUs int
-		gpuConfig, err := utils.GetGPUConfigBySKU(ragEngineObj.Spec.Compute.InstanceType)
-		if err != nil {
-			gpuConfig, err = utils.TryGetGPUConfigFromNode(ctx, kubeClient, ragEngineObj.Status.WorkerNodes)
-			if err != nil {
-				skuNumGPUs = 1
-			}
+		var gpuConfig *sku.GPUConfig = nil
+		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
+			}
 		}
 
 		resourceReq = corev1.ResourceRequirements{
diff --git a/website/docs/proposals/20250630-kaito-cli.md b/website/docs/proposals/20250630-kaito-cli.md
new file mode 100644
index 0000000..365e784
--- /dev/null
+++ b/website/docs/proposals/20250630-kaito-cli.md
@@ -0,0 +1,368 @@
+---
+title: Kaito Kubectl CLI Plugin
+authors:
+  - "@helayoty"
+creation-date: 2025-06-30
+last-updated: 2025-07-29
+status: proposal
+see-also:
+---
+
+## Summary
+
+Kaito CLI provides a user-friendly kubectl extension for deploying and managing KAITO workspaces on Kubernetes. Kaito kubectl plugin aims to reduce friction for data scientists and ML engineers by abstracting away error-prone YAML authoring and enabling streamlined model deployment workflows. The initial focus is on core deployment and inference scenarios, with potential expansion to a standalone CLI if advanced features beyond kubectl's scope are needed.
+
+## Motivation
+
+Currently, users interact with KAITO by manually crafting Kubernetes YAML manifests. This process is error-prone, unfriendly to non-Kubernetes experts, and slows down experimentation and deployment. As KAITO adoption grows, there is a need for a more accessible, robust, and automation-friendly interface that:
+
+- Reduces the learning curve for new users
+- Minimizes configuration errors
+- Accelerates model deployment and iteration
+- Supports both single-cluster and multi-cluster workflows
+- Integrates with CI/CD pipelines and platform automation
+
+### Goals
+
+- Provide a user-friendly kubectl plugin for deploying and managing KAITO workspaces
+- Enable rapid model deployment with minimal YAML configuration
+- Support core inference scenarios and workspace management
+- Integrate seamlessly with existing Kubernetes authentication and RBAC
+- Facilitate both interactive use and CI/CD automation
+
+### Non-Goals/Future Work
+
+- Replacing or deprecating existing YAML workflows
+- Implementing a web-based GUI (focus is CLI/terminal UX)
+- Changing the underlying KAITO resource model or API
+
+## Solution Design
+
+### Kubectl Plugin
+
+A new kubectl plugin distributed through [Krew](https://github.com/kubernetes-sigs/krew) for Kaito that provides seamless integration with existing Kubernetes workflows.
+
+Designed for engineers already comfortable with native Kubernetes tooling. By distributing the plugin through [Krew](https://github.com/kubernetes-sigs/krew), users avoid installing a separate binary, inherit their existing kube-contexts and RBAC, and benefit from familiar `kubectl` verb–noun syntax.
+
+## Core Features and Commands
+
+### Deploy Command
+
+The **deploy** command is the primary entry point for KAITO workspace management, covering both inference and tuning scenarios.
+
+#### Inference Deployment
+
+```bash
+# Basic inference deployment with preset model
+kubectl kaito deploy --workspace-name my-workspace --model phi-3.5-mini-instruct
+
+# Deploy with specific instance type and node count
+kubectl kaito deploy --workspace-name my-workspace --model llama-3.1-8b-instruct \
+  --instance-type Standard_NC96ads_A100_v4 --count 2
+
+# Deploy with model access secret for gated models
+kubectl kaito deploy --workspace-name my-workspace --model llama-3.3-70b-instruct \
+  --model-access-secret hf-token
+
+# Deploy with single adapter
+kubectl kaito deploy --workspace-name my-workspace --model phi-3-mini-128k-instruct \
+  --adapter phi-3-adapter="myregistry.azurecr.io/adapters/phi-3-adapter:v1",strength=1.0
+
+# Deploy with multiple adapters
+kubectl kaito deploy --workspace-name my-workspace --model phi-3-mini-128k-instruct \
+  --adapter adapter1="myregistry.azurecr.io/adapters/adapter1:v1",strength=0.8 \
+  --adapter adapter2="myregistry.azurecr.io/adapters/adapter2:v1",strength=1.0 \
+  --adapter adapter3="myregistry.azurecr.io/adapters/adapter3:v1",strength=0.5
+
+# Deploy with custom inference configuration
+kubectl kaito deploy --workspace-name my-workspace --model deepseek-r1-distill-qwen-14b \
+  --inference-config ds-inference-params
+
+# Deploy with specific node preferences  
+kubectl kaito deploy --workspace-name my-workspace --model mistral-7b-instruct \
+  --preferred-nodes node1,node2 --instance-type Standard_NC24ads_A100_v4
+
+# Deploy with label selector for node targeting
+kubectl kaito deploy --workspace-name my-workspace --model qwen2.5-coder-7b-instruct \
+  --label-selector apps=qwen-2-5-coder
+
+# Deploy with special annotations for testing
+kubectl kaito deploy --workspace-name my-workspace --model falcon-7b-instruct \
+  --bypass-resource-checks --enable-load-balancer
+
+# Deploy with dry-run to generate YAML manifest
+kubectl kaito deploy --workspace-name my-workspace --model phi-4-mini-instruct --dry-run
+```
+
+#### Tuning Deployment
+
+```bash
+# Deploy QLoRA tuning with URL input and image output
+kubectl kaito deploy --workspace-name workspace-tuning-phi-3 --model phi-3-mini-128k-instruct \
+  --tuning --tuning-method qlora \
+  --input-urls "https://huggingface.co/datasets/philschmid/dolly-15k-oai-style/resolve/main/data/train-00000-of-00001-54e3756291ca09c6.parquet?download=true" \
+  --output-image "myregistry.azurecr.io/adapters/phi-3-tuned:v1" \
+  --output-image-push-secret my-registry-secret
+
+# Deploy QLoRA tuning with custom configuration
+kubectl kaito deploy --workspace-name workspace-tuning-falcon-7b --model falcon-7b \
+  --tuning --tuning-method qlora --tuning-config qlora-params-template \
+  --input-urls "https://huggingface.co/datasets/philschmid/dolly-15k-oai-style/resolve/main/data/train-00000-of-00001-54e3756291ca09c6.parquet?download=true" \
+  --output-image "myregistry.azurecr.io/adapters/falcon-7b-tuned:v1" \
+  --output-image-push-secret my-registry-secret
+
+# Deploy tuning with PVC volume sources
+kubectl kaito deploy --workspace-name workspace-tuning-phi-3-pvc --model phi-3-mini-128k-instruct \
+  --tuning --tuning-method qlora \
+  --input-pvc pvc-azurefile-input \
+  --output-pvc pvc-azurefile-output \
+  --instance-type Standard_NC6s_v3
+
+# Deploy tuning with private model image
+kubectl kaito deploy --workspace-name workspace-tuning-falcon-7b-private --model falcon-7b \
+  --tuning --tuning-method qlora --tuning-config my-qlora-params \
+  --model-access-mode private \
+  --model-image "aimodelsregistry.azurecr.io/kaito-falcon-7b:0.0.4" \
+  --input-urls "https://huggingface.co/datasets/philschmid/dolly-15k-oai-style/resolve/main/data/train-00000-of-00001-54e3756291ca09c6.parquet?download=true" \
+  --output-image "myregistry.azurecr.io/adapters/falcon-7b-private:v1" \
+  --output-image-push-secret my-registry-secret
+```
+
+#### Supporting Commands
+
+```bash
+# Check workspace status with detailed information
+kubectl kaito status --workspace-name my-workspace
+kubectl kaito status --workspace-name my-workspace --show-conditions
+kubectl kaito status --workspace-name my-workspace --show-worker-nodes
+
+# Get workspace inference endpoint
+kubectl kaito get-endpoint --workspace-name my-workspace
+
+# Interactive chat with deployed model
+kubectl kaito chat --workspace-name my-workspace
+kubectl kaito chat --workspace-name my-workspace --system-prompt "You are a helpful coding assistant"
+kubectl kaito chat --workspace-name my-workspace --temperature 0.7 --max-tokens 2048
+
+# List supported models
+kubectl kaito models list
+kubectl kaito models list --type text-generation
+kubectl kaito models describe phi-3.5-mini-instruct
+kubectl kaito models describe llama-3.1-8b-instruct --show-runtime-info
+```
+
+### New Core Commands
+
+#### Get-Endpoint Command
+
+The `get-endpoint` command retrieves the inference service endpoint for a deployed workspace, making it easy to programmatically access the model API.
+
+```bash
+# Get endpoint URL
+kubectl kaito get-endpoint --workspace-name my-workspace
+# Output: http://workspace-my-workspace.default.svc.cluster.local:80/chat/completions
+
+# Get external endpoint when LoadBalancer is enabled
+kubectl kaito get-endpoint --workspace-name my-workspace --external
+# Output: http://20.123.45.67:80/chat/completions
+```
+
+#### Chat Command
+
+The `chat` command provides an interactive chat interface with deployed models, similar to Ollama's CLI experience.
+
+```bash
+# Start basic interactive chat session
+kubectl kaito chat --workspace-name my-workspace
+
+# Chat with custom system prompt
+kubectl kaito chat --workspace-name my-workspace --system-prompt "You are a helpful coding assistant specialized in Python"
+
+# Chat with inference parameters
+kubectl kaito chat --workspace-name my-workspace --temperature 0.7 --max-tokens 2048 --top-p 0.9
+
+# Single question mode (non-interactive)
+kubectl kaito chat --workspace-name my-workspace --message "Explain machine learning in simple terms"
+
+# Chat with custom API parameters
+kubectl kaito chat --workspace-name my-workspace --stream=false --echo=true
+```
+
+**Interactive Chat Session Example:**
+
+```
+$ kubectl kaito chat --workspace-name code-assistant
+Connected to workspace: code-assistant (model: phi-3.5-mini-instruct)
+Type /help for commands or /quit to exit.
+
+>>> Write a Python function to calculate fibonacci numbers
+def fibonacci(n):
+    if n <= 1:
+        return n
+    return fibonacci(n-1) + fibonacci(n-2)
+
+>>> Can you optimize this for better performance?
+Here's an optimized version using dynamic programming:
+
+def fibonacci_optimized(n):
+    if n <= 1:
+        return n
+    
+    a, b = 0, 1
+    for _ in range(2, n + 1):
+        a, b = b, a + b
+    return b
+
+>>> /help
+Available commands:
+  /help        - Show this help message
+  /quit        - Exit the chat session
+  /clear       - Clear the conversation history
+  /model       - Show current model information
+  /params      - Show current inference parameters
+  /set <param> <value> - Set inference parameter (temperature, max_tokens, etc.)
+
+>>> /quit
+Chat session ended.
+```
+
+### Complete Workflow Examples
+
+#### Deploy Phi-3.5 for Code Generation
+
+```bash
+# 1. List available models to find code-generation models
+kubectl kaito models list --type text-generation
+# Output shows: phi-3.5-mini-instruct, qwen2.5-coder-7b-instruct, etc.
+
+# 2. Get details about the model
+kubectl kaito models describe phi-3.5-mini-instruct
+# Shows: runtime=tfs, version, tag, etc.
+
+# 3. Deploy the model for inference
+kubectl kaito deploy --workspace-name workspace-phi-3-5-mini \
+  --model phi-3.5-mini-instruct \
+  --instance-type Standard_NC24ads_A100_v4 \
+  --label-selector apps=phi-3-5
+
+# 4. Check deployment status
+kubectl kaito status --workspace-name workspace-phi-3-5-mini --show-conditions
+# Shows: ResourceReady, InferenceReady conditions
+
+# 5. Retrieve workspace endpoint (once ready)
+kubectl kaito get-endpoint --workspace-name workspace-phi-3-5-mini
+# Output: http://workspace-phi-3-5-mini.default.svc.cluster.local:80/chat/completions
+
+# 6. Test the model with interactive chat
+kubectl kaito chat --workspace-name workspace-phi-3-5-mini --system-prompt "You are a code generation assistant"
+```
+
+#### Fine-tune Phi-3 with QLoRA
+
+```bash
+# 1. Deploy QLoRA tuning job with Dolly dataset
+kubectl kaito deploy --workspace-name workspace-tuning-phi-3 \
+  --model phi-3-mini-128k-instruct \
+  --tuning --tuning-method qlora \
+  --input-urls "https://huggingface.co/datasets/philschmid/dolly-15k-oai-style/resolve/main/data/train-00000-of-00001-54e3756291ca09c6.parquet?download=true" \
+  --output-image "myregistry.azurecr.io/adapters/phi-3-tuned:v1" \
+  --output-image-push-secret my-registry-secret \
+  --instance-type Standard_NC24ads_A100_v4 \
+  --label-selector app=tuning-phi-3
+
+# 2. Monitor tuning progress
+kubectl kaito status --workspace-name workspace-tuning-phi-3 --show-conditions
+
+# 3. Check if tuning completed successfully
+kubectl kaito status --workspace-name workspace-tuning-phi-3
+# Look for WorkspaceSucceeded condition
+
+# 4. Deploy the tuned model with adapter
+kubectl kaito deploy --workspace-name workspace-phi-3-mini-adapter \
+  --model phi-3-mini-128k-instruct \
+  --adapter phi-3-adapter="myregistry.azurecr.io/adapters/phi-3-tuned:v1",strength=1.0 \
+  --label-selector apps=phi-3-adapter
+
+# 5. Test the fine-tuned model
+kubectl kaito status --workspace-name workspace-phi-3-mini-adapter --show-conditions
+kubectl kaito get-endpoint --workspace-name workspace-phi-3-mini-adapter
+
+# 6. Chat with the fine-tuned model to test improvements
+kubectl kaito chat --workspace-name workspace-phi-3-mini-adapter --message "Generate a helpful response using the training data knowledge"
+```
+
+#### Deploy Llama-3.3 70B with Multi-Node Setup
+
+```bash
+# 1. Create secret for HuggingFace token (required for gated model)
+kubectl create secret generic hf-token --from-literal=token=hf_your_token_here
+
+# 2. Deploy the large model with multi-node configuration
+kubectl kaito deploy --workspace-name workspace-llama-3-3-70b-instruct \
+  --model llama-3.3-70b-instruct \
+  --model-access-secret hf-token \
+  --instance-type Standard_NC48ads_A100_v4 \
+  --count 2 \
+  --label-selector apps=llama-3-3-70b-instruct
+
+# 3. Check status
+kubectl kaito status --workspace-name workspace-llama-3-3-70b-instruct --show-worker-nodes
+
+# 4. Get the service endpoint once ready
+kubectl kaito get-endpoint --workspace-name workspace-llama-3-3-70b-instruct --format json
+# Output: {"url": "http://workspace-llama-3-3-70b-instruct.default.svc.cluster.local:80", "ready": true}
+
+# 5. Test the large model with a complex question
+kubectl kaito chat --workspace-name workspace-llama-3-3-70b-instruct --temperature 0.3 --max-tokens 1024
+```
+
+#### Deploy RAG Engine with Phi-3
+
+```bash
+# 1. First deploy a workspace for inference (if not using external URL)
+kubectl kaito deploy --workspace-name workspace-phi-3-inference \
+  --model phi-3.5-mini-instruct \
+  --instance-type Standard_NC6s_v3
+
+# 2. Deploy RAG engine with local embedding model
+kubectl kaito rag deploy --ragengine-name ragengine-start \
+  --embedding-model "BAAI/bge-small-en-v1.5" \
+  --inference-workspace workspace-phi-3-inference \
+  --instance-type Standard_NC6s_v3 \
+  --label-selector apps=ragengine-example
+
+# 3. Check RAG engine status
+kubectl kaito status ragengine/ragengine-start
+
+# 4. Query the RAG system
+  kubectl kaito rag query --ragengine ragengine-start --question "What is KAITO?"
+```
+
+## Technical Architecture
+
+### Implementation Architecture
+
+The kubectl plugin will be developed in a separate repository under the `kaito-project` organization on GitHub: `github.com/kaito-project/kaito-cli`.
+
+```mermaid
+graph TB
+    KubectlPlugin[kubectl kaito] --> PluginCore[Plugin Core]
+    PluginCore --> WorkspaceAPI[Workspace API v1beta1]
+    PluginCore --> ModelCatalog[Model Catalog]
+    PluginCore --> K8sClient[Kubernetes Client-go]
+    
+    ModelCatalog --> supported_models.yaml
+    WorkspaceAPI --> WorkspaceController[Workspace Controller]
+    
+    K8sClient --> KubeConfig[kubeconfig]
+    K8sClient --> RBAC[Kubernetes RBAC]
+    K8sClient --> WorkspaceCRD[Workspace CRDs]
+    
+    subgraph "KAITO Core"
+        WorkspaceController
+    end
+```
+
+## POC/Demo
+
+- [kubectl-kaito demo repository](https://github.com/helayoty/kubectl-kaito)
diff --git a/website/docs/quick-start.mdx b/website/docs/quick-start.mdx
index 3663355..11ccc5e 100644
--- a/website/docs/quick-start.mdx
+++ b/website/docs/quick-start.mdx
@@ -190,11 +190,11 @@ You should see output similar to:
 Now make an inference call using the model:
 
 ```bash
-kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/completions \
+kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/chat/completions \
   -H "Content-Type: application/json" \
   -d '{
     "model": "phi-4-mini-instruct",
-    "prompt": "What is kubernetes?",
+    "messages": [{"role": "user", "content": "What is kubernetes?"}],
     "max_tokens": 50,
     "temperature": 0
   }'
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index 6ec011a..32fa8ec 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -12,6 +12,18 @@ const config = {
     tagline: 'Kubernetes AI Toolchain Operator',
     favicon: 'img/favicon.ico',
 
+    headTags: [
+        {
+            tagName: "meta",
+            attributes: {
+                // Allow Algolia crawler to index the site
+                // See https://www.algolia.com/doc/tools/crawler/getting-started/create-crawler/#verify-your-domain.
+                name: "algolia-site-verification",
+                content: process.env.ALGOLIA_SITE_VERIFICATION || "DUMMY_SITE_VERIFICATION",
+            }
+        },
+    ],
+
     // Set the production url of your site here
     url: 'https://kaito-project.github.io',
     // Set the /<baseUrl>/ pathname under which your site is served
diff --git a/website/versioned_docs/version-v0.5.0/quick-start.md b/website/versioned_docs/version-v0.5.0/quick-start.md
index eb681c7..db63531 100644
--- a/website/versioned_docs/version-v0.5.0/quick-start.md
+++ b/website/versioned_docs/version-v0.5.0/quick-start.md
@@ -86,11 +86,11 @@ You should see output similar to:
 Now make an inference call using the model:
 
 ```bash
-kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/completions \
+kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/chat/completions \
   -H "Content-Type: application/json" \
   -d '{
     "model": "phi-3.5-mini-instruct",
-    "prompt": "What is kubernetes?",
+    "messages": [{"role": "user", "content": "What is kubernetes?"}],
     "max_tokens": 50,
     "temperature": 0
   }'
diff --git a/website/versioned_docs/version-v0.5.1/quick-start.md b/website/versioned_docs/version-v0.5.1/quick-start.md
index eb681c7..db63531 100644
--- a/website/versioned_docs/version-v0.5.1/quick-start.md
+++ b/website/versioned_docs/version-v0.5.1/quick-start.md
@@ -86,11 +86,11 @@ You should see output similar to:
 Now make an inference call using the model:
 
 ```bash
-kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/completions \
+kubectl run -it --rm --restart=Never curl --image=curlimages/curl -- curl -X POST http://$CLUSTERIP/v1/chat/completions \
   -H "Content-Type: application/json" \
   -d '{
     "model": "phi-3.5-mini-instruct",
-    "prompt": "What is kubernetes?",
+    "messages": [{"role": "user", "content": "What is kubernetes?"}],
     "max_tokens": 50,
     "temperature": 0
   }'



Test patch:
diff --git a/pkg/ragengine/controllers/preset-rag_test.go b/pkg/ragengine/controllers/preset-rag_test.go
index ca6acec..d24453b 100644
--- a/pkg/ragengine/controllers/preset-rag_test.go
+++ b/pkg/ragengine/controllers/preset-rag_test.go
@@ -20,8 +20,11 @@ import (
 
 	"github.com/stretchr/testify/mock"
 	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
 
+	"github.com/kaito-project/kaito/api/v1alpha1"
 	"github.com/kaito-project/kaito/pkg/utils/consts"
+	"github.com/kaito-project/kaito/pkg/utils/resources"
 	"github.com/kaito-project/kaito/pkg/utils/test"
 )
 
@@ -93,3 +96,67 @@ func TestCreatePresetRAG(t *testing.T) {
 		})
 	}
 }
+
+func TestGPUConfigLogic(t *testing.T) {
+	test.RegisterTestModel()
+
+	testcases := map[string]struct {
+		nodeCount        int
+		callMocks        func(c *test.MockClient)
+		ragEngine        *v1alpha1.RAGEngine
+		expectedGPUReq   int64
+		expectedLimitReq int64
+	}{
+		"test-rag-model": {
+			callMocks: func(c *test.MockClient) {
+				c.On("Create", mock.IsType(context.TODO()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
+			},
+			ragEngine:        test.MockRAGEngineWithPreset,
+			expectedGPUReq:   int64(2),
+			expectedLimitReq: int64(2),
+		},
+		"test-rag-preferred-nodes": {
+			callMocks: func(c *test.MockClient) {
+				c.On("Create", mock.IsType(context.TODO()), mock.IsType(&appsv1.Deployment{}), mock.Anything).Return(nil)
+			},
+			ragEngine:        test.MockRAGEngineWithPresetPreferredCPUNodes,
+			expectedGPUReq:   int64(0),
+			expectedLimitReq: int64(0),
+		},
+	}
+
+	for k, tc := range testcases {
+		t.Run(k, func(t *testing.T) {
+			t.Setenv("CLOUD_PROVIDER", consts.AzureCloudName)
+
+			mockClient := test.NewClient()
+			tc.callMocks(mockClient)
+
+			ragEngineObj := tc.ragEngine
+			createdObject, _ := CreatePresetRAG(context.TODO(), ragEngineObj, "1", mockClient)
+
+			resourceReq := createdObject.(*appsv1.Deployment).Spec.Template.Spec.Containers[0].Resources
+
+			gpuRequests, exists := resourceReq.Requests[corev1.ResourceName(resources.CapacityNvidiaGPU)]
+			if !exists {
+				t.Errorf("GPU requests not found in resource requirements")
+				return
+			}
+
+			gpuLimits, exists := resourceReq.Limits[corev1.ResourceName(resources.CapacityNvidiaGPU)]
+			if !exists {
+				t.Errorf("GPU limits not found in resource requirements")
+				return
+			}
+
+			gpuRequestCount := gpuRequests.Value()
+			gpuLimitCount := gpuLimits.Value()
+			if gpuRequestCount != tc.expectedGPUReq {
+				t.Errorf("%s: GPU request count is not expected, got %d, expected %d", k, gpuRequestCount, tc.expectedGPUReq)
+			}
+			if gpuLimitCount != tc.expectedLimitReq {
+				t.Errorf("%s: GPU limit count is not expected, got %d, expected %d", k, gpuLimitCount, tc.expectedLimitReq)
+			}
+		})
+	}
+}
diff --git a/pkg/utils/test/testUtils.go b/pkg/utils/test/testUtils.go
index a2deaf7..d036df6 100644
--- a/pkg/utils/test/testUtils.go
+++ b/pkg/utils/test/testUtils.go
@@ -331,6 +331,33 @@ var (
 			},
 		},
 	}
+	MockRAGEngineWithPresetPreferredCPUNodes = &v1alpha1.RAGEngine{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "testRAGEngine",
+			Namespace: "kaito",
+			UID:       "test-uid",
+		},
+		Spec: &v1alpha1.RAGEngineSpec{
+			Compute: &v1alpha1.ResourceSpec{
+				Count:          &gpuNodeCount,
+				PreferredNodes: []string{"aks-ragpool-00000000-vmss000000"},
+				InstanceType:   "Standard_NC24ads_A100_v4",
+				LabelSelector: &metav1.LabelSelector{
+					MatchLabels: map[string]string{
+						"ragengine.kaito.io/name": "testRAGEngine",
+					},
+				},
+			},
+			Embedding: &v1alpha1.EmbeddingSpec{
+				Local: &v1alpha1.LocalEmbeddingSpec{
+					ModelID: "BAAI/bge-small-en-v1.5",
+				},
+			},
+			InferenceService: &v1alpha1.InferenceServiceSpec{
+				URL: "http://localhost:5000/chat",
+			},
+		},
+	}
 	MockRAGEngineWithRevision1 = &v1alpha1.RAGEngine{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        "testRAGEngine",
diff --git a/test/rage2e/rag_test.go b/test/rage2e/rag_test.go
index d1aae84..d85dc7b 100644
--- a/test/rage2e/rag_test.go
+++ b/test/rage2e/rag_test.go
@@ -102,7 +102,7 @@ var _ = Describe("RAGEngine", func() {
 		validateInferenceandRAGResource(ragengineObj.ObjectMeta, int32(numOfReplica), false)
 		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.RAGEngineConditionTypeSucceeded), "ragengine to be ready")
 
-		indexDoc, err := createAndValidateIndexPod(ragengineObj)
+		indexDoc, err := createAndValidateIndexPod(ragengineObj, "index-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate IndexPod")
 		Expect(indexDoc).NotTo(BeNil(), "Index document should not be nil")
 		Expect(indexDoc["doc_id"]).NotTo(BeNil(), "Index document ID should not be nil")
@@ -110,24 +110,24 @@ var _ = Describe("RAGEngine", func() {
 		docID := indexDoc["doc_id"].(string)
 
 		searchQuerySuccess := "\\n\\nKaito is an operator that is designed to automate the AI/ML model inference or tuning workload in a Kubernetes cluster."
-		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, true)
+		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, true, "query-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryPod")
 
 		persistLogSuccess := "Successfully persisted index kaito"
-		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess)
+		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess, "persist-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate PersistPod")
 
 		loadLogSuccess := "Successfully loaded index kaito"
-		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess)
+		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess, "load-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate LoadPod")
 
-		err = createAndValidateUpdateDocumentPod(ragengineObj, docID)
+		err = createAndValidateUpdateDocumentPod(ragengineObj, docID, "update-document-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate UpdateDocumentPod")
 
-		err = createAndValidateDeleteDocumentPod(ragengineObj, docID)
+		err = createAndValidateDeleteDocumentPod(ragengineObj, docID, "delete-document-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteDocumentPod")
 
-		err = createAndValidateDeleteIndexPod(ragengineObj)
+		err = createAndValidateDeleteIndexPod(ragengineObj, "delete-index-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteIndexPod")
 	})
 
@@ -165,7 +165,7 @@ var _ = Describe("RAGEngine", func() {
 		validateInferenceandRAGResource(ragengineObj.ObjectMeta, int32(numOfReplica), false)
 		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.RAGEngineConditionTypeSucceeded), "ragengine to be ready")
 
-		indexDoc, err := createAndValidateIndexPod(ragengineObj)
+		indexDoc, err := createAndValidateIndexPod(ragengineObj, "index-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate IndexPod")
 		Expect(indexDoc).NotTo(BeNil(), "Index document should not be nil")
 		Expect(indexDoc["doc_id"]).NotTo(BeNil(), "Index document ID should not be nil")
@@ -173,24 +173,91 @@ var _ = Describe("RAGEngine", func() {
 		docID := indexDoc["doc_id"].(string)
 
 		searchQuerySuccess := "\\nKaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster.\\n\\n\\n"
-		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, false)
+		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, false, "query-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryPod")
 
 		persistLogSuccess := "Successfully persisted index kaito"
-		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess)
+		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess, "persist-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate PersistPod")
 
 		loadLogSuccess := "Successfully loaded index kaito"
-		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess)
+		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess, "load-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate LoadPod")
 
-		err = createAndValidateUpdateDocumentPod(ragengineObj, docID)
+		err = createAndValidateUpdateDocumentPod(ragengineObj, docID, "update-document-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate UpdateDocumentPod")
 
-		err = createAndValidateDeleteDocumentPod(ragengineObj, docID)
+		err = createAndValidateDeleteDocumentPod(ragengineObj, docID, "delete-document-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteDocumentPod")
 
-		err = createAndValidateDeleteIndexPod(ragengineObj)
+		err = createAndValidateDeleteIndexPod(ragengineObj, "delete-index-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteIndexPod")
+	})
+
+	It("should create RAG with preferred nodes and kaito VLLM workspace successfully", utils.GinkgoLabelFastCheck, func() {
+		numOfReplica := 1
+		workspaceObj := createPhi3WorkspaceWithPresetPublicModeAndVLLM(numOfReplica)
+
+		time.Sleep(30 * time.Second)
+
+		validateWorkspaceResourceStatus(workspaceObj)
+
+		validateAssociatedService(workspaceObj.ObjectMeta)
+
+		validateInferenceandRAGResource(workspaceObj.ObjectMeta, int32(numOfReplica), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+
+		serviceName := workspaceObj.Name
+		serviceNamespace := workspaceObj.Namespace
+		service := &v1.Service{}
+
+		_ = utils.TestingCluster.KubeClient.Get(ctx, client.ObjectKey{
+			Namespace: serviceNamespace,
+			Name:      serviceName,
+		}, service)
+
+		clusterIP := service.Spec.ClusterIP
+
+		preferredNode, err := getRagPoolNode()
+		Expect(err).NotTo(HaveOccurred(), "Failed to get ragpool node")
+
+		// Get the preferred node from ragpool
+		ragengineObj := createLocalPreferredNodesRAGEngine(clusterIP, preferredNode)
+
+		defer cleanupResources(workspaceObj, ragengineObj)
+
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.ConditionTypeResourceStatus), "ragengineObj resource status to be ready")
+		validateAssociatedService(ragengineObj.ObjectMeta)
+		validateInferenceandRAGResource(ragengineObj.ObjectMeta, int32(numOfReplica), false)
+		validateRAGEngineCondition(ragengineObj, string(kaitov1alpha1.RAGEngineConditionTypeSucceeded), "ragengine to be ready")
+
+		indexDoc, err := createAndValidateIndexPod(ragengineObj, "index-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate IndexPod")
+		Expect(indexDoc).NotTo(BeNil(), "Index document should not be nil")
+		Expect(indexDoc["doc_id"]).NotTo(BeNil(), "Index document ID should not be nil")
+		Expect(indexDoc["text"]).NotTo(BeNil(), "Index document text should not be nil")
+		docID := indexDoc["doc_id"].(string)
+
+		searchQuerySuccess := "\\nKaito is an operator that automates the AI/ML model inference or tuning workload in a Kubernetes cluster.\\n\\n\\n"
+		err = createAndValidateQueryPod(ragengineObj, searchQuerySuccess, false, "query-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate QueryPod")
+
+		persistLogSuccess := "Successfully persisted index kaito"
+		err = createAndValidatePersistPod(ragengineObj, persistLogSuccess, "persist-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate PersistPod")
+
+		loadLogSuccess := "Successfully loaded index kaito"
+		err = createAndValidateLoadPod(ragengineObj, loadLogSuccess, "load-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate LoadPod")
+
+		err = createAndValidateUpdateDocumentPod(ragengineObj, docID, "update-document-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate UpdateDocumentPod")
+
+		err = createAndValidateDeleteDocumentPod(ragengineObj, docID, "delete-document-pod")
+		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteDocumentPod")
+
+		err = createAndValidateDeleteIndexPod(ragengineObj, "delete-index-pod")
 		Expect(err).NotTo(HaveOccurred(), "Failed to create and validate DeleteIndexPod")
 	})
 
@@ -257,6 +324,27 @@ func GenerateLocalEmbeddingRAGEngineManifest(name, namespace, instanceType, embe
 	}
 }
 
+func GenerateLocalEmbeddingRAGEngineManifestWithPreferredNodes(name, namespace, preferredNodes, embeddingModelID string, labelSelector *metav1.LabelSelector, inferenceSpec *kaitov1alpha1.InferenceServiceSpec) *kaitov1alpha1.RAGEngine {
+	return &kaitov1alpha1.RAGEngine{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      name,
+			Namespace: namespace,
+		},
+		Spec: &kaitov1alpha1.RAGEngineSpec{
+			Compute: &kaitov1alpha1.ResourceSpec{
+				PreferredNodes: []string{preferredNodes},
+				LabelSelector:  labelSelector,
+			},
+			Embedding: &kaitov1alpha1.EmbeddingSpec{
+				Local: &kaitov1alpha1.LocalEmbeddingSpec{
+					ModelID: embeddingModelID,
+				},
+			},
+			InferenceService: inferenceSpec,
+		},
+	}
+}
+
 // validateWorkspaceReadiness validates workspace readiness
 func validateWorkspaceReadiness(workspaceObj *kaitov1beta1.Workspace) {
 	By("Checking the workspace status is ready", func() {
@@ -298,6 +386,25 @@ func createLocalEmbeddingKaitoVLLMRAGEngine(baseURL string) *kaitov1alpha1.RAGEn
 	return ragEngineObj
 }
 
+func createLocalPreferredNodesRAGEngine(baseURL, preferredNode string) *kaitov1alpha1.RAGEngine {
+	ragEngineObj := &kaitov1alpha1.RAGEngine{}
+	serviceURL := fmt.Sprintf("http://%s/v1/completions", baseURL)
+	By("Creating RAG with localembedding and kaito vllm inference", func() {
+		uniqueID := fmt.Sprint("rag-", rand.Intn(1000))
+		ragEngineObj = GenerateLocalEmbeddingRAGEngineManifestWithPreferredNodes(uniqueID, namespaceName, preferredNode, "BAAI/bge-small-en-v1.5",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"apps": "phi-3"},
+			},
+			&kaitov1alpha1.InferenceServiceSpec{
+				URL: serviceURL,
+			},
+		)
+
+		createAndValidateRAGEngine(ragEngineObj)
+	})
+	return ragEngineObj
+}
+
 func createLocalEmbeddingHFURLRAGEngine() *kaitov1alpha1.RAGEngine {
 	ragEngineObj := &kaitov1alpha1.RAGEngine{}
 	hfURL := "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta/v1/completions"
@@ -516,9 +623,8 @@ func deleteWorkspace(workspaceObj *kaitov1beta1.Workspace) error {
 	return nil
 }
 
-func createAndValidateIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) (map[string]any, error) {
+func createAndValidateIndexPod(ragengineObj *kaitov1alpha1.RAGEngine, podName string) (map[string]any, error) {
 	indexResp := []map[string]any{}
-	podName := "index-pod"
 	By("Creating index pod", func() {
 		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/index \
 -H "Content-Type: application/json" \
@@ -582,8 +688,7 @@ func createAndValidateIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) (map[strin
 	return indexResp[0], nil
 }
 
-func createAndValidateUpdateDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, docID string) error {
-	podName := "update-document-pod"
+func createAndValidateUpdateDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, docID string, podName string) error {
 	By("Creating update document pod", func() {
 		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/indexes/kaito/documents \
 -H "Content-Type: application/json" \
@@ -626,8 +731,7 @@ func createAndValidateUpdateDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, d
 	return nil
 }
 
-func createAndValidateDeleteDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, docID string) error {
-	podName := "delete-document-pod"
+func createAndValidateDeleteDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, docID string, podName string) error {
 	By("Creating delete document pod", func() {
 		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/indexes/kaito/documents/delete \
 -H "Content-Type: application/json" \
@@ -662,8 +766,7 @@ func createAndValidateDeleteDocumentPod(ragengineObj *kaitov1alpha1.RAGEngine, d
 	return nil
 }
 
-func createAndValidateDeleteIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) error {
-	podName := "delete-index-pod"
+func createAndValidateDeleteIndexPod(ragengineObj *kaitov1alpha1.RAGEngine, podName string) error {
 	By("Creating delete index pod", func() {
 		curlCommand := `curl -X DELETE ` + ragengineObj.Name + `:80/indexes/kaito \
 -H "Content-Type: application/json"`
@@ -697,8 +800,7 @@ func createAndValidateDeleteIndexPod(ragengineObj *kaitov1alpha1.RAGEngine) erro
 	return nil
 }
 
-func createAndValidateQueryPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSearchQueries string, remote bool) error {
-	podName := "query-pod"
+func createAndValidateQueryPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSearchQueries string, remote bool, podName string) error {
 	By("Creating query pod", func() {
 		var curlCommand string
 		// Note: Request without model specified should still succeed with vLLM. As model name is dynamically fetched.
@@ -766,8 +868,7 @@ func createAndValidateQueryPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedSe
 	return nil
 }
 
-func createAndValidatePersistPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedPersistResult string) error {
-	podName := "persist-pod"
+func createAndValidatePersistPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedPersistResult string, podName string) error {
 	By("Creating Persist pod", func() {
 		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/persist/kaito`
 		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
@@ -810,8 +911,7 @@ func createAndValidatePersistPod(ragengineObj *kaitov1alpha1.RAGEngine, expected
 	return nil
 }
 
-func createAndValidateLoadPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedLoadResult string) error {
-	podName := "load-pod"
+func createAndValidateLoadPod(ragengineObj *kaitov1alpha1.RAGEngine, expectedLoadResult string, podName string) error {
 	By("Creating Load Pod", func() {
 		curlCommand := `curl -X POST ` + ragengineObj.Name + `:80/load/kaito?overwrite=True`
 		pod := GenerateCURLPodManifest(podName, curlCommand, ragengineObj.Namespace)
@@ -899,3 +999,19 @@ func createAndValidateSecret() {
 		})
 	})
 }
+
+func getRagPoolNode() (string, error) {
+	nodeList := &v1.NodeList{}
+	err := utils.TestingCluster.KubeClient.List(ctx, nodeList)
+	if err != nil {
+		return "", fmt.Errorf("failed to list nodes: %v", err)
+	}
+
+	for _, node := range nodeList.Items {
+		if strings.Contains(node.Name, "ragpool") {
+			return node.Name, nil
+		}
+	}
+
+	return "", fmt.Errorf("no node containing 'ragpool' found")
+}