Repository: trustyai-explainability/trustyai-service-operator
Pull request #485: fix: Refine chat template argument
Author: login: RobGeada
Labels: name: ok-to-test
name: lgtm
Created: 2025-05-22T12:20:10Z | Merged: 2025-05-22T15:46:32Z

Now:

```yaml
chatTemplate:
    enabled: true
    name: "some_name"
```

## Summary by Sourcery

Introduce a ChatTemplate object to replace the old ApplyChatTemplate flag, updating the API, CRD, controller logic, tests, and generated deepcopy code to support enabling/disabling and naming chat templates (including quoted names with spaces).

New Features:
- Allow naming and enabling/disabling chat templates via the new ChatTemplate API object

Enhancements:
- Replace the deprecated ApplyChatTemplate string with a ChatTemplate struct in the LMEvalJobSpec and controller
- Update generateArgs logic to handle ChatTemplate.Enabled and ChatTemplate.Name, including quoting names with spaces

Documentation:
- Revise the CRD schema to add ChatTemplate properties and remove the old applyChatTemplate field

Tests:
- Refactor controller tests to use ChatTemplate and add cases for default, named, and space-containing template names

Chores:
- Add autogenerated deepcopy methods for ChatTemplate
- Reformat volume definitions in tests for consistent indentation


Diff:
diff --git a/api/lmes/v1alpha1/lmevaljob_types.go b/api/lmes/v1alpha1/lmevaljob_types.go
index d9df2ceb1..fd4e142b4 100644
--- a/api/lmes/v1alpha1/lmevaljob_types.go
+++ b/api/lmes/v1alpha1/lmevaljob_types.go
@@ -467,6 +467,12 @@ type OfflineSpec struct {
 	StorageSpec OfflineStorageSpec `json:"storage"`
 }
 
+// ChatTemplate defines the configuration for the applied chat template during the evaluation.
+type ChatTemplate struct {
+	Enabled bool   `json:"enabled"`
+	Name    string `json:"name,omitempty"`
+}
+
 func (p *LMEvalPodSpec) GetAffinity() *corev1.Affinity {
 	if p == nil {
 		return nil
@@ -534,9 +540,9 @@ type LMEvalJobSpec struct {
 	// SystemInstruction will set the system instruction for all prompts passed to the evaluated model
 	// +optional
 	SystemInstruction string `json:"systemInstruction,omitempty"`
-	// ApplyChatTemplate will apply the specified chat template to prompts. This is required for chat-completions models.
+	// ChatTemplate defines whether to apply the default or specified chat template to prompts. This is required for chat-completions models.
 	// +optional
-	ApplyChatTemplate string `json:"applyChatTemplate,omitempty"`
+	ChatTemplate *ChatTemplate `json:"chatTemplate,omitempty"`
 }
 
 // IsOffline returns whether this LMEvalJob is configured to run offline
diff --git a/api/lmes/v1alpha1/zz_generated.deepcopy.go b/api/lmes/v1alpha1/zz_generated.deepcopy.go
index 4962741a9..22b60b5de 100644
--- a/api/lmes/v1alpha1/zz_generated.deepcopy.go
+++ b/api/lmes/v1alpha1/zz_generated.deepcopy.go
@@ -55,6 +55,21 @@ func (in *Card) DeepCopy() *Card {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ChatTemplate) DeepCopyInto(out *ChatTemplate) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChatTemplate.
+func (in *ChatTemplate) DeepCopy() *ChatTemplate {
+	if in == nil {
+		return nil
+	}
+	out := new(ChatTemplate)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *CustomArtifact) DeepCopyInto(out *CustomArtifact) {
 	*out = *in
@@ -314,6 +329,11 @@ func (in *LMEvalJobSpec) DeepCopyInto(out *LMEvalJobSpec) {
 		*out = new(bool)
 		**out = **in
 	}
+	if in.ChatTemplate != nil {
+		in, out := &in.ChatTemplate, &out.ChatTemplate
+		*out = new(ChatTemplate)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LMEvalJobSpec.
diff --git a/config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml b/config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml
index 132a793e3..50bfc38b3 100644
--- a/config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml
+++ b/config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml
@@ -53,15 +53,23 @@ spec:
                 description: AllowOnly specifies whether the LMEvalJob can directly
                   download remote code, datasets and metrics. Default is false.
                 type: boolean
-              applyChatTemplate:
-                description: ApplyChatTemplate will apply the specified chat template
-                  to prompts. This is required for chat-completions models.
-                type: string
               batchSize:
                 description: |-
                   Batch size for the evaluation. This is used by the models that run and are loaded
                   locally and not apply for the commercial APIs.
                 type: string
+              chatTemplate:
+                description: ChatTemplate defines whether to apply the default or
+                  specified chat template to prompts. This is required for chat-completions
+                  models.
+                properties:
+                  enabled:
+                    type: boolean
+                  name:
+                    type: string
+                required:
+                - enabled
+                type: object
               genArgs:
                 description: Map to `--gen_kwargs` parameter for the underlying library.
                 items:
diff --git a/controllers/lmes/lmevaljob_controller.go b/controllers/lmes/lmevaljob_controller.go
index 7dcfd7042..961fb124a 100644
--- a/controllers/lmes/lmevaljob_controller.go
+++ b/controllers/lmes/lmevaljob_controller.go
@@ -1265,13 +1265,13 @@ func generateArgs(svcOpts *serviceOptions, job *lmesv1alpha1.LMEvalJob, log logr
 
 	// --apply_chat_template
 	// check for the default string value of "false"
-	if job.Spec.ApplyChatTemplate != "" {
-		if job.Spec.ApplyChatTemplate == "false" {
+	if job.Spec.ChatTemplate != nil {
+		if !job.Spec.ChatTemplate.Enabled {
 
-		} else if job.Spec.ApplyChatTemplate == "true" {
+		} else if job.Spec.ChatTemplate.Enabled && job.Spec.ChatTemplate.Name == "" {
 			cmd.WriteString("--apply_chat_template")
 		} else {
-			cmd.WriteString("--apply_chat_template " + job.Spec.ApplyChatTemplate)
+			cmd.WriteString("--apply_chat_template \"" + job.Spec.ChatTemplate.Name + "\"")
 		}
 	}
 
diff --git a/controllers/lmes/lmevaljob_controller_test.go b/controllers/lmes/lmevaljob_controller_test.go
index 726ddf2d4..148edc606 100644
--- a/controllers/lmes/lmevaljob_controller_test.go
+++ b/controllers/lmes/lmevaljob_controller_test.go
@@ -176,8 +176,8 @@ func Test_SimplePod(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
@@ -411,8 +411,8 @@ func Test_WithCustomPod(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "additionalVolume",
@@ -624,8 +624,8 @@ func Test_EnvSecretsPod(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
@@ -801,8 +801,8 @@ func Test_FileSecretsPod(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "secVol1",
@@ -918,24 +918,31 @@ func Test_ChatTemplate(t *testing.T) {
 	}, generateArgs(svcOpts, job, log))
 
 	// chat template == false
-	job.Spec.ApplyChatTemplate = "false"
+	job.Spec.ChatTemplate = &lmesv1alpha1.ChatTemplate{Enabled: false}
 	assert.Equal(t, []string{
 		"sh", "-ec",
 		"python -m lm_eval --output_path /opt/app-root/src/output --model test --model_args arg1=value1 --tasks task1,task2 --include_path /opt/app-root/src/my_tasks --batch_size " + svcOpts.DefaultBatchSize + " ",
 	}, generateArgs(svcOpts, job, log))
 
 	// chat template == true
-	job.Spec.ApplyChatTemplate = "true"
+	job.Spec.ChatTemplate = &lmesv1alpha1.ChatTemplate{Enabled: true}
 	assert.Equal(t, []string{
 		"sh", "-ec",
 		"python -m lm_eval --output_path /opt/app-root/src/output --model test --model_args arg1=value1 --tasks task1,task2 --include_path /opt/app-root/src/my_tasks --batch_size " + svcOpts.DefaultBatchSize + " --apply_chat_template",
 	}, generateArgs(svcOpts, job, log))
 
 	// chat template == some_template
-	job.Spec.ApplyChatTemplate = "some_template"
+	job.Spec.ChatTemplate = &lmesv1alpha1.ChatTemplate{Enabled: true, Name: "some_template"}
 	assert.Equal(t, []string{
 		"sh", "-ec",
-		"python -m lm_eval --output_path /opt/app-root/src/output --model test --model_args arg1=value1 --tasks task1,task2 --include_path /opt/app-root/src/my_tasks --batch_size " + svcOpts.DefaultBatchSize + " --apply_chat_template some_template",
+		"python -m lm_eval --output_path /opt/app-root/src/output --model test --model_args arg1=value1 --tasks task1,task2 --include_path /opt/app-root/src/my_tasks --batch_size " + svcOpts.DefaultBatchSize + " --apply_chat_template \"some_template\"",
+	}, generateArgs(svcOpts, job, log))
+
+	// chat template == some with spaces template
+	job.Spec.ChatTemplate = &lmesv1alpha1.ChatTemplate{Enabled: true, Name: "some template with spaces"}
+	assert.Equal(t, []string{
+		"sh", "-ec",
+		"python -m lm_eval --output_path /opt/app-root/src/output --model test --model_args arg1=value1 --tasks task1,task2 --include_path /opt/app-root/src/my_tasks --batch_size " + svcOpts.DefaultBatchSize + " --apply_chat_template \"some template with spaces\"",
 	}, generateArgs(svcOpts, job, log))
 }
 
@@ -1760,17 +1767,17 @@ func Test_ManagedPVC(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "outputs", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: jobName + "-pvc",
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: jobName + "-pvc",
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -1923,17 +1930,17 @@ func Test_ExistingPVC(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "outputs", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -2109,17 +2116,17 @@ func Test_PVCPreference(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "outputs", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: jobName + "-pvc",
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: jobName + "-pvc",
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -2321,17 +2328,17 @@ func Test_OfflineMode(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -2534,17 +2541,17 @@ func Test_ProtectedVars(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -2726,17 +2733,17 @@ func Test_OnlineModeDisabled(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -2894,17 +2901,17 @@ func Test_OnlineMode(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -3065,17 +3072,17 @@ func Test_AllowCodeOnlineMode(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -3254,17 +3261,17 @@ func Test_AllowCodeOfflineMode(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: pvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: pvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},
@@ -3447,25 +3454,25 @@ func Test_OfflineModeWithOutput(t *testing.T) {
 			Volumes: []corev1.Volume{
 				{
 					Name: "shared", VolumeSource: corev1.VolumeSource{
-					EmptyDir: &corev1.EmptyDirVolumeSource{},
-				},
+						EmptyDir: &corev1.EmptyDirVolumeSource{},
+					},
 				},
 				{
 					Name: "outputs", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: outputPvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: outputPvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 				{
 					Name: "offline", VolumeSource: corev1.VolumeSource{
-					PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
-						ClaimName: offlinePvcName,
-						ReadOnly:  false,
+						PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
+							ClaimName: offlinePvcName,
+							ReadOnly:  false,
+						},
 					},
 				},
-				},
 			},
 			RestartPolicy: corev1.RestartPolicyNever,
 		},


Reviews:
user: login: sourcery-ai[bot]
body: Hey @RobGeada - I've reviewed your changes and found some issues that need to be addressed.

**Blocking issues**:
- Direct concatenation of the template name into the flag string risks injection or quoting issues ([link](https://github.com/trustyai-explainability/trustyai-service-operator/pull/485/files#diff-0fa38f514f11b54bb3632a09a67c330eaad66302a17b8f1deedc7e20b41cf4b6R1274))

**General comments**:

- The JSON tag for ChatTemplate should use a lowercase key (e.g. `json:"chatTemplate,omitempty"`) to match the CRD and ensure proper serialization.
- Instead of manually wrapping the template name in quotes, consider using a shell-escaping or quoting utility (e.g. `strconv.Quote` or a shlex helper) to robustly handle special characters.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟡 **General issues**: 2 issues found
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:26:18Z
user: login: ruivieira
body: LGTM, I would just change the key name from `ChatTemplate` to `chatTemplate`
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-22T12:37:22Z
user: login: RobGeada
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T13:20:49Z
user: login: ruivieira
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T13:22:54Z

Review comments:
diff_hunk: @@ -534,9 +540,9 @@ type LMEvalJobSpec struct {
 	// SystemInstruction will set the system instruction for all prompts passed to the evaluated model
 	// +optional
 	SystemInstruction string `json:"systemInstruction,omitempty"`
-	// ApplyChatTemplate will apply the specified chat template to prompts. This is required for chat-completions models.
+	// ChatTemplate defines whether to apply the default or specified chat template to prompts. This is required for chat-completions models.
 	// +optional
-	ApplyChatTemplate string `json:"applyChatTemplate,omitempty"`
+	ChatTemplate *ChatTemplate `json:"ChatTemplate,omitempty"`
path: api/lmes/v1alpha1/lmevaljob_types.go
user: login: sourcery-ai[bot]
body: **issue:** JSON tag for ChatTemplate uses uppercase key

Update the JSON tag to `json:"chatTemplate,omitempty"` for consistency with other fields and client expectations.
created_at: 2025-05-22T12:26:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,6 +43,18 @@ spec:
           spec:
             description: LMEvalJobSpec defines the desired state of LMEvalJob
             properties:
+              ChatTemplate:
path: config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml
user: login: sourcery-ai[bot]
body: **issue:** CRD property name 'ChatTemplate' is capitalized; use lowerCamelCase

Rename to `chatTemplate` for consistency with Kubernetes conventions and Go struct JSON tags.
created_at: 2025-05-22T12:26:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1265,13 +1265,13 @@ func generateArgs(svcOpts *serviceOptions, job *lmesv1alpha1.LMEvalJob, log logr
 
 	// --apply_chat_template
 	// check for the default string value of "false"
-	if job.Spec.ApplyChatTemplate != "" {
-		if job.Spec.ApplyChatTemplate == "false" {
+	if job.Spec.ChatTemplate != nil {
+		if !job.Spec.ChatTemplate.Enabled {
 
-		} else if job.Spec.ApplyChatTemplate == "true" {
+		} else if job.Spec.ChatTemplate.Enabled && job.Spec.ChatTemplate.Name == "" {
 			cmd.WriteString("--apply_chat_template")
 		} else {
-			cmd.WriteString("--apply_chat_template " + job.Spec.ApplyChatTemplate)
+			cmd.WriteString("--apply_chat_template \"" + job.Spec.ChatTemplate.Name + "\"")
path: controllers/lmes/lmevaljob_controller.go
user: login: sourcery-ai[bot]
body: **🚨 suggestion (security):** Direct concatenation of the template name into the flag string risks injection or quoting issues

Use fmt.Sprintf with %q or a dedicated function to escape the template name and avoid shell injection risks.
created_at: 2025-05-22T12:26:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1265,13 +1265,13 @@ func generateArgs(svcOpts *serviceOptions, job *lmesv1alpha1.LMEvalJob, log logr
 
 	// --apply_chat_template
 	// check for the default string value of "false"
-	if job.Spec.ApplyChatTemplate != "" {
-		if job.Spec.ApplyChatTemplate == "false" {
+	if job.Spec.ChatTemplate != nil {
+		if !job.Spec.ChatTemplate.Enabled {
 
-		} else if job.Spec.ApplyChatTemplate == "true" {
+		} else if job.Spec.ChatTemplate.Enabled && job.Spec.ChatTemplate.Name == "" {
 			cmd.WriteString("--apply_chat_template")
 		} else {
-			cmd.WriteString("--apply_chat_template " + job.Spec.ApplyChatTemplate)
+			cmd.WriteString("--apply_chat_template \"" + job.Spec.ChatTemplate.Name + "\"")
path: controllers/lmes/lmevaljob_controller.go
user: login: RobGeada
body: whoops
created_at: 2025-05-22T13:20:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-05-22T12:20:15Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide

This PR refactors the chat template setting in LMEvalJob from a plain string field to a structured ChatTemplate object, updating the API definitions, CRD schema, deepcopy functions, controller logic for argument generation, and corresponding tests, along with consistent indentation fixes in test volume declarations.

#### Sequence Diagram: Chat Template Argument Processing Logic

```mermaid
sequenceDiagram
    participant ControllerLogic as "generateArgs() in LMEvalJobController"
    participant InputSpec as "job.Spec.ChatTemplate"
    participant OutputArgs as "Command Line Arguments"

    ControllerLogic->>InputSpec: Check if ChatTemplate is defined (job.Spec.ChatTemplate)
    alt ChatTemplate is defined (not nil)
        ControllerLogic->>InputSpec: Get .Enabled
        alt .Enabled is true
            ControllerLogic->>InputSpec: Get .Name
            alt .Name is empty
                ControllerLogic->>OutputArgs: Add "--apply_chat_template"
            else .Name is not empty
                ControllerLogic->>OutputArgs: Add "--apply_chat_template \"[InputSpec.Name]\""
            end
        else .Enabled is false
            ControllerLogic->>OutputArgs: No chat template argument added
        end
    else ChatTemplate is not defined (nil)
        ControllerLogic->>OutputArgs: No chat template argument added
    end
```

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Replace ApplyChatTemplate string with ChatTemplate struct in API and CRD | <ul><li>Add ChatTemplate type with Enabled and Name fields</li><li>Remove ApplyChatTemplate field from LMEvalJobSpec</li><li>Update CRD YAML to define ChatTemplate schema</li></ul> | `api/lmes/v1alpha1/lmevaljob_types.go`<br/>`config/crd/bases/trustyai.opendatahub.io_lmevaljobs.yaml` |
| Implement DeepCopy support for ChatTemplate | <ul><li>Add DeepCopyInto and DeepCopy for ChatTemplate</li><li>Include ChatTemplate in LMEvalJobSpec DeepCopyInto logic</li></ul> | `api/lmes/v1alpha1/zz_generated.deepcopy.go` |
| Update controller argument generation to use ChatTemplate struct | <ul><li>Replace ApplyChatTemplate checks with ChatTemplate pointer logic</li><li>Handle flag output for disabled, enabled unnamed, and named templates</li></ul> | `controllers/lmes/lmevaljob_controller.go` |
| Revise tests to use ChatTemplate and fix volume indentation | <ul><li>Switch test cases from ApplyChatTemplate string to ChatTemplate pointer</li><li>Add tests for templates with spaces in the name</li><li>Standardize indentation for EmptyDir and PVC volume sources</li></ul> | `controllers/lmes/lmevaljob_controller_test.go` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-22T12:34:59Z
body: PR image build and manifest generation completed successfully!

📦 [PR image](https://quay.io/trustyai/trustyai-service-operator-ci:211536717517b128435279279991ef867bfb9910): `quay.io/trustyai/trustyai-service-operator-ci:211536717517b128435279279991ef867bfb9910`

📦 [LMES driver image](https://quay.io/trustyai/ta-lmes-driver:211536717517b128435279279991ef867bfb9910): `quay.io/trustyai/ta-lmes-driver:211536717517b128435279279991ef867bfb9910`

📦 [LMES job image](https://quay.io/trustyai/ta-lmes-job:211536717517b128435279279991ef867bfb9910): `quay.io/trustyai/ta-lmes-job:211536717517b128435279279991ef867bfb9910`

📦 [Guardrails orchestrator image](https://quay.io/trustyai/ta-guardrails-orchestrator:211536717517b128435279279991ef867bfb9910): `quay.io/trustyai/ta-guardrails-orchestrator:211536717517b128435279279991ef867bfb9910`

🗂️ [CI manifests](https://github.com/trustyai-explainability/trustyai-service-operator-ci/tree/operator-211536717517b128435279279991ef867bfb9910)

```
devFlags:
  manifests:
    - contextDir: config
      sourcePath: ''
      uri: https://api.github.com/repos/trustyai-explainability/trustyai-service-operator-ci/tarball/operator-211536717517b128435279279991ef867bfb9910
```
author_association: NONE
user: login: openshift-ci[bot]
created_at: 2025-05-22T13:22:59Z
body: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/trustyai-explainability/trustyai-service-operator/pull/485#pullrequestreview-2861218147" title="Approved">ruivieira</a>*


The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=trustyai-explainability%2Ftrustyai-service-operator).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/trustyai-explainability/trustyai-service-operator/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE
user: login: openshift-ci[bot]
created_at: 2025-05-22T14:41:19Z
body: @RobGeada: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
ci/prow/trustyai-service-operator-e2e | 211536717517b128435279279991ef867bfb9910 | [link](https://prow.ci.openshift.org/view/gs/test-platform-results/pr-logs/pull/trustyai-explainability_trustyai-service-operator/485/pull-ci-trustyai-explainability-trustyai-service-operator-main-trustyai-service-operator-e2e/1925542620260470784) | true | `/test trustyai-service-operator-e2e`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=trustyai-explainability&repo=trustyai-service-operator&pr=485). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:RobGeada).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes-sigs/prow](https://github.com/kubernetes-sigs/prow/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE
user: login: sheltoncyril
created_at: 2025-05-22T15:27:18Z
body: /lgtm
author_association: CONTRIBUTOR