fix(image): add response-header timeout and idempotent retry
Give the image client a configurable ResponseHeaderTimeout (default 4m, capped at the overall timeout) so a stalled upstream fails fast, and retry such timeouts when an idempotency key is set so a slow-but-safe request can recover instead of terminating. Raise the Next.js API proxy maxDuration to 1200s to accommodate long generations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -25,8 +25,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultImageGenerationTimeout = 10 * time.Minute
|
||||
maxImageGenerationTimeout = 10 * time.Minute
|
||||
defaultImageGenerationTimeout = 10 * time.Minute
|
||||
defaultImageGenerationResponseHeaderTimeout = 4 * time.Minute
|
||||
maxImageGenerationTimeout = 10 * time.Minute
|
||||
)
|
||||
|
||||
type ImageGenerator interface {
|
||||
@@ -47,10 +48,11 @@ type ImageRequestOptions struct {
|
||||
}
|
||||
|
||||
type ImageClientOptions struct {
|
||||
BaseURL string
|
||||
APIKey string
|
||||
InputImageTransport string
|
||||
TimeoutSeconds int
|
||||
BaseURL string
|
||||
APIKey string
|
||||
InputImageTransport string
|
||||
TimeoutSeconds int
|
||||
ResponseHeaderTimeoutSeconds int
|
||||
}
|
||||
|
||||
type ImageClient struct {
|
||||
@@ -71,6 +73,13 @@ func NewImageClient(opts ImageClientOptions) *ImageClient {
|
||||
if timeout > maxImageGenerationTimeout {
|
||||
timeout = maxImageGenerationTimeout
|
||||
}
|
||||
responseHeaderTimeout := time.Duration(opts.ResponseHeaderTimeoutSeconds) * time.Second
|
||||
if responseHeaderTimeout <= 0 {
|
||||
responseHeaderTimeout = defaultImageGenerationResponseHeaderTimeout
|
||||
}
|
||||
if responseHeaderTimeout > timeout {
|
||||
responseHeaderTimeout = timeout
|
||||
}
|
||||
|
||||
return &ImageClient{
|
||||
baseURL: strings.TrimRight(opts.BaseURL, "/"),
|
||||
@@ -83,6 +92,7 @@ func NewImageClient(opts ImageClientOptions) *ImageClient {
|
||||
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
DisableKeepAlives: true,
|
||||
ForceAttemptHTTP2: false,
|
||||
ResponseHeaderTimeout: responseHeaderTimeout,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: time.Second,
|
||||
},
|
||||
@@ -147,9 +157,13 @@ func (c *ImageClient) Generate(ctx context.Context, prompt string, opts ImageReq
|
||||
)
|
||||
return image, nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return GeneratedImage{}, ctx.Err()
|
||||
}
|
||||
lastErr = err
|
||||
retryable := isRetryableImageGenerationError(err)
|
||||
terminal := isImageGenerationTerminalError(err)
|
||||
timeoutRetryable := idempotencyKey != "" && isImageGenerationTimeoutError(err)
|
||||
retryable := isRetryableImageGenerationError(err) || timeoutRetryable
|
||||
terminal := isImageGenerationTerminalError(err) && !timeoutRetryable
|
||||
logx.Errorf(
|
||||
"gpt image generation failed model=%s size=%s count=%d prompt_runes=%d attempt=%d elapsed=%s timeout=%s retryable=%t terminal=%t idempotency_key=%s err=%v",
|
||||
model,
|
||||
@@ -537,6 +551,13 @@ func isImageGenerationTerminalError(err error) bool {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
return isImageGenerationTimeoutError(err)
|
||||
}
|
||||
|
||||
func isImageGenerationTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var netErr net.Error
|
||||
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user