Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions go/api/v1alpha2/modelconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,19 @@ type OllamaConfig struct {

type GeminiConfig struct{}

// APIKeyConfig contains API key secret configuration for model provider authentication.
type APIKeyConfig struct {
// SecretRef is the name of the Kubernetes Secret containing the API key.
// The Secret must be in the same namespace as the ModelConfig.
// +optional
SecretRef string `json:"secretRef,omitempty"`

// SecretKey is the key within the Secret that contains the API key data.
// Required when SecretRef is set.
// +optional
SecretKey string `json:"secretKey,omitempty"`
}

// TLSConfig contains TLS/SSL configuration options for model provider connections.
// This enables agents to connect to internal LiteLLM gateways or other providers
// that use self-signed certificates or custom certificate authorities.
Expand Down Expand Up @@ -255,6 +268,9 @@ type TLSConfig struct {
// +kubebuilder:validation:XValidation:message="provider.gemini must be nil if the provider is not Gemini",rule="!(has(self.gemini) && self.provider != 'Gemini')"
// +kubebuilder:validation:XValidation:message="provider.geminiVertexAI must be nil if the provider is not GeminiVertexAI",rule="!(has(self.geminiVertexAI) && self.provider != 'GeminiVertexAI')"
// +kubebuilder:validation:XValidation:message="provider.anthropicVertexAI must be nil if the provider is not AnthropicVertexAI",rule="!(has(self.anthropicVertexAI) && self.provider != 'AnthropicVertexAI')"
// +kubebuilder:validation:XValidation:message="cannot use both apiKey and deprecated apiKeySecret/apiKeySecretKey fields",rule="!(has(self.apiKey) && (has(self.apiKeySecret) || has(self.apiKeySecretKey)))"
// +kubebuilder:validation:XValidation:message="apiKey.secretRef must be set if apiKey.secretKey is set",rule="!(has(self.apiKey) && has(self.apiKey.secretKey) && size(self.apiKey.secretKey) > 0 && (!has(self.apiKey.secretRef) || size(self.apiKey.secretRef) == 0))"
// +kubebuilder:validation:XValidation:message="apiKey.secretKey must be set if apiKey.secretRef is set",rule="!(has(self.apiKey) && has(self.apiKey.secretRef) && size(self.apiKey.secretRef) > 0 && (!has(self.apiKey.secretKey) || size(self.apiKey.secretKey) == 0))"
// +kubebuilder:validation:XValidation:message="apiKeySecret must be set if apiKeySecretKey is set",rule="!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))"
// +kubebuilder:validation:XValidation:message="apiKeySecretKey must be set if apiKeySecret is set",rule="!(has(self.apiKeySecret) && !has(self.apiKeySecretKey))"
// +kubebuilder:validation:XValidation:message="caCertSecretKey requires caCertSecretRef",rule="!(has(self.tls) && has(self.tls.caCertSecretKey) && size(self.tls.caCertSecretKey) > 0 && (!has(self.tls.caCertSecretRef) || size(self.tls.caCertSecretRef) == 0))"
Expand All @@ -263,13 +279,19 @@ type TLSConfig struct {
type ModelConfigSpec struct {
Model string `json:"model"`

// The name of the secret that contains the API key. Must be a reference to the name of a secret in the same namespace as the referencing ModelConfig
// APIKey contains the API key secret configuration.
// +optional
APIKeySecret string `json:"apiKeySecret"`
APIKey *APIKeyConfig `json:"apiKey,omitempty"`

// The key in the secret that contains the API key
// Deprecated: Use APIKey.SecretRef instead.
// The name of the secret that contains the API key. Must be a reference to the name of a secret in the same namespace as the referencing ModelConfig.
// +optional
APIKeySecretKey string `json:"apiKeySecretKey"`
APIKeySecret string `json:"apiKeySecret,omitempty"`

// Deprecated: Use APIKey.SecretKey instead.
// The key in the secret that contains the API key.
// +optional
APIKeySecretKey string `json:"apiKeySecretKey,omitempty"`

// +optional
DefaultHeaders map[string]string `json:"defaultHeaders,omitempty"`
Expand Down Expand Up @@ -346,6 +368,23 @@ type ModelConfigList struct {
Items []ModelConfig `json:"items"`
}

// GetAPIKeySecretRef returns the API key secret name and key, supporting both the new nested
// structure (APIKey) and the deprecated flat fields (APIKeySecret/APIKeySecretKey).
// The new nested structure takes precedence if set.
func (s *ModelConfigSpec) GetAPIKeySecretRef() (secretName, secretKey string) {
if s.APIKey != nil && s.APIKey.SecretRef != "" {
return s.APIKey.SecretRef, s.APIKey.SecretKey
}
// Fall back to deprecated fields
return s.APIKeySecret, s.APIKeySecretKey
}

// IsUsingDeprecatedAPIKeyFields returns true if the deprecated APIKeySecret/APIKeySecretKey
// fields are being used instead of the new nested APIKey structure.
func (s *ModelConfigSpec) IsUsingDeprecatedAPIKeyFields() bool {
return (s.APIKeySecret != "" || s.APIKeySecretKey != "") && (s.APIKey == nil || s.APIKey.SecretRef == "")
}

func init() {
SchemeBuilder.Register(&ModelConfig{}, &ModelConfigList{})
}
20 changes: 20 additions & 0 deletions go/api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 31 additions & 4 deletions go/config/crd/bases/kagent.dev_modelconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,29 @@ spec:
- location
- projectID
type: object
apiKey:
description: APIKey contains the API key secret configuration.
properties:
secretKey:
description: |-
SecretKey is the key within the Secret that contains the API key data.
Required when SecretRef is set.
type: string
secretRef:
description: |-
SecretRef is the name of the Kubernetes Secret containing the API key.
The Secret must be in the same namespace as the ModelConfig.
type: string
type: object
apiKeySecret:
description: The name of the secret that contains the API key. Must
be a reference to the name of a secret in the same namespace as
the referencing ModelConfig
description: |-
Deprecated: Use APIKey.SecretRef instead.
The name of the secret that contains the API key. Must be a reference to the name of a secret in the same namespace as the referencing ModelConfig.
type: string
apiKeySecretKey:
description: The key in the secret that contains the API key
description: |-
Deprecated: Use APIKey.SecretKey instead.
The key in the secret that contains the API key.
type: string
azureOpenAI:
description: Azure OpenAI-specific configuration
Expand Down Expand Up @@ -636,6 +652,17 @@ spec:
- message: provider.anthropicVertexAI must be nil if the provider is not
AnthropicVertexAI
rule: '!(has(self.anthropicVertexAI) && self.provider != ''AnthropicVertexAI'')'
- message: cannot use both apiKey and deprecated apiKeySecret/apiKeySecretKey
fields
rule: '!(has(self.apiKey) && (has(self.apiKeySecret) || has(self.apiKeySecretKey)))'
- message: apiKey.secretRef must be set if apiKey.secretKey is set
rule: '!(has(self.apiKey) && has(self.apiKey.secretKey) && size(self.apiKey.secretKey)
> 0 && (!has(self.apiKey.secretRef) || size(self.apiKey.secretRef)
== 0))'
- message: apiKey.secretKey must be set if apiKey.secretRef is set
rule: '!(has(self.apiKey) && has(self.apiKey.secretRef) && size(self.apiKey.secretRef)
> 0 && (!has(self.apiKey.secretKey) || size(self.apiKey.secretKey)
== 0))'
- message: apiKeySecret must be set if apiKeySecretKey is set
rule: '!(has(self.apiKeySecretKey) && !has(self.apiKeySecret))'
- message: apiKeySecretKey must be set if apiKeySecret is set
Expand Down
4 changes: 2 additions & 2 deletions go/internal/controller/modelconfig_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ func modelReferencesSecret(model *v1alpha2.ModelConfig, secretObj types.Namespac
return false
}

// check if secret is referenced as an APIKey
if model.Spec.APIKeySecret != "" && model.Spec.APIKeySecret == secretObj.Name {
apiKeySecretName, _ := model.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" && apiKeySecretName == secretObj.Name {
return true
}

Expand Down
15 changes: 11 additions & 4 deletions go/internal/controller/reconciler/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,20 @@ func (a *kagentReconciler) ReconcileKagentModelConfig(ctx context.Context, req c
var err error
var secrets []secretRef

// check for api key secret
if modelConfig.Spec.APIKeySecret != "" {
if modelConfig.Spec.IsUsingDeprecatedAPIKeyFields() {
reconcileLog.Info(
"DEPRECATION WARNING: apiKeySecret and apiKeySecretKey fields are deprecated, use apiKey.secretRef and apiKey.secretKey instead",
"modelConfig", utils.GetObjectRef(modelConfig),
)
}

apiKeySecretName, _ := modelConfig.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" {
secret := &corev1.Secret{}
namespacedName := types.NamespacedName{Namespace: modelConfig.Namespace, Name: modelConfig.Spec.APIKeySecret}
namespacedName := types.NamespacedName{Namespace: modelConfig.Namespace, Name: apiKeySecretName}

if kubeErr := a.kube.Get(ctx, namespacedName, secret); kubeErr != nil {
err = multierror.Append(err, fmt.Errorf("failed to get secret %s: %v", modelConfig.Spec.APIKeySecret, kubeErr))
err = multierror.Append(err, fmt.Errorf("failed to get secret %s: %v", apiKeySecretName, kubeErr))
} else {
secrets = append(secrets, secretRef{
NamespacedName: namespacedName,
Expand Down
38 changes: 22 additions & 16 deletions go/internal/controller/translator/agent/adk_api_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,15 +654,16 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC

switch model.Spec.Provider {
case v1alpha2.ModelProviderOpenAI:
if model.Spec.APIKeySecret != "" {
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" {
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "OPENAI_API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: model.Spec.APIKeySecret,
Name: apiKeySecretName,
},
Key: model.Spec.APIKeySecretKey,
Key: apiKeySecretKey,
},
},
})
Expand Down Expand Up @@ -709,15 +710,16 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
}
return openai, modelDeploymentData, secretHashBytes, nil
case v1alpha2.ModelProviderAnthropic:
if model.Spec.APIKeySecret != "" {
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" {
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "ANTHROPIC_API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: model.Spec.APIKeySecret,
Name: apiKeySecretName,
},
Key: model.Spec.APIKeySecretKey,
Key: apiKeySecretKey,
},
},
})
Expand All @@ -739,14 +741,15 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
if model.Spec.AzureOpenAI == nil {
return nil, nil, nil, fmt.Errorf("AzureOpenAI model config is required")
}
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "AZURE_OPENAI_API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: model.Spec.APIKeySecret,
Name: apiKeySecretName,
},
Key: model.Spec.APIKeySecretKey,
Key: apiKeySecretKey,
},
},
})
Expand Down Expand Up @@ -794,16 +797,17 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
Name: "GOOGLE_GENAI_USE_VERTEXAI",
Value: "true",
})
if model.Spec.APIKeySecret != "" {
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" {
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "GOOGLE_APPLICATION_CREDENTIALS",
Value: "/creds/" + model.Spec.APIKeySecretKey,
Value: "/creds/" + apiKeySecretKey,
})
modelDeploymentData.Volumes = append(modelDeploymentData.Volumes, corev1.Volume{
Name: googleCredsVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: model.Spec.APIKeySecret,
SecretName: apiKeySecretName,
},
},
})
Expand Down Expand Up @@ -834,16 +838,17 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC
Name: "GOOGLE_CLOUD_LOCATION",
Value: model.Spec.AnthropicVertexAI.Location,
})
if model.Spec.APIKeySecret != "" {
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
if apiKeySecretName != "" {
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "GOOGLE_APPLICATION_CREDENTIALS",
Value: "/creds/" + model.Spec.APIKeySecretKey,
Value: "/creds/" + apiKeySecretKey,
})
modelDeploymentData.Volumes = append(modelDeploymentData.Volumes, corev1.Volume{
Name: googleCredsVolumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: model.Spec.APIKeySecret,
SecretName: apiKeySecretName,
},
},
})
Expand Down Expand Up @@ -881,14 +886,15 @@ func (a *adkApiTranslator) translateModel(ctx context.Context, namespace, modelC

return ollama, modelDeploymentData, secretHashBytes, nil
case v1alpha2.ModelProviderGemini:
apiKeySecretName, apiKeySecretKey := model.Spec.GetAPIKeySecretRef()
modelDeploymentData.EnvVars = append(modelDeploymentData.EnvVars, corev1.EnvVar{
Name: "GOOGLE_API_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: model.Spec.APIKeySecret,
Name: apiKeySecretName,
},
Key: model.Spec.APIKeySecretKey,
Key: apiKeySecretKey,
},
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
operation: translateAgent
targetObject: test-agent
namespace: default
objects:
- apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
name: nested-apikey-model
namespace: default
spec:
model: gpt-4
provider: OpenAI
apiKey:
secretRef: my-nested-secret
secretKey: my-nested-key
openAI:
baseUrl: https://api.openai.com/v1
temperature: "0.7"
- apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: test-agent
namespace: default
spec:
type: Declarative
declarative:
modelConfig: nested-apikey-model
systemMessage: "You are a test agent."
Loading