141 lines
5.3 KiB
Go
141 lines
5.3 KiB
Go
package processing
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// openAIErrorBody is the error object returned by OpenAI-compatible APIs.
|
|
type openAIErrorBody struct {
|
|
Message string `json:"message"`
|
|
Type string `json:"type"`
|
|
Code any `json:"code"` // string or number depending on provider
|
|
Param string `json:"param"`
|
|
}
|
|
|
|
func (e *openAIErrorBody) codeString() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
switch v := e.Code.(type) {
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case float64:
|
|
if v == float64(int64(v)) {
|
|
return fmt.Sprintf("%d", int64(v))
|
|
}
|
|
return fmt.Sprintf("%g", v)
|
|
case int:
|
|
return fmt.Sprintf("%d", v)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// formatOpenAIHTTPError turns an upstream HTTP error into a short, secret-safe
|
|
// operator message. Intentionally avoids embedding model ids (gpt-…) so
|
|
// TruncateError / mapPublicErrorCode do not collapse useful validation text to
|
|
// "processing_failed".
|
|
func formatOpenAIHTTPError(status int, errObj *openAIErrorBody) string {
|
|
msg := ""
|
|
code := ""
|
|
param := ""
|
|
typ := ""
|
|
if errObj != nil {
|
|
msg = strings.TrimSpace(errObj.Message)
|
|
code = errObj.codeString()
|
|
param = strings.TrimSpace(errObj.Param)
|
|
typ = strings.TrimSpace(errObj.Type)
|
|
}
|
|
lower := strings.ToLower(msg)
|
|
codeLower := strings.ToLower(code)
|
|
paramLower := strings.ToLower(param)
|
|
typLower := strings.ToLower(typ)
|
|
|
|
switch {
|
|
case status == 401 || codeLower == "invalid_api_key" ||
|
|
strings.Contains(lower, "invalid api key") ||
|
|
strings.Contains(lower, "incorrect api key"):
|
|
return "AI provider rejected the API key"
|
|
case status == 403:
|
|
return "AI provider forbidden the request"
|
|
// 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 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"
|
|
case strings.Contains(lower, "max_tokens") && strings.Contains(lower, "max_completion"):
|
|
return "model requires completion-token budget (legacy token cap unsupported)"
|
|
case paramLower == "temperature" || strings.Contains(lower, "temperature"):
|
|
return "model rejects custom temperature — omit temperature for reasoning chat models"
|
|
case paramLower == "max_tokens" || (strings.Contains(lower, "unsupported parameter") && strings.Contains(lower, "max_tokens")):
|
|
return "model rejects legacy token cap — use completion-token budget"
|
|
case paramLower == "reasoning_effort" || strings.Contains(lower, "reasoning_effort"):
|
|
return "model rejected reasoning effort — try none/low or omit"
|
|
case strings.Contains(lower, "unsupported parameter") || strings.Contains(lower, "unsupported value"):
|
|
if param != "" {
|
|
return fmt.Sprintf("unsupported chat parameter %q", param)
|
|
}
|
|
return "unsupported chat parameter for this model"
|
|
case status >= 500:
|
|
return "AI provider temporarily unavailable (server error)"
|
|
}
|
|
|
|
// Prefer structured fields over raw message (raw often embeds model ids / urls).
|
|
if code != "" && param != "" {
|
|
return fmt.Sprintf("provider http %d (%s param=%s)", status, code, param)
|
|
}
|
|
if code != "" {
|
|
return fmt.Sprintf("provider http %d (%s)", status, code)
|
|
}
|
|
if typ != "" {
|
|
return fmt.Sprintf("provider http %d (%s)", status, typ)
|
|
}
|
|
if status > 0 {
|
|
return fmt.Sprintf("provider http %d", status)
|
|
}
|
|
return "provider request failed"
|
|
}
|
|
|
|
// ProbeCompleteOptions is the Chat Completions budget for admin connection tests.
|
|
// Explicit max_completion_tokens + reasoning_effort=none avoids GPT-5.6 defaults
|
|
// spending the budget on hidden reasoning for a one-token "ok" reply.
|
|
func ProbeCompleteOptions() CompleteOptions {
|
|
return CompleteOptions{
|
|
MaxTokens: 64,
|
|
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")
|
|
}
|