// internal/ai/google_gemini.go // Дата: 2026-03-12 // Реализация LLMProvider для Google Gemini API. // Использует REST API напрямую через net/http — без внешних SDK, // чтобы не нарушать совместимость существующего go.mod. package ai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" ) const geminiBaseURL = "https://generativelanguage.googleapis.com/v1beta/models/%s:generateContent" // GeminiProvider реализует LLMProvider через Google Gemini REST API. type GeminiProvider struct { apiKey string model string httpClient *http.Client } // NewGeminiProvider создаёт провайдера для Google Gemini. // apiKey — значение env GOOGLE_AI_API_KEY. func NewGeminiProvider(apiKey, model string) (*GeminiProvider, error) { if apiKey == "" { return nil, fmt.Errorf("GOOGLE_AI_API_KEY не задан") } return &GeminiProvider{ apiKey: apiKey, model: model, httpClient: &http.Client{ Timeout: 60 * time.Second, }, }, nil } func (g *GeminiProvider) Name() string { return fmt.Sprintf("google-gemini/%s", g.model) } // geminiRequest — структура запроса к Gemini API. type geminiRequest struct { Contents []geminiContent `json:"contents"` } type geminiContent struct { Parts []geminiPart `json:"parts"` } type geminiPart struct { Text string `json:"text"` } // geminiResponse — структура ответа от Gemini API. type geminiResponse struct { Candidates []struct { Content struct { Parts []struct { Text string `json:"text"` } `json:"parts"` } `json:"content"` } `json:"candidates"` Error *struct { Message string `json:"message"` Code int `json:"code"` } `json:"error,omitempty"` } // Analyze отправляет промпт в Gemini и возвращает текстовый ответ. func (g *GeminiProvider) Analyze(ctx context.Context, prompt string) (string, error) { reqBody := geminiRequest{ Contents: []geminiContent{ {Parts: []geminiPart{{Text: prompt}}}, }, } bodyBytes, err := json.Marshal(reqBody) if err != nil { return "", fmt.Errorf("ошибка маршалинга запроса: %w", err) } url := fmt.Sprintf(geminiBaseURL+"?key=%s", g.model, g.apiKey) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(bodyBytes)) if err != nil { return "", fmt.Errorf("ошибка создания HTTP запроса: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := g.httpClient.Do(req) if err != nil { return "", fmt.Errorf("ошибка HTTP запроса к Gemini: %w", err) } defer resp.Body.Close() respBytes, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("ошибка чтения ответа: %w", err) } var gemResp geminiResponse if err := json.Unmarshal(respBytes, &gemResp); err != nil { return "", fmt.Errorf("ошибка парсинга ответа Gemini: %w", err) } // Проверяем HTTP-ошибку и ошибку в теле ответа if resp.StatusCode != http.StatusOK { msg := fmt.Sprintf("HTTP %d", resp.StatusCode) if gemResp.Error != nil { msg = gemResp.Error.Message } return "", fmt.Errorf("Gemini API ошибка: %s", msg) } if len(gemResp.Candidates) == 0 || len(gemResp.Candidates[0].Content.Parts) == 0 { return "", fmt.Errorf("Gemini вернул пустой ответ") } return gemResp.Candidates[0].Content.Parts[0].Text, nil }