Repository: kaito-project/kaito

Issue:
Add Gemma 3 models as KAITO Presets
**Is your feature request related to a problem? Please describe.**

- https://huggingface.co/google/gemma-3-4b-it
- https://huggingface.co/google/gemma-3-27b-it 

<img width="478" height="100" alt="Image" src="https://github.com/user-attachments/assets/3926b070-138a-4beb-a323-c582bbfdd61a" />

https://kaito-z6a6575.slack.com/archives/C085UN12YEN/p1751167484821239

**Describe the solution you'd like**

**Describe alternatives you've considered**

**Additional context**

Discussion:
@icebelly29: hey @chewong, i'd love to be on this@chewong: @icebelly29 if you could start off with a quick proposal (template: https://github.com/kaito-project/kaito/blob/main/website/docs/proposals/YYYYMMDD-model-template.md), that'd be great!

Solution patch:
diff --git a/.github/workflows/aikit.yaml b/.github/workflows/aikit.yaml
index 85e1020..289eac6 100644
--- a/.github/workflows/aikit.yaml
+++ b/.github/workflows/aikit.yaml
@@ -25,7 +25,7 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
 
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index 447a064..13823bd 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -22,7 +22,7 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml
index 571b5d3..5878075 100644
--- a/.github/workflows/create-release.yaml
+++ b/.github/workflows/create-release.yaml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml
index 6d40337..497279e 100644
--- a/.github/workflows/dependency-review.yaml
+++ b/.github/workflows/dependency-review.yaml
@@ -17,7 +17,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml
index 23cbfe2..0f61d82 100644
--- a/.github/workflows/deploy-docs.yaml
+++ b/.github/workflows/deploy-docs.yaml
@@ -31,7 +31,7 @@ jobs:
     - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 
     - name: Harden Runner
-      uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911
+      uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a
       with:
         disable-sudo: true
         egress-policy: block
diff --git a/.github/workflows/docs-versioning.yaml b/.github/workflows/docs-versioning.yaml
index fe69360..3dc0df1 100644
--- a/.github/workflows/docs-versioning.yaml
+++ b/.github/workflows/docs-versioning.yaml
@@ -21,7 +21,7 @@ jobs:
         working-directory: website
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
 
diff --git a/.github/workflows/e2e-workflow.yaml b/.github/workflows/e2e-workflow.yaml
index ab954c8..5387e30 100644
--- a/.github/workflows/e2e-workflow.yaml
+++ b/.github/workflows/e2e-workflow.yaml
@@ -44,7 +44,7 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/license-header.yaml b/.github/workflows/license-header.yaml
index 21be500..e8d7fa9 100644
--- a/.github/workflows/license-header.yaml
+++ b/.github/workflows/license-header.yaml
@@ -30,7 +30,7 @@ jobs:
             boilerplate_file: 'hack/boilerplate.python.txt'
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/lint-go.yaml b/.github/workflows/lint-go.yaml
index b96e79b..3c7c08d 100644
--- a/.github/workflows/lint-go.yaml
+++ b/.github/workflows/lint-go.yaml
@@ -21,7 +21,7 @@ jobs:
       contents: read
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/markdown-link-check.yaml b/.github/workflows/markdown-link-check.yaml
index 5e71850..4c19124 100644
--- a/.github/workflows/markdown-link-check.yaml
+++ b/.github/workflows/markdown-link-check.yaml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/pr-title-lint.yaml b/.github/workflows/pr-title-lint.yaml
index 795d4ff..27aff20 100644
--- a/.github/workflows/pr-title-lint.yaml
+++ b/.github/workflows/pr-title-lint.yaml
@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/publish-rag-controller-gh-image.yaml b/.github/workflows/publish-rag-controller-gh-image.yaml
index 1ede09e..944c7cd 100644
--- a/.github/workflows/publish-rag-controller-gh-image.yaml
+++ b/.github/workflows/publish-rag-controller-gh-image.yaml
@@ -33,7 +33,7 @@ jobs:
           echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/publish-rag-service-gh-image.yaml b/.github/workflows/publish-rag-service-gh-image.yaml
index f3f7ff1..74cf905 100644
--- a/.github/workflows/publish-rag-service-gh-image.yaml
+++ b/.github/workflows/publish-rag-service-gh-image.yaml
@@ -33,7 +33,7 @@ jobs:
           echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/publish-ragengine-helm-chart.yaml b/.github/workflows/publish-ragengine-helm-chart.yaml
index dd99d21..74beefa 100644
--- a/.github/workflows/publish-ragengine-helm-chart.yaml
+++ b/.github/workflows/publish-ragengine-helm-chart.yaml
@@ -22,7 +22,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/publish-workspace-gh-image.yaml b/.github/workflows/publish-workspace-gh-image.yaml
index bf78c78..c7714a7 100644
--- a/.github/workflows/publish-workspace-gh-image.yaml
+++ b/.github/workflows/publish-workspace-gh-image.yaml
@@ -33,7 +33,7 @@ jobs:
             echo "tag=$(echo ${{ github.event.inputs.release_version }})" >> $GITHUB_OUTPUT
 
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/publish-workspace-helm-chart.yaml b/.github/workflows/publish-workspace-helm-chart.yaml
index 408d07a..ac5fa9f 100644
--- a/.github/workflows/publish-workspace-helm-chart.yaml
+++ b/.github/workflows/publish-workspace-helm-chart.yaml
@@ -22,7 +22,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/ragengine-e2e-workflow.yaml b/.github/workflows/ragengine-e2e-workflow.yaml
index dbab59c..9c5a801 100644
--- a/.github/workflows/ragengine-e2e-workflow.yaml
+++ b/.github/workflows/ragengine-e2e-workflow.yaml
@@ -41,7 +41,7 @@ jobs:
 
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/unit-tests-ragengine.yaml b/.github/workflows/unit-tests-ragengine.yaml
index 40cc856..3949500 100644
--- a/.github/workflows/unit-tests-ragengine.yaml
+++ b/.github/workflows/unit-tests-ragengine.yaml
@@ -25,7 +25,7 @@ jobs:
     environment: unit-tests
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml
index c461a1f..6507a38 100644
--- a/.github/workflows/unit-tests.yaml
+++ b/.github/workflows/unit-tests.yaml
@@ -21,7 +21,7 @@ jobs:
     environment: unit-tests
     steps:
       - name: Harden Runner
-        uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
+        uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
         with:
           egress-policy: audit
           disable-sudo: true
diff --git a/charts/kaito/workspace/templates/supported-models-configmap.yaml b/charts/kaito/workspace/templates/supported-models-configmap.yaml
index 2c816c4..93b8038 100644
--- a/charts/kaito/workspace/templates/supported-models-configmap.yaml
+++ b/charts/kaito/workspace/templates/supported-models-configmap.yaml
@@ -124,6 +124,18 @@ data:
         version: https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Llama-8B/commit/6a6f4aa4197940add57724a7707d069478df56b1
         runtime: tfs
         tag: 0.2.0
+      - name: gemma-3-4b-instruct
+        type: text-generation
+        version: https://huggingface.co/google/gemma-3-4b-it/commit/093f9f388b31de276ce2de164bdc2081324b9767
+        runtime: tfs
+        downloadAtRuntime: true
+        tag: 0.0.1
+      - name: gemma-3-27b-instruct
+        type: text-generation
+        version: https://huggingface.co/google/gemma-3-27b-it/commit/005ad3404e59d6023443cb575daa05336842228a
+        runtime: tfs
+        downloadAtRuntime: true
+        tag: 0.0.1
       - name: gpt-oss-20b
         type: text-generation
         version: https://huggingface.co/openai/gpt-oss-20b/commit/db6852caddef0cf0c1f729034ed700c84eab376e
diff --git a/cmd/workspace/models.go b/cmd/workspace/models.go
index 73cc007..8004163 100644
--- a/cmd/workspace/models.go
+++ b/cmd/workspace/models.go
@@ -17,6 +17,7 @@ import (
 	_ "github.com/kaito-project/kaito/presets/workspace/models"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/deepseek"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/falcon"
+	_ "github.com/kaito-project/kaito/presets/workspace/models/gemma3"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/gpt"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/llama3"
 	_ "github.com/kaito-project/kaito/presets/workspace/models/mistral"
diff --git a/examples/inference/kaito_workspace_gemma_3_27b_instruct.yaml b/examples/inference/kaito_workspace_gemma_3_27b_instruct.yaml
new file mode 100644
index 0000000..47bf4f3
--- /dev/null
+++ b/examples/inference/kaito_workspace_gemma_3_27b_instruct.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1beta1
+kind: Workspace
+metadata:
+  name: workspace-gemma-3-27b-instruct
+resource:
+  instanceType: "Standard_NC24ads_A100_v4"  # A100 required for 27B model
+  labelSelector:
+    matchLabels:
+      apps: gemma-3-27b-instruct
+inference:
+  preset:
+    name: "gemma-3-27b-instruct"
+    presetOptions:
+      modelAccessSecret: hf-token
\ No newline at end of file
diff --git a/examples/inference/kaito_workspace_gemma_3_4b_instruct.yaml b/examples/inference/kaito_workspace_gemma_3_4b_instruct.yaml
new file mode 100644
index 0000000..e827fe6
--- /dev/null
+++ b/examples/inference/kaito_workspace_gemma_3_4b_instruct.yaml
@@ -0,0 +1,14 @@
+apiVersion: kaito.sh/v1beta1
+kind: Workspace
+metadata:
+  name: workspace-gemma-3-4b-instruct
+resource:
+  instanceType: "Standard_NV36ads_A10_v5"  # A10 sufficient for 4B model
+  labelSelector:
+    matchLabels:
+      apps: gemma-3-4b-instruct
+inference:
+  preset:
+    name: "gemma-3-4b-instruct"
+    presetOptions:
+      modelAccessSecret: hf-token
\ No newline at end of file
diff --git a/presets/ragengine/main.py b/presets/ragengine/main.py
index 71303df..fef6656 100644
--- a/presets/ragengine/main.py
+++ b/presets/ragengine/main.py
@@ -510,15 +510,13 @@ async def list_documents_in_index(
 
         # Decode the index_name in case it was URL-encoded by the client
         decoded_index_name = unquote(index_name)
-        documents = await rag_ops.list_documents_in_index(
+        result = await rag_ops.list_documents_in_index(
             index_name=decoded_index_name,
             limit=limit,
             offset=offset,
             max_text_length=max_text_length,
             metadata_filter=metadata_filter,
         )
-
-        result = ListDocumentsResponse(documents=documents, count=len(documents))
         status = STATUS_SUCCESS
         return result
     except HTTPException as http_exc:
diff --git a/presets/ragengine/models.py b/presets/ragengine/models.py
index 7996077..ccb97c3 100644
--- a/presets/ragengine/models.py
+++ b/presets/ragengine/models.py
@@ -32,6 +32,7 @@ class Document(BaseModel):
 class ListDocumentsResponse(BaseModel):
     documents: list[Document]  # List of DocumentResponses
     count: int  # Number of documents in the current response
+    total_items: int  # Total number of document with filters applied
 
 
 class IndexRequest(BaseModel):
diff --git a/presets/ragengine/vector_store/base.py b/presets/ragengine/vector_store/base.py
index 0b79b8a..9c4275d 100644
--- a/presets/ragengine/vector_store/base.py
+++ b/presets/ragengine/vector_store/base.py
@@ -46,6 +46,7 @@ from ragengine.inference.inference import Inference
 from ragengine.models import (
     ChatCompletionResponse,
     Document,
+    ListDocumentsResponse,
     input_messages_to_llamaindex_messages,
 )
 from ragengine.vector_store.node_processors.contex_selection_node_processor import (
@@ -689,10 +690,28 @@ class BaseVectorStore(ABC):
         offset: int,
         max_text_length: int | None = None,
         metadata_filter: dict[str, Any] | None = None,
-    ) -> list[dict[str, Any]]:
+    ) -> ListDocumentsResponse:
         """
         Return a dictionary of document metadata for the given index.
         """
+        if self.use_rwlock:
+            async with self.rwlock.reader_lock:
+                return await self._list_documents_in_index(
+                    index_name, limit, offset, max_text_length, metadata_filter
+                )
+
+        return await self._list_documents_in_index(
+            index_name, limit, offset, max_text_length, metadata_filter
+        )
+
+    async def _list_documents_in_index(
+        self,
+        index_name: str,
+        limit: int,
+        offset: int,
+        max_text_length: int | None = None,
+        metadata_filter: dict[str, Any] | None = None,
+    ) -> ListDocumentsResponse:
         vector_store_index = self.index_map.get(index_name)
         if not vector_store_index:
             raise HTTPException(
@@ -701,8 +720,9 @@ class BaseVectorStore(ABC):
 
         doc_store = vector_store_index.docstore
         doc_store_items = doc_store.docs.items()
+        total_count = len(doc_store_items)
         if metadata_filter is not None:
-            docs_items = await self._filter_documents(
+            docs_items, total_count = await self._filter_documents(
                 doc_store_items, metadata_filter, offset, limit
             )
         else:
@@ -718,7 +738,10 @@ class BaseVectorStore(ABC):
         )
 
         # Return list of valid documents
-        return [doc for doc in docs if isinstance(doc, dict)]
+        docs = [doc for doc in docs if isinstance(doc, dict)]
+        return ListDocumentsResponse(
+            documents=docs, count=len(docs), total_items=total_count
+        )
 
     async def delete_index(self, index_name: str):
         """Common logic for deleting an index."""
@@ -739,12 +762,15 @@ class BaseVectorStore(ABC):
         """
         Filter documents based on metadata.
         """
+        total_count = 0
         filtered_docs = []
         for doc_id, doc_stub in doc_items:
             doc_metadata = getattr(doc_stub, "metadata", {})
             if all(doc_metadata.get(k) == v for k, v in metadata_filter.items()):
-                filtered_docs.append((doc_id, doc_stub))
-        return islice(filtered_docs, offset, offset + limit)
+                if total_count >= offset and len(filtered_docs) < limit:
+                    filtered_docs.append((doc_id, doc_stub))
+                total_count += 1
+        return filtered_docs, total_count
 
     async def document_exists(
         self, index_name: str, doc: Document, doc_id: str
diff --git a/presets/ragengine/vector_store_manager/manager.py b/presets/ragengine/vector_store_manager/manager.py
index d613e1a..5832be7 100644
--- a/presets/ragengine/vector_store_manager/manager.py
+++ b/presets/ragengine/vector_store_manager/manager.py
@@ -12,9 +12,7 @@
 # limitations under the License.
 
 
-from typing import Any
-
-from ragengine.models import Document
+from ragengine.models import Document, ListDocumentsResponse
 from ragengine.vector_store.base import BaseVectorStore
 
 
@@ -54,7 +52,7 @@ class VectorStoreManager:
         offset: int,
         max_text_length: int,
         metadata_filter: dict,
-    ) -> list[dict[str, Any]]:
+    ) -> ListDocumentsResponse:
         """List all documents in index."""
         return await self.vector_store.list_documents_in_index(
             index_name, limit, offset, max_text_length, metadata_filter
diff --git a/presets/workspace/models/deepseek/README.md b/presets/workspace/models/deepseek/README.md
index 33af2f3..7e4e5c8 100644
--- a/presets/workspace/models/deepseek/README.md
+++ b/presets/workspace/models/deepseek/README.md
@@ -12,4 +12,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/falcon/README.md b/presets/workspace/models/falcon/README.md
index 24d77fc..8bd907d 100644
--- a/presets/workspace/models/falcon/README.md
+++ b/presets/workspace/models/falcon/README.md
@@ -11,7 +11,7 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
 
 ## Fine-tuning Configuration
 
diff --git a/presets/workspace/models/gemma3/README.md b/presets/workspace/models/gemma3/README.md
new file mode 100644
index 0000000..dba2b1d
--- /dev/null
+++ b/presets/workspace/models/gemma3/README.md
@@ -0,0 +1,14 @@
+## Supported Models
+
+| Model name                |                             Model source                              |                                Sample workspace                                | Kubernetes Workload | Distributed inference |
+|---------------------------|:----------------------------------------------------------------------:|:------------------------------------------------------------------------------:|:-------------------:|:---------------------:|
+| gemma-3-4b-instruct       | [gemma-3-4b-instruct](https://huggingface.co/google/gemma-3-4b-it)                | [link](../../../../examples/inference/kaito_workspace_gemma_3_4b_instruct.yaml) |     Deployment      |         false         |
+| gemma-3-27b-instruct      | [gemma-3-27b-instruct](https://huggingface.co/google/gemma-3-27b-it)               | [link](../../../../examples/inference/kaito_workspace_gemma_3_27b_instruct.yaml)|     Deployment      |         false         |
+
+## Image Source
+
+- **Public**: KAITO maintainers manage the lifecycle of the inference service images. Model weights are downloaded directly from HuggingFace at runtime.
+
+## Usage
+
+See [document](../../../../website/docs/inference.md).
\ No newline at end of file
diff --git a/presets/workspace/models/gemma3/model.go b/presets/workspace/models/gemma3/model.go
new file mode 100644
index 0000000..9d789bc
--- /dev/null
+++ b/presets/workspace/models/gemma3/model.go
@@ -0,0 +1,132 @@
+// 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 gemma3
+
+import (
+	"time"
+
+	"github.com/kaito-project/kaito/pkg/model"
+	"github.com/kaito-project/kaito/pkg/utils/plugin"
+	"github.com/kaito-project/kaito/pkg/workspace/inference"
+	metadata "github.com/kaito-project/kaito/presets/workspace/models"
+)
+
+const (
+	PresetGemma3_4BInstructModel  = "gemma-3-4b-instruct"
+	PresetGemma3_27BInstructModel = "gemma-3-27b-instruct"
+)
+
+var (
+	baseCommandPresetGemma3Inference = "accelerate launch"
+	gemma3RunParams                  = map[string]string{
+		"torch_dtype":        "auto",
+		"pipeline":           "text-generation",
+		"allow_remote_files": "",
+	}
+	gemma3RunParamsVLLM = map[string]string{
+		"dtype": "bfloat16",
+	}
+)
+
+var gemma3_4bInst gemma3_4BInstruct
+var gemma3_27bInst gemma3_27BInstruct
+
+func init() {
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetGemma3_4BInstructModel,
+		Instance: &gemma3_4bInst,
+	})
+	plugin.KaitoModelRegister.Register(&plugin.Registration{
+		Name:     PresetGemma3_27BInstructModel,
+		Instance: &gemma3_27bInst,
+	})
+}
+
+type gemma3_4BInstruct struct{}
+
+func (*gemma3_4BInstruct) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		Metadata:                metadata.MustGet(PresetGemma3_4BInstructModel),
+		DiskStorageRequirement:  "60Gi",
+		GPUCountRequirement:     "1",
+		TotalSafeTensorFileSize: "8.6Gi",
+		BytesPerToken:           348160,
+		ModelTokenLimit:         131072,
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetGemma3Inference,
+				AccelerateParams:  inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    gemma3RunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      PresetGemma3_4BInstructModel,
+				ModelRunParams: gemma3RunParamsVLLM,
+			},
+		},
+		ReadinessTimeout: time.Duration(30) * time.Minute,
+	}
+}
+
+func (*gemma3_4BInstruct) GetTuningParameters() *model.PresetParam {
+	return nil
+}
+
+func (*gemma3_4BInstruct) SupportDistributedInference() bool {
+	return false
+}
+
+func (*gemma3_4BInstruct) SupportTuning() bool {
+	return false
+}
+
+type gemma3_27BInstruct struct{}
+
+func (*gemma3_27BInstruct) GetInferenceParameters() *model.PresetParam {
+	return &model.PresetParam{
+		Metadata:                metadata.MustGet(PresetGemma3_27BInstructModel),
+		DiskStorageRequirement:  "200Gi",
+		GPUCountRequirement:     "1",
+		TotalSafeTensorFileSize: "54.8Gi",
+		BytesPerToken:           666624,
+		ModelTokenLimit:         131072,
+		RuntimeParam: model.RuntimeParam{
+			Transformers: model.HuggingfaceTransformersParam{
+				BaseCommand:       baseCommandPresetGemma3Inference,
+				AccelerateParams:  inference.DefaultAccelerateParams,
+				InferenceMainFile: inference.DefaultTransformersMainFile,
+				ModelRunParams:    gemma3RunParams,
+			},
+			VLLM: model.VLLMParam{
+				BaseCommand:    inference.DefaultVLLMCommand,
+				ModelName:      PresetGemma3_27BInstructModel,
+				ModelRunParams: gemma3RunParamsVLLM,
+			},
+		},
+		ReadinessTimeout: time.Duration(45) * time.Minute,
+	}
+}
+
+func (*gemma3_27BInstruct) GetTuningParameters() *model.PresetParam {
+	return nil
+}
+
+func (*gemma3_27BInstruct) SupportDistributedInference() bool {
+	return false
+}
+
+func (*gemma3_27BInstruct) SupportTuning() bool {
+	return false
+}
diff --git a/presets/workspace/models/gpt/README.md b/presets/workspace/models/gpt/README.md
index bd23e04..9b1f63d 100644
--- a/presets/workspace/models/gpt/README.md
+++ b/presets/workspace/models/gpt/README.md
@@ -9,4 +9,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/llama3/README.md b/presets/workspace/models/llama3/README.md
index 6f76837..0632888 100644
--- a/presets/workspace/models/llama3/README.md
+++ b/presets/workspace/models/llama3/README.md
@@ -12,4 +12,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/mistral/README.md b/presets/workspace/models/mistral/README.md
index e66f58b..97daf01 100644
--- a/presets/workspace/models/mistral/README.md
+++ b/presets/workspace/models/mistral/README.md
@@ -10,4 +10,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/phi2/README.md b/presets/workspace/models/phi2/README.md
index 653a0ba..41a3184 100644
--- a/presets/workspace/models/phi2/README.md
+++ b/presets/workspace/models/phi2/README.md
@@ -9,7 +9,7 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
 
 ## Fine-tuning Configuration
 
diff --git a/presets/workspace/models/phi3/README.md b/presets/workspace/models/phi3/README.md
index ab3a8ca..687198d 100644
--- a/presets/workspace/models/phi3/README.md
+++ b/presets/workspace/models/phi3/README.md
@@ -12,4 +12,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/phi4/README.md b/presets/workspace/models/phi4/README.md
index 7ef9dec..71c7f79 100644
--- a/presets/workspace/models/phi4/README.md
+++ b/presets/workspace/models/phi4/README.md
@@ -9,4 +9,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/qwen/README.md b/presets/workspace/models/qwen/README.md
index e49755c..49fa427 100644
--- a/presets/workspace/models/qwen/README.md
+++ b/presets/workspace/models/qwen/README.md
@@ -9,4 +9,4 @@
 
 ## Usage
 
-See [document](../../../../docs/inference/README.md).
+See [document](../../../../website/docs/inference.md).
diff --git a/presets/workspace/models/supported_models.yaml b/presets/workspace/models/supported_models.yaml
index 9d8d55b..e51a04a 100644
--- a/presets/workspace/models/supported_models.yaml
+++ b/presets/workspace/models/supported_models.yaml
@@ -229,6 +229,24 @@ models:
     # 0.1.0 - vLLM, Transformers, and several other library updates and fixes
     # 0.0.1 - New Model!
 
+  # Gemma 3
+  - name: gemma-3-4b-instruct
+    type: text-generation
+    version: https://huggingface.co/google/gemma-3-4b-it/commit/093f9f388b31de276ce2de164bdc2081324b9767
+    runtime: tfs
+    downloadAtRuntime: true
+    tag: 0.0.1
+    # Tag history:
+    # 0.0.1 - New Model!
+  - name: gemma-3-27b-instruct
+    type: text-generation
+    version: https://huggingface.co/google/gemma-3-27b-it/commit/005ad3404e59d6023443cb575daa05336842228a
+    runtime: tfs
+    downloadAtRuntime: true
+    tag: 0.0.1
+    # Tag history:
+    # 0.0.1 - New Model!
+
   # GPT-OSS
   - name: gpt-oss-20b
     type: text-generation



Test patch:
diff --git a/presets/ragengine/tests/api/test_main.py b/presets/ragengine/tests/api/test_main.py
index 5e6f550..9f837d1 100644
--- a/presets/ragengine/tests/api/test_main.py
+++ b/presets/ragengine/tests/api/test_main.py
@@ -582,6 +582,7 @@ async def test_list_documents_in_index_success(async_client):
 
     # Ensure documents exist correctly in the specific index
     assert response_json["count"] == 2
+    assert response_json["total_items"] == 2
     assert len(response_json["documents"]) == 2
     assert all(
         (
diff --git a/presets/ragengine/tests/vector_store/test_base_store.py b/presets/ragengine/tests/vector_store/test_base_store.py
index a2270de..60f3d82 100644
--- a/presets/ragengine/tests/vector_store/test_base_store.py
+++ b/presets/ragengine/tests/vector_store/test_base_store.py
@@ -349,11 +349,12 @@ func main() {}"""
             "test_code_index", documents[0], result[0]
         )
 
-        all_docs = await vector_store_manager.list_documents_in_index(
+        resp = await vector_store_manager.list_documents_in_index(
             "test_code_index", limit=10, offset=0
         )
-        assert len(all_docs) == 1
-        assert all_docs[0]["text"] == sample_go_code
+        assert len(resp.documents) == 1
+        assert resp.documents[0].text == sample_go_code
+        assert resp.total_items == 1
 
         try:
             # Attempt to index a document with no language
@@ -491,13 +492,13 @@ func main() {}"""
         ids = await vector_store_manager.index_documents("test_add_index", documents)
 
         # Check if the documents were indexed correctly
-        all_docs = await vector_store_manager.list_documents_in_index(
+        resp = await vector_store_manager.list_documents_in_index(
             "test_add_index", limit=10, offset=1
         )
-        print(all_docs)
 
         # Validate id's from index_documents match the expected ids
-        assert all(doc["doc_id"] == ids[idx] for idx, doc in enumerate(all_docs))
+        assert all(doc.doc_id == ids[idx] for idx, doc in enumerate(resp.documents))
+        assert resp.total_items == 11
 
     @pytest.mark.asyncio
     async def test_persist_index(self, vector_store_manager):
@@ -539,50 +540,58 @@ func main() {}"""
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=0
         )
-        assert len(result) == 5
+        assert len(result.documents) == 5
+        assert result.total_items == 10
 
         # 2. Offset 5, Limit 5 (Next Batch)
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=5
         )
-        assert len(result) == 5
+        assert len(result.documents) == 5
+        assert result.total_items == 10
 
         # 3. Offset at max (Empty Case)
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=10
         )
-        assert index_name not in result or len(result) == 0
+        assert index_name not in result.documents or len(result.documents) == 0
+        assert result.total_items == 10
 
         # 4. Limit larger than available docs
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=15, offset=0
         )
-        assert len(result) == 10  # Should return only available docs
+        assert len(result.documents) == 10  # Should return only available docs
+        assert result.total_items == 10
 
         # 5. Limit exactly matches available docs
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=10, offset=0
         )
-        assert len(result) == 10
+        assert len(result.documents) == 10
+        assert result.total_items == 10
 
         # 6. Limit of 1 (Single-Doc Retrieval)
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=1, offset=0
         )
-        assert len(result) == 1
+        assert len(result.documents) == 1
+        assert result.total_items == 10
 
         # 7. max_text_length truncation check
         truncated_result = await vector_store_manager.list_documents_in_index(
             index_name, limit=1, offset=0, max_text_length=5
         )
-        assert len(next(iter(truncated_result))["text"]) == 5  # Ensure truncation
+        assert (
+            len(next(iter(truncated_result.documents)).text) == 5
+        )  # Ensure truncation
 
         # 8. max_text_length is None (Full text should return)
         full_text_result = await vector_store_manager.list_documents_in_index(
             index_name, limit=1, offset=0, max_text_length=None
         )
         assert (
-            "Document" in next(iter(full_text_result))["text"]
+            "Document" in next(iter(full_text_result.documents)).text
         )  # Ensure no truncation
 
     @pytest.mark.asyncio
@@ -604,21 +613,28 @@ func main() {}"""
         result = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=0, metadata_filter={"filename": "file_1"}
         )
-        assert len(result) == 1
-        assert result[0]["metadata"]["filename"] == "file_1"
+        assert len(result.documents) == 1
+        assert result.documents[0].metadata["filename"] == "file_1"
+        assert result.total_items == 1
 
         first_five_results = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=0, metadata_filter={"branch": "main"}
         )
-        assert len(first_five_results) == 5
-        assert all(doc["metadata"]["branch"] == "main" for doc in first_five_results)
+        assert len(first_five_results.documents) == 5
+        assert all(
+            doc.metadata["branch"] == "main" for doc in first_five_results.documents
+        )
+        assert first_five_results.total_items == 10
 
         second_five_results = await vector_store_manager.list_documents_in_index(
             index_name, limit=5, offset=5, metadata_filter={"branch": "main"}
         )
-        assert len(second_five_results) == 5
-        assert all(doc["metadata"]["branch"] == "main" for doc in second_five_results)
+        assert len(second_five_results.documents) == 5
+        assert all(
+            doc.metadata["branch"] == "main" for doc in second_five_results.documents
+        )
         assert first_five_results != second_five_results
+        assert second_five_results.total_items == 10
 
         # multiple filters
         result = await vector_store_manager.list_documents_in_index(
@@ -627,9 +643,10 @@ func main() {}"""
             offset=0,
             metadata_filter={"filename": "file_5", "branch": "main"},
         )
-        assert len(result) == 1
-        assert result[0]["metadata"]["filename"] == "file_5"
-        assert result[0]["metadata"]["branch"] == "main"
+        assert len(result.documents) == 1
+        assert result.documents[0].metadata["filename"] == "file_5"
+        assert result.documents[0].metadata["branch"] == "main"
+        assert result.total_items == 1
 
         # no results
         result = await vector_store_manager.list_documents_in_index(
@@ -638,7 +655,33 @@ func main() {}"""
             offset=0,
             metadata_filter={"filename": "file_15", "branch": "main"},
         )
-        assert len(result) == 0
+        assert len(result.documents) == 0
+        assert result.total_items == 0
+
+        new_branch_docs = [
+            Document(
+                text=f"New Document {i}",
+                metadata={
+                    "type": "text",
+                    "filename": f"file_{i}",
+                    "branch": "new_branch",
+                },
+            )
+            for i in range(7)
+        ]
+
+        await vector_store_manager.index_documents(index_name, new_branch_docs)
+        # multiple filters
+        result = await vector_store_manager.list_documents_in_index(
+            index_name,
+            limit=1,
+            offset=0,
+            metadata_filter={"branch": "new_branch"},
+        )
+
+        assert len(result.documents) == 1
+        assert result.total_items == 7
+        assert result.documents[0].metadata["branch"] == "new_branch"
 
     @pytest.mark.asyncio
     async def test_persist_and_load_as_seperate_index(self, vector_store_manager):
@@ -662,11 +705,12 @@ func main() {}"""
         result = await vector_store_manager.list_documents_in_index(
             second_index_name, limit=5, offset=0
         )
-        assert len(result) == 5
+        assert len(result.documents) == 5
+        assert result.total_items == 10
 
         # validate loaded index doesn't change the original index
         await vector_store_manager.delete_documents(
-            second_index_name, [result[0]["doc_id"]]
+            second_index_name, [result.documents[0].doc_id]
         )
 
         first_index_result = await vector_store_manager.list_documents_in_index(
@@ -675,17 +719,17 @@ func main() {}"""
         second_index_result = await vector_store_manager.list_documents_in_index(
             second_index_name, limit=10, offset=0
         )
-        assert len(first_index_result) == 10
-        assert len(second_index_result) == 9
+        assert len(first_index_result.documents) == 10
+        assert len(second_index_result.documents) == 9
 
-        second_index_result[0]["text"] = "Modified text"
+        second_index_result.documents[0].text = "Modified text"
         second_update_result = await vector_store_manager.update_documents(
             second_index_name,
             [
                 Document(
-                    doc_id=second_index_result[0]["doc_id"],
+                    doc_id=second_index_result.documents[0].doc_id,
                     text="Modified text",
-                    metadata=second_index_result[0]["metadata"],
+                    metadata=second_index_result.documents[0].metadata,
                 )
             ],
         )
@@ -693,10 +737,10 @@ func main() {}"""
         assert second_update_result["updated_documents"][0].text == "Modified text"
 
         second_delete_result = await vector_store_manager.delete_documents(
-            second_index_name, [second_index_result[0]["doc_id"]]
+            second_index_name, [second_index_result.documents[0].doc_id]
         )
         assert len(second_delete_result["deleted_doc_ids"]) == 1
         assert (
             second_delete_result["deleted_doc_ids"][0]
-            == second_index_result[0]["doc_id"]
+            == second_index_result.documents[0].doc_id
         )
diff --git a/presets/ragengine/tests/vector_store/test_faiss_store.py b/presets/ragengine/tests/vector_store/test_faiss_store.py
index b276d1b..ed52493 100644
--- a/presets/ragengine/tests/vector_store/test_faiss_store.py
+++ b/presets/ragengine/tests/vector_store/test_faiss_store.py
@@ -17,6 +17,7 @@ from tempfile import TemporaryDirectory
 
 import pytest
 
+from ragengine.models import Document
 from ragengine.tests.vector_store.test_base_store import BaseVectorStoreTest
 from ragengine.vector_store.faiss_store import FaissVectorStoreHandler
 
@@ -34,39 +35,37 @@ class TestFaissVectorStore(BaseVectorStoreTest):
     @pytest.mark.asyncio
     async def check_indexed_documents(self, vector_store_manager):
         expected_output_1 = [
-            {
-                "hash_value": "1e64a170be48c45efeaa8667ab35919106da0489ec99a11d0029f2842db133aa",
-                "text": "First document in index1",
-                "is_truncated": False,
-                "metadata": {
-                    "type": "text",
-                },
-            }
+            Document(
+                doc_id="",
+                text="First document in index1",
+                metadata={"type": "text"},
+                hash_value="1e64a170be48c45efeaa8667ab35919106da0489ec99a11d0029f2842db133aa",
+                is_truncated=False,
+            )
         ]
-
         expected_output_2 = [
-            {
-                "hash_value": "a222f875b83ce8b6eb72b3cae278b620de9bcc7c6b73222424d3ce979d1a463b",
-                "text": "First document in index2",
-                "is_truncated": False,
-                "metadata": {
-                    "type": "text",
-                },
-            }
+            Document(
+                doc_id="",
+                text="First document in index2",
+                metadata={"type": "text"},
+                hash_value="a222f875b83ce8b6eb72b3cae278b620de9bcc7c6b73222424d3ce979d1a463b",
+                is_truncated=False,
+            )
         ]
 
         for index, expected_output in zip(
             ["index1", "index2"], [expected_output_1, expected_output_2], strict=False
         ):
-            response = await vector_store_manager.list_documents_in_index(
+            resp = await vector_store_manager.list_documents_in_index(
                 index, limit=10, offset=0, max_text_length=1000
             )
 
-            # Remove "doc_id" from each document in the specified index
-            def remove_doc_id(data: list) -> list:
-                return [{k: v for k, v in doc.items() if k != "doc_id"} for doc in data]
-
-            assert remove_doc_id(response) == expected_output
+            assert all(
+                resp_doc.text == expected_doc.text
+                and resp_doc.hash_value == expected_doc.hash_value
+                and resp_doc.metadata == expected_doc.metadata
+                for resp_doc, expected_doc in zip(resp.documents, expected_output)
+            )
 
     @property
     def expected_query_score(self):
diff --git a/test/e2e/preset_test.go b/test/e2e/preset_test.go
index 720dc87..97000a8 100644
--- a/test/e2e/preset_test.go
+++ b/test/e2e/preset_test.go
@@ -50,6 +50,8 @@ const (
 	PresetDeepSeekR1DistillLlama8BModel = "deepseek-r1-distill-llama-8b"
 	PresetDeepSeekR1DistillQwen14BModel = "deepseek-r1-distill-qwen-14b"
 	PresetPhi4MiniModel                 = "phi-4-mini-instruct"
+	PresetGemma3_4BInstructModel        = "gemma-3-4b-instruct"
+	PresetGemma3_27BInstructModel       = "gemma-3-27b-instruct"
 	PresetGPT_OSS_20BModel              = "gpt-oss-20b"
 	PresetGPT_OSS_120BModel             = "gpt-oss-120b"
 	WorkspaceHashAnnotation             = "workspace.kaito.io/hash"
@@ -225,6 +227,36 @@ func createGPTOss120BWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.
 	return workspaceObj
 }
 
+func createGemma3_4BInstructWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	modelSecret := createAndValidateModelSecret()
+	workspaceObj := &kaitov1beta1.Workspace{}
+	By("Creating a workspace CR with Gemma 3 4B preset public mode", func() {
+		uniqueID := fmt.Sprint("preset-gemma-3-4b-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "",
+			numOfNode, "Standard_NV36ads_A10_v5", &metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-gemma-3-4b"},
+			}, nil, PresetGemma3_4BInstructModel, nil, nil, nil, modelSecret.Name)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+	return workspaceObj
+}
+
+func createGemma3_27BInstructWorkspaceWithPresetPublicMode(numOfNode int) *kaitov1beta1.Workspace {
+	modelSecret := createAndValidateModelSecret()
+	workspaceObj := &kaitov1beta1.Workspace{}
+	By("Creating a workspace CR with Gemma 3 27B preset public mode", func() {
+		uniqueID := fmt.Sprint("preset-gemma-3-27b-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifest(uniqueID, namespaceName, "",
+			numOfNode, "Standard_NC24ads_A100_v4", &metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-gemma-3-27b"},
+			}, nil, PresetGemma3_27BInstructModel, nil, nil, nil, modelSecret.Name)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+	return workspaceObj
+}
+
 func createCustomTuningConfigMapForE2E() *corev1.ConfigMap {
 	configMap := utils.GenerateE2ETuningConfigMapManifest(namespaceName)
 
@@ -1178,6 +1210,46 @@ var _ = Describe("Workspace Preset", func() {
 		validateWorkspaceReadiness(workspaceObj)
 	})
 
+	It("should create a gemma-3-4b-instruct workspace with preset public mode successfully", utils.GinkgoLabelFastCheck, func() {
+		numOfNode := 1
+		workspaceObj := createGemma3_4BInstructWorkspaceWithPresetPublicMode(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+	})
+
+	It("should create a gemma-3-27b-instruct workspace with preset public mode successfully", utils.GinkgoLabelA100Required, func() {
+		numOfNode := 1
+		workspaceObj := createGemma3_27BInstructWorkspaceWithPresetPublicMode(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+	})
+
 	It("should create a workspace for tuning successfully, and update the workspace with another dataset and output image", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		configMap := createCustomTuningConfigMapForE2E()
diff --git a/test/e2e/preset_vllm_test.go b/test/e2e/preset_vllm_test.go
index fec41ba..ca8064c 100644
--- a/test/e2e/preset_vllm_test.go
+++ b/test/e2e/preset_vllm_test.go
@@ -318,6 +318,52 @@ var _ = Describe("Workspace Preset on vllm runtime", func() {
 		validateGatewayAPIInferenceExtensionResources(workspaceObj)
 	})
 
+	It("should create a gemma-3-4b-instruct workspace with preset public mode successfully", utils.GinkgoLabelFastCheck, func() {
+		numOfNode := 1
+		workspaceObj := createGemma3_4BInstructWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
+		validateGatewayAPIInferenceExtensionResources(workspaceObj)
+	})
+
+	It("should create a gemma-3-27b-instruct workspace with preset public mode successfully", utils.GinkgoLabelA100Required, func() {
+		numOfNode := 1
+		workspaceObj := createGemma3_27BInstructWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
+
+		defer cleanupResources(workspaceObj)
+		time.Sleep(30 * time.Second)
+
+		validateCreateNode(workspaceObj, numOfNode)
+		validateResourceStatus(workspaceObj)
+
+		time.Sleep(30 * time.Second)
+
+		validateAssociatedService(workspaceObj)
+		validateInferenceConfig(workspaceObj)
+
+		validateInferenceResource(workspaceObj, int32(numOfNode), false)
+
+		validateWorkspaceReadiness(workspaceObj)
+		validateModelsEndpoint(workspaceObj)
+		validateCompletionsEndpoint(workspaceObj)
+		validateGatewayAPIInferenceExtensionResources(workspaceObj)
+	})
+
 	It("should create a gpt-oss-20b workspace with preset public mode successfully", utils.GinkgoLabelFastCheck, func() {
 		numOfNode := 1
 		workspaceObj := createGPTOss20BWorkspaceWithPresetPublicModeAndVLLM(numOfNode)
@@ -505,6 +551,40 @@ func createLlama3_3_70BInstructWorkspaceWithPresetPublicModeAndVLLM(numOfNode in
 	return workspaceObj
 }
 
+func createGemma3_4BInstructWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	modelSecret := createAndValidateModelSecret()
+	workspaceObj := &kaitov1beta1.Workspace{}
+
+	By("Creating a workspace CR with Gemma 3 4B preset public mode and vLLM", func() {
+		uniqueID := fmt.Sprint("preset-gemma-3-4b-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NV36ads_A10_v5",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-gemma-3-4b-vllm"},
+			}, nil, PresetGemma3_4BInstructModel, nil, nil, nil, modelSecret.Name)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+
+	return workspaceObj
+}
+
+func createGemma3_27BInstructWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
+	modelSecret := createAndValidateModelSecret()
+	workspaceObj := &kaitov1beta1.Workspace{}
+
+	By("Creating a workspace CR with Gemma 3 27B preset public mode and vLLM", func() {
+		uniqueID := fmt.Sprint("preset-gemma-3-27b-", rand.Intn(1000))
+		workspaceObj = utils.GenerateInferenceWorkspaceManifestWithVLLM(uniqueID, namespaceName, "", numOfNode, "Standard_NC24ads_A100_v4",
+			&metav1.LabelSelector{
+				MatchLabels: map[string]string{"kaito-workspace": "public-preset-e2e-test-gemma-3-27b-vllm"},
+			}, nil, PresetGemma3_27BInstructModel, nil, nil, nil, modelSecret.Name)
+
+		createAndValidateWorkspace(workspaceObj)
+	})
+
+	return workspaceObj
+}
+
 func createGPTOss20BWorkspaceWithPresetPublicModeAndVLLM(numOfNode int) *kaitov1beta1.Workspace {
 	workspaceObj := &kaitov1beta1.Workspace{}