Files
descrybe/apps/api/internal/processing/openai_api_error.go
T
2026-08-23 12:56:19 +02:00

111 lines
3.9 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)
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"
case status == 429 || codeLower == "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 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",
}
}