gitcrawl/internal/openai/errors.go
Dallin Romney 940f940f79
feat(embed): retry transient embedding errors (#6)
* 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>
2026-04-30 23:35:21 -07:00

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
}