Skip to content
Draft
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
21 changes: 21 additions & 0 deletions .github/workflows/unit-tests.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion api/v1beta1/openstacklightspeed_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 8 additions & 2 deletions internal/controller/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/lcore_deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 15 additions & 5 deletions internal/controller/llama_stack_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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{}{}

Expand All @@ -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)
Expand Down
151 changes: 151 additions & 0 deletions internal/controller/llama_stack_config_test.go
Original file line number Diff line number Diff line change
@@ -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),
)
})
})
2 changes: 1 addition & 1 deletion internal/controller/openstacklightspeed_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
},
Expand Down
Loading