!38 fix: 为图片生成 API 添加超时与 5xx 重试机制

Merge pull request !38 from 何朝晖/hotfix/nginx
This commit is contained in:
2026-05-25 06:08:33 +00:00
committed by Gitee
3 changed files with 71 additions and 21 deletions
+3
View File
@@ -26,6 +26,9 @@ GEN2D_IMAGE_MODEL=gpt-image-2-token
GEN2D_IMAGE_WIDTH=1024
GEN2D_IMAGE_HEIGHT=1024
GEN2D_IMAGE_QUALITY=low
GEN2D_IMAGE_TIMEOUT=120
GEN2D_IMAGE_MAX_RETRIES=2
GEN2D_IMAGE_RETRY_DELAY=5
GEN2D_IMAGE_NUM_IMAGES=1
GEN2D_IMAGE_STEPS=30
GEN2D_IMAGE_CFG_SCALE=7.0
+15 -6
View File
@@ -46,12 +46,15 @@ type LLMConfig struct {
// ImageGenConfig OpenAI 兼容文生图模型配置。
type ImageGenConfig struct {
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"`
Width int `mapstructure:"width"`
Height int `mapstructure:"height"`
Quality string `mapstructure:"quality"`
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
Model string `mapstructure:"model"`
Width int `mapstructure:"width"`
Height int `mapstructure:"height"`
Quality string `mapstructure:"quality"`
Timeout int `mapstructure:"timeout"` // HTTP 请求超时秒数,默认 120
MaxRetries int `mapstructure:"max_retries"` // 5xx 错误重试次数,默认 2
RetryDelay int `mapstructure:"retry_delay"` // 重试间隔秒数,默认 5
}
// QiniuConfig 七牛云对象存储配置。
@@ -117,6 +120,9 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("image_gen.width", 1024)
v.SetDefault("image_gen.height", 1024)
v.SetDefault("image_gen.quality", "low")
v.SetDefault("image_gen.timeout", 120)
v.SetDefault("image_gen.max_retries", 2)
v.SetDefault("image_gen.retry_delay", 5)
v.SetDefault("image_gen.num_images", 1)
v.SetDefault("image_gen.steps", 30)
v.SetDefault("image_gen.cfg_scale", 7.0)
@@ -148,6 +154,9 @@ func bindEnvVars(v *viper.Viper) {
v.BindEnv("image_gen.width", "GEN2D_IMAGE_WIDTH")
v.BindEnv("image_gen.height", "GEN2D_IMAGE_HEIGHT")
v.BindEnv("image_gen.quality", "GEN2D_IMAGE_QUALITY")
v.BindEnv("image_gen.timeout", "GEN2D_IMAGE_TIMEOUT")
v.BindEnv("image_gen.max_retries", "GEN2D_IMAGE_MAX_RETRIES")
v.BindEnv("image_gen.retry_delay", "GEN2D_IMAGE_RETRY_DELAY")
v.BindEnv("image_gen.num_images", "GEN2D_IMAGE_NUM_IMAGES")
v.BindEnv("image_gen.steps", "GEN2D_IMAGE_STEPS")
v.BindEnv("image_gen.cfg_scale", "GEN2D_IMAGE_CFG_SCALE")
+53 -15
View File
@@ -15,6 +15,7 @@ import (
"mime/multipart"
"net/http"
"strings"
"time"
"gen2d/internal/config"
)
@@ -22,9 +23,17 @@ import (
// imgCfg 保存文生图配置,由 main 通过 InitImageGenConfig 注入。
var imgCfg config.ImageGenConfig
// imageHTTPClient 带超时的 HTTP 客户端,由 InitImageGenConfig 初始化。
var imageHTTPClient *http.Client
// InitImageGenConfig 注入文生图配置。
func InitImageGenConfig(cfg config.ImageGenConfig) {
imgCfg = cfg
timeout := time.Duration(cfg.Timeout) * time.Second
if timeout <= 0 {
timeout = 120 * time.Second
}
imageHTTPClient = &http.Client{Timeout: timeout}
}
// ======================== OpenAI 兼容 Images API 类型 ========================
@@ -73,7 +82,7 @@ func GenerateImages(ctx context.Context, prompt string, params AssetParams) ([]G
return generateMockImages(size, count)
}
// callImageAPI 调用 OpenAI 兼容 Images API。
// callImageAPI 调用 OpenAI 兼容 Images API,5xx 错误自动重试。
func callImageAPI(ctx context.Context, prompt string, count, width, height int) ([]GeneratedImage, error) {
reqBody := imageGenRequest{
Model: imgCfg.Model,
@@ -89,25 +98,54 @@ func callImageAPI(ctx context.Context, prompt string, count, width, height int)
}
url := strings.TrimRight(imgCfg.BaseURL, "/") + "/images/generations"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
maxRetries := imgCfg.MaxRetries
if maxRetries <= 0 {
maxRetries = 2
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+imgCfg.APIKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
retryDelay := time.Duration(imgCfg.RetryDelay) * time.Second
if retryDelay <= 0 {
retryDelay = 5 * time.Second
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
log.Printf("[inference] retrying image API (attempt %d/%d)", attempt, maxRetries)
select {
case <-ctx.Done():
return nil, fmt.Errorf("context cancelled during retry: %w", ctx.Err())
case <-time.After(retryDelay):
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+imgCfg.APIKey)
resp, err := imageHTTPClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("send request: %w", err)
continue
}
if resp.StatusCode == http.StatusOK {
return parseImageResponse(ctx, resp.Body, width, height)
}
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
resp.Body.Close()
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b))
continue
}
return nil, fmt.Errorf("image api error %d: %s", resp.StatusCode, string(b))
}
return parseImageResponse(ctx, resp.Body, width, height)
return nil, lastErr
}
// parseImageResponse 解析 OpenAI 兼容图片响应(b64_json 或 url)。
@@ -152,7 +190,7 @@ func downloadImage(ctx context.Context, url string) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("create download request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
resp, err := imageHTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("download: %w", err)
}
@@ -199,7 +237,7 @@ func EditImages(ctx context.Context, imageData []byte, prompt string, count int)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+imgCfg.APIKey)
resp, err := http.DefaultClient.Do(req)
resp, err := imageHTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("send request: %w", err)
}