@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,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
- } );