forked from di-sukharev/opencommit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathengineErrorHandler.ts
More file actions
208 lines (183 loc) · 5.43 KB
/
Copy pathengineErrorHandler.ts
File metadata and controls
208 lines (183 loc) · 5.43 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
import axios from 'axios';
import {
AuthenticationError,
InsufficientCreditsError,
ModelNotFoundError,
RateLimitError,
ServiceUnavailableError
} from './errors';
/**
* Extracts HTTP status code from various error types
*/
function getStatusCode(error: unknown): number | null {
// Direct status property (common in API SDKs)
if (typeof (error as any)?.status === 'number') {
return (error as any).status;
}
// Axios-style errors
if (axios.isAxiosError(error)) {
return error.response?.status ?? null;
}
// Response object with status
if (typeof (error as any)?.response?.status === 'number') {
return (error as any).response.status;
}
return null;
}
/**
* Extracts retry-after value from error headers (for rate limiting)
*/
function getRetryAfter(error: unknown): number | undefined {
const headers = (error as any)?.response?.headers;
if (headers) {
const retryAfter = headers['retry-after'] || headers['Retry-After'];
if (retryAfter) {
const seconds = parseInt(retryAfter, 10);
if (!isNaN(seconds)) {
return seconds;
}
}
}
return undefined;
}
/**
* Extracts the error message from various error structures
*/
function extractErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
// API error response structures
const apiError = (error as any)?.response?.data?.error;
if (apiError) {
if (typeof apiError === 'string') {
return apiError;
}
if (apiError.message) {
return apiError.message;
}
}
// Direct error data
const errorData = (error as any)?.error;
if (errorData) {
if (typeof errorData === 'string') {
return errorData;
}
if (errorData.message) {
return errorData.message;
}
}
// Fallback
if (typeof error === 'string') {
return error;
}
return 'An unknown error occurred';
}
/**
* Checks if the error message indicates a model not found error
*/
function isModelNotFoundMessage(message: string): boolean {
const lowerMessage = message.toLowerCase();
return (
(lowerMessage.includes('model') &&
(lowerMessage.includes('not found') ||
lowerMessage.includes('does not exist') ||
lowerMessage.includes('pull'))) ||
lowerMessage.includes('does_not_exist')
);
}
/**
* Checks if the error message indicates insufficient credits
*/
function isInsufficientCreditsMessage(message: string): boolean {
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes('insufficient') ||
lowerMessage.includes('credit') ||
lowerMessage.includes('quota') ||
lowerMessage.includes('balance too low') ||
lowerMessage.includes('billing') ||
lowerMessage.includes('payment required') ||
lowerMessage.includes('exceeded')
);
}
/**
* Normalizes raw API errors into typed error classes.
* This provides consistent error handling across all engine implementations.
*
* @param error - The raw error from the API call
* @param provider - The AI provider name (e.g., 'openai', 'anthropic')
* @param model - The model being used
* @returns A typed Error instance
*/
export function normalizeEngineError(
error: unknown,
provider: string,
model: string
): Error {
// If it's already one of our custom errors, return as-is
if (
error instanceof ModelNotFoundError ||
error instanceof AuthenticationError ||
error instanceof InsufficientCreditsError ||
error instanceof RateLimitError ||
error instanceof ServiceUnavailableError
) {
return error;
}
const statusCode = getStatusCode(error);
const message = extractErrorMessage(error);
// Handle based on HTTP status codes
switch (statusCode) {
case 401:
return new AuthenticationError(provider, message);
case 402:
return new InsufficientCreditsError(provider, message);
case 404:
// Could be model not found or endpoint not found
if (isModelNotFoundMessage(message)) {
return new ModelNotFoundError(model, provider, 404);
}
// Return generic error for other 404s
return error instanceof Error ? error : new Error(message);
case 429:
const retryAfter = getRetryAfter(error);
return new RateLimitError(provider, retryAfter, message);
case 500:
case 502:
case 503:
case 504:
return new ServiceUnavailableError(provider, statusCode, message);
}
// Handle based on error message content
if (statusCode === 400) {
return error instanceof Error ? error : new Error(message);
}
if (isModelNotFoundMessage(message)) {
return new ModelNotFoundError(model, provider, 404);
}
if (isInsufficientCreditsMessage(message)) {
return new InsufficientCreditsError(provider, message);
}
// Check for rate limit patterns in message
const lowerMessage = message.toLowerCase();
if (
lowerMessage.includes('rate limit') ||
lowerMessage.includes('rate_limit') ||
lowerMessage.includes('too many requests')
) {
return new RateLimitError(provider, undefined, message);
}
// Check for auth patterns in message
if (
lowerMessage.includes('unauthorized') ||
lowerMessage.includes('api key') ||
lowerMessage.includes('apikey') ||
lowerMessage.includes('authentication') ||
lowerMessage.includes('invalid_api_key')
) {
return new AuthenticationError(provider, message);
}
// Return original error or wrap in Error if needed
return error instanceof Error ? error : new Error(message);
}