mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-26 14:15:16 +00:00
Merge pull request #1668 from lyd123qw2008/fix/codex-usage-limit-retry-after
fix(codex): honor usage_limit_reached resets_at for retry_after
This commit is contained in:
@@ -406,6 +406,9 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
|
|||||||
if !auth.LastRefreshedAt.IsZero() {
|
if !auth.LastRefreshedAt.IsZero() {
|
||||||
entry["last_refresh"] = auth.LastRefreshedAt
|
entry["last_refresh"] = auth.LastRefreshedAt
|
||||||
}
|
}
|
||||||
|
if !auth.NextRetryAfter.IsZero() {
|
||||||
|
entry["next_retry_after"] = auth.NextRetryAfter
|
||||||
|
}
|
||||||
if path != "" {
|
if path != "" {
|
||||||
entry["path"] = path
|
entry["path"] = path
|
||||||
entry["source"] = "file"
|
entry["source"] = "file"
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = newCodexStatusErr(httpResp.StatusCode, b)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
@@ -260,7 +260,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
appendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = newCodexStatusErr(httpResp.StatusCode, b)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
@@ -358,7 +358,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(data)}
|
err = newCodexStatusErr(httpResp.StatusCode, data)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out := make(chan cliproxyexecutor.StreamChunk)
|
out := make(chan cliproxyexecutor.StreamChunk)
|
||||||
@@ -673,6 +673,35 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s
|
|||||||
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newCodexStatusErr(statusCode int, body []byte) statusErr {
|
||||||
|
err := statusErr{code: statusCode, msg: string(body)}
|
||||||
|
if retryAfter := parseCodexRetryAfter(statusCode, body, time.Now()); retryAfter != nil {
|
||||||
|
err.retryAfter = retryAfter
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCodexRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration {
|
||||||
|
if statusCode != http.StatusTooManyRequests || len(errorBody) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(gjson.GetBytes(errorBody, "error.type").String()) != "usage_limit_reached" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resetsAt := gjson.GetBytes(errorBody, "error.resets_at").Int(); resetsAt > 0 {
|
||||||
|
resetAtTime := time.Unix(resetsAt, 0)
|
||||||
|
if resetAtTime.After(now) {
|
||||||
|
retryAfter := resetAtTime.Sub(now)
|
||||||
|
return &retryAfter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resetsInSeconds := gjson.GetBytes(errorBody, "error.resets_in_seconds").Int(); resetsInSeconds > 0 {
|
||||||
|
retryAfter := time.Duration(resetsInSeconds) * time.Second
|
||||||
|
return &retryAfter
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
func codexCreds(a *cliproxyauth.Auth) (apiKey, baseURL string) {
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|||||||
65
internal/runtime/executor/codex_executor_retry_test.go
Normal file
65
internal/runtime/executor/codex_executor_retry_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCodexRetryAfter(t *testing.T) {
|
||||||
|
now := time.Unix(1_700_000_000, 0)
|
||||||
|
|
||||||
|
t.Run("resets_in_seconds", func(t *testing.T) {
|
||||||
|
body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":123}}`)
|
||||||
|
retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now)
|
||||||
|
if retryAfter == nil {
|
||||||
|
t.Fatalf("expected retryAfter, got nil")
|
||||||
|
}
|
||||||
|
if *retryAfter != 123*time.Second {
|
||||||
|
t.Fatalf("retryAfter = %v, want %v", *retryAfter, 123*time.Second)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("prefers resets_at", func(t *testing.T) {
|
||||||
|
resetAt := now.Add(5 * time.Minute).Unix()
|
||||||
|
body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":1}}`)
|
||||||
|
retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now)
|
||||||
|
if retryAfter == nil {
|
||||||
|
t.Fatalf("expected retryAfter, got nil")
|
||||||
|
}
|
||||||
|
if *retryAfter != 5*time.Minute {
|
||||||
|
t.Fatalf("retryAfter = %v, want %v", *retryAfter, 5*time.Minute)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fallback when resets_at is past", func(t *testing.T) {
|
||||||
|
resetAt := now.Add(-1 * time.Minute).Unix()
|
||||||
|
body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":77}}`)
|
||||||
|
retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now)
|
||||||
|
if retryAfter == nil {
|
||||||
|
t.Fatalf("expected retryAfter, got nil")
|
||||||
|
}
|
||||||
|
if *retryAfter != 77*time.Second {
|
||||||
|
t.Fatalf("retryAfter = %v, want %v", *retryAfter, 77*time.Second)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non-429 status code", func(t *testing.T) {
|
||||||
|
body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":30}}`)
|
||||||
|
if got := parseCodexRetryAfter(http.StatusBadRequest, body, now); got != nil {
|
||||||
|
t.Fatalf("expected nil for non-429, got %v", *got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("non usage_limit_reached error type", func(t *testing.T) {
|
||||||
|
body := []byte(`{"error":{"type":"server_error","resets_in_seconds":30}}`)
|
||||||
|
if got := parseCodexRetryAfter(http.StatusTooManyRequests, body, now); got != nil {
|
||||||
|
t.Fatalf("expected nil for non-usage_limit_reached, got %v", *got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func itoa(v int64) string {
|
||||||
|
return strconv.FormatInt(v, 10)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user