@outputai/llm 0.11.0 → 0.11.1-next.2223fa5.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 +2 -2
- package/src/agent.js +105 -83
- package/src/agent.spec.js +312 -225
- package/src/ai_provider.js +2 -2
- package/src/ai_sdk_options.js +93 -39
- package/src/ai_sdk_options.spec.js +314 -154
- package/src/consts.js +6 -0
- package/src/generate.js +81 -0
- package/src/generate.spec.js +466 -0
- package/src/index.d.ts +222 -242
- package/src/index.js +3 -4
- package/src/prompt/content.js +70 -0
- package/src/prompt/content.spec.js +208 -0
- package/src/prompt/{escape.js → interpolations.js} +11 -18
- package/src/prompt/interpolations.spec.js +109 -0
- package/src/prompt/loader.full.spec.js +293 -0
- package/src/prompt/loader.js +45 -39
- package/src/prompt/loader.spec.js +186 -289
- package/src/prompt/markup/attributes.js +52 -0
- package/src/prompt/markup/attributes.spec.js +132 -0
- package/src/prompt/markup/nodes.js +71 -0
- package/src/prompt/markup/nodes.spec.js +333 -0
- package/src/prompt/markup/tokenizer.js +3 -0
- package/src/prompt/markup/tokenizer.spec.js +149 -0
- package/src/prompt/markup/tokens.js +26 -0
- package/src/prompt/markup/tokens.spec.js +250 -0
- package/src/prompt/validations.js +98 -67
- package/src/prompt/validations.spec.js +204 -47
- package/src/{prompt/load_content.js → utils/file.js} +12 -15
- package/src/utils/file.spec.js +89 -0
- package/src/utils/models.js +15 -0
- package/src/utils/models.spec.js +119 -0
- package/src/utils/skills.js +74 -0
- package/src/utils/skills.spec.js +168 -0
- package/src/utils/sources.js +48 -0
- package/src/utils/sources.spec.js +122 -0
- package/src/utils/stream.js +22 -0
- package/src/utils/stream.spec.js +55 -0
- package/src/utils/tools.js +47 -0
- package/src/utils/tools.spec.js +167 -0
- package/src/utils/wrap.js +148 -0
- package/src/utils/wrap.spec.js +359 -0
- package/src/validations.js +149 -27
- package/src/validations.spec.js +451 -53
- package/src/ai_model.js +0 -60
- package/src/ai_model.spec.js +0 -259
- package/src/ai_sdk.js +0 -92
- package/src/ai_sdk.spec.js +0 -564
- package/src/prompt/block_options.js +0 -58
- package/src/prompt/block_options.spec.js +0 -71
- package/src/prompt/blocks.js +0 -47
- package/src/prompt/blocks.spec.js +0 -63
- package/src/prompt/escape.spec.js +0 -159
- package/src/prompt/load_content.spec.js +0 -83
- package/src/prompt/loader_validation.spec.js +0 -128
- package/src/prompt/parser.js +0 -16
- package/src/prompt/parser.spec.js +0 -186
- package/src/prompt/prepare_text.js +0 -27
- package/src/prompt/prepare_text.spec.js +0 -141
- package/src/prompt/skill.js +0 -128
- package/src/prompt/skill.spec.js +0 -172
- package/src/utils/message.js +0 -3
- package/src/utils/message.spec.js +0 -29
- package/src/utils/response_wrappers.js +0 -100
- package/src/utils/response_wrappers.spec.js +0 -240
- package/src/utils/source_extraction.js +0 -53
- package/src/utils/source_extraction.spec.js +0 -194
- package/src/utils/trace.js +0 -19
- package/src/utils/trace.spec.js +0 -112
package/src/validations.spec.js
CHANGED
|
@@ -1,101 +1,310 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ValidationError } from '@outputai/core';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
parseGenerateTextArgs,
|
|
5
|
+
parseGenerateTextWithStreamingArgs,
|
|
6
|
+
parseStreamTextArgs,
|
|
7
|
+
parseGenerateImageArgs,
|
|
8
|
+
parseAgentArgs,
|
|
9
|
+
parseAgentGenerateArgs,
|
|
10
|
+
parseAgentGenerateWithStreamingArgs,
|
|
11
|
+
parseAgentStreamArgs
|
|
12
|
+
} from './validations.js';
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
14
|
+
const makeOutput = () => ( { name: 'object' } );
|
|
15
|
+
|
|
16
|
+
const searchTool = { description: 'Search' };
|
|
17
|
+
const stopWhen = () => false;
|
|
18
|
+
|
|
19
|
+
describe( 'parseGenerateTextArgs', () => {
|
|
20
|
+
it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
|
|
21
|
+
const abortSignal = AbortSignal.abort();
|
|
22
|
+
const tools = { search: searchTool };
|
|
23
|
+
const output = makeOutput();
|
|
24
|
+
|
|
25
|
+
expect( parseGenerateTextArgs( {
|
|
8
26
|
prompt: 'summary@v1',
|
|
9
|
-
variables: { topic: 'testing' },
|
|
27
|
+
variables: { topic: 'testing', count: 2, draft: false },
|
|
10
28
|
promptDir: '/prompts',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
tools,
|
|
30
|
+
output,
|
|
31
|
+
toolChoice: { type: 'tool', toolName: 'search' },
|
|
32
|
+
stopWhen,
|
|
33
|
+
abortSignal
|
|
34
|
+
} ) ).toEqual( {
|
|
35
|
+
promptFile: 'summary@v1',
|
|
36
|
+
variables: { topic: 'testing', count: 2, draft: false },
|
|
37
|
+
promptDir: '/prompts',
|
|
38
|
+
tools,
|
|
39
|
+
output,
|
|
40
|
+
toolChoice: { type: 'tool', toolName: 'search' },
|
|
41
|
+
stopWhen,
|
|
42
|
+
abortSignal
|
|
43
|
+
} );
|
|
14
44
|
} );
|
|
15
45
|
|
|
16
|
-
it( 'accepts
|
|
17
|
-
|
|
46
|
+
it( 'accepts an array of stopWhen functions', () => {
|
|
47
|
+
const parsed = parseGenerateTextArgs( {
|
|
18
48
|
prompt: 'summary@v1',
|
|
19
|
-
|
|
20
|
-
} )
|
|
49
|
+
stopWhen: [ stopWhen ]
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
expect( parsed.stopWhen ).toEqual( [ stopWhen ] );
|
|
21
53
|
} );
|
|
22
54
|
|
|
23
55
|
it( 'throws ValidationError with generateText prefix for invalid args', () => {
|
|
24
|
-
expect( () =>
|
|
56
|
+
expect( () => parseGenerateTextArgs( {
|
|
25
57
|
variables: { topic: 'testing' }
|
|
26
58
|
} ) ).toThrow( ValidationError );
|
|
27
59
|
|
|
28
|
-
expect( () =>
|
|
60
|
+
expect( () => parseGenerateTextArgs( {
|
|
29
61
|
prompt: 123
|
|
30
62
|
} ) ).toThrow( /Invalid generateText\(\) arguments/ );
|
|
31
|
-
} );
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
expect( () => validateGenerateTextArgs( {
|
|
64
|
+
expect( () => parseGenerateTextArgs( {
|
|
35
65
|
prompt: 'summary@v1',
|
|
36
66
|
promptDir: ''
|
|
37
67
|
} ) ).toThrow( ValidationError );
|
|
38
68
|
|
|
39
|
-
expect( () =>
|
|
69
|
+
expect( () => parseGenerateTextArgs( {
|
|
40
70
|
prompt: 'summary@v1',
|
|
41
|
-
|
|
71
|
+
abortSignal: {}
|
|
42
72
|
} ) ).toThrow( ValidationError );
|
|
73
|
+
} );
|
|
43
74
|
|
|
44
|
-
|
|
75
|
+
it( 'does not keep prompt-owned call arguments on the parsed result', () => {
|
|
76
|
+
expect( () => parseGenerateTextArgs( {
|
|
45
77
|
prompt: 'summary@v1',
|
|
46
|
-
|
|
78
|
+
skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
|
|
79
|
+
} ) ).toThrow( /skills must be set in the prompt file/ );
|
|
80
|
+
|
|
81
|
+
expect( () => parseGenerateTextArgs( {
|
|
82
|
+
prompt: 'summary@v1',
|
|
83
|
+
maxSteps: 4,
|
|
84
|
+
skills: [ './writer.md' ]
|
|
85
|
+
} ) ).toThrow( /must be set in the prompt file/ );
|
|
86
|
+
} );
|
|
87
|
+
|
|
88
|
+
it( 'throws ValidationError for unrecognized call arguments', () => {
|
|
89
|
+
expect( () => parseGenerateTextArgs( {
|
|
90
|
+
prompt: 'summary@v1',
|
|
91
|
+
temperature: 0.2
|
|
92
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
93
|
+
|
|
94
|
+
expect( () => parseGenerateTextArgs( {
|
|
95
|
+
prompt: 'summary@v1',
|
|
96
|
+
maxRetries: 2
|
|
97
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
98
|
+
|
|
99
|
+
expect( () => parseGenerateTextArgs( {
|
|
100
|
+
prompt: 'summary@v1',
|
|
101
|
+
onChunk: () => {}
|
|
102
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
it( 'accepts nested objects and arrays as variables', () => {
|
|
106
|
+
const variables = {
|
|
107
|
+
company: { name: 'Acme', industries: [ 'SaaS', 'AI' ] },
|
|
108
|
+
criteria: [ { name: 'Accuracy', required: true } ]
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
expect( parseGenerateTextArgs( {
|
|
112
|
+
prompt: 'summary@v1',
|
|
113
|
+
variables
|
|
114
|
+
} ).variables ).toEqual( variables );
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
it( 'throws ValidationError for invalid allowlisted field shapes', () => {
|
|
118
|
+
expect( () => parseGenerateTextArgs( {
|
|
119
|
+
prompt: 'summary@v1',
|
|
120
|
+
variables: [ 'not a record' ]
|
|
121
|
+
} ) ).toThrow( ValidationError );
|
|
122
|
+
|
|
123
|
+
expect( () => parseGenerateTextArgs( {
|
|
124
|
+
prompt: 'summary@v1',
|
|
125
|
+
tools: { search: true }
|
|
126
|
+
} ) ).toThrow( ValidationError );
|
|
127
|
+
|
|
128
|
+
expect( () => parseGenerateTextArgs( {
|
|
129
|
+
prompt: 'summary@v1',
|
|
130
|
+
output: true
|
|
131
|
+
} ) ).toThrow( ValidationError );
|
|
132
|
+
|
|
133
|
+
expect( () => parseGenerateTextArgs( {
|
|
134
|
+
prompt: 'summary@v1',
|
|
135
|
+
stopWhen: { type: 'custom-stop' }
|
|
136
|
+
} ) ).toThrow( ValidationError );
|
|
137
|
+
|
|
138
|
+
expect( () => parseGenerateTextArgs( {
|
|
139
|
+
prompt: 'summary@v1',
|
|
140
|
+
toolChoice: 'maybe'
|
|
47
141
|
} ) ).toThrow( ValidationError );
|
|
48
142
|
} );
|
|
49
|
-
} );
|
|
50
143
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
expect( () => validateStreamTextArgs( {
|
|
144
|
+
it( 'accepts opaque tools and output objects', () => {
|
|
145
|
+
expect( parseGenerateTextArgs( {
|
|
54
146
|
prompt: 'summary@v1',
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
147
|
+
tools: { search: { description: 'Search' } },
|
|
148
|
+
output: { type: 'object' }
|
|
149
|
+
} ) ).toMatchObject( {
|
|
150
|
+
tools: { search: { description: 'Search' } },
|
|
151
|
+
output: { type: 'object' }
|
|
152
|
+
} );
|
|
60
153
|
} );
|
|
61
154
|
|
|
62
|
-
it( '
|
|
63
|
-
|
|
155
|
+
it( 'preserves class-based tool and output identities', () => {
|
|
156
|
+
class TestTool {
|
|
157
|
+
#result = 'searched';
|
|
158
|
+
|
|
159
|
+
execute() {
|
|
160
|
+
return this.#result;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class TestOutput {
|
|
165
|
+
#result = 'parsed';
|
|
166
|
+
|
|
167
|
+
parseCompleteOutput() {
|
|
168
|
+
return this.#result;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const tool = new TestTool();
|
|
173
|
+
const output = new TestOutput();
|
|
174
|
+
const parsed = parseGenerateTextArgs( {
|
|
64
175
|
prompt: 'summary@v1',
|
|
65
|
-
|
|
66
|
-
|
|
176
|
+
tools: { search: tool },
|
|
177
|
+
output
|
|
178
|
+
} );
|
|
179
|
+
|
|
180
|
+
expect( parsed.tools.search ).toBe( tool );
|
|
181
|
+
expect( parsed.tools.search.execute() ).toBe( 'searched' );
|
|
182
|
+
expect( parsed.output ).toBe( output );
|
|
183
|
+
expect( parsed.output.parseCompleteOutput() ).toBe( 'parsed' );
|
|
67
184
|
} );
|
|
185
|
+
} );
|
|
68
186
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
187
|
+
describe( 'parseStreamTextArgs', () => {
|
|
188
|
+
it( 'keeps stream callbacks on the parsed result', () => {
|
|
189
|
+
const parsed = parseStreamTextArgs( {
|
|
190
|
+
prompt: 'summary@v1',
|
|
191
|
+
variables: { topic: 'testing' },
|
|
192
|
+
promptDir: '/prompts',
|
|
193
|
+
onFinish: () => {},
|
|
194
|
+
onError: () => {},
|
|
195
|
+
onChunk: () => {}
|
|
196
|
+
} );
|
|
197
|
+
|
|
198
|
+
expect( parsed.promptFile ).toBe( 'summary@v1' );
|
|
199
|
+
expect( parsed.variables ).toEqual( { topic: 'testing' } );
|
|
200
|
+
expect( parsed.promptDir ).toBe( '/prompts' );
|
|
201
|
+
expect( parsed.onFinish ).toBeTypeOf( 'function' );
|
|
202
|
+
expect( parsed.onError ).toBeTypeOf( 'function' );
|
|
203
|
+
expect( parsed.onChunk ).toBeTypeOf( 'function' );
|
|
72
204
|
} );
|
|
73
205
|
|
|
74
|
-
it( 'throws ValidationError
|
|
75
|
-
expect( () =>
|
|
206
|
+
it( 'throws ValidationError with streamText prefix for invalid args', () => {
|
|
207
|
+
expect( () => parseStreamTextArgs( {} ) ).toThrow( ValidationError );
|
|
208
|
+
expect( () => parseStreamTextArgs( { prompt: null } ) ).toThrow( /Invalid streamText\(\) arguments/ );
|
|
209
|
+
|
|
210
|
+
expect( () => parseStreamTextArgs( {
|
|
76
211
|
prompt: 'summary@v1',
|
|
77
212
|
promptDir: ''
|
|
78
213
|
} ) ).toThrow( ValidationError );
|
|
214
|
+
} );
|
|
79
215
|
|
|
80
|
-
|
|
216
|
+
it( 'throws ValidationError for invalid callbacks', () => {
|
|
217
|
+
expect( () => parseStreamTextArgs( {
|
|
81
218
|
prompt: 'summary@v1',
|
|
82
|
-
|
|
219
|
+
onFinish: 'not a function'
|
|
83
220
|
} ) ).toThrow( ValidationError );
|
|
84
221
|
|
|
85
|
-
expect( () =>
|
|
222
|
+
expect( () => parseStreamTextArgs( {
|
|
86
223
|
prompt: 'summary@v1',
|
|
87
|
-
|
|
224
|
+
onError: {}
|
|
88
225
|
} ) ).toThrow( ValidationError );
|
|
226
|
+
|
|
227
|
+
expect( () => parseStreamTextArgs( {
|
|
228
|
+
prompt: 'summary@v1',
|
|
229
|
+
onChunk: 'not a function'
|
|
230
|
+
} ) ).toThrow( ValidationError );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'throws ValidationError for unrecognized call arguments', () => {
|
|
234
|
+
expect( () => parseStreamTextArgs( {
|
|
235
|
+
prompt: 'summary@v1',
|
|
236
|
+
temperature: 0.2
|
|
237
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
238
|
+
|
|
239
|
+
expect( () => parseStreamTextArgs( {
|
|
240
|
+
prompt: 'summary@v1',
|
|
241
|
+
skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
|
|
242
|
+
} ) ).toThrow( /skills must be set in the prompt file/ );
|
|
243
|
+
|
|
244
|
+
expect( () => parseStreamTextArgs( {
|
|
245
|
+
prompt: 'summary@v1',
|
|
246
|
+
maxSteps: 1.5
|
|
247
|
+
} ) ).toThrow( /maxSteps must be set in the prompt file/ );
|
|
248
|
+
} );
|
|
249
|
+
} );
|
|
250
|
+
|
|
251
|
+
describe( 'parseGenerateTextWithStreamingArgs', () => {
|
|
252
|
+
it( 'keeps onChunk and omits streamText-only callbacks', () => {
|
|
253
|
+
const parsed = parseGenerateTextWithStreamingArgs( {
|
|
254
|
+
prompt: 'summary@v1',
|
|
255
|
+
onChunk: () => {}
|
|
256
|
+
} );
|
|
257
|
+
|
|
258
|
+
expect( parsed.promptFile ).toBe( 'summary@v1' );
|
|
259
|
+
expect( parsed.onChunk ).toBeTypeOf( 'function' );
|
|
260
|
+
|
|
261
|
+
expect( () => parseGenerateTextWithStreamingArgs( {
|
|
262
|
+
prompt: 'summary@v1',
|
|
263
|
+
onFinish: () => {}
|
|
264
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
265
|
+
} );
|
|
266
|
+
|
|
267
|
+
it( 'uses the generateTextWithStreaming error prefix', () => {
|
|
268
|
+
expect( () => parseGenerateTextWithStreamingArgs( {
|
|
269
|
+
prompt: 'summary@v1',
|
|
270
|
+
maxSteps: 0
|
|
271
|
+
} ) ).toThrow( /Invalid generateTextWithStreaming\(\) arguments[\s\S]*maxSteps must be set in the prompt file/ );
|
|
89
272
|
} );
|
|
90
273
|
} );
|
|
91
274
|
|
|
92
|
-
describe( '
|
|
275
|
+
describe( 'parseGenerateImageArgs', () => {
|
|
276
|
+
it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
|
|
277
|
+
const images = [ Buffer.from( 'image-bytes' ) ];
|
|
278
|
+
const mask = Buffer.from( 'mask-bytes' );
|
|
279
|
+
const abortSignal = AbortSignal.abort();
|
|
280
|
+
|
|
281
|
+
expect( parseGenerateImageArgs( {
|
|
282
|
+
prompt: 'image@v1',
|
|
283
|
+
variables: { scene: 'race cars' },
|
|
284
|
+
promptDir: '/prompts',
|
|
285
|
+
images,
|
|
286
|
+
mask,
|
|
287
|
+
abortSignal
|
|
288
|
+
} ) ).toEqual( {
|
|
289
|
+
promptFile: 'image@v1',
|
|
290
|
+
variables: { scene: 'race cars' },
|
|
291
|
+
promptDir: '/prompts',
|
|
292
|
+
images,
|
|
293
|
+
mask,
|
|
294
|
+
abortSignal
|
|
295
|
+
} );
|
|
296
|
+
} );
|
|
297
|
+
|
|
93
298
|
it( 'accepts text-to-image args without images or mask', () => {
|
|
94
|
-
expect( (
|
|
299
|
+
expect( parseGenerateImageArgs( {
|
|
95
300
|
prompt: 'image@v1',
|
|
96
301
|
variables: { topic: 'race cars' },
|
|
97
302
|
promptDir: '/prompts'
|
|
98
|
-
} ) ).
|
|
303
|
+
} ) ).toEqual( {
|
|
304
|
+
promptFile: 'image@v1',
|
|
305
|
+
variables: { topic: 'race cars' },
|
|
306
|
+
promptDir: '/prompts'
|
|
307
|
+
} );
|
|
99
308
|
} );
|
|
100
309
|
|
|
101
310
|
it( 'accepts all supported image input shapes', () => {
|
|
@@ -105,7 +314,7 @@ describe( 'validateGenerateImageArgs', () => {
|
|
|
105
314
|
const paddedBase64 = 'aW1hZ2UtYnl0ZXM=';
|
|
106
315
|
const unpaddedBase64 = 'aW1hZ2U';
|
|
107
316
|
|
|
108
|
-
expect( () =>
|
|
317
|
+
expect( () => parseGenerateImageArgs( {
|
|
109
318
|
prompt: 'image@v1',
|
|
110
319
|
images: [
|
|
111
320
|
buffer,
|
|
@@ -123,21 +332,21 @@ describe( 'validateGenerateImageArgs', () => {
|
|
|
123
332
|
} );
|
|
124
333
|
|
|
125
334
|
it( 'throws ValidationError for invalid image args', () => {
|
|
126
|
-
expect( () =>
|
|
335
|
+
expect( () => parseGenerateImageArgs( {
|
|
127
336
|
prompt: ''
|
|
128
337
|
} ) ).toThrow( /Invalid generateImage\(\) arguments/ );
|
|
129
338
|
|
|
130
|
-
expect( () =>
|
|
339
|
+
expect( () => parseGenerateImageArgs( {
|
|
131
340
|
prompt: 'image@v1',
|
|
132
341
|
images: []
|
|
133
342
|
} ) ).toThrow( ValidationError );
|
|
134
343
|
|
|
135
|
-
expect( () =>
|
|
344
|
+
expect( () => parseGenerateImageArgs( {
|
|
136
345
|
prompt: 'image@v1',
|
|
137
346
|
images: [ { data: null } ]
|
|
138
347
|
} ) ).toThrow( ValidationError );
|
|
139
348
|
|
|
140
|
-
expect( () =>
|
|
349
|
+
expect( () => parseGenerateImageArgs( {
|
|
141
350
|
prompt: 'image@v1',
|
|
142
351
|
images: [ { data: 'aW1hZ2U=', mediaType: '' } ]
|
|
143
352
|
} ) ).toThrow( ValidationError );
|
|
@@ -150,7 +359,7 @@ describe( 'validateGenerateImageArgs', () => {
|
|
|
150
359
|
'not base64',
|
|
151
360
|
'abcde'
|
|
152
361
|
] ) {
|
|
153
|
-
expect( () =>
|
|
362
|
+
expect( () => parseGenerateImageArgs( {
|
|
154
363
|
prompt: 'image@v1',
|
|
155
364
|
images: [ image ]
|
|
156
365
|
} ) ).toThrow( /Image strings must be raw base64 data/ );
|
|
@@ -158,9 +367,198 @@ describe( 'validateGenerateImageArgs', () => {
|
|
|
158
367
|
} );
|
|
159
368
|
|
|
160
369
|
it( 'requires images when mask is provided', () => {
|
|
161
|
-
expect( () =>
|
|
370
|
+
expect( () => parseGenerateImageArgs( {
|
|
162
371
|
prompt: 'image@v1',
|
|
163
372
|
mask: Buffer.from( 'mask-bytes' )
|
|
164
373
|
} ) ).toThrow( /mask requires images/ );
|
|
165
374
|
} );
|
|
375
|
+
|
|
376
|
+
it( 'throws ValidationError for unrecognized or prompt-owned call arguments', () => {
|
|
377
|
+
expect( () => parseGenerateImageArgs( {
|
|
378
|
+
prompt: 'image@v1',
|
|
379
|
+
size: '1024x1024',
|
|
380
|
+
n: 2
|
|
381
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
382
|
+
|
|
383
|
+
expect( () => parseGenerateImageArgs( {
|
|
384
|
+
prompt: 'image@v1',
|
|
385
|
+
aspectRatio: '1:1'
|
|
386
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
387
|
+
|
|
388
|
+
expect( () => parseGenerateImageArgs( {
|
|
389
|
+
prompt: 'image@v1',
|
|
390
|
+
providerOptions: { openai: { quality: 'high' } }
|
|
391
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
392
|
+
|
|
393
|
+
expect( () => parseGenerateImageArgs( {
|
|
394
|
+
prompt: 'image@v1',
|
|
395
|
+
maxSteps: 4
|
|
396
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
397
|
+
} );
|
|
398
|
+
} );
|
|
399
|
+
|
|
400
|
+
describe( 'parseAgentArgs', () => {
|
|
401
|
+
it( 'renames prompt to promptFile and keeps allowlisted leftovers', () => {
|
|
402
|
+
const tools = { search: searchTool };
|
|
403
|
+
const output = makeOutput();
|
|
404
|
+
const messageStore = { getMessages() {}, addMessages() {} };
|
|
405
|
+
|
|
406
|
+
const parsed = parseAgentArgs( {
|
|
407
|
+
prompt: 'summary@v1',
|
|
408
|
+
variables: { topic: 'testing' },
|
|
409
|
+
promptDir: '/prompts',
|
|
410
|
+
tools,
|
|
411
|
+
output,
|
|
412
|
+
stopWhen,
|
|
413
|
+
messageStore
|
|
414
|
+
} );
|
|
415
|
+
|
|
416
|
+
expect( parsed ).toEqual( {
|
|
417
|
+
promptFile: 'summary@v1',
|
|
418
|
+
variables: { topic: 'testing' },
|
|
419
|
+
promptDir: '/prompts',
|
|
420
|
+
tools,
|
|
421
|
+
output,
|
|
422
|
+
stopWhen,
|
|
423
|
+
messageStore
|
|
424
|
+
} );
|
|
425
|
+
expect( parsed.messageStore ).toBe( messageStore );
|
|
426
|
+
} );
|
|
427
|
+
|
|
428
|
+
it( 'preserves class-based message store identity', () => {
|
|
429
|
+
class TestMessageStore {
|
|
430
|
+
#messages = [];
|
|
431
|
+
|
|
432
|
+
getMessages() {
|
|
433
|
+
return this.#messages;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
addMessages( messages ) {
|
|
437
|
+
this.#messages.push( ...messages );
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const messageStore = new TestMessageStore();
|
|
442
|
+
const parsed = parseAgentArgs( {
|
|
443
|
+
prompt: 'summary@v1',
|
|
444
|
+
messageStore
|
|
445
|
+
} );
|
|
446
|
+
const message = { role: 'user', content: 'Hello' };
|
|
447
|
+
|
|
448
|
+
parsed.messageStore.addMessages( [ message ] );
|
|
449
|
+
|
|
450
|
+
expect( parsed.messageStore ).toBe( messageStore );
|
|
451
|
+
expect( parsed.messageStore.getMessages() ).toEqual( [ message ] );
|
|
452
|
+
} );
|
|
453
|
+
|
|
454
|
+
it( 'throws ValidationError with Agent prefix for invalid args', () => {
|
|
455
|
+
expect( () => parseAgentArgs( {} ) ).toThrow( ValidationError );
|
|
456
|
+
expect( () => parseAgentArgs( { prompt: '' } ) ).toThrow( /Invalid Agent\(\) arguments/ );
|
|
457
|
+
|
|
458
|
+
expect( () => parseAgentArgs( {
|
|
459
|
+
prompt: 'summary@v1',
|
|
460
|
+
promptDir: ''
|
|
461
|
+
} ) ).toThrow( ValidationError );
|
|
462
|
+
} );
|
|
463
|
+
|
|
464
|
+
it( 'does not keep prompt-owned call arguments on the parsed result', () => {
|
|
465
|
+
expect( () => parseAgentArgs( {
|
|
466
|
+
prompt: 'summary@v1',
|
|
467
|
+
skills: [ { name: 'style', description: 'Style', instructions: '# Style' } ]
|
|
468
|
+
} ) ).toThrow( /skills must be set in the prompt file/ );
|
|
469
|
+
|
|
470
|
+
expect( () => parseAgentArgs( {
|
|
471
|
+
prompt: 'summary@v1',
|
|
472
|
+
maxSteps: 4,
|
|
473
|
+
skills: [ './writer.md' ]
|
|
474
|
+
} ) ).toThrow( /must be set in the prompt file/ );
|
|
475
|
+
} );
|
|
476
|
+
|
|
477
|
+
it( 'throws ValidationError for unrecognized constructor arguments', () => {
|
|
478
|
+
expect( () => parseAgentArgs( {
|
|
479
|
+
prompt: 'summary@v1',
|
|
480
|
+
temperature: 0.2
|
|
481
|
+
} ) ).toThrow( /unrecognized key/i );
|
|
482
|
+
} );
|
|
483
|
+
|
|
484
|
+
it( 'throws ValidationError for an invalid message store', () => {
|
|
485
|
+
expect( () => parseAgentArgs( {
|
|
486
|
+
prompt: 'summary@v1',
|
|
487
|
+
messageStore: { getMessages() {} }
|
|
488
|
+
} ) ).toThrow( ValidationError );
|
|
489
|
+
} );
|
|
490
|
+
} );
|
|
491
|
+
|
|
492
|
+
describe( 'parseAgentGenerateArgs', () => {
|
|
493
|
+
it( 'defaults omitted args and messages to an empty list', () => {
|
|
494
|
+
expect( parseAgentGenerateArgs() ).toEqual( { messages: [] } );
|
|
495
|
+
expect( parseAgentGenerateArgs( undefined ) ).toEqual( { messages: [] } );
|
|
496
|
+
expect( parseAgentGenerateArgs( {} ) ).toEqual( { messages: [] } );
|
|
497
|
+
} );
|
|
498
|
+
|
|
499
|
+
it( 'keeps allowlisted generate options', () => {
|
|
500
|
+
const messages = [ { role: 'user', content: 'Hello' } ];
|
|
501
|
+
const abortSignal = AbortSignal.abort();
|
|
502
|
+
|
|
503
|
+
expect( parseAgentGenerateArgs( {
|
|
504
|
+
messages,
|
|
505
|
+
abortSignal,
|
|
506
|
+
toolChoice: 'required'
|
|
507
|
+
} ) ).toEqual( {
|
|
508
|
+
messages,
|
|
509
|
+
abortSignal,
|
|
510
|
+
toolChoice: 'required'
|
|
511
|
+
} );
|
|
512
|
+
} );
|
|
513
|
+
|
|
514
|
+
it( 'throws ValidationError for unrecognized or prompt-owned call arguments', () => {
|
|
515
|
+
expect( () => parseAgentGenerateArgs( { temperature: 0.2 } ) ).toThrow( /unrecognized key/i );
|
|
516
|
+
expect( () => parseAgentGenerateArgs( { onChunk: () => {} } ) ).toThrow( /unrecognized key/i );
|
|
517
|
+
expect( () => parseAgentGenerateArgs( { maxSteps: 4 } ) ).toThrow( /maxSteps must be set in the prompt file/ );
|
|
518
|
+
} );
|
|
519
|
+
|
|
520
|
+
it( 'throws ValidationError for invalid messages or toolChoice', () => {
|
|
521
|
+
expect( () => parseAgentGenerateArgs( {
|
|
522
|
+
messages: [ { content: 'Hello' } ]
|
|
523
|
+
} ) ).toThrow( ValidationError );
|
|
524
|
+
|
|
525
|
+
expect( () => parseAgentGenerateArgs( {
|
|
526
|
+
messages: [ { role: 'moderator', content: 'Hello' } ]
|
|
527
|
+
} ) ).toThrow( ValidationError );
|
|
528
|
+
|
|
529
|
+
expect( () => parseAgentGenerateArgs( {
|
|
530
|
+
toolChoice: { type: 'tool' }
|
|
531
|
+
} ) ).toThrow( ValidationError );
|
|
532
|
+
} );
|
|
533
|
+
} );
|
|
534
|
+
|
|
535
|
+
describe( 'parseAgentGenerateWithStreamingArgs', () => {
|
|
536
|
+
it( 'defaults omitted args and accepts onChunk', () => {
|
|
537
|
+
expect( parseAgentGenerateWithStreamingArgs() ).toEqual( { messages: [] } );
|
|
538
|
+
|
|
539
|
+
const parsed = parseAgentGenerateWithStreamingArgs( { onChunk: () => {} } );
|
|
540
|
+
expect( parsed.messages ).toEqual( [] );
|
|
541
|
+
expect( parsed.onChunk ).toBeTypeOf( 'function' );
|
|
542
|
+
} );
|
|
543
|
+
|
|
544
|
+
it( 'rejects stream-only callbacks', () => {
|
|
545
|
+
expect( () => parseAgentGenerateWithStreamingArgs( { onFinish: () => {} } ) ).toThrow( /unrecognized key/i );
|
|
546
|
+
} );
|
|
547
|
+
} );
|
|
548
|
+
|
|
549
|
+
describe( 'parseAgentStreamArgs', () => {
|
|
550
|
+
it( 'defaults omitted args and keeps stream callbacks', () => {
|
|
551
|
+
expect( parseAgentStreamArgs() ).toEqual( { messages: [] } );
|
|
552
|
+
|
|
553
|
+
const parsed = parseAgentStreamArgs( {
|
|
554
|
+
onFinish: () => {},
|
|
555
|
+
onError: () => {},
|
|
556
|
+
onChunk: () => {}
|
|
557
|
+
} );
|
|
558
|
+
|
|
559
|
+
expect( parsed.messages ).toEqual( [] );
|
|
560
|
+
expect( parsed.onFinish ).toBeTypeOf( 'function' );
|
|
561
|
+
expect( parsed.onError ).toBeTypeOf( 'function' );
|
|
562
|
+
expect( parsed.onChunk ).toBeTypeOf( 'function' );
|
|
563
|
+
} );
|
|
166
564
|
} );
|
package/src/ai_model.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { ValidationError } from '@outputai/core';
|
|
2
|
-
import { getProvider } from './ai_provider.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Load a text model from a loaded prompt config.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
|
|
8
|
-
* @returns {unknown} AI SDK language model
|
|
9
|
-
*/
|
|
10
|
-
export const loadTextModel = prompt => getProvider( prompt.config.provider )( prompt.config.model );
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Load an image model from a loaded prompt config.
|
|
14
|
-
*
|
|
15
|
-
* @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
|
|
16
|
-
* @returns {unknown} AI SDK image model
|
|
17
|
-
*/
|
|
18
|
-
export const loadImageModel = prompt => {
|
|
19
|
-
const { provider: providerName, model } = prompt.config;
|
|
20
|
-
const provider = getProvider( prompt.config.provider );
|
|
21
|
-
|
|
22
|
-
const imageModelFactory = provider.image ?? provider.imageModel;
|
|
23
|
-
if ( typeof imageModelFactory !== 'function' ) {
|
|
24
|
-
throw new ValidationError( `Provider "${providerName}" does not support image models.` );
|
|
25
|
-
}
|
|
26
|
-
return imageModelFactory( model );
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Load provider-specific tools configured in a prompt.
|
|
31
|
-
*
|
|
32
|
-
* @param {object} prompt - Loaded prompt object with `config.provider` and optional `config.tools`
|
|
33
|
-
* @returns {Record<string, unknown> | null} AI SDK tools, or null when none are configured
|
|
34
|
-
*/
|
|
35
|
-
export const loadTools = prompt => {
|
|
36
|
-
const { tools: promptTools, provider: providerName } = prompt.config;
|
|
37
|
-
|
|
38
|
-
if ( Object.keys( promptTools ?? {} ).length === 0 ) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const provider = getProvider( providerName );
|
|
43
|
-
|
|
44
|
-
if ( !provider.tools ) {
|
|
45
|
-
throw new ValidationError( `Provider "${providerName}" does not support provider-specific tools.` );
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const supportedTools = Object.keys( provider.tools );
|
|
49
|
-
const invalidTools = Object.keys( promptTools ).filter( name => !supportedTools.includes( name ) );
|
|
50
|
-
|
|
51
|
-
if ( invalidTools.length > 0 ) {
|
|
52
|
-
throw new ValidationError( `Invalid tool(s) ${invalidTools.join( ', ' )} for provider "${providerName}". \
|
|
53
|
-
Available: ${supportedTools.join( ', ' )}.` );
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// load all tools and return in an object
|
|
57
|
-
return Object.fromEntries(
|
|
58
|
-
Object.entries( promptTools ).map( ( [ name, args ] ) => [ name, provider.tools[name]( args ) ] )
|
|
59
|
-
);
|
|
60
|
-
};
|