mirror of
				https://github.com/go-micro/go-micro.git
				synced 2025-10-30 23:27:41 +02:00 
			
		
		
		
	GenAI interface (#2790)
* genai interface * x * x * text to speech * Re-add events package (#2761) * Re-add events package * run redis as a dep * remove redis events * fix: data race on event subscriber * fix: data race in tests * fix: store errors * fix: lint issues * feat: default stream * Update file.go --------- Co-authored-by: Brian Ketelsen <bketelsen@gmail.com> * . * copilot couldn't make it compile so I did * copilot couldn't make it compile so I did * x --------- Co-authored-by: Brian Ketelsen <bketelsen@gmail.com>
This commit is contained in:
		| @@ -362,7 +362,6 @@ func BenchmarkSub32(b *testing.B) { | ||||
| 	sub(b, 32) | ||||
| } | ||||
|  | ||||
|  | ||||
| func BenchmarkPub1(b *testing.B) { | ||||
| 	pub(b, 1) | ||||
| } | ||||
|   | ||||
| @@ -45,8 +45,6 @@ type publication struct { | ||||
| 	err error | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| func (p *publication) Ack() error { | ||||
| 	return p.d.Ack(false) | ||||
| } | ||||
|   | ||||
							
								
								
									
										57
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								cmd/cmd.go
									
									
									
									
									
								
							| @@ -4,17 +4,12 @@ package cmd | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"math/rand" | ||||
| 	"os" | ||||
| 	"sort" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/urfave/cli/v2" | ||||
| 	"go-micro.dev/v5/auth" | ||||
| 	nbroker "go-micro.dev/v5/broker/nats" | ||||
| 	rabbit "go-micro.dev/v5/broker/rabbitmq" | ||||
|  | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	"go-micro.dev/v5/cache" | ||||
| 	"go-micro.dev/v5/cache/redis" | ||||
| 	"go-micro.dev/v5/client" | ||||
| @@ -26,6 +21,13 @@ import ( | ||||
| 	"go-micro.dev/v5/events" | ||||
| 	"go-micro.dev/v5/logger" | ||||
| 	mprofile "go-micro.dev/v5/profile" | ||||
| 	"go-micro.dev/v5/auth" | ||||
| 	"go-micro.dev/v5/broker" | ||||
| 	nbroker "go-micro.dev/v5/broker/nats" | ||||
| 	rabbit "go-micro.dev/v5/broker/rabbitmq" | ||||
| 	"go-micro.dev/v5/genai" | ||||
| 	"go-micro.dev/v5/genai/gemini" | ||||
| 	"go-micro.dev/v5/genai/openai" | ||||
| 	"go-micro.dev/v5/registry" | ||||
| 	"go-micro.dev/v5/registry/consul" | ||||
| 	"go-micro.dev/v5/registry/etcd" | ||||
| @@ -246,6 +248,21 @@ var ( | ||||
| 			EnvVars: []string{"MICRO_CONFIG"}, | ||||
| 			Usage:   "The source of the config to be used to get configuration", | ||||
| 		}, | ||||
| 		&cli.StringFlag{ | ||||
| 			Name:    "genai", | ||||
| 			EnvVars: []string{"MICRO_GENAI"}, | ||||
| 			Usage:   "GenAI provider to use (e.g. openai, gemini, noop)", | ||||
| 		}, | ||||
| 		&cli.StringFlag{ | ||||
| 			Name:    "genai_key", | ||||
| 			EnvVars: []string{"MICRO_GENAI_KEY"}, | ||||
| 			Usage:   "GenAI API key", | ||||
| 		}, | ||||
| 		&cli.StringFlag{ | ||||
| 			Name:    "genai_model", | ||||
| 			EnvVars: []string{"MICRO_GENAI_MODEL"}, | ||||
| 			Usage:   "GenAI model to use (optional)", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	DefaultBrokers = map[string]func(...broker.Option) broker.Broker{ | ||||
| @@ -295,6 +312,11 @@ var ( | ||||
| 		"redis": redis.NewRedisCache, | ||||
| 	} | ||||
| 	DefaultStreams = map[string]func(...events.Option) (events.Stream, error){} | ||||
|  | ||||
| 	DefaultGenAI = map[string]func(...genai.Option) genai.GenAI{ | ||||
| 		"openai": openai.New, | ||||
| 		"gemini": gemini.New, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| func init() { | ||||
| @@ -367,6 +389,8 @@ func (c *cmd) Options() Options { | ||||
| } | ||||
|  | ||||
| func (c *cmd) Before(ctx *cli.Context) error { | ||||
| 	// Set GenAI provider from flags/env | ||||
| 	setGenAIFromFlags(ctx) | ||||
| 	// If flags are set then use them otherwise do nothing | ||||
| 	var serverOpts []server.Option | ||||
| 	var clientOpts []client.Option | ||||
| @@ -799,3 +823,24 @@ func Register(cmds ...*cli.Command) { | ||||
| 		return app.Commands[i].Name < app.Commands[j].Name | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func setGenAIFromFlags(ctx *cli.Context) { | ||||
| 	provider := ctx.String("genai") | ||||
| 	key := ctx.String("genai_key") | ||||
| 	model := ctx.String("genai_model") | ||||
|  | ||||
| 	switch provider { | ||||
| 	case "openai": | ||||
| 		if key == "" { | ||||
| 			key = os.Getenv("OPENAI_API_KEY") | ||||
| 		} | ||||
| 		genai.DefaultGenAI = openai.New(genai.WithAPIKey(key), genai.WithModel(model)) | ||||
| 	case "gemini": | ||||
| 		if key == "" { | ||||
| 			key = os.Getenv("GEMINI_API_KEY") | ||||
| 		} | ||||
| 		genai.DefaultGenAI = gemini.New(genai.WithAPIKey(key), genai.WithModel(model)) | ||||
| 	default: | ||||
| 		genai.DefaultGenAI = genai.Default | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -8,11 +8,11 @@ import ( | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"go-micro.dev/v5/events/natsjs" | ||||
| 	nserver "github.com/nats-io/nats-server/v2/server" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/test-go/testify/require" | ||||
| 	"go-micro.dev/v5/events" | ||||
| 	"go-micro.dev/v5/events/natsjs" | ||||
| ) | ||||
|  | ||||
| type Payload struct { | ||||
|   | ||||
							
								
								
									
										16
									
								
								genai/default.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								genai/default.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package genai | ||||
|  | ||||
| import ( | ||||
| 	"sync" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	DefaultGenAI GenAI = &noopGenAI{} | ||||
| 	defaultOnce  sync.Once | ||||
| ) | ||||
|  | ||||
| func SetDefault(g GenAI) { | ||||
| 	defaultOnce.Do(func() { | ||||
| 		DefaultGenAI = g | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										161
									
								
								genai/gemini/gemini.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								genai/gemini/gemini.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,161 @@ | ||||
| package gemini | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
|  | ||||
| 	"go-micro.dev/v5/genai" | ||||
| ) | ||||
|  | ||||
| // gemini implements the GenAI interface using Google Gemini 2.5 API. | ||||
| type gemini struct { | ||||
| 	options genai.Options | ||||
| } | ||||
|  | ||||
| func New(opts ...genai.Option) genai.GenAI { | ||||
| 	var options genai.Options | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| 	if options.APIKey == "" { | ||||
| 		options.APIKey = os.Getenv("GEMINI_API_KEY") | ||||
| 	} | ||||
| 	return &gemini{options: options} | ||||
| } | ||||
|  | ||||
| func (g *gemini) Generate(prompt string, opts ...genai.Option) (*genai.Result, error) { | ||||
| 	options := g.options | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| 	ctx := context.Background() | ||||
|  | ||||
| 	res := &genai.Result{Prompt: prompt, Type: options.Type} | ||||
|  | ||||
| 	endpoint := options.Endpoint | ||||
| 	if endpoint == "" { | ||||
| 		endpoint = "https://generativelanguage.googleapis.com/v1beta/models/" | ||||
| 	} | ||||
|  | ||||
| 	var url string | ||||
| 	var body map[string]interface{} | ||||
|  | ||||
| 	// Determine model to use | ||||
| 	var model string | ||||
| 	switch options.Type { | ||||
| 	case "image": | ||||
| 		if options.Model != "" { | ||||
| 			model = options.Model | ||||
| 		} else { | ||||
| 			model = "gemini-2.5-pro-vision" | ||||
| 		} | ||||
| 		url = endpoint + model + ":generateContent?key=" + options.APIKey | ||||
| 		body = map[string]interface{}{ | ||||
| 			"contents": []map[string]interface{}{ | ||||
| 				{"parts": []map[string]string{{"text": prompt}}}, | ||||
| 			}, | ||||
| 		} | ||||
| 	case "audio": | ||||
| 		if options.Model != "" { | ||||
| 			model = options.Model | ||||
| 		} else { | ||||
| 			model = "gemini-2.5-pro" | ||||
| 		} | ||||
| 		url = endpoint + model + ":generateContent?key=" + options.APIKey | ||||
| 		body = map[string]interface{}{ | ||||
| 			"contents": []map[string]interface{}{ | ||||
| 				{"parts": []map[string]string{{"text": prompt}}}, | ||||
| 			}, | ||||
| 			"response_mime_type": "audio/wav", | ||||
| 		} | ||||
| 	case "text": | ||||
| 		fallthrough | ||||
| 	default: | ||||
| 		if options.Model != "" { | ||||
| 			model = options.Model | ||||
| 		} else { | ||||
| 			model = "gemini-2.5-pro" | ||||
| 		} | ||||
| 		url = endpoint + model + ":generateContent?key=" + options.APIKey | ||||
| 		body = map[string]interface{}{ | ||||
| 			"contents": []map[string]interface{}{ | ||||
| 				{"parts": []map[string]string{{"text": prompt}}}, | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b, _ := json.Marshal(body) | ||||
| 	req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(b)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
|  | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	if options.Type == "audio" { | ||||
| 		var result struct { | ||||
| 			Candidates []struct { | ||||
| 				Content struct { | ||||
| 					Parts []struct { | ||||
| 						InlineData struct { | ||||
| 							Data []byte `json:"data"` | ||||
| 						} `json:"inline_data"` | ||||
| 					} `json:"parts"` | ||||
| 				} `json:"content"` | ||||
| 			} `json:"candidates"` | ||||
| 		} | ||||
| 		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 { | ||||
| 			return nil, fmt.Errorf("no audio returned") | ||||
| 		} | ||||
| 		res.Data = result.Candidates[0].Content.Parts[0].InlineData.Data | ||||
| 		return res, nil | ||||
| 	} | ||||
|  | ||||
| 	var result struct { | ||||
| 		Candidates []struct { | ||||
| 			Content struct { | ||||
| 				Parts []struct { | ||||
| 					Text string `json:"text"` | ||||
| 				} `json:"parts"` | ||||
| 			} `json:"content"` | ||||
| 		} `json:"candidates"` | ||||
| 	} | ||||
| 	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if len(result.Candidates) == 0 || len(result.Candidates[0].Content.Parts) == 0 { | ||||
| 		return nil, fmt.Errorf("no candidates returned") | ||||
| 	} | ||||
| 	res.Text = result.Candidates[0].Content.Parts[0].Text | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| func (g *gemini) Stream(prompt string, opts ...genai.Option) (*genai.Stream, error) { | ||||
| 	results := make(chan *genai.Result) | ||||
| 	go func() { | ||||
| 		defer close(results) | ||||
| 		res, err := g.Generate(prompt, opts...) | ||||
| 		if err != nil { | ||||
| 			// Send error via Stream.Err, not channel | ||||
| 			return | ||||
| 		} | ||||
| 		results <- res | ||||
| 	}() | ||||
| 	return &genai.Stream{Results: results}, nil | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	genai.Register("gemini", New()) | ||||
| } | ||||
							
								
								
									
										53
									
								
								genai/genai.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								genai/genai.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // Package genai provides a generic interface for generative AI providers. | ||||
| package genai | ||||
|  | ||||
| // Result is the unified response from GenAI providers. | ||||
| type Result struct { | ||||
| 	Prompt string | ||||
| 	Type   string | ||||
| 	Data   []byte // for audio/image binary data | ||||
| 	Text   string // for text or image URL | ||||
| } | ||||
|  | ||||
| // Stream represents a streaming response from a GenAI provider. | ||||
| type Stream struct { | ||||
| 	Results <-chan *Result | ||||
| 	Err     error | ||||
| 	// You can add fields for cancellation, errors, etc. if needed | ||||
| } | ||||
|  | ||||
| // GenAI is the generic interface for generative AI providers. | ||||
| type GenAI interface { | ||||
| 	Generate(prompt string, opts ...Option) (*Result, error) | ||||
| 	Stream(prompt string, opts ...Option) (*Stream, error) | ||||
| } | ||||
|  | ||||
| // Option is a functional option for configuring providers. | ||||
| type Option func(*Options) | ||||
|  | ||||
| // Options holds configuration for providers. | ||||
| type Options struct { | ||||
| 	APIKey   string | ||||
| 	Endpoint string | ||||
| 	Type     string // "text", "image", "audio", etc. | ||||
| 	Model    string // model name, e.g. "gemini-2.5-pro" | ||||
| 	// Add more fields as needed | ||||
| } | ||||
|  | ||||
| // Option functions for generation type | ||||
| func Text(o *Options)  { o.Type = "text" } | ||||
| func Image(o *Options) { o.Type = "image" } | ||||
| func Audio(o *Options) { o.Type = "audio" } | ||||
|  | ||||
| // Provider registry | ||||
| var providers = make(map[string]GenAI) | ||||
|  | ||||
| // Register a GenAI provider by name. | ||||
| func Register(name string, provider GenAI) { | ||||
| 	providers[name] = provider | ||||
| } | ||||
|  | ||||
| // Get a GenAI provider by name. | ||||
| func Get(name string) GenAI { | ||||
| 	return providers[name] | ||||
| } | ||||
							
								
								
									
										16
									
								
								genai/noop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								genai/noop.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| package genai | ||||
|  | ||||
| type noopGenAI struct{} | ||||
|  | ||||
| func (n *noopGenAI) Generate(prompt string, opts ...Option) (*Result, error) { | ||||
| 	return &Result{Prompt: prompt, Type: "noop", Text: "noop response"}, nil | ||||
| } | ||||
|  | ||||
| func (n *noopGenAI) Stream(prompt string, opts ...Option) (*Stream, error) { | ||||
| 	results := make(chan *Result, 1) | ||||
| 	results <- &Result{Prompt: prompt, Type: "noop", Text: "noop response"} | ||||
| 	close(results) | ||||
| 	return &Stream{Results: results}, nil | ||||
| } | ||||
|  | ||||
| var Default = &noopGenAI{} | ||||
							
								
								
									
										151
									
								
								genai/openai/openai.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								genai/openai/openai.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| package openai | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
|  | ||||
| 	"go-micro.dev/v5/genai" | ||||
| ) | ||||
|  | ||||
| type openAI struct { | ||||
| 	options genai.Options | ||||
| } | ||||
|  | ||||
| func New(opts ...genai.Option) genai.GenAI { | ||||
| 	var options genai.Options | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| 	if options.APIKey == "" { | ||||
| 		options.APIKey = os.Getenv("OPENAI_API_KEY") | ||||
| 	} | ||||
| 	return &openAI{options: options} | ||||
| } | ||||
|  | ||||
| func (o *openAI) Generate(prompt string, opts ...genai.Option) (*genai.Result, error) { | ||||
| 	options := o.options | ||||
| 	for _, opt := range opts { | ||||
| 		opt(&options) | ||||
| 	} | ||||
|  | ||||
| 	res := &genai.Result{Prompt: prompt, Type: options.Type} | ||||
|  | ||||
| 	var url string | ||||
| 	var body map[string]interface{} | ||||
|  | ||||
| 	switch options.Type { | ||||
| 	case "image": | ||||
| 		model := options.Model | ||||
| 		if model == "" { | ||||
| 			model = "dall-e-3" | ||||
| 		} | ||||
| 		url = "https://api.openai.com/v1/images/generations" | ||||
| 		body = map[string]interface{}{ | ||||
| 			"prompt": prompt, | ||||
| 			"n":      1, | ||||
| 			"size":   "1024x1024", | ||||
| 			"model":  model, | ||||
| 		} | ||||
| 	case "audio": | ||||
| 		model := options.Model | ||||
| 		if model == "" { | ||||
| 			model = "tts-1" | ||||
| 		} | ||||
| 		url = "https://api.openai.com/v1/audio/speech" | ||||
| 		body = map[string]interface{}{ | ||||
| 			"model": model, | ||||
| 			"input": prompt, | ||||
| 			"voice": "alloy", // or another supported voice | ||||
| 		} | ||||
| 	case "text": | ||||
| 		fallthrough | ||||
| 	default: | ||||
| 		model := options.Model | ||||
| 		if model == "" { | ||||
| 			model = "gpt-3.5-turbo" | ||||
| 		} | ||||
| 		url = "https://api.openai.com/v1/chat/completions" | ||||
| 		body = map[string]interface{}{ | ||||
| 			"model":    model, | ||||
| 			"messages": []map[string]string{{"role": "user", "content": prompt}}, | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	b, _ := json.Marshal(body) | ||||
| 	req, err := http.NewRequest("POST", url, bytes.NewReader(b)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	req.Header.Set("Authorization", "Bearer "+options.APIKey) | ||||
| 	req.Header.Set("Content-Type", "application/json") | ||||
|  | ||||
| 	resp, err := http.DefaultClient.Do(req) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	switch options.Type { | ||||
| 	case "image": | ||||
| 		var result struct { | ||||
| 			Data []struct { | ||||
| 				URL string `json:"url"` | ||||
| 			} `json:"data"` | ||||
| 		} | ||||
| 		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(result.Data) == 0 { | ||||
| 			return nil, fmt.Errorf("no image returned") | ||||
| 		} | ||||
| 		res.Text = result.Data[0].URL | ||||
| 		return res, nil | ||||
| 	case "audio": | ||||
| 		data, err := io.ReadAll(resp.Body) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		res.Data = data | ||||
| 		return res, nil | ||||
| 	case "text": | ||||
| 		fallthrough | ||||
| 	default: | ||||
| 		var result struct { | ||||
| 			Choices []struct { | ||||
| 				Message struct { | ||||
| 					Content string `json:"content"` | ||||
| 				} `json:"message"` | ||||
| 			} `json:"choices"` | ||||
| 		} | ||||
| 		if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(result.Choices) == 0 { | ||||
| 			return nil, fmt.Errorf("no choices returned") | ||||
| 		} | ||||
| 		res.Text = result.Choices[0].Message.Content | ||||
| 		return res, nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (o *openAI) Stream(prompt string, opts ...genai.Option) (*genai.Stream, error) { | ||||
| 	results := make(chan *genai.Result) | ||||
| 	go func() { | ||||
| 		defer close(results) | ||||
| 		res, err := o.Generate(prompt, opts...) | ||||
| 		if err != nil { | ||||
| 			// Send error via Stream.Err, not channel | ||||
| 			return | ||||
| 		} | ||||
| 		results <- res | ||||
| 	}() | ||||
| 	return &genai.Stream{Results: results}, nil | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	genai.Register("openai", New()) | ||||
| } | ||||
							
								
								
									
										37
									
								
								genai/openai/openai_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								genai/openai/openai_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| package openai | ||||
|  | ||||
| import ( | ||||
| 	"go-micro.dev/v5/genai" | ||||
| 	"os" | ||||
| 	"testing" | ||||
| ) | ||||
|  | ||||
| func TestOpenAI_GenerateText(t *testing.T) { | ||||
| 	apiKey := os.Getenv("OPENAI_API_KEY") | ||||
| 	if apiKey == "" { | ||||
| 		t.Skip("OPENAI_API_KEY not set") | ||||
| 	} | ||||
| 	client := New(genai.WithAPIKey(apiKey)) | ||||
| 	res, err := client.Generate("Say hello world", genai.Text) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Generate error: %v", err) | ||||
| 	} | ||||
| 	if res == nil || res.Text == "" { | ||||
| 		t.Error("Expected non-empty text response") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestOpenAI_GenerateImage(t *testing.T) { | ||||
| 	apiKey := os.Getenv("OPENAI_API_KEY") | ||||
| 	if apiKey == "" { | ||||
| 		t.Skip("OPENAI_API_KEY not set") | ||||
| 	} | ||||
| 	client := New(genai.WithAPIKey(apiKey)) | ||||
| 	res, err := client.Generate("A cat wearing sunglasses", genai.Image) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Generate error: %v", err) | ||||
| 	} | ||||
| 	if res == nil || res.Text == "" { | ||||
| 		t.Error("Expected non-empty image URL") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										20
									
								
								genai/options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								genai/options.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| package genai | ||||
|  | ||||
| // Option sets options for a GenAI provider. | ||||
| func WithAPIKey(key string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.APIKey = key | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func WithEndpoint(endpoint string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Endpoint = endpoint | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func WithModel(model string) Option { | ||||
| 	return func(o *Options) { | ||||
| 		o.Model = model | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								go.mod
									
									
									
									
									
								
							| @@ -40,12 +40,16 @@ require ( | ||||
| 	golang.org/x/crypto v0.37.0 | ||||
| 	golang.org/x/net v0.38.0 | ||||
| 	golang.org/x/sync v0.13.0 | ||||
| 	google.golang.org/genai v1.12.0 | ||||
| 	google.golang.org/grpc v1.71.1 | ||||
| 	google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057 | ||||
| 	google.golang.org/protobuf v1.36.6 | ||||
| ) | ||||
|  | ||||
| require ( | ||||
| 	cloud.google.com/go v0.120.0 // indirect | ||||
| 	cloud.google.com/go/auth v0.15.0 // indirect | ||||
| 	cloud.google.com/go/compute/metadata v0.6.0 // indirect | ||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | ||||
| 	github.com/armon/go-metrics v0.4.1 // indirect | ||||
| 	github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect | ||||
| @@ -55,10 +59,16 @@ require ( | ||||
| 	github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect | ||||
| 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect | ||||
| 	github.com/fatih/color v1.16.0 // indirect | ||||
| 	github.com/felixge/httpsnoop v1.0.4 // indirect | ||||
| 	github.com/go-logr/logr v1.4.2 // indirect | ||||
| 	github.com/go-logr/stdr v1.2.2 // indirect | ||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | ||||
| 	github.com/google/go-cmp v0.7.0 // indirect | ||||
| 	github.com/google/go-tpm v0.9.3 // indirect | ||||
| 	github.com/google/s2a-go v0.1.9 // indirect | ||||
| 	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect | ||||
| 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect | ||||
| 	github.com/gorilla/websocket v1.5.3 // indirect | ||||
| 	github.com/hashicorp/errwrap v1.1.0 // indirect | ||||
| 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect | ||||
| 	github.com/hashicorp/go-hclog v1.5.0 // indirect | ||||
| @@ -91,6 +101,7 @@ require ( | ||||
| 	github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect | ||||
| 	go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect | ||||
| 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect | ||||
| 	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect | ||||
| 	go.opentelemetry.io/otel/metric v1.35.0 // indirect | ||||
| 	go.uber.org/multierr v1.10.0 // indirect | ||||
| 	golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect | ||||
|   | ||||
							
								
								
									
										20
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.sum
									
									
									
									
									
								
							| @@ -1,3 +1,9 @@ | ||||
| cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= | ||||
| cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= | ||||
| cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= | ||||
| cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= | ||||
| cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= | ||||
| cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= | ||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||
| github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= | ||||
| @@ -55,6 +61,8 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL | ||||
| github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= | ||||
| github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= | ||||
| github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= | ||||
| github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= | ||||
| github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= | ||||
| github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= | ||||
| github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= | ||||
| github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= | ||||
| @@ -95,8 +103,16 @@ github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= | ||||
| github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= | ||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||
| github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= | ||||
| github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= | ||||
| github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= | ||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= | ||||
| github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= | ||||
| github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= | ||||
| github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= | ||||
| github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= | ||||
| github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/hashicorp/consul/api v1.32.1 h1:0+osr/3t/aZNAdJX558crU3PEjVrG4x6715aZHRgceE= | ||||
| github.com/hashicorp/consul/api v1.32.1/go.mod h1:mXUWLnxftwTmDv4W3lzxYCPD199iNLLUyLfLGFJbtl4= | ||||
| github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg= | ||||
| @@ -355,6 +371,8 @@ go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY | ||||
| go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU= | ||||
| go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= | ||||
| go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= | ||||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= | ||||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= | ||||
| go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= | ||||
| go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= | ||||
| go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= | ||||
| @@ -492,6 +510,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/genai v1.12.0 h1:0JjAdwvEAha9ZpPH5hL6dVG8bpMnRbAMCgv2f2LDnz4= | ||||
| google.golang.org/genai v1.12.0/go.mod h1:HFXR1zT3LCdLxd/NW6IOSCczOYyRAxwaShvYbgPSeVw= | ||||
| google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM= | ||||
| google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8= | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= | ||||
|   | ||||
| @@ -43,6 +43,7 @@ type natsStore struct { | ||||
| 	js      nats.JetStreamContext | ||||
| 	buckets *hashmap.Map[string, nats.KeyValue] | ||||
| } | ||||
|  | ||||
| // NewStore will create a new NATS JetStream Object Store. | ||||
| func NewStore(opts ...store.Option) store.Store { | ||||
| 	options := store.Options{ | ||||
|   | ||||
| @@ -198,11 +198,11 @@ func (s *sqlStore) List(opts ...store.ListOption) ([]string, error) { | ||||
|  | ||||
| 	var rows pgx.Rows | ||||
| 	if options.Limit > 0 { | ||||
| 			rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset) | ||||
| 		rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset) | ||||
|  | ||||
| 	} else { | ||||
|  | ||||
| 			rows, err = db.Query(s.options.Context, queries.ListAsc, pattern) | ||||
| 		rows, err = db.Query(s.options.Context, queries.ListAsc, pattern) | ||||
|  | ||||
| 	} | ||||
| 	if err != nil { | ||||
| @@ -273,9 +273,7 @@ func (s *sqlStore) rowsToRecords(rows pgx.Rows) ([]*store.Record, error) { | ||||
|  | ||||
| // Read a single key | ||||
| func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, error) { | ||||
| 	options := store.ReadOptions{ | ||||
|  | ||||
| 	} | ||||
| 	options := store.ReadOptions{} | ||||
| 	for _, o := range opts { | ||||
| 		o(&options) | ||||
| 	} | ||||
| @@ -307,11 +305,11 @@ func (s *sqlStore) Read(key string, opts ...store.ReadOption) ([]*store.Record, | ||||
| 	var rows pgx.Rows | ||||
| 	if options.Limit > 0 { | ||||
|  | ||||
| 			rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset) | ||||
| 		rows, err = db.Query(s.options.Context, queries.ListAscLimit, pattern, options.Limit, options.Offset) | ||||
|  | ||||
| 	} else { | ||||
|  | ||||
| 			rows, err = db.Query(s.options.Context, queries.ListAsc, pattern) | ||||
| 		rows, err = db.Query(s.options.Context, queries.ListAsc, pattern) | ||||
|  | ||||
| 	} | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -257,8 +257,6 @@ func (s *sqlStore) prepare(database, table, query string) (*sql.Stmt, error) { | ||||
| 		return nil, errors.New("unsupported statement") | ||||
| 	} | ||||
|  | ||||
|  | ||||
|  | ||||
| 	// get DB | ||||
| 	database, table = s.getDB(database, table) | ||||
|  | ||||
|   | ||||
| @@ -61,8 +61,6 @@ var ( | ||||
| 	DefaultTimeout = time.Minute | ||||
| ) | ||||
|  | ||||
|  | ||||
|  | ||||
| func configure(n *ntport, opts ...transport.Option) { | ||||
| 	for _, o := range opts { | ||||
| 		o(&n.opts) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user