forked from zendev-sh/goai
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
223 lines (199 loc) · 6.8 KB
/
errors.go
File metadata and controls
223 lines (199 loc) · 6.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package goai
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"slices"
)
// ContextOverflowError indicates the prompt exceeded the model's context window.
type ContextOverflowError struct {
Message string
ResponseBody string
}
func (e *ContextOverflowError) Error() string {
return e.Message
}
// APIError represents a non-overflow API error.
type APIError struct {
Message string
StatusCode int
IsRetryable bool
ResponseBody string
ResponseHeaders map[string]string
}
func (e *APIError) Error() string {
return e.Message
}
// Compiled overflow detection patterns -- ported from opencode's error.ts.
var overflowPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)prompt is too long`), // Anthropic
regexp.MustCompile(`(?i)input is too long for requested model`), // Amazon Bedrock
regexp.MustCompile(`(?i)exceeds the context window`), // OpenAI
regexp.MustCompile(`(?i)input token count.*exceeds the maximum`), // Google Gemini
regexp.MustCompile(`(?i)maximum prompt length is \d+`), // xAI (Grok)
regexp.MustCompile(`(?i)reduce the length of the messages`), // Groq
regexp.MustCompile(`(?i)maximum context length is \d+ tokens`), // OpenRouter, DeepSeek
regexp.MustCompile(`(?i)exceeds the limit of \d+`), // GitHub Copilot
regexp.MustCompile(`(?i)exceeds the available context size`), // llama.cpp server
regexp.MustCompile(`(?i)greater than the context length`), // LM Studio
regexp.MustCompile(`(?i)context window exceeds limit`), // MiniMax
regexp.MustCompile(`(?i)exceeded model token limit`), // Kimi, Moonshot
regexp.MustCompile(`(?i)context[_ ]length[_ ]exceeded`), // Generic fallback
regexp.MustCompile(`(?i)^4(00|13)\s*(status code)?\s*\(no body\)`), // Cerebras, Mistral
}
// IsOverflow checks if an error message indicates a context overflow.
func IsOverflow(message string) bool {
return slices.ContainsFunc(overflowPatterns, func(p *regexp.Regexp) bool {
return p.MatchString(message)
})
}
// StreamErrorType classifies parsed stream errors.
type StreamErrorType string
const (
StreamErrorContextOverflow StreamErrorType = "context_overflow"
StreamErrorAPI StreamErrorType = "api_error"
)
// ParsedStreamError represents a parsed error from an SSE stream.
type ParsedStreamError struct {
Type StreamErrorType
Message string
IsRetryable bool
ResponseBody string
}
// ParseStreamError parses a stream error event (used by Anthropic/OpenAI error events).
func ParseStreamError(body []byte) *ParsedStreamError {
var obj struct {
Type string `json:"type"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(body, &obj); err != nil {
return nil
}
if obj.Type != "error" {
return nil
}
switch obj.Error.Code {
case "context_length_exceeded":
return &ParsedStreamError{
Type: StreamErrorContextOverflow,
Message: "Input exceeds context window of this model",
ResponseBody: string(body),
}
case "insufficient_quota":
return &ParsedStreamError{
Type: StreamErrorAPI,
Message: "Quota exceeded. Check your plan and billing details.",
IsRetryable: false,
ResponseBody: string(body),
}
case "usage_not_included":
return &ParsedStreamError{
Type: StreamErrorAPI,
Message: "To use Codex with your ChatGPT plan, upgrade to Plus.",
IsRetryable: false,
ResponseBody: string(body),
}
case "invalid_prompt":
msg := "Invalid prompt."
if obj.Error.Message != "" {
msg = obj.Error.Message
}
return &ParsedStreamError{
Type: StreamErrorAPI,
Message: msg,
IsRetryable: false,
ResponseBody: string(body),
}
}
return nil
}
// ClassifyStreamError parses a stream error event and returns the appropriate
// typed error (*ContextOverflowError or *APIError), or nil if the data is not
// a recognized error event.
func ClassifyStreamError(body []byte) error {
parsed := ParseStreamError(body)
if parsed == nil {
return nil
}
if parsed.Type == StreamErrorContextOverflow {
return &ContextOverflowError{Message: parsed.Message, ResponseBody: parsed.ResponseBody}
}
return &APIError{Message: parsed.Message, IsRetryable: parsed.IsRetryable}
}
// ParseHTTPError classifies an HTTP error response.
func ParseHTTPError(providerID string, statusCode int, body []byte) error {
return ParseHTTPErrorWithHeaders(providerID, statusCode, body, nil)
}
// ParseHTTPErrorWithHeaders parses an HTTP error response, preserving retry-related headers.
func ParseHTTPErrorWithHeaders(providerID string, statusCode int, body []byte, headers http.Header) error {
message := extractErrorMessage(statusCode, body)
if IsOverflow(message) {
return &ContextOverflowError{
Message: message,
ResponseBody: string(body),
}
}
isRetryable := statusCode == http.StatusTooManyRequests ||
statusCode == http.StatusServiceUnavailable ||
statusCode >= 500
// OpenAI sometimes returns 404 for models that are actually available.
if providerID == "openai" && statusCode == http.StatusNotFound {
isRetryable = true
}
// Extract retry-related headers for backoff logic.
var respHeaders map[string]string
if headers != nil {
for _, key := range []string{"retry-after", "retry-after-ms"} {
if v := headers.Get(key); v != "" {
if respHeaders == nil {
respHeaders = make(map[string]string)
}
respHeaders[key] = v
}
}
}
return &APIError{
Message: message,
StatusCode: statusCode,
IsRetryable: isRetryable,
ResponseBody: string(body),
ResponseHeaders: respHeaders,
}
}
// extractErrorMessage tries to extract a human-readable error message from an API response.
// Handles both Chat Completions format ({error: {message}}) and
// Responses API format ({message, code, type}).
func extractErrorMessage(statusCode int, body []byte) string {
if len(body) == 0 {
return fmt.Sprintf("%d (no body)", statusCode)
}
var obj map[string]any
if err := json.Unmarshal(body, &obj); err == nil {
// Try error.message (Chat Completions format: Anthropic, OpenAI)
if errObj, ok := obj["error"].(map[string]any); ok {
if msg, ok := errObj["message"].(string); ok && msg != "" {
return msg
}
}
// Try top-level message (Responses API error format)
if msg, ok := obj["message"].(string); ok && msg != "" {
return msg
}
// Try error as string
if msg, ok := obj["error"].(string); ok && msg != "" {
return msg
}
}
// Fallback: include truncated body for debugging.
// body is non-empty here (len(body)==0 returned early above).
statusText := http.StatusText(statusCode)
bodyStr := string(body)
if len(bodyStr) > 200 {
bodyStr = bodyStr[:200] + "..."
}
return fmt.Sprintf("%d %s: %s", statusCode, statusText, bodyStr)
}