Skip to content
Merged
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
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ KUBECONFIG ?= $(HOME)/.kube/karmada.config

# API server common flags
API_COMMON_FLAGS = \
$(if $(OPENAI_API_KEY),--openai-api-key="$(OPENAI_API_KEY)") \
$(if $(OPENAI_MODEL),--openai-model="$(OPENAI_MODEL)",--openai-model="gpt-3.5-turbo") \
$(if $(OPENAI_ENDPOINT),--openai-endpoint="$(OPENAI_ENDPOINT)",--openai-endpoint="https://api.openai.com/v1") \
$(if $(LLM_API_KEY),--llm-api-key="$(LLM_API_KEY)") \
$(if $(LLM_MODEL),--llm-model="$(LLM_MODEL)",--llm-model="gpt-3.5-turbo") \
$(if $(LLM_ENDPOINT),--llm-endpoint="$(LLM_ENDPOINT)",--llm-endpoint="https://api.openai.com/v1") \
$(if $(filter true,$(ENABLE_MCP)),--enable-mcp=true) \
$(if $(MCP_TRANSPORT_MODE),--mcp-transport-mode="$(MCP_TRANSPORT_MODE)",--mcp-transport-mode="stdio") \
$(if $(KARMADA_MCP_SERVER_PATH),--mcp-server-path="$(KARMADA_MCP_SERVER_PATH)") \
Expand Down
19 changes: 11 additions & 8 deletions cmd/api/app/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package options

import (
"net"
"time"

"github.com/spf13/pflag"
)
Expand All @@ -44,10 +45,11 @@ type Options struct {
MCPServerPath string
MCPSSEEndpoint string

// OpenAI related options
OpenAIAPIKey string
OpenAIModel string
OpenAIEndpoint string
// LLM related options
LLMAPIKey string
LLMModel string
LLMEndpoint string
LLMTimeout time.Duration
}

// NewOptions returns initialized Options.
Expand Down Expand Up @@ -80,8 +82,9 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&o.MCPServerPath, "mcp-server-path", "", "Path to the MCP server binary (required for stdio mode)")
fs.StringVar(&o.MCPSSEEndpoint, "mcp-sse-endpoint", "", "MCP SSE endpoint URL (required for sse mode)")

// OpenAI related flags
fs.StringVar(&o.OpenAIAPIKey, "openai-api-key", "", "OpenAI API key for AI assistant functionality")
fs.StringVar(&o.OpenAIModel, "openai-model", "gpt-3.5-turbo", "OpenAI model to use for AI assistant")
fs.StringVar(&o.OpenAIEndpoint, "openai-endpoint", "https://api.openai.com/v1", "OpenAI API endpoint URL")
// LLM related flags
fs.StringVar(&o.LLMAPIKey, "llm-api-key", "", "LLM API key for AI assistant functionality")
fs.StringVar(&o.LLMModel, "llm-model", "gpt-3.5-turbo", "LLM model to use for AI assistant")
fs.StringVar(&o.LLMEndpoint, "llm-endpoint", "https://api.openai.com/v1", "LLM API endpoint URL")
fs.DurationVar(&o.LLMTimeout, "llm-timeout", 30*time.Second, "Timeout for LLM API requests")
}
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ require (
github.com/gobuffalo/flect v1.0.3
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/karmada-io/karmada v1.15.0
github.com/mark3labs/mcp-go v0.26.0
github.com/mark3labs/mcp-go v0.42.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/common v0.65.0
github.com/sashabaranov/go-openai v1.40.5
github.com/sashabaranov/go-openai v1.41.2
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.6
golang.org/x/net v0.40.0
Expand All @@ -42,8 +42,10 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Yiling-J/theine-go v0.6.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
Expand Down Expand Up @@ -77,6 +79,7 @@ require (
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand Down Expand Up @@ -109,6 +112,7 @@ require (
github.com/spf13/cast v1.7.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
Expand Down
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ github.com/Yiling-J/theine-go v0.6.0 h1:jv7V/tcD6ijL0T4kfbJDKP81TCZBkoriNTPSqwiv
github.com/Yiling-J/theine-go v0.6.0/go.mod h1:mdch1vjgGWd7s3rWKvY+MF5InRLfRv/CWVI9RVNQ8wY=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
Expand Down Expand Up @@ -122,6 +126,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
Expand Down Expand Up @@ -158,8 +164,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.26.0 h1:xz/Kv1cHLYovF8txv6btBM39/88q3YOjnxqhi51jB0w=
github.com/mark3labs/mcp-go v0.26.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mark3labs/mcp-go v0.42.0 h1:gk/8nYJh8t3yroCAOBhNbYsM9TCKvkM13I5t5Hfu6Ls=
github.com/mark3labs/mcp-go v0.42.0/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
Expand Down Expand Up @@ -214,8 +220,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sashabaranov/go-openai v1.40.5 h1:SwIlNdWflzR1Rxd1gv3pUg6pwPc6cQ2uMoHs8ai+/NY=
github.com/sashabaranov/go-openai v1.40.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
Expand All @@ -242,6 +248,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
Expand Down
143 changes: 143 additions & 0 deletions pkg/llm/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
Copyright 2025 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package llm

import (
"context"
"fmt"
"net/http"
"sync"
"time"

"github.com/sashabaranov/go-openai"
"k8s.io/klog/v2"
)

var (
globalLLMConfig *Config
globalLLMClient *openai.Client
llmInitialized bool
llmMutex sync.RWMutex
)

// InitLLMConfig initializes LLM configuration from Config.
func InitLLMConfig(config *Config) {
llmMutex.Lock()
defer llmMutex.Unlock()

globalLLMConfig = config
globalLLMClient = nil // Reset client to force recreation
llmInitialized = true

klog.InfoS("LLM configuration initialized",
"hasAPIKey", config.LLMAPIKey != "",
"model", config.LLMModel,
"endpoint", config.LLMEndpoint,
"timeout", config.Timeout)
}

// GetLLMClient returns a configured LLM client (singleton).
// Note: This assumes the configured endpoint supports OpenAI API format.
// For incompatible providers, consider using their native SDKs directly.
func GetLLMClient() (*openai.Client, error) {
llmMutex.Lock()
defer llmMutex.Unlock()

// Return existing client if already created (singleton)
if globalLLMClient != nil {
return globalLLMClient, nil
}

if !llmInitialized || globalLLMConfig == nil {
return nil, fmt.Errorf("%w: call InitLLMConfig first", ErrLLMNotInitialized)
}

// Validate configuration before creating client
if err := globalLLMConfig.Validate(); err != nil {
return nil, fmt.Errorf("invalid LLM configuration: %w", err)
}

// Create new client
config := openai.DefaultConfig(globalLLMConfig.LLMAPIKey)
if globalLLMConfig.LLMEndpoint != "" {
config.BaseURL = globalLLMConfig.LLMEndpoint
}

// Configure HTTP client with timeout
config.HTTPClient = &http.Client{
Timeout: globalLLMConfig.Timeout,
}

globalLLMClient = openai.NewClientWithConfig(config)

klog.InfoS("LLM client created successfully",
"endpoint", config.BaseURL,
"model", globalLLMConfig.LLMModel,
"timeout", globalLLMConfig.Timeout)

return globalLLMClient, nil
}

// GetLLMModel returns the configured LLM model.
func GetLLMModel() string {
llmMutex.RLock()
defer llmMutex.RUnlock()

if !llmInitialized || globalLLMConfig == nil || globalLLMConfig.LLMModel == "" {
return openai.GPT3Dot5Turbo // default value
}
return globalLLMConfig.LLMModel
}

// IsLLMConfigured returns true if LLM is properly configured.
func IsLLMConfigured() bool {
llmMutex.RLock()
defer llmMutex.RUnlock()

return llmInitialized && globalLLMConfig != nil && globalLLMConfig.LLMAPIKey != ""
}

// ValidateLLMConnection performs a health check to validate the LLM connection.
// This function sends a simple API request to verify that the endpoint is reachable
// and compatible with the OpenAI API format.
func ValidateLLMConnection(ctx context.Context) error {
client, err := GetLLMClient()
if err != nil {
return fmt.Errorf("%w: %v", ErrConnectionFailed, err)
}

// Create a context with timeout if none provided
if ctx == nil {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
}

// Send a simple models list request to validate connection
// This is a lightweight operation that verifies API compatibility
_, err = client.ListModels(ctx)
if err != nil {
klog.ErrorS(err, "LLM connection validation failed",
"endpoint", globalLLMConfig.LLMEndpoint)
return fmt.Errorf("%w: unable to communicate with LLM endpoint: %v",
ErrConnectionFailed, err)
}

klog.InfoS("LLM connection validated successfully",
"endpoint", globalLLMConfig.LLMEndpoint)
return nil
}
Loading