diff --git a/cmd/sless-plan/main.go b/cmd/sless-plan/main.go index 4a49054..03329c0 100644 --- a/cmd/sless-plan/main.go +++ b/cmd/sless-plan/main.go @@ -10,9 +10,12 @@ // // Env-переменные: // AI_HINT_LEVEL — уровень анализа 0–5 (0 = выключен, по умолчанию) -// AI_PROVIDER — провайдер: "google" (по умолчанию) или "cloud" -// GOOGLE_AI_API_KEY — API ключ Google Gemini -// GOOGLE_AI_MODEL — модель (по умолчанию "gemini-2.0-flash") +// AI_PROVIDER — провайдер: "groq" (по умолчанию), "google", "cloud" +// GROQ_API_KEY — API ключ Groq (рекомендуется, работает из РФ) +// GROQ_MODEL — модель Groq (по умолчанию "llama-3.3-70b-versatile") +// GROQ_ENDPOINT — endpoint (по умолчанию https://api.groq.com/openai/v1/chat/completions) +// GOOGLE_AI_API_KEY — API ключ Google Gemini (геоблок в РФ) +// GOOGLE_AI_MODEL — модель Gemini (по умолчанию "gemini-2.0-flash") // CLOUD_LLM_ENDPOINT — endpoint будущего облачного LLM // CLOUD_LLM_TOKEN — токен для облачного LLM @@ -114,7 +117,10 @@ func runTerraform(args []string) (string, int) { func runAIAnalysis(level int, planOutput string) { cfg := ai.Config{ HintLevel: level, - Provider: getEnv("AI_PROVIDER", "google"), + Provider: getEnv("AI_PROVIDER", "groq"), + GroqAPIKey: os.Getenv("GROQ_API_KEY"), + GroqModel: getEnv("GROQ_MODEL", "llama-3.3-70b-versatile"), + GroqEndpoint: os.Getenv("GROQ_ENDPOINT"), GoogleAPIKey: os.Getenv("GOOGLE_AI_API_KEY"), GoogleModel: getEnv("GOOGLE_AI_MODEL", "gemini-2.0-flash"), CloudEndpoint: os.Getenv("CLOUD_LLM_ENDPOINT"), diff --git a/examples/pg-list-python/main.tf b/examples/pg-list-python/main.tf index f99500c..046017d 100644 --- a/examples/pg-list-python/main.tf +++ b/examples/pg-list-python/main.tf @@ -14,4 +14,5 @@ provider "sless" { endpoint = "https://sless-api.kube5s.ru" token = var.token nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1" + ai_hint_level = 3 } diff --git a/internal/ai/openai_compat.go b/internal/ai/openai_compat.go new file mode 100644 index 0000000..33fc8bc --- /dev/null +++ b/internal/ai/openai_compat.go @@ -0,0 +1,125 @@ +// internal/ai/openai_compat.go +// Дата: 2026-03-12 +// OpenAI-совместимый провайдер — работает с Groq, OpenRouter, OpenAI и любым +// другим сервисом реализующим /v1/chat/completions API. +// Нет геоблока в отличие от Google Gemini. + +package ai + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// OpenAICompatProvider реализует LLMProvider через OpenAI-совместимый API. +// Groq endpoint: https://api.groq.com/openai/v1/chat/completions +type OpenAICompatProvider struct { + endpoint string + apiKey string + model string + httpClient *http.Client +} + +// NewOpenAICompatProvider создаёт провайдера для любого OpenAI-совместимого API. +// endpoint — полный URL до /v1/chat/completions +// apiKey — Bearer токен (GROQ_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY) +// model — название модели (например "llama-3.3-70b-versatile" для Groq) +func NewOpenAICompatProvider(endpoint, apiKey, model string) (*OpenAICompatProvider, error) { + if apiKey == "" { + return nil, fmt.Errorf("GROQ_API_KEY не задан") + } + if endpoint == "" { + endpoint = "https://api.groq.com/openai/v1/chat/completions" + } + if model == "" { + model = "llama-3.3-70b-versatile" + } + return &OpenAICompatProvider{ + endpoint: endpoint, + apiKey: apiKey, + model: model, + httpClient: &http.Client{ + Timeout: 60 * time.Second, + }, + }, nil +} + +func (p *OpenAICompatProvider) Name() string { + return fmt.Sprintf("groq/%s", p.model) +} + +// openAIRequest — тело запроса к /v1/chat/completions +type openAIRequest struct { + Model string `json:"model"` + Messages []openAIMessage `json:"messages"` +} + +type openAIMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +// openAIResponse — тело ответа от /v1/chat/completions +type openAIResponse struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + } `json:"error,omitempty"` +} + +// Analyze отправляет промпт в OpenAI-совместимый API и возвращает ответ. +func (p *OpenAICompatProvider) Analyze(ctx context.Context, prompt string) (string, error) { + reqBody := openAIRequest{ + Model: p.model, + Messages: []openAIMessage{ + {Role: "user", Content: prompt}, + }, + } + + bodyBytes, err := json.Marshal(reqBody) + if err != nil { + return "", fmt.Errorf("marshal error: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.endpoint, bytes.NewReader(bodyBytes)) + if err != nil { + return "", fmt.Errorf("create request error: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+p.apiKey) + + resp, err := p.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + respBytes, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response error: %w", err) + } + + var parsed openAIResponse + if err := json.Unmarshal(respBytes, &parsed); err != nil { + return "", fmt.Errorf("parse response error: %w", err) + } + + if parsed.Error != nil { + return "", fmt.Errorf("API ошибка: %s", parsed.Error.Message) + } + + if len(parsed.Choices) == 0 || parsed.Choices[0].Message.Content == "" { + return "", fmt.Errorf("пустой ответ от API") + } + + return parsed.Choices[0].Message.Content, nil +} diff --git a/internal/ai/provider.go b/internal/ai/provider.go index fa2f7f5..32c767f 100644 --- a/internal/ai/provider.go +++ b/internal/ai/provider.go @@ -1,7 +1,7 @@ // internal/ai/provider.go -// Дата: 2026-03-12 +// Дата: 2026-03-12 (обновлён 2026-03-12: добавлен Groq / OpenAI-compat) // Интерфейс LLMProvider — абстракция над любым LLM-бэкендом. -// Сейчас реализован Google Gemini. В будущем — облачный LLM (drop-in замена). +// Реализации: Google Gemini, Groq (OpenAI-compat), облачный LLM (stub). package ai @@ -18,13 +18,17 @@ type LLMProvider interface { // Config — конфигурация анализатора, читается из env-переменных. type Config struct { - // Provider: "google" или "cloud" (в будущем) + // Provider: "groq" (по умолчанию), "google", "cloud" Provider string // HintLevel: 0–5, если 0 — AI не вызывается HintLevel int - // Google Gemini + // Groq / OpenAI-совместимые провайдеры + GroqAPIKey string + GroqModel string // по умолчанию "llama-3.3-70b-versatile" + GroqEndpoint string // по умолчанию https://api.groq.com/openai/v1/chat/completions + // Google Gemini (геоблок в РФ) GoogleAPIKey string - GoogleModel string // по умолчанию "gemini-pro" + GoogleModel string // по умолчанию "gemini-2.0-flash" // Будущий облачный LLM CloudEndpoint string CloudToken string @@ -34,6 +38,8 @@ type Config struct { // При добавлении нового провайдера — только сюда добавить case. func NewProvider(cfg Config) (LLMProvider, error) { switch cfg.Provider { + case "groq": + return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel) case "google": model := cfg.GoogleModel if model == "" { @@ -43,11 +49,7 @@ func NewProvider(cfg Config) (LLMProvider, error) { case "cloud": return NewCloudLLMProvider(cfg.CloudEndpoint, cfg.CloudToken) default: - // По умолчанию google, если AI_PROVIDER не задан - model := cfg.GoogleModel - if model == "" { - model = "gemini-2.0-flash" - } - return NewGeminiProvider(cfg.GoogleAPIKey, model) + // По умолчанию groq — работает из РФ без геоблоков + return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel) } }