@outputai/llm 0.4.1-dev.92bc2fb.0 → 0.4.1-next.6bc541c.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.4.1-dev.92bc2fb.0",
3
+ "version": "0.4.1-next.6bc541c.0",
4
4
  "description": "Framework abstraction to interact with LLM models",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -23,7 +23,8 @@
23
23
  "entities": "8.0.0",
24
24
  "gray-matter": "4.0.3",
25
25
  "liquidjs": "10.25.7",
26
- "@outputai/core": "0.4.1-dev.92bc2fb.0"
26
+ "undici": "8.1.0",
27
+ "@outputai/core": "0.4.1-next.6bc541c.0"
27
28
  },
28
29
  "license": "Apache-2.0",
29
30
  "publishConfig": {
package/src/ai_model.js CHANGED
@@ -1,12 +1,29 @@
1
- import { bedrock } from '@ai-sdk/amazon-bedrock';
2
- import { anthropic } from '@ai-sdk/anthropic';
3
- import { azure } from '@ai-sdk/azure';
4
- import { vertex } from '@ai-sdk/google-vertex';
5
- import { openai } from '@ai-sdk/openai';
6
- import { perplexity } from '@ai-sdk/perplexity';
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
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
+ };
8
26
 
9
- export const builtInProviders = { azure, anthropic, openai, vertex, bedrock, perplexity };
10
27
  export const providers = { ...builtInProviders };
11
28
 
12
29
  const registerProviderSchema = z.object( {
@@ -14,20 +31,17 @@ const registerProviderSchema = z.object( {
14
31
  providerFn: z.function()
15
32
  } );
16
33
 
34
+ const toolConfigSchema = z.record( z.string(), z.unknown() );
35
+
17
36
  export function registerProvider( name, providerFn ) {
18
37
  const result = registerProviderSchema.safeParse( { name, providerFn } );
19
38
  if ( !result.success ) {
20
- throw new ValidationError(
21
- `Invalid provider registration: ${z.prettifyError( result.error )}`,
22
- { cause: result.error }
23
- );
39
+ throw new ValidationError( `Invalid provider registration: ${z.prettifyError( result.error )}` );
24
40
  }
25
41
  providers[name] = providerFn;
26
42
  }
27
43
 
28
- export function getRegisteredProviders() {
29
- return Object.keys( providers );
30
- }
44
+ export const getRegisteredProviders = () => Object.keys( providers );
31
45
 
32
46
  export function loadModel( prompt ) {
33
47
  const config = prompt?.config;
@@ -49,10 +63,8 @@ export function loadModel( prompt ) {
49
63
  const provider = providers[providerName];
50
64
 
51
65
  if ( !provider ) {
52
- const validProviders = Object.keys( providers ).join( ', ' );
53
- throw new Error(
54
- `Invalid provider "${providerName}". Valid providers: ${validProviders}`
55
- );
66
+ const availableProviders = Object.keys( providers ).join( ', ' );
67
+ throw new Error( `Invalid provider "${providerName}". Valid providers: ${availableProviders}` );
56
68
  }
57
69
 
58
70
  return provider( modelName );
@@ -60,7 +72,12 @@ export function loadModel( prompt ) {
60
72
 
61
73
  export function loadTools( prompt ) {
62
74
  const config = prompt?.config;
63
- const toolsConfig = config?.tools;
75
+
76
+ if ( !config ) {
77
+ throw new Error( 'Prompt is missing config object' );
78
+ }
79
+
80
+ const { tools: toolsConfig, provider: providerName } = config;
64
81
 
65
82
  if ( !toolsConfig ) {
66
83
  return null;
@@ -84,20 +101,15 @@ export function loadTools( prompt ) {
84
101
  return null;
85
102
  }
86
103
 
87
- const providerName = config.provider;
88
104
  const provider = providers[providerName];
89
105
 
90
106
  if ( !provider ) {
91
- const validProviders = Object.keys( providers ).join( ', ' );
92
- throw new Error(
93
- `Invalid provider "${providerName}". Valid providers: ${validProviders}`
94
- );
107
+ const availableProviders = Object.keys( providers ).join( ', ' );
108
+ throw new Error( `Invalid provider "${providerName}". Valid providers: ${availableProviders}` );
95
109
  }
96
110
 
97
111
  if ( !provider.tools || typeof provider.tools !== 'object' ) {
98
- throw new Error(
99
- `Provider "${providerName}" does not support provider-specific tools.`
100
- );
112
+ throw new Error( `Provider "${providerName}" does not support provider-specific tools.` );
101
113
  }
102
114
 
103
115
  const tools = {};
@@ -109,18 +121,14 @@ export function loadTools( prompt ) {
109
121
  const availableTools = Object.keys( provider.tools )
110
122
  .filter( key => typeof provider.tools[key] === 'function' )
111
123
  .join( ', ' );
124
+ const toolsMessage = availableTools ? `Available tools: ${availableTools}` : 'No tools are available';
112
125
 
113
- throw new Error(
114
- `Unknown tool "${toolName}" for provider "${providerName}".` +
115
- ( availableTools ? ` Available tools: ${availableTools}` : '' )
116
- );
126
+ throw new Error( `Unknown tool "${toolName}" for provider "${providerName}". ${toolsMessage}` );
117
127
  }
118
128
 
119
- if ( typeof toolConfig !== 'object' || toolConfig === null ) {
120
- throw new Error(
121
- `Configuration for tool "${toolName}" must be an object. ` +
122
- `Use "${toolName}: {}" for tools without configuration.`
123
- );
129
+ const result = toolConfigSchema.safeParse( toolConfig );
130
+ if ( !result.success ) {
131
+ throw new ValidationError( `Invalid config for tool "${toolName}": ${z.prettifyError( result.error )}` );
124
132
  }
125
133
 
126
134
  tools[toolName] = toolFactory( toolConfig );
@@ -1,10 +1,12 @@
1
1
  import { it, expect, vi, afterEach, describe } from 'vitest';
2
2
 
3
- const openaiImpl = vi.fn( model => `openai:${model}` );
4
- const azureImpl = vi.fn( model => `azure:${model}` );
5
- const anthropicImpl = vi.fn( model => `anthropic:${model}` );
6
- const bedrockImpl = vi.fn( model => `bedrock:${model}` );
7
- const perplexityImpl = vi.fn( model => `perplexity:${model}` );
3
+ const providerFactoryOptions = vi.hoisted( () => ( {} ) );
4
+ const openaiImpl = vi.hoisted( () => vi.fn( model => `openai:${model}` ) );
5
+ const azureImpl = vi.hoisted( () => vi.fn( model => `azure:${model}` ) );
6
+ const anthropicImpl = vi.hoisted( () => vi.fn( model => `anthropic:${model}` ) );
7
+ const bedrockImpl = vi.hoisted( () => vi.fn( model => `bedrock:${model}` ) );
8
+ const perplexityImpl = vi.hoisted( () => vi.fn( model => `perplexity:${model}` ) );
9
+ const vertexImpl = vi.hoisted( () => vi.fn( model => `vertex:${model}` ) );
8
10
 
9
11
  // OpenAI mock with tools support
10
12
  vi.mock( '@ai-sdk/openai', () => {
@@ -12,12 +14,20 @@ vi.mock( '@ai-sdk/openai', () => {
12
14
  openaiMock.tools = {
13
15
  webSearch: ( config = {} ) => ( { type: 'webSearch', config } )
14
16
  };
15
- return { openai: openaiMock };
17
+ return {
18
+ createOpenAI: options => {
19
+ providerFactoryOptions.openai = options;
20
+ return openaiMock;
21
+ }
22
+ };
16
23
  } );
17
24
 
18
25
  // Azure mock without tools support
19
26
  vi.mock( '@ai-sdk/azure', () => ( {
20
- azure: ( ...values ) => azureImpl( ...values )
27
+ createAzure: options => {
28
+ providerFactoryOptions.azure = options;
29
+ return ( ...values ) => azureImpl( ...values );
30
+ }
21
31
  } ) );
22
32
 
23
33
  // Anthropic mock with tools support
@@ -30,7 +40,12 @@ vi.mock( '@ai-sdk/anthropic', () => {
30
40
  codeExecution_20250522: ( config = {} ) => ( { type: 'codeExecution_20250522', config } ),
31
41
  codeExecution_20250825: ( config = {} ) => ( { type: 'codeExecution_20250825', config } )
32
42
  };
33
- return { anthropic: anthropicMock };
43
+ return {
44
+ createAnthropic: options => {
45
+ providerFactoryOptions.anthropic = options;
46
+ return anthropicMock;
47
+ }
48
+ };
34
49
  } );
35
50
 
36
51
  // Bedrock mock with tools support
@@ -42,17 +57,25 @@ vi.mock( '@ai-sdk/amazon-bedrock', () => {
42
57
  textEditor_20250429: ( config = {} ) => ( { type: 'textEditor_20250429', config } ),
43
58
  computer_20241022: ( config = {} ) => ( { type: 'computer_20241022', config } )
44
59
  };
45
- return { bedrock: bedrockMock };
60
+ return {
61
+ createAmazonBedrock: options => {
62
+ providerFactoryOptions.bedrock = options;
63
+ return bedrockMock;
64
+ }
65
+ };
46
66
  } );
47
67
 
48
68
  // Perplexity mock
49
69
  vi.mock( '@ai-sdk/perplexity', () => ( {
50
- perplexity: ( ...values ) => perplexityImpl( ...values )
70
+ createPerplexity: options => {
71
+ providerFactoryOptions.perplexity = options;
72
+ return ( ...values ) => perplexityImpl( ...values );
73
+ }
51
74
  } ) );
52
75
 
53
76
  // Vertex mock with tools support
54
77
  vi.mock( '@ai-sdk/google-vertex', () => {
55
- const vertexFn = model => `vertex:${model}`;
78
+ const vertexFn = ( ...values ) => vertexImpl( ...values );
56
79
  vertexFn.tools = {
57
80
  googleSearch: ( config = {} ) => ( { type: 'googleSearch', config } ),
58
81
  fileSearch: ( config = {} ) => ( { type: 'fileSearch', config } ),
@@ -62,7 +85,12 @@ vi.mock( '@ai-sdk/google-vertex', () => {
62
85
  codeExecution: ( config = {} ) => ( { type: 'codeExecution', config } ),
63
86
  vertexRagStore: ( config = {} ) => ( { type: 'vertexRagStore', config } )
64
87
  };
65
- return { vertex: vertexFn };
88
+ return {
89
+ createVertex: options => {
90
+ providerFactoryOptions.vertex = options;
91
+ return vertexFn;
92
+ }
93
+ };
66
94
  } );
67
95
 
68
96
  import { loadModel, loadTools, registerProvider, getRegisteredProviders, providers, builtInProviders } from './ai_model.js';
@@ -73,6 +101,17 @@ afterEach( async () => {
73
101
  } );
74
102
 
75
103
  describe( 'loadModel', () => {
104
+ it( 'initializes built-in providers with custom fetch', () => {
105
+ expect( providerFactoryOptions ).toMatchObject( {
106
+ azure: { fetch: expect.any( Function ) },
107
+ anthropic: { fetch: expect.any( Function ) },
108
+ openai: { fetch: expect.any( Function ) },
109
+ vertex: { fetch: expect.any( Function ) },
110
+ bedrock: { fetch: expect.any( Function ) },
111
+ perplexity: { fetch: expect.any( Function ) }
112
+ } );
113
+ } );
114
+
76
115
  it( 'loads model using selected provider', () => {
77
116
  const result = loadModel( { config: { provider: 'openai', model: 'gpt-4o-mini' } } );
78
117
 
@@ -617,7 +656,7 @@ describe( 'loadTools', () => {
617
656
  provider: 'vertex',
618
657
  tools: { googleSearch: null }
619
658
  }
620
- } ) ).toThrow( 'Configuration for tool "googleSearch" must be an object' );
659
+ } ) ).toThrow( /Invalid config for tool "googleSearch".*expected record, received null/s );
621
660
  } );
622
661
 
623
662
  it( 'throws error when tool config is a string', () => {
@@ -626,7 +665,7 @@ describe( 'loadTools', () => {
626
665
  provider: 'vertex',
627
666
  tools: { googleSearch: 'MODE_DYNAMIC' }
628
667
  }
629
- } ) ).toThrow( 'Configuration for tool "googleSearch" must be an object' );
668
+ } ) ).toThrow( /Invalid config for tool "googleSearch".*expected record, received string/s );
630
669
  } );
631
670
 
632
671
  it( 'throws error for unknown tool on Bedrock with dynamic tool listing', () => {
package/src/ai_sdk.js CHANGED
@@ -64,10 +64,10 @@ export const hydratePromptTemplate = ( prompt, variables, promptDir, callerSkill
64
64
  return { loadedPrompt: meta, allVariables: variables, tools };
65
65
  };
66
66
 
67
- export async function generateText( { prompt, variables, promptDir, skills = [], maxSteps = 10, ...extraAiSdkOptions } ) {
67
+ export async function generateText( { prompt, variables, promptDir, skills = [], maxSteps = 10, ...aiSdkArgs } ) {
68
68
  const callerSkills = typeof skills === 'function' ? await skills( variables ) : skills;
69
69
  const { loadedPrompt, allVariables, tools } =
70
- hydratePromptTemplate( prompt, variables, promptDir, callerSkills, extraAiSdkOptions.tools );
70
+ hydratePromptTemplate( prompt, variables, promptDir, callerSkills, aiSdkArgs.tools );
71
71
  const hasTools = Object.keys( tools ).length > 0;
72
72
 
73
73
  validateGenerateTextArgs( { prompt, variables: allVariables } );
@@ -78,9 +78,10 @@ export async function generateText( { prompt, variables, promptDir, skills = [],
78
78
  try {
79
79
  const response = await AI.generateText( {
80
80
  ...loadAiSdkOptionsFromPrompt( loadedPrompt ),
81
- ...extraAiSdkOptions,
81
+ maxRetries: 0,
82
+ ...aiSdkArgs,
82
83
  ...( hasTools ? { tools } : {} ),
83
- ...( hasTools && !extraAiSdkOptions.stopWhen ? { stopWhen: stepCountIs( maxSteps ) } : {} )
84
+ ...( hasTools && !aiSdkArgs.stopWhen ? { stopWhen: stepCountIs( maxSteps ) } : {} )
84
85
  } );
85
86
  return wrapTextResponse( { traceId, modelId, response } );
86
87
  } catch ( error ) {
@@ -89,7 +90,7 @@ export async function generateText( { prompt, variables, promptDir, skills = [],
89
90
  }
90
91
  }
91
92
 
92
- export function streamText( { prompt, variables, onFinish, onError, ...restOptions } ) {
93
+ export function streamText( { prompt, variables, onFinish, onError, ...aiSdkArgs } ) {
93
94
  validateStreamTextArgs( { prompt, variables } );
94
95
  const loadedPrompt = loadPrompt( prompt, variables );
95
96
  const traceId = startTrace( { name: 'streamText', prompt, variables, loadedPrompt } );
@@ -98,7 +99,8 @@ export function streamText( { prompt, variables, onFinish, onError, ...restOptio
98
99
  try {
99
100
  return AI.streamText( {
100
101
  ...loadAiSdkOptionsFromPrompt( loadedPrompt ),
101
- ...restOptions,
102
+ maxRetries: 0,
103
+ ...aiSdkArgs,
102
104
  ...wrapStreamOnFinishResponse( { traceId, modelId, onFinish } ),
103
105
  onError( event ) {
104
106
  endTraceWithError( { traceId, error: event.error } );
@@ -149,6 +149,7 @@ describe( 'ai_sdk', () => {
149
149
  model: 'MODEL',
150
150
  messages: basePrompt.messages,
151
151
  temperature: 0.3,
152
+ maxRetries: 0,
152
153
  providerOptions: basePrompt.config.providerOptions
153
154
  } );
154
155
  expect( result ).toBe( generateTextAiFixture.response );
@@ -182,6 +183,7 @@ describe( 'ai_sdk', () => {
182
183
  expect( aiFns.generateText ).toHaveBeenCalledWith( {
183
184
  model: 'MODEL',
184
185
  messages: promptWithProviderOptions.messages,
186
+ maxRetries: 0,
185
187
  providerOptions: {
186
188
  thinking: {
187
189
  type: 'enabled',
@@ -362,6 +364,7 @@ describe( 'ai_sdk', () => {
362
364
  model: 'MODEL',
363
365
  messages: basePrompt.messages,
364
366
  temperature: 0.3,
367
+ maxRetries: 0,
365
368
  providerOptions: basePrompt.config.providerOptions,
366
369
  onFinish: expect.any( Function ),
367
370
  onError: expect.any( Function )
@@ -13,8 +13,6 @@ export const endTraceWithError = ( { traceId, error } ) => {
13
13
  export const endTraceWithSuccess = ( { traceId, modelId, response, cost, ...extra } ) => {
14
14
  const { totalUsage: usage, text: result, providerMetadata } = response;
15
15
  Tracing.addEventAttribute( { eventId: traceId, name: Tracing.Attribute.COST, value: cost } );
16
- Tracing.addEventAttribute( { eventId: traceId, name: Tracing.Attribute.TOKEN_USAGE, value: usage } );
17
- Tracing.addEventEnd( { id: traceId, details: { result, providerMetadata, ...extra } } );
16
+ Tracing.addEventEnd( { id: traceId, details: { result, usage, providerMetadata, ...extra } } );
18
17
  emitEvent( 'cost:llm:request', { modelId, cost, usage } );
19
- emitEvent( 'token_usage:llm:request', { modelId, usage } );
20
18
  };
@@ -1,6 +1,4 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import buildTraceTree from '../../../core/src/tracing/tools/build_trace_tree.js';
3
- import { EventAction } from '../../../core/src/tracing/trace_consts.js';
4
2
 
5
3
  vi.mock( '@outputai/core/sdk_activity_integration', () => ( {
6
4
  Tracing: {
@@ -9,8 +7,7 @@ vi.mock( '@outputai/core/sdk_activity_integration', () => ( {
9
7
  addEventAttribute: vi.fn(),
10
8
  addEventEnd: vi.fn(),
11
9
  Attribute: {
12
- COST: 'cost',
13
- TOKEN_USAGE: 'token_usage'
10
+ COST: 'cost'
14
11
  }
15
12
  },
16
13
  emitEvent: vi.fn()
@@ -57,7 +54,7 @@ describe( 'trace utils', () => {
57
54
  } );
58
55
 
59
56
  describe( 'endTraceWithSuccess', () => {
60
- it( 'adds cost + token_usage attributes, ends the trace without usage in output, and emits both cost and token_usage events', () => {
57
+ it( 'adds cost attribute, ends the trace with response fields and extra details, and emits cost:llm:request', () => {
61
58
  const cost = { total: 0.01, components: [] };
62
59
  const usage = { inputTokens: 2, outputTokens: 3 };
63
60
  const response = {
@@ -79,63 +76,20 @@ describe( 'trace utils', () => {
79
76
  name: 'cost',
80
77
  value: cost
81
78
  } );
82
- expect( tracing.addEventAttribute ).toHaveBeenCalledWith( {
83
- eventId: 'trace-a',
84
- name: 'token_usage',
85
- value: usage
86
- } );
87
79
  expect( tracing.addEventEnd ).toHaveBeenCalledWith( {
88
80
  id: 'trace-a',
89
81
  details: {
90
82
  result: 'hello',
83
+ usage,
91
84
  providerMetadata: { provider: 'x' },
92
85
  sourcesFromTools: [ { url: 'https://u.test', title: '' } ]
93
86
  }
94
87
  } );
95
- expect( tracing.addEventEnd.mock.calls[0][0].details ).not.toHaveProperty( 'usage' );
96
88
  expect( emitEvent ).toHaveBeenCalledWith( 'cost:llm:request', {
97
89
  modelId: 'my-model',
98
90
  cost,
99
91
  usage
100
92
  } );
101
- expect( emitEvent ).toHaveBeenCalledWith( 'token_usage:llm:request', {
102
- modelId: 'my-model',
103
- usage
104
- } );
105
- } );
106
-
107
- it( 'produces an llm trace node with attributes.token_usage and no output.usage when fed through buildTraceTree', () => {
108
- const usage = { inputTokens: 12, outputTokens: 7, cachedInputTokens: 3, totalTokens: 22 };
109
- const cost = { total: 0.0042, components: [ { name: 'input_tokens', value: 0.002 } ] };
110
- const response = {
111
- text: 'tree result',
112
- totalUsage: usage,
113
- providerMetadata: { provider: 'p' }
114
- };
115
-
116
- // Capture the calls the wrapper makes against Tracing/emit, and translate them into trace
117
- // entries — what buildTraceTree consumes server-side to materialize the persisted trace JSON.
118
- endTraceWithSuccess( { traceId: 'llm-1', modelId: 'm', response, cost } );
119
-
120
- const entries = [
121
- { kind: 'workflow', action: EventAction.START, name: 'wf', id: 'wf', parentId: undefined, details: {}, timestamp: 1 },
122
- { kind: 'llm', action: EventAction.START, name: 'generateText', id: 'llm-1', parentId: 'wf', details: { prompt: 'p' }, timestamp: 10 }
123
- ];
124
- for ( const call of Tracing.addEventAttribute.mock.calls ) {
125
- entries.push( { id: call[0].eventId, action: EventAction.ADD_ATTR, details: { name: call[0].name, value: call[0].value }, timestamp: 20 } );
126
- }
127
- const endDetails = Tracing.addEventEnd.mock.calls[0][0].details;
128
- entries.push( { id: 'llm-1', action: EventAction.END, details: endDetails, timestamp: 30 } );
129
- entries.push( { id: 'wf', action: EventAction.END, details: {}, timestamp: 40 } );
130
-
131
- const tree = buildTraceTree( entries );
132
- const llmNode = tree.children[0];
133
-
134
- expect( llmNode.kind ).toBe( 'llm' );
135
- expect( llmNode.attributes.token_usage ).toEqual( usage );
136
- expect( llmNode.attributes.cost ).toEqual( cost );
137
- expect( llmNode.output ).not.toHaveProperty( 'usage' );
138
- expect( llmNode.output ).toMatchObject( { result: 'tree result', providerMetadata: { provider: 'p' } } );
139
93
  } );
140
94
  } );
141
95
  } );