From 36973d4a6f5debc93e768a00b6741a66fadacf8d Mon Sep 17 00:00:00 2001 From: pjpj Date: Wed, 25 Mar 2026 23:25:31 +0800 Subject: [PATCH] Handle Codex capacity errors as retryable --- internal/runtime/executor/codex_executor.go | 30 +++++++++++++++++-- .../executor/codex_executor_retry_test.go | 13 ++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 7e4163b8..1c3a916a 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -685,13 +685,39 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s } func newCodexStatusErr(statusCode int, body []byte) statusErr { - err := statusErr{code: statusCode, msg: string(body)} - if retryAfter := parseCodexRetryAfter(statusCode, body, time.Now()); retryAfter != nil { + errCode := statusCode + if isCodexModelCapacityError(body) { + errCode = http.StatusTooManyRequests + } + err := statusErr{code: errCode, msg: string(body)} + if retryAfter := parseCodexRetryAfter(errCode, body, time.Now()); retryAfter != nil { err.retryAfter = retryAfter } return err } +func isCodexModelCapacityError(errorBody []byte) bool { + if len(errorBody) == 0 { + return false + } + candidates := []string{ + gjson.GetBytes(errorBody, "error.message").String(), + gjson.GetBytes(errorBody, "message").String(), + string(errorBody), + } + for _, candidate := range candidates { + lower := strings.ToLower(strings.TrimSpace(candidate)) + if lower == "" { + continue + } + if strings.Contains(lower, "selected model is at capacity") || + strings.Contains(lower, "model is at capacity. please try a different model") { + return true + } + } + return false +} + func parseCodexRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration { if statusCode != http.StatusTooManyRequests || len(errorBody) == 0 { return nil diff --git a/internal/runtime/executor/codex_executor_retry_test.go b/internal/runtime/executor/codex_executor_retry_test.go index 3e54ae7c..249d40d6 100644 --- a/internal/runtime/executor/codex_executor_retry_test.go +++ b/internal/runtime/executor/codex_executor_retry_test.go @@ -60,6 +60,19 @@ func TestParseCodexRetryAfter(t *testing.T) { }) } +func TestNewCodexStatusErrTreatsCapacityAsRetryableRateLimit(t *testing.T) { + body := []byte(`{"error":{"message":"Selected model is at capacity. Please try a different model."}}`) + + err := newCodexStatusErr(http.StatusBadRequest, body) + + if got := err.StatusCode(); got != http.StatusTooManyRequests { + t.Fatalf("status code = %d, want %d", got, http.StatusTooManyRequests) + } + if err.RetryAfter() != nil { + t.Fatalf("expected nil explicit retryAfter for capacity fallback, got %v", *err.RetryAfter()) + } +} + func itoa(v int64) string { return strconv.FormatInt(v, 10) }