11import test from 'ava' ;
2+ import { parseAPIError } from './ai-sdk-client.js' ;
23
3- // Note: parseAPIError is an internal function in ai-sdk-client.ts that is not exported.
4- // For testing purposes, we replicate the logic here to verify it works correctly.
5-
6- /**
7- * Parses API errors into user-friendly messages
8- * This is a copy of the internal parseAPIError function for testing
9- */
10- function parseAPIErrorForTest ( error : unknown ) : string {
11- if ( ! ( error instanceof Error ) ) {
12- return 'An unknown error occurred while communicating with the model' ;
13- }
14-
15- const errorMessage = error . message ;
16-
17- // Handle Ollama-specific unmarshal/JSON parsing errors
18- if (
19- errorMessage . includes ( 'unmarshal' ) ||
20- ( errorMessage . includes ( 'invalid character' ) &&
21- errorMessage . includes ( 'after top-level value' ) )
22- ) {
23- return (
24- 'Ollama server error: The model returned malformed JSON. ' +
25- 'This usually indicates an issue with the Ollama server or model. ' +
26- 'Try:\n' +
27- ' 1. Restart Ollama: systemctl restart ollama (Linux) or restart the Ollama app\n' +
28- ' 2. Re-pull the model: ollama pull <model-name>\n' +
29- ' 3. Check Ollama logs for more details\n' +
30- ' 4. Try a different model to see if the issue is model-specific\n' +
31- `Original error: ${ errorMessage } `
32- ) ;
33- }
34-
35- // Extract status code and clean message from common error patterns
36- const statusMatch = errorMessage . match (
37- / (?: E r r o r : ) ? ( \d { 3 } ) \s + (?: \d { 3 } \s + ) ? (?: B a d R e q u e s t | [ ^ : ] + ) : \s * ( .+ ) / i,
38- ) ;
39- if ( statusMatch ) {
40- const [ , statusCode , message ] = statusMatch ;
41- const cleanMessage = message . trim ( ) ;
42-
43- switch ( statusCode ) {
44- case '400' :
45- return `Bad request: ${ cleanMessage } ` ;
46- case '401' :
47- return 'Authentication failed: Invalid API key or credentials' ;
48- case '403' :
49- return 'Access forbidden: Check your API permissions' ;
50- case '404' :
51- return 'Model not found: The requested model may not exist or is unavailable' ;
52- case '429' :
53- return 'Rate limit exceeded: Too many requests. Please wait and try again' ;
54- case '500' :
55- case '502' :
56- case '503' :
57- return `Server error: ${ cleanMessage } ` ;
58- default :
59- return `Request failed (${ statusCode } ): ${ cleanMessage } ` ;
60- }
61- }
62-
63- // Handle timeout errors
64- if ( errorMessage . includes ( 'timeout' ) || errorMessage . includes ( 'ETIMEDOUT' ) ) {
65- return 'Request timed out: The model took too long to respond' ;
66- }
67-
68- // Handle network errors
69- if (
70- errorMessage . includes ( 'ECONNREFUSED' ) ||
71- errorMessage . includes ( 'connect' )
72- ) {
73- return 'Connection failed: Unable to reach the model server' ;
74- }
75-
76- // Handle context length errors
77- if (
78- errorMessage . includes ( 'context length' ) ||
79- errorMessage . includes ( 'too many tokens' )
80- ) {
81- return 'Context too large: Please reduce the conversation length or message size' ;
82- }
83-
84- // Handle token limit errors
85- if ( errorMessage . includes ( 'reduce the number of tokens' ) ) {
86- return 'Too many tokens: Please shorten your message or clear conversation history' ;
87- }
88-
89- // If we can't parse it, return a cleaned up version
90- return errorMessage . replace ( / ^ E r r o r : \s * / i, '' ) . split ( '\n' ) [ 0 ] ;
91- }
4+ // Tests for parseAPIError function
5+ // Now using the actual exported function instead of a duplicated copy
926
937test ( 'parseAPIError - handles Ollama unmarshal error from issue #87' , t => {
948 const error = new Error (
959 "RetryError [AI_RetryError]: Failed after 3 attempts. Last error: unmarshal: invalid character '{' after top-level value" ,
9610 ) ;
9711
98- const result = parseAPIErrorForTest ( error ) ;
12+ const result = parseAPIError ( error ) ;
9913
10014 t . true ( result . includes ( 'Ollama server error' ) ) ;
10115 t . true ( result . includes ( 'malformed JSON' ) ) ;
@@ -109,35 +23,38 @@ test('parseAPIError - handles Ollama unmarshal error from issue #87', t => {
10923test ( 'parseAPIError - handles unmarshal error without retry wrapper' , t => {
11024 const error = new Error ( "unmarshal: invalid character '{' after top-level value" ) ;
11125
112- const result = parseAPIErrorForTest ( error ) ;
26+ const result = parseAPIError ( error ) ;
11327
11428 t . true ( result . includes ( 'Ollama server error' ) ) ;
11529 t . true ( result . includes ( 'malformed JSON' ) ) ;
11630} ) ;
11731
118- test ( 'parseAPIError - handles invalid character error' , t => {
32+ test ( 'parseAPIError - handles 500 error with invalid character (status code takes precedence)' , t => {
33+ // This test verifies that HTTP status codes are parsed FIRST,
34+ // so a 500 error with "invalid character" in the message is treated
35+ // as a server error, not an Ollama-specific error
11936 const error = new Error (
12037 "500 Internal Server Error: invalid character 'x' after top-level value" ,
12138 ) ;
12239
123- const result = parseAPIErrorForTest ( error ) ;
40+ const result = parseAPIError ( error ) ;
12441
125- t . true ( result . includes ( ' Ollama server error' ) ) ;
126- t . true ( result . includes ( 'malformed JSON' ) ) ;
42+ // Status code parsing takes precedence over Ollama-specific pattern matching
43+ t . is ( result , "Server error: invalid character 'x' after top-level value" ) ;
12744} ) ;
12845
12946test ( 'parseAPIError - handles 500 error without JSON parsing issue' , t => {
13047 const error = new Error ( '500 Internal Server Error: database connection failed' ) ;
13148
132- const result = parseAPIErrorForTest ( error ) ;
49+ const result = parseAPIError ( error ) ;
13350
13451 t . is ( result , 'Server error: database connection failed' ) ;
13552} ) ;
13653
13754test ( 'parseAPIError - handles 404 error' , t => {
13855 const error = new Error ( '404 Not Found: model not available' ) ;
13956
140- const result = parseAPIErrorForTest ( error ) ;
57+ const result = parseAPIError ( error ) ;
14158
14259 t . is (
14360 result ,
@@ -148,45 +65,110 @@ test('parseAPIError - handles 404 error', t => {
14865test ( 'parseAPIError - handles connection refused' , t => {
14966 const error = new Error ( 'ECONNREFUSED: Connection refused' ) ;
15067
151- const result = parseAPIErrorForTest ( error ) ;
68+ const result = parseAPIError ( error ) ;
15269
15370 t . is ( result , 'Connection failed: Unable to reach the model server' ) ;
15471} ) ;
15572
15673test ( 'parseAPIError - handles timeout error' , t => {
15774 const error = new Error ( 'Request timeout: ETIMEDOUT' ) ;
15875
159- const result = parseAPIErrorForTest ( error ) ;
76+ const result = parseAPIError ( error ) ;
16077
16178 t . is ( result , 'Request timed out: The model took too long to respond' ) ;
16279} ) ;
16380
16481test ( 'parseAPIError - handles non-Error objects' , t => {
165- const result = parseAPIErrorForTest ( 'string error' ) ;
82+ const result = parseAPIError ( 'string error' ) ;
16683
16784 t . is ( result , 'An unknown error occurred while communicating with the model' ) ;
16885} ) ;
16986
17087test ( 'parseAPIError - handles context length errors' , t => {
17188 const error = new Error (
172- 'context length exceeded, please reduce the number of tokens ' ,
89+ 'context length exceeded' ,
17390 ) ;
17491
175- const result = parseAPIErrorForTest ( error ) ;
92+ const result = parseAPIError ( error ) ;
93+
94+ // Use exact assertion instead of OR condition
95+ t . is ( result , 'Context too large: Please reduce the conversation length or message size' ) ;
96+ } ) ;
17697
177- t . true (
178- result . includes ( 'Context too large' ) ||
179- result . includes ( 'Too many tokens' ) ,
98+ test ( 'parseAPIError - handles too many tokens errors' , t => {
99+ const error = new Error (
100+ 'too many tokens in the request' ,
180101 ) ;
102+
103+ const result = parseAPIError ( error ) ;
104+
105+ t . is ( result , 'Context too large: Please reduce the conversation length or message size' ) ;
181106} ) ;
182107
183108test ( 'parseAPIError - handles 400 with context length in message' , t => {
184109 const error = new Error (
185110 '400 Bad Request: context length exceeded' ,
186111 ) ;
187112
188- const result = parseAPIErrorForTest ( error ) ;
113+ const result = parseAPIError ( error ) ;
189114
190115 // The 400 status code pattern matches first, so we get the full message
191116 t . is ( result , 'Bad request: context length exceeded' ) ;
192117} ) ;
118+
119+ test ( 'parseAPIError - handles 401 authentication error' , t => {
120+ const error = new Error ( '401 Unauthorized: Invalid API key' ) ;
121+
122+ const result = parseAPIError ( error ) ;
123+
124+ t . is ( result , 'Authentication failed: Invalid API key or credentials' ) ;
125+ } ) ;
126+
127+ test ( 'parseAPIError - handles 403 forbidden error' , t => {
128+ const error = new Error ( '403 Forbidden: Access denied' ) ;
129+
130+ const result = parseAPIError ( error ) ;
131+
132+ t . is ( result , 'Access forbidden: Check your API permissions' ) ;
133+ } ) ;
134+
135+ test ( 'parseAPIError - handles 429 rate limit error' , t => {
136+ const error = new Error ( '429 Too Many Requests: Rate limit exceeded' ) ;
137+
138+ const result = parseAPIError ( error ) ;
139+
140+ t . is ( result , 'Rate limit exceeded: Too many requests. Please wait and try again' ) ;
141+ } ) ;
142+
143+ test ( 'parseAPIError - handles 502 bad gateway error' , t => {
144+ const error = new Error ( '502 Bad Gateway: upstream error' ) ;
145+
146+ const result = parseAPIError ( error ) ;
147+
148+ t . is ( result , 'Server error: upstream error' ) ;
149+ } ) ;
150+
151+ test ( 'parseAPIError - handles 503 service unavailable error' , t => {
152+ const error = new Error ( '503 Service Unavailable: server overloaded' ) ;
153+
154+ const result = parseAPIError ( error ) ;
155+
156+ t . is ( result , 'Server error: server overloaded' ) ;
157+ } ) ;
158+
159+ test ( 'parseAPIError - handles reduce tokens message' , t => {
160+ const error = new Error ( 'Please reduce the number of tokens in your request' ) ;
161+
162+ const result = parseAPIError ( error ) ;
163+
164+ t . is ( result , 'Too many tokens: Please shorten your message or clear conversation history' ) ;
165+ } ) ;
166+
167+ test ( 'parseAPIError - cleans up unknown errors' , t => {
168+ const error = new Error ( 'Error: Something unexpected happened\nWith more details' ) ;
169+
170+ const result = parseAPIError ( error ) ;
171+
172+ // Should strip "Error: " prefix and only return first line
173+ t . is ( result , 'Something unexpected happened' ) ;
174+ } ) ;
0 commit comments