* feat(embed): retry transient embedding errors and survive partial failures Classify OpenAI embedding errors into a typed APIError and retry transient ones (429, 5xx, network timeouts) with Retry-After-aware exponential backoff and jitter; longer base for overloaded_error. insufficient_quota, 4xx, and ctx errors surface immediately. Replace abort-on-first-error with a per-batch retry queue: each batch retries once with fresh backoff and the rest keep going. Final run status is success / partial / error / cancelled, and stats_json carries retries plus per-batch failure metadata for diagnostics. * fix(embed): avoid final retry sleep --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
89 lines
1.8 KiB
Go
89 lines
1.8 KiB
Go
package openai
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type APIError struct {
|
|
Status int
|
|
Type string
|
|
Code string
|
|
Message string
|
|
RetryAfter time.Duration
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
parts := []string{fmt.Sprintf("openai embeddings status=%d", e.Status)}
|
|
if e.Type != "" {
|
|
parts = append(parts, "type="+e.Type)
|
|
}
|
|
if e.Code != "" {
|
|
parts = append(parts, "code="+e.Code)
|
|
}
|
|
if e.Message != "" {
|
|
parts = append(parts, "message="+e.Message)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func (e *APIError) Retryable() bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
switch e.Status {
|
|
case http.StatusRequestTimeout, http.StatusTooManyRequests:
|
|
return e.Type != "insufficient_quota" && e.Code != "insufficient_quota"
|
|
case http.StatusInternalServerError, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (e *APIError) IsOverloaded() bool {
|
|
return e != nil && (e.Type == "overloaded_error" || (e.Status == http.StatusServiceUnavailable && e.Code == "overloaded"))
|
|
}
|
|
|
|
func AsAPIError(err error) *APIError {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
var apiErr *APIError
|
|
if errors.As(err, &apiErr) {
|
|
return apiErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseRetryAfter(header string, now time.Time) time.Duration {
|
|
header = strings.TrimSpace(header)
|
|
if header == "" {
|
|
return 0
|
|
}
|
|
if seconds, err := strconv.Atoi(header); err == nil {
|
|
if seconds < 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(seconds) * time.Second
|
|
}
|
|
if seconds, err := strconv.ParseFloat(header, 64); err == nil {
|
|
if seconds < 0 {
|
|
return 0
|
|
}
|
|
return time.Duration(seconds * float64(time.Second))
|
|
}
|
|
if when, err := http.ParseTime(header); err == nil {
|
|
delta := when.Sub(now)
|
|
if delta < 0 {
|
|
return 0
|
|
}
|
|
return delta
|
|
}
|
|
return 0
|
|
}
|