@output.ai/llm 0.2.3 → 0.2.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@output.ai/llm",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -13,7 +13,7 @@
13
13
  "@ai-sdk/azure": "2.0.53",
14
14
  "@ai-sdk/openai": "2.0.52",
15
15
  "@output.ai/core": ">=0.0.1",
16
- "ai": "5.0.48",
16
+ "ai": "5.0.52",
17
17
  "gray-matter": "4.0.3",
18
18
  "liquidjs": "10.22.0"
19
19
  },
package/src/ai_sdk.js CHANGED
@@ -61,6 +61,8 @@ const extraAiSdkOptionsFromPrompt = prompt => {
61
61
  * @param {object} args - Generation arguments
62
62
  * @param {string} args.prompt - Prompt file name
63
63
  * @param {Record<string, string | number>} [args.variables] - Variables to interpolate
64
+ * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
65
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
64
66
  * @returns {Promise<string>} Generated text
65
67
  */
66
68
  export async function generateText( { prompt, variables } ) {
@@ -83,6 +85,8 @@ export async function generateText( { prompt, variables } ) {
83
85
  * @param {z.ZodObject} args.schema - Output schema
84
86
  * @param {string} [args.schemaName] - Output schema name
85
87
  * @param {string} [args.schemaDescription] - Output schema description
88
+ * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
89
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
86
90
  * @returns {Promise<object>} Object matching the provided schema
87
91
  */
88
92
  export async function generateObject( args ) {
@@ -112,6 +116,8 @@ export async function generateObject( args ) {
112
116
  * @param {z.ZodType} args.schema - Output schema (array item)
113
117
  * @param {string} [args.schemaName] - Output schema name
114
118
  * @param {string} [args.schemaDescription] - Output schema description
119
+ * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
120
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
115
121
  * @returns {Promise<object>} Array where each element matches the schema
116
122
  */
117
123
  export async function generateArray( args ) {
@@ -139,6 +145,8 @@ export async function generateArray( args ) {
139
145
  * @param {string} args.prompt - Prompt file name
140
146
  * @param {Record<string, string | number>} [args.variables] - Variables to interpolate
141
147
  * @param {string[]} args.enum - Allowed values for the generation
148
+ * @throws {ValidationError} If the prompt config is invalid (e.g., snake_case fields)
149
+ * @throws {FatalError} If the prompt file is not found or template rendering fails
142
150
  * @returns {Promise<string>} One of the provided enum values
143
151
  */
144
152
  export async function generateEnum( args ) {
package/src/index.d.ts CHANGED
@@ -47,9 +47,6 @@ export type Prompt = {
47
47
  /** Maximum tokens in the response */
48
48
  maxTokens?: number;
49
49
 
50
- /** Additional provider-specific options */
51
- options?: Record<string, Record<string, JSONValue>>;
52
-
53
50
  /** Provider-specific configurations */
54
51
  providerOptions?: Record<string, unknown>;
55
52
  };
@@ -62,12 +59,12 @@ export type Prompt = {
62
59
  * Load a prompt file and render it with variables.
63
60
  *
64
61
  * @param {string} name - Name of the prompt file (without .prompt extension)
65
- * @param {Record<string, string | number>} [variables] - Variables to interpolate
62
+ * @param {Record<string, string | number | boolean>} [variables] - Variables to interpolate
66
63
  * @returns {Prompt} Loaded and rendered prompt object
67
64
  */
68
65
  export function loadPrompt(
69
66
  name: string,
70
- variables?: Record<string, string | number>
67
+ variables?: Record<string, string | number | boolean>
71
68
  ): Prompt;
72
69
 
73
70
  /**
@@ -78,13 +75,13 @@ export function loadPrompt(
78
75
  *
79
76
  * @param {object} args - Generation arguments
80
77
  * @param {string} args.prompt - Prompt file name
81
- * @param {Record<string, string | number>} args.variables - Variables to interpolate
78
+ * @param {Record<string, string | number | boolean>} args.variables - Variables to interpolate
82
79
  * @returns {Promise<string>} Generated text
83
80
  */
84
81
  export function generateText(
85
82
  args: {
86
83
  prompt: string,
87
- variables?: Record<string, string | number>
84
+ variables?: Record<string, string | number | boolean>
88
85
  }
89
86
  ): Promise<string>;
90
87
 
@@ -96,7 +93,7 @@ export function generateText(
96
93
  *
97
94
  * @param {object} args - Generation arguments
98
95
  * @param {string} args.prompt - Prompt file name
99
- * @param {Record<string, string | number>} args.variables - Variables to interpolate
96
+ * @param {Record<string, string | number | boolean>} args.variables - Variables to interpolate
100
97
  * @param {z.ZodObject} args.schema - Output schema
101
98
  * @param {string} [args.schemaName] - Output schema name
102
99
  * @param {string} [args.schemaDescription] - Output schema description
@@ -105,7 +102,7 @@ export function generateText(
105
102
  export function generateObject<TSchema extends z.ZodObject>(
106
103
  args: {
107
104
  prompt: string,
108
- variables?: Record<string, string | number>,
105
+ variables?: Record<string, string | number | boolean>,
109
106
  schema: TSchema,
110
107
  schemaName?: string,
111
108
  schemaDescription?: string
@@ -120,7 +117,7 @@ export function generateObject<TSchema extends z.ZodObject>(
120
117
  *
121
118
  * @param {object} args - Generation arguments
122
119
  * @param {string} args.prompt - Prompt file name
123
- * @param {Record<string, string | number>} args.variables - Variables to interpolate
120
+ * @param {Record<string, string | number | boolean>} args.variables - Variables to interpolate
124
121
  * @param {z.ZodType} args.schema - Output schema (array item)
125
122
  * @param {string} [args.schemaName] - Output schema name
126
123
  * @param {string} [args.schemaDescription] - Output schema description
@@ -129,7 +126,7 @@ export function generateObject<TSchema extends z.ZodObject>(
129
126
  export function generateArray<TSchema extends z.ZodType>(
130
127
  args: {
131
128
  prompt: string,
132
- variables?: Record<string, string | number>,
129
+ variables?: Record<string, string | number | boolean>,
133
130
  schema: TSchema,
134
131
  schemaName?: string,
135
132
  schemaDescription?: string
@@ -144,14 +141,14 @@ export function generateArray<TSchema extends z.ZodType>(
144
141
  *
145
142
  * @param {object} args - Generation arguments
146
143
  * @param {string} args.prompt - Prompt file name
147
- * @param {Record<string, string | number>} args.variables - Variables to interpolate
144
+ * @param {Record<string, string | number | boolean>} args.variables - Variables to interpolate
148
145
  * @param {string[]} args.enum - Allowed values for the generation
149
146
  * @returns {Promise<string>} One of the provided enum values
150
147
  */
151
148
  export function generateEnum<const TEnum extends readonly [string, ...string[]]>(
152
149
  args: {
153
150
  prompt: string,
154
- variables?: Record<string, string | number>,
151
+ variables?: Record<string, string | number | boolean>,
155
152
  enum: TEnum
156
153
  }
157
154
  ): Promise<TEnum[number]>;
@@ -56,4 +56,67 @@ This is just plain text without any message tags.`;
56
56
  parsePrompt( raw );
57
57
  } ).toThrow( /Expected format/ );
58
58
  } );
59
+
60
+ it( 'should use providerOptions with budgetTokens in camelCase', () => {
61
+ // Frontmatter uses canonical format: providerOptions.thinking.budgetTokens
62
+ const raw = `---
63
+ provider: anthropic
64
+ model: claude-sonnet-4-20250514
65
+ temperature: 0.7
66
+ maxTokens: 64000
67
+ providerOptions:
68
+ thinking:
69
+ type: enabled
70
+ budgetTokens: 1500
71
+ ---
72
+
73
+ <user>Test</user>`;
74
+
75
+ const result = parsePrompt( raw );
76
+
77
+ expect( result.config.provider ).toBe( 'anthropic' );
78
+ expect( result.config.model ).toBe( 'claude-sonnet-4-20250514' );
79
+ expect( result.config.temperature ).toBe( 0.7 );
80
+ expect( result.config.maxTokens ).toBe( 64000 );
81
+
82
+ // Now expects canonical schema format in front-matter
83
+ expect( result.config.providerOptions ).toBeDefined();
84
+ expect( result.config.providerOptions.thinking ).toBeDefined();
85
+ expect( result.config.providerOptions.thinking.type ).toBe( 'enabled' );
86
+ expect( result.config.providerOptions.thinking.budgetTokens ).toBe( 1500 );
87
+ } );
88
+
89
+ it( 'should parse snake_case fields as-is (validation catches them later)', () => {
90
+ // Parser extracts fields as-is from frontmatter; validation happens later
91
+ const raw = `---
92
+ provider: anthropic
93
+ model: claude-sonnet-4-20250514
94
+ temperature: 0.7
95
+ max_tokens: 64000
96
+ providerOptions:
97
+ thinking:
98
+ type: enabled
99
+ budget_tokens: 1500
100
+ ---
101
+
102
+ <user>Test</user>`;
103
+
104
+ const result = parsePrompt( raw );
105
+
106
+ // Parser extracts snake_case fields as-is
107
+ expect( result.config.provider ).toBe( 'anthropic' );
108
+ expect( result.config.model ).toBe( 'claude-sonnet-4-20250514' );
109
+ expect( result.config.temperature ).toBe( 0.7 );
110
+ // snake_case preserved here because we're only calling parsePrompt, not validatePrompt
111
+ expect( result.config.max_tokens ).toBe( 64000 );
112
+ // camelCase not set because we only call parsePrompt
113
+ expect( result.config.maxTokens ).toBeUndefined();
114
+
115
+ // Snake_case fields in nested objects also preserved
116
+ expect( result.config.providerOptions ).toBeDefined();
117
+ expect( result.config.providerOptions.thinking ).toBeDefined();
118
+ expect( result.config.providerOptions.thinking.type ).toBe( 'enabled' );
119
+ expect( result.config.providerOptions.thinking.budget_tokens ).toBe( 1500 ); // snake_case preserved
120
+ expect( result.config.providerOptions.thinking.budgetTokens ).toBeUndefined(); // camelCase not set
121
+ } );
59
122
  } );
@@ -18,7 +18,7 @@ const renderPrompt = ( name, content, values ) => {
18
18
  * Load a prompt file and render it with variables.
19
19
  *
20
20
  * @param {string} name - Name of the prompt file (without .prompt extension)
21
- * @param {Record<string, string | number>} [values] - Variables to interpolate
21
+ * @param {Record<string, string | number | boolean>} [values] - Variables to interpolate
22
22
  * @returns {Prompt} Loaded and rendered prompt object
23
23
  */
24
24
  export const loadPrompt = ( name, values = {} ) => {
@@ -129,4 +129,51 @@ temperature: 0.7
129
129
  expect( result.config.model ).toBe( 'claude-sonnet-4' );
130
130
  } );
131
131
 
132
+ it( 'should use camelCase config keys', () => {
133
+ const promptContent = `---
134
+ provider: anthropic
135
+ model: claude-3-5-sonnet-20241022
136
+ maxTokens: 1024
137
+ temperature: 0.7
138
+ ---
139
+
140
+ <user>Hello</user>`;
141
+
142
+ loadContent.mockReturnValue( promptContent );
143
+
144
+ const result = loadPrompt( 'test', {} );
145
+
146
+ expect( result.config.maxTokens ).toBe( 1024 );
147
+ } );
148
+
149
+ it( 'should render boolean variables correctly', () => {
150
+ const promptContent = `---
151
+ provider: anthropic
152
+ model: claude-3-5-sonnet-20241022
153
+ ---
154
+
155
+ <user>{% if debug %}Debug mode enabled{% else %}Debug mode disabled{% endif %}</user>`;
156
+
157
+ loadContent.mockReturnValue( promptContent );
158
+
159
+ const result = loadPrompt( 'test', { debug: true } );
160
+
161
+ expect( result.messages[0].content ).toBe( 'Debug mode enabled' );
162
+ } );
163
+
164
+ it( 'should render false boolean variables', () => {
165
+ const promptContent = `---
166
+ provider: anthropic
167
+ model: claude-3-5-sonnet-20241022
168
+ ---
169
+
170
+ <user>{% if enabled %}Feature enabled{% else %}Feature disabled{% endif %}</user>`;
171
+
172
+ loadContent.mockReturnValue( promptContent );
173
+
174
+ const result = loadPrompt( 'test', { enabled: false } );
175
+
176
+ expect( result.messages[0].content ).toBe( 'Feature disabled' );
177
+ } );
178
+
132
179
  } );
@@ -0,0 +1,174 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { loadPrompt } from './prompt_loader.js';
3
+
4
+ vi.mock( './load_content.js', () => ( {
5
+ loadContent: vi.fn()
6
+ } ) );
7
+
8
+ import { loadContent } from './load_content.js';
9
+ import { ValidationError } from '@output.ai/core';
10
+
11
+ describe( 'loadPrompt - validation with real schema', () => {
12
+ beforeEach( () => {
13
+ vi.clearAllMocks();
14
+ } );
15
+
16
+ it( 'should accept valid camelCase format with providerOptions and budgetTokens', () => {
17
+ // Frontmatter uses canonical format: providerOptions.thinking.budgetTokens
18
+ const promptContent = `---
19
+ provider: anthropic
20
+ model: claude-sonnet-4-20250514
21
+ temperature: 0.7
22
+ maxTokens: 64000
23
+ providerOptions:
24
+ thinking:
25
+ type: enabled
26
+ budgetTokens: 1500
27
+ ---
28
+
29
+ <user>Hello</user>`;
30
+
31
+ loadContent.mockReturnValue( promptContent );
32
+
33
+ const result = loadPrompt( 'test', {} );
34
+
35
+ // Validation passes with canonical schema format
36
+ expect( result.config.providerOptions ).toBeDefined();
37
+ expect( result.config.providerOptions.thinking.type ).toBe( 'enabled' );
38
+ expect( result.config.providerOptions.thinking.budgetTokens ).toBe( 1500 );
39
+ } );
40
+
41
+ it( 'should REJECT snake_case max_tokens (not canonical)', () => {
42
+ // Using snake_case max_tokens should fail validation
43
+ const promptContent = `---
44
+ provider: anthropic
45
+ model: claude-sonnet-4-20250514
46
+ max_tokens: 64000
47
+ ---
48
+
49
+ <user>Hello</user>`;
50
+
51
+ loadContent.mockReturnValue( promptContent );
52
+
53
+ // loadPrompt should throw ValidationError because schema should reject max_tokens.
54
+ // Other tests check the actual message, but this one specifically checks that we throw
55
+ // the right kind of error, just to check that as well
56
+ expect( () => {
57
+ loadPrompt( 'test', {} );
58
+ } ).toThrow( ValidationError );
59
+ expect( () => {
60
+ loadPrompt( 'test', {} );
61
+ } ).toThrow( /max_tokens/ );
62
+ } );
63
+
64
+ it( 'should REJECT deprecated "options" field (use providerOptions)', () => {
65
+ // Using 'options' instead of 'providerOptions' should fail validation
66
+ const promptContent = `---
67
+ provider: anthropic
68
+ model: claude-sonnet-4-20250514
69
+ options:
70
+ thinking:
71
+ type: enabled
72
+ budgetTokens: 1500
73
+ ---
74
+
75
+ <user>Hello</user>`;
76
+
77
+ loadContent.mockReturnValue( promptContent );
78
+
79
+ // loadPrompt should throw ValidationError because schema should reject 'options'
80
+ expect( () => {
81
+ loadPrompt( 'test', {} );
82
+ } ).toThrow( /Invalid prompt file/ );
83
+ } );
84
+
85
+ it( 'should REJECT snake_case budget_tokens in providerOptions', () => {
86
+ // Using snake_case budget_tokens should fail validation
87
+ const promptContent = `---
88
+ provider: anthropic
89
+ model: claude-sonnet-4-20250514
90
+ providerOptions:
91
+ thinking:
92
+ type: enabled
93
+ budget_tokens: 1500
94
+ ---
95
+
96
+ <user>Hello</user>`;
97
+
98
+ loadContent.mockReturnValue( promptContent );
99
+
100
+ // loadPrompt should throw ValidationError because schema rejects budget_tokens
101
+ expect( () => {
102
+ loadPrompt( 'test', {} );
103
+ } ).toThrow( /Invalid prompt file/ );
104
+ } );
105
+
106
+ it( 'end-to-end: should accept canonical camelCase format', () => {
107
+ // End-to-end test: frontmatter uses canonical providerOptions.thinking.budgetTokens
108
+ const promptContent = `---
109
+ provider: anthropic
110
+ model: claude-sonnet-4-20250514
111
+ temperature: 0.7
112
+ maxTokens: 64000
113
+ providerOptions:
114
+ thinking:
115
+ type: enabled
116
+ budgetTokens: 1500
117
+ ---
118
+
119
+ <user>Hello</user>`;
120
+
121
+ loadContent.mockReturnValue( promptContent );
122
+
123
+ const result = loadPrompt( 'test', {} );
124
+
125
+ // After loadPrompt, uses canonical format
126
+ expect( result.config.providerOptions ).toBeDefined();
127
+ expect( result.config.providerOptions.thinking.type ).toBe( 'enabled' );
128
+ expect( result.config.providerOptions.thinking.budgetTokens ).toBe( 1500 );
129
+ } );
130
+
131
+ it( 'end-to-end: should FAIL validation with snake_case budget_tokens', () => {
132
+ // End-to-end test: using snake_case should fail validation
133
+ const promptContent = `---
134
+ provider: anthropic
135
+ model: claude-sonnet-4-20250514
136
+ providerOptions:
137
+ thinking:
138
+ type: enabled
139
+ budget_tokens: 1500
140
+ ---
141
+
142
+ <user>Hello</user>`;
143
+
144
+ loadContent.mockReturnValue( promptContent );
145
+
146
+ // loadPrompt should throw ValidationError because schema rejects budget_tokens
147
+ expect( () => {
148
+ loadPrompt( 'test', {} );
149
+ } ).toThrow( /Invalid prompt file/ );
150
+ } );
151
+
152
+ it( 'end-to-end: should FAIL validation with snake_case max_tokens', () => {
153
+ // End-to-end test: using snake_case should fail validation
154
+ const promptContent = `---
155
+ provider: anthropic
156
+ model: claude-sonnet-4-20250514
157
+ max_tokens: 4000
158
+ providerOptions:
159
+ thinking:
160
+ type: enabled
161
+ budget_tokens: 1500
162
+ ---
163
+
164
+ <user>Hello</user>`;
165
+
166
+ loadContent.mockReturnValue( promptContent );
167
+
168
+ // loadPrompt should throw ValidationError because schema rejects max_tokens
169
+ expect( () => {
170
+ loadPrompt( 'test', {} );
171
+ } ).toThrow( /Invalid prompt file/ );
172
+ } );
173
+
174
+ } );
@@ -11,20 +11,40 @@ export const promptSchema = z.object( {
11
11
  thinking: z.object( {
12
12
  type: z.literal( 'enabled' ),
13
13
  budgetTokens: z.number()
14
- } ).optional()
15
- } ).optional()
16
- } ),
14
+ } ).strict().optional()
15
+ } ).strict().optional()
16
+ } ).strict(),
17
17
  messages: z.array(
18
18
  z.object( {
19
19
  role: z.string(),
20
20
  content: z.string()
21
- } )
21
+ } ).strict()
22
22
  )
23
- } );
23
+ } ).strict();
24
+
25
+ const getHintForError = errorMessage => {
26
+ if ( errorMessage.includes( 'max_tokens' ) ) {
27
+ return '\nHint: Use "maxTokens" (camelCase) instead of "max_tokens".';
28
+ }
29
+ if ( errorMessage.includes( 'budget_tokens' ) ) {
30
+ return '\nHint: Use "budgetTokens" (camelCase) instead of "budget_tokens".';
31
+ }
32
+ if ( errorMessage.includes( '"options"' ) ) {
33
+ return '\nHint: Use "providerOptions" instead of "options".';
34
+ }
35
+ return '';
36
+ };
24
37
 
25
38
  export function validatePrompt( prompt ) {
26
39
  const result = promptSchema.safeParse( prompt );
27
40
  if ( !result.success ) {
28
- throw new ValidationError( `Invalid prompt file: ${z.prettifyError( result.error )}` );
41
+ const promptIdentifier = prompt?.name ? `"${prompt.name}"` : '(unnamed)';
42
+ const errorMessage = z.prettifyError( result.error );
43
+ const hint = getHintForError( errorMessage );
44
+
45
+ throw new ValidationError(
46
+ `Invalid prompt file ${promptIdentifier}: ${errorMessage}${hint}`,
47
+ { cause: result.error }
48
+ );
29
49
  }
30
50
  }
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ValidationError } from '@output.ai/core';
3
+ import { validatePrompt } from './prompt_validations.js';
4
+
5
+ describe( 'validatePrompt', () => {
6
+ it( 'should validate a correct prompt with all required fields', () => {
7
+ const validPrompt = {
8
+ name: 'test-prompt',
9
+ config: {
10
+ provider: 'anthropic',
11
+ model: 'claude-3-opus-20240229',
12
+ temperature: 0.7,
13
+ maxTokens: 1000
14
+ },
15
+ messages: [
16
+ {
17
+ role: 'user',
18
+ content: 'Hello, world!'
19
+ }
20
+ ]
21
+ };
22
+
23
+ expect( () => validatePrompt( validPrompt ) ).not.toThrow();
24
+ } );
25
+
26
+ it( 'should validate a minimal prompt with only required fields', () => {
27
+ const minimalPrompt = {
28
+ name: 'minimal-prompt',
29
+ config: {
30
+ provider: 'openai',
31
+ model: 'gpt-4'
32
+ },
33
+ messages: [
34
+ {
35
+ role: 'system',
36
+ content: 'You are a helpful assistant.'
37
+ }
38
+ ]
39
+ };
40
+
41
+ expect( () => validatePrompt( minimalPrompt ) ).not.toThrow();
42
+ } );
43
+
44
+ it( 'should validate a prompt with thinking providerOptions', () => {
45
+ const promptWithThinking = {
46
+ name: 'thinking-prompt',
47
+ config: {
48
+ provider: 'anthropic',
49
+ model: 'claude-3-5-sonnet-20241022',
50
+ providerOptions: {
51
+ thinking: {
52
+ type: 'enabled',
53
+ budgetTokens: 5000
54
+ }
55
+ }
56
+ },
57
+ messages: [
58
+ {
59
+ role: 'user',
60
+ content: 'Solve this problem.'
61
+ }
62
+ ]
63
+ };
64
+
65
+ expect( () => validatePrompt( promptWithThinking ) ).not.toThrow();
66
+ } );
67
+
68
+ it( 'should throw ValidationError when provider is invalid', () => {
69
+ const invalidPrompt = {
70
+ name: 'invalid-provider',
71
+ config: {
72
+ provider: 'invalid-provider',
73
+ model: 'some-model'
74
+ },
75
+ messages: [
76
+ {
77
+ role: 'user',
78
+ content: 'Test'
79
+ }
80
+ ]
81
+ };
82
+
83
+ expect( () => validatePrompt( invalidPrompt ) ).toThrow( ValidationError );
84
+ } );
85
+
86
+ it( 'should throw ValidationError when required fields are missing', () => {
87
+ const missingNamePrompt = {
88
+ config: {
89
+ provider: 'anthropic',
90
+ model: 'claude-3-opus-20240229'
91
+ },
92
+ messages: [
93
+ {
94
+ role: 'user',
95
+ content: 'Test'
96
+ }
97
+ ]
98
+ };
99
+
100
+ expect( () => validatePrompt( missingNamePrompt ) ).toThrow( ValidationError );
101
+ } );
102
+
103
+ it( 'should throw ValidationError when max tokens is snake_case', () => {
104
+ const maxTokensSnakeCase = {
105
+ name: 'test-prompt',
106
+ config: {
107
+ provider: 'anthropic',
108
+ model: 'claude-3-opus-20240229',
109
+ max_tokens: 4000
110
+ },
111
+ messages: [
112
+ {
113
+ role: 'user',
114
+ content: 'Test'
115
+ }
116
+ ]
117
+ };
118
+
119
+ expect( () => validatePrompt( maxTokensSnakeCase ) ).toThrow( ValidationError );
120
+ } );
121
+ } );