268 lines
6.1 KiB
Go
268 lines
6.1 KiB
Go
package google
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"google.golang.org/genai"
|
|
|
|
"github.com/yourusername/go-llm-gateway/internal/api"
|
|
"github.com/yourusername/go-llm-gateway/internal/config"
|
|
)
|
|
|
|
const Name = "google"
|
|
|
|
// Provider implements the Google Generative AI integration.
|
|
type Provider struct {
|
|
cfg config.ProviderConfig
|
|
client *genai.Client
|
|
}
|
|
|
|
// New constructs a Provider using the provided configuration.
|
|
func New(cfg config.ProviderConfig) *Provider {
|
|
var client *genai.Client
|
|
if cfg.APIKey != "" {
|
|
var err error
|
|
client, err = genai.NewClient(context.Background(), &genai.ClientConfig{
|
|
APIKey: cfg.APIKey,
|
|
})
|
|
if err != nil {
|
|
// Log error but don't fail construction - will fail on Generate
|
|
fmt.Printf("warning: failed to create google client: %v\n", err)
|
|
}
|
|
}
|
|
return &Provider{
|
|
cfg: cfg,
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
func (p *Provider) Name() string { return Name }
|
|
|
|
// Generate routes the Open Responses request to Gemini.
|
|
func (p *Provider) Generate(ctx context.Context, req *api.ResponseRequest) (*api.Response, error) {
|
|
if p.cfg.APIKey == "" {
|
|
return nil, fmt.Errorf("google api key missing")
|
|
}
|
|
if p.client == nil {
|
|
return nil, fmt.Errorf("google client not initialized")
|
|
}
|
|
|
|
model := chooseModel(req.Model, p.cfg.Model)
|
|
|
|
// Convert Open Responses messages to Gemini format
|
|
var contents []*genai.Content
|
|
var systemText string
|
|
|
|
for _, msg := range req.Input {
|
|
if msg.Role == "system" {
|
|
for _, block := range msg.Content {
|
|
if block.Type == "input_text" || block.Type == "output_text" {
|
|
systemText += block.Text
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
var parts []*genai.Part
|
|
for _, block := range msg.Content {
|
|
if block.Type == "input_text" || block.Type == "output_text" {
|
|
parts = append(parts, genai.NewPartFromText(block.Text))
|
|
}
|
|
}
|
|
|
|
role := "user"
|
|
if msg.Role == "assistant" || msg.Role == "model" {
|
|
role = "model"
|
|
}
|
|
|
|
contents = append(contents, &genai.Content{
|
|
Role: role,
|
|
Parts: parts,
|
|
})
|
|
}
|
|
|
|
// Build config with system instruction if present
|
|
var config *genai.GenerateContentConfig
|
|
if systemText != "" {
|
|
config = &genai.GenerateContentConfig{
|
|
SystemInstruction: &genai.Content{
|
|
Parts: []*genai.Part{genai.NewPartFromText(systemText)},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Generate content
|
|
resp, err := p.client.Models.GenerateContent(ctx, model, contents, config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("google api error: %w", err)
|
|
}
|
|
|
|
// Convert Gemini response to Open Responses format
|
|
output := make([]api.Message, 0, 1)
|
|
var text string
|
|
|
|
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
|
for _, part := range resp.Candidates[0].Content.Parts {
|
|
if part != nil {
|
|
text += part.Text
|
|
}
|
|
}
|
|
}
|
|
|
|
output = append(output, api.Message{
|
|
Role: "assistant",
|
|
Content: []api.ContentBlock{
|
|
{Type: "output_text", Text: text},
|
|
},
|
|
})
|
|
|
|
// Extract usage info if available
|
|
var inputTokens, outputTokens int
|
|
if resp.UsageMetadata != nil {
|
|
inputTokens = int(resp.UsageMetadata.PromptTokenCount)
|
|
outputTokens = int(resp.UsageMetadata.CandidatesTokenCount)
|
|
}
|
|
|
|
return &api.Response{
|
|
ID: uuid.NewString(),
|
|
Object: "response",
|
|
Created: time.Now().Unix(),
|
|
Model: model,
|
|
Provider: Name,
|
|
Output: output,
|
|
Usage: api.Usage{
|
|
InputTokens: inputTokens,
|
|
OutputTokens: outputTokens,
|
|
TotalTokens: inputTokens + outputTokens,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// GenerateStream handles streaming requests to Google.
|
|
func (p *Provider) GenerateStream(ctx context.Context, req *api.ResponseRequest) (<-chan *api.StreamChunk, <-chan error) {
|
|
chunkChan := make(chan *api.StreamChunk)
|
|
errChan := make(chan error, 1)
|
|
|
|
go func() {
|
|
defer close(chunkChan)
|
|
defer close(errChan)
|
|
|
|
if p.cfg.APIKey == "" {
|
|
errChan <- fmt.Errorf("google api key missing")
|
|
return
|
|
}
|
|
if p.client == nil {
|
|
errChan <- fmt.Errorf("google client not initialized")
|
|
return
|
|
}
|
|
|
|
model := chooseModel(req.Model, p.cfg.Model)
|
|
|
|
// Convert messages
|
|
var contents []*genai.Content
|
|
var systemText string
|
|
|
|
for _, msg := range req.Input {
|
|
if msg.Role == "system" {
|
|
for _, block := range msg.Content {
|
|
if block.Type == "input_text" || block.Type == "output_text" {
|
|
systemText += block.Text
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
var parts []*genai.Part
|
|
for _, block := range msg.Content {
|
|
if block.Type == "input_text" || block.Type == "output_text" {
|
|
parts = append(parts, genai.NewPartFromText(block.Text))
|
|
}
|
|
}
|
|
|
|
role := "user"
|
|
if msg.Role == "assistant" || msg.Role == "model" {
|
|
role = "model"
|
|
}
|
|
|
|
contents = append(contents, &genai.Content{
|
|
Role: role,
|
|
Parts: parts,
|
|
})
|
|
}
|
|
|
|
// Build config with system instruction if present
|
|
var config *genai.GenerateContentConfig
|
|
if systemText != "" {
|
|
config = &genai.GenerateContentConfig{
|
|
SystemInstruction: &genai.Content{
|
|
Parts: []*genai.Part{genai.NewPartFromText(systemText)},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Create stream
|
|
stream := p.client.Models.GenerateContentStream(ctx, model, contents, config)
|
|
|
|
// Process stream
|
|
for resp, err := range stream {
|
|
if err != nil {
|
|
errChan <- fmt.Errorf("google stream error: %w", err)
|
|
return
|
|
}
|
|
|
|
var text string
|
|
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
|
for _, part := range resp.Candidates[0].Content.Parts {
|
|
if part != nil {
|
|
text += part.Text
|
|
}
|
|
}
|
|
}
|
|
|
|
delta := &api.StreamDelta{}
|
|
if text != "" {
|
|
delta.Content = []api.ContentBlock{
|
|
{Type: "output_text", Text: text},
|
|
}
|
|
}
|
|
|
|
streamChunk := &api.StreamChunk{
|
|
Object: "response.chunk",
|
|
Created: time.Now().Unix(),
|
|
Model: model,
|
|
Provider: Name,
|
|
Delta: delta,
|
|
}
|
|
|
|
select {
|
|
case chunkChan <- streamChunk:
|
|
case <-ctx.Done():
|
|
errChan <- ctx.Err()
|
|
return
|
|
}
|
|
}
|
|
|
|
// Send final chunk
|
|
select {
|
|
case chunkChan <- &api.StreamChunk{Object: "response.chunk", Done: true}:
|
|
case <-ctx.Done():
|
|
errChan <- ctx.Err()
|
|
}
|
|
}()
|
|
|
|
return chunkChan, errChan
|
|
}
|
|
|
|
func chooseModel(requested, defaultModel string) string {
|
|
if requested != "" {
|
|
return requested
|
|
}
|
|
if defaultModel != "" {
|
|
return defaultModel
|
|
}
|
|
return "gemini-2.0-flash-exp"
|
|
}
|