@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,333 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { parseMarkup } from './nodes.js';
3
-
4
- const blocks = content =>
5
- parseMarkup( content ).map( ( { tagName, content: blockContent } ) => ( {
6
- tagName,
7
- content: blockContent
8
- } ) );
9
-
10
- describe( 'parseMarkup', () => {
11
- describe( 'top-level blocks', () => {
12
- it( 'parses one generic block', () => {
13
- expect( blocks( '<user>Hello</user>' ) ).toEqual( [
14
- { tagName: 'user', content: 'Hello' }
15
- ] );
16
- } );
17
-
18
- it( 'parses multiple sibling blocks', () => {
19
- expect( blocks( '<system>Rules</system>\n\n<user>Question</user>' ) ).toEqual( [
20
- { tagName: 'system', content: 'Rules' },
21
- { tagName: 'user', content: 'Question' }
22
- ] );
23
- } );
24
-
25
- it( 'supports generic and hyphenated tag names', () => {
26
- expect( blocks( '<custom-block>Value</custom-block>' ) ).toEqual( [
27
- { tagName: 'custom-block', content: 'Value' }
28
- ] );
29
- } );
30
-
31
- it( 'matches tag names case-insensitively', () => {
32
- expect( blocks( '<User>Hello</USER>' ) ).toEqual( [
33
- { tagName: 'user', content: 'Hello' }
34
- ] );
35
- } );
36
-
37
- it( 'parses an empty block', () => {
38
- expect( blocks( '<user></user>' ) ).toEqual( [
39
- { tagName: 'user', content: '' }
40
- ] );
41
- } );
42
-
43
- it( 'rejects a top-level self-closing block', () => {
44
- expect( () => parseMarkup( '<user />' ) )
45
- .toThrow( /^Invalid root tag: self-closing tag "<user \/>" is not allowed\.$/ );
46
- } );
47
-
48
- it( 'returns no blocks for empty content', () => {
49
- expect( parseMarkup( '' ) ).toEqual( [] );
50
- } );
51
-
52
- it( 'returns no blocks for plain text', () => {
53
- expect( parseMarkup( 'Generate a cinematic poster.' ) ).toEqual( [] );
54
- } );
55
-
56
- it( 'returns no blocks for text mixed with whitespace and comments', () => {
57
- expect( parseMarkup( '<!-- before -->\nGenerate a poster.\n<!-- after -->' ) ).toEqual( [] );
58
- } );
59
-
60
- it( 'returns no blocks when text precedes otherwise valid markup', () => {
61
- expect( parseMarkup( 'Note\n<user>Hello</user>\nTail' ) ).toEqual( [] );
62
- } );
63
-
64
- it( 'uses the first meaningful token after leading comments', () => {
65
- expect( parseMarkup( '<!-- note -->\nExplain <user>Hello</user> literally.' ) ).toEqual( [] );
66
- } );
67
-
68
- it( 'does not validate later tag-shaped content after instruction mode starts', () => {
69
- expect( parseMarkup( 'Explain </user>, <image />, and <user> examples.' ) ).toEqual( [] );
70
- } );
71
-
72
- it( 'returns no blocks for tag-shaped text that is not a valid tag', () => {
73
- expect( parseMarkup( 'Use a<b, b>c, and c < d.' ) ).toEqual( [] );
74
- } );
75
-
76
- it( 'treats an unterminated comment marker as plain text', () => {
77
- expect( parseMarkup( '<!-- unfinished' ) ).toEqual( [] );
78
- } );
79
-
80
- it( 'returns no blocks for whitespace and comments only', () => {
81
- expect( parseMarkup( '\n<!-- first -->\n<!-- second\nnote -->\n' ) ).toEqual( [] );
82
- } );
83
-
84
- it( 'ignores surrounding whitespace and comments', () => {
85
- expect( blocks( '<!-- prompt note -->\n\n<user>Hello</user>\n' ) ).toEqual( [
86
- { tagName: 'user', content: 'Hello' }
87
- ] );
88
- } );
89
-
90
- it( 'ignores multiline comments outside blocks', () => {
91
- expect( blocks( '<!-- prompt\nnote -->\n<user>Hello</user>' ) ).toEqual( [
92
- { tagName: 'user', content: 'Hello' }
93
- ] );
94
- } );
95
-
96
- it( 'ignores comments before, between, and after blocks', () => {
97
- expect( blocks(
98
- '<!-- before --><system>Rules</system><!-- between --><user>Question</user><!-- after -->'
99
- ) ).toEqual( [
100
- { tagName: 'system', content: 'Rules' },
101
- { tagName: 'user', content: 'Question' }
102
- ] );
103
- } );
104
-
105
- it( 'supports whitespace inside a closing tag', () => {
106
- expect( blocks( '<user>Hello</ user >' ) ).toEqual( [
107
- { tagName: 'user', content: 'Hello' }
108
- ] );
109
- } );
110
- } );
111
-
112
- describe( 'opaque block content', () => {
113
- it( 'preserves a closed nested tag with a different name', () => {
114
- expect( blocks( '<user>Use <title>Example</title> here.</user>' ) ).toEqual( [
115
- { tagName: 'user', content: 'Use <title>Example</title> here.' }
116
- ] );
117
- } );
118
-
119
- it( 'preserves an open nested tag with a different name', () => {
120
- expect( blocks( '<user>Use the <title> tag here.</user>' ) ).toEqual( [
121
- { tagName: 'user', content: 'Use the <title> tag here.' }
122
- ] );
123
- } );
124
-
125
- it( 'preserves several unclosed nested tags with different names', () => {
126
- expect( blocks( '<user><article>Use <title>Example.</user>' ) ).toEqual( [
127
- { tagName: 'user', content: '<article>Use <title>Example.' }
128
- ] );
129
- } );
130
-
131
- it( 'preserves a nested tag matching another supported role', () => {
132
- expect( blocks( '<user>Describe the <system> tag.</user>' ) ).toEqual( [
133
- { tagName: 'user', content: 'Describe the <system> tag.' }
134
- ] );
135
- } );
136
-
137
- it( 'preserves angle-bracket TypeScript syntax', () => {
138
- expect( blocks( '<user>Explain Array<string> and Promise<Result>.</user>' ) ).toEqual( [
139
- { tagName: 'user', content: 'Explain Array<string> and Promise<Result>.' }
140
- ] );
141
- } );
142
-
143
- it( 'preserves unmatched angle brackets in text', () => {
144
- expect( blocks( '<user>Use a<b, b>c, c < d and d > e.</user>' ) ).toEqual( [
145
- { tagName: 'user', content: 'Use a<b, b>c, c < d and d > e.' }
146
- ] );
147
- } );
148
-
149
- it( 'preserves a different unmatched closing tag', () => {
150
- expect( blocks( '<user>Show </title> literally.</user>' ) ).toEqual( [
151
- { tagName: 'user', content: 'Show </title> literally.' }
152
- ] );
153
- } );
154
-
155
- it( 'preserves comments inside a block', () => {
156
- expect( blocks( '<user>Before<!-- hidden > note -->After</user>' ) ).toEqual( [
157
- { tagName: 'user', content: 'Before<!-- hidden > note -->After' }
158
- ] );
159
- } );
160
-
161
- it( 'preserves multiline comments inside a block', () => {
162
- expect( blocks( '<user>Before<!-- hidden\nnote -->After</user>' ) ).toEqual( [
163
- { tagName: 'user', content: 'Before<!-- hidden\nnote -->After' }
164
- ] );
165
- } );
166
-
167
- it( 'preserves a closing-tag-shaped value inside a comment', () => {
168
- expect( blocks( '<user>Before<!-- </user> -->After</user>' ) ).toEqual( [
169
- { tagName: 'user', content: 'Before<!-- </user> -->After' }
170
- ] );
171
- } );
172
-
173
- it( 'preserves empty comments inside a block', () => {
174
- expect( blocks( '<user>Before<!---->After</user>' ) ).toEqual( [
175
- { tagName: 'user', content: 'Before<!---->After' }
176
- ] );
177
- } );
178
-
179
- it( 'preserves whitespace, line endings, and Unicode content exactly', () => {
180
- expect( blocks( '<user>\r\n Olá 👋\t \r\n</user>' ) ).toEqual( [
181
- { tagName: 'user', content: '\r\n Olá 👋\t \r\n' }
182
- ] );
183
- } );
184
-
185
- it( 'preserves self-closing tags inside a block', () => {
186
- expect( blocks( '<user>Before<image source="avatar.png" />After</user>' ) ).toEqual( [
187
- { tagName: 'user', content: 'Before<image source="avatar.png" />After' }
188
- ] );
189
- } );
190
-
191
- it( 'preserves a self-closing tag matching the block name', () => {
192
- expect( blocks( '<user>Write <user /> literally.</user>' ) ).toEqual( [
193
- { tagName: 'user', content: 'Write <user /> literally.' }
194
- ] );
195
- } );
196
-
197
- it( 'allows escaped same-name tags as text', () => {
198
- expect( blocks( '<user>Use &lt;user&gt; and &lt;/user&gt;.</user>' ) ).toEqual( [
199
- { tagName: 'user', content: 'Use &lt;user&gt; and &lt;/user&gt;.' }
200
- ] );
201
- } );
202
-
203
- } );
204
-
205
- describe( 'attributes', () => {
206
- it( 'parses quoted, unquoted, and boolean attributes', () => {
207
- const [ node ] = parseMarkup( '<user options="cached" mode=fast pinned>Hello</user>' );
208
-
209
- expect( node.attributes ).toEqual( {
210
- options: 'cached',
211
- mode: 'fast',
212
- pinned: true
213
- } );
214
- } );
215
-
216
- it( 'supports whitespace around attribute equals signs', () => {
217
- const [ node ] = parseMarkup( '<user options = "cached shared" mode = fast pinned>Hello</user>' );
218
-
219
- expect( node.attributes ).toEqual( {
220
- options: 'cached shared',
221
- mode: 'fast',
222
- pinned: true
223
- } );
224
- } );
225
-
226
- it( 'lowercases attribute names and preserves values as strings', () => {
227
- const [ node ] = parseMarkup( '<user ENABLED=false COUNT=001 RATIO=1.25>Hello</user>' );
228
-
229
- expect( node.attributes ).toEqual( {
230
- enabled: 'false',
231
- count: '001',
232
- ratio: '1.25'
233
- } );
234
- } );
235
-
236
- it( 'parses empty explicit attribute values', () => {
237
- const [ node ] = parseMarkup( '<user first="" second=\'\' third=>Hello</user>' );
238
-
239
- expect( node.attributes ).toEqual( {
240
- first: '',
241
- second: '',
242
- third: ''
243
- } );
244
- } );
245
-
246
- it( 'allows the opposite quote type inside quoted values', () => {
247
- const [ node ] = parseMarkup( '<user single="it\'s ready" double=\'say "ready"\'>Hello</user>' );
248
-
249
- expect( node.attributes ).toEqual( {
250
- single: 'it\'s ready',
251
- double: 'say "ready"'
252
- } );
253
- } );
254
-
255
- it( 'allows greater-than characters inside double-quoted attributes', () => {
256
- const [ node ] = parseMarkup( '<user condition="score > 10">Hello</user>' );
257
-
258
- expect( node.attributes ).toEqual( { condition: 'score > 10' } );
259
- expect( node.content ).toBe( 'Hello' );
260
- } );
261
-
262
- it( 'allows greater-than characters inside single-quoted attributes', () => {
263
- const [ node ] = parseMarkup( '<user condition=\'score > 10\'>Hello</user>' );
264
-
265
- expect( node.attributes ).toEqual( { condition: 'score > 10' } );
266
- expect( node.content ).toBe( 'Hello' );
267
- } );
268
-
269
- it( 'rejects on single-quoted attributes missing end quote', () => {
270
- expect( () => parseMarkup( '<user color=\'red>Hello</user>' ) ).toThrow();
271
- } );
272
-
273
- it( 'rejects on double-quoted attributes missing end quote', () => {
274
- expect( () => parseMarkup( '<user color="red>Hello</user>' ) ).toThrow();
275
- } );
276
-
277
- it( 'rejects attributes closed by a different quote type', () => {
278
- expect( () => parseMarkup( '<user color=\'red">Hello</user>' ) ).toThrow();
279
- expect( () => parseMarkup( '<user color="red\'>Hello</user>' ) ).toThrow();
280
- } );
281
-
282
- it( 'rejects stray quotes in unquoted attribute values', () => {
283
- expect( () => parseMarkup( '<user color=red\'>Hello</user>' ) ).toThrow();
284
- expect( () => parseMarkup( '<user color=red">Hello</user>' ) ).toThrow();
285
- } );
286
- } );
287
-
288
- describe( 'invalid markup', () => {
289
- it( 'rejects a closed nested tag with the same name', () => {
290
- expect( () => parseMarkup( '<user>Outer <user>Inner</user> tail</user>' ) )
291
- .toThrow( /^Invalid child tag: <user> cannot appear inside another <user>\. Escape it as "&lt;user&gt;"\.$/ );
292
- } );
293
-
294
- it( 'rejects an open nested tag with the same name', () => {
295
- expect( () => parseMarkup( '<user>Use the <user> tag.</user>' ) ).toThrow();
296
- } );
297
-
298
- it( 'rejects a same-name nested tag with different casing', () => {
299
- expect( () => parseMarkup( '<User>Use the <user> tag.</USER>' ) ).toThrow();
300
- } );
301
-
302
- it( 'rejects a missing top-level closing tag', () => {
303
- expect( () => parseMarkup( '<user>Hello' ) )
304
- .toThrow( /^Invalid tag: <user> is missing its closing tag\.$/ );
305
- } );
306
-
307
- it( 'rejects a mismatched top-level closing tag', () => {
308
- expect( () => parseMarkup( '<user>Hello</system>' ) ).toThrow();
309
- } );
310
-
311
- it( 'rejects an unmatched closing tag outside a block', () => {
312
- expect( () => parseMarkup( '</user>' ) )
313
- .toThrow( /^Invalid root tag: closing tag "<\/user>" has no matching opening tag\.$/ );
314
- } );
315
-
316
- it( 'treats non-whitespace text before a block as instruction mode', () => {
317
- expect( parseMarkup( 'Prefix <user>Hello</user>' ) ).toEqual( [] );
318
- } );
319
-
320
- it( 'rejects non-whitespace text between blocks', () => {
321
- expect( () => parseMarkup( '<system>Rules</system> stray <user>Hello</user>' ) ).toThrow();
322
- } );
323
-
324
- it( 'rejects non-whitespace text after a block', () => {
325
- expect( () => parseMarkup( '<user>Hello</user> suffix' ) ).toThrow();
326
- } );
327
-
328
- it( 'rejects root text immediately after markup mode starts', () => {
329
- expect( () => parseMarkup( '<system>Rules</system> stray <user>Hello' ) )
330
- .toThrow( /^Invalid root text: text is not allowed outside message tags\.$/ );
331
- } );
332
- } );
333
- } );
@@ -1,149 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { tokenize } from './tokenizer.js';
3
-
4
- describe( 'tokenize', () => {
5
- it( 'returns null for empty input', () => {
6
- expect( tokenize( '' ) ).toBeNull();
7
- } );
8
-
9
- it( 'returns plain text as one token', () => {
10
- expect( tokenize( 'plain text' ) ).toEqual( [ 'plain text' ] );
11
- } );
12
-
13
- it( 'separates opening tags, content, and closing tags', () => {
14
- expect( tokenize( '<user>Hello</user>' ) ).toEqual( [
15
- '<user>',
16
- 'Hello',
17
- '</user>'
18
- ] );
19
- } );
20
-
21
- it( 'tokenizes multiple sibling blocks and their whitespace', () => {
22
- expect( tokenize( '<system>Rules</system>\n\n<user>Question</user>' ) ).toEqual( [
23
- '<system>',
24
- 'Rules',
25
- '</system>',
26
- '\n\n',
27
- '<user>',
28
- 'Question',
29
- '</user>'
30
- ] );
31
- } );
32
-
33
- it( 'keeps opening tags with attributes intact', () => {
34
- expect( tokenize( '<user options="cached shared" mode=fast>Hello</user>' ) ).toEqual( [
35
- '<user options="cached shared" mode=fast>',
36
- 'Hello',
37
- '</user>'
38
- ] );
39
- } );
40
-
41
- it( 'keeps greater-than characters inside quoted attributes', () => {
42
- expect( tokenize( '<user condition="score > 10">Hello</user>' ) ).toEqual( [
43
- '<user condition="score > 10">',
44
- 'Hello',
45
- '</user>'
46
- ] );
47
- } );
48
-
49
- it( 'keeps self-closing tags intact', () => {
50
- expect( tokenize( '<user>Before<image source="avatar.png" />After</user>' ) ).toEqual( [
51
- '<user>',
52
- 'Before',
53
- '<image source="avatar.png" />',
54
- 'After',
55
- '</user>'
56
- ] );
57
- } );
58
-
59
- it( 'supports whitespace in closing tags', () => {
60
- expect( tokenize( '<user>Hello</ user >' ) ).toEqual( [
61
- '<user>',
62
- 'Hello',
63
- '</ user >'
64
- ] );
65
- } );
66
-
67
- it( 'keeps single-line comments intact', () => {
68
- expect( tokenize( '<!-- note --><user>Hello</user>' ) ).toEqual( [
69
- '<!-- note -->',
70
- '<user>',
71
- 'Hello',
72
- '</user>'
73
- ] );
74
- } );
75
-
76
- it( 'keeps multiline comments intact', () => {
77
- expect( tokenize( '<!-- first\nsecond --><user>Hello</user>' ) ).toEqual( [
78
- '<!-- first\nsecond -->',
79
- '<user>',
80
- 'Hello',
81
- '</user>'
82
- ] );
83
- } );
84
-
85
- it( 'tokenizes adjacent comments separately', () => {
86
- expect( tokenize( '<!-- first --><!-- second -->' ) ).toEqual( [
87
- '<!-- first -->',
88
- '<!-- second -->'
89
- ] );
90
- } );
91
-
92
- it( 'preserves a lone less-than character', () => {
93
- expect( tokenize( '<' ) ).toEqual( [ '<' ] );
94
- } );
95
-
96
- it( 'preserves an unterminated comment losslessly', () => {
97
- const input = '<!-- unfinished';
98
- const tokens = tokenize( input );
99
-
100
- expect( tokens ).toEqual( [ '<', '!-- unfinished' ] );
101
- expect( tokens.join( '' ) ).toBe( input );
102
- } );
103
-
104
- it( 'keeps a tag with an unterminated attribute quote intact for later validation', () => {
105
- const input = '<user color=\'red>Hello</user>';
106
-
107
- expect( tokenize( input ) ).toEqual( [
108
- '<user color=\'red>',
109
- 'Hello',
110
- '</user>'
111
- ] );
112
- } );
113
-
114
- it( 'preserves TypeScript angle-bracket syntax losslessly', () => {
115
- const input = 'Explain Array<string> and Promise<Result>.';
116
-
117
- expect( tokenize( input ).join( '' ) ).toBe( input );
118
- } );
119
-
120
- it( 'preserves comparison operators and tag-shaped text losslessly', () => {
121
- const input = 'Use 1<2, a<b, b>c, c < d, and d > e.';
122
-
123
- expect( tokenize( input ).join( '' ) ).toBe( input );
124
- } );
125
-
126
- it( 'preserves every character in mixed markup', () => {
127
- const input = '<!-- note -->\n<user condition="a > b">Use x<y and <title>.</user>';
128
-
129
- expect( tokenize( input ).join( '' ) ).toBe( input );
130
- } );
131
-
132
- it.each( [
133
- '',
134
- '<',
135
- '<<<user>',
136
- '<!-- first --><!-- second -->',
137
- '<!-- unfinished',
138
- '<user>Hello</user>',
139
- '<user />',
140
- '<user condition="score > 10">Hello</user>',
141
- '<user color=\'red>Hello</user>',
142
- '<user color=\'red">Hello</user>',
143
- '<b,b>',
144
- '<user>Use 1<2, a<b, and b>c.</user>',
145
- '<user>\r\nUnicode: Olá 👋\r\n</user>'
146
- ] )( 'is lossless for %s', input => {
147
- expect( ( tokenize( input ) ?? [] ).join( '' ) ).toBe( input );
148
- } );
149
- } );