- Introduced `image_generate` tool for generating images from text prompts, with options for specifying output file paths and image dimensions. - Updated `image_read` functionality to allow optional provider specification for enhanced image analysis. - Implemented new `GenerateImage` method in the LLM API for seamless integration of image generation capabilities. - Enhanced documentation to include detailed usage examples for both image reading and generation tools. - Updated tests to validate new image generation features and ensure robust functionality across image tools.
183 lines
4.8 KiB
Go
183 lines
4.8 KiB
Go
package llm
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/yaoapp/gou/connector"
|
|
gouhttp "github.com/yaoapp/gou/http"
|
|
goullm "github.com/yaoapp/gou/llm"
|
|
)
|
|
|
|
// ImageGenResponse holds the result of an image generation call.
|
|
// Image is always base64 encoded; if the provider returns a URL, it is downloaded and converted.
|
|
type ImageGenResponse struct {
|
|
Image string `json:"image"` // base64 encoded image data
|
|
Format string `json:"format"` // image format, e.g. "png", "jpeg"
|
|
}
|
|
|
|
// GenerateImage calls the /images/generations endpoint through the connector.
|
|
// options may include: size, n, quality, style, model, etc.
|
|
func GenerateImage(conn connector.Connector, prompt string, options map[string]interface{}) (*ImageGenResponse, error) {
|
|
host, key, authMode := resolveConnSettings(conn)
|
|
if host == "" {
|
|
return nil, fmt.Errorf("no host found in connector settings")
|
|
}
|
|
if key == "" {
|
|
return nil, fmt.Errorf("API key is not set")
|
|
}
|
|
|
|
if options == nil {
|
|
options = map[string]interface{}{}
|
|
}
|
|
options["prompt"] = prompt
|
|
if _, ok := options["model"]; !ok {
|
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
|
if m := lc.GetModel(); m != "" {
|
|
options["model"] = m
|
|
}
|
|
}
|
|
}
|
|
|
|
url := connector.BuildAPIURL(host, "/images/generations")
|
|
req := gouhttp.New(url)
|
|
req.SetHeader("Content-Type", "application/json")
|
|
setImageAuthHeaders(req, authMode, key)
|
|
|
|
resp := req.Post(options)
|
|
if resp.Status != 200 {
|
|
errMsg := extractAPIError(resp.Data)
|
|
return nil, fmt.Errorf("image generation failed (status %d, url %s): %s", resp.Status, url, errMsg)
|
|
}
|
|
|
|
return extractImageFromResponse(resp.Data)
|
|
}
|
|
|
|
func resolveConnSettings(conn connector.Connector) (host, key string, authMode goullm.AuthMode) {
|
|
authMode = goullm.AuthBearer
|
|
if lc, ok := conn.(goullm.LLMConnector); ok {
|
|
host = lc.GetURL()
|
|
key = lc.GetKey()
|
|
authMode = lc.GetAuthMode()
|
|
}
|
|
if host == "" || key == "" {
|
|
setting := conn.Setting()
|
|
if host == "" {
|
|
host, _ = setting["host"].(string)
|
|
}
|
|
if key == "" {
|
|
key, _ = setting["key"].(string)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func setImageAuthHeaders(req *gouhttp.Request, authMode goullm.AuthMode, key string) {
|
|
switch authMode {
|
|
case goullm.AuthAPIKey:
|
|
req.SetHeader("api-key", key)
|
|
case goullm.AuthXAPIKey:
|
|
req.SetHeader("x-api-key", key)
|
|
default:
|
|
req.SetHeader("Authorization", fmt.Sprintf("Bearer %s", key))
|
|
}
|
|
}
|
|
|
|
func extractImageFromResponse(data interface{}) (*ImageGenResponse, error) {
|
|
raw, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal response: %w", err)
|
|
}
|
|
|
|
var parsed struct {
|
|
Data []struct {
|
|
B64JSON *string `json:"b64_json"`
|
|
URL *string `json:"url"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
|
return nil, fmt.Errorf("unmarshal response: %w", err)
|
|
}
|
|
if len(parsed.Data) == 0 {
|
|
return nil, fmt.Errorf("provider returned empty data array, no image was generated")
|
|
}
|
|
|
|
item := parsed.Data[0]
|
|
|
|
if item.B64JSON != nil && *item.B64JSON != "" {
|
|
return &ImageGenResponse{Image: *item.B64JSON, Format: "png"}, nil
|
|
}
|
|
|
|
if item.URL != nil && *item.URL != "" {
|
|
b64, format, err := downloadImageAsBase64(*item.URL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("provider returned url but download failed: %w", err)
|
|
}
|
|
return &ImageGenResponse{Image: b64, Format: format}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("provider returned data but neither b64_json nor url field is present, the model may not support image generation")
|
|
}
|
|
|
|
func downloadImageAsBase64(imageURL string) (b64 string, format string, err error) {
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Get(imageURL)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("http get: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return "", "", fmt.Errorf("download returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("read body: %w", err)
|
|
}
|
|
if len(body) == 0 {
|
|
return "", "", fmt.Errorf("downloaded image is empty")
|
|
}
|
|
|
|
format = "png"
|
|
ct := resp.Header.Get("Content-Type")
|
|
switch {
|
|
case strings.Contains(ct, "jpeg") || strings.Contains(ct, "jpg"):
|
|
format = "jpeg"
|
|
case strings.Contains(ct, "webp"):
|
|
format = "webp"
|
|
case strings.Contains(ct, "gif"):
|
|
format = "gif"
|
|
default:
|
|
if strings.Contains(imageURL, ".jpeg") || strings.Contains(imageURL, ".jpg") {
|
|
format = "jpeg"
|
|
} else if strings.Contains(imageURL, ".webp") {
|
|
format = "webp"
|
|
}
|
|
}
|
|
|
|
b64 = base64.StdEncoding.EncodeToString(body)
|
|
return b64, format, nil
|
|
}
|
|
|
|
func extractAPIError(data interface{}) string {
|
|
raw, err := json.Marshal(data)
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", data)
|
|
}
|
|
|
|
var parsed struct {
|
|
Error struct {
|
|
Message string `json:"message"`
|
|
} `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(raw, &parsed); err == nil && parsed.Error.Message != "" {
|
|
return parsed.Error.Message
|
|
}
|
|
return string(raw)
|
|
}
|