Skip to content

Commit 60f108b

Browse files
feat[modules-config](socai): generalized socai connection check and validations
1 parent f9bd402 commit 60f108b

File tree

13 files changed

+763
-168
lines changed

13 files changed

+763
-168
lines changed
Lines changed: 6 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,116 +1,26 @@
11
package validations
2-
32
import (
43
"encoding/json"
54
"fmt"
6-
"maps"
7-
"net/http"
8-
"strings"
95

10-
"github.com/threatwinds/go-sdk/utils"
116
"github.com/utmstack/UTMStack/plugins/modules-config/config"
7+
"github.com/utmstack/UTMStack/plugins/modules-config/validations/socai"
128
)
139

14-
// isAnthropicProvider detects if the URL is for Anthropic API
15-
func isAnthropicProvider(url string) bool {
16-
return strings.Contains(url, "anthropic.com")
17-
}
18-
19-
// SOCAIConfig holds the parsed SOC-AI configuration
20-
type SOCAIConfig struct {
21-
AutoAnalyze bool
22-
IncidentCreation bool
23-
ChangeAlertStatus bool
24-
Provider string
25-
URL string
26-
Model string
27-
AuthType string // "custom-headers", "none"
28-
MaxTokens string
29-
CustomHeaders map[string]string // All headers including auth (from frontend)
30-
}
31-
32-
var providerDefaultURLs = map[string]string{
33-
"openai": "https://api.openai.com/v1/chat/completions",
34-
"anthropic": "https://api.anthropic.com/v1/messages",
35-
"azure": "",
36-
"gemini": "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
37-
"ollama": "http://localhost:11434/v1/chat/completions",
38-
"mistral": "https://api.mistral.ai/v1/chat/completions",
39-
"deepseek": "https://api.deepseek.com/chat/completions",
40-
"groq": "https://api.groq.com/openai/v1/chat/completions",
41-
}
42-
4310

44-
var providerDisplayNames = map[string]string{
45-
"openai": "OpenAI",
46-
"anthropic": "Anthropic",
47-
"azure": "Azure OpenAI",
48-
"gemini": "Google Gemini",
49-
"ollama": "Ollama",
50-
"mistral": "Mistral AI",
51-
"deepseek": "DeepSeek",
52-
"groq": "Groq",
53-
"custom": "Custom",
54-
}
5511

56-
func getProviderName(provider string) string {
57-
if name, ok := providerDisplayNames[provider]; ok {
58-
return name
59-
}
60-
return provider
61-
}
6212

6313
func ValidateSOCAIConfig(cfg *config.ModuleGroup) error {
6414
if cfg == nil {
6515
return fmt.Errorf("SOC AI configuration is not provided")
6616
}
67-
68-
socai := parseSOCAIConfig(cfg)
69-
providerName := getProviderName(socai.Provider)
70-
71-
// Validate required fields
72-
if socai.URL == "" {
73-
if socai.Provider == "custom" || socai.Provider == "azure" || socai.Provider == "ollama" {
74-
return fmt.Errorf("API URL is required for %s provider", providerName)
75-
}
76-
return fmt.Errorf("API URL could not be determined. Please verify the provider configuration.")
77-
}
78-
if socai.Model == "" {
79-
return fmt.Errorf("Model is required for %s provider", providerName)
80-
}
81-
82-
// Validate authType
83-
if socai.AuthType == "" {
84-
socai.AuthType = "none"
85-
}
86-
if socai.AuthType != "custom-headers" && socai.AuthType != "none" {
87-
return fmt.Errorf("Invalid authentication type '%s'. Must be 'custom-headers' or 'none'.", socai.AuthType)
88-
}
89-
90-
// Validate auth headers exist when needed
91-
if socai.AuthType == "custom-headers" && len(socai.CustomHeaders) == 0 {
92-
if socai.Provider == "ollama" {
93-
// Ollama doesn't need auth
94-
} else {
95-
return fmt.Errorf("API Key is required for %s. Please provide your %s API Key.", providerName, providerName)
96-
}
97-
}
98-
99-
// Anthropic requires maxTokens
100-
if isAnthropicProvider(socai.URL) && socai.MaxTokens == "" {
101-
return fmt.Errorf("Max Tokens is required for Anthropic. Please set a value (e.g., 4096).")
102-
}
103-
104-
// Test connection
105-
if err := testSOCAIConnection(socai); err != nil {
106-
return err
107-
}
108-
109-
return nil
17+
socai_config := parseSOCAIConfig(cfg)
18+
verificator := socai.NewSocaiVerification(socai_config)
19+
return verificator.Validate()
11020
}
11121

112-
func parseSOCAIConfig(cfg *config.ModuleGroup) SOCAIConfig {
113-
socai := SOCAIConfig{
22+
func parseSOCAIConfig(cfg *config.ModuleGroup) socai.SOCAIConfig {
23+
socai := socai.SOCAIConfig{
11424
AuthType: "none",
11525
CustomHeaders: make(map[string]string),
11626
}
@@ -144,80 +54,8 @@ func parseSOCAIConfig(cfg *config.ModuleGroup) SOCAIConfig {
14454
}
14555
}
14656

147-
// Resolve URL from provider if not custom
148-
if socai.Provider != "" && socai.Provider != "custom" {
149-
if defaultURL, ok := providerDefaultURLs[socai.Provider]; ok && defaultURL != "" {
150-
socai.URL = defaultURL
151-
}
152-
}
153-
15457
return socai
15558
}
15659

157-
func testSOCAIConnection(socai SOCAIConfig) error {
158-
providerName := getProviderName(socai.Provider)
15960

160-
headers := map[string]string{
161-
"Content-Type": "application/json",
162-
}
163-
164-
// Add custom headers (includes auth headers configured by frontend)
165-
if socai.AuthType == "custom-headers" {
166-
maps.Copy(headers, socai.CustomHeaders)
167-
}
16861

169-
// Test connection with GET request (most APIs return error but validate auth)
170-
_, status, err := utils.DoReq[map[string]any](socai.URL, nil, "POST", headers, false)
171-
172-
switch status {
173-
case http.StatusOK, http.StatusBadRequest:
174-
// These are acceptable - means we reached the API and auth worked
175-
return nil
176-
case http.StatusUnauthorized:
177-
if socai.Provider == "anthropic" {
178-
return fmt.Errorf("Invalid Anthropic API Key. Please verify your x-api-key is correct.")
179-
}
180-
if socai.Provider == "azure" {
181-
return fmt.Errorf("Invalid Azure OpenAI API Key. Please verify your api-key is correct.")
182-
}
183-
return fmt.Errorf("Invalid API Key for %s. Please verify your API Key is correct.", providerName)
184-
case http.StatusForbidden:
185-
return fmt.Errorf("%s API Key does not have the required permissions (HTTP 403). Please verify the API Key has access to the chat completions endpoint.", providerName)
186-
case http.StatusNotFound, http.StatusMethodNotAllowed:
187-
if socai.Provider == "azure" {
188-
return fmt.Errorf("Azure OpenAI endpoint not found (HTTP 404). Please verify your Endpoint URL includes the correct resource name and deployment.")
189-
}
190-
if socai.Provider == "ollama" {
191-
return fmt.Errorf("Ollama API not found at '%s' (HTTP 404). Please verify Ollama is running and the URL is correct.", socai.URL)
192-
}
193-
return fmt.Errorf("%s API endpoint not found (HTTP 404). Please verify the API URL is correct.", providerName)
194-
case http.StatusRequestTimeout:
195-
if socai.Provider == "ollama" {
196-
return fmt.Errorf("Connection to Ollama timed out. Please verify Ollama is running at '%s' and is accessible from this server.", socai.URL)
197-
}
198-
return fmt.Errorf("Connection to %s timed out. Please verify the API URL is accessible from this server.", providerName)
199-
case http.StatusTooManyRequests:
200-
return fmt.Errorf("%s API rate limit exceeded (HTTP 429). Your API Key may have exceeded its quota. Please check your %s account billing/usage.", providerName, providerName)
201-
default:
202-
if err != nil {
203-
errMsg := strings.ToLower(err.Error())
204-
if strings.Contains(errMsg, "no such host") || strings.Contains(errMsg, "lookup") {
205-
if socai.Provider == "ollama" {
206-
return fmt.Errorf("Cannot resolve Ollama server at '%s'. Please verify the hostname is correct and accessible.", socai.URL)
207-
}
208-
return fmt.Errorf("Cannot resolve %s API host. Please verify the API URL is correct.", providerName)
209-
}
210-
if strings.Contains(errMsg, "connection refused") {
211-
if socai.Provider == "ollama" {
212-
return fmt.Errorf("Connection refused by Ollama at '%s'. Please verify Ollama is running.", socai.URL)
213-
}
214-
return fmt.Errorf("Connection refused by %s. Please verify the API URL is correct.", providerName)
215-
}
216-
if strings.Contains(errMsg, "timeout") || strings.Contains(errMsg, "deadline") {
217-
return fmt.Errorf("Connection to %s timed out. Please verify the API URL is accessible from this server.", providerName)
218-
}
219-
return fmt.Errorf("Cannot connect to %s. Please verify the API URL and API Key are correct.", providerName)
220-
}
221-
return nil // Accept other status codes as potentially valid
222-
}
223-
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package socai
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/utmstack/UTMStack/plugins/modules-config/validations/socai/providers"
7+
)
8+
9+
type Provider string
10+
11+
const (
12+
Openai Provider = "openai"
13+
Anthropic Provider = "anthropic"
14+
Azure Provider = "azure"
15+
Gemini Provider = "gemini"
16+
Ollama Provider = "ollama"
17+
Mistral Provider = "mistral"
18+
Deepseek Provider = "deepseek"
19+
Groq Provider = "groq"
20+
Custom Provider = "custom"
21+
)
22+
23+
type SOCAIConfig struct {
24+
AutoAnalyze bool
25+
IncidentCreation bool
26+
ChangeAlertStatus bool
27+
Provider string
28+
URL string
29+
Model string
30+
AuthType string // "custom-headers", "none"
31+
MaxTokens string
32+
CustomHeaders map[string]string // All headers including auth (from frontend)
33+
}
34+
35+
type ProviderVerificationBuilder struct {
36+
config SOCAIConfig
37+
}
38+
39+
func NewSocaiVerification(config SOCAIConfig) providers.IProvider {
40+
return &ProviderVerificationBuilder{
41+
config: config,
42+
}
43+
}
44+
45+
func (p *ProviderVerificationBuilder) Validate() error {
46+
var provider providers.IProvider
47+
48+
switch Provider(p.config.Provider) {
49+
case Openai:
50+
provider = providers.NewOpenAIProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders)
51+
case Anthropic:
52+
provider = providers.NewAnthropicProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders, p.config.MaxTokens)
53+
case Azure:
54+
provider = providers.NewAzureProvider(p.config.URL, p.config.Model, p.config.AuthType, p.config.CustomHeaders)
55+
case Gemini:
56+
provider = providers.NewGeminiProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders)
57+
case Ollama:
58+
provider = providers.NewOllamaProvider(p.config.URL, p.config.Model, p.config.AuthType, p.config.CustomHeaders)
59+
case Mistral:
60+
provider = providers.NewMistralProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders)
61+
case Deepseek:
62+
provider = providers.NewDeepSeekProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders)
63+
case Groq:
64+
provider = providers.NewGroqProvider(p.config.Model, p.config.AuthType, p.config.CustomHeaders)
65+
case Custom:
66+
provider = providers.NewCustomProvider(p.config.URL, p.config.Model, p.config.AuthType, p.config.CustomHeaders)
67+
default:
68+
return fmt.Errorf("unsupported provider: %s", p.config.Provider)
69+
}
70+
71+
return provider.Validate()
72+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package providers
2+
3+
import (
4+
"fmt"
5+
"maps"
6+
7+
"github.com/threatwinds/go-sdk/utils"
8+
)
9+
10+
type AbstractProvider struct {
11+
URL string
12+
Model string
13+
AuthType string
14+
CustomHeaders map[string]string
15+
}
16+
17+
func NewAbstractProvider(
18+
URL string,
19+
Model string,
20+
AuthType string,
21+
CustomHeaders map[string]string,
22+
) *AbstractProvider {
23+
return &AbstractProvider{
24+
URL: URL,
25+
Model: Model,
26+
AuthType: AuthType,
27+
CustomHeaders: CustomHeaders,
28+
}
29+
}
30+
31+
func (p *AbstractProvider) Validate() error {
32+
if p.AuthType == "" {
33+
p.AuthType = "none"
34+
}
35+
if p.AuthType != "custom-headers" && p.AuthType != "none" {
36+
return fmt.Errorf("Invalid authentication type '%s'. Must be 'custom-headers' or 'none'.", p.AuthType)
37+
}
38+
39+
return nil
40+
}
41+
42+
func (p *AbstractProvider) PerformTestRequest() (int, error) {
43+
headers := map[string]string{
44+
"Content-Type": "application/json",
45+
}
46+
47+
if p.AuthType == "custom-headers" {
48+
maps.Copy(headers, p.CustomHeaders)
49+
}
50+
51+
_, status, err := utils.DoReq[map[string]any](p.URL, nil, "POST", headers, false)
52+
return status, err
53+
}

0 commit comments

Comments
 (0)