fix
This commit is contained in:
@@ -507,7 +507,9 @@ func (c *OpenAIClient) doComplete(ctx context.Context, system, user string, temp
|
||||
msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error)
|
||||
httpErr := errors.New(msg)
|
||||
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 {
|
||||
msg := formatOpenAIHTTPError(res.StatusCode, parsed.Error)
|
||||
|
||||
@@ -50,6 +50,7 @@ func formatOpenAIHTTPError(status int, errObj *openAIErrorBody) string {
|
||||
lower := strings.ToLower(msg)
|
||||
codeLower := strings.ToLower(code)
|
||||
paramLower := strings.ToLower(param)
|
||||
typLower := strings.ToLower(typ)
|
||||
|
||||
switch {
|
||||
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"
|
||||
case status == 403:
|
||||
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"
|
||||
case codeLower == "insufficient_quota" || strings.Contains(lower, "insufficient_quota") ||
|
||||
strings.Contains(lower, "exceeded your current quota"):
|
||||
return "OpenAI quota exceeded — check billing"
|
||||
case status == 429:
|
||||
// Bare 429 with no body fields — still often quota on new accounts.
|
||||
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") ||
|
||||
codeLower == "model_not_found":
|
||||
return "model not found or API key lacks access — check model name and project permissions"
|
||||
@@ -108,3 +118,23 @@ func ProbeCompleteOptions() CompleteOptions {
|
||||
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",
|
||||
},
|
||||
{
|
||||
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,
|
||||
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) {
|
||||
t.Parallel()
|
||||
opts := ProbeCompleteOptions()
|
||||
|
||||
@@ -247,8 +247,13 @@ func classifyProviderError(msg string) string {
|
||||
strings.Contains(lower, "rate limited —"),
|
||||
strings.Contains(lower, "rate limited -"):
|
||||
return "AI provider rate limited — retry later"
|
||||
case strings.Contains(lower, "quota exceeded"):
|
||||
return "OpenAI quota exceeded — check billing"
|
||||
case strings.Contains(lower, "quota exceeded"),
|
||||
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"),
|
||||
strings.Contains(lower, "lacks access"):
|
||||
return "model not found or API key lacks access — check model name and project permissions"
|
||||
|
||||
Reference in New Issue
Block a user