From 02d4692632ef0a21e016eaf46fc333fe70c4bf4e Mon Sep 17 00:00:00 2001 From: Ondrej Metelka Date: Wed, 15 Jul 2026 15:11:48 +0200 Subject: [PATCH] OLS-3588: Add reasoningConfig field to Agent CRD Add spec.reasoningConfig freeform map to AgentSpec, allowing cluster admins to pass provider-specific reasoning parameters (Claude thinking/ effort, Gemini thinking_budget, OpenAI reasoning.effort) through the Agent CR. The operator will serialize this as LIGHTSPEED_REASONING_CONFIG env var on sandbox pods (wiring in OLS-3589). Co-authored-by: Cursor --- .golangci-kal.yml | 1 + api/v1alpha1/agent_types.go | 20 +++++++++- api/v1alpha1/zz_generated.deepcopy.go | 39 +++++++++++-------- .../bases/agentic.openshift.io_agents.yaml | 27 ++++++++++--- 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/.golangci-kal.yml b/.golangci-kal.yml index cd8017d2..421f738b 100644 --- a/.golangci-kal.yml +++ b/.golangci-kal.yml @@ -21,6 +21,7 @@ linters: disable: - statusoptional # Legacy, not recommended - nonpointerstructs # Intended for native types, not CRD types + - nomaps # ReasoningConfig is a deliberate freeform map (spec rule 18) lintersConfig: conditions: isFirstField: Warn diff --git a/api/v1alpha1/agent_types.go b/api/v1alpha1/agent_types.go index f893eaa9..faaae166 100644 --- a/api/v1alpha1/agent_types.go +++ b/api/v1alpha1/agent_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -81,6 +82,20 @@ type AgentSpec struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=500 MaxTurns int32 `json:"maxTurns,omitempty"` + + // reasoningConfig is a freeform map of provider- and model-specific + // reasoning parameters. The exact keys and values depend on the provider + // and model — consult the provider's SDK documentation for supported + // parameters (e.g., Claude: thinking/budget_tokens, Gemini: thinking_budget/ + // thinking_level, OpenAI: reasoning.effort/reasoning.summary). + // The operator serializes this map as the LIGHTSPEED_REASONING_CONFIG JSON + // env var on the sandbox pod without validation — the sandbox and upstream + // SDK/API validate at invocation time. Invalid keys are ignored by the + // adapter; invalid values on recognized keys are rejected by the SDK/API. + // When omitted, the env var is not set and the sandbox uses SDK defaults. + // +optional + // +kubebuilder:validation:MinProperties=1 + ReasoningConfig map[string]apiextensionsv1.JSON `json:"reasoningConfig,omitempty"` } // +kubebuilder:object:root=true @@ -99,7 +114,7 @@ type AgentSpec struct { // The "default" agent must exist; "smart" and "fast" are optional (the // operator auto-links to "default" if absent). // -// Example — a high-capability agent tier: +// Example — a high-capability agent tier with extended thinking: // // apiVersion: agentic.openshift.io/v1alpha1 // kind: Agent @@ -113,6 +128,9 @@ type AgentSpec struct { // analysisSeconds: 300 // executionSeconds: 600 // maxTurns: 200 +// reasoningConfig: +// thinking: "enabled" +// effort: "high" // // Example — a fast, cost-efficient agent tier: // diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 13f6e6a0..aa93afca 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -5,8 +5,8 @@ package v1alpha1 import ( - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -31,7 +31,7 @@ func (in *Agent) DeepCopyInto(out *Agent) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -90,6 +90,13 @@ func (in *AgentSpec) DeepCopyInto(out *AgentSpec) { *out = *in out.LLMProvider = in.LLMProvider out.Timeouts = in.Timeouts + if in.ReasoningConfig != nil { + in, out := &in.ReasoningConfig, &out.ReasoningConfig + *out = make(map[string]v1.JSON, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentSpec. @@ -107,7 +114,7 @@ func (in *AgentStatus) DeepCopyInto(out *AgentStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -219,7 +226,7 @@ func (in *AgenticOLSConfigStatus) DeepCopyInto(out *AgenticOLSConfigStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -429,7 +436,7 @@ func (in *AgenticRunStatus) DeepCopyInto(out *AgenticRunStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -576,7 +583,7 @@ func (in *AnalysisResultStatus) DeepCopyInto(out *AnalysisResultStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -606,7 +613,7 @@ func (in *AnalysisStepStatus) DeepCopyInto(out *AnalysisStepStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -762,7 +769,7 @@ func (in *ApprovalStageStatus) DeepCopyInto(out *ApprovalStageStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -950,7 +957,7 @@ func (in *EscalationResultStatus) DeepCopyInto(out *EscalationResultStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -973,7 +980,7 @@ func (in *EscalationStepStatus) DeepCopyInto(out *EscalationStepStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1115,7 +1122,7 @@ func (in *ExecutionResultStatus) DeepCopyInto(out *ExecutionResultStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1144,7 +1151,7 @@ func (in *ExecutionStepStatus) DeepCopyInto(out *ExecutionStepStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1452,7 +1459,7 @@ func (in *RemediationOption) DeepCopyInto(out *RemediationOption) { in.RBAC.DeepCopyInto(&out.RBAC) if in.Components != nil { in, out := &in.Components, &out.Components - *out = new(apiextensionsv1.JSON) + *out = new(v1.JSON) (*in).DeepCopyInto(*out) } } @@ -1803,7 +1810,7 @@ func (in *VerificationResultStatus) DeepCopyInto(out *VerificationResultStatus) *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1846,7 +1853,7 @@ func (in *VerificationStepStatus) DeepCopyInto(out *VerificationStepStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) + *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } diff --git a/config/crd/bases/agentic.openshift.io_agents.yaml b/config/crd/bases/agentic.openshift.io_agents.yaml index 742abc60..07f73dac 100644 --- a/config/crd/bases/agentic.openshift.io_agents.yaml +++ b/config/crd/bases/agentic.openshift.io_agents.yaml @@ -36,11 +36,12 @@ spec: name per step.\n\nAgent is cluster-scoped. The metadata.name serves as the tier identifier.\nThe \"default\" agent must exist; \"smart\" and \"fast\" are optional (the\noperator auto-links to \"default\" if absent).\n\nExample - — a high-capability agent tier:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: - Agent\n\tmetadata:\n\t name: smart\n\tspec:\n\t llmProvider:\n\t name: - vertex-ai\n\t model: claude-opus-4-6\n\t timeouts:\n\t analysisSeconds: - 300\n\t executionSeconds: 600\n\t maxTurns: 200\n\nExample — a fast, - cost-efficient agent tier:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: + — a high-capability agent tier with extended thinking:\n\n\tapiVersion: + agentic.openshift.io/v1alpha1\n\tkind: Agent\n\tmetadata:\n\t name: smart\n\tspec:\n\t + \ llmProvider:\n\t name: vertex-ai\n\t model: claude-opus-4-6\n\t timeouts:\n\t + \ analysisSeconds: 300\n\t executionSeconds: 600\n\t maxTurns: 200\n\t + \ reasoningConfig:\n\t thinking: \"enabled\"\n\t effort: \"high\"\n\nExample + — a fast, cost-efficient agent tier:\n\n\tapiVersion: agentic.openshift.io/v1alpha1\n\tkind: Agent\n\tmetadata:\n\t name: fast\n\tspec:\n\t llmProvider:\n\t name: vertex-ai\n\t model: claude-haiku-4-5\n\t timeouts:\n\t analysisSeconds: 120\n\t executionSeconds: 300\n\t maxTurns: 100" @@ -108,6 +109,22 @@ spec: only alphanumerics, dots, hyphens, underscores, slashes, colons, and at-signs rule: self.matches('^[a-zA-Z0-9][a-zA-Z0-9._\\-/:@]*$') + reasoningConfig: + additionalProperties: + x-kubernetes-preserve-unknown-fields: true + description: |- + reasoningConfig is a freeform map of provider- and model-specific + reasoning parameters. The exact keys and values depend on the provider + and model — consult the provider's SDK documentation for supported + parameters (e.g., Claude: thinking/budget_tokens, Gemini: thinking_budget/ + thinking_level, OpenAI: reasoning.effort/reasoning.summary). + The operator serializes this map as the LIGHTSPEED_REASONING_CONFIG JSON + env var on the sandbox pod without validation — the sandbox and upstream + SDK/API validate at invocation time. Invalid keys are ignored by the + adapter; invalid values on recognized keys are rejected by the SDK/API. + When omitted, the env var is not set and the sandbox uses SDK defaults. + minProperties: 1 + type: object timeouts: description: |- timeouts configures per-step and per-turn timeout limits.