@outputai/llm 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.1070949.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/prompt/loader.js +5 -1
- package/src/utils/error_handler.js +26 -106
- package/src/utils/error_handler.spec.js +11 -91
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/llm",
|
|
3
|
-
"version": "0.10.1-
|
|
3
|
+
"version": "0.10.1-next.1070949.0",
|
|
4
4
|
"description": "Framework abstraction to interact with LLM models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"entities": "8.0.0",
|
|
16
16
|
"gray-matter": "4.0.3",
|
|
17
|
-
"liquidjs": "10.
|
|
18
|
-
"undici": "8.
|
|
19
|
-
"@outputai/core": "0.10.1-
|
|
17
|
+
"liquidjs": "10.27.2",
|
|
18
|
+
"undici": "8.9.0",
|
|
19
|
+
"@outputai/core": "0.10.1-next.1070949.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@ai-sdk/amazon-bedrock": "4.0.
|
|
22
|
+
"@ai-sdk/amazon-bedrock": "4.0.138",
|
|
23
23
|
"@ai-sdk/anthropic": "3.0.81",
|
|
24
24
|
"@ai-sdk/azure": "3.0.68",
|
|
25
25
|
"@ai-sdk/google-vertex": "4.0.140",
|
package/src/prompt/loader.js
CHANGED
|
@@ -5,7 +5,11 @@ import { validatePrompt } from './validations.js';
|
|
|
5
5
|
import { FatalError } from '@outputai/core';
|
|
6
6
|
import { escape, decode, setupLiquidEncodeFilter } from './escape.js';
|
|
7
7
|
|
|
8
|
-
const liquid = new Liquid(
|
|
8
|
+
const liquid = new Liquid( {
|
|
9
|
+
strictFilters: true,
|
|
10
|
+
strictVariables: true,
|
|
11
|
+
lenientIf: true
|
|
12
|
+
} );
|
|
9
13
|
setupLiquidEncodeFilter( liquid );
|
|
10
14
|
|
|
11
15
|
/** Uses LiquidJS to interpolate variables in the prompt file content. */
|
|
@@ -1,56 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
NoSuchProviderError,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// AI SDK does not expose a dedicated schema-mismatch discriminator for NoObjectGeneratedError.
|
|
17
|
-
const NO_OBJECT_SCHEMA_MISMATCH_MESSAGE = 'No object generated: response did not match schema.';
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Recursively search an error cause chain until finds an error which is instance of given prototype.
|
|
21
|
-
*
|
|
22
|
-
* @param {object} error - Error instance.
|
|
23
|
-
* @param {Function|string} _class - Target constructor or constructor name.
|
|
24
|
-
* @param {number} depth - Current depth, search up to 10 causes deep.
|
|
25
|
-
* @returns {object|null} - Error or null if not found.
|
|
26
|
-
*/
|
|
27
|
-
export const findInstanceInCauseChain = ( error, _class, depth = 0 ) => {
|
|
28
|
-
if ( !error || typeof error !== 'object' ) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
if ( typeof _class === 'string' && error.constructor?.name === _class ) {
|
|
32
|
-
return error;
|
|
33
|
-
}
|
|
34
|
-
if ( typeof _class === 'function' && error instanceof _class ) {
|
|
35
|
-
return error;
|
|
36
|
-
}
|
|
37
|
-
if ( depth >= 10 ) {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
return error.cause ? findInstanceInCauseChain( error.cause, _class, depth + 1 ) : null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const toFatalError = ( error, extraMessage = '' ) => new FatalError(
|
|
44
|
-
`AI-SDK fatal error${extraMessage ? ` (${extraMessage})` : ''}: ${error.message}`,
|
|
45
|
-
{ cause: error }
|
|
46
|
-
);
|
|
1
|
+
import * as ai from 'ai';
|
|
2
|
+
import { FatalError, TransparentFatalError } from '@outputai/core';
|
|
3
|
+
|
|
4
|
+
const nonRetryableAiSdkErrorTypes = [
|
|
5
|
+
ai.InvalidArgumentError, // Invalid call settings are deterministic caller bugs, so retrying the same activity cannot fix them.
|
|
6
|
+
ai.InvalidDataContentError, // Invalid media content has the wrong local shape/encoding and will fail again with the same input.
|
|
7
|
+
ai.InvalidPromptError, // Invalid prompt structure is a deterministic request-construction error.
|
|
8
|
+
ai.LoadAPIKeyError, // Missing or invalid API key configuration will not change during an activity retry.
|
|
9
|
+
ai.LoadSettingError, // Missing or invalid provider settings are deployment/configuration problems.
|
|
10
|
+
ai.NoImageGeneratedError, // Image generation completed provider calls but collected zero images; repeating identical input is not useful.
|
|
11
|
+
ai.NoSuchProviderError, // A missing provider id is a deterministic provider registry/configuration error.
|
|
12
|
+
ai.NoSuchModelError, // A missing model id is a deterministic provider/model configuration error.
|
|
13
|
+
ai.UnsupportedFunctionalityError // The selected model/output mode does not support the requested feature.
|
|
14
|
+
];
|
|
47
15
|
|
|
48
16
|
/**
|
|
49
|
-
*
|
|
17
|
+
* Maps an AI SDK error to a framework error:
|
|
18
|
+
* - AI SDK Unrecoverable error types become TransparentFatalErrors;
|
|
19
|
+
* - AI SDK API error with isRetryable=false become TransparentFatalErrors;
|
|
20
|
+
* Some errors are not mapped:
|
|
21
|
+
* - Grammar which technically are isRetryable=false, will be rethrown, because they are actually transient;
|
|
22
|
+
* - Other errors are rethrown as well;
|
|
50
23
|
*
|
|
51
|
-
* - AI SDK Unrecoverable errors become FatalErrors, check code to see options.
|
|
52
|
-
* - NoObjectGeneratedError from invalid schema are reinitialized with a better message.
|
|
53
|
-
* - Other errors are preserved.
|
|
54
24
|
* @param {object} error - Original Error
|
|
55
25
|
* @returns {object} A new Error
|
|
56
26
|
*/
|
|
@@ -59,25 +29,7 @@ export const mapAiError = error => {
|
|
|
59
29
|
return error;
|
|
60
30
|
}
|
|
61
31
|
|
|
62
|
-
|
|
63
|
-
// This re-creates the error with a better message, making it easier to debug.
|
|
64
|
-
if ( NoObjectGeneratedError.isInstance( error ) && error.message.includes( NO_OBJECT_SCHEMA_MISMATCH_MESSAGE ) ) {
|
|
65
|
-
const zodError = findInstanceInCauseChain( error, 'ZodError' );
|
|
66
|
-
if ( zodError && zodError.issues?.length > 0 ) {
|
|
67
|
-
const [ { path, message } ] = zodError.issues;
|
|
68
|
-
return new NoObjectGeneratedError( {
|
|
69
|
-
message: `${error.message} First issue is "${message}" at path [${path.join( ', ' )}].`,
|
|
70
|
-
cause: error.cause,
|
|
71
|
-
text: error.text,
|
|
72
|
-
response: error.response,
|
|
73
|
-
usage: error.usage,
|
|
74
|
-
finishReason: error.finishReason
|
|
75
|
-
} );
|
|
76
|
-
}
|
|
77
|
-
return error;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const isApiError = APICallError.isInstance( error );
|
|
32
|
+
const isApiError = ai.APICallError.isInstance( error );
|
|
81
33
|
const isGrammarCompilationError = error.message === 'Grammar compilation timed out.';
|
|
82
34
|
|
|
83
35
|
// This error is actually transient, so instead of FatalError, return it
|
|
@@ -85,45 +37,13 @@ export const mapAiError = error => {
|
|
|
85
37
|
return error;
|
|
86
38
|
}
|
|
87
39
|
|
|
40
|
+
// Non-retryable API failures are already classified by AI SDK as permanent provider failures.
|
|
88
41
|
if ( isApiError && !error.isRetryable ) {
|
|
89
|
-
|
|
90
|
-
return toFatalError( error, error.statusCode ? `HTTP ${error.statusCode}` : '' );
|
|
91
|
-
}
|
|
92
|
-
if ( InvalidArgumentError.isInstance( error ) ) {
|
|
93
|
-
// Invalid call settings are deterministic caller bugs, so retrying the same activity cannot fix them.
|
|
94
|
-
return toFatalError( error );
|
|
95
|
-
}
|
|
96
|
-
if ( InvalidDataContentError.isInstance( error ) ) {
|
|
97
|
-
// Invalid media content has the wrong local shape/encoding and will fail again with the same input.
|
|
98
|
-
return toFatalError( error );
|
|
99
|
-
}
|
|
100
|
-
if ( InvalidPromptError.isInstance( error ) ) {
|
|
101
|
-
// Invalid prompt structure is a deterministic request-construction error.
|
|
102
|
-
return toFatalError( error );
|
|
42
|
+
return new TransparentFatalError( error );
|
|
103
43
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
if ( LoadSettingError.isInstance( error ) ) {
|
|
109
|
-
// Missing or invalid provider settings are deployment/configuration problems.
|
|
110
|
-
return toFatalError( error );
|
|
111
|
-
}
|
|
112
|
-
if ( NoImageGeneratedError.isInstance( error ) ) {
|
|
113
|
-
// Image generation completed provider calls but collected zero images; repeating identical input is not useful.
|
|
114
|
-
return toFatalError( error );
|
|
115
|
-
}
|
|
116
|
-
if ( NoSuchProviderError.isInstance( error ) ) {
|
|
117
|
-
// A missing provider id is a deterministic provider registry/configuration error.
|
|
118
|
-
return toFatalError( error );
|
|
119
|
-
}
|
|
120
|
-
if ( NoSuchModelError.isInstance( error ) ) {
|
|
121
|
-
// A missing model id is a deterministic provider/model configuration error.
|
|
122
|
-
return toFatalError( error );
|
|
123
|
-
}
|
|
124
|
-
if ( UnsupportedFunctionalityError.isInstance( error ) ) {
|
|
125
|
-
// The selected model/output mode does not support the requested feature.
|
|
126
|
-
return toFatalError( error );
|
|
44
|
+
|
|
45
|
+
if ( nonRetryableAiSdkErrorTypes.some( E => E.isInstance( error ) ) ) {
|
|
46
|
+
return new TransparentFatalError( error );
|
|
127
47
|
}
|
|
128
48
|
return error;
|
|
129
49
|
};
|
|
@@ -19,8 +19,8 @@ import {
|
|
|
19
19
|
ToolCallRepairError,
|
|
20
20
|
UnsupportedFunctionalityError
|
|
21
21
|
} from 'ai';
|
|
22
|
-
import { FatalError } from '@outputai/core';
|
|
23
|
-
import {
|
|
22
|
+
import { FatalError, TransparentFatalError } from '@outputai/core';
|
|
23
|
+
import { mapAiError } from './error_handler.js';
|
|
24
24
|
|
|
25
25
|
const makeApiCallError = ( input = {} ) => new APICallError( {
|
|
26
26
|
message: 'Provider rejected the request',
|
|
@@ -130,63 +130,6 @@ const preservedAiSdkErrors = [
|
|
|
130
130
|
]
|
|
131
131
|
];
|
|
132
132
|
|
|
133
|
-
describe( 'findInstanceInCauseChain', () => {
|
|
134
|
-
class FirstCustomError extends Error {}
|
|
135
|
-
class SecondCustomError extends Error {}
|
|
136
|
-
|
|
137
|
-
it( 'returns the input error when it matches the target constructor', () => {
|
|
138
|
-
const error = new FirstCustomError( 'first' );
|
|
139
|
-
|
|
140
|
-
expect( findInstanceInCauseChain( error, FirstCustomError ) ).toBe( error );
|
|
141
|
-
} );
|
|
142
|
-
|
|
143
|
-
it( 'returns the input error when it matches the target constructor name', () => {
|
|
144
|
-
const error = new FirstCustomError( 'first' );
|
|
145
|
-
|
|
146
|
-
expect( findInstanceInCauseChain( error, 'FirstCustomError' ) ).toBe( error );
|
|
147
|
-
} );
|
|
148
|
-
|
|
149
|
-
it( 'walks the cause chain to find an error by constructor', () => {
|
|
150
|
-
const target = new SecondCustomError( 'second' );
|
|
151
|
-
const wrapper = new FirstCustomError( 'first', { cause: target } );
|
|
152
|
-
|
|
153
|
-
expect( findInstanceInCauseChain( wrapper, SecondCustomError ) ).toBe( target );
|
|
154
|
-
} );
|
|
155
|
-
|
|
156
|
-
it( 'walks the cause chain to find an error by constructor name', () => {
|
|
157
|
-
const target = new SecondCustomError( 'second' );
|
|
158
|
-
const wrapper = new FirstCustomError( 'first', { cause: target } );
|
|
159
|
-
|
|
160
|
-
expect( findInstanceInCauseChain( wrapper, 'SecondCustomError' ) ).toBe( target );
|
|
161
|
-
} );
|
|
162
|
-
|
|
163
|
-
it( 'returns null when the target is not found', () => {
|
|
164
|
-
const error = new FirstCustomError( 'first', { cause: new Error( 'root' ) } );
|
|
165
|
-
|
|
166
|
-
expect( findInstanceInCauseChain( error, SecondCustomError ) ).toBeNull();
|
|
167
|
-
} );
|
|
168
|
-
|
|
169
|
-
it( 'returns null for empty or non-object inputs', () => {
|
|
170
|
-
expect( findInstanceInCauseChain( null, Error ) ).toBeNull();
|
|
171
|
-
expect( findInstanceInCauseChain( 'not an error', Error ) ).toBeNull();
|
|
172
|
-
} );
|
|
173
|
-
|
|
174
|
-
it( 'returns null for object causes without constructors', () => {
|
|
175
|
-
const cause = Object.create( null );
|
|
176
|
-
const error = new FirstCustomError( 'first', { cause } );
|
|
177
|
-
|
|
178
|
-
expect( findInstanceInCauseChain( error, 'SecondCustomError' ) ).toBeNull();
|
|
179
|
-
} );
|
|
180
|
-
|
|
181
|
-
it( 'stops searching after the depth limit', () => {
|
|
182
|
-
const makeErrorChain = depth => depth === 0 ?
|
|
183
|
-
new SecondCustomError( 'target' ) :
|
|
184
|
-
new FirstCustomError( `level ${depth}`, { cause: makeErrorChain( depth - 1 ) } );
|
|
185
|
-
|
|
186
|
-
expect( findInstanceInCauseChain( makeErrorChain( 11 ), SecondCustomError ) ).toBeNull();
|
|
187
|
-
} );
|
|
188
|
-
} );
|
|
189
|
-
|
|
190
133
|
describe( 'mapAiError', () => {
|
|
191
134
|
it( 'preserves existing FatalError instances', () => {
|
|
192
135
|
const error = new FatalError( 'Already fatal' );
|
|
@@ -194,7 +137,7 @@ describe( 'mapAiError', () => {
|
|
|
194
137
|
expect( mapAiError( error ) ).toBe( error );
|
|
195
138
|
} );
|
|
196
139
|
|
|
197
|
-
it( '
|
|
140
|
+
it( 'preserves NoObjectGeneratedError with its complete schema issue details', () => {
|
|
198
141
|
class ZodError extends Error {
|
|
199
142
|
constructor( issues ) {
|
|
200
143
|
super( 'schema failed' );
|
|
@@ -217,32 +160,12 @@ describe( 'mapAiError', () => {
|
|
|
217
160
|
cause: validationError
|
|
218
161
|
} );
|
|
219
162
|
|
|
220
|
-
const result = mapAiError( error );
|
|
221
|
-
|
|
222
|
-
expect( result ).not.toBe( error );
|
|
223
|
-
expect( NoObjectGeneratedError.isInstance( result ) ).toBe( true );
|
|
224
|
-
expect( result.name ).toBe( 'AI_NoObjectGeneratedError' );
|
|
225
|
-
expect( result.message ).toBe(
|
|
226
|
-
'No object generated: response did not match schema. First issue is "Expected string" at path [items, 0, title].'
|
|
227
|
-
);
|
|
228
|
-
expect( result.cause ).toBe( validationError );
|
|
229
|
-
expect( result.text ).toBe( error.text );
|
|
230
|
-
expect( result.response ).toBe( error.response );
|
|
231
|
-
expect( result.usage ).toBe( error.usage );
|
|
232
|
-
expect( result.finishReason ).toBe( error.finishReason );
|
|
233
|
-
} );
|
|
234
|
-
|
|
235
|
-
it( 'preserves NoObjectGeneratedError schema mismatches when no schema issue is available', () => {
|
|
236
|
-
const error = new NoObjectGeneratedError( {
|
|
237
|
-
message: 'No object generated: response did not match schema.',
|
|
238
|
-
text: '{"items":[{}]}',
|
|
239
|
-
cause: new Error( 'validation failed' )
|
|
240
|
-
} );
|
|
241
|
-
|
|
242
163
|
expect( mapAiError( error ) ).toBe( error );
|
|
164
|
+
expect( error.cause.cause ).toBe( zodError );
|
|
165
|
+
expect( error.cause.cause.issues ).toEqual( zodError.issues );
|
|
243
166
|
} );
|
|
244
167
|
|
|
245
|
-
it( 'maps non-retryable APICallError instances to
|
|
168
|
+
it( 'maps non-retryable APICallError instances to TransparentFatalError', () => {
|
|
246
169
|
const error = makeApiCallError( {
|
|
247
170
|
statusCode: 400,
|
|
248
171
|
isRetryable: false
|
|
@@ -250,20 +173,18 @@ describe( 'mapAiError', () => {
|
|
|
250
173
|
|
|
251
174
|
const result = mapAiError( error );
|
|
252
175
|
|
|
253
|
-
expect( result ).toBeInstanceOf(
|
|
254
|
-
expect( result.message ).toBe( 'AI-SDK fatal error (HTTP 400): Provider rejected the request' );
|
|
176
|
+
expect( result ).toBeInstanceOf( TransparentFatalError );
|
|
255
177
|
expect( result.cause ).toBe( error );
|
|
256
178
|
} );
|
|
257
179
|
|
|
258
|
-
it( 'maps non-retryable APICallError instances without status codes to
|
|
180
|
+
it( 'maps non-retryable APICallError instances without status codes to TransparentFatalError', () => {
|
|
259
181
|
const error = makeApiCallError( {
|
|
260
182
|
isRetryable: false
|
|
261
183
|
} );
|
|
262
184
|
|
|
263
185
|
const result = mapAiError( error );
|
|
264
186
|
|
|
265
|
-
expect( result ).toBeInstanceOf(
|
|
266
|
-
expect( result.message ).toBe( 'AI-SDK fatal error: Provider rejected the request' );
|
|
187
|
+
expect( result ).toBeInstanceOf( TransparentFatalError );
|
|
267
188
|
expect( result.cause ).toBe( error );
|
|
268
189
|
} );
|
|
269
190
|
|
|
@@ -286,13 +207,12 @@ describe( 'mapAiError', () => {
|
|
|
286
207
|
expect( mapAiError( error ) ).toBe( error );
|
|
287
208
|
} );
|
|
288
209
|
|
|
289
|
-
it.each( fatalAiSdkErrors )( 'maps %s to
|
|
210
|
+
it.each( fatalAiSdkErrors )( 'maps %s to TransparentFatalError', ( _name, makeError ) => {
|
|
290
211
|
const error = makeError();
|
|
291
212
|
|
|
292
213
|
const result = mapAiError( error );
|
|
293
214
|
|
|
294
|
-
expect( result ).toBeInstanceOf(
|
|
295
|
-
expect( result.message ).toBe( `AI-SDK fatal error: ${error.message}` );
|
|
215
|
+
expect( result ).toBeInstanceOf( TransparentFatalError );
|
|
296
216
|
expect( result.cause ).toBe( error );
|
|
297
217
|
} );
|
|
298
218
|
|