@output.ai/llm 0.2.10 → 0.2.12
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 +1 -1
- package/src/ai_model.js +76 -0
- package/src/ai_model.spec.js +583 -14
- package/src/ai_sdk.js +9 -1
- package/src/ai_sdk.spec.js +5 -1
- package/src/index.d.ts +22 -0
- package/src/prompt_validations.js +1 -0
package/package.json
CHANGED
package/src/ai_model.js
CHANGED
|
@@ -33,3 +33,79 @@ export function loadModel( prompt ) {
|
|
|
33
33
|
|
|
34
34
|
return provider( modelName );
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export function loadTools( prompt ) {
|
|
38
|
+
const config = prompt?.config;
|
|
39
|
+
const toolsConfig = config?.tools;
|
|
40
|
+
|
|
41
|
+
if ( !toolsConfig ) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if ( Array.isArray( toolsConfig ) ) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
'tools must be an object with tool configurations, got array. ' +
|
|
48
|
+
'Use "tools: { googleSearch: {} }" not "tools: [googleSearch]"'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ( typeof toolsConfig !== 'object' || toolsConfig === null ) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`tools must be an object, got ${typeof toolsConfig}. ` +
|
|
55
|
+
'Use "tools: { googleSearch: {} }"'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Return null if empty object
|
|
60
|
+
if ( Object.keys( toolsConfig ).length === 0 ) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const providerName = config.provider;
|
|
65
|
+
const provider = providers[providerName];
|
|
66
|
+
|
|
67
|
+
if ( !provider ) {
|
|
68
|
+
const validProviders = Object.keys( providers ).join( ', ' );
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid provider "${providerName}". Valid providers: ${validProviders}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if provider has tools object
|
|
75
|
+
if ( !provider.tools || typeof provider.tools !== 'object' ) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Provider "${providerName}" does not support provider-specific tools.`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const tools = {};
|
|
82
|
+
|
|
83
|
+
for ( const [ toolName, toolConfig ] of Object.entries( toolsConfig ) ) {
|
|
84
|
+
// Access tool factory directly from provider.tools (dynamic!)
|
|
85
|
+
const toolFactory = provider.tools[toolName];
|
|
86
|
+
|
|
87
|
+
if ( !toolFactory || typeof toolFactory !== 'function' ) {
|
|
88
|
+
// Dynamically list available tools for this provider
|
|
89
|
+
const availableTools = Object.keys( provider.tools )
|
|
90
|
+
.filter( key => typeof provider.tools[key] === 'function' )
|
|
91
|
+
.join( ', ' );
|
|
92
|
+
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Unknown tool "${toolName}" for provider "${providerName}".` +
|
|
95
|
+
( availableTools ? ` Available tools: ${availableTools}` : '' )
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if ( typeof toolConfig !== 'object' || toolConfig === null ) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Configuration for tool "${toolName}" must be an object. ` +
|
|
102
|
+
`Use "${toolName}: {}" for tools without configuration.`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Call factory with config - this passes configuration to AI SDK
|
|
107
|
+
tools[toolName] = toolFactory( toolConfig );
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return tools;
|
|
111
|
+
}
|
package/src/ai_model.spec.js
CHANGED
|
@@ -1,33 +1,602 @@
|
|
|
1
|
-
import { it, expect, vi, afterEach } from 'vitest';
|
|
1
|
+
import { it, expect, vi, afterEach, describe } from 'vitest';
|
|
2
2
|
|
|
3
3
|
const openaiImpl = vi.fn( model => `openai:${model}` );
|
|
4
4
|
const azureImpl = vi.fn( model => `azure:${model}` );
|
|
5
5
|
const anthropicImpl = vi.fn( model => `anthropic:${model}` );
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
// OpenAI mock with tools support
|
|
8
|
+
vi.mock( '@ai-sdk/openai', () => {
|
|
9
|
+
const openaiMock = ( ...values ) => openaiImpl( ...values );
|
|
10
|
+
openaiMock.tools = {
|
|
11
|
+
webSearch: ( config = {} ) => ( { type: 'webSearch', config } )
|
|
12
|
+
};
|
|
13
|
+
return { openai: openaiMock };
|
|
14
|
+
} );
|
|
10
15
|
|
|
16
|
+
// Azure mock without tools support
|
|
11
17
|
vi.mock( '@ai-sdk/azure', () => ( {
|
|
12
18
|
azure: ( ...values ) => azureImpl( ...values )
|
|
13
19
|
} ) );
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
// Anthropic mock with tools support
|
|
22
|
+
vi.mock( '@ai-sdk/anthropic', () => {
|
|
23
|
+
const anthropicMock = ( ...values ) => anthropicImpl( ...values );
|
|
24
|
+
anthropicMock.tools = {
|
|
25
|
+
webSearch_20250305: ( config = {} ) => ( { type: 'webSearch_20250305', config } ),
|
|
26
|
+
bash_20241022: ( config = {} ) => ( { type: 'bash_20241022', config } ),
|
|
27
|
+
bash_20250124: ( config = {} ) => ( { type: 'bash_20250124', config } ),
|
|
28
|
+
codeExecution_20250522: ( config = {} ) => ( { type: 'codeExecution_20250522', config } ),
|
|
29
|
+
codeExecution_20250825: ( config = {} ) => ( { type: 'codeExecution_20250825', config } )
|
|
30
|
+
};
|
|
31
|
+
return { anthropic: anthropicMock };
|
|
32
|
+
} );
|
|
18
33
|
|
|
19
|
-
|
|
34
|
+
// Vertex mock with tools support
|
|
35
|
+
vi.mock( '@ai-sdk/google-vertex', () => {
|
|
36
|
+
const vertexFn = model => `vertex:${model}`;
|
|
37
|
+
vertexFn.tools = {
|
|
38
|
+
googleSearch: ( config = {} ) => ( { type: 'googleSearch', config } ),
|
|
39
|
+
fileSearch: ( config = {} ) => ( { type: 'fileSearch', config } ),
|
|
40
|
+
urlContext: ( config = {} ) => ( { type: 'urlContext', config } ),
|
|
41
|
+
enterpriseWebSearch: ( config = {} ) => ( { type: 'enterpriseWebSearch', config } ),
|
|
42
|
+
googleMaps: ( config = {} ) => ( { type: 'googleMaps', config } ),
|
|
43
|
+
codeExecution: ( config = {} ) => ( { type: 'codeExecution', config } ),
|
|
44
|
+
vertexRagStore: ( config = {} ) => ( { type: 'vertexRagStore', config } )
|
|
45
|
+
};
|
|
46
|
+
return { vertex: vertexFn };
|
|
47
|
+
} );
|
|
48
|
+
|
|
49
|
+
import { loadModel, loadTools } from './ai_model.js';
|
|
20
50
|
|
|
21
51
|
afterEach( async () => {
|
|
22
52
|
await vi.resetModules();
|
|
23
53
|
vi.clearAllMocks();
|
|
24
54
|
} );
|
|
25
55
|
|
|
26
|
-
|
|
27
|
-
|
|
56
|
+
describe( 'loadModel', () => {
|
|
57
|
+
it( 'loads model using selected provider', () => {
|
|
58
|
+
const result = loadModel( { config: { provider: 'openai', model: 'gpt-4o-mini' } } );
|
|
59
|
+
|
|
60
|
+
expect( result ).toBe( 'openai:gpt-4o-mini' );
|
|
61
|
+
expect( openaiImpl ).toHaveBeenCalledWith( 'gpt-4o-mini' );
|
|
62
|
+
expect( azureImpl ).not.toHaveBeenCalled();
|
|
63
|
+
expect( anthropicImpl ).not.toHaveBeenCalled();
|
|
64
|
+
} );
|
|
65
|
+
} );
|
|
66
|
+
|
|
67
|
+
describe( 'loadTools', () => {
|
|
68
|
+
// Category 1: Basic Functionality (5 tests)
|
|
69
|
+
describe( 'Basic Functionality', () => {
|
|
70
|
+
it( 'returns null when no tools configured', () => {
|
|
71
|
+
const result = loadTools( { config: { provider: 'vertex', model: 'gemini-2.0-flash' } } );
|
|
72
|
+
expect( result ).toBeNull();
|
|
73
|
+
} );
|
|
74
|
+
|
|
75
|
+
it( 'returns null when tools is empty object', () => {
|
|
76
|
+
const result = loadTools( { config: { provider: 'vertex', model: 'gemini-2.0-flash', tools: {} } } );
|
|
77
|
+
expect( result ).toBeNull();
|
|
78
|
+
} );
|
|
79
|
+
|
|
80
|
+
it( 'loads single tool with empty config', () => {
|
|
81
|
+
const result = loadTools( {
|
|
82
|
+
config: { provider: 'vertex', tools: { googleSearch: {} } }
|
|
83
|
+
} );
|
|
84
|
+
|
|
85
|
+
expect( result ).toEqual( {
|
|
86
|
+
googleSearch: { type: 'googleSearch', config: {} }
|
|
87
|
+
} );
|
|
88
|
+
} );
|
|
89
|
+
|
|
90
|
+
it( 'loads single tool with config', () => {
|
|
91
|
+
const result = loadTools( {
|
|
92
|
+
config: {
|
|
93
|
+
provider: 'vertex',
|
|
94
|
+
tools: { googleSearch: { mode: 'MODE_DYNAMIC' } }
|
|
95
|
+
}
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
expect( result ).toEqual( {
|
|
99
|
+
googleSearch: { type: 'googleSearch', config: { mode: 'MODE_DYNAMIC' } }
|
|
100
|
+
} );
|
|
101
|
+
} );
|
|
102
|
+
|
|
103
|
+
it( 'loads multiple tools with different configs', () => {
|
|
104
|
+
const result = loadTools( {
|
|
105
|
+
config: {
|
|
106
|
+
provider: 'vertex',
|
|
107
|
+
tools: {
|
|
108
|
+
googleSearch: { mode: 'MODE_DYNAMIC' },
|
|
109
|
+
urlContext: {},
|
|
110
|
+
fileSearch: { topK: 5 }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} );
|
|
114
|
+
|
|
115
|
+
expect( Object.keys( result ) ).toEqual( [ 'googleSearch', 'urlContext', 'fileSearch' ] );
|
|
116
|
+
expect( result.googleSearch.config ).toEqual( { mode: 'MODE_DYNAMIC' } );
|
|
117
|
+
expect( result.urlContext.config ).toEqual( {} );
|
|
118
|
+
expect( result.fileSearch.config ).toEqual( { topK: 5 } );
|
|
119
|
+
} );
|
|
120
|
+
} );
|
|
121
|
+
|
|
122
|
+
// Category 2: Vertex Provider (8 tests)
|
|
123
|
+
describe( 'Vertex Provider', () => {
|
|
124
|
+
it( 'loads googleSearch with mode and dynamicThreshold', () => {
|
|
125
|
+
const result = loadTools( {
|
|
126
|
+
config: {
|
|
127
|
+
provider: 'vertex',
|
|
128
|
+
tools: {
|
|
129
|
+
googleSearch: {
|
|
130
|
+
mode: 'MODE_DYNAMIC',
|
|
131
|
+
dynamicThreshold: 0.8
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} );
|
|
136
|
+
|
|
137
|
+
expect( result.googleSearch ).toEqual( {
|
|
138
|
+
type: 'googleSearch',
|
|
139
|
+
config: {
|
|
140
|
+
mode: 'MODE_DYNAMIC',
|
|
141
|
+
dynamicThreshold: 0.8
|
|
142
|
+
}
|
|
143
|
+
} );
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
it( 'loads fileSearch with fileSearchStoreNames and topK', () => {
|
|
147
|
+
const result = loadTools( {
|
|
148
|
+
config: {
|
|
149
|
+
provider: 'vertex',
|
|
150
|
+
tools: {
|
|
151
|
+
fileSearch: {
|
|
152
|
+
fileSearchStoreNames: [ 'store-1', 'store-2' ],
|
|
153
|
+
topK: 5,
|
|
154
|
+
metadataFilter: 'category = "docs"'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} );
|
|
159
|
+
|
|
160
|
+
expect( result.fileSearch.config ).toEqual( {
|
|
161
|
+
fileSearchStoreNames: [ 'store-1', 'store-2' ],
|
|
162
|
+
topK: 5,
|
|
163
|
+
metadataFilter: 'category = "docs"'
|
|
164
|
+
} );
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
it( 'loads urlContext with empty config', () => {
|
|
168
|
+
const result = loadTools( {
|
|
169
|
+
config: {
|
|
170
|
+
provider: 'vertex',
|
|
171
|
+
tools: { urlContext: {} }
|
|
172
|
+
}
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
expect( result.urlContext ).toEqual( {
|
|
176
|
+
type: 'urlContext',
|
|
177
|
+
config: {}
|
|
178
|
+
} );
|
|
179
|
+
} );
|
|
180
|
+
|
|
181
|
+
it( 'loads enterpriseWebSearch with config', () => {
|
|
182
|
+
const result = loadTools( {
|
|
183
|
+
config: {
|
|
184
|
+
provider: 'vertex',
|
|
185
|
+
tools: { enterpriseWebSearch: { threshold: 0.5 } }
|
|
186
|
+
}
|
|
187
|
+
} );
|
|
188
|
+
|
|
189
|
+
expect( result.enterpriseWebSearch.config ).toEqual( { threshold: 0.5 } );
|
|
190
|
+
} );
|
|
191
|
+
|
|
192
|
+
it( 'loads googleMaps with config', () => {
|
|
193
|
+
const result = loadTools( {
|
|
194
|
+
config: {
|
|
195
|
+
provider: 'vertex',
|
|
196
|
+
tools: { googleMaps: { region: 'US' } }
|
|
197
|
+
}
|
|
198
|
+
} );
|
|
199
|
+
|
|
200
|
+
expect( result.googleMaps.config ).toEqual( { region: 'US' } );
|
|
201
|
+
} );
|
|
202
|
+
|
|
203
|
+
it( 'loads codeExecution with config', () => {
|
|
204
|
+
const result = loadTools( {
|
|
205
|
+
config: {
|
|
206
|
+
provider: 'vertex',
|
|
207
|
+
tools: { codeExecution: { timeout: 30 } }
|
|
208
|
+
}
|
|
209
|
+
} );
|
|
210
|
+
|
|
211
|
+
expect( result.codeExecution.config ).toEqual( { timeout: 30 } );
|
|
212
|
+
} );
|
|
213
|
+
|
|
214
|
+
it( 'loads vertexRagStore with config', () => {
|
|
215
|
+
const result = loadTools( {
|
|
216
|
+
config: {
|
|
217
|
+
provider: 'vertex',
|
|
218
|
+
tools: {
|
|
219
|
+
vertexRagStore: {
|
|
220
|
+
ragCorpus: 'my-corpus-id',
|
|
221
|
+
topK: 3
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} );
|
|
226
|
+
|
|
227
|
+
expect( result.vertexRagStore.config ).toEqual( {
|
|
228
|
+
ragCorpus: 'my-corpus-id',
|
|
229
|
+
topK: 3
|
|
230
|
+
} );
|
|
231
|
+
} );
|
|
232
|
+
|
|
233
|
+
it( 'loads multiple Vertex tools simultaneously', () => {
|
|
234
|
+
const result = loadTools( {
|
|
235
|
+
config: {
|
|
236
|
+
provider: 'vertex',
|
|
237
|
+
tools: {
|
|
238
|
+
googleSearch: { mode: 'MODE_DYNAMIC' },
|
|
239
|
+
fileSearch: { topK: 5 },
|
|
240
|
+
urlContext: {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
expect( Object.keys( result ) ).toEqual( [ 'googleSearch', 'fileSearch', 'urlContext' ] );
|
|
246
|
+
expect( result.googleSearch.type ).toBe( 'googleSearch' );
|
|
247
|
+
expect( result.fileSearch.type ).toBe( 'fileSearch' );
|
|
248
|
+
expect( result.urlContext.type ).toBe( 'urlContext' );
|
|
249
|
+
} );
|
|
250
|
+
} );
|
|
251
|
+
|
|
252
|
+
// Category 3: OpenAI Provider (5 tests)
|
|
253
|
+
describe( 'OpenAI Provider', () => {
|
|
254
|
+
it( 'loads webSearch with empty config', () => {
|
|
255
|
+
const result = loadTools( {
|
|
256
|
+
config: {
|
|
257
|
+
provider: 'openai',
|
|
258
|
+
tools: { webSearch: {} }
|
|
259
|
+
}
|
|
260
|
+
} );
|
|
261
|
+
|
|
262
|
+
expect( result.webSearch ).toEqual( {
|
|
263
|
+
type: 'webSearch',
|
|
264
|
+
config: {}
|
|
265
|
+
} );
|
|
266
|
+
} );
|
|
267
|
+
|
|
268
|
+
it( 'loads webSearch with searchContextSize', () => {
|
|
269
|
+
const result = loadTools( {
|
|
270
|
+
config: {
|
|
271
|
+
provider: 'openai',
|
|
272
|
+
tools: {
|
|
273
|
+
webSearch: { searchContextSize: 'high' }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} );
|
|
277
|
+
|
|
278
|
+
expect( result.webSearch.config.searchContextSize ).toBe( 'high' );
|
|
279
|
+
} );
|
|
280
|
+
|
|
281
|
+
it( 'loads webSearch with filters.allowedDomains array', () => {
|
|
282
|
+
const result = loadTools( {
|
|
283
|
+
config: {
|
|
284
|
+
provider: 'openai',
|
|
285
|
+
tools: {
|
|
286
|
+
webSearch: {
|
|
287
|
+
filters: {
|
|
288
|
+
allowedDomains: [ 'wikipedia.org', 'github.com' ]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} );
|
|
294
|
+
|
|
295
|
+
expect( result.webSearch.config.filters.allowedDomains ).toEqual( [
|
|
296
|
+
'wikipedia.org',
|
|
297
|
+
'github.com'
|
|
298
|
+
] );
|
|
299
|
+
} );
|
|
300
|
+
|
|
301
|
+
it( 'loads webSearch with userLocation object', () => {
|
|
302
|
+
const result = loadTools( {
|
|
303
|
+
config: {
|
|
304
|
+
provider: 'openai',
|
|
305
|
+
tools: {
|
|
306
|
+
webSearch: {
|
|
307
|
+
userLocation: {
|
|
308
|
+
type: 'approximate',
|
|
309
|
+
country: 'US',
|
|
310
|
+
city: 'San Francisco'
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} );
|
|
316
|
+
|
|
317
|
+
expect( result.webSearch.config.userLocation ).toEqual( {
|
|
318
|
+
type: 'approximate',
|
|
319
|
+
country: 'US',
|
|
320
|
+
city: 'San Francisco'
|
|
321
|
+
} );
|
|
322
|
+
} );
|
|
323
|
+
|
|
324
|
+
it( 'loads webSearch with all config options combined', () => {
|
|
325
|
+
const result = loadTools( {
|
|
326
|
+
config: {
|
|
327
|
+
provider: 'openai',
|
|
328
|
+
tools: {
|
|
329
|
+
webSearch: {
|
|
330
|
+
searchContextSize: 'high',
|
|
331
|
+
filters: {
|
|
332
|
+
allowedDomains: [ 'wikipedia.org' ]
|
|
333
|
+
},
|
|
334
|
+
userLocation: {
|
|
335
|
+
type: 'approximate',
|
|
336
|
+
country: 'US'
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
expect( result.webSearch.config.searchContextSize ).toBe( 'high' );
|
|
344
|
+
expect( result.webSearch.config.filters.allowedDomains ).toHaveLength( 1 );
|
|
345
|
+
expect( result.webSearch.config.userLocation.country ).toBe( 'US' );
|
|
346
|
+
} );
|
|
347
|
+
} );
|
|
348
|
+
|
|
349
|
+
// Category 4: Anthropic Provider (6 tests)
|
|
350
|
+
describe( 'Anthropic Provider', () => {
|
|
351
|
+
it( 'loads webSearch_20250305 with empty config', () => {
|
|
352
|
+
const result = loadTools( {
|
|
353
|
+
config: {
|
|
354
|
+
provider: 'anthropic',
|
|
355
|
+
tools: { webSearch_20250305: {} }
|
|
356
|
+
}
|
|
357
|
+
} );
|
|
358
|
+
|
|
359
|
+
expect( result.webSearch_20250305 ).toEqual( {
|
|
360
|
+
type: 'webSearch_20250305',
|
|
361
|
+
config: {}
|
|
362
|
+
} );
|
|
363
|
+
} );
|
|
364
|
+
|
|
365
|
+
it( 'loads webSearch_20250305 with maxUses number', () => {
|
|
366
|
+
const result = loadTools( {
|
|
367
|
+
config: {
|
|
368
|
+
provider: 'anthropic',
|
|
369
|
+
tools: {
|
|
370
|
+
webSearch_20250305: { maxUses: 3 }
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} );
|
|
374
|
+
|
|
375
|
+
expect( result.webSearch_20250305.config.maxUses ).toBe( 3 );
|
|
376
|
+
} );
|
|
377
|
+
|
|
378
|
+
it( 'loads webSearch_20250305 with allowedDomains and blockedDomains', () => {
|
|
379
|
+
const result = loadTools( {
|
|
380
|
+
config: {
|
|
381
|
+
provider: 'anthropic',
|
|
382
|
+
tools: {
|
|
383
|
+
webSearch_20250305: {
|
|
384
|
+
allowedDomains: [ 'reuters.com', 'bbc.com' ],
|
|
385
|
+
blockedDomains: [ 'tabloid.com' ]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} );
|
|
390
|
+
|
|
391
|
+
expect( result.webSearch_20250305.config.allowedDomains ).toEqual( [
|
|
392
|
+
'reuters.com',
|
|
393
|
+
'bbc.com'
|
|
394
|
+
] );
|
|
395
|
+
expect( result.webSearch_20250305.config.blockedDomains ).toEqual( [ 'tabloid.com' ] );
|
|
396
|
+
} );
|
|
397
|
+
|
|
398
|
+
it( 'loads webSearch_20250305 with userLocation object', () => {
|
|
399
|
+
const result = loadTools( {
|
|
400
|
+
config: {
|
|
401
|
+
provider: 'anthropic',
|
|
402
|
+
tools: {
|
|
403
|
+
webSearch_20250305: {
|
|
404
|
+
userLocation: {
|
|
405
|
+
type: 'approximate',
|
|
406
|
+
country: 'GB',
|
|
407
|
+
city: 'London',
|
|
408
|
+
timezone: 'Europe/London'
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} );
|
|
414
|
+
|
|
415
|
+
expect( result.webSearch_20250305.config.userLocation.city ).toBe( 'London' );
|
|
416
|
+
expect( result.webSearch_20250305.config.userLocation.timezone ).toBe( 'Europe/London' );
|
|
417
|
+
} );
|
|
418
|
+
|
|
419
|
+
it( 'loads bash_20241022 and bash_20250124 tools', () => {
|
|
420
|
+
const result = loadTools( {
|
|
421
|
+
config: {
|
|
422
|
+
provider: 'anthropic',
|
|
423
|
+
tools: {
|
|
424
|
+
bash_20241022: {},
|
|
425
|
+
bash_20250124: {}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} );
|
|
429
|
+
|
|
430
|
+
expect( result.bash_20241022.type ).toBe( 'bash_20241022' );
|
|
431
|
+
expect( result.bash_20250124.type ).toBe( 'bash_20250124' );
|
|
432
|
+
} );
|
|
433
|
+
|
|
434
|
+
it( 'loads codeExecution_20250522 and codeExecution_20250825 tools', () => {
|
|
435
|
+
const result = loadTools( {
|
|
436
|
+
config: {
|
|
437
|
+
provider: 'anthropic',
|
|
438
|
+
tools: {
|
|
439
|
+
codeExecution_20250522: {},
|
|
440
|
+
codeExecution_20250825: {}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} );
|
|
444
|
+
|
|
445
|
+
expect( result.codeExecution_20250522.type ).toBe( 'codeExecution_20250522' );
|
|
446
|
+
expect( result.codeExecution_20250825.type ).toBe( 'codeExecution_20250825' );
|
|
447
|
+
} );
|
|
448
|
+
} );
|
|
449
|
+
|
|
450
|
+
// Category 5: Error Handling (9 tests)
|
|
451
|
+
describe( 'Error Handling', () => {
|
|
452
|
+
it( 'throws clear error for array format (migration guide)', () => {
|
|
453
|
+
expect( () => loadTools( {
|
|
454
|
+
config: {
|
|
455
|
+
provider: 'vertex',
|
|
456
|
+
tools: [ 'googleSearch', 'urlContext' ]
|
|
457
|
+
}
|
|
458
|
+
} ) ).toThrow( 'tools must be an object with tool configurations, got array' );
|
|
459
|
+
} );
|
|
460
|
+
|
|
461
|
+
it( 'throws error for string format', () => {
|
|
462
|
+
expect( () => loadTools( {
|
|
463
|
+
config: {
|
|
464
|
+
provider: 'vertex',
|
|
465
|
+
tools: 'googleSearch'
|
|
466
|
+
}
|
|
467
|
+
} ) ).toThrow( 'tools must be an object' );
|
|
468
|
+
} );
|
|
469
|
+
|
|
470
|
+
it( 'throws error for number format', () => {
|
|
471
|
+
expect( () => loadTools( {
|
|
472
|
+
config: {
|
|
473
|
+
provider: 'vertex',
|
|
474
|
+
tools: 123
|
|
475
|
+
}
|
|
476
|
+
} ) ).toThrow( 'tools must be an object' );
|
|
477
|
+
} );
|
|
478
|
+
|
|
479
|
+
it( 'throws error for provider without tools support', () => {
|
|
480
|
+
expect( () => loadTools( {
|
|
481
|
+
config: {
|
|
482
|
+
provider: 'azure',
|
|
483
|
+
tools: { someTool: {} }
|
|
484
|
+
}
|
|
485
|
+
} ) ).toThrow( 'does not support provider-specific tools' );
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
it( 'throws error for unknown tool on Vertex with dynamic tool listing', () => {
|
|
489
|
+
expect( () => loadTools( {
|
|
490
|
+
config: {
|
|
491
|
+
provider: 'vertex',
|
|
492
|
+
tools: { unknownTool: {} }
|
|
493
|
+
}
|
|
494
|
+
} ) ).toThrow( /Unknown tool "unknownTool" for provider "vertex".*Available tools:/ );
|
|
495
|
+
} );
|
|
496
|
+
|
|
497
|
+
it( 'throws error for unknown tool on OpenAI with dynamic tool listing', () => {
|
|
498
|
+
expect( () => loadTools( {
|
|
499
|
+
config: {
|
|
500
|
+
provider: 'openai',
|
|
501
|
+
tools: { googleSearch: {} }
|
|
502
|
+
}
|
|
503
|
+
} ) ).toThrow( /Unknown tool "googleSearch" for provider "openai".*Available tools:/ );
|
|
504
|
+
} );
|
|
505
|
+
|
|
506
|
+
it( 'throws error for unknown tool on Anthropic with dynamic tool listing', () => {
|
|
507
|
+
expect( () => loadTools( {
|
|
508
|
+
config: {
|
|
509
|
+
provider: 'anthropic',
|
|
510
|
+
tools: { googleSearch: {} }
|
|
511
|
+
}
|
|
512
|
+
} ) ).toThrow( /Unknown tool "googleSearch" for provider "anthropic".*Available tools:/ );
|
|
513
|
+
} );
|
|
514
|
+
|
|
515
|
+
it( 'throws error when tool config is null', () => {
|
|
516
|
+
expect( () => loadTools( {
|
|
517
|
+
config: {
|
|
518
|
+
provider: 'vertex',
|
|
519
|
+
tools: { googleSearch: null }
|
|
520
|
+
}
|
|
521
|
+
} ) ).toThrow( 'Configuration for tool "googleSearch" must be an object' );
|
|
522
|
+
} );
|
|
523
|
+
|
|
524
|
+
it( 'throws error when tool config is a string', () => {
|
|
525
|
+
expect( () => loadTools( {
|
|
526
|
+
config: {
|
|
527
|
+
provider: 'vertex',
|
|
528
|
+
tools: { googleSearch: 'MODE_DYNAMIC' }
|
|
529
|
+
}
|
|
530
|
+
} ) ).toThrow( 'Configuration for tool "googleSearch" must be an object' );
|
|
531
|
+
} );
|
|
532
|
+
} );
|
|
533
|
+
|
|
534
|
+
// Category 6: Integration (3 tests)
|
|
535
|
+
describe( 'Integration', () => {
|
|
536
|
+
it( 'simulates variable interpolation scenario', () => {
|
|
537
|
+
// Simulate what would come from renderPrompt + parsePrompt
|
|
538
|
+
const renderedConfig = {
|
|
539
|
+
provider: 'vertex',
|
|
540
|
+
model: 'gemini-2.0-flash',
|
|
541
|
+
tools: {
|
|
542
|
+
googleSearch: {
|
|
543
|
+
mode: 'MODE_DYNAMIC',
|
|
544
|
+
dynamicThreshold: 0.8
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const result = loadTools( { config: renderedConfig } );
|
|
550
|
+
|
|
551
|
+
expect( result.googleSearch.config ).toEqual( {
|
|
552
|
+
mode: 'MODE_DYNAMIC',
|
|
553
|
+
dynamicThreshold: 0.8
|
|
554
|
+
} );
|
|
555
|
+
} );
|
|
556
|
+
|
|
557
|
+
it( 'validates that config objects are passed to factory functions', () => {
|
|
558
|
+
const customConfig = {
|
|
559
|
+
mode: 'MODE_DYNAMIC',
|
|
560
|
+
dynamicThreshold: 0.7,
|
|
561
|
+
customField: 'value'
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const result = loadTools( {
|
|
565
|
+
config: {
|
|
566
|
+
provider: 'vertex',
|
|
567
|
+
tools: { googleSearch: customConfig }
|
|
568
|
+
}
|
|
569
|
+
} );
|
|
570
|
+
|
|
571
|
+
// The mock returns { type, config }, so we can verify config was passed through
|
|
572
|
+
expect( result.googleSearch.config ).toEqual( customConfig );
|
|
573
|
+
} );
|
|
574
|
+
|
|
575
|
+
it( 'handles nested configuration objects', () => {
|
|
576
|
+
const result = loadTools( {
|
|
577
|
+
config: {
|
|
578
|
+
provider: 'openai',
|
|
579
|
+
tools: {
|
|
580
|
+
webSearch: {
|
|
581
|
+
searchContextSize: 'high',
|
|
582
|
+
filters: {
|
|
583
|
+
allowedDomains: [ 'example.com' ],
|
|
584
|
+
blockedDomains: [ 'spam.com' ]
|
|
585
|
+
},
|
|
586
|
+
userLocation: {
|
|
587
|
+
type: 'approximate',
|
|
588
|
+
country: 'US',
|
|
589
|
+
city: 'Seattle',
|
|
590
|
+
region: 'WA'
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} );
|
|
28
596
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
597
|
+
expect( result.webSearch.config.filters ).toBeDefined();
|
|
598
|
+
expect( result.webSearch.config.userLocation ).toBeDefined();
|
|
599
|
+
expect( result.webSearch.config.filters.allowedDomains ).toEqual( [ 'example.com' ] );
|
|
600
|
+
} );
|
|
601
|
+
} );
|
|
33
602
|
} );
|
package/src/ai_sdk.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Tracing } from '@output.ai/core/tracing';
|
|
2
2
|
import { z } from '@output.ai/core';
|
|
3
|
-
import { loadModel } from './ai_model.js';
|
|
3
|
+
import { loadModel, loadTools } from './ai_model.js';
|
|
4
4
|
import * as AI from 'ai';
|
|
5
5
|
import { validateGenerateTextArgs, validateGenerateObjectArgs, validateGenerateArrayArgs, validateGenerateEnumArgs } from './validations.js';
|
|
6
6
|
import { loadPrompt } from './prompt_loader.js';
|
|
@@ -51,6 +51,14 @@ const aiSdkOptionsFromPrompt = prompt => {
|
|
|
51
51
|
|
|
52
52
|
if ( prompt.config.maxTokens ) {
|
|
53
53
|
options.maxOutputTokens = prompt.config.maxTokens;
|
|
54
|
+
} else if ( prompt.config.provider === 'anthropic' ) {
|
|
55
|
+
// Override Anthropic SDK's low 4096 default - Claude models support up to 64k output tokens
|
|
56
|
+
options.maxOutputTokens = 64000;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const tools = loadTools( prompt );
|
|
60
|
+
if ( tools ) {
|
|
61
|
+
options.tools = tools;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
return options;
|
package/src/ai_sdk.spec.js
CHANGED
|
@@ -9,8 +9,10 @@ const tracingSpies = {
|
|
|
9
9
|
vi.mock( '@output.ai/core/tracing', () => ( { Tracing: tracingSpies } ), { virtual: true } );
|
|
10
10
|
|
|
11
11
|
const loadModelImpl = vi.fn();
|
|
12
|
+
const loadToolsImpl = vi.fn();
|
|
12
13
|
vi.mock( './ai_model.js', () => ( {
|
|
13
|
-
loadModel: ( ...values ) => loadModelImpl( ...values )
|
|
14
|
+
loadModel: ( ...values ) => loadModelImpl( ...values ),
|
|
15
|
+
loadTools: ( ...values ) => loadToolsImpl( ...values )
|
|
14
16
|
} ) );
|
|
15
17
|
|
|
16
18
|
const aiFns = {
|
|
@@ -219,6 +221,7 @@ describe( 'ai_sdk', () => {
|
|
|
219
221
|
expect( aiFns.generateText ).toHaveBeenCalledWith( {
|
|
220
222
|
model: 'MODEL',
|
|
221
223
|
messages: promptWithProviderOptions.messages,
|
|
224
|
+
maxOutputTokens: 64000,
|
|
222
225
|
providerOptions: {
|
|
223
226
|
thinking: {
|
|
224
227
|
type: 'enabled',
|
|
@@ -337,6 +340,7 @@ describe( 'ai_sdk', () => {
|
|
|
337
340
|
enum: [ 'A', 'B', 'C' ],
|
|
338
341
|
model: 'MODEL',
|
|
339
342
|
messages: promptWithMixedOptions.messages,
|
|
343
|
+
maxOutputTokens: 64000,
|
|
340
344
|
providerOptions: {
|
|
341
345
|
thinking: {
|
|
342
346
|
type: 'enabled',
|
package/src/index.d.ts
CHANGED
|
@@ -60,6 +60,28 @@ export type Prompt = {
|
|
|
60
60
|
/** Maximum number of tokens in the response */
|
|
61
61
|
maxTokens?: number;
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Provider-specific tools with configuration.
|
|
65
|
+
*
|
|
66
|
+
* @example Vertex googleSearch with config
|
|
67
|
+
* ```yaml
|
|
68
|
+
* tools:
|
|
69
|
+
* googleSearch:
|
|
70
|
+
* mode: MODE_DYNAMIC
|
|
71
|
+
* dynamicThreshold: 0.8
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example OpenAI webSearch with filters
|
|
75
|
+
* ```yaml
|
|
76
|
+
* tools:
|
|
77
|
+
* webSearch:
|
|
78
|
+
* searchContextSize: high
|
|
79
|
+
* filters:
|
|
80
|
+
* allowedDomains: [wikipedia.org]
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
tools?: Record<string, Record<string, unknown>>;
|
|
84
|
+
|
|
63
85
|
/** Provider-specific options */
|
|
64
86
|
providerOptions?: Record<string, unknown>;
|
|
65
87
|
};
|
|
@@ -7,6 +7,7 @@ export const promptSchema = z.object( {
|
|
|
7
7
|
model: z.string(),
|
|
8
8
|
temperature: z.number().optional(),
|
|
9
9
|
maxTokens: z.number().optional(),
|
|
10
|
+
tools: z.record( z.string(), z.object( {} ).passthrough() ).optional(),
|
|
10
11
|
providerOptions: z.object( {
|
|
11
12
|
thinking: z.object( {
|
|
12
13
|
type: z.literal( 'enabled' ),
|