@outputai/llm 0.11.1-next.da26845.0 → 0.12.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.
Files changed (47) hide show
  1. package/package.json +7 -3
  2. package/src/agent.js +7 -2
  3. package/src/generate.js +1 -0
  4. package/src/index.d.ts +14 -6
  5. package/src/utils/wrap.js +35 -2
  6. package/src/agent.spec.js +0 -595
  7. package/src/ai_provider.spec.js +0 -217
  8. package/src/ai_sdk_options.spec.js +0 -390
  9. package/src/fixtures/image_response_v7_google_vertex.js +0 -72
  10. package/src/fixtures/image_response_v7_openai.js +0 -77
  11. package/src/fixtures/models_api_light.json +0 -675
  12. package/src/fixtures/models_api_v7_subset.json +0 -259
  13. package/src/fixtures/stream_response_v7_anthropic.js +0 -389
  14. package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
  15. package/src/fixtures/stream_response_v7_openai.js +0 -353
  16. package/src/fixtures/stream_response_v7_perplexity.js +0 -671
  17. package/src/fixtures/text_response_v7_anthropic.js +0 -359
  18. package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
  19. package/src/fixtures/text_response_v7_google_vertex.js +0 -435
  20. package/src/fixtures/text_response_v7_openai.js +0 -347
  21. package/src/fixtures/text_response_v7_perplexity.js +0 -764
  22. package/src/generate.spec.js +0 -525
  23. package/src/prompt/content.spec.js +0 -208
  24. package/src/prompt/interpolations.spec.js +0 -109
  25. package/src/prompt/loader.full.spec.js +0 -309
  26. package/src/prompt/loader.spec.js +0 -258
  27. package/src/prompt/markup/attributes.spec.js +0 -132
  28. package/src/prompt/markup/nodes.spec.js +0 -333
  29. package/src/prompt/markup/tokenizer.spec.js +0 -149
  30. package/src/prompt/markup/tokens.spec.js +0 -250
  31. package/src/prompt/validations.spec.js +0 -812
  32. package/src/utils/cost.full.spec.js +0 -272
  33. package/src/utils/cost.spec.js +0 -290
  34. package/src/utils/error_handler.spec.js +0 -230
  35. package/src/utils/file.spec.js +0 -89
  36. package/src/utils/image.spec.js +0 -20
  37. package/src/utils/legacy_cost_attribute.spec.js +0 -278
  38. package/src/utils/models.spec.js +0 -119
  39. package/src/utils/models_pricing.spec.js +0 -162
  40. package/src/utils/skills.spec.js +0 -168
  41. package/src/utils/sources.spec.js +0 -122
  42. package/src/utils/stream.spec.js +0 -55
  43. package/src/utils/tools.spec.js +0 -167
  44. package/src/utils/usage.full.spec.js +0 -215
  45. package/src/utils/usage.spec.js +0 -246
  46. package/src/utils/wrap.spec.js +0 -467
  47. package/src/validations.spec.js +0 -631
@@ -1,258 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
-
3
- const liquidMocks = vi.hoisted( () => ( {
4
- createContext: vi.fn(),
5
- options: { strictVariables: true },
6
- parseAndRenderSync: vi.fn(),
7
- registerFilter: vi.fn()
8
- } ) );
9
-
10
- vi.mock( 'liquidjs', () => ( {
11
- Context: class Context {
12
- constructor( ...args ) {
13
- liquidMocks.createContext( this, ...args );
14
- }
15
- },
16
- Liquid: class Liquid {
17
- options = liquidMocks.options;
18
- parseAndRenderSync = liquidMocks.parseAndRenderSync;
19
- registerFilter = liquidMocks.registerFilter;
20
- }
21
- } ) );
22
-
23
- vi.mock( 'gray-matter', () => ( {
24
- default: vi.fn()
25
- } ) );
26
-
27
- vi.mock( '@outputai/core/sdk/helpers', () => ( {
28
- Path: {
29
- resolveInvocationDir: vi.fn( () => '/invocation' )
30
- }
31
- } ) );
32
-
33
- vi.mock( './interpolations.js', () => ( {
34
- pipeInterpolations: vi.fn( value => value ),
35
- interpolationFilterToken: '__var_safe',
36
- encode: vi.fn(),
37
- decode: vi.fn( value => value )
38
- } ) );
39
-
40
- vi.mock( '../utils/file.js', () => ( {
41
- searchAndReadFile: vi.fn()
42
- } ) );
43
-
44
- vi.mock( './content.js', () => ( {
45
- parseContent: vi.fn()
46
- } ) );
47
-
48
- vi.mock( './validations.js', () => ( {
49
- parsePromptSchema: vi.fn( prompt => prompt )
50
- } ) );
51
-
52
- import { loadPrompt } from './loader.js';
53
- import { Path } from '@outputai/core/sdk/helpers';
54
- import matter from 'gray-matter';
55
- import { decode, pipeInterpolations } from './interpolations.js';
56
- import { searchAndReadFile } from '../utils/file.js';
57
- import { parseContent } from './content.js';
58
- import { parsePromptSchema } from './validations.js';
59
-
60
- const defaultConfig = {
61
- provider: 'anthropic',
62
- model: 'claude-3-5-sonnet-20241022'
63
- };
64
-
65
- const defaultParsed = {
66
- messages: [ { role: 'user', content: 'Hi' } ],
67
- instructions: null
68
- };
69
-
70
- const stubMatter = ( config = defaultConfig ) => {
71
- matter.mockImplementation( ( _input, options ) => {
72
- if ( options?.engines ) {
73
- return { matter: 'RAW_YAML', content: 'RAW_BODY' };
74
- }
75
- return { data: { ...config } };
76
- } );
77
- };
78
-
79
- describe( 'loadPrompt', () => {
80
- beforeEach( () => {
81
- vi.clearAllMocks();
82
- Path.resolveInvocationDir.mockReturnValue( '/invocation' );
83
- searchAndReadFile.mockReturnValue( { content: 'FILE', dir: '/mock/dir' } );
84
- pipeInterpolations.mockImplementation( value => value );
85
- decode.mockImplementation( value => value );
86
- liquidMocks.parseAndRenderSync.mockImplementation( template => template );
87
- parseContent.mockReturnValue( { ...defaultParsed, messages: [ ...defaultParsed.messages ] } );
88
- parsePromptSchema.mockImplementation( prompt => prompt );
89
- stubMatter();
90
- } );
91
-
92
- afterEach( () => {
93
- vi.restoreAllMocks();
94
- } );
95
-
96
- it( 'splits, renders, parses, and validates the prompt', () => {
97
- const variables = { name: 'World' };
98
-
99
- const result = loadPrompt( 'test', variables );
100
-
101
- expect( searchAndReadFile ).toHaveBeenCalledWith( '/invocation', 'test.prompt' );
102
- expect( matter ).toHaveBeenNthCalledWith( 1, 'FILE', expect.objectContaining( {
103
- engines: { yaml: expect.any( Function ) }
104
- } ) );
105
- expect( matter ).toHaveBeenNthCalledWith( 2, '---\nRAW_YAML\n---\n' );
106
- expect( decode ).toHaveBeenCalledWith( defaultConfig );
107
- expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', undefined );
108
- expect( parsePromptSchema ).toHaveBeenCalledWith( {
109
- name: 'test',
110
- config: defaultConfig,
111
- messages: defaultParsed.messages,
112
- instructions: null,
113
- fileDir: '/mock/dir',
114
- variables
115
- } );
116
- expect( result ).toEqual( parsePromptSchema.mock.calls[0][0] );
117
- } );
118
-
119
- it( 'renders frontmatter before content using one Liquid context', () => {
120
- const variables = { name: 'World' };
121
-
122
- loadPrompt( 'test', variables );
123
-
124
- const context = liquidMocks.createContext.mock.calls[0][0];
125
- expect( liquidMocks.createContext ).toHaveBeenCalledWith( context, variables, liquidMocks.options, { sync: true } );
126
- expect( pipeInterpolations ).toHaveBeenNthCalledWith( 1, 'RAW_YAML' );
127
- expect( pipeInterpolations ).toHaveBeenNthCalledWith( 2, 'RAW_BODY' );
128
- expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 1, 'RAW_YAML', context );
129
- expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 2, 'RAW_BODY', context );
130
- } );
131
-
132
- it( 'uses decoded frontmatter for content parsing and validation', () => {
133
- const encodedConfig = {
134
- ...defaultConfig,
135
- messageOptions: { cached: { openai: { label: 'R&D' } } }
136
- };
137
- const decodedConfig = {
138
- ...defaultConfig,
139
- messageOptions: { cached: { openai: { label: 'R&D' } } }
140
- };
141
- stubMatter( encodedConfig );
142
- decode.mockReturnValue( decodedConfig );
143
-
144
- loadPrompt( 'test' );
145
-
146
- expect( decode ).toHaveBeenCalledWith( encodedConfig );
147
- expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', decodedConfig.messageOptions );
148
- expect( parsePromptSchema ).toHaveBeenCalledWith( expect.objectContaining( {
149
- config: decodedConfig
150
- } ) );
151
- } );
152
-
153
- it( 'passes the provided prompt directory to searchAndReadFile', () => {
154
- loadPrompt( 'test', {}, '/custom/prompts' );
155
-
156
- expect( searchAndReadFile ).toHaveBeenCalledWith( '/custom/prompts', 'test.prompt' );
157
- expect( Path.resolveInvocationDir ).not.toHaveBeenCalled();
158
- } );
159
-
160
- it( 'renders the piped template, not the raw body', () => {
161
- pipeInterpolations.mockReturnValue( 'PIPED' );
162
-
163
- loadPrompt( 'test', { name: 'World' } );
164
-
165
- expect( liquidMocks.parseAndRenderSync ).toHaveBeenNthCalledWith( 2, 'PIPED', expect.any( Object ) );
166
- expect( parseContent ).toHaveBeenCalledWith( 'PIPED', undefined );
167
- } );
168
-
169
- it( 'passes config.messageOptions to parseContent', () => {
170
- const messageOptions = { cached: { anthropic: { cacheControl: { type: 'ephemeral' } } } };
171
- stubMatter( { ...defaultConfig, messageOptions } );
172
-
173
- loadPrompt( 'test' );
174
-
175
- expect( parseContent ).toHaveBeenCalledWith( 'RAW_BODY', messageOptions );
176
- } );
177
-
178
- it( 'uses parseContent for messages and instructions', () => {
179
- parseContent.mockReturnValue( {
180
- messages: [],
181
- instructions: 'Generate a poster.'
182
- } );
183
-
184
- const result = loadPrompt( 'image_prompt' );
185
-
186
- expect( result.messages ).toEqual( [] );
187
- expect( result.instructions ).toBe( 'Generate a poster.' );
188
- } );
189
-
190
- it( 'throws when the prompt file is not found', () => {
191
- searchAndReadFile.mockReturnValue( null );
192
-
193
- expect( () => loadPrompt( 'nonexistent' ) ).toThrow( /Prompt file "nonexistent" not found/ );
194
- } );
195
-
196
- it( 'throws when rendered content is empty', () => {
197
- liquidMocks.parseAndRenderSync
198
- .mockReturnValueOnce( 'RAW_YAML' )
199
- .mockReturnValueOnce( ' ' );
200
-
201
- expect( () => loadPrompt( 'empty_prompt' ) ).toThrow( /Prompt "empty_prompt" has no content/ );
202
- } );
203
-
204
- it( 'wraps split errors with the prompt name', () => {
205
- matter.mockImplementation( () => {
206
- throw new Error( 'bad delimiters' );
207
- } );
208
-
209
- expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
210
- /Error parsing frontmatter on prompt "writer@v1": bad delimiters/
211
- );
212
- } );
213
-
214
- it( 'wraps content render errors with the prompt name', () => {
215
- liquidMocks.parseAndRenderSync
216
- .mockReturnValueOnce( 'RAW_YAML' )
217
- .mockImplementationOnce( () => {
218
- throw new Error( 'unknown variable' );
219
- } );
220
-
221
- expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
222
- /Error rendering content on prompt "writer@v1": unknown variable/
223
- );
224
- } );
225
-
226
- it( 'wraps frontmatter render errors with the prompt name', () => {
227
- liquidMocks.parseAndRenderSync.mockImplementationOnce( () => {
228
- throw new Error( 'unknown filter' );
229
- } );
230
-
231
- expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
232
- /Error rendering frontmatter on prompt "writer@v1": unknown filter/
233
- );
234
- } );
235
-
236
- it( 'wraps frontmatter yaml parse errors with the prompt name', () => {
237
- matter.mockImplementation( ( _input, options ) => {
238
- if ( options?.engines ) {
239
- return { matter: 'RAW_YAML', content: 'RAW_BODY' };
240
- }
241
- throw new Error( 'invalid yaml' );
242
- } );
243
-
244
- expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
245
- /Error converting frontmatter yaml to js on prompt "writer@v1": invalid yaml/
246
- );
247
- } );
248
-
249
- it( 'wraps content parse errors with the prompt name', () => {
250
- parseContent.mockImplementation( () => {
251
- throw new Error( 'unknown option' );
252
- } );
253
-
254
- expect( () => loadPrompt( 'writer@v1' ) ).toThrow(
255
- /Error parsing content on prompt "writer@v1": unknown option/
256
- );
257
- } );
258
- } );
@@ -1,132 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { extractAttributes } from './attributes.js';
3
-
4
- describe( 'extractAttributes', () => {
5
- it( 'returns no attributes for empty input', () => {
6
- expect( extractAttributes( '' ) ).toEqual( {} );
7
- } );
8
-
9
- it( 'returns no attributes for whitespace-only input', () => {
10
- expect( extractAttributes( ' \n\t ' ) ).toEqual( {} );
11
- } );
12
-
13
- it( 'parses bare attributes as true', () => {
14
- expect( extractAttributes( 'cached pinned' ) ).toEqual( {
15
- cached: true,
16
- pinned: true
17
- } );
18
- } );
19
-
20
- it( 'lowercases attribute names', () => {
21
- expect( extractAttributes( 'OPTIONS=cached PINNED' ) ).toEqual( {
22
- options: 'cached',
23
- pinned: true
24
- } );
25
- } );
26
-
27
- it( 'preserves unquoted values as strings', () => {
28
- expect( extractAttributes( 'enabled=false count=001 ratio=1.25' ) ).toEqual( {
29
- enabled: 'false',
30
- count: '001',
31
- ratio: '1.25'
32
- } );
33
- } );
34
-
35
- it( 'removes matching double quotes', () => {
36
- expect( extractAttributes( 'options="cached shared"' ) ).toEqual( {
37
- options: 'cached shared'
38
- } );
39
- } );
40
-
41
- it( 'removes matching single quotes', () => {
42
- expect( extractAttributes( 'options=\'cached shared\'' ) ).toEqual( {
43
- options: 'cached shared'
44
- } );
45
- } );
46
-
47
- it( 'allows the opposite quote type inside quoted values', () => {
48
- expect( extractAttributes( 'single="it\'s ready" double=\'say "ready"\'' ) ).toEqual( {
49
- single: 'it\'s ready',
50
- double: 'say "ready"'
51
- } );
52
- } );
53
-
54
- it( 'preserves equals and greater-than characters inside quoted values', () => {
55
- expect( extractAttributes( 'condition="score >= 10" expression=\'a = b\'' ) ).toEqual( {
56
- condition: 'score >= 10',
57
- expression: 'a = b'
58
- } );
59
- } );
60
-
61
- it( 'supports whitespace around equals signs', () => {
62
- expect( extractAttributes( 'options = "cached shared" mode = fast pinned' ) ).toEqual( {
63
- options: 'cached shared',
64
- mode: 'fast',
65
- pinned: true
66
- } );
67
- } );
68
-
69
- it( 'supports tabs and newlines between attributes', () => {
70
- expect( extractAttributes( 'options="cached"\nmode=fast\tpinned' ) ).toEqual( {
71
- options: 'cached',
72
- mode: 'fast',
73
- pinned: true
74
- } );
75
- } );
76
-
77
- it( 'ignores surrounding whitespace', () => {
78
- expect( extractAttributes( ' options = "cached" pinned ' ) ).toEqual( {
79
- options: 'cached',
80
- pinned: true
81
- } );
82
- } );
83
-
84
- it( 'parses an explicitly empty value', () => {
85
- expect( extractAttributes( 'options=' ) ).toEqual( { options: '' } );
86
- } );
87
-
88
- it( 'uses the last value when an attribute is repeated', () => {
89
- expect( extractAttributes( 'mode=slow mode=fast' ) ).toEqual( { mode: 'fast' } );
90
- } );
91
-
92
- it( 'rejects an unterminated single-quoted value', () => {
93
- expect( () => extractAttributes( 'color=\'red' ) )
94
- .toThrow( /^Invalid attribute "color": quoted value is missing its closing quote \('\)\.$/ );
95
- } );
96
-
97
- it( 'rejects an unterminated double-quoted value', () => {
98
- expect( () => extractAttributes( 'color="red' ) )
99
- .toThrow( /^Invalid attribute "color": quoted value is missing its closing quote \("\)\.$/ );
100
- } );
101
-
102
- it( 'rejects values closed by a different quote type', () => {
103
- expect( () => extractAttributes( 'color=\'red"' ) ).toThrow();
104
- expect( () => extractAttributes( 'color="red\'' ) ).toThrow();
105
- } );
106
-
107
- it( 'rejects stray quotes in unquoted values', () => {
108
- expect( () => extractAttributes( 'color=red\'' ) ).toThrow();
109
- expect( () => extractAttributes( 'color=red"' ) ).toThrow();
110
- expect( () => extractAttributes( 'color=arby\'s' ) ).toThrow();
111
- expect( () => extractAttributes( 'color=jack"madman"silva' ) ).toThrow();
112
- } );
113
-
114
- it( 'rejects matching quote characters inside quoted values', () => {
115
- expect( () => extractAttributes( 'message="say "hello""' ) )
116
- .toThrow( /^Invalid attribute "message": quoted value contains an unexpected quote \("\)\.$/ );
117
- expect( () => extractAttributes( 'message=\'say \'hello\'\'' ) ).toThrow();
118
- expect( () => extractAttributes( 'color="red"dark"' ) ).toThrow();
119
- expect( () => extractAttributes( 'color=\'red\'dark\'' ) ).toThrow();
120
- } );
121
-
122
- it( 'rejects an empty attribute name', () => {
123
- expect( () => extractAttributes( '=value' ) )
124
- .toThrow( /^Invalid attribute "=value": name is missing\.$/ );
125
- } );
126
-
127
- it( 'rejects invalid valued and bare attribute names', () => {
128
- expect( () => extractAttributes( 'bad/name=value' ) )
129
- .toThrow( /^Invalid attribute name "bad\/name"\.$/ );
130
- expect( () => extractAttributes( 'bad/name' ) ).toThrow();
131
- } );
132
- } );