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
63 changes: 38 additions & 25 deletions go/core/cli/internal/cli/agent/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand Down
82 changes: 82 additions & 0 deletions go/core/cli/internal/cli/agent/install_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
2 changes: 1 addition & 1 deletion go/core/cli/internal/profiles/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions go/core/cli/internal/profiles/minimal.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 9 additions & 0 deletions go/core/cli/internal/profiles/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
8 changes: 7 additions & 1 deletion helm/kagent/templates/modelconfig-secret.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
Comment on lines +1 to 8
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This template indexes .Values.providers using .Values.providers.default without a fallback. If providers.default is unset/empty, Helm can error before reaching the apiKey checks. modelconfig.yaml defensively defaults the provider key to "openAI"; modelconfig-secret.yaml should mirror that defaulting (or otherwise guard against a missing providers.default) to keep chart behavior consistent and robust.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mirrored the modelconfig.yaml fallback here, so an empty providers.default now falls back to openAI instead of indexing a missing key. Added a Helm unit test in 8cb2bcf.

---
apiVersion: v1
Expand All @@ -13,3 +18,4 @@ type: Opaque
data:
{{ $model.apiKeySecretKey | default (printf "%s_API_KEY" $model.provider | upper) }}: {{ $model.apiKey | b64enc }}
{{- end }}
{{- end }}
2 changes: 2 additions & 0 deletions helm/kagent/templates/modelconfig.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{{- if ne .Values.providers.createDefaultModelConfig false }}
{{- $dot := . }}
{{- $defaultProfider := .Values.providers.default | default "openAI" }}
{{- $model := index .Values.providers $defaultProfider }}
Expand Down Expand Up @@ -31,3 +32,4 @@ spec:
{{- toYaml $model.config | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
26 changes: 25 additions & 1 deletion helm/kagent/tests/modelconfig-secret_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -99,4 +113,14 @@ tests:
asserts:
- equal:
path: metadata.namespace
value: custom-namespace
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
10 changes: 9 additions & 1 deletion helm/kagent/tests/modelconfig_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,12 @@ tests:
asserts:
- equal:
path: metadata.namespace
value: custom-namespace
value: custom-namespace

- it: should not render modelconfig when disabled
set:
providers:
createDefaultModelConfig: false
asserts:
- hasDocuments:
count: 0
2 changes: 2 additions & 0 deletions helm/kagent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading