feat(ai): add Groq/OpenAI-compat provider as default, keep Gemini as option
This commit is contained in:
+10
-4
@@ -10,9 +10,12 @@
|
|||||||
//
|
//
|
||||||
// Env-переменные:
|
// Env-переменные:
|
||||||
// AI_HINT_LEVEL — уровень анализа 0–5 (0 = выключен, по умолчанию)
|
// AI_HINT_LEVEL — уровень анализа 0–5 (0 = выключен, по умолчанию)
|
||||||
// AI_PROVIDER — провайдер: "google" (по умолчанию) или "cloud"
|
// AI_PROVIDER — провайдер: "groq" (по умолчанию), "google", "cloud"
|
||||||
// GOOGLE_AI_API_KEY — API ключ Google Gemini
|
// GROQ_API_KEY — API ключ Groq (рекомендуется, работает из РФ)
|
||||||
// GOOGLE_AI_MODEL — модель (по умолчанию "gemini-2.0-flash")
|
// 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_ENDPOINT — endpoint будущего облачного LLM
|
||||||
// CLOUD_LLM_TOKEN — токен для облачного LLM
|
// CLOUD_LLM_TOKEN — токен для облачного LLM
|
||||||
|
|
||||||
@@ -114,7 +117,10 @@ func runTerraform(args []string) (string, int) {
|
|||||||
func runAIAnalysis(level int, planOutput string) {
|
func runAIAnalysis(level int, planOutput string) {
|
||||||
cfg := ai.Config{
|
cfg := ai.Config{
|
||||||
HintLevel: level,
|
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"),
|
GoogleAPIKey: os.Getenv("GOOGLE_AI_API_KEY"),
|
||||||
GoogleModel: getEnv("GOOGLE_AI_MODEL", "gemini-2.0-flash"),
|
GoogleModel: getEnv("GOOGLE_AI_MODEL", "gemini-2.0-flash"),
|
||||||
CloudEndpoint: os.Getenv("CLOUD_LLM_ENDPOINT"),
|
CloudEndpoint: os.Getenv("CLOUD_LLM_ENDPOINT"),
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ provider "sless" {
|
|||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless-api.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||||
|
ai_hint_level = 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+13
-11
@@ -1,7 +1,7 @@
|
|||||||
// internal/ai/provider.go
|
// internal/ai/provider.go
|
||||||
// Дата: 2026-03-12
|
// Дата: 2026-03-12 (обновлён 2026-03-12: добавлен Groq / OpenAI-compat)
|
||||||
// Интерфейс LLMProvider — абстракция над любым LLM-бэкендом.
|
// Интерфейс LLMProvider — абстракция над любым LLM-бэкендом.
|
||||||
// Сейчас реализован Google Gemini. В будущем — облачный LLM (drop-in замена).
|
// Реализации: Google Gemini, Groq (OpenAI-compat), облачный LLM (stub).
|
||||||
|
|
||||||
package ai
|
package ai
|
||||||
|
|
||||||
@@ -18,13 +18,17 @@ type LLMProvider interface {
|
|||||||
|
|
||||||
// Config — конфигурация анализатора, читается из env-переменных.
|
// Config — конфигурация анализатора, читается из env-переменных.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Provider: "google" или "cloud" (в будущем)
|
// Provider: "groq" (по умолчанию), "google", "cloud"
|
||||||
Provider string
|
Provider string
|
||||||
// HintLevel: 0–5, если 0 — AI не вызывается
|
// HintLevel: 0–5, если 0 — AI не вызывается
|
||||||
HintLevel int
|
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
|
GoogleAPIKey string
|
||||||
GoogleModel string // по умолчанию "gemini-pro"
|
GoogleModel string // по умолчанию "gemini-2.0-flash"
|
||||||
// Будущий облачный LLM
|
// Будущий облачный LLM
|
||||||
CloudEndpoint string
|
CloudEndpoint string
|
||||||
CloudToken string
|
CloudToken string
|
||||||
@@ -34,6 +38,8 @@ type Config struct {
|
|||||||
// При добавлении нового провайдера — только сюда добавить case.
|
// При добавлении нового провайдера — только сюда добавить case.
|
||||||
func NewProvider(cfg Config) (LLMProvider, error) {
|
func NewProvider(cfg Config) (LLMProvider, error) {
|
||||||
switch cfg.Provider {
|
switch cfg.Provider {
|
||||||
|
case "groq":
|
||||||
|
return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel)
|
||||||
case "google":
|
case "google":
|
||||||
model := cfg.GoogleModel
|
model := cfg.GoogleModel
|
||||||
if model == "" {
|
if model == "" {
|
||||||
@@ -43,11 +49,7 @@ func NewProvider(cfg Config) (LLMProvider, error) {
|
|||||||
case "cloud":
|
case "cloud":
|
||||||
return NewCloudLLMProvider(cfg.CloudEndpoint, cfg.CloudToken)
|
return NewCloudLLMProvider(cfg.CloudEndpoint, cfg.CloudToken)
|
||||||
default:
|
default:
|
||||||
// По умолчанию google, если AI_PROVIDER не задан
|
// По умолчанию groq — работает из РФ без геоблоков
|
||||||
model := cfg.GoogleModel
|
return NewOpenAICompatProvider(cfg.GroqEndpoint, cfg.GroqAPIKey, cfg.GroqModel)
|
||||||
if model == "" {
|
|
||||||
model = "gemini-2.0-flash"
|
|
||||||
}
|
|
||||||
return NewGeminiProvider(cfg.GoogleAPIKey, model)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user