From 65930c0efa5ca14e07937a98c0bcc0052466a9c6 Mon Sep 17 00:00:00 2001 From: Lukas Piwowarski Date: Wed, 24 Jun 2026 08:25:50 -0400 Subject: [PATCH 1/2] Introduce check job for make test This commit introduces a new Github Workflow named "Unit Tests". This workflow is responsible for running unit tests triggered by (make tests). This commit also addresses the following error that was encountered when one ran make test locally or in a job: --- unable to fetch hash for requested version: unable fetch metadata for kubebuilder-tools-1.30.0-linux-amd64.tar.gz -- got status "401 Unauthorized" from GCS --- The fixes bumps up the test dependencies (ENVTEST_VERSION release-0.18 -> release-0.22). The previous vrsion was trying to pull the the test dependencies from GCS server that has been deprecated as part of 0.19 [1]. [1] https://github.com/kubernetes-sigs/controller-runtime/pull/2915 --- .github/workflows/unit-tests.yaml | 21 +++++++++++++++++++++ Makefile | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/unit-tests.yaml diff --git a/.github/workflows/unit-tests.yaml b/.github/workflows/unit-tests.yaml new file mode 100644 index 00000000..809d6f2f --- /dev/null +++ b/.github/workflows/unit-tests.yaml @@ -0,0 +1,21 @@ +name: Unit Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Set up Go + uses: actions/setup-go@v6 + + - name: Run tests + run: make test diff --git a/Makefile b/Makefile index 09e511f0..d71eadd4 100644 --- a/Makefile +++ b/Makefile @@ -246,7 +246,7 @@ KUTTL ?= $(LOCALBIN)/kubectl-kuttl ## Tool Versions KUSTOMIZE_VERSION ?= v5.4.2 CONTROLLER_TOOLS_VERSION ?= v0.16.5 -ENVTEST_VERSION ?= release-0.18 +ENVTEST_VERSION ?= release-0.22 GOLANGCI_LINT_VERSION ?= v2.6.0 KUTTL_VERSION ?= 0.22.0 From 1be5cd12e68a553a3801f76cc92334f995c8da89 Mon Sep 17 00:00:00 2001 From: Lukas Piwowarski Date: Wed, 24 Jun 2026 08:09:51 -0400 Subject: [PATCH 2/2] Clean up LLMEndpointType Clean up LLMEndpointType providers by dropping providers unsupported by OGX and lightspeed-core/lightspeed-stack: 1) BAM is no longer supported by IBM and was replaced by watsonx.ai [1]. 2) fake_providers is an artifact that persisted through the migration from openshift/lightspeed-operator. This provider is supported only by openshift/lightspeed-service and not lightspeed-core/lightspeed-stack [2]. Additionally, add unit tests for buildLlamaStackModels and buildLlamaStackInferenceProviders to ground the configuration. Note: this commit aims to align the providers configuration as closely as possible with the recommended configuration in lightspeed-core/lightspeed-stack [3]. It was not tested with the actual providers. Additional testing is likely required and may lead to minor tweaks in the providers code. Also, address lint issues discovered by the pre-commit checks (replace repeating strings with constants). [1] https://github.com/openshift/lightspeed-service/pull/2827 [2] https://github.com/openshift/lightspeed-service/blob/main/docs/ai/providers.md [3] https://github.com/lightspeed-core/lightspeed-stack/tree/main/examples --- api/v1beta1/openstacklightspeed_types.go | 2 +- ...ed.openstack.org_openstacklightspeeds.yaml | 2 - ...ed.openstack.org_openstacklightspeeds.yaml | 2 - internal/controller/constants.go | 10 +- internal/controller/lcore_deployment.go | 4 +- internal/controller/llama_stack_config.go | 20 ++- .../controller/llama_stack_config_test.go | 151 ++++++++++++++++++ .../openstacklightspeed_controller_test.go | 2 +- 8 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 internal/controller/llama_stack_config_test.go diff --git a/api/v1beta1/openstacklightspeed_types.go b/api/v1beta1/openstacklightspeed_types.go index 6bce34ed..827c4f40 100644 --- a/api/v1beta1/openstacklightspeed_types.go +++ b/api/v1beta1/openstacklightspeed_types.go @@ -155,7 +155,7 @@ type OpenStackLightspeedCore struct { LLMEndpoint string `json:"llmEndpoint"` // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=azure_openai;bam;openai;watsonx;rhoai_vllm;rhelai_vllm;fake_provider;gemini + // +kubebuilder:validation:Enum=azure_openai;openai;watsonx;rhoai_vllm;rhelai_vllm;gemini // +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Provider Type" // Type of the provider serving the LLM LLMEndpointType string `json:"llmEndpointType"` diff --git a/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml b/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml index 73b110a9..8176063e 100644 --- a/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/bundle/manifests/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -103,12 +103,10 @@ spec: description: Type of the provider serving the LLM enum: - azure_openai - - bam - openai - watsonx - rhoai_vllm - rhelai_vllm - - fake_provider - gemini type: string llmProjectID: diff --git a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml index e90ba4d5..2612ab58 100644 --- a/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml +++ b/config/crd/bases/lightspeed.openstack.org_openstacklightspeeds.yaml @@ -103,12 +103,10 @@ spec: description: Type of the provider serving the LLM enum: - azure_openai - - bam - openai - watsonx - rhoai_vllm - rhelai_vllm - - fake_provider - gemini type: string llmProjectID: diff --git a/internal/controller/constants.go b/internal/controller/constants.go index 20b74038..b3c4920b 100644 --- a/internal/controller/constants.go +++ b/internal/controller/constants.go @@ -109,8 +109,14 @@ const ( ConsoleProxyAlias = "ols" ConsoleUINetworkPolicyName = "lightspeed-console-plugin" - // Azure - AzureOpenAIType = "azure_openai" + // Provider name constants representing valid values for + // OpenStackLightpseed.Spec.LLMEndpointType (providers available to users) + RHELAIVLLMProviderName = "rhelai_vllm" + RHOAIVLLMProviderName = "rhoai_vllm" + GeminiProviderName = "gemini" + AzureOpenAIProviderName = "azure_openai" + OpenAIProviderName = "openai" + WatsonXProviderName = "watsonx" // EnvVarSuffixAPIKey is the environment variable suffix for API key credentials EnvVarSuffixAPIKey = "_API_KEY" diff --git a/internal/controller/lcore_deployment.go b/internal/controller/lcore_deployment.go index e5ac3bbf..6f8d24d6 100644 --- a/internal/controller/lcore_deployment.go +++ b/internal/controller/lcore_deployment.go @@ -458,7 +458,7 @@ func buildLlamaStackEnvVars(h *common_helper.Helper, ctx context.Context, instan envVarName := providerNameToEnvVarName(provider.Name) - if provider.Type == AzureOpenAIType { + if provider.Type == AzureOpenAIProviderName { // Azure supports both API key and client credentials authentication. // Read the secret to determine which fields are present. secret := &corev1.Secret{} @@ -535,7 +535,7 @@ func buildLlamaStackEnvVars(h *common_helper.Helper, ctx context.Context, instan // For vLLM providers, also set the URL environment variable // The vLLM adapter checks for VLLM_URL as a fallback if URL is not in config - if provider.Type == "rhoai_vllm" || provider.Type == "rhelai_vllm" { + if provider.Type == RHOAIVLLMProviderName || provider.Type == RHELAIVLLMProviderName { if provider.URL != "" { envVars = append(envVars, corev1.EnvVar{ Name: "VLLM_URL", diff --git a/internal/controller/llama_stack_config.go b/internal/controller/llama_stack_config.go index 20e0e4d5..b96bce40 100644 --- a/internal/controller/llama_stack_config.go +++ b/internal/controller/llama_stack_config.go @@ -122,17 +122,17 @@ func buildLlamaStackInferenceProviders(_ *common_helper.Helper, _ context.Contex // Map provider types to Llama Stack provider types switch provider.Type { - case "openai", "gemini", "rhoai_vllm", "rhelai_vllm": + case OpenAIProviderName, GeminiProviderName, RHOAIVLLMProviderName, RHELAIVLLMProviderName: config := map[string]interface{}{} // Determine the appropriate Llama Stack provider type: // - OpenAI uses remote::openai // - vLLM uses remote::vllm var apiKeyField string switch provider.Type { - case "openai": + case OpenAIProviderName: providerConfig["provider_type"] = "remote::openai" apiKeyField = "api_key" - case "gemini": + case GeminiProviderName: providerConfig["provider_type"] = "remote::gemini" apiKeyField = "api_key" default: @@ -149,7 +149,7 @@ func buildLlamaStackInferenceProviders(_ *common_helper.Helper, _ context.Contex providerConfig["config"] = config - case "azure_openai": + case AzureOpenAIProviderName: providerConfig["provider_type"] = "remote::azure" config := map[string]interface{}{} @@ -174,7 +174,17 @@ func buildLlamaStackInferenceProviders(_ *common_helper.Helper, _ context.Contex } providerConfig["config"] = config - case "watsonx", "bam": + case WatsonXProviderName: + providerConfig["provider_type"] = "remote::watsonx" + + config := map[string]interface{}{} + config["base_url"] = provider.URL + config["project_id"] = provider.WatsonProjectID + config["api_key"] = fmt.Sprintf("${env.%s_API_KEY:=}", envVarName) + + providerConfig["config"] = config + + case "bam": // These providers are not supported by Llama Stack // They are handled directly by lightspeed-stack (LCS), not Llama Stack return nil, fmt.Errorf("provider type '%s' (provider '%s') is not currently supported by Llama Stack. Supported types: openai, gemini, azure_openai, rhoai_vllm, rhelai_vllm", provider.Type, provider.Name) diff --git a/internal/controller/llama_stack_config_test.go b/internal/controller/llama_stack_config_test.go new file mode 100644 index 00000000..485f47eb --- /dev/null +++ b/internal/controller/llama_stack_config_test.go @@ -0,0 +1,151 @@ +package controller + +import ( + "context" + "fmt" + + apiv1beta1 "github.com/openstack-lightspeed/operator/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func expectSentenceTransformersProvider(providers []interface{}) { + sentenceTransformers := providers[0].(map[string]interface{}) + Expect(sentenceTransformers["provider_id"]).To(Equal("sentence-transformers")) + Expect(sentenceTransformers["provider_type"]).To(Equal("inline::sentence-transformers")) +} + +func getOpenStackLightspeedProvidersInstance(provider string) *apiv1beta1.OpenStackLightspeed { + instance := &apiv1beta1.OpenStackLightspeed{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openstack-lightspeed", + Namespace: "openstack-lightspeed", + }, + } + + switch provider { + case OpenAIProviderName: + instance.Spec.LLMEndpointType = OpenAIProviderName + instance.Spec.LLMEndpoint = "https://api.openai.com/v1" + instance.Spec.ModelName = "gpt-4o" + return instance + case GeminiProviderName: + instance.Spec.LLMEndpointType = GeminiProviderName + instance.Spec.ModelName = "gemini-2.0-flash" + return instance + case RHOAIVLLMProviderName: + instance.Spec.LLMEndpointType = RHOAIVLLMProviderName + instance.Spec.LLMEndpoint = "https://vllm.example.com/v1" + instance.Spec.ModelName = "meta-llama/Llama-3.1-70B-Instruct" + return instance + case RHELAIVLLMProviderName: + instance.Spec.LLMEndpointType = RHELAIVLLMProviderName + instance.Spec.LLMEndpoint = "https://rhelai-vllm.example.com/v1" + instance.Spec.ModelName = "meta-llama/Llama-3.1-70B-Instruct" + return instance + case AzureOpenAIProviderName: + instance.Spec.LLMEndpointType = AzureOpenAIProviderName + instance.Spec.LLMEndpoint = "https://my-resource.openai.azure.com" + instance.Spec.LLMDeploymentName = "gpt-4o-deployment" + instance.Spec.LLMAPIVersion = "2024-02-01" + instance.Spec.ModelName = "gpt-4o" + return instance + case WatsonXProviderName: + instance.Spec.LLMEndpointType = WatsonXProviderName + instance.Spec.LLMEndpoint = "https://watsonx.example.com" + instance.Spec.LLMProjectID = "test-project-id" + instance.Spec.ModelName = "ibm/granite-13b-chat-v2" + return instance + default: + Fail(fmt.Sprintf("Unknown provider %s", provider)) + } + + return nil +} + +func checkModelCommonConfig(modelConfig map[string]interface{}, instance *apiv1beta1.OpenStackLightspeed) { + Expect(modelConfig["model_id"]).To(Equal(instance.Spec.ModelName)) + Expect(modelConfig["model_type"]).To(Equal("llm")) + Expect(modelConfig["provider_id"]).To(Equal(OpenStackLightspeedDefaultProvider)) + Expect(modelConfig["provider_model_id"]).To(Equal(instance.Spec.ModelName)) + Expect(modelConfig).NotTo(HaveKey("metadata")) +} + +var _ = Describe("Llama Stack config", func() { + Describe("buildLlamaStackInferenceProviders", func() { + DescribeTable("should return correct inference providers config", + func(provider, providerType string, checkConfig func(map[string]interface{}, *apiv1beta1.OpenStackLightspeed)) { + instance := getOpenStackLightspeedProvidersInstance(provider) + inferenceProvidersConfig, err := buildLlamaStackInferenceProviders(nil, context.Background(), instance) + + Expect(err).NotTo(HaveOccurred()) + Expect(inferenceProvidersConfig).To(HaveLen(2)) + + expectSentenceTransformersProvider(inferenceProvidersConfig) + + inferenceProvider := inferenceProvidersConfig[1].(map[string]interface{}) + Expect(inferenceProvider["provider_id"]).To(Equal(OpenStackLightspeedDefaultProvider)) + Expect(inferenceProvider["provider_type"]).To(Equal(providerType)) + + checkConfig(inferenceProvider["config"].(map[string]interface{}), instance) + }, + Entry("for openai", OpenAIProviderName, "remote::openai", + func(config map[string]interface{}, _ *apiv1beta1.OpenStackLightspeed) { + Expect(config["api_key"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY}")) + }), + Entry("for gemini", GeminiProviderName, "remote::gemini", + func(config map[string]interface{}, _ *apiv1beta1.OpenStackLightspeed) { + Expect(config["api_key"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY}")) + Expect(config).NotTo(HaveKey("base_url")) + }), + Entry("for rhoai_vllm", RHOAIVLLMProviderName, "remote::vllm", + func(config map[string]interface{}, instance *apiv1beta1.OpenStackLightspeed) { + Expect(config["api_token"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY}")) + Expect(config["base_url"]).To(Equal(instance.Spec.LLMEndpoint)) + }), + Entry("for rhelai_vllm", RHELAIVLLMProviderName, "remote::vllm", + func(config map[string]interface{}, instance *apiv1beta1.OpenStackLightspeed) { + Expect(config["api_token"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY}")) + Expect(config["base_url"]).To(Equal(instance.Spec.LLMEndpoint)) + }), + Entry("for azure_openai", AzureOpenAIProviderName, "remote::azure", + func(config map[string]interface{}, instance *apiv1beta1.OpenStackLightspeed) { + Expect(config["api_key"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY}")) + Expect(config["client_id"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_CLIENT_ID:=}")) + Expect(config["tenant_id"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_TENANT_ID:=}")) + Expect(config["client_secret"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_CLIENT_SECRET:=}")) + Expect(config["api_base"]).To(Equal(instance.Spec.LLMEndpoint)) + Expect(config["deployment_name"]).To(Equal(instance.Spec.LLMDeploymentName)) + Expect(config["api_version"]).To(Equal(instance.Spec.LLMAPIVersion)) + }), + Entry("for watsonx", WatsonXProviderName, "remote::watsonx", + func(config map[string]interface{}, instance *apiv1beta1.OpenStackLightspeed) { + Expect(config["base_url"]).To(Equal(instance.Spec.LLMEndpoint)) + Expect(config["project_id"]).To(Equal(instance.Spec.LLMProjectID)) + Expect(config["api_key"]).To(Equal("${env.OPENSTACK_LIGHTSPEED_PROVIDER_API_KEY:=}")) + }), + ) + }) + + Describe("buildLlamaStackModels", func() { + DescribeTable("should return correct models config", + func(provider string) { + instance := getOpenStackLightspeedProvidersInstance(provider) + modelsConfig := buildLlamaStackModels(nil, instance) + + Expect(modelsConfig).To(HaveLen(1)) + + modelConfig := modelsConfig[0].(map[string]interface{}) + checkModelCommonConfig(modelConfig, instance) + }, + Entry("for openai", OpenAIProviderName), + Entry("for gemini", GeminiProviderName), + Entry("for rhoai_vllm", RHOAIVLLMProviderName), + Entry("for rhelai_vllm", RHELAIVLLMProviderName), + Entry("for azure_openai", AzureOpenAIProviderName), + Entry("for watsonx", WatsonXProviderName), + ) + }) +}) diff --git a/internal/controller/openstacklightspeed_controller_test.go b/internal/controller/openstacklightspeed_controller_test.go index 3bba0b2e..b00b64c1 100644 --- a/internal/controller/openstacklightspeed_controller_test.go +++ b/internal/controller/openstacklightspeed_controller_test.go @@ -54,7 +54,7 @@ var _ = Describe("OpenStackLightspeed Controller", func() { Spec: apiv1beta1.OpenStackLightspeedSpec{ OpenStackLightspeedCore: apiv1beta1.OpenStackLightspeedCore{ LLMEndpoint: "https://example.com/llm", - LLMEndpointType: "openai", + LLMEndpointType: OpenAIProviderName, ModelName: "test-model", }, },