mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 22:17:36 +00:00
fix(tools): use /images/generations endpoint for Gemini and OpenAI image gen (#9)
create_image exclusively used /chat/completions with modalities:["image","text"] which only works on OpenRouter. Gemini returns HTTP 400: "Image generation is not yet supported on the chat.completions endpoint" OpenAI's DALL-E models also require /images/generations, not /chat/completions. Fix: route OpenRouter through /chat/completions (supports modalities), route all other providers (Gemini, OpenAI, etc.) through the standard /images/generations endpoint with response_format:"b64_json". Also update default Gemini model from deprecated gemini-2.0-flash-exp to gemini-2.5-flash-image.
This commit is contained in:
1 parent
4104420d5e
commit
370c290642
1 file changed
+73
-2
@@ -30,7 +30,7 @@ var imageGenProviderPriority = []string{"openrouter", "gemini", "openai"}
|
||||
var imageGenModelDefaults = map[string]string{
|
||||
"openrouter": "google/gemini-2.5-flash-image",
|
||||
"openai": "dall-e-3",
|
||||
"gemini": "gemini-2.0-flash-exp",
|
||||
"gemini": "gemini-2.5-flash-image",
|
||||
}
|
||||
|
||||
// CreateImageTool generates images using an image generation API.
|
||||
@@ -92,7 +92,18 @@ func (t *CreateImageTool) Execute(ctx context.Context, args map[string]interface
|
||||
slog.Info("create_image: calling image generation API",
|
||||
"provider", providerName, "model", model, "aspect_ratio", aspectRatio)
|
||||
|
||||
imageBytes, usage, err := t.callImageGenAPI(ctx, cp.APIKey(), cp.APIBase(), model, prompt, aspectRatio)
|
||||
// OpenRouter supports modalities on /chat/completions; Gemini, OpenAI and others use /images/generations
|
||||
var imageBytes []byte
|
||||
var usage *providers.Usage
|
||||
if providerName == "openrouter" {
|
||||
var genErr error
|
||||
imageBytes, usage, genErr = t.callImageGenAPI(ctx, cp.APIKey(), cp.APIBase(), model, prompt, aspectRatio)
|
||||
err = genErr
|
||||
} else {
|
||||
var genErr error
|
||||
imageBytes, usage, genErr = t.callStandardImageGenAPI(ctx, cp.APIKey(), cp.APIBase(), model, prompt)
|
||||
err = genErr
|
||||
}
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("image generation failed: %v", err))
|
||||
}
|
||||
@@ -219,6 +230,66 @@ func (t *CreateImageTool) callImageGenAPI(ctx context.Context, apiKey, apiBase,
|
||||
return t.parseImageResponse(respBody)
|
||||
}
|
||||
|
||||
// callStandardImageGenAPI uses the /images/generations endpoint (Gemini, OpenAI, and compatible providers).
|
||||
// This is the standard OpenAI-compatible image generation endpoint that returns b64_json data.
|
||||
func (t *CreateImageTool) callStandardImageGenAPI(ctx context.Context, apiKey, apiBase, model, prompt string) ([]byte, *providers.Usage, error) {
|
||||
body := map[string]interface{}{
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"n": 1,
|
||||
"response_format": "b64_json",
|
||||
}
|
||||
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
url := strings.TrimRight(apiBase, "/") + "/images/generations"
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonBody))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
|
||||
client := &http.Client{Timeout: 120 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, nil, fmt.Errorf("API error %d: %s", resp.StatusCode, truncateBytes(respBody, 500))
|
||||
}
|
||||
|
||||
// Parse OpenAI-compat images/generations response: {data: [{b64_json: "..."}]}
|
||||
var imgResp struct {
|
||||
Data []struct {
|
||||
B64JSON string `json:"b64_json"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &imgResp); err != nil {
|
||||
return nil, nil, fmt.Errorf("parse response: %w", err)
|
||||
}
|
||||
if len(imgResp.Data) == 0 || imgResp.Data[0].B64JSON == "" {
|
||||
return nil, nil, fmt.Errorf("no image data in response")
|
||||
}
|
||||
|
||||
imageBytes, err := base64.StdEncoding.DecodeString(imgResp.Data[0].B64JSON)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("decode base64: %w", err)
|
||||
}
|
||||
|
||||
return imageBytes, nil, nil
|
||||
}
|
||||
|
||||
// parseImageResponse extracts base64 image data from the OpenAI-compat chat response.
|
||||
// Looks for images in choices[0].message.content (multipart) or choices[0].message.images.
|
||||
func (t *CreateImageTool) parseImageResponse(respBody []byte) ([]byte, *providers.Usage, error) {
|
||||
|
||||
Reference in new issue
Block a user