fix: 为图片生成 API 添加超时与 5xx 重试机制
- ImageGenConfig 新增 timeout/max_retries/retry_delay 配置项(默认 120s/2次/5s) - 替换 http.DefaultClient 为带超时的 imageHTTPClient - callImageAPI 对 5xx 错误自动重试,避免 504 等瞬时故障直接失败
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user