@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.
- package/package.json +7 -3
- package/src/agent.js +7 -2
- package/src/generate.js +1 -0
- package/src/index.d.ts +14 -6
- package/src/utils/wrap.js +35 -2
- package/src/agent.spec.js +0 -595
- package/src/ai_provider.spec.js +0 -217
- package/src/ai_sdk_options.spec.js +0 -390
- package/src/fixtures/image_response_v7_google_vertex.js +0 -72
- package/src/fixtures/image_response_v7_openai.js +0 -77
- package/src/fixtures/models_api_light.json +0 -675
- package/src/fixtures/models_api_v7_subset.json +0 -259
- package/src/fixtures/stream_response_v7_anthropic.js +0 -389
- package/src/fixtures/stream_response_v7_google_vertex.js +0 -462
- package/src/fixtures/stream_response_v7_openai.js +0 -353
- package/src/fixtures/stream_response_v7_perplexity.js +0 -671
- package/src/fixtures/text_response_v7_anthropic.js +0 -359
- package/src/fixtures/text_response_v7_anthropic_cache.js +0 -719
- package/src/fixtures/text_response_v7_google_vertex.js +0 -435
- package/src/fixtures/text_response_v7_openai.js +0 -347
- package/src/fixtures/text_response_v7_perplexity.js +0 -764
- package/src/generate.spec.js +0 -525
- package/src/prompt/content.spec.js +0 -208
- package/src/prompt/interpolations.spec.js +0 -109
- package/src/prompt/loader.full.spec.js +0 -309
- package/src/prompt/loader.spec.js +0 -258
- package/src/prompt/markup/attributes.spec.js +0 -132
- package/src/prompt/markup/nodes.spec.js +0 -333
- package/src/prompt/markup/tokenizer.spec.js +0 -149
- package/src/prompt/markup/tokens.spec.js +0 -250
- package/src/prompt/validations.spec.js +0 -812
- package/src/utils/cost.full.spec.js +0 -272
- package/src/utils/cost.spec.js +0 -290
- package/src/utils/error_handler.spec.js +0 -230
- package/src/utils/file.spec.js +0 -89
- package/src/utils/image.spec.js +0 -20
- package/src/utils/legacy_cost_attribute.spec.js +0 -278
- package/src/utils/models.spec.js +0 -119
- package/src/utils/models_pricing.spec.js +0 -162
- package/src/utils/skills.spec.js +0 -168
- package/src/utils/sources.spec.js +0 -122
- package/src/utils/stream.spec.js +0 -55
- package/src/utils/tools.spec.js +0 -167
- package/src/utils/usage.full.spec.js +0 -215
- package/src/utils/usage.spec.js +0 -246
- package/src/utils/wrap.spec.js +0 -467
- package/src/validations.spec.js +0 -631
|
@@ -1,812 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ValidationError } from '@outputai/core';
|
|
3
|
-
import { parsePromptSchema } from './validations.js';
|
|
4
|
-
|
|
5
|
-
const parse = prompt => parsePromptSchema( { fileDir: '/prompts', ...prompt } );
|
|
6
|
-
|
|
7
|
-
describe( 'parsePromptSchema', () => {
|
|
8
|
-
it( 'should validate a correct prompt with all required fields', () => {
|
|
9
|
-
const validPrompt = {
|
|
10
|
-
name: 'test-prompt',
|
|
11
|
-
config: {
|
|
12
|
-
provider: 'anthropic',
|
|
13
|
-
model: 'claude-3-opus-20240229',
|
|
14
|
-
temperature: 0.7,
|
|
15
|
-
maxOutputTokens: 1000,
|
|
16
|
-
topP: 0.9,
|
|
17
|
-
topK: 40,
|
|
18
|
-
presencePenalty: 0.2,
|
|
19
|
-
frequencyPenalty: 0.3,
|
|
20
|
-
stopSequences: [ 'END' ],
|
|
21
|
-
seed: 42
|
|
22
|
-
},
|
|
23
|
-
messages: [
|
|
24
|
-
{
|
|
25
|
-
role: 'user',
|
|
26
|
-
content: 'Hello, world!'
|
|
27
|
-
}
|
|
28
|
-
]
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
expect( () => parse( validPrompt ) ).not.toThrow();
|
|
32
|
-
} );
|
|
33
|
-
|
|
34
|
-
it.each( [ 'temperature', 'topK', 'topP' ] )( 'should reject a negative %s', field => {
|
|
35
|
-
expect( () => parse( {
|
|
36
|
-
name: `negative-${field}`,
|
|
37
|
-
config: {
|
|
38
|
-
provider: 'anthropic',
|
|
39
|
-
model: 'claude-3-opus-20240229',
|
|
40
|
-
[field]: -0.1
|
|
41
|
-
},
|
|
42
|
-
messages: [ { role: 'user', content: 'Hello' } ]
|
|
43
|
-
} ) ).toThrow( ValidationError );
|
|
44
|
-
} );
|
|
45
|
-
|
|
46
|
-
it( 'should validate a minimal prompt with only required fields', () => {
|
|
47
|
-
const minimalPrompt = {
|
|
48
|
-
name: 'minimal-prompt',
|
|
49
|
-
config: {
|
|
50
|
-
provider: 'openai',
|
|
51
|
-
model: 'gpt-4'
|
|
52
|
-
},
|
|
53
|
-
messages: [
|
|
54
|
-
{
|
|
55
|
-
role: 'system',
|
|
56
|
-
content: 'You are a helpful assistant.'
|
|
57
|
-
}
|
|
58
|
-
]
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
expect( parse( minimalPrompt ).config.skills ).toEqual( [] );
|
|
62
|
-
expect( parse( minimalPrompt ).config.maxSteps ).toBe( 10 );
|
|
63
|
-
expect( parse( minimalPrompt ).variables ).toEqual( {} );
|
|
64
|
-
} );
|
|
65
|
-
|
|
66
|
-
it( 'should copy deprecated maxTokens to maxOutputTokens', () => {
|
|
67
|
-
const result = parse( {
|
|
68
|
-
name: 'deprecated-max-tokens',
|
|
69
|
-
config: {
|
|
70
|
-
provider: 'openai',
|
|
71
|
-
model: 'gpt-4',
|
|
72
|
-
maxTokens: 1000
|
|
73
|
-
},
|
|
74
|
-
messages: [ { role: 'user', content: 'Hello' } ]
|
|
75
|
-
} );
|
|
76
|
-
|
|
77
|
-
expect( result.config.maxTokens ).toBe( 1000 );
|
|
78
|
-
expect( result.config.maxOutputTokens ).toBe( 1000 );
|
|
79
|
-
} );
|
|
80
|
-
|
|
81
|
-
it( 'should keep maxOutputTokens when both token limit keys are set', () => {
|
|
82
|
-
const result = parse( {
|
|
83
|
-
name: 'both-token-limits',
|
|
84
|
-
config: {
|
|
85
|
-
provider: 'openai',
|
|
86
|
-
model: 'gpt-4',
|
|
87
|
-
maxTokens: 1000,
|
|
88
|
-
maxOutputTokens: 2000
|
|
89
|
-
},
|
|
90
|
-
messages: [ { role: 'user', content: 'Hello' } ]
|
|
91
|
-
} );
|
|
92
|
-
|
|
93
|
-
expect( result.config.maxTokens ).toBe( 1000 );
|
|
94
|
-
expect( result.config.maxOutputTokens ).toBe( 2000 );
|
|
95
|
-
} );
|
|
96
|
-
|
|
97
|
-
it( 'should reject an unsupported message role', () => {
|
|
98
|
-
expect( () => parse( {
|
|
99
|
-
name: 'invalid-role',
|
|
100
|
-
config: {
|
|
101
|
-
provider: 'openai',
|
|
102
|
-
model: 'gpt-4'
|
|
103
|
-
},
|
|
104
|
-
messages: [ {
|
|
105
|
-
role: 'developer',
|
|
106
|
-
content: 'You are a helpful assistant.'
|
|
107
|
-
} ]
|
|
108
|
-
} ) ).toThrow( /messages\[0\]\.role/ );
|
|
109
|
-
} );
|
|
110
|
-
|
|
111
|
-
it( 'should validate a prompt with thinking providerOptions', () => {
|
|
112
|
-
const promptWithThinking = {
|
|
113
|
-
name: 'thinking-prompt',
|
|
114
|
-
config: {
|
|
115
|
-
provider: 'anthropic',
|
|
116
|
-
model: 'claude-3-5-sonnet-20241022',
|
|
117
|
-
providerOptions: {
|
|
118
|
-
thinking: {
|
|
119
|
-
type: 'enabled',
|
|
120
|
-
budgetTokens: 5000
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
messages: [
|
|
125
|
-
{
|
|
126
|
-
role: 'user',
|
|
127
|
-
content: 'Solve this problem.'
|
|
128
|
-
}
|
|
129
|
-
]
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
expect( () => parse( promptWithThinking ) ).not.toThrow();
|
|
133
|
-
} );
|
|
134
|
-
|
|
135
|
-
it( 'should validate a prompt with thinking type disabled', () => {
|
|
136
|
-
const promptWithThinkingDisabled = {
|
|
137
|
-
name: 'thinking-disabled-prompt',
|
|
138
|
-
config: {
|
|
139
|
-
provider: 'anthropic',
|
|
140
|
-
model: 'claude-3-5-sonnet-20241022',
|
|
141
|
-
providerOptions: {
|
|
142
|
-
thinking: {
|
|
143
|
-
type: 'disabled'
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
messages: [
|
|
148
|
-
{
|
|
149
|
-
role: 'user',
|
|
150
|
-
content: 'Simple task.'
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
expect( () => parse( promptWithThinkingDisabled ) ).not.toThrow();
|
|
156
|
-
} );
|
|
157
|
-
|
|
158
|
-
it( 'should validate a prompt with thinking without budgetTokens', () => {
|
|
159
|
-
const promptWithThinkingNoBudget = {
|
|
160
|
-
name: 'thinking-no-budget',
|
|
161
|
-
config: {
|
|
162
|
-
provider: 'anthropic',
|
|
163
|
-
model: 'claude-3-5-sonnet-20241022',
|
|
164
|
-
providerOptions: {
|
|
165
|
-
thinking: {
|
|
166
|
-
type: 'enabled'
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
messages: [
|
|
171
|
-
{
|
|
172
|
-
role: 'user',
|
|
173
|
-
content: 'Think about this.'
|
|
174
|
-
}
|
|
175
|
-
]
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
expect( () => parse( promptWithThinkingNoBudget ) ).not.toThrow();
|
|
179
|
-
} );
|
|
180
|
-
|
|
181
|
-
it( 'should validate a prompt with anthropic-specific providerOptions', () => {
|
|
182
|
-
const promptWithAnthropicOptions = {
|
|
183
|
-
name: 'anthropic-options-prompt',
|
|
184
|
-
config: {
|
|
185
|
-
provider: 'anthropic',
|
|
186
|
-
model: 'claude-sonnet-4-20250514',
|
|
187
|
-
providerOptions: {
|
|
188
|
-
thinking: {
|
|
189
|
-
type: 'enabled',
|
|
190
|
-
budgetTokens: 5000
|
|
191
|
-
},
|
|
192
|
-
anthropic: {
|
|
193
|
-
effort: 'medium',
|
|
194
|
-
customOption: 'value'
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
messages: [
|
|
199
|
-
{
|
|
200
|
-
role: 'user',
|
|
201
|
-
content: 'Solve this problem.'
|
|
202
|
-
}
|
|
203
|
-
]
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
expect( () => parse( promptWithAnthropicOptions ) ).not.toThrow();
|
|
207
|
-
} );
|
|
208
|
-
|
|
209
|
-
it( 'should validate a prompt with openai-specific providerOptions', () => {
|
|
210
|
-
const promptWithOpenAIOptions = {
|
|
211
|
-
name: 'openai-options-prompt',
|
|
212
|
-
config: {
|
|
213
|
-
provider: 'openai',
|
|
214
|
-
model: 'o3-mini',
|
|
215
|
-
providerOptions: {
|
|
216
|
-
openai: {
|
|
217
|
-
reasoningEffort: 'high',
|
|
218
|
-
reasoningSummary: 'detailed',
|
|
219
|
-
customParameter: 'test'
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
messages: [
|
|
224
|
-
{
|
|
225
|
-
role: 'user',
|
|
226
|
-
content: 'Analyze this data.'
|
|
227
|
-
}
|
|
228
|
-
]
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
expect( () => parse( promptWithOpenAIOptions ) ).not.toThrow();
|
|
232
|
-
} );
|
|
233
|
-
|
|
234
|
-
it( 'should validate a prompt with azure-specific providerOptions', () => {
|
|
235
|
-
const promptWithAzureOptions = {
|
|
236
|
-
name: 'azure-options-prompt',
|
|
237
|
-
config: {
|
|
238
|
-
provider: 'azure',
|
|
239
|
-
model: 'gpt-4',
|
|
240
|
-
providerOptions: {
|
|
241
|
-
azure: {
|
|
242
|
-
deploymentName: 'my-deployment',
|
|
243
|
-
customConfig: { key: 'value' }
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
},
|
|
247
|
-
messages: [
|
|
248
|
-
{
|
|
249
|
-
role: 'user',
|
|
250
|
-
content: 'Process this request.'
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
expect( () => parse( promptWithAzureOptions ) ).not.toThrow();
|
|
256
|
-
} );
|
|
257
|
-
|
|
258
|
-
it( 'should validate a prompt with mixed providerOptions including unknown fields', () => {
|
|
259
|
-
const promptWithMixedOptions = {
|
|
260
|
-
name: 'mixed-options-prompt',
|
|
261
|
-
config: {
|
|
262
|
-
provider: 'anthropic',
|
|
263
|
-
model: 'claude-3-opus-20240229',
|
|
264
|
-
providerOptions: {
|
|
265
|
-
thinking: {
|
|
266
|
-
type: 'enabled',
|
|
267
|
-
budgetTokens: 3000
|
|
268
|
-
},
|
|
269
|
-
anthropic: {
|
|
270
|
-
effort: 'high'
|
|
271
|
-
},
|
|
272
|
-
customProviderField: 'should-be-allowed',
|
|
273
|
-
anotherCustomField: {
|
|
274
|
-
nested: 'value',
|
|
275
|
-
array: [ 1, 2, 3 ]
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
},
|
|
279
|
-
messages: [
|
|
280
|
-
{
|
|
281
|
-
role: 'user',
|
|
282
|
-
content: 'Complex request with multiple options.'
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
expect( () => parse( promptWithMixedOptions ) ).not.toThrow();
|
|
288
|
-
} );
|
|
289
|
-
|
|
290
|
-
it( 'should accept custom provider names for dynamic providers', () => {
|
|
291
|
-
const customProviderPrompt = {
|
|
292
|
-
name: 'custom-provider-prompt',
|
|
293
|
-
config: {
|
|
294
|
-
provider: 'my-custom-provider',
|
|
295
|
-
model: 'custom-model-v1'
|
|
296
|
-
},
|
|
297
|
-
messages: [
|
|
298
|
-
{
|
|
299
|
-
role: 'user',
|
|
300
|
-
content: 'Test'
|
|
301
|
-
}
|
|
302
|
-
]
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
expect( () => parse( customProviderPrompt ) ).not.toThrow();
|
|
306
|
-
} );
|
|
307
|
-
|
|
308
|
-
it( 'should validate image generation config fields', () => {
|
|
309
|
-
const imagePrompt = {
|
|
310
|
-
name: 'image-prompt',
|
|
311
|
-
config: {
|
|
312
|
-
provider: 'openai',
|
|
313
|
-
model: 'gpt-image-1',
|
|
314
|
-
n: 2,
|
|
315
|
-
maxImagesPerCall: 1,
|
|
316
|
-
size: '1024x1024',
|
|
317
|
-
aspectRatio: '1:1',
|
|
318
|
-
seed: 42,
|
|
319
|
-
providerOptions: {
|
|
320
|
-
openai: {
|
|
321
|
-
quality: 'high'
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
},
|
|
325
|
-
messages: [
|
|
326
|
-
{
|
|
327
|
-
role: 'user',
|
|
328
|
-
content: 'Generate an image of a mountain.'
|
|
329
|
-
}
|
|
330
|
-
]
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
expect( () => parse( imagePrompt ) ).not.toThrow();
|
|
334
|
-
} );
|
|
335
|
-
|
|
336
|
-
it( 'should validate a plain-instructions prompt without messages', () => {
|
|
337
|
-
const instructionsPrompt = {
|
|
338
|
-
name: 'image-prompt',
|
|
339
|
-
config: {
|
|
340
|
-
provider: 'openai',
|
|
341
|
-
model: 'gpt-image-1'
|
|
342
|
-
},
|
|
343
|
-
messages: [],
|
|
344
|
-
instructions: 'Generate an image of a mountain.'
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
expect( () => parse( instructionsPrompt ) ).not.toThrow();
|
|
348
|
-
} );
|
|
349
|
-
|
|
350
|
-
it( 'should validate role messages with null instructions', () => {
|
|
351
|
-
const messagesPrompt = {
|
|
352
|
-
name: 'messages-prompt',
|
|
353
|
-
config: {
|
|
354
|
-
provider: 'anthropic',
|
|
355
|
-
model: 'claude-3-opus-20240229'
|
|
356
|
-
},
|
|
357
|
-
messages: [
|
|
358
|
-
{
|
|
359
|
-
role: 'user',
|
|
360
|
-
content: 'Write a summary.'
|
|
361
|
-
}
|
|
362
|
-
],
|
|
363
|
-
instructions: null
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
expect( () => parse( messagesPrompt ) ).not.toThrow();
|
|
367
|
-
} );
|
|
368
|
-
|
|
369
|
-
it( 'should throw ValidationError when instructions are only whitespace', () => {
|
|
370
|
-
const whitespaceInstructionsPrompt = {
|
|
371
|
-
name: 'empty-instructions-prompt',
|
|
372
|
-
config: {
|
|
373
|
-
provider: 'openai',
|
|
374
|
-
model: 'gpt-image-1'
|
|
375
|
-
},
|
|
376
|
-
messages: [],
|
|
377
|
-
instructions: ' '
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
expect( () => parse( whitespaceInstructionsPrompt ) ).toThrow( ValidationError );
|
|
381
|
-
} );
|
|
382
|
-
|
|
383
|
-
it( 'should throw ValidationError when both messages and instructions are present', () => {
|
|
384
|
-
const mixedPrompt = {
|
|
385
|
-
name: 'mixed-prompt',
|
|
386
|
-
config: {
|
|
387
|
-
provider: 'anthropic',
|
|
388
|
-
model: 'claude-3-opus-20240229'
|
|
389
|
-
},
|
|
390
|
-
messages: [
|
|
391
|
-
{
|
|
392
|
-
role: 'user',
|
|
393
|
-
content: 'Write a summary.'
|
|
394
|
-
}
|
|
395
|
-
],
|
|
396
|
-
instructions: 'Plain instructions should not be mixed with messages.'
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
expect( () => parse( mixedPrompt ) ).toThrow( ValidationError );
|
|
400
|
-
} );
|
|
401
|
-
|
|
402
|
-
it( 'should throw ValidationError when neither messages nor instructions are present', () => {
|
|
403
|
-
const emptyPrompt = {
|
|
404
|
-
name: 'empty-prompt',
|
|
405
|
-
config: {
|
|
406
|
-
provider: 'anthropic',
|
|
407
|
-
model: 'claude-3-opus-20240229'
|
|
408
|
-
},
|
|
409
|
-
messages: [],
|
|
410
|
-
instructions: null
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
expect( () => parse( emptyPrompt ) ).toThrow( ValidationError );
|
|
414
|
-
} );
|
|
415
|
-
|
|
416
|
-
it( 'should validate a prompt with skill path config', () => {
|
|
417
|
-
const promptWithSkills = {
|
|
418
|
-
name: 'skills-prompt',
|
|
419
|
-
config: {
|
|
420
|
-
provider: 'anthropic',
|
|
421
|
-
model: 'claude-3-opus-20240229',
|
|
422
|
-
skills: [ './skills', './review.md' ]
|
|
423
|
-
},
|
|
424
|
-
messages: [
|
|
425
|
-
{
|
|
426
|
-
role: 'user',
|
|
427
|
-
content: 'Review this.'
|
|
428
|
-
}
|
|
429
|
-
]
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
expect( parse( promptWithSkills ).config.skills ).toEqual( [ './skills', './review.md' ] );
|
|
433
|
-
} );
|
|
434
|
-
|
|
435
|
-
it( 'coerces a single skill path to an array', () => {
|
|
436
|
-
const promptWithSkill = {
|
|
437
|
-
name: 'skill-prompt',
|
|
438
|
-
config: {
|
|
439
|
-
provider: 'anthropic',
|
|
440
|
-
model: 'claude-3-opus-20240229',
|
|
441
|
-
skills: './skills'
|
|
442
|
-
},
|
|
443
|
-
messages: [
|
|
444
|
-
{
|
|
445
|
-
role: 'user',
|
|
446
|
-
content: 'Review this.'
|
|
447
|
-
}
|
|
448
|
-
]
|
|
449
|
-
};
|
|
450
|
-
|
|
451
|
-
expect( parse( promptWithSkill ).config.skills ).toEqual( [ './skills' ] );
|
|
452
|
-
} );
|
|
453
|
-
|
|
454
|
-
it( 'coerces null skills to an empty array', () => {
|
|
455
|
-
const promptWithNullSkills = {
|
|
456
|
-
name: 'null-skills-prompt',
|
|
457
|
-
config: {
|
|
458
|
-
provider: 'anthropic',
|
|
459
|
-
model: 'claude-3-opus-20240229',
|
|
460
|
-
skills: null
|
|
461
|
-
},
|
|
462
|
-
messages: [
|
|
463
|
-
{
|
|
464
|
-
role: 'user',
|
|
465
|
-
content: 'Review this.'
|
|
466
|
-
}
|
|
467
|
-
]
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
expect( parse( promptWithNullSkills ).config.skills ).toEqual( [] );
|
|
471
|
-
} );
|
|
472
|
-
|
|
473
|
-
it( 'should validate prompt maxSteps', () => {
|
|
474
|
-
const promptWithMaxSteps = {
|
|
475
|
-
name: 'max-steps-prompt',
|
|
476
|
-
config: {
|
|
477
|
-
provider: 'anthropic',
|
|
478
|
-
model: 'claude-3-opus-20240229',
|
|
479
|
-
maxSteps: 4
|
|
480
|
-
},
|
|
481
|
-
messages: [
|
|
482
|
-
{
|
|
483
|
-
role: 'user',
|
|
484
|
-
content: 'Hello'
|
|
485
|
-
}
|
|
486
|
-
]
|
|
487
|
-
};
|
|
488
|
-
|
|
489
|
-
expect( parse( promptWithMaxSteps ).config.maxSteps ).toBe( 4 );
|
|
490
|
-
} );
|
|
491
|
-
|
|
492
|
-
it( 'should throw ValidationError for invalid maxSteps', () => {
|
|
493
|
-
for ( const maxSteps of [ 0, -1, 1.5 ] ) {
|
|
494
|
-
expect( () => parse( {
|
|
495
|
-
name: 'invalid-max-steps',
|
|
496
|
-
config: {
|
|
497
|
-
provider: 'anthropic',
|
|
498
|
-
model: 'claude-3-opus-20240229',
|
|
499
|
-
maxSteps
|
|
500
|
-
},
|
|
501
|
-
messages: [
|
|
502
|
-
{
|
|
503
|
-
role: 'user',
|
|
504
|
-
content: 'Hello'
|
|
505
|
-
}
|
|
506
|
-
]
|
|
507
|
-
} ) ).toThrow( ValidationError );
|
|
508
|
-
}
|
|
509
|
-
} );
|
|
510
|
-
|
|
511
|
-
it( 'throws ValidationError when fileDir is missing', () => {
|
|
512
|
-
expect( () => parsePromptSchema( {
|
|
513
|
-
name: 'no-file-dir',
|
|
514
|
-
config: {
|
|
515
|
-
provider: 'anthropic',
|
|
516
|
-
model: 'claude-3-opus-20240229'
|
|
517
|
-
},
|
|
518
|
-
messages: [
|
|
519
|
-
{
|
|
520
|
-
role: 'user',
|
|
521
|
-
content: 'Hello'
|
|
522
|
-
}
|
|
523
|
-
]
|
|
524
|
-
} ) ).toThrow( ValidationError );
|
|
525
|
-
} );
|
|
526
|
-
|
|
527
|
-
it( 'accepts scalar, object, and array variables', () => {
|
|
528
|
-
expect( parse( {
|
|
529
|
-
name: 'typed-variables',
|
|
530
|
-
config: {
|
|
531
|
-
provider: 'anthropic',
|
|
532
|
-
model: 'claude-3-opus-20240229'
|
|
533
|
-
},
|
|
534
|
-
messages: [
|
|
535
|
-
{
|
|
536
|
-
role: 'user',
|
|
537
|
-
content: 'Hello'
|
|
538
|
-
}
|
|
539
|
-
],
|
|
540
|
-
variables: {
|
|
541
|
-
name: 'Acme',
|
|
542
|
-
size: 250,
|
|
543
|
-
active: true,
|
|
544
|
-
company: { name: 'Acme' },
|
|
545
|
-
industries: [ 'SaaS', 'AI' ]
|
|
546
|
-
}
|
|
547
|
-
} ).variables ).toEqual( {
|
|
548
|
-
name: 'Acme',
|
|
549
|
-
size: 250,
|
|
550
|
-
active: true,
|
|
551
|
-
company: { name: 'Acme' },
|
|
552
|
-
industries: [ 'SaaS', 'AI' ]
|
|
553
|
-
} );
|
|
554
|
-
} );
|
|
555
|
-
|
|
556
|
-
it( 'should validate provider tool config records', () => {
|
|
557
|
-
const promptWithTools = {
|
|
558
|
-
name: 'tools-prompt',
|
|
559
|
-
config: {
|
|
560
|
-
provider: 'vertex',
|
|
561
|
-
model: 'gemini-2.0-flash',
|
|
562
|
-
tools: {
|
|
563
|
-
googleSearch: {
|
|
564
|
-
mode: 'MODE_DYNAMIC',
|
|
565
|
-
dynamicThreshold: 0.8
|
|
566
|
-
},
|
|
567
|
-
urlContext: {}
|
|
568
|
-
}
|
|
569
|
-
},
|
|
570
|
-
messages: [
|
|
571
|
-
{
|
|
572
|
-
role: 'user',
|
|
573
|
-
content: 'Research this.'
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
};
|
|
577
|
-
|
|
578
|
-
expect( () => parse( promptWithTools ) ).not.toThrow();
|
|
579
|
-
} );
|
|
580
|
-
|
|
581
|
-
it( 'should throw ValidationError when tools config is not a record', () => {
|
|
582
|
-
const invalidToolsPrompt = {
|
|
583
|
-
name: 'invalid-tools-prompt',
|
|
584
|
-
config: {
|
|
585
|
-
provider: 'vertex',
|
|
586
|
-
model: 'gemini-2.0-flash',
|
|
587
|
-
tools: [ 'googleSearch' ]
|
|
588
|
-
},
|
|
589
|
-
messages: [
|
|
590
|
-
{
|
|
591
|
-
role: 'user',
|
|
592
|
-
content: 'Research this.'
|
|
593
|
-
}
|
|
594
|
-
]
|
|
595
|
-
};
|
|
596
|
-
|
|
597
|
-
expect( () => parse( invalidToolsPrompt ) ).toThrow( ValidationError );
|
|
598
|
-
} );
|
|
599
|
-
|
|
600
|
-
it( 'should throw ValidationError when a tool config is not a record', () => {
|
|
601
|
-
const invalidToolConfigPrompt = {
|
|
602
|
-
name: 'invalid-tool-config-prompt',
|
|
603
|
-
config: {
|
|
604
|
-
provider: 'vertex',
|
|
605
|
-
model: 'gemini-2.0-flash',
|
|
606
|
-
tools: {
|
|
607
|
-
googleSearch: 'MODE_DYNAMIC'
|
|
608
|
-
}
|
|
609
|
-
},
|
|
610
|
-
messages: [
|
|
611
|
-
{
|
|
612
|
-
role: 'user',
|
|
613
|
-
content: 'Research this.'
|
|
614
|
-
}
|
|
615
|
-
]
|
|
616
|
-
};
|
|
617
|
-
|
|
618
|
-
expect( () => parse( invalidToolConfigPrompt ) ).toThrow( ValidationError );
|
|
619
|
-
} );
|
|
620
|
-
|
|
621
|
-
it( 'should throw ValidationError for invalid skill path config', () => {
|
|
622
|
-
const invalidSkillsPrompt = {
|
|
623
|
-
name: 'invalid-skills-prompt',
|
|
624
|
-
config: {
|
|
625
|
-
provider: 'anthropic',
|
|
626
|
-
model: 'claude-3-opus-20240229',
|
|
627
|
-
skills: [ './skills', '' ]
|
|
628
|
-
},
|
|
629
|
-
messages: [
|
|
630
|
-
{
|
|
631
|
-
role: 'user',
|
|
632
|
-
content: 'Review this.'
|
|
633
|
-
}
|
|
634
|
-
]
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
expect( () => parse( invalidSkillsPrompt ) ).toThrow( ValidationError );
|
|
638
|
-
} );
|
|
639
|
-
|
|
640
|
-
it( 'should throw ValidationError for invalid image generation config fields', () => {
|
|
641
|
-
const invalidImagePrompt = {
|
|
642
|
-
name: 'invalid-image-prompt',
|
|
643
|
-
config: {
|
|
644
|
-
provider: 'openai',
|
|
645
|
-
model: 'gpt-image-1',
|
|
646
|
-
n: 0,
|
|
647
|
-
maxImagesPerCall: 1.5,
|
|
648
|
-
size: 'square',
|
|
649
|
-
aspectRatio: '16x9',
|
|
650
|
-
seed: 1.2
|
|
651
|
-
},
|
|
652
|
-
messages: [
|
|
653
|
-
{
|
|
654
|
-
role: 'user',
|
|
655
|
-
content: 'Generate an image of a mountain.'
|
|
656
|
-
}
|
|
657
|
-
]
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
expect( () => parse( invalidImagePrompt ) ).toThrow( ValidationError );
|
|
661
|
-
} );
|
|
662
|
-
|
|
663
|
-
it( 'should reject unrecognized config fields', () => {
|
|
664
|
-
const extraFieldsPrompt = {
|
|
665
|
-
name: 'extra-fields-prompt',
|
|
666
|
-
config: {
|
|
667
|
-
provider: 'openai',
|
|
668
|
-
model: 'gpt-4',
|
|
669
|
-
unsupportedOption: true
|
|
670
|
-
},
|
|
671
|
-
messages: [
|
|
672
|
-
{
|
|
673
|
-
role: 'user',
|
|
674
|
-
content: 'Test'
|
|
675
|
-
}
|
|
676
|
-
]
|
|
677
|
-
};
|
|
678
|
-
|
|
679
|
-
expect( () => parse( extraFieldsPrompt ) ).toThrow( /unrecognized key/i );
|
|
680
|
-
} );
|
|
681
|
-
|
|
682
|
-
it( 'should throw ValidationError when provider is empty string', () => {
|
|
683
|
-
const emptyProviderPrompt = {
|
|
684
|
-
name: 'empty-provider',
|
|
685
|
-
config: {
|
|
686
|
-
provider: '',
|
|
687
|
-
model: 'some-model'
|
|
688
|
-
},
|
|
689
|
-
messages: [
|
|
690
|
-
{
|
|
691
|
-
role: 'user',
|
|
692
|
-
content: 'Test'
|
|
693
|
-
}
|
|
694
|
-
]
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
expect( () => parse( emptyProviderPrompt ) ).toThrow( ValidationError );
|
|
698
|
-
} );
|
|
699
|
-
|
|
700
|
-
it( 'should throw ValidationError when required fields are missing', () => {
|
|
701
|
-
const missingNamePrompt = {
|
|
702
|
-
config: {
|
|
703
|
-
provider: 'anthropic',
|
|
704
|
-
model: 'claude-3-opus-20240229'
|
|
705
|
-
},
|
|
706
|
-
messages: [
|
|
707
|
-
{
|
|
708
|
-
role: 'user',
|
|
709
|
-
content: 'Test'
|
|
710
|
-
}
|
|
711
|
-
]
|
|
712
|
-
};
|
|
713
|
-
|
|
714
|
-
expect( () => parse( missingNamePrompt ) ).toThrow( ValidationError );
|
|
715
|
-
} );
|
|
716
|
-
|
|
717
|
-
it( 'should pass through budget_tokens in thinking and warn about snake_case', () => {
|
|
718
|
-
const promptWithBudgetTokensSnake = {
|
|
719
|
-
name: 'thinking-budget-snake',
|
|
720
|
-
config: {
|
|
721
|
-
provider: 'anthropic',
|
|
722
|
-
model: 'claude-sonnet-4-20250514',
|
|
723
|
-
providerOptions: {
|
|
724
|
-
thinking: {
|
|
725
|
-
type: 'enabled',
|
|
726
|
-
budget_tokens: 10000
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
messages: [
|
|
731
|
-
{
|
|
732
|
-
role: 'user',
|
|
733
|
-
content: 'Think hard.'
|
|
734
|
-
}
|
|
735
|
-
]
|
|
736
|
-
};
|
|
737
|
-
|
|
738
|
-
expect( () => parse( promptWithBudgetTokensSnake ) ).not.toThrow();
|
|
739
|
-
} );
|
|
740
|
-
|
|
741
|
-
it( 'should suggest camelCase when a snake_case config key matches a known field', () => {
|
|
742
|
-
const maxTokensSnakeCase = {
|
|
743
|
-
name: 'test-prompt',
|
|
744
|
-
config: {
|
|
745
|
-
provider: 'anthropic',
|
|
746
|
-
model: 'claude-3-opus-20240229',
|
|
747
|
-
max_tokens: 4000
|
|
748
|
-
},
|
|
749
|
-
messages: [
|
|
750
|
-
{
|
|
751
|
-
role: 'user',
|
|
752
|
-
content: 'Test'
|
|
753
|
-
}
|
|
754
|
-
]
|
|
755
|
-
};
|
|
756
|
-
|
|
757
|
-
expect( () => parse( maxTokensSnakeCase ) ).toThrow( /"max_tokens" is not valid; use "maxTokens"/ );
|
|
758
|
-
} );
|
|
759
|
-
|
|
760
|
-
it( 'should validate per-message providerOptions', () => {
|
|
761
|
-
const promptWithMessageProviderOptions = {
|
|
762
|
-
name: 'message-options-prompt',
|
|
763
|
-
config: {
|
|
764
|
-
provider: 'anthropic',
|
|
765
|
-
model: 'claude-sonnet-4-5',
|
|
766
|
-
messageOptions: {
|
|
767
|
-
cached: { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
|
768
|
-
}
|
|
769
|
-
},
|
|
770
|
-
messages: [
|
|
771
|
-
{
|
|
772
|
-
role: 'system',
|
|
773
|
-
content: 'Docs.',
|
|
774
|
-
providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
|
|
775
|
-
},
|
|
776
|
-
{ role: 'user', content: 'Question' }
|
|
777
|
-
]
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
expect( () => parse( promptWithMessageProviderOptions ) ).not.toThrow();
|
|
781
|
-
} );
|
|
782
|
-
|
|
783
|
-
it( 'should reject leftover attributes on a message', () => {
|
|
784
|
-
const leftoverAttributesPrompt = {
|
|
785
|
-
name: 'leftover-attributes-prompt',
|
|
786
|
-
config: {
|
|
787
|
-
provider: 'anthropic',
|
|
788
|
-
model: 'claude-sonnet-4-5'
|
|
789
|
-
},
|
|
790
|
-
messages: [
|
|
791
|
-
{ role: 'system', content: 'Static.', attributes: { options: 'cached' } }
|
|
792
|
-
]
|
|
793
|
-
};
|
|
794
|
-
|
|
795
|
-
expect( () => parse( leftoverAttributesPrompt ) ).toThrow( ValidationError );
|
|
796
|
-
} );
|
|
797
|
-
|
|
798
|
-
it( 'should throw ValidationError for unknown top-level message fields', () => {
|
|
799
|
-
const unknownFieldPrompt = {
|
|
800
|
-
name: 'unknown-field-prompt',
|
|
801
|
-
config: {
|
|
802
|
-
provider: 'anthropic',
|
|
803
|
-
model: 'claude-sonnet-4-5'
|
|
804
|
-
},
|
|
805
|
-
messages: [
|
|
806
|
-
{ role: 'user', content: 'Hi', options: 'cached' }
|
|
807
|
-
]
|
|
808
|
-
};
|
|
809
|
-
|
|
810
|
-
expect( () => parse( unknownFieldPrompt ) ).toThrow( ValidationError );
|
|
811
|
-
} );
|
|
812
|
-
} );
|