diff --git a/go/core/cli/internal/cli/agent/install.go b/go/core/cli/internal/cli/agent/install.go index 27f5577c9..34b031b95 100644 --- a/go/core/cli/internal/cli/agent/install.go +++ b/go/core/cli/internal/cli/agent/install.go @@ -24,6 +24,24 @@ type InstallCfg struct { Profile string } +func resolveInstallProfile(profile string) string { + profile = strings.TrimSpace(profile) + if profile == "" { + return "" + } + + if slices.Contains(profiles.Profiles, profile) { + return profile + } + + fmt.Fprintf(os.Stderr, "Invalid --profile value (%s), defaulting to demo\n", profile) + return profiles.ProfileDemo +} + +func shouldRequireProviderCredentials(profile string, modelProvider v1alpha2.ModelProvider) bool { + return profiles.InstallsDefaultModelConfig(profile) && GetProviderAPIKey(modelProvider) != "" +} + // installChart installs or upgrades a Helm chart with the given parameters func installChart(ctx context.Context, chartName string, namespace string, registry string, version string, setValues []string, inlineValues string) (string, error) { args := []string{ @@ -78,27 +96,23 @@ func InstallCmd(ctx context.Context, cfg *InstallCfg) *PortForward { // get model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable or use DefaultModelProvider modelProvider := GetModelProvider() + selectedProfile := resolveInstallProfile(cfg.Profile) - // If model provider is openai, check if the API key is set apiKeyName := GetProviderAPIKey(modelProvider) apiKeyValue := os.Getenv(apiKeyName) - if apiKeyName != "" && apiKeyValue == "" { + if shouldRequireProviderCredentials(selectedProfile, modelProvider) && apiKeyValue == "" { fmt.Fprintf(os.Stderr, "%s is not set\n", apiKeyName) fmt.Fprintf(os.Stderr, "Please set the %s environment variable\n", apiKeyName) return nil } - helmConfig := setupHelmConfig(modelProvider, apiKeyValue) + helmConfig := setupHelmConfig(modelProvider, apiKeyValue, profiles.InstallsDefaultModelConfig(selectedProfile)) // setup profile if provided - if cfg.Profile = strings.TrimSpace(cfg.Profile); cfg.Profile != "" { - if !slices.Contains(profiles.Profiles, cfg.Profile) { - fmt.Fprintf(os.Stderr, "Invalid --profile value (%s), defaulting to demo\n", cfg.Profile) - cfg.Profile = profiles.ProfileDemo - } - - helmConfig.inlineValues = profiles.GetProfileYaml(cfg.Profile) + if selectedProfile != "" { + cfg.Profile = selectedProfile + helmConfig.inlineValues = profiles.GetProfileYaml(selectedProfile) } return install(ctx, cfg.Config, helmConfig, modelProvider) @@ -120,22 +134,19 @@ func InteractiveInstallCmd(ctx context.Context, c *ishell.Context) *PortForward // get model provider from KAGENT_DEFAULT_MODEL_PROVIDER environment variable or use DefaultModelProvider modelProvider := GetModelProvider() - // if model provider is openai, check if the api key is set + // Add profile selection + profileIdx := c.MultiChoice(profiles.Profiles, "Select a profile:") + selectedProfile := profiles.Profiles[profileIdx] + apiKeyName := GetProviderAPIKey(modelProvider) apiKeyValue := os.Getenv(apiKeyName) - - if apiKeyName != "" && apiKeyValue == "" { + if shouldRequireProviderCredentials(selectedProfile, modelProvider) && apiKeyValue == "" { fmt.Fprintf(os.Stderr, "%s is not set\n", apiKeyName) fmt.Fprintf(os.Stderr, "Please set the %s environment variable\n", apiKeyName) return nil } - helmConfig := setupHelmConfig(modelProvider, apiKeyValue) - - // Add profile selection - profileIdx := c.MultiChoice(profiles.Profiles, "Select a profile:") - selectedProfile := profiles.Profiles[profileIdx] - + helmConfig := setupHelmConfig(modelProvider, apiKeyValue, profiles.InstallsDefaultModelConfig(selectedProfile)) helmConfig.inlineValues = profiles.GetProfileYaml(selectedProfile) return install(ctx, cfg, helmConfig, modelProvider) @@ -153,12 +164,14 @@ type helmConfig struct { // setupHelmConfig sets up the helm config for the kagent chart // This sets up the general configuration for a helm installation without the profile, which is calculated later based on the installation type (interactive or non-interactive) -func setupHelmConfig(modelProvider v1alpha2.ModelProvider, apiKeyValue string) helmConfig { - // Build Helm values - helmProviderKey := GetModelProviderHelmValuesKey(modelProvider) - values := []string{ - fmt.Sprintf("providers.default=%s", helmProviderKey), - fmt.Sprintf("providers.%s.apiKey=%s", helmProviderKey, apiKeyValue), +func setupHelmConfig(modelProvider v1alpha2.ModelProvider, apiKeyValue string, installDefaultModelConfig bool) helmConfig { + values := []string{} + if installDefaultModelConfig { + helmProviderKey := GetModelProviderHelmValuesKey(modelProvider) + values = append(values, fmt.Sprintf("providers.default=%s", helmProviderKey)) + if apiKeyValue != "" { + values = append(values, fmt.Sprintf("providers.%s.apiKey=%s", helmProviderKey, apiKeyValue)) + } } // allow user to set the helm registry and version diff --git a/go/core/cli/internal/cli/agent/install_test.go b/go/core/cli/internal/cli/agent/install_test.go new file mode 100644 index 000000000..f3f712544 --- /dev/null +++ b/go/core/cli/internal/cli/agent/install_test.go @@ -0,0 +1,82 @@ +package cli + +import ( + "testing" + + "github.com/kagent-dev/kagent/go/api/v1alpha2" + "github.com/kagent-dev/kagent/go/core/cli/internal/profiles" + "github.com/kagent-dev/kagent/go/core/pkg/env" + "github.com/stretchr/testify/assert" +) + +func TestResolveInstallProfile(t *testing.T) { + t.Run("empty profile remains empty", func(t *testing.T) { + assert.Equal(t, "", resolveInstallProfile("")) + }) + + t.Run("valid profile is preserved", func(t *testing.T) { + assert.Equal(t, profiles.ProfileMinimal, resolveInstallProfile(" minimal ")) + }) + + t.Run("invalid profile falls back to demo", func(t *testing.T) { + assert.Equal(t, profiles.ProfileDemo, resolveInstallProfile("unknown")) + }) +} + +func TestShouldRequireProviderCredentials(t *testing.T) { + tests := []struct { + name string + profile string + modelProvider v1alpha2.ModelProvider + want bool + }{ + { + name: "default install requires credentials for openai", + profile: "", + modelProvider: v1alpha2.ModelProviderOpenAI, + want: true, + }, + { + name: "minimal install skips credentials for openai", + profile: profiles.ProfileMinimal, + modelProvider: v1alpha2.ModelProviderOpenAI, + want: false, + }, + { + name: "demo install still requires credentials for anthropic", + profile: profiles.ProfileDemo, + modelProvider: v1alpha2.ModelProviderAnthropic, + want: true, + }, + { + name: "ollama never requires credentials", + profile: profiles.ProfileDemo, + modelProvider: v1alpha2.ModelProviderOllama, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, shouldRequireProviderCredentials(tt.profile, tt.modelProvider)) + }) + } +} + +func TestSetupHelmConfig(t *testing.T) { + t.Setenv(env.KagentHelmRepo.Name(), "") + t.Setenv(env.KagentHelmVersion.Name(), "") + t.Setenv(env.KagentHelmExtraArgs.Name(), "") + + t.Run("includes provider values when default modelconfig is installed", func(t *testing.T) { + cfg := setupHelmConfig(v1alpha2.ModelProviderOpenAI, "test-key", true) + assert.Contains(t, cfg.values, "providers.default=openAI") + assert.Contains(t, cfg.values, "providers.openAI.apiKey=test-key") + }) + + t.Run("omits provider values when default modelconfig is disabled", func(t *testing.T) { + cfg := setupHelmConfig(v1alpha2.ModelProviderOpenAI, "test-key", false) + assert.NotContains(t, cfg.values, "providers.default=openAI") + assert.NotContains(t, cfg.values, "providers.openAI.apiKey=test-key") + }) +} diff --git a/go/core/cli/internal/profiles/README.md b/go/core/cli/internal/profiles/README.md index 36c6fc182..4b1d1d658 100644 --- a/go/core/cli/internal/profiles/README.md +++ b/go/core/cli/internal/profiles/README.md @@ -4,6 +4,6 @@ KAgent's profiles provide a simpler way to set up KAgent in a configured way bas Currently, there are two profiles: 1. `Demo`: For an installation of kagent that includes all our agents. This is useful for demo purposes and new users. -2. `Minimal`: (default) For an installation that does not include any pre-defined agent. This is useful for users who want to start from scratch. +2. `Minimal`: (default) For an installation that does not include any pre-defined agent or a default model configuration. This is useful for users who want to start from scratch. **Important**: When adding a new profile or updating a name, make sure to update the proper embeddings for it. diff --git a/go/core/cli/internal/profiles/minimal.yaml b/go/core/cli/internal/profiles/minimal.yaml index 59cc778d5..d13c12a4a 100644 --- a/go/core/cli/internal/profiles/minimal.yaml +++ b/go/core/cli/internal/profiles/minimal.yaml @@ -1,5 +1,8 @@ # The minimal profile does not install any agents, and is meant as a bare minimum installation for kagent. # This is useful for users who only want to set up kagent without any extra agents. +providers: + createDefaultModelConfig: false + agents: k8s-agent: enabled: false diff --git a/go/core/cli/internal/profiles/profiles.go b/go/core/cli/internal/profiles/profiles.go index 3074eee71..a3ad9f580 100644 --- a/go/core/cli/internal/profiles/profiles.go +++ b/go/core/cli/internal/profiles/profiles.go @@ -25,3 +25,12 @@ func GetProfileYaml(profile string) string { return DemoProfileYaml } } + +func InstallsDefaultModelConfig(profile string) bool { + switch profile { + case ProfileMinimal: + return false + default: + return true + } +} diff --git a/helm/kagent/templates/modelconfig-secret.yaml b/helm/kagent/templates/modelconfig-secret.yaml index 804c90d6d..b5531d589 100644 --- a/helm/kagent/templates/modelconfig-secret.yaml +++ b/helm/kagent/templates/modelconfig-secret.yaml @@ -1,5 +1,10 @@ +{{- if ne .Values.providers.createDefaultModelConfig false }} {{- $dot := . }} -{{- $model := index $dot.Values.providers $dot.Values.providers.default }} +{{- $defaultProvider := $dot.Values.providers.default | default "openAI" }} +{{- if hasKey $dot.Values.providers $defaultProvider | not }} +{{- fail (printf "Provider key=%s is not found under .Values.providers" $defaultProvider) }} +{{- end }} +{{- $model := index $dot.Values.providers $defaultProvider }} {{- if and $model.apiKeySecretRef $model.apiKey }} --- apiVersion: v1 @@ -13,3 +18,4 @@ type: Opaque data: {{ $model.apiKeySecretKey | default (printf "%s_API_KEY" $model.provider | upper) }}: {{ $model.apiKey | b64enc }} {{- end }} +{{- end }} diff --git a/helm/kagent/templates/modelconfig.yaml b/helm/kagent/templates/modelconfig.yaml index 4c7651e51..074386978 100644 --- a/helm/kagent/templates/modelconfig.yaml +++ b/helm/kagent/templates/modelconfig.yaml @@ -1,3 +1,4 @@ +{{- if ne .Values.providers.createDefaultModelConfig false }} {{- $dot := . }} {{- $defaultProfider := .Values.providers.default | default "openAI" }} {{- $model := index .Values.providers $defaultProfider }} @@ -31,3 +32,4 @@ spec: {{- toYaml $model.config | nindent 4 }} {{- end }} {{- end }} +{{- end }} diff --git a/helm/kagent/tests/modelconfig-secret_test.yaml b/helm/kagent/tests/modelconfig-secret_test.yaml index 88db33953..fd736bbbf 100644 --- a/helm/kagent/tests/modelconfig-secret_test.yaml +++ b/helm/kagent/tests/modelconfig-secret_test.yaml @@ -41,6 +41,20 @@ tests: path: data.ANTHROPIC_API_KEY value: YW50aHJvcGljLXRlc3Qta2V5 # base64 of "anthropic-test-key" + - it: should fall back to openai secret when default provider is empty + set: + providers: + default: "" + openAI: + apiKey: "fallback-openai-key" + asserts: + - equal: + path: metadata.name + value: kagent-openai + - equal: + path: data.OPENAI_API_KEY + value: ZmFsbGJhY2stb3BlbmFpLWtleQ== # base64 of "fallback-openai-key" + - it: should render azure openai secret when azure provider is default set: providers: @@ -99,4 +113,14 @@ tests: asserts: - equal: path: metadata.namespace - value: custom-namespace \ No newline at end of file + value: custom-namespace + + - it: should not render secret when default modelconfig is disabled + set: + providers: + createDefaultModelConfig: false + openAI: + apiKey: "test-key" + asserts: + - hasDocuments: + count: 0 diff --git a/helm/kagent/tests/modelconfig_test.yaml b/helm/kagent/tests/modelconfig_test.yaml index 304aa4001..bad730892 100644 --- a/helm/kagent/tests/modelconfig_test.yaml +++ b/helm/kagent/tests/modelconfig_test.yaml @@ -113,4 +113,12 @@ tests: asserts: - equal: path: metadata.namespace - value: custom-namespace \ No newline at end of file + value: custom-namespace + + - it: should not render modelconfig when disabled + set: + providers: + createDefaultModelConfig: false + asserts: + - hasDocuments: + count: 0 diff --git a/helm/kagent/values.yaml b/helm/kagent/values.yaml index e06adbdb4..9acaa59ff 100644 --- a/helm/kagent/values.yaml +++ b/helm/kagent/values.yaml @@ -266,6 +266,8 @@ ui: # https://kagent.dev/docs/getting-started/configuring-providers providers: + # -- Create the default ModelConfig resource during installation. + createDefaultModelConfig: true default: openAI openAI: provider: OpenAI