@outputai/llm 0.6.1-next.fc6a93e.0 → 0.7.1-dev.144d64f.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 +20 -12
- package/src/agent.js +5 -4
- package/src/agent.spec.js +62 -19
- package/src/ai_model.js +25 -146
- package/src/ai_model.spec.js +185 -762
- package/src/ai_provider.js +86 -0
- package/src/ai_provider.spec.js +147 -0
- package/src/ai_sdk_options.js +2 -1
- package/src/ai_sdk_options.spec.js +28 -0
- package/src/cost/index.js +7 -2
- package/src/cost/index.spec.js +5 -2
- package/src/index.d.ts +64 -16
- package/src/index.js +1 -4
- package/src/prompt/block_options.js +58 -0
- package/src/prompt/block_options.spec.js +71 -0
- package/src/prompt/blocks.js +47 -0
- package/src/prompt/blocks.spec.js +63 -0
- package/src/prompt/parser.js +2 -5
- package/src/prompt/parser.spec.js +19 -0
- package/src/prompt/validations.js +12 -3
- package/src/prompt/validations.spec.js +114 -0
- package/src/utils/error_handler.js +25 -8
- package/src/utils/error_handler.spec.js +17 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Roles that introduce a message block. Add a role here to support a new
|
|
3
|
+
* `<role>...</role>` block — the tokenizer pattern is derived from this set,
|
|
4
|
+
* so no other parser change is required.
|
|
5
|
+
*/
|
|
6
|
+
export const BLOCK_ROLES = new Set( [ 'system', 'user', 'assistant', 'tool' ] );
|
|
7
|
+
|
|
8
|
+
const BLOCK_PATTERN = new RegExp(
|
|
9
|
+
`<(${[ ...BLOCK_ROLES ].join( '|' )})((?:\\s[^>]*)?)>([\\s\\S]*?)<\\/\\1>`,
|
|
10
|
+
'gm'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const ATTRIBUTE_PATTERN = /([a-zA-Z][\w-]*)(?:=(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse a raw opening-tag attribute string into a plain object. Supports bare booleans
|
|
17
|
+
* (`cache`), double/single-quoted values, and unquoted values:
|
|
18
|
+
* `cache options="a b" ttl='1h'` → `{ cache: true, options: 'a b', ttl: '1h' }`.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} [raw] - Raw attribute text between the role and the closing `>`
|
|
21
|
+
* @returns {Record<string, string | true>} Parsed attributes
|
|
22
|
+
*/
|
|
23
|
+
export const parseAttributes = ( raw = '' ) =>
|
|
24
|
+
Object.fromEntries(
|
|
25
|
+
[ ...raw.matchAll( ATTRIBUTE_PATTERN ) ].map(
|
|
26
|
+
( [ _, key, doubleQuoted, singleQuoted, bare ] ) =>
|
|
27
|
+
[ key, doubleQuoted ?? singleQuoted ?? bare ?? true ]
|
|
28
|
+
)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Tokenize a rendered prompt body into message blocks. Each block is `{ role, content }`,
|
|
33
|
+
* plus `attributes` when the opening tag carried any. Content between role tags is treated
|
|
34
|
+
* as opaque text, so prompt bodies may freely contain other angle-bracket markup.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} content - Rendered prompt body (after frontmatter is stripped)
|
|
37
|
+
* @returns {Array<{ role: string, content: string, attributes?: Record<string, string | true> }>}
|
|
38
|
+
*/
|
|
39
|
+
export const tokenizeBlocks = content =>
|
|
40
|
+
[ ...content.matchAll( BLOCK_PATTERN ) ].map( ( [ _, role, rawAttributes, text ] ) => {
|
|
41
|
+
const attributes = parseAttributes( rawAttributes.trim() );
|
|
42
|
+
return {
|
|
43
|
+
role,
|
|
44
|
+
content: text.trim(),
|
|
45
|
+
...( Object.keys( attributes ).length > 0 && { attributes } )
|
|
46
|
+
};
|
|
47
|
+
} );
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseAttributes, tokenizeBlocks, BLOCK_ROLES } from './blocks.js';
|
|
3
|
+
|
|
4
|
+
describe( 'parseAttributes', () => {
|
|
5
|
+
it( 'parses a bare attribute as boolean true', () => {
|
|
6
|
+
expect( parseAttributes( 'pinned' ) ).toEqual( { pinned: true } );
|
|
7
|
+
} );
|
|
8
|
+
|
|
9
|
+
it( 'parses double- and single-quoted values', () => {
|
|
10
|
+
expect( parseAttributes( 'ttl="1h" mode=\'fast\'' ) ).toEqual( { ttl: '1h', mode: 'fast' } );
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
it( 'parses unquoted values', () => {
|
|
14
|
+
expect( parseAttributes( 'ttl=1h' ) ).toEqual( { ttl: '1h' } );
|
|
15
|
+
} );
|
|
16
|
+
|
|
17
|
+
it( 'parses multiple attributes and preserves spaces inside quotes', () => {
|
|
18
|
+
expect( parseAttributes( 'pinned options="cached fast"' ) ).toEqual( {
|
|
19
|
+
pinned: true,
|
|
20
|
+
options: 'cached fast'
|
|
21
|
+
} );
|
|
22
|
+
} );
|
|
23
|
+
|
|
24
|
+
it( 'returns an empty object for blank input', () => {
|
|
25
|
+
expect( parseAttributes( '' ) ).toEqual( {} );
|
|
26
|
+
expect( parseAttributes() ).toEqual( {} );
|
|
27
|
+
} );
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
describe( 'tokenizeBlocks', () => {
|
|
31
|
+
it( 'tokenizes plain blocks without an attributes key', () => {
|
|
32
|
+
const blocks = tokenizeBlocks( '<system>Hi</system>\n<user>Yo</user>' );
|
|
33
|
+
expect( blocks ).toEqual( [
|
|
34
|
+
{ role: 'system', content: 'Hi' },
|
|
35
|
+
{ role: 'user', content: 'Yo' }
|
|
36
|
+
] );
|
|
37
|
+
} );
|
|
38
|
+
|
|
39
|
+
it( 'attaches parsed attributes to the block', () => {
|
|
40
|
+
const blocks = tokenizeBlocks( '<system options="a b" pinned>Hi</system>' );
|
|
41
|
+
expect( blocks[0] ).toEqual( {
|
|
42
|
+
role: 'system',
|
|
43
|
+
content: 'Hi',
|
|
44
|
+
attributes: { options: 'a b', pinned: true }
|
|
45
|
+
} );
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
it( 'captures unknown attributes generically (validation rejects them later)', () => {
|
|
49
|
+
const blocks = tokenizeBlocks( '<user data="x">Hi</user>' );
|
|
50
|
+
expect( blocks[0].attributes ).toEqual( { data: 'x' } );
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
it( 'treats angle-bracket markup inside a block as opaque content', () => {
|
|
54
|
+
const blocks = tokenizeBlocks( '<user>Compare <div> and <span> tags</user>' );
|
|
55
|
+
expect( blocks[0] ).toEqual( { role: 'user', content: 'Compare <div> and <span> tags' } );
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
it( 'tokenizes every registered role', () => {
|
|
59
|
+
const body = [ ...BLOCK_ROLES ].map( role => `<${role}>${role} body</${role}>` ).join( '\n' );
|
|
60
|
+
const blocks = tokenizeBlocks( body );
|
|
61
|
+
expect( blocks.map( block => block.role ) ).toEqual( [ ...BLOCK_ROLES ] );
|
|
62
|
+
} );
|
|
63
|
+
} );
|
package/src/prompt/parser.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import matter from 'gray-matter';
|
|
2
2
|
import { FatalError } from '@outputai/core';
|
|
3
|
+
import { tokenizeBlocks } from './blocks.js';
|
|
3
4
|
|
|
4
5
|
export function parsePrompt( { name, raw } ) {
|
|
5
6
|
const { data: config, content } = matter( raw );
|
|
@@ -8,11 +9,7 @@ export function parsePrompt( { name, raw } ) {
|
|
|
8
9
|
throw new FatalError( `Prompt "${name}" has no content after frontmatter` );
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const
|
|
12
|
-
const messages = [ ...content.matchAll( infoExtractor ) ].map(
|
|
13
|
-
( [ _, role, text ] ) => ( { role, content: text.trim() } )
|
|
14
|
-
);
|
|
15
|
-
|
|
12
|
+
const messages = tokenizeBlocks( content );
|
|
16
13
|
const instructions = messages.length === 0 ? content.trim() : null;
|
|
17
14
|
|
|
18
15
|
return { config, messages, instructions };
|
|
@@ -164,4 +164,23 @@ model: claude-3-5-sonnet-20241022
|
|
|
164
164
|
] );
|
|
165
165
|
expect( result.instructions ).toBeNull();
|
|
166
166
|
} );
|
|
167
|
+
|
|
168
|
+
it( 'surfaces block opening-tag attributes as an attributes object', () => {
|
|
169
|
+
const raw = `---
|
|
170
|
+
provider: anthropic
|
|
171
|
+
model: claude-sonnet-4-5
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
<system options="cached">Static.</system>
|
|
175
|
+
<user>Question</user>`;
|
|
176
|
+
|
|
177
|
+
const result = parsePrompt( { name: 'test', raw } );
|
|
178
|
+
|
|
179
|
+
expect( result.messages[0] ).toEqual( {
|
|
180
|
+
role: 'system',
|
|
181
|
+
content: 'Static.',
|
|
182
|
+
attributes: { options: 'cached' }
|
|
183
|
+
} );
|
|
184
|
+
expect( result.messages[1] ).toEqual( { role: 'user', content: 'Question' } );
|
|
185
|
+
} );
|
|
167
186
|
} );
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { ValidationError, z } from '@outputai/core';
|
|
2
|
+
import { attributesSchema } from './block_options.js';
|
|
3
|
+
|
|
4
|
+
const toolConfigSchema = z.record( z.string(), z.unknown() );
|
|
5
|
+
const toolsConfigSchema = z.record( z.string(), toolConfigSchema );
|
|
6
|
+
|
|
7
|
+
// A provider-namespaced options object, e.g. { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
|
8
|
+
const providerOptionsSchema = z.record( z.string(), z.record( z.string(), z.unknown() ) );
|
|
2
9
|
|
|
3
10
|
export const promptSchema = z.object( {
|
|
4
11
|
name: z.string(),
|
|
@@ -13,18 +20,20 @@ export const promptSchema = z.object( {
|
|
|
13
20
|
aspectRatio: z.string().regex( /^\d+:\d+$/ ).optional(),
|
|
14
21
|
seed: z.number().int().optional(),
|
|
15
22
|
skills: z.union( [ z.string().min( 1 ), z.array( z.string().min( 1 ) ) ] ).optional(),
|
|
16
|
-
tools:
|
|
23
|
+
tools: toolsConfigSchema.optional(),
|
|
17
24
|
providerOptions: z.object( {
|
|
18
25
|
thinking: z.object( {
|
|
19
26
|
type: z.enum( [ 'enabled', 'disabled' ] ),
|
|
20
27
|
budgetTokens: z.number().optional()
|
|
21
28
|
} ).loose().optional()
|
|
22
|
-
} ).loose().optional()
|
|
29
|
+
} ).loose().optional(),
|
|
30
|
+
messageOptions: z.record( z.string(), providerOptionsSchema ).optional()
|
|
23
31
|
} ).loose(),
|
|
24
32
|
messages: z.array(
|
|
25
33
|
z.object( {
|
|
26
34
|
role: z.string(),
|
|
27
|
-
content: z.string()
|
|
35
|
+
content: z.string(),
|
|
36
|
+
attributes: attributesSchema.optional()
|
|
28
37
|
} ).strict()
|
|
29
38
|
),
|
|
30
39
|
instructions: z.string().trim().min( 1 ).nullable().optional()
|
|
@@ -384,6 +384,71 @@ describe( 'validatePrompt', () => {
|
|
|
384
384
|
expect( () => validatePrompt( promptWithSkill ) ).not.toThrow();
|
|
385
385
|
} );
|
|
386
386
|
|
|
387
|
+
it( 'should validate provider tool config records', () => {
|
|
388
|
+
const promptWithTools = {
|
|
389
|
+
name: 'tools-prompt',
|
|
390
|
+
config: {
|
|
391
|
+
provider: 'vertex',
|
|
392
|
+
model: 'gemini-2.0-flash',
|
|
393
|
+
tools: {
|
|
394
|
+
googleSearch: {
|
|
395
|
+
mode: 'MODE_DYNAMIC',
|
|
396
|
+
dynamicThreshold: 0.8
|
|
397
|
+
},
|
|
398
|
+
urlContext: {}
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
messages: [
|
|
402
|
+
{
|
|
403
|
+
role: 'user',
|
|
404
|
+
content: 'Research this.'
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
expect( () => validatePrompt( promptWithTools ) ).not.toThrow();
|
|
410
|
+
} );
|
|
411
|
+
|
|
412
|
+
it( 'should throw ValidationError when tools config is not a record', () => {
|
|
413
|
+
const invalidToolsPrompt = {
|
|
414
|
+
name: 'invalid-tools-prompt',
|
|
415
|
+
config: {
|
|
416
|
+
provider: 'vertex',
|
|
417
|
+
model: 'gemini-2.0-flash',
|
|
418
|
+
tools: [ 'googleSearch' ]
|
|
419
|
+
},
|
|
420
|
+
messages: [
|
|
421
|
+
{
|
|
422
|
+
role: 'user',
|
|
423
|
+
content: 'Research this.'
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
expect( () => validatePrompt( invalidToolsPrompt ) ).toThrow( ValidationError );
|
|
429
|
+
} );
|
|
430
|
+
|
|
431
|
+
it( 'should throw ValidationError when a tool config is not a record', () => {
|
|
432
|
+
const invalidToolConfigPrompt = {
|
|
433
|
+
name: 'invalid-tool-config-prompt',
|
|
434
|
+
config: {
|
|
435
|
+
provider: 'vertex',
|
|
436
|
+
model: 'gemini-2.0-flash',
|
|
437
|
+
tools: {
|
|
438
|
+
googleSearch: 'MODE_DYNAMIC'
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
messages: [
|
|
442
|
+
{
|
|
443
|
+
role: 'user',
|
|
444
|
+
content: 'Research this.'
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
expect( () => validatePrompt( invalidToolConfigPrompt ) ).toThrow( ValidationError );
|
|
450
|
+
} );
|
|
451
|
+
|
|
387
452
|
it( 'should throw ValidationError for invalid skill path config', () => {
|
|
388
453
|
const invalidSkillsPrompt = {
|
|
389
454
|
name: 'invalid-skills-prompt',
|
|
@@ -531,4 +596,53 @@ describe( 'validatePrompt', () => {
|
|
|
531
596
|
|
|
532
597
|
expect( () => validatePrompt( maxTokensSnakeCase ) ).not.toThrow();
|
|
533
598
|
} );
|
|
599
|
+
|
|
600
|
+
it( 'should validate the options attribute referencing messageOptions sets', () => {
|
|
601
|
+
const promptWithMessageOptions = {
|
|
602
|
+
name: 'message-options-prompt',
|
|
603
|
+
config: {
|
|
604
|
+
provider: 'anthropic',
|
|
605
|
+
model: 'claude-sonnet-4-5',
|
|
606
|
+
messageOptions: {
|
|
607
|
+
cached: { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
messages: [
|
|
611
|
+
{ role: 'system', content: 'Docs.', attributes: { options: 'cached' } },
|
|
612
|
+
{ role: 'user', content: 'Question' }
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
expect( () => validatePrompt( promptWithMessageOptions ) ).not.toThrow();
|
|
617
|
+
} );
|
|
618
|
+
|
|
619
|
+
it( 'should reject the removed cache shorthand as an unknown block attribute', () => {
|
|
620
|
+
const cacheShorthandPrompt = {
|
|
621
|
+
name: 'cache-shorthand-prompt',
|
|
622
|
+
config: {
|
|
623
|
+
provider: 'anthropic',
|
|
624
|
+
model: 'claude-sonnet-4-5'
|
|
625
|
+
},
|
|
626
|
+
messages: [
|
|
627
|
+
{ role: 'system', content: 'Static.', attributes: { cache: true } }
|
|
628
|
+
]
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
expect( () => validatePrompt( cacheShorthandPrompt ) ).toThrow( ValidationError );
|
|
632
|
+
} );
|
|
633
|
+
|
|
634
|
+
it( 'should throw ValidationError for unknown top-level message fields', () => {
|
|
635
|
+
const unknownFieldPrompt = {
|
|
636
|
+
name: 'unknown-field-prompt',
|
|
637
|
+
config: {
|
|
638
|
+
provider: 'anthropic',
|
|
639
|
+
model: 'claude-sonnet-4-5'
|
|
640
|
+
},
|
|
641
|
+
messages: [
|
|
642
|
+
{ role: 'user', content: 'Hi', options: 'cached' }
|
|
643
|
+
]
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
expect( () => validatePrompt( unknownFieldPrompt ) ).toThrow( ValidationError );
|
|
647
|
+
} );
|
|
534
648
|
} );
|
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
} from 'ai';
|
|
14
14
|
import { FatalError } from '@outputai/core';
|
|
15
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
|
+
|
|
16
19
|
/**
|
|
17
20
|
* Recursively search an error cause chain until finds an error which is instance of given prototype.
|
|
18
21
|
*
|
|
@@ -25,7 +28,7 @@ export const findInstanceInCauseChain = ( error, _class, depth = 0 ) => {
|
|
|
25
28
|
if ( !error || typeof error !== 'object' ) {
|
|
26
29
|
return null;
|
|
27
30
|
}
|
|
28
|
-
if ( typeof _class === 'string' && error.constructor
|
|
31
|
+
if ( typeof _class === 'string' && error.constructor?.name === _class ) {
|
|
29
32
|
return error;
|
|
30
33
|
}
|
|
31
34
|
if ( typeof _class === 'function' && error instanceof _class ) {
|
|
@@ -42,20 +45,34 @@ const toFatalError = ( error, extraMessage = '' ) => new FatalError(
|
|
|
42
45
|
{ cause: error }
|
|
43
46
|
);
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Map an AI SDK error to a framework specific error:
|
|
50
|
+
*
|
|
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
|
+
* @param {object} error - Original Error
|
|
55
|
+
* @returns {object} A new Error
|
|
56
|
+
*/
|
|
45
57
|
export const mapAiError = error => {
|
|
46
58
|
if ( error instanceof FatalError ) {
|
|
47
59
|
return error;
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
// NoObjectGeneratedError can be thrown when the response doesn't match the schema
|
|
51
|
-
// This
|
|
52
|
-
if ( NoObjectGeneratedError.isInstance( error ) && error.message.includes(
|
|
62
|
+
// NoObjectGeneratedError can be thrown when the response doesn't match the schema.
|
|
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 ) ) {
|
|
53
65
|
const zodError = findInstanceInCauseChain( error, 'ZodError' );
|
|
54
66
|
if ( zodError && zodError.issues?.length > 0 ) {
|
|
55
|
-
const { path, message } = zodError.issues
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
} );
|
|
59
76
|
}
|
|
60
77
|
return error;
|
|
61
78
|
}
|
|
@@ -171,6 +171,13 @@ describe( 'findInstanceInCauseChain', () => {
|
|
|
171
171
|
expect( findInstanceInCauseChain( 'not an error', Error ) ).toBeNull();
|
|
172
172
|
} );
|
|
173
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
|
+
|
|
174
181
|
it( 'stops searching after the depth limit', () => {
|
|
175
182
|
const makeErrorChain = depth => depth === 0 ?
|
|
176
183
|
new SecondCustomError( 'target' ) :
|
|
@@ -204,17 +211,25 @@ describe( 'mapAiError', () => {
|
|
|
204
211
|
const error = new NoObjectGeneratedError( {
|
|
205
212
|
message: 'No object generated: response did not match schema.',
|
|
206
213
|
text: '{"items":[{}]}',
|
|
214
|
+
response: { id: 'response-1' },
|
|
215
|
+
usage: { totalTokens: 10 },
|
|
216
|
+
finishReason: 'stop',
|
|
207
217
|
cause: validationError
|
|
208
218
|
} );
|
|
209
219
|
|
|
210
220
|
const result = mapAiError( error );
|
|
211
221
|
|
|
212
222
|
expect( result ).not.toBe( error );
|
|
213
|
-
expect(
|
|
223
|
+
expect( NoObjectGeneratedError.isInstance( result ) ).toBe( true );
|
|
224
|
+
expect( result.name ).toBe( 'AI_NoObjectGeneratedError' );
|
|
214
225
|
expect( result.message ).toBe(
|
|
215
226
|
'No object generated: response did not match schema. First issue is "Expected string" at path [items, 0, title].'
|
|
216
227
|
);
|
|
217
|
-
expect( result.cause ).toBe(
|
|
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 );
|
|
218
233
|
} );
|
|
219
234
|
|
|
220
235
|
it( 'preserves NoObjectGeneratedError schema mismatches when no schema issue is available', () => {
|