feat: add modular REST and gRPC Gemini clients with interface

This commit is contained in:
StirGpea 2026-08-21 13:31:35 +00:00
parent 7222a72f65
commit bd9fb7145c
3 changed files with 131 additions and 50 deletions

6
bot.log Normal file
View file

@ -0,0 +1,6 @@
pievr bot starting...
2026/08/21 13:21:01 Telegram bot is starting...
2026/08/21 13:21:29 Received message from 1294124438: /start
2026/08/21 13:21:29 Failed to generate response from Gemini: proto: syntax error (line 1:1): unexpected token
2026/08/21 13:21:34 Received message from 1294124438: Алло?
2026/08/21 13:21:34 Failed to generate response from Gemini: proto: syntax error (line 1:1): unexpected token

View file

@ -11,11 +11,11 @@ import (
)
type Bot struct {
b *bot.Bot
geminiCli *gemini.Client
b *bot.Bot
geminiCli gemini.Client
}
func New(token string, geminiCli *gemini.Client) (*Bot, error) {
func New(token string, geminiCli gemini.Client) (*Bot, error) {
myBot := &Bot{
geminiCli: geminiCli,
}
@ -48,7 +48,7 @@ func (s *Bot) handler(ctx context.Context, b *bot.Bot, update *models.Update) {
log.Printf("Received message from %d: %s", chatID, userMsg)
// Send prompt to Gemini
// Send prompt to Gemini (via interface)
reply, err := s.geminiCli.Generate(ctx, userMsg)
if err != nil {
log.Printf("Failed to generate response from Gemini: %v", err)

View file

@ -1,53 +1,135 @@
package gemini
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
type Client struct {
// Client defines the common interface for Gemini interactions
type Client interface {
Generate(ctx context.Context, prompt string) (string, error)
Close() error
}
// --- REST Client (for custom BaseURL / proxy like lar) ---
type RestClient struct {
httpClient *http.Client
baseURL string
apiKey string
model string
}
func NewRestClient(apiKey, baseURL, model string) (*RestClient, error) {
baseURL = strings.TrimSuffix(baseURL, "/")
return &RestClient{
httpClient: &http.Client{},
baseURL: baseURL,
apiKey: apiKey,
model: model,
}, nil
}
type restRequest struct {
Contents []restContent `json:"contents"`
}
type restContent struct {
Parts []restPart `json:"parts"`
}
type restPart struct {
Text string `json:"text"`
}
type restResponse struct {
Candidates []restCandidate `json:"candidates"`
}
type restCandidate struct {
Content restContent `json:"content"`
}
func (c *RestClient) Generate(ctx context.Context, prompt string) (string, error) {
// API key is passed as a query parameter in Gemini REST API
url := fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", c.baseURL, c.model, c.apiKey)
reqBody := restRequest{
Contents: []restContent{
{
Parts: []restPart{
{Text: prompt},
},
},
},
}
jsonBytes, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("failed to marshal rest request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBytes))
if err != nil {
return "", fmt.Errorf("failed to create http request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("gemini rest api error (status %d): %s", resp.StatusCode, string(bodyBytes))
}
var res restResponse
if err := json.Unmarshal(bodyBytes, &res); err != nil {
return "", fmt.Errorf("failed to decode rest response: %w (body: %s)", err, string(bodyBytes))
}
if len(res.Candidates) == 0 || len(res.Candidates[0].Content.Parts) == 0 {
return "", fmt.Errorf("empty candidates in rest response")
}
return res.Candidates[0].Content.Parts[0].Text, nil
}
func (c *RestClient) Close() error {
return nil
}
// --- gRPC Client (official SDK for direct Google API connection) ---
type GrpcClient struct {
client *genai.Client
model string
}
func NewClient(ctx context.Context, apiKey, baseURL, modelName string) (*Client, error) {
var opts []option.ClientOption
if apiKey != "" {
opts = append(opts, option.WithAPIKey(apiKey))
}
if baseURL != "" {
parsedURL, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("invalid base url: %w", err)
}
// Custom HTTP client endpoint override for Gemini API proxy/baseURL
customClient := &http.Client{
Transport: &baseURITransport{
BaseURL: parsedURL,
Transport: http.DefaultTransport,
},
}
opts = append(opts, option.WithHTTPClient(customClient))
}
c, err := genai.NewClient(ctx, opts...)
func NewGrpcClient(ctx context.Context, apiKey, model string) (*GrpcClient, error) {
c, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
return nil, err
}
return &Client{
return &GrpcClient{
client: c,
model: modelName,
model: model,
}, nil
}
func (c *Client) Generate(ctx context.Context, prompt string) (string, error) {
func (c *GrpcClient) Generate(ctx context.Context, prompt string) (string, error) {
model := c.client.GenerativeModel(c.model)
resp, err := model.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
@ -55,7 +137,7 @@ func (c *Client) Generate(ctx context.Context, prompt string) (string, error) {
}
if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
return "", fmt.Errorf("empty response from Gemini")
return "", fmt.Errorf("empty response from Gemini gRPC")
}
part := resp.Candidates[0].Content.Parts[0]
@ -66,25 +148,18 @@ func (c *Client) Generate(ctx context.Context, prompt string) (string, error) {
return fmt.Sprintf("%v", part), nil
}
func (c *Client) Close() {
c.client.Close()
func (c *GrpcClient) Close() error {
return c.client.Close()
}
type baseURITransport struct {
BaseURL *url.URL
Transport http.RoundTripper
}
func (t *baseURITransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Rewrite request URL to use custom BaseURL while preserving path & query
req.URL.Scheme = t.BaseURL.Scheme
req.URL.Host = t.BaseURL.Host
if t.BaseURL.Path != "" {
req.URL.Path = t.BaseURL.Path + req.URL.Path
// --- Factory helper ---
func NewClient(ctx context.Context, apiKey, baseURL, model string) (Client, error) {
if baseURL != "" {
// If custom base_url is specified (e.g. lar proxy), use REST client
return NewRestClient(apiKey, baseURL, model)
}
tr := t.Transport
if tr == nil {
tr = http.DefaultTransport
}
return tr.RoundTrip(req)
// Otherwise use official gRPC client
return NewGrpcClient(ctx, apiKey, model)
}