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
22 changes: 21 additions & 1 deletion api/v1alpha1/olsconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,16 @@ type QueryFiltersSpec struct {
ReplaceWith string `json:"replaceWith,omitempty"`
}

// VertexConfig defines the configuration for the Google Vertex provider.
type VertexConfig struct {
// Google Cloud project ID
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Google Cloud Project ID"
ProjectID string `json:"projectID,omitempty"`
// Server region location
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Server Region Location"
Location string `json:"location,omitempty"`
}

// ModelParametersSpec
type ModelParametersSpec struct {
// Max tokens for response. The default is 2048 tokens.
Expand Down Expand Up @@ -479,6 +489,10 @@ type ModelSpec struct {
// +kubebuilder:validation:XValidation:message="Llama Stack Generic mode (providerType set) requires type='llamaStackGeneric'",rule="!has(self.providerType) || self.type == \"llamaStackGeneric\""
// +kubebuilder:validation:XValidation:message="Llama Stack Generic mode cannot use legacy provider-specific fields",rule="self.type != \"llamaStackGeneric\" || (!has(self.deploymentName) && !has(self.projectID) && !has(self.url) && !has(self.apiVersion))"
// +kubebuilder:validation:XValidation:message="credentialKey must not be empty or whitespace",rule="!has(self.credentialKey) || !self.credentialKey.matches('^[ \\t\\n\\r\\v\\f]*$')"
// +kubebuilder:validation:XValidation:message="googleVertexConfig is required for google_vertex provider",rule="self.type != \"google_vertex\" || has(self.googleVertexConfig)"
// +kubebuilder:validation:XValidation:message="googleVertexAnthropicConfig is required for google_vertex_anthropic provider",rule="self.type != \"google_vertex_anthropic\" || has(self.googleVertexAnthropicConfig)"
// +kubebuilder:validation:XValidation:message="googleVertexConfig may only be set when type is google_vertex",rule="self.type == \"google_vertex\" || !has(self.googleVertexConfig)"
// +kubebuilder:validation:XValidation:message="googleVertexAnthropicConfig may only be set when type is google_vertex_anthropic",rule="self.type == \"google_vertex_anthropic\" || !has(self.googleVertexAnthropicConfig)"
type ProviderSpec struct {
// Provider name
// +kubebuilder:validation:Required
Expand All @@ -503,7 +517,7 @@ type ProviderSpec struct {
// Provider type
// +kubebuilder:validation:Required
// +required
// +kubebuilder:validation:Enum=azure_openai;bam;openai;watsonx;rhoai_vllm;rhelai_vllm;fake_provider;llamaStackGeneric
// +kubebuilder:validation:Enum=azure_openai;bam;openai;watsonx;rhoai_vllm;rhelai_vllm;fake_provider;llamaStackGeneric;google_vertex;google_vertex_anthropic
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Provider Type"
Type string `json:"type"`
// Deployment name for Azure OpenAI provider
Expand All @@ -515,6 +529,12 @@ type ProviderSpec struct {
// Watsonx Project ID
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Watsonx Project ID"
WatsonProjectID string `json:"projectID,omitempty"`
// Google Vertex Config
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Google Vertex Config"
GoogleVertexConfig *VertexConfig `json:"googleVertexConfig,omitempty"`
// Google Vertex Anthropic Config
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Google Vertex Anthropic Config"
GoogleVertexAnthropicConfig *VertexConfig `json:"googleVertexAnthropicConfig,omitempty"`
// Fake Provider MCP Tool Call
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Fake Provider MCP Tool Call"
FakeProviderMCPToolCall bool `json:"fakeProviderMCPToolCall,omitempty"`
Expand Down
15 changes: 15 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

33 changes: 33 additions & 0 deletions config/crd/bases/ols.openshift.io_olsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,26 @@ spec:
fakeProviderMCPToolCall:
description: Fake Provider MCP Tool Call
type: boolean
googleVertexAnthropicConfig:
description: Google Vertex Anthropic Config
properties:
location:
description: Server region location
type: string
projectID:
description: Google Cloud project ID
type: string
type: object
googleVertexConfig:
description: Google Vertex Config
properties:
location:
description: Server region location
type: string
projectID:
description: Google Cloud project ID
type: string
type: object
models:
description: List of models from the provider
items:
Expand Down Expand Up @@ -292,6 +312,8 @@ spec:
- rhelai_vllm
- fake_provider
- llamaStackGeneric
- google_vertex
- google_vertex_anthropic
type: string
url:
description: Provider API URL
Expand Down Expand Up @@ -326,6 +348,17 @@ spec:
- message: credentialKey must not be empty or whitespace
rule: '!has(self.credentialKey) || !self.credentialKey.matches(''^[
\t\n\r\v\f]*$'')'
- message: googleVertexConfig is required for google_vertex
provider
rule: self.type != "google_vertex" || has(self.googleVertexConfig)
- message: googleVertexAnthropicConfig is required for google_vertex_anthropic
provider
rule: self.type != "google_vertex_anthropic" || has(self.googleVertexAnthropicConfig)
- message: googleVertexConfig may only be set when type is google_vertex
rule: self.type == "google_vertex" || !has(self.googleVertexConfig)
- message: googleVertexAnthropicConfig may only be set when
type is google_vertex_anthropic
rule: self.type == "google_vertex_anthropic" || !has(self.googleVertexAnthropicConfig)
maxItems: 10
type: array
required:
Expand Down
47 changes: 42 additions & 5 deletions internal/controller/appserver/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func postgresCacheConfig(r reconciler.Reconciler, _ *olsv1alpha1.OLSConfig) util

// buildProviderConfigs builds the provider configurations for the OLS config from the CR spec.
// It handles Azure OpenAI, fake providers, and standard providers with their respective models.
func buildProviderConfigs(cr *olsv1alpha1.OLSConfig) []utils.ProviderConfig {
func buildProviderConfigs(cr *olsv1alpha1.OLSConfig) ([]utils.ProviderConfig, error) {
providerConfigs := []utils.ProviderConfig{}
for _, provider := range cr.Spec.LLMConfig.Providers {
credentialPath := path.Join(utils.APIKeyMountRoot, provider.CredentialsSecretRef.Name)
Expand All @@ -128,7 +128,8 @@ func buildProviderConfigs(cr *olsv1alpha1.OLSConfig) []utils.ProviderConfig {
modelConfigs = append(modelConfigs, modelConfig)
}
var providerConfig utils.ProviderConfig
if provider.Type == utils.AzureOpenAIType {
switch provider.Type {
case utils.AzureOpenAIType:
providerConfig = utils.ProviderConfig{
Name: provider.Name,
Type: provider.Type,
Expand All @@ -140,7 +141,40 @@ func buildProviderConfigs(cr *olsv1alpha1.OLSConfig) []utils.ProviderConfig {
AzureDeploymentName: provider.AzureDeploymentName,
},
}
} else {
case utils.GoogleVertexType, utils.GoogleVertexAnthropicType:
if provider.CredentialKey != "" {
credentialPath = path.Join(utils.APIKeyMountRoot, provider.CredentialsSecretRef.Name, provider.CredentialKey)
} else {
credentialPath = path.Join(utils.APIKeyMountRoot, provider.CredentialsSecretRef.Name, utils.DefaultCredentialKey)
}
providerConfig = utils.ProviderConfig{
Name: provider.Name,
Type: provider.Type,
URL: provider.URL,
CredentialsPath: credentialPath,
Models: modelConfigs,
}
switch provider.Type {
case utils.GoogleVertexType:
if provider.GoogleVertexConfig != nil {
providerConfig.GoogleVertexConfig = &utils.GoogleVertexConfig{
Project: provider.GoogleVertexConfig.ProjectID,
Location: provider.GoogleVertexConfig.Location,
}
} else {
return []utils.ProviderConfig{}, fmt.Errorf("googleVertexConfig is required for google_vertex provider")
}
case utils.GoogleVertexAnthropicType:
if provider.GoogleVertexAnthropicConfig != nil {
providerConfig.GoogleVertexAnthropicConfig = &utils.GoogleVertexAnthropicConfig{
Project: provider.GoogleVertexAnthropicConfig.ProjectID,
Location: provider.GoogleVertexAnthropicConfig.Location,
}
} else {
return []utils.ProviderConfig{}, fmt.Errorf("googleVertexAnthropicConfig is required for google_vertex_anthropic provider")
}
}
default:
providerConfig = utils.ProviderConfig{
Name: provider.Name,
Type: provider.Type,
Expand All @@ -163,7 +197,7 @@ func buildProviderConfigs(cr *olsv1alpha1.OLSConfig) []utils.ProviderConfig {
}
providerConfigs = append(providerConfigs, providerConfig)
}
return providerConfigs
return providerConfigs, nil
}

// buildToolFilteringConfig builds the tool filtering configuration if enabled and MCP servers exist.
Expand Down Expand Up @@ -397,7 +431,10 @@ func generateMCPServerConfigs(r reconciler.Reconciler, cr *olsv1alpha1.OLSConfig

func GenerateOLSConfigMap(r reconciler.Reconciler, ctx context.Context, cr *olsv1alpha1.OLSConfig) (*corev1.ConfigMap, error) {
// Build provider configurations (Azure, fake providers, standard providers)
providerConfigs := buildProviderConfigs(cr)
providerConfigs, err := buildProviderConfigs(cr)
if err != nil {
return nil, fmt.Errorf("failed to build provider configurations: %w", err)
}

// Check data collector status (needed for both buildOLSConfig and later)
dataCollectorEnabled, err := dataCollectorEnabled(r, cr)
Expand Down
62 changes: 58 additions & 4 deletions internal/controller/appserver/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,60 @@ var _ = Describe("App server assets", func() {
}))))
})

It("should generate configmap with googleVertex provider", func() {
cr := utils.WithGoogleVertexProvider(cr)
cm, err := GenerateOLSConfigMap(testReconcilerInstance, context.TODO(), cr)
Expect(err).NotTo(HaveOccurred())

var olsConfigMap map[string]interface{}
err = yaml.Unmarshal([]byte(cm.Data[utils.OLSConfigFilename]), &olsConfigMap)
Expect(err).NotTo(HaveOccurred())
Expect(olsConfigMap).To(HaveKeyWithValue("llm_providers", ContainElement(MatchKeys(Options(IgnoreExtras), Keys{
"name": Equal("google_vertex"),
"type": Equal("google_vertex"),
"credentials_path": Equal("/etc/apikeys/test-secret/apitoken"),
"google_vertex_config": MatchKeys(Options(IgnoreExtras), Keys{
"project": Equal("testProjectID"),
"location": Equal("testLocation"),
}),
}))))
})

It("should return error when googleVertexConfig is not specified for google_vertex provider", func() {
cr := utils.WithGoogleVertexProvider(cr)
cr.Spec.LLMConfig.Providers[0].GoogleVertexConfig = nil
_, err := GenerateOLSConfigMap(testReconcilerInstance, context.TODO(), cr)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("googleVertexConfig is required for google_vertex provider"))
})

It("should generate configmap with googleVertexAnthropic provider", func() {
cr := utils.WithGoogleVertexAnthropicProvider(cr)
cm, err := GenerateOLSConfigMap(testReconcilerInstance, context.TODO(), cr)
Expect(err).NotTo(HaveOccurred())

var olsConfigMap map[string]interface{}
err = yaml.Unmarshal([]byte(cm.Data[utils.OLSConfigFilename]), &olsConfigMap)

Expect(olsConfigMap).To(HaveKeyWithValue("llm_providers", ContainElement(MatchKeys(Options(IgnoreExtras), Keys{
"name": Equal("google_vertex_anthropic"),
"type": Equal("google_vertex_anthropic"),
"credentials_path": Equal("/etc/apikeys/test-secret/apitoken"),
"google_vertex_anthropic_config": MatchKeys(Options(IgnoreExtras), Keys{
"project": Equal("testProjectID"),
"location": Equal("testLocation"),
}),
}))))
})

It("should return error when googleVertexAnthropicConfig is not specified for google_vertex_anthropic provider", func() {
cr := utils.WithGoogleVertexAnthropicProvider(cr)
cr.Spec.LLMConfig.Providers[0].GoogleVertexAnthropicConfig = nil
_, err := GenerateOLSConfigMap(testReconcilerInstance, context.TODO(), cr)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("googleVertexAnthropicConfig is required for google_vertex_anthropic provider"))
})

It("should generate configmap with introspectionEnabled", func() {
cr.Spec.OLSConfig.IntrospectionEnabled = true
cm, err := GenerateOLSConfigMap(testReconcilerInstance, context.TODO(), cr)
Expand Down Expand Up @@ -2045,11 +2099,11 @@ var _ = Describe("Helper function unit tests", func() {
It("should return error when proxy CA certificate ConfigMap does not exist", func() {
cr.Spec.OLSConfig.ProxyConfig = &olsv1alpha1.ProxyConfig{
ProxyURL: "http://proxy.example.com:8080",
ProxyCACertificateRef: &olsv1alpha1.ProxyCACertConfigMapRef{
LocalObjectReference: corev1.LocalObjectReference{
Name: "nonexistent-proxy-ca",
ProxyCACertificateRef: &olsv1alpha1.ProxyCACertConfigMapRef{
LocalObjectReference: corev1.LocalObjectReference{
Name: "nonexistent-proxy-ca",
},
},
},
}
// Don't create the ConfigMap - validation should fail
_, err := buildOLSConfig(testReconcilerInstance, ctx, cr, false)
Expand Down
42 changes: 35 additions & 7 deletions internal/controller/lcore/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ func buildLlamaStackInferenceProviders(_ reconciler.Reconciler, _ context.Contex
}
providerConfig["config"] = config

case utils.GoogleVertexType, utils.GoogleVertexAnthropicType:
providerConfig["config"] = buildVertexAIInferenceConfig(&provider)

default:
return nil, fmt.Errorf("internal error: no config builder for legacy provider type '%s' (provider '%s'); update the switch in buildLlamaStackInferenceProviders", provider.Type, provider.Name)
}
Expand All @@ -265,16 +268,18 @@ func buildLlamaStackInferenceProviders(_ reconciler.Reconciler, _ context.Contex
// 2. Set the Llama Stack provider_type value (e.g., "remote::new-provider")
// 3. Add credential/config handling in the switch inside buildLlamaStackInferenceProviders
var providerTypeMapping = map[string]string{
"openai": "remote::openai",
"rhoai_vllm": "remote::vllm",
"rhelai_vllm": "remote::vllm",
"azure_openai": "remote::azure",
"openai": "remote::openai",
"rhoai_vllm": "remote::vllm",
"rhelai_vllm": "remote::vllm",
"azure_openai": "remote::azure",
utils.GoogleVertexType: "remote::vertexai",
utils.GoogleVertexAnthropicType: "remote::vertexai",
// fake_provider is included in the CRD enum for testing purposes
"fake_provider": "remote::fake",
}

// getProviderType returns the Llama Stack provider_type string for a legacy OLSConfig
// provider type (openai, azure_openai, rhoai_vllm, rhelai_vllm).
// provider type (openai, azure_openai, rhoai_vllm, rhelai_vllm, google_vertex, google_vertex_anthropic).
// It is only called for providers where ProviderType == "" (the legacy path);
// generic providers (ProviderType != "") set provider_type directly without this function.
// Returns an error for unsupported types (watsonx, bam) or invalid generic usage.
Expand All @@ -287,12 +292,35 @@ func getProviderType(provider *olsv1alpha1.ProviderSpec) (string, error) {
// Unsupported provider type
switch provider.Type {
case "watsonx", "bam":
return "", fmt.Errorf("provider type '%s' (provider '%s') is not currently supported by Llama Stack. Supported types: openai, azure_openai, rhoai_vllm, rhelai_vllm, %s", provider.Type, provider.Name, utils.LlamaStackGenericType)
return "", fmt.Errorf("provider type '%s' (provider '%s') is not currently supported by Llama Stack. Supported types: openai, azure_openai, rhoai_vllm, rhelai_vllm, google_vertex, google_vertex_anthropic, %s", provider.Type, provider.Name, utils.LlamaStackGenericType)
case utils.LlamaStackGenericType:
return "", fmt.Errorf("provider type '%s' (provider '%s') requires providerType and config fields to be set", utils.LlamaStackGenericType, provider.Name)
default:
return "", fmt.Errorf("unknown provider type '%s' (provider '%s'). Supported types: openai, azure_openai, rhoai_vllm, rhelai_vllm, %s", provider.Type, provider.Name, utils.LlamaStackGenericType)
return "", fmt.Errorf("unknown provider type '%s' (provider '%s'). Supported types: openai, azure_openai, rhoai_vllm, rhelai_vllm, google_vertex, google_vertex_anthropic, %s", provider.Type, provider.Name, utils.LlamaStackGenericType)
}
}

// buildVertexAIInferenceConfig builds the Llama Stack remote::vertexai provider config
// (project, location). See https://llamastack.github.io/docs/providers/inference/remote_vertexai
func buildVertexAIInferenceConfig(provider *olsv1alpha1.ProviderSpec) map[string]interface{} {
var vc *olsv1alpha1.VertexConfig
switch provider.Type {
case utils.GoogleVertexType:
vc = provider.GoogleVertexConfig
case utils.GoogleVertexAnthropicType:
vc = provider.GoogleVertexAnthropicConfig
default:
return map[string]interface{}{}
}

config := map[string]interface{}{}
if vc != nil && vc.ProjectID != "" {
config["project"] = vc.ProjectID
}
if vc != nil && vc.Location != "" {
config["location"] = vc.Location
}
return config
}

// deepCopyMap creates a deep copy of a map[string]interface{}, including nested maps
Expand Down
30 changes: 30 additions & 0 deletions internal/controller/lcore/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -630,3 +630,33 @@ func TestBuildLCoreConfigYAML_WithoutToolsApproval(t *testing.T) {
t.Error("Expected YAML NOT to contain 'tools_approval:' section when not configured")
}
}

func TestBuildVertexAIInferenceConfig(t *testing.T) {
cr := &olsv1alpha1.OLSConfig{
Spec: olsv1alpha1.OLSConfigSpec{
LLMConfig: olsv1alpha1.LLMSpec{
Providers: []olsv1alpha1.ProviderSpec{
{
Name: "google_vertex",
Type: utils.GoogleVertexType,
GoogleVertexConfig: &olsv1alpha1.VertexConfig{
ProjectID: "testProjectID",
Location: "testLocation",
},
},
},
},
},
}

result := buildVertexAIInferenceConfig(&cr.Spec.LLMConfig.Providers[0])
if result == nil {
t.Fatal("Expected non-nil result")
}
if result["project"] != "testProjectID" {
t.Errorf("Expected project to be testProjectID, got %s", result["project"])
}
if result["location"] != "testLocation" {
t.Errorf("Expected location to be testLocation, got %s", result["location"])
}
}
Loading