Files

141 lines
5.3 KiB
Go
Raw Permalink Normal View History

2026-08-23 12:56:19 +02:00
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)
2026-08-23 13:08:14 +02:00
typLower := strings.ToLower(typ)
2026-08-23 12:56:19 +02:00
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"
2026-08-23 13:08:14 +02:00
// 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"):
2026-08-23 12:56:19 +02:00
return "AI provider rate limited — retry later"
2026-08-23 13:08:14 +02:00
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"
2026-08-23 12:56:19 +02:00
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",
}
}
2026-08-23 13:08:14 +02:00
// 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")
}