This commit is contained in:
2026-08-23 13:08:14 +02:00
parent ee1405cc1a
commit 89ae7e5fa8
4 changed files with 74 additions and 7 deletions
+3 -1
View File
@@ -507,7 +507,9 @@ func (c *OpenAIClient) doComplete(ctx context.Context, system, user string, temp
msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error) msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error)
httpErr := errors.New(msg) httpErr := errors.New(msg)
stopWait(httpErr) stopWait(httpErr)
return Completion{}, true, httpErr // insufficient_quota also arrives as HTTP 429 — do not burn MaxRetries.
retryable := res.StatusCode >= 500 || openAIErrorIsRetryableRateLimit(parsed.Error)
return Completion{}, retryable, httpErr
} }
if res.StatusCode >= 400 { if res.StatusCode >= 400 {
msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error) msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error)
@@ -50,6 +50,7 @@ func formatOpenAIHTTPError(status int, errObj *openAIErrorBody) string {
lower := strings.ToLower(msg) lower := strings.ToLower(msg)
codeLower := strings.ToLower(code) codeLower := strings.ToLower(code)
paramLower := strings.ToLower(param) paramLower := strings.ToLower(param)
typLower := strings.ToLower(typ)
switch { switch {
case status == 401 || codeLower == "invalid_api_key" || case status == 401 || codeLower == "invalid_api_key" ||
@@ -58,11 +59,20 @@ func formatOpenAIHTTPError(status int, errObj *openAIErrorBody) string {
return "AI provider rejected the API key" return "AI provider rejected the API key"
case status == 403: case status == 403:
return "AI provider forbidden the request" return "AI provider forbidden the request"
case status == 429 || codeLower == "rate_limit_exceeded": // OpenAI returns HTTP 429 for BOTH true rate limits and missing prepaid
// credits (insufficient_quota) — check quota BEFORE treating 429 as RPM.
case codeLower == "insufficient_quota" || typLower == "insufficient_quota" ||
strings.Contains(lower, "insufficient_quota") ||
strings.Contains(lower, "exceeded your current quota") ||
strings.Contains(lower, "check your plan and billing"):
return "API billing/quota empty — add prepaid API credits in Platform billing (not ChatGPT Plus)"
case codeLower == "rate_limit_exceeded" ||
strings.Contains(lower, "rate limit reached") ||
strings.Contains(lower, "rate_limit_exceeded"):
return "AI provider rate limited — retry later" return "AI provider rate limited — retry later"
case codeLower == "insufficient_quota" || strings.Contains(lower, "insufficient_quota") || case status == 429:
strings.Contains(lower, "exceeded your current quota"): // Bare 429 with no body fields — still often quota on new accounts.
return "OpenAI quota exceeded — check billing" return "API HTTP 429 — usually empty Platform billing/credits (not ChatGPT Plus); add prepaid credits then retry"
case strings.Contains(lower, "does not exist") || strings.Contains(lower, "do not have access") || case strings.Contains(lower, "does not exist") || strings.Contains(lower, "do not have access") ||
codeLower == "model_not_found": codeLower == "model_not_found":
return "model not found or API key lacks access — check model name and project permissions" return "model not found or API key lacks access — check model name and project permissions"
@@ -108,3 +118,23 @@ func ProbeCompleteOptions() CompleteOptions {
ReasoningEffort: "none", ReasoningEffort: "none",
} }
} }
// openAIErrorIsRetryableRateLimit is true only for genuine RPM/TPM limits.
// HTTP 429 + insufficient_quota (empty prepaid billing) must not be retried.
func openAIErrorIsRetryableRateLimit(errObj *openAIErrorBody) bool {
if errObj == nil {
return false
}
code := strings.ToLower(errObj.codeString())
typ := strings.ToLower(strings.TrimSpace(errObj.Type))
msg := strings.ToLower(strings.TrimSpace(errObj.Message))
if code == "insufficient_quota" || typ == "insufficient_quota" ||
strings.Contains(msg, "insufficient_quota") ||
strings.Contains(msg, "exceeded your current quota") ||
strings.Contains(msg, "check your plan and billing") {
return false
}
return code == "rate_limit_exceeded" ||
strings.Contains(msg, "rate limit reached") ||
strings.Contains(msg, "rate_limit_exceeded")
}
@@ -47,6 +47,23 @@ func TestFormatOpenAIHTTPError_preservesUsefulHints(t *testing.T) {
}, },
want: "model rejects legacy token cap", want: "model rejects legacy token cap",
}, },
{
status: 429,
err: &openAIErrorBody{
Message: "You exceeded your current quota, please check your plan and billing details.",
Type: "insufficient_quota",
Code: "insufficient_quota",
},
want: "API billing/quota empty",
},
{
status: 429,
err: &openAIErrorBody{
Message: "Rate limit reached for requests",
Code: "rate_limit_exceeded",
},
want: "AI provider rate limited",
},
{ {
status: 401, status: 401,
err: &openAIErrorBody{ err: &openAIErrorBody{
@@ -81,6 +98,19 @@ func TestFormatOpenAIHTTPError_preservesUsefulHints(t *testing.T) {
} }
} }
func TestOpenAIErrorIsRetryableRateLimit(t *testing.T) {
t.Parallel()
if openAIErrorIsRetryableRateLimit(&openAIErrorBody{Code: "insufficient_quota", Type: "insufficient_quota"}) {
t.Fatal("quota must not retry")
}
if !openAIErrorIsRetryableRateLimit(&openAIErrorBody{Code: "rate_limit_exceeded"}) {
t.Fatal("true rate limit should retry")
}
if openAIErrorIsRetryableRateLimit(nil) {
t.Fatal("nil 429 body must not auto-retry")
}
}
func TestProbeCompleteOptions(t *testing.T) { func TestProbeCompleteOptions(t *testing.T) {
t.Parallel() t.Parallel()
opts := ProbeCompleteOptions() opts := ProbeCompleteOptions()
+7 -2
View File
@@ -247,8 +247,13 @@ func classifyProviderError(msg string) string {
strings.Contains(lower, "rate limited —"), strings.Contains(lower, "rate limited —"),
strings.Contains(lower, "rate limited -"): strings.Contains(lower, "rate limited -"):
return "AI provider rate limited — retry later" return "AI provider rate limited — retry later"
case strings.Contains(lower, "quota exceeded"): case strings.Contains(lower, "quota exceeded"),
return "OpenAI quota exceeded — check billing" strings.Contains(lower, "insufficient_quota"),
strings.Contains(lower, "billing/quota empty"),
strings.Contains(lower, "prepaid api credits"),
strings.Contains(lower, "check your plan and billing"),
strings.Contains(lower, "http 429 — usually empty"):
return "API billing/quota empty — add prepaid API credits in Platform billing (not ChatGPT Plus)"
case strings.Contains(lower, "model not found"), case strings.Contains(lower, "model not found"),
strings.Contains(lower, "lacks access"): strings.Contains(lower, "lacks access"):
return "model not found or API key lacks access — check model name and project permissions" return "model not found or API key lacks access — check model name and project permissions"