diff --git a/SUPPORTED_MODELS.md b/SUPPORTED_MODELS.md index ba25da0e..2b9878db 100644 --- a/SUPPORTED_MODELS.md +++ b/SUPPORTED_MODELS.md @@ -83,6 +83,28 @@ K8sGPT supports a variety of AI/LLM providers (backends). Some providers have a ### Groq - **Model:** User-configurable (any model supported by Groq, e.g., `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`) +### Amazon Bedrock Mantle (OpenAI-compatible) +- **Auth:** Reads the API key from the `AWS_BEARER_TOKEN_BEDROCK` environment variable +- **Region:** Required — the endpoint is auto-constructed as `https://bedrock-mantle.{region}.api.aws/v1` +- **API:** Uses the OpenAI-compatible Chat Completions API (`/v1/chat/completions`) +- **Supported Models:** + + | Provider | Models | + |---|---| + | Anthropic | Claude Mythos Preview, Claude Opus 4.7 | + | DeepSeek | DeepSeek V3.2, DeepSeek-V3.1 | + | Google | Gemma 3 4B IT, Gemma 3 12B IT, Gemma 3 27B PT | + | MiniMax | MiniMax M2, M2.1, M2.5 | + | Mistral AI | Devstral 2 123B, Magistral Small 2509, Ministral 14B 3.0, Ministral 3 8B, Ministral 3B, Mistral Large 3, Voxtral Mini 3B, Voxtral Small 24B | + | Moonshot AI | Kimi K2 Thinking, Kimi K2.5 | + | NVIDIA | Nemotron Nano 9B v2, Nemotron Nano 12B v2 VL, Nemotron Nano 3 30B, Nemotron 3 Super 120B | + | OpenAI | gpt-oss-120b, gpt-oss-20b, GPT OSS Safeguard 120B, GPT OSS Safeguard 20B | + | Qwen | Qwen3 235B, Qwen3 32B, Qwen3 Coder 480B, Qwen3 Coder Next, Qwen3 Next 80B, Qwen3 VL 235B, Qwen3-Coder-30B | + | Writer | Palmyra Vision 7B | + | Z.AI | GLM 4.7, GLM 4.7 Flash, GLM 5 | + + > **Note:** For the latest model availability, see [Endpoint availability](https://docs.aws.amazon.com/bedrock/latest/userguide/models-endpoint-availability.html). + --- For more details on configuring each provider and model, refer to the official K8sGPT documentation and the provider's own documentation. diff --git a/cmd/auth/add.go b/cmd/auth/add.go index f4d6d452..c906f915 100644 --- a/cmd/auth/add.go +++ b/cmd/auth/add.go @@ -54,6 +54,9 @@ var addCmd = &cobra.Command{ if strings.ToLower(backend) == "amazonbedrockconverse" { _ = cmd.MarkFlagRequired("providerRegion") } + if strings.ToLower(backend) == "bedrockmantle" { + _ = cmd.MarkFlagRequired("providerRegion") + } if strings.ToLower(backend) == "ibmwatsonxai" { _ = cmd.MarkFlagRequired("providerId") } @@ -184,9 +187,9 @@ func init() { addCmd.Flags().Float32VarP(&temperature, "temperature", "t", 0.7, "The sampling temperature, value ranges between 0 ( output be more deterministic) and 1 (more random)") // add flag for azure open ai engine/deployment name addCmd.Flags().StringVarP(&engine, "engine", "e", "", "Azure AI deployment name (only for azureopenai backend)") - // add flag for amazonbedrock region name - addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, amazonbedrockconverse, googlevertexai backend)") - // add flag for vertexAI/WatsonxAI Project ID + //add flag for amazonbedrock region name + addCmd.Flags().StringVarP(&providerRegion, "providerRegion", "r", "", "Provider Region name (only for amazonbedrock, amazonbedrockconverse, bedrockmantle, googlevertexai backend)") + //add flag for vertexAI/WatsonxAI Project ID addCmd.Flags().StringVarP(&providerId, "providerId", "i", "", "Provider specific ID for e.g. project (only for googlevertexai/ibmwatsonxai backend)") // add flag for OCI Compartment ID addCmd.Flags().StringVarP(&compartmentId, "compartmentId", "k", "", "Compartment ID for generative AI model (only for oci backend)") diff --git a/pkg/ai/bedrockmantle.go b/pkg/ai/bedrockmantle.go new file mode 100644 index 00000000..fd0110d8 --- /dev/null +++ b/pkg/ai/bedrockmantle.go @@ -0,0 +1,94 @@ +package ai + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "os" + + "github.com/sashabaranov/go-openai" +) + +const bedrockMantleClientName = "bedrockmantle" + +type AmazonBedrockMantleClient struct { + nopCloser + + client *openai.Client + model string + temperature float32 + topP float32 +} + +func (c *AmazonBedrockMantleClient) Configure(config IAIConfig) error { + region := getRegion(config.GetProviderRegion()) + if region == "" { + return errors.New("provider region is required for bedrockmantle (e.g. us-east-1)") + } + + token := os.Getenv("AWS_BEARER_TOKEN_BEDROCK") + if token == "" { + return errors.New("AWS_BEARER_TOKEN_BEDROCK environment variable is required for bedrockmantle") + } + defaultConfig := openai.DefaultConfig(token) + + baseURL := config.GetBaseURL() + if baseURL != "" { + defaultConfig.BaseURL = baseURL + } else { + defaultConfig.BaseURL = fmt.Sprintf("https://bedrock-mantle.%s.api.aws/v1", region) + } + + transport := &http.Transport{} + if proxyEndpoint := config.GetProxyEndpoint(); proxyEndpoint != "" { + proxyUrl, err := url.Parse(proxyEndpoint) + if err != nil { + return err + } + transport.Proxy = http.ProxyURL(proxyUrl) + } + + defaultConfig.HTTPClient = &http.Client{ + Transport: &OpenAIHeaderTransport{ + Origin: transport, + Headers: config.GetCustomHeaders(), + }, + } + + client := openai.NewClientWithConfig(defaultConfig) + if client == nil { + return errors.New("error creating Bedrock Mantle client") + } + c.client = client + c.model = config.GetModel() + c.temperature = config.GetTemperature() + c.topP = config.GetTopP() + return nil +} + +func (c *AmazonBedrockMantleClient) GetCompletion(ctx context.Context, prompt string) (string, error) { + resp, err := c.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: c.model, + Messages: []openai.ChatCompletionMessage{ + { + Role: "user", + Content: prompt, + }, + }, + Temperature: c.temperature, + MaxTokens: maxToken, + PresencePenalty: presencePenalty, + FrequencyPenalty: frequencyPenalty, + TopP: c.topP, + }) + if err != nil { + return "", err + } + return resp.Choices[0].Message.Content, nil +} + +func (c *AmazonBedrockMantleClient) GetName() string { + return bedrockMantleClientName +} diff --git a/pkg/ai/bedrockmantle_test.go b/pkg/ai/bedrockmantle_test.go new file mode 100644 index 00000000..35694340 --- /dev/null +++ b/pkg/ai/bedrockmantle_test.go @@ -0,0 +1,158 @@ +package ai + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBedrockMantleGetName(t *testing.T) { + client := &AmazonBedrockMantleClient{} + assert.Equal(t, "bedrockmantle", client.GetName()) +} + +func TestBedrockMantleConfigure(t *testing.T) { + tests := []struct { + name string + envRegion string + envToken string + cfg *AIProvider + expectError bool + errContains string + }{ + { + name: "missing region", + envRegion: "", + envToken: "test-key", + cfg: &AIProvider{ + Model: "test-model", + }, + expectError: true, + errContains: "provider region is required", + }, + { + name: "missing token", + envRegion: "", + envToken: "", + cfg: &AIProvider{ + Model: "test-model", + ProviderRegion: "us-east-1", + }, + expectError: true, + errContains: "AWS_BEARER_TOKEN_BEDROCK", + }, + { + name: "success with region and token", + envRegion: "", + envToken: "test-key", + cfg: &AIProvider{ + Model: "anthropic.claude-3-sonnet", + ProviderRegion: "us-east-1", + Temperature: 0.7, + TopP: 0.9, + }, + expectError: false, + }, + { + name: "env region overrides config", + envRegion: "eu-west-1", + envToken: "test-key", + cfg: &AIProvider{ + Model: "test-model", + ProviderRegion: "us-east-1", + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("AWS_DEFAULT_REGION", tt.envRegion) + t.Setenv("AWS_BEARER_TOKEN_BEDROCK", tt.envToken) + + client := &AmazonBedrockMantleClient{} + err := client.Configure(tt.cfg) + + if tt.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.cfg.Model, client.model) + assert.Equal(t, tt.cfg.Temperature, client.temperature) + assert.Equal(t, tt.cfg.TopP, client.topP) + }) + } +} + +func TestBedrockMantleConfigure_CustomBaseURL(t *testing.T) { + t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "test-key") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + cfg := &AIProvider{ + Model: "test-model", + BaseURL: server.URL, + ProviderRegion: "us-east-1", + } + client := &AmazonBedrockMantleClient{} + err := client.Configure(cfg) + assert.NoError(t, err) +} + +func TestBedrockMantleGetCompletion_Success(t *testing.T) { + t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "test-key") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := map[string]interface{}{ + "id": "chatcmpl-123", + "object": "chat.completion", + "choices": []map[string]interface{}{{"index": 0, "message": map[string]string{"role": "assistant", "content": "mock response"}, "finish_reason": "stop"}}, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) + })) + defer server.Close() + + cfg := &AIProvider{ + Model: "test-model", + BaseURL: server.URL, + ProviderRegion: "us-east-1", + } + client := &AmazonBedrockMantleClient{} + err := client.Configure(cfg) + assert.NoError(t, err) + + result, err := client.GetCompletion(context.Background(), "hello") + assert.NoError(t, err) + assert.Equal(t, "mock response", result) +} + +func TestBedrockMantleGetCompletion_Error(t *testing.T) { + t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "test-key") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "error": map[string]string{"message": "model error", "type": "server_error"}, + }) + })) + defer server.Close() + + cfg := &AIProvider{ + Model: "test-model", + BaseURL: server.URL, + ProviderRegion: "us-east-1", + } + client := &AmazonBedrockMantleClient{} + err := client.Configure(cfg) + assert.NoError(t, err) + + _, err = client.GetCompletion(context.Background(), "hello") + assert.Error(t, err) +} diff --git a/pkg/ai/iai.go b/pkg/ai/iai.go index 05b374b6..18ae792a 100644 --- a/pkg/ai/iai.go +++ b/pkg/ai/iai.go @@ -37,6 +37,7 @@ var ( &CustomRestClient{}, &IBMWatsonxAIClient{}, &GroqClient{}, + &AmazonBedrockMantleClient{}, } Backends = []string{ openAIClientName, @@ -56,6 +57,7 @@ var ( CustomRestClientName, ibmWatsonxAIClientName, groqAIClientName, + bedrockMantleClientName, } ) @@ -207,7 +209,7 @@ func (p *AIProvider) GetCustomHeaders() []http.Header { return p.CustomHeaders } -var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "amazonbedrockconverse", "googlevertexai", "oci", "customrest"} +var passwordlessProviders = []string{"localai", "ollama", "amazonsagemaker", "amazonbedrock", "amazonbedrockconverse", "googlevertexai", "oci", "customrest", "bedrockmantle"} func NeedPassword(backend string) bool { for _, b := range passwordlessProviders {