@outputai/llm 0.6.1-next.fc6a93e.0 → 0.7.1-dev.144d64f.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outputai/llm",
3
- "version": "0.6.1-next.fc6a93e.0",
3
+ "version": "0.7.1-dev.144d64f.0",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -9,21 +9,29 @@
9
9
  "./src"
10
10
  ],
11
11
  "dependencies": {
12
- "@ai-sdk/amazon-bedrock": "4.0.96",
13
- "@ai-sdk/anthropic": "3.0.71",
14
- "@ai-sdk/azure": "3.0.54",
15
- "@ai-sdk/google-vertex": "4.0.112",
16
- "@ai-sdk/openai": "3.0.53",
17
- "@ai-sdk/perplexity": "3.0.29",
18
- "@exalabs/ai-sdk": "2.0.1",
19
- "@perplexity-ai/ai-sdk": "0.1.3",
20
- "@tavily/ai-sdk": "0.4.1",
21
- "ai": "6.0.168",
22
12
  "entities": "8.0.0",
23
13
  "gray-matter": "4.0.3",
24
14
  "liquidjs": "10.25.7",
25
15
  "undici": "8.1.0",
26
- "@outputai/core": "0.6.1-next.fc6a93e.0"
16
+ "@outputai/core": "0.7.1-dev.144d64f.0"
17
+ },
18
+ "devDependencies": {
19
+ "ai": "6.0.168",
20
+ "@ai-sdk/amazon-bedrock": "4.0.111",
21
+ "@ai-sdk/anthropic": "3.0.81",
22
+ "@ai-sdk/azure": "3.0.68",
23
+ "@ai-sdk/google-vertex": "4.0.140",
24
+ "@ai-sdk/openai": "3.0.67",
25
+ "@ai-sdk/perplexity": "3.0.33"
26
+ },
27
+ "peerDependencies": {
28
+ "ai": ">=6 <7",
29
+ "@ai-sdk/amazon-bedrock": ">=4 <5",
30
+ "@ai-sdk/anthropic": ">=3 <4",
31
+ "@ai-sdk/azure": ">=3 <4",
32
+ "@ai-sdk/google-vertex": ">=4 <5",
33
+ "@ai-sdk/openai": ">=3 <4",
34
+ "@ai-sdk/perplexity": ">=3 <4"
27
35
  },
28
36
  "license": "Apache-2.0",
29
37
  "publishConfig": {
package/src/agent.js CHANGED
@@ -5,7 +5,7 @@ import { loadAiSdkTextOptions } from './ai_sdk_options.js';
5
5
  import { prepareTextPrompt } from './prompt/prepare_text.js';
6
6
  import { startTrace, endTraceWithError } from './utils/trace.js';
7
7
  import { wrapTextResponse, wrapStreamOnFinishResponse } from './utils/response_wrappers.js';
8
- import { ROLE, isRole, getContent } from './utils/message.js';
8
+ import { ROLE, isRole } from './utils/message.js';
9
9
  export { skill } from './prompt/skill.js';
10
10
 
11
11
  export const createMemoryConversationStore = () => {
@@ -47,12 +47,13 @@ export class Agent extends AIToolLoopAgent {
47
47
 
48
48
  // Extract system messages as `instructions` for the ToolLoopAgent constructor
49
49
  // and keep user messages for generate() calls — avoids provider errors
50
- // with multiple system messages during multi-step tool loops
51
- const systemContent = allMessages.filter( isRole( ROLE.SYSTEM ) ).map( getContent ).join( '\n\n' );
50
+ // with multiple system messages during multi-step tool loops.
51
+ // Pass message objects (not a string) so per-message providerOptions are preserved.
52
+ const systemMessages = allMessages.filter( isRole( ROLE.SYSTEM ) );
52
53
 
53
54
  super( {
54
55
  ...constructorOptions,
55
- ...( systemContent ? { instructions: systemContent } : {} ),
56
+ ...( systemMessages.length > 0 ? { instructions: systemMessages } : {} ),
56
57
  ...( tools ? { tools } : {} ),
57
58
  stopWhen: stopWhen ?? stepCountIs( maxSteps ),
58
59
  ...rest
package/src/agent.spec.js CHANGED
@@ -86,7 +86,8 @@ vi.mock( './utils/trace.js', () => ( {
86
86
 
87
87
  vi.mock( './utils/response_wrappers.js', () => ( {
88
88
  wrapTextResponse: ( ...args ) => wrapMocks.wrapTextResponse( ...args ),
89
- wrapStreamOnFinishResponse: ( ...args ) => wrapMocks.wrapStreamOnFinishResponse( ...args )
89
+ wrapStreamOnFinishResponse: ( ...args ) =>
90
+ wrapMocks.wrapStreamOnFinishResponse( ...args )
90
91
  } ) );
91
92
 
92
93
  vi.mock( './prompt/skill.js', () => ( {
@@ -131,7 +132,9 @@ describe( 'Agent', () => {
131
132
  aiMocks.superConstructor.mockReset();
132
133
  aiMocks.superGenerate.mockReset().mockResolvedValue( aiResponse );
133
134
  aiMocks.superStream.mockReset().mockReturnValue( { textStream: 'stream' } );
134
- aiMocks.stepCountIs.mockReset().mockImplementation( count => ( { type: 'step-count', count } ) );
135
+ aiMocks.stepCountIs
136
+ .mockReset()
137
+ .mockImplementation( count => ( { type: 'step-count', count } ) );
135
138
 
136
139
  promptMocks.prepareTextPrompt.mockReset().mockReturnValue( {
137
140
  loadedPrompt,
@@ -143,7 +146,9 @@ describe( 'Agent', () => {
143
146
  traceMocks.startTrace.mockReset().mockReturnValue( 'trace-id' );
144
147
  traceMocks.endTraceWithError.mockReset();
145
148
 
146
- wrapMocks.wrapTextResponse.mockReset().mockImplementation( async ( { response } ) => response );
149
+ wrapMocks.wrapTextResponse
150
+ .mockReset()
151
+ .mockImplementation( async ( { response } ) => response );
147
152
  wrapMocks.wrapStreamOnFinishResponse.mockReset().mockReturnValue( {
148
153
  onFinish: vi.fn()
149
154
  } );
@@ -175,7 +180,9 @@ describe( 'Agent', () => {
175
180
 
176
181
  it( 'prepares the prompt using the resolved invocation dir', async () => {
177
182
  const { Agent } = await importSut();
178
- const skills = [ { name: 'style', description: 'Style', instructions: '# Style' } ];
183
+ const skills = [
184
+ { name: 'style', description: 'Style', instructions: '# Style' }
185
+ ];
179
186
  const tools = { search: { description: 'Search' } };
180
187
 
181
188
  new Agent( {
@@ -199,9 +206,11 @@ describe( 'Agent', () => {
199
206
 
200
207
  new Agent( { prompt: 'test@v1', promptDir: '/explicit/prompts' } );
201
208
 
202
- expect( promptMocks.prepareTextPrompt ).toHaveBeenCalledWith( expect.objectContaining( {
203
- promptDir: '/explicit/prompts'
204
- } ) );
209
+ expect( promptMocks.prepareTextPrompt ).toHaveBeenCalledWith(
210
+ expect.objectContaining( {
211
+ promptDir: '/explicit/prompts'
212
+ } )
213
+ );
205
214
  } );
206
215
 
207
216
  it( 'constructs ToolLoopAgent with text options, instructions, tools, and default stopWhen', async () => {
@@ -215,12 +224,33 @@ describe( 'Agent', () => {
215
224
  model,
216
225
  providerOptions: { test: true },
217
226
  temperature: 0.3,
218
- instructions: 'You are concise.',
227
+ instructions: [ { role: 'system', content: 'You are concise.' } ],
219
228
  tools: preparedTools,
220
229
  stopWhen: { type: 'step-count', count: 10 }
221
230
  } );
222
231
  } );
223
232
 
233
+ it( 'preserves per-message providerOptions on system messages passed as instructions', async () => {
234
+ const { Agent } = await importSut();
235
+ const systemMessage = {
236
+ role: 'system',
237
+ content: 'You are concise.',
238
+ providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } }
239
+ };
240
+ optionMocks.loadAiSdkTextOptions.mockReturnValueOnce( {
241
+ model,
242
+ messages: [ systemMessage, { role: 'user', content: 'Hello' } ]
243
+ } );
244
+
245
+ new Agent( { prompt: 'test@v1' } );
246
+
247
+ expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
248
+ expect.objectContaining( {
249
+ instructions: [ systemMessage ]
250
+ } )
251
+ );
252
+ } );
253
+
224
254
  it( 'omits tools when prompt preparation returns null tools', async () => {
225
255
  const { Agent } = await importSut();
226
256
  promptMocks.prepareTextPrompt.mockReturnValueOnce( {
@@ -234,7 +264,7 @@ describe( 'Agent', () => {
234
264
  model,
235
265
  providerOptions: { test: true },
236
266
  temperature: 0.3,
237
- instructions: 'You are concise.',
267
+ instructions: [ { role: 'system', content: 'You are concise.' } ],
238
268
  stopWhen: { type: 'step-count', count: 10 }
239
269
  } );
240
270
  } );
@@ -246,9 +276,11 @@ describe( 'Agent', () => {
246
276
  new Agent( { prompt: 'test@v1', stopWhen } );
247
277
 
248
278
  expect( aiMocks.stepCountIs ).not.toHaveBeenCalled();
249
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith( expect.objectContaining( {
250
- stopWhen
251
- } ) );
279
+ expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
280
+ expect.objectContaining( {
281
+ stopWhen
282
+ } )
283
+ );
252
284
  } );
253
285
 
254
286
  it( 'passes custom constructor options through', async () => {
@@ -256,10 +288,12 @@ describe( 'Agent', () => {
256
288
 
257
289
  new Agent( { prompt: 'test@v1', temperature: 0.8, seed: 42 } );
258
290
 
259
- expect( aiMocks.superConstructor ).toHaveBeenCalledWith( expect.objectContaining( {
260
- temperature: 0.8,
261
- seed: 42
262
- } ) );
291
+ expect( aiMocks.superConstructor ).toHaveBeenCalledWith(
292
+ expect.objectContaining( {
293
+ temperature: 0.8,
294
+ seed: 42
295
+ } )
296
+ );
263
297
  } );
264
298
 
265
299
  it( 'keeps only user prompt messages as initial generate messages', async () => {
@@ -275,7 +309,9 @@ describe( 'Agent', () => {
275
309
 
276
310
  it( 'combines initial, stored, and caller messages for generate', async () => {
277
311
  const store = {
278
- getMessages: vi.fn( () => [ { role: 'assistant', content: 'Stored reply' } ] ),
312
+ getMessages: vi.fn( () => [
313
+ { role: 'assistant', content: 'Stored reply' }
314
+ ] ),
279
315
  addMessages: vi.fn()
280
316
  };
281
317
  const callerMessage = { role: 'user', content: 'New question' };
@@ -336,7 +372,9 @@ describe( 'Agent', () => {
336
372
 
337
373
  it( 'streams with initial, stored, and caller messages', async () => {
338
374
  const store = {
339
- getMessages: vi.fn( () => [ { role: 'assistant', content: 'Stored reply' } ] ),
375
+ getMessages: vi.fn( () => [
376
+ { role: 'assistant', content: 'Stored reply' }
377
+ ] ),
340
378
  addMessages: vi.fn()
341
379
  };
342
380
  const onFinish = vi.fn();
@@ -345,7 +383,12 @@ describe( 'Agent', () => {
345
383
  const { Agent } = await importSut();
346
384
  const agent = new Agent( { prompt: 'test@v1', conversationStore: store } );
347
385
 
348
- const result = await agent.stream( { messages: [ callerMessage ], onFinish, onError, maxRetries: 1 } );
386
+ const result = await agent.stream( {
387
+ messages: [ callerMessage ],
388
+ onFinish,
389
+ onError,
390
+ maxRetries: 1
391
+ } );
349
392
 
350
393
  expect( traceMocks.startTrace ).toHaveBeenCalledWith( {
351
394
  name: 'Agent.stream',
package/src/ai_model.js CHANGED
@@ -1,89 +1,5 @@
1
- import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
2
- import { createAnthropic } from '@ai-sdk/anthropic';
3
- import { createAzure } from '@ai-sdk/azure';
4
- import { createVertex } from '@ai-sdk/google-vertex';
5
- import { createOpenAI } from '@ai-sdk/openai';
6
- import { createPerplexity } from '@ai-sdk/perplexity';
7
- import { ValidationError, z } from '@outputai/core';
8
- import { Agent, fetch } from 'undici';
9
-
10
- const dispatcher = new Agent( {
11
- headersTimeout: 15 * 60 * 1000, // 15 min
12
- bodyTimeout: 15 * 60 * 1000
13
- } );
14
-
15
- const customFetch = ( input, init ) => fetch( input, { dispatcher, ...init } );
16
- const initProvider = factory => factory( { fetch: customFetch } );
17
-
18
- export const builtInProviders = {
19
- azure: initProvider( createAzure ),
20
- anthropic: initProvider( createAnthropic ),
21
- openai: initProvider( createOpenAI ),
22
- vertex: initProvider( createVertex ),
23
- bedrock: initProvider( createAmazonBedrock ),
24
- perplexity: initProvider( createPerplexity )
25
- };
26
-
27
- export const providers = { ...builtInProviders };
28
-
29
- const registerProviderSchema = z.object( {
30
- name: z.string().min( 1, 'Provider name must be a non-empty string' ),
31
- providerFn: z.function()
32
- } );
33
-
34
- const modelPromptSchema = z.object( {
35
- config: z.object( {
36
- provider: z.string().min( 1 ),
37
- model: z.string().min( 1 )
38
- } ).loose()
39
- } ).loose();
40
-
41
- const toolsPromptSchema = z.object( {
42
- config: z.object( {
43
- provider: z.string().min( 1 ),
44
- tools: z.record( z.string(), z.record( z.string(), z.unknown() ) ).optional()
45
- } ).loose()
46
- } ).loose();
47
-
48
- const toolConfigSchema = z.record( z.string(), z.unknown() );
49
-
50
- const parseModelPrompt = prompt => {
51
- const result = modelPromptSchema.safeParse( prompt );
52
- if ( !result.success ) {
53
- throw new ValidationError( `Invalid model prompt config: ${z.prettifyError( result.error )}` );
54
- }
55
- return result.data.config;
56
- };
57
-
58
- const parseToolsPrompt = prompt => {
59
- const result = toolsPromptSchema.safeParse( prompt );
60
- if ( !result.success ) {
61
- throw new ValidationError( `Invalid tools prompt config: ${z.prettifyError( result.error )}` );
62
- }
63
- return result.data.config;
64
- };
65
-
66
- /**
67
- * Register or override an AI SDK provider factory by name.
68
- *
69
- * @param {string} name - Provider name used in prompt frontmatter
70
- * @param {Function} providerFn - Factory function that receives a model id
71
- * @returns {void}
72
- */
73
- export function registerProvider( name, providerFn ) {
74
- const result = registerProviderSchema.safeParse( { name, providerFn } );
75
- if ( !result.success ) {
76
- throw new ValidationError( `Invalid provider registration: ${z.prettifyError( result.error )}` );
77
- }
78
- providers[name] = providerFn;
79
- }
80
-
81
- /**
82
- * List all currently registered provider names.
83
- *
84
- * @returns {string[]} Provider names
85
- */
86
- export const getRegisteredProviders = () => Object.keys( providers );
1
+ import { ValidationError } from '@outputai/core';
2
+ import { getProvider } from './ai_provider.js';
87
3
 
88
4
  /**
89
5
  * Load a text model from a loaded prompt config.
@@ -91,19 +7,7 @@ export const getRegisteredProviders = () => Object.keys( providers );
91
7
  * @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
92
8
  * @returns {unknown} AI SDK language model
93
9
  */
94
- export function loadTextModel( prompt ) {
95
- const config = parseModelPrompt( prompt );
96
- const { provider: providerName, model: modelName } = config;
97
-
98
- const provider = providers[providerName];
99
-
100
- if ( !provider ) {
101
- const availableProviders = Object.keys( providers ).join( ', ' );
102
- throw new Error( `Invalid provider "${providerName}". Valid providers: ${availableProviders}` );
103
- }
104
-
105
- return provider( modelName );
106
- }
10
+ export const loadTextModel = prompt => getProvider( prompt.config.provider )( prompt.config.model );
107
11
 
108
12
  /**
109
13
  * Load an image model from a loaded prompt config.
@@ -111,24 +15,16 @@ export function loadTextModel( prompt ) {
111
15
  * @param {object} prompt - Loaded prompt object with `config.provider` and `config.model`
112
16
  * @returns {unknown} AI SDK image model
113
17
  */
114
- export function loadImageModel( prompt ) {
115
- const config = parseModelPrompt( prompt );
116
- const { provider: providerName, model: modelName } = config;
117
-
118
- const provider = providers[providerName];
119
-
120
- if ( !provider ) {
121
- const availableProviders = Object.keys( providers ).join( ', ' );
122
- throw new Error( `Invalid provider "${providerName}". Valid providers: ${availableProviders}` );
123
- }
18
+ export const loadImageModel = prompt => {
19
+ const { provider: providerName, model } = prompt.config;
20
+ const provider = getProvider( prompt.config.provider );
124
21
 
125
22
  const imageModelFactory = provider.image ?? provider.imageModel;
126
23
  if ( typeof imageModelFactory !== 'function' ) {
127
- throw new Error( `Provider "${providerName}" does not support image models.` );
24
+ throw new ValidationError( `Provider "${providerName}" does not support image models.` );
128
25
  }
129
-
130
- return imageModelFactory( modelName );
131
- }
26
+ return imageModelFactory( model );
27
+ };
132
28
 
133
29
  /**
134
30
  * Load provider-specific tools configured in a prompt.
@@ -136,46 +32,29 @@ export function loadImageModel( prompt ) {
136
32
  * @param {object} prompt - Loaded prompt object with `config.provider` and optional `config.tools`
137
33
  * @returns {Record<string, unknown> | null} AI SDK tools, or null when none are configured
138
34
  */
139
- export function loadTools( prompt ) {
140
- const config = parseToolsPrompt( prompt );
141
- const { tools: toolsConfig, provider: providerName } = config;
35
+ export const loadTools = prompt => {
36
+ const { tools: promptTools, provider: providerName } = prompt.config;
142
37
 
143
- if ( !toolsConfig || Object.keys( toolsConfig ).length === 0 ) {
38
+ if ( Object.keys( promptTools ?? {} ).length === 0 ) {
144
39
  return null;
145
40
  }
146
41
 
147
- const provider = providers[providerName];
148
-
149
- if ( !provider ) {
150
- const availableProviders = Object.keys( providers ).join( ', ' );
151
- throw new Error( `Invalid provider "${providerName}". Valid providers: ${availableProviders}` );
152
- }
42
+ const provider = getProvider( providerName );
153
43
 
154
- if ( !provider.tools || typeof provider.tools !== 'object' ) {
155
- throw new Error( `Provider "${providerName}" does not support provider-specific tools.` );
44
+ if ( !provider.tools ) {
45
+ throw new ValidationError( `Provider "${providerName}" does not support provider-specific tools.` );
156
46
  }
157
47
 
158
- const tools = {};
48
+ const supportedTools = Object.keys( provider.tools );
49
+ const invalidTools = Object.keys( promptTools ).filter( name => !supportedTools.includes( name ) );
159
50
 
160
- for ( const [ toolName, toolConfig ] of Object.entries( toolsConfig ) ) {
161
- const toolFactory = provider.tools[toolName];
162
-
163
- if ( !toolFactory || typeof toolFactory !== 'function' ) {
164
- const availableTools = Object.keys( provider.tools )
165
- .filter( key => typeof provider.tools[key] === 'function' )
166
- .join( ', ' );
167
- const toolsMessage = availableTools ? `Available tools: ${availableTools}` : 'No tools are available';
168
-
169
- throw new Error( `Unknown tool "${toolName}" for provider "${providerName}". ${toolsMessage}` );
170
- }
171
-
172
- const result = toolConfigSchema.safeParse( toolConfig );
173
- if ( !result.success ) {
174
- throw new ValidationError( `Invalid config for tool "${toolName}": ${z.prettifyError( result.error )}` );
175
- }
176
-
177
- tools[toolName] = toolFactory( toolConfig );
51
+ if ( invalidTools.length > 0 ) {
52
+ throw new ValidationError( `Invalid tool(s) ${invalidTools.join( ', ' )} for provider "${providerName}". \
53
+ Available: ${supportedTools.join( ', ' )}.` );
178
54
  }
179
55
 
180
- return tools;
181
- }
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
+ };