feat: add modular REST and gRPC Gemini clients with interface
This commit is contained in:
parent
7222a72f65
commit
bd9fb7145c
3 changed files with 131 additions and 50 deletions
6
bot.log
Normal file
6
bot.log
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue