@output.ai/llm 0.2.2 → 0.2.3

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.2",
3
+ "version": "0.2.3",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,12 +1,13 @@
1
1
  import { join } from 'path';
2
2
  import { readdirSync, readFileSync } from 'node:fs';
3
3
  import { resolveInvocationDir } from '@output.ai/core/utils';
4
+ import { FatalError } from '@output.ai/core';
4
5
 
5
6
  const scanDir = dir => {
6
7
  try {
7
8
  return readdirSync( dir, { withFileTypes: true } );
8
9
  } catch ( error ) {
9
- throw new Error( `Error scanning directory "${dir}"`, { cause: error } );
10
+ throw new FatalError( `Error scanning directory "${dir}"`, { cause: error } );
10
11
  }
11
12
  };
12
13
 
@@ -14,7 +15,7 @@ const loadFile = path => {
14
15
  try {
15
16
  return readFileSync( path, 'utf-8' );
16
17
  } catch ( error ) {
17
- throw new Error( `Error reading file "${path}"`, { cause: error } );
18
+ throw new FatalError( `Error reading file "${path}"`, { cause: error } );
18
19
  }
19
20
  };
20
21
 
package/src/parser.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import matter from 'gray-matter';
2
+ import { FatalError } from '@output.ai/core';
2
3
 
3
4
  export function parsePrompt( raw ) {
4
- const { data, content } = matter( raw );
5
+ const { data: config, content } = matter( raw );
5
6
 
6
7
  if ( !content || content.trim() === '' ) {
7
- throw new Error( 'Prompt file has no content after frontmatter' );
8
+ throw new FatalError( 'Prompt file has no content after frontmatter' );
8
9
  }
9
10
 
10
11
  const infoExtractor = /<(system|user|assistant|tool)>([\s\S]*?)<\/\1>/gm;
@@ -16,12 +17,12 @@ export function parsePrompt( raw ) {
16
17
  const contentPreview = content.substring( 0, 200 );
17
18
  const ellipsis = content.length > 200 ? '...' : '';
18
19
 
19
- throw new Error(
20
- `No valid message blocks found in prompt file.
21
- Expected format: <system>...</system>, <user>...</user>, etc.
20
+ throw new FatalError(
21
+ `No valid message blocks found in prompt file.
22
+ Expected format: <system>...</system>, <user>...</user>, etc.
22
23
  Content preview: ${contentPreview}${ellipsis}`
23
24
  );
24
25
  }
25
26
 
26
- return { data, messages };
27
+ return { config, messages };
27
28
  }
@@ -13,7 +13,7 @@ model: claude-3-5-sonnet-20241022
13
13
 
14
14
  const result = parsePrompt( raw );
15
15
 
16
- expect( result.data ).toEqual( {
16
+ expect( result.config ).toEqual( {
17
17
  provider: 'anthropic',
18
18
  model: 'claude-3-5-sonnet-20241022'
19
19
  } );
@@ -2,30 +2,15 @@ import { parsePrompt } from './parser.js';
2
2
  import { Liquid } from 'liquidjs';
3
3
  import { loadContent } from './load_content.js';
4
4
  import { validatePrompt } from './prompt_validations.js';
5
+ import { FatalError } from '@output.ai/core';
5
6
 
6
7
  const liquid = new Liquid();
7
8
 
8
- /**
9
- * Render a single message with template variables.
10
- *
11
- * @param {string} role - The message role
12
- * @param {string} content - The message content template
13
- * @param {Record<string, string | number>} values - Variables to interpolate
14
- * @param {string} promptName - Name of the prompt (for error messages)
15
- * @param {number} index - Message index (for error messages)
16
- * @returns {{role: string, content: string}} Rendered message object
17
- */
18
- const renderMessage = ( role, content, values, promptName, index ) => {
9
+ const renderPrompt = ( name, content, values ) => {
19
10
  try {
20
- return {
21
- role,
22
- content: liquid.parseAndRenderSync( content, values )
23
- };
11
+ return liquid.parseAndRenderSync( content, values );
24
12
  } catch ( error ) {
25
- throw new Error(
26
- `Failed to render template in message ${index + 1} (role: ${role}) of prompt "${promptName}": ${error.message}`,
27
- { cause: error }
28
- );
13
+ throw new FatalError( `Failed to render template in prompt "${name}": ${error.message}`, { cause: error } );
29
14
  }
30
15
  };
31
16
 
@@ -36,20 +21,20 @@ const renderMessage = ( role, content, values, promptName, index ) => {
36
21
  * @param {Record<string, string | number>} [values] - Variables to interpolate
37
22
  * @returns {Prompt} Loaded and rendered prompt object
38
23
  */
39
- export const loadPrompt = ( name, values ) => {
24
+ export const loadPrompt = ( name, values = {} ) => {
40
25
  const promptContent = loadContent( `${name}.prompt` );
41
26
  if ( !promptContent ) {
42
- throw new Error( `Prompt ${name} not found.` );
27
+ throw new FatalError( `Prompt ${name} not found.` );
43
28
  }
44
29
 
45
- const { data: config, messages } = parsePrompt( promptContent );
30
+ const renderedContent = renderPrompt( name, promptContent, values );
31
+
32
+ const { config, messages } = parsePrompt( renderedContent );
46
33
 
47
34
  const prompt = {
48
35
  name,
49
36
  config,
50
- messages: messages.map( ( { role, content }, index ) =>
51
- renderMessage( role, content, values, name, index )
52
- )
37
+ messages
53
38
  };
54
39
 
55
40
  validatePrompt( prompt );
@@ -1,21 +1,16 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { loadPrompt } from './prompt_loader.js';
3
3
 
4
- // Mock dependencies
4
+ // Mock dependencies that perform I/O or validation
5
5
  vi.mock( './load_content.js', () => ( {
6
6
  loadContent: vi.fn()
7
7
  } ) );
8
8
 
9
- vi.mock( './parser.js', () => ( {
10
- parsePrompt: vi.fn()
11
- } ) );
12
-
13
9
  vi.mock( './prompt_validations.js', () => ( {
14
10
  validatePrompt: vi.fn()
15
11
  } ) );
16
12
 
17
13
  import { loadContent } from './load_content.js';
18
- import { parsePrompt } from './parser.js';
19
14
  import { validatePrompt } from './prompt_validations.js';
20
15
 
21
16
  describe( 'loadPrompt', () => {
@@ -31,10 +26,6 @@ model: claude-3-5-sonnet-20241022
31
26
  <user>Hello {{ name }}!</user>`;
32
27
 
33
28
  loadContent.mockReturnValue( promptContent );
34
- parsePrompt.mockReturnValue( {
35
- data: { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' },
36
- messages: [ { role: 'user', content: 'Hello {{ name }}!' } ]
37
- } );
38
29
 
39
30
  const result = loadPrompt( 'test', { name: 'World' } );
40
31
 
@@ -54,3 +45,88 @@ model: claude-3-5-sonnet-20241022
54
45
  } );
55
46
 
56
47
  } );
48
+
49
+ describe( 'loadPrompt - template hydration in headers (integration tests)', () => {
50
+ beforeEach( () => {
51
+ vi.clearAllMocks();
52
+ } );
53
+
54
+ it( 'should hydrate template variables in YAML headers', () => {
55
+ const promptContent = `---
56
+ provider: {{ provider_name }}
57
+ model: {{ model_id }}
58
+ temperature: 0.7
59
+ ---
60
+
61
+ <user>Hello</user>`;
62
+
63
+ loadContent.mockReturnValue( promptContent );
64
+
65
+ const result = loadPrompt( 'test', {
66
+ provider_name: 'anthropic',
67
+ model_id: 'claude-sonnet-4'
68
+ } );
69
+
70
+ expect( result.config.provider ).toBe( 'anthropic' );
71
+ expect( result.config.model ).toBe( 'claude-sonnet-4' );
72
+ expect( result.config.temperature ).toBe( 0.7 );
73
+ } );
74
+
75
+ it( 'should hydrate template variables in both headers and messages', () => {
76
+ const promptContent = `---
77
+ provider: {{ provider }}
78
+ model: {{ model }}
79
+ ---
80
+
81
+ <user>Tell me about {{ topic }}</user>`;
82
+
83
+ loadContent.mockReturnValue( promptContent );
84
+
85
+ const result = loadPrompt( 'test', {
86
+ provider: 'openai',
87
+ model: 'gpt-4',
88
+ topic: 'testing'
89
+ } );
90
+
91
+ expect( result.config.provider ).toBe( 'openai' );
92
+ expect( result.config.model ).toBe( 'gpt-4' );
93
+ expect( result.messages[0].content ).toBe( 'Tell me about testing' );
94
+ } );
95
+
96
+ it( 'should render undefined template variables as null', () => {
97
+ const promptContent = `---
98
+ provider: {{ undefined_var }}
99
+ model: claude-3-5-sonnet-20241022
100
+ ---
101
+
102
+ <user>Hello</user>`;
103
+
104
+ loadContent.mockReturnValue( promptContent );
105
+
106
+ const result = loadPrompt( 'test', {} );
107
+
108
+ // Liquid renders undefined variables as empty, which becomes null in YAML
109
+ expect( result.config.provider ).toBe( null );
110
+ expect( result.config.model ).toBe( 'claude-3-5-sonnet-20241022' );
111
+ } );
112
+
113
+ it( 'should handle complex template expressions in headers', () => {
114
+ const promptContent = `---
115
+ provider: anthropic
116
+ model: {{ base_model }}-{{ version }}
117
+ temperature: 0.7
118
+ ---
119
+
120
+ <user>Hello</user>`;
121
+
122
+ loadContent.mockReturnValue( promptContent );
123
+
124
+ const result = loadPrompt( 'test', {
125
+ base_model: 'claude-sonnet',
126
+ version: '4'
127
+ } );
128
+
129
+ expect( result.config.model ).toBe( 'claude-sonnet-4' );
130
+ } );
131
+
132
+ } );