@outputai/llm 0.6.1-next.fc6a93e.0 → 0.7.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.
@@ -1,836 +1,259 @@
1
- import { it, expect, vi, afterEach, describe } from 'vitest';
2
-
3
- const providerFactoryOptions = vi.hoisted( () => ( {} ) );
4
- const openaiImpl = vi.hoisted( () => vi.fn( model => `openai:${model}` ) );
5
- const openaiImageImpl = vi.hoisted( () => vi.fn( model => `openai-image:${model}` ) );
6
- const azureImpl = vi.hoisted( () => vi.fn( model => `azure:${model}` ) );
7
- const anthropicImpl = vi.hoisted( () => vi.fn( model => `anthropic:${model}` ) );
8
- const bedrockImpl = vi.hoisted( () => vi.fn( model => `bedrock:${model}` ) );
9
- const perplexityImpl = vi.hoisted( () => vi.fn( model => `perplexity:${model}` ) );
10
- const vertexImpl = vi.hoisted( () => vi.fn( model => `vertex:${model}` ) );
11
-
12
- // OpenAI mock with tools support
13
- vi.mock( '@ai-sdk/openai', () => {
14
- const openaiMock = ( ...values ) => openaiImpl( ...values );
15
- openaiMock.image = ( ...values ) => openaiImageImpl( ...values );
16
- openaiMock.tools = {
17
- webSearch: ( config = {} ) => ( { type: 'webSearch', config } )
18
- };
19
- return {
20
- createOpenAI: options => {
21
- providerFactoryOptions.openai = options;
22
- return openaiMock;
23
- }
24
- };
25
- } );
26
-
27
- // Azure mock without tools support
28
- vi.mock( '@ai-sdk/azure', () => ( {
29
- createAzure: options => {
30
- providerFactoryOptions.azure = options;
31
- return ( ...values ) => azureImpl( ...values );
32
- }
33
- } ) );
34
-
35
- // Anthropic mock with tools support
36
- vi.mock( '@ai-sdk/anthropic', () => {
37
- const anthropicMock = ( ...values ) => anthropicImpl( ...values );
38
- anthropicMock.tools = {
39
- webSearch_20250305: ( config = {} ) => ( { type: 'webSearch_20250305', config } ),
40
- bash_20241022: ( config = {} ) => ( { type: 'bash_20241022', config } ),
41
- bash_20250124: ( config = {} ) => ( { type: 'bash_20250124', config } ),
42
- codeExecution_20250522: ( config = {} ) => ( { type: 'codeExecution_20250522', config } ),
43
- codeExecution_20250825: ( config = {} ) => ( { type: 'codeExecution_20250825', config } )
44
- };
45
- return {
46
- createAnthropic: options => {
47
- providerFactoryOptions.anthropic = options;
48
- return anthropicMock;
49
- }
50
- };
51
- } );
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
52
2
 
53
- // Bedrock mock with tools support
54
- vi.mock( '@ai-sdk/amazon-bedrock', () => {
55
- const bedrockMock = ( ...values ) => bedrockImpl( ...values );
56
- bedrockMock.tools = {
57
- bash_20241022: ( config = {} ) => ( { type: 'bash_20241022', config } ),
58
- textEditor_20241022: ( config = {} ) => ( { type: 'textEditor_20241022', config } ),
59
- textEditor_20250429: ( config = {} ) => ( { type: 'textEditor_20250429', config } ),
60
- computer_20241022: ( config = {} ) => ( { type: 'computer_20241022', config } )
61
- };
62
- return {
63
- createAmazonBedrock: options => {
64
- providerFactoryOptions.bedrock = options;
65
- return bedrockMock;
66
- }
67
- };
68
- } );
3
+ const getProvider = vi.hoisted( () => vi.fn() );
69
4
 
70
- // Perplexity mock
71
- vi.mock( '@ai-sdk/perplexity', () => ( {
72
- createPerplexity: options => {
73
- providerFactoryOptions.perplexity = options;
74
- return ( ...values ) => perplexityImpl( ...values );
75
- }
5
+ vi.mock( './ai_provider.js', () => ( {
6
+ getProvider
76
7
  } ) );
77
8
 
78
- // Vertex mock with tools support
79
- vi.mock( '@ai-sdk/google-vertex', () => {
80
- const vertexFn = ( ...values ) => vertexImpl( ...values );
81
- vertexFn.tools = {
82
- googleSearch: ( config = {} ) => ( { type: 'googleSearch', config } ),
83
- fileSearch: ( config = {} ) => ( { type: 'fileSearch', config } ),
84
- urlContext: ( config = {} ) => ( { type: 'urlContext', config } ),
85
- enterpriseWebSearch: ( config = {} ) => ( { type: 'enterpriseWebSearch', config } ),
86
- googleMaps: ( config = {} ) => ( { type: 'googleMaps', config } ),
87
- codeExecution: ( config = {} ) => ( { type: 'codeExecution', config } ),
88
- vertexRagStore: ( config = {} ) => ( { type: 'vertexRagStore', config } )
89
- };
90
- return {
91
- createVertex: options => {
92
- providerFactoryOptions.vertex = options;
93
- return vertexFn;
94
- }
95
- };
96
- } );
97
-
98
- import { loadImageModel, loadTextModel, loadTools, registerProvider, getRegisteredProviders, providers, builtInProviders } from './ai_model.js';
9
+ import { loadImageModel, loadTextModel, loadTools } from './ai_model.js';
99
10
 
100
- afterEach( async () => {
101
- await vi.resetModules();
11
+ afterEach( () => {
102
12
  vi.clearAllMocks();
103
13
  } );
104
14
 
105
15
  describe( 'loadTextModel', () => {
106
- it( 'initializes built-in providers with custom fetch', () => {
107
- expect( providerFactoryOptions ).toMatchObject( {
108
- azure: { fetch: expect.any( Function ) },
109
- anthropic: { fetch: expect.any( Function ) },
110
- openai: { fetch: expect.any( Function ) },
111
- vertex: { fetch: expect.any( Function ) },
112
- bedrock: { fetch: expect.any( Function ) },
113
- perplexity: { fetch: expect.any( Function ) }
114
- } );
115
- } );
116
-
117
- it( 'loads model using selected provider', () => {
118
- const result = loadTextModel( { config: { provider: 'openai', model: 'gpt-4o-mini' } } );
119
-
120
- expect( result ).toBe( 'openai:gpt-4o-mini' );
121
- expect( openaiImpl ).toHaveBeenCalledWith( 'gpt-4o-mini' );
122
- expect( azureImpl ).not.toHaveBeenCalled();
123
- expect( anthropicImpl ).not.toHaveBeenCalled();
124
- } );
16
+ it( 'loads a text model using the prompt provider and model', () => {
17
+ const provider = vi.fn( model => ( { type: 'text-model', model } ) );
18
+ getProvider.mockReturnValue( provider );
125
19
 
126
- it( 'loads model using bedrock provider', () => {
127
- const result = loadTextModel( { config: { provider: 'bedrock', model: 'anthropic.claude-sonnet-4-20250514-v1:0' } } );
20
+ const result = loadTextModel( {
21
+ config: {
22
+ provider: 'openai',
23
+ model: 'gpt-4o-mini'
24
+ }
25
+ } );
128
26
 
129
- expect( result ).toBe( 'bedrock:anthropic.claude-sonnet-4-20250514-v1:0' );
130
- expect( bedrockImpl ).toHaveBeenCalledWith( 'anthropic.claude-sonnet-4-20250514-v1:0' );
27
+ expect( getProvider ).toHaveBeenCalledWith( 'openai' );
28
+ expect( provider ).toHaveBeenCalledWith( 'gpt-4o-mini' );
29
+ expect( result ).toEqual( {
30
+ type: 'text-model',
31
+ model: 'gpt-4o-mini'
32
+ } );
131
33
  } );
132
34
 
133
- it( 'loads model using perplexity provider', () => {
134
- const result = loadTextModel( { config: { provider: 'perplexity', model: 'sonar-pro' } } );
35
+ it( 'propagates provider lookup errors', () => {
36
+ getProvider.mockImplementation( () => {
37
+ throw new Error( 'Unsupported provider "missing"' );
38
+ } );
135
39
 
136
- expect( result ).toBe( 'perplexity:sonar-pro' );
137
- expect( perplexityImpl ).toHaveBeenCalledWith( 'sonar-pro' );
40
+ expect( () => loadTextModel( {
41
+ config: {
42
+ provider: 'missing',
43
+ model: 'model'
44
+ }
45
+ } ) ).toThrow( 'Unsupported provider "missing"' );
138
46
  } );
139
47
  } );
140
48
 
141
49
  describe( 'loadImageModel', () => {
142
- it( 'loads image model using provider image factory', () => {
143
- const result = loadImageModel( { config: { provider: 'openai', model: 'gpt-image-1' } } );
144
-
145
- expect( result ).toBe( 'openai-image:gpt-image-1' );
146
- expect( openaiImageImpl ).toHaveBeenCalledWith( 'gpt-image-1' );
147
- expect( openaiImpl ).not.toHaveBeenCalled();
148
- } );
149
-
150
- it( 'throws a clear error when provider does not support image models', () => {
151
- expect( () => loadImageModel( {
152
- config: { provider: 'azure', model: 'gpt-image-1' }
153
- } ) ).toThrow( 'Provider "azure" does not support image models.' );
154
- } );
155
- } );
156
-
157
- describe( 'loadTools', () => {
158
- // Category 1: Basic Functionality (5 tests)
159
- describe( 'Basic Functionality', () => {
160
- it( 'returns null when no tools configured', () => {
161
- const result = loadTools( { config: { provider: 'vertex', model: 'gemini-2.0-flash' } } );
162
- expect( result ).toBeNull();
163
- } );
164
-
165
- it( 'returns null when tools is empty object', () => {
166
- const result = loadTools( { config: { provider: 'vertex', model: 'gemini-2.0-flash', tools: {} } } );
167
- expect( result ).toBeNull();
168
- } );
169
-
170
- it( 'loads single tool with empty config', () => {
171
- const result = loadTools( {
172
- config: { provider: 'vertex', tools: { googleSearch: {} } }
173
- } );
174
-
175
- expect( result ).toEqual( {
176
- googleSearch: { type: 'googleSearch', config: {} }
177
- } );
178
- } );
50
+ it( 'loads an image model using provider.image', () => {
51
+ const textProvider = vi.fn();
52
+ textProvider.image = vi.fn( model => ( { type: 'image-model', model } ) );
53
+ getProvider.mockReturnValue( textProvider );
179
54
 
180
- it( 'loads single tool with config', () => {
181
- const result = loadTools( {
182
- config: {
183
- provider: 'vertex',
184
- tools: { googleSearch: { mode: 'MODE_DYNAMIC' } }
185
- }
186
- } );
187
-
188
- expect( result ).toEqual( {
189
- googleSearch: { type: 'googleSearch', config: { mode: 'MODE_DYNAMIC' } }
190
- } );
55
+ const result = loadImageModel( {
56
+ config: {
57
+ provider: 'openai',
58
+ model: 'gpt-image-1'
59
+ }
191
60
  } );
192
61
 
193
- it( 'loads multiple tools with different configs', () => {
194
- const result = loadTools( {
195
- config: {
196
- provider: 'vertex',
197
- tools: {
198
- googleSearch: { mode: 'MODE_DYNAMIC' },
199
- urlContext: {},
200
- fileSearch: { topK: 5 }
201
- }
202
- }
203
- } );
204
-
205
- expect( Object.keys( result ) ).toEqual( [ 'googleSearch', 'urlContext', 'fileSearch' ] );
206
- expect( result.googleSearch.config ).toEqual( { mode: 'MODE_DYNAMIC' } );
207
- expect( result.urlContext.config ).toEqual( {} );
208
- expect( result.fileSearch.config ).toEqual( { topK: 5 } );
62
+ expect( getProvider ).toHaveBeenCalledWith( 'openai' );
63
+ expect( textProvider.image ).toHaveBeenCalledWith( 'gpt-image-1' );
64
+ expect( textProvider ).not.toHaveBeenCalled();
65
+ expect( result ).toEqual( {
66
+ type: 'image-model',
67
+ model: 'gpt-image-1'
209
68
  } );
210
69
  } );
211
70
 
212
- // Category 2: Vertex Provider (8 tests)
213
- describe( 'Vertex Provider', () => {
214
- it( 'loads googleSearch with mode and dynamicThreshold', () => {
215
- const result = loadTools( {
216
- config: {
217
- provider: 'vertex',
218
- tools: {
219
- googleSearch: {
220
- mode: 'MODE_DYNAMIC',
221
- dynamicThreshold: 0.8
222
- }
223
- }
224
- }
225
- } );
71
+ it( 'falls back to provider.imageModel', () => {
72
+ const provider = vi.fn();
73
+ provider.imageModel = vi.fn( model => ( { type: 'legacy-image-model', model } ) );
74
+ getProvider.mockReturnValue( provider );
226
75
 
227
- expect( result.googleSearch ).toEqual( {
228
- type: 'googleSearch',
229
- config: {
230
- mode: 'MODE_DYNAMIC',
231
- dynamicThreshold: 0.8
232
- }
233
- } );
76
+ const result = loadImageModel( {
77
+ config: {
78
+ provider: 'custom',
79
+ model: 'image-v1'
80
+ }
234
81
  } );
235
82
 
236
- it( 'loads fileSearch with fileSearchStoreNames and topK', () => {
237
- const result = loadTools( {
238
- config: {
239
- provider: 'vertex',
240
- tools: {
241
- fileSearch: {
242
- fileSearchStoreNames: [ 'store-1', 'store-2' ],
243
- topK: 5,
244
- metadataFilter: 'category = "docs"'
245
- }
246
- }
247
- }
248
- } );
249
-
250
- expect( result.fileSearch.config ).toEqual( {
251
- fileSearchStoreNames: [ 'store-1', 'store-2' ],
252
- topK: 5,
253
- metadataFilter: 'category = "docs"'
254
- } );
255
- } );
256
-
257
- it( 'loads urlContext with empty config', () => {
258
- const result = loadTools( {
259
- config: {
260
- provider: 'vertex',
261
- tools: { urlContext: {} }
262
- }
263
- } );
264
-
265
- expect( result.urlContext ).toEqual( {
266
- type: 'urlContext',
267
- config: {}
268
- } );
269
- } );
270
-
271
- it( 'loads enterpriseWebSearch with config', () => {
272
- const result = loadTools( {
273
- config: {
274
- provider: 'vertex',
275
- tools: { enterpriseWebSearch: { threshold: 0.5 } }
276
- }
277
- } );
278
-
279
- expect( result.enterpriseWebSearch.config ).toEqual( { threshold: 0.5 } );
280
- } );
281
-
282
- it( 'loads googleMaps with config', () => {
283
- const result = loadTools( {
284
- config: {
285
- provider: 'vertex',
286
- tools: { googleMaps: { region: 'US' } }
287
- }
288
- } );
289
-
290
- expect( result.googleMaps.config ).toEqual( { region: 'US' } );
291
- } );
292
-
293
- it( 'loads codeExecution with config', () => {
294
- const result = loadTools( {
295
- config: {
296
- provider: 'vertex',
297
- tools: { codeExecution: { timeout: 30 } }
298
- }
299
- } );
300
-
301
- expect( result.codeExecution.config ).toEqual( { timeout: 30 } );
302
- } );
303
-
304
- it( 'loads vertexRagStore with config', () => {
305
- const result = loadTools( {
306
- config: {
307
- provider: 'vertex',
308
- tools: {
309
- vertexRagStore: {
310
- ragCorpus: 'my-corpus-id',
311
- topK: 3
312
- }
313
- }
314
- }
315
- } );
316
-
317
- expect( result.vertexRagStore.config ).toEqual( {
318
- ragCorpus: 'my-corpus-id',
319
- topK: 3
320
- } );
321
- } );
322
-
323
- it( 'loads multiple Vertex tools simultaneously', () => {
324
- const result = loadTools( {
325
- config: {
326
- provider: 'vertex',
327
- tools: {
328
- googleSearch: { mode: 'MODE_DYNAMIC' },
329
- fileSearch: { topK: 5 },
330
- urlContext: {}
331
- }
332
- }
333
- } );
334
-
335
- expect( Object.keys( result ) ).toEqual( [ 'googleSearch', 'fileSearch', 'urlContext' ] );
336
- expect( result.googleSearch.type ).toBe( 'googleSearch' );
337
- expect( result.fileSearch.type ).toBe( 'fileSearch' );
338
- expect( result.urlContext.type ).toBe( 'urlContext' );
83
+ expect( provider.imageModel ).toHaveBeenCalledWith( 'image-v1' );
84
+ expect( result ).toEqual( {
85
+ type: 'legacy-image-model',
86
+ model: 'image-v1'
339
87
  } );
340
88
  } );
341
89
 
342
- // Category 3: OpenAI Provider (5 tests)
343
- describe( 'OpenAI Provider', () => {
344
- it( 'loads webSearch with empty config', () => {
345
- const result = loadTools( {
346
- config: {
347
- provider: 'openai',
348
- tools: { webSearch: {} }
349
- }
350
- } );
90
+ it( 'prefers provider.image when both image factories exist', () => {
91
+ const provider = vi.fn();
92
+ provider.image = vi.fn( model => ( { type: 'image', model } ) );
93
+ provider.imageModel = vi.fn( model => ( { type: 'imageModel', model } ) );
94
+ getProvider.mockReturnValue( provider );
351
95
 
352
- expect( result.webSearch ).toEqual( {
353
- type: 'webSearch',
354
- config: {}
355
- } );
96
+ const result = loadImageModel( {
97
+ config: {
98
+ provider: 'custom',
99
+ model: 'image-v1'
100
+ }
356
101
  } );
357
102
 
358
- it( 'loads webSearch with searchContextSize', () => {
359
- const result = loadTools( {
360
- config: {
361
- provider: 'openai',
362
- tools: {
363
- webSearch: { searchContextSize: 'high' }
364
- }
365
- }
366
- } );
367
-
368
- expect( result.webSearch.config.searchContextSize ).toBe( 'high' );
369
- } );
370
-
371
- it( 'loads webSearch with filters.allowedDomains array', () => {
372
- const result = loadTools( {
373
- config: {
374
- provider: 'openai',
375
- tools: {
376
- webSearch: {
377
- filters: {
378
- allowedDomains: [ 'wikipedia.org', 'github.com' ]
379
- }
380
- }
381
- }
382
- }
383
- } );
384
-
385
- expect( result.webSearch.config.filters.allowedDomains ).toEqual( [
386
- 'wikipedia.org',
387
- 'github.com'
388
- ] );
389
- } );
390
-
391
- it( 'loads webSearch with userLocation object', () => {
392
- const result = loadTools( {
393
- config: {
394
- provider: 'openai',
395
- tools: {
396
- webSearch: {
397
- userLocation: {
398
- type: 'approximate',
399
- country: 'US',
400
- city: 'San Francisco'
401
- }
402
- }
403
- }
404
- }
405
- } );
406
-
407
- expect( result.webSearch.config.userLocation ).toEqual( {
408
- type: 'approximate',
409
- country: 'US',
410
- city: 'San Francisco'
411
- } );
412
- } );
413
-
414
- it( 'loads webSearch with all config options combined', () => {
415
- const result = loadTools( {
416
- config: {
417
- provider: 'openai',
418
- tools: {
419
- webSearch: {
420
- searchContextSize: 'high',
421
- filters: {
422
- allowedDomains: [ 'wikipedia.org' ]
423
- },
424
- userLocation: {
425
- type: 'approximate',
426
- country: 'US'
427
- }
428
- }
429
- }
430
- }
431
- } );
432
-
433
- expect( result.webSearch.config.searchContextSize ).toBe( 'high' );
434
- expect( result.webSearch.config.filters.allowedDomains ).toHaveLength( 1 );
435
- expect( result.webSearch.config.userLocation.country ).toBe( 'US' );
436
- } );
103
+ expect( provider.image ).toHaveBeenCalledWith( 'image-v1' );
104
+ expect( provider.imageModel ).not.toHaveBeenCalled();
105
+ expect( result.type ).toBe( 'image' );
437
106
  } );
438
107
 
439
- // Category 4: Anthropic Provider (6 tests)
440
- describe( 'Anthropic Provider', () => {
441
- it( 'loads webSearch_20250305 with empty config', () => {
442
- const result = loadTools( {
443
- config: {
444
- provider: 'anthropic',
445
- tools: { webSearch_20250305: {} }
446
- }
447
- } );
448
-
449
- expect( result.webSearch_20250305 ).toEqual( {
450
- type: 'webSearch_20250305',
451
- config: {}
452
- } );
453
- } );
454
-
455
- it( 'loads webSearch_20250305 with maxUses number', () => {
456
- const result = loadTools( {
457
- config: {
458
- provider: 'anthropic',
459
- tools: {
460
- webSearch_20250305: { maxUses: 3 }
461
- }
462
- }
463
- } );
464
-
465
- expect( result.webSearch_20250305.config.maxUses ).toBe( 3 );
466
- } );
467
-
468
- it( 'loads webSearch_20250305 with allowedDomains and blockedDomains', () => {
469
- const result = loadTools( {
470
- config: {
471
- provider: 'anthropic',
472
- tools: {
473
- webSearch_20250305: {
474
- allowedDomains: [ 'reuters.com', 'bbc.com' ],
475
- blockedDomains: [ 'tabloid.com' ]
476
- }
477
- }
478
- }
479
- } );
480
-
481
- expect( result.webSearch_20250305.config.allowedDomains ).toEqual( [
482
- 'reuters.com',
483
- 'bbc.com'
484
- ] );
485
- expect( result.webSearch_20250305.config.blockedDomains ).toEqual( [ 'tabloid.com' ] );
486
- } );
108
+ it( 'throws a clear error when the provider does not support image models', () => {
109
+ getProvider.mockReturnValue( vi.fn() );
487
110
 
488
- it( 'loads webSearch_20250305 with userLocation object', () => {
489
- const result = loadTools( {
490
- config: {
491
- provider: 'anthropic',
492
- tools: {
493
- webSearch_20250305: {
494
- userLocation: {
495
- type: 'approximate',
496
- country: 'GB',
497
- city: 'London',
498
- timezone: 'Europe/London'
499
- }
500
- }
501
- }
502
- }
503
- } );
504
-
505
- expect( result.webSearch_20250305.config.userLocation.city ).toBe( 'London' );
506
- expect( result.webSearch_20250305.config.userLocation.timezone ).toBe( 'Europe/London' );
507
- } );
508
-
509
- it( 'loads bash_20241022 and bash_20250124 tools', () => {
510
- const result = loadTools( {
511
- config: {
512
- provider: 'anthropic',
513
- tools: {
514
- bash_20241022: {},
515
- bash_20250124: {}
516
- }
517
- }
518
- } );
519
-
520
- expect( result.bash_20241022.type ).toBe( 'bash_20241022' );
521
- expect( result.bash_20250124.type ).toBe( 'bash_20250124' );
522
- } );
523
-
524
- it( 'loads codeExecution_20250522 and codeExecution_20250825 tools', () => {
525
- const result = loadTools( {
526
- config: {
527
- provider: 'anthropic',
528
- tools: {
529
- codeExecution_20250522: {},
530
- codeExecution_20250825: {}
531
- }
532
- }
533
- } );
534
-
535
- expect( result.codeExecution_20250522.type ).toBe( 'codeExecution_20250522' );
536
- expect( result.codeExecution_20250825.type ).toBe( 'codeExecution_20250825' );
537
- } );
111
+ expect( () => loadImageModel( {
112
+ config: {
113
+ provider: 'azure',
114
+ model: 'gpt-image-1'
115
+ }
116
+ } ) ).toThrow( 'Provider "azure" does not support image models.' );
538
117
  } );
118
+ } );
539
119
 
540
- // Category 5: Bedrock Provider (4 tests)
541
- describe( 'Bedrock Provider', () => {
542
- it( 'loads bash_20241022 tool', () => {
543
- const result = loadTools( {
544
- config: {
545
- provider: 'bedrock',
546
- tools: { bash_20241022: {} }
547
- }
548
- } );
549
-
550
- expect( result.bash_20241022 ).toEqual( {
551
- type: 'bash_20241022',
552
- config: {}
553
- } );
554
- } );
555
-
556
- it( 'loads textEditor_20241022 and textEditor_20250429 tools', () => {
557
- const result = loadTools( {
558
- config: {
559
- provider: 'bedrock',
560
- tools: {
561
- textEditor_20241022: {},
562
- textEditor_20250429: {}
563
- }
564
- }
565
- } );
566
-
567
- expect( result.textEditor_20241022.type ).toBe( 'textEditor_20241022' );
568
- expect( result.textEditor_20250429.type ).toBe( 'textEditor_20250429' );
569
- } );
570
-
571
- it( 'loads computer_20241022 with config', () => {
572
- const result = loadTools( {
573
- config: {
574
- provider: 'bedrock',
575
- tools: {
576
- computer_20241022: { displayWidthPx: 1024, displayHeightPx: 768 }
577
- }
578
- }
579
- } );
580
-
581
- expect( result.computer_20241022.config ).toEqual( {
582
- displayWidthPx: 1024,
583
- displayHeightPx: 768
584
- } );
120
+ describe( 'loadTools', () => {
121
+ it( 'returns null and does not load the provider when no tools are configured', () => {
122
+ const result = loadTools( {
123
+ config: {
124
+ provider: 'vertex',
125
+ model: 'gemini-2.0-flash'
126
+ }
585
127
  } );
586
128
 
587
- it( 'loads multiple Bedrock tools simultaneously', () => {
588
- const result = loadTools( {
589
- config: {
590
- provider: 'bedrock',
591
- tools: {
592
- bash_20241022: {},
593
- textEditor_20250429: {},
594
- computer_20241022: { displayWidthPx: 1920, displayHeightPx: 1080 }
595
- }
596
- }
597
- } );
598
-
599
- expect( Object.keys( result ) ).toEqual( [ 'bash_20241022', 'textEditor_20250429', 'computer_20241022' ] );
600
- expect( result.bash_20241022.type ).toBe( 'bash_20241022' );
601
- expect( result.textEditor_20250429.type ).toBe( 'textEditor_20250429' );
602
- expect( result.computer_20241022.type ).toBe( 'computer_20241022' );
603
- } );
129
+ expect( result ).toBeNull();
130
+ expect( getProvider ).not.toHaveBeenCalled();
604
131
  } );
605
132
 
606
- // Category 6: Error Handling (10 tests)
607
- describe( 'Error Handling', () => {
608
- it( 'throws clear error for array format (migration guide)', () => {
609
- expect( () => loadTools( {
610
- config: {
611
- provider: 'vertex',
612
- tools: [ 'googleSearch', 'urlContext' ]
613
- }
614
- } ) ).toThrow( /Invalid tools prompt config/ );
615
- } );
616
-
617
- it( 'throws error for string format', () => {
618
- expect( () => loadTools( {
619
- config: {
620
- provider: 'vertex',
621
- tools: 'googleSearch'
622
- }
623
- } ) ).toThrow( /Invalid tools prompt config/ );
624
- } );
625
-
626
- it( 'throws error for number format', () => {
627
- expect( () => loadTools( {
628
- config: {
629
- provider: 'vertex',
630
- tools: 123
631
- }
632
- } ) ).toThrow( /Invalid tools prompt config/ );
633
- } );
634
-
635
- it( 'throws error for provider without tools support', () => {
636
- expect( () => loadTools( {
637
- config: {
638
- provider: 'azure',
639
- tools: { someTool: {} }
640
- }
641
- } ) ).toThrow( 'does not support provider-specific tools' );
642
- } );
643
-
644
- it( 'throws error for unknown tool on Vertex with dynamic tool listing', () => {
645
- expect( () => loadTools( {
646
- config: {
647
- provider: 'vertex',
648
- tools: { unknownTool: {} }
649
- }
650
- } ) ).toThrow( /Unknown tool "unknownTool" for provider "vertex".*Available tools:/ );
651
- } );
652
-
653
- it( 'throws error for unknown tool on OpenAI with dynamic tool listing', () => {
654
- expect( () => loadTools( {
655
- config: {
656
- provider: 'openai',
657
- tools: { googleSearch: {} }
658
- }
659
- } ) ).toThrow( /Unknown tool "googleSearch" for provider "openai".*Available tools:/ );
133
+ it( 'returns null and does not load the provider when tools config is empty', () => {
134
+ const result = loadTools( {
135
+ config: {
136
+ provider: 'vertex',
137
+ tools: {}
138
+ }
660
139
  } );
661
140
 
662
- it( 'throws error for unknown tool on Anthropic with dynamic tool listing', () => {
663
- expect( () => loadTools( {
664
- config: {
665
- provider: 'anthropic',
666
- tools: { googleSearch: {} }
667
- }
668
- } ) ).toThrow( /Unknown tool "googleSearch" for provider "anthropic".*Available tools:/ );
669
- } );
141
+ expect( result ).toBeNull();
142
+ expect( getProvider ).not.toHaveBeenCalled();
143
+ } );
670
144
 
671
- it( 'throws error when tool config is null', () => {
672
- expect( () => loadTools( {
673
- config: {
674
- provider: 'vertex',
675
- tools: { googleSearch: null }
676
- }
677
- } ) ).toThrow( /Invalid tools prompt config.*expected record, received null/s );
678
- } );
145
+ it( 'throws when the provider has no tools object', () => {
146
+ getProvider.mockReturnValue( vi.fn() );
679
147
 
680
- it( 'throws error when tool config is a string', () => {
681
- expect( () => loadTools( {
682
- config: {
683
- provider: 'vertex',
684
- tools: { googleSearch: 'MODE_DYNAMIC' }
148
+ expect( () => loadTools( {
149
+ config: {
150
+ provider: 'azure',
151
+ tools: {
152
+ webSearch: {}
685
153
  }
686
- } ) ).toThrow( /Invalid tools prompt config.*expected record, received string/s );
687
- } );
688
-
689
- it( 'throws error for unknown tool on Bedrock with dynamic tool listing', () => {
690
- expect( () => loadTools( {
691
- config: { provider: 'bedrock', tools: { webSearch: {} } }
692
- } ) ).toThrow( /Unknown tool "webSearch" for provider "bedrock".*Available tools:/ );
693
- } );
154
+ }
155
+ } ) ).toThrow( 'Provider "azure" does not support provider-specific tools.' );
694
156
  } );
695
157
 
696
- // Category 7: Integration (3 tests)
697
- describe( 'Integration', () => {
698
- it( 'simulates variable interpolation scenario', () => {
699
- // Simulate what would come from renderPrompt + parsePrompt
700
- const renderedConfig = {
158
+ it( 'loads a single provider tool with config', () => {
159
+ const googleSearch = vi.fn( config => ( { type: 'googleSearch', config } ) );
160
+ const provider = {
161
+ tools: {
162
+ googleSearch
163
+ }
164
+ };
165
+ getProvider.mockReturnValue( provider );
166
+
167
+ const result = loadTools( {
168
+ config: {
701
169
  provider: 'vertex',
702
- model: 'gemini-2.0-flash',
703
170
  tools: {
704
171
  googleSearch: {
705
172
  mode: 'MODE_DYNAMIC',
706
173
  dynamicThreshold: 0.8
707
174
  }
708
175
  }
709
- };
710
-
711
- const result = loadTools( { config: renderedConfig } );
712
-
713
- expect( result.googleSearch.config ).toEqual( {
714
- mode: 'MODE_DYNAMIC',
715
- dynamicThreshold: 0.8
716
- } );
176
+ }
717
177
  } );
718
178
 
719
- it( 'validates that config objects are passed to factory functions', () => {
720
- const customConfig = {
721
- mode: 'MODE_DYNAMIC',
722
- dynamicThreshold: 0.7,
723
- customField: 'value'
724
- };
725
-
726
- const result = loadTools( {
727
- config: {
728
- provider: 'vertex',
729
- tools: { googleSearch: customConfig }
730
- }
731
- } );
732
-
733
- // The mock returns { type, config }, so we can verify config was passed through
734
- expect( result.googleSearch.config ).toEqual( customConfig );
179
+ expect( getProvider ).toHaveBeenCalledWith( 'vertex' );
180
+ expect( googleSearch ).toHaveBeenCalledWith( {
181
+ mode: 'MODE_DYNAMIC',
182
+ dynamicThreshold: 0.8
735
183
  } );
736
-
737
- it( 'handles nested configuration objects', () => {
738
- const result = loadTools( {
184
+ expect( result ).toEqual( {
185
+ googleSearch: {
186
+ type: 'googleSearch',
739
187
  config: {
740
- provider: 'openai',
741
- tools: {
742
- webSearch: {
743
- searchContextSize: 'high',
744
- filters: {
745
- allowedDomains: [ 'example.com' ],
746
- blockedDomains: [ 'spam.com' ]
747
- },
748
- userLocation: {
749
- type: 'approximate',
750
- country: 'US',
751
- city: 'Seattle',
752
- region: 'WA'
753
- }
754
- }
755
- }
188
+ mode: 'MODE_DYNAMIC',
189
+ dynamicThreshold: 0.8
756
190
  }
757
- } );
758
-
759
- expect( result.webSearch.config.filters ).toBeDefined();
760
- expect( result.webSearch.config.userLocation ).toBeDefined();
761
- expect( result.webSearch.config.filters.allowedDomains ).toEqual( [ 'example.com' ] );
191
+ }
762
192
  } );
763
193
  } );
764
- } );
765
-
766
- describe( 'registerProvider', () => {
767
- afterEach( () => {
768
- for ( const key of Object.keys( providers ) ) {
769
- delete providers[key];
770
- }
771
- Object.assign( providers, builtInProviders );
772
- } );
773
194
 
774
- it( 'registers a custom provider and uses it in loadTextModel', () => {
775
- const customProvider = vi.fn( model => `custom:${model}` );
776
- registerProvider( 'custom', customProvider );
777
-
778
- const result = loadTextModel( { config: { provider: 'custom', model: 'my-model' } } );
779
-
780
- expect( result ).toBe( 'custom:my-model' );
781
- expect( customProvider ).toHaveBeenCalledWith( 'my-model' );
782
- } );
783
-
784
- it( 'overrides a built-in provider', () => {
785
- const overrideOpenai = vi.fn( model => `override:${model}` );
786
- registerProvider( 'openai', overrideOpenai );
787
-
788
- const result = loadTextModel( { config: { provider: 'openai', model: 'gpt-custom' } } );
789
-
790
- expect( result ).toBe( 'override:gpt-custom' );
791
- } );
792
-
793
- it( 'throws when name is empty string', () => {
794
- expect( () => registerProvider( '', vi.fn() ) ).toThrow( 'non-empty string' );
795
- } );
796
-
797
- it( 'throws when name is not a string', () => {
798
- expect( () => registerProvider( 123, vi.fn() ) ).toThrow( 'expected string, received number' );
799
- } );
800
-
801
- it( 'throws when providerFn is not a function', () => {
802
- expect( () => registerProvider( 'bad', 'not-a-function' ) ).toThrow( 'expected function, received string' );
803
- } );
195
+ it( 'loads multiple provider tools', () => {
196
+ const googleSearch = vi.fn( config => ( { type: 'googleSearch', config } ) );
197
+ const urlContext = vi.fn( config => ( { type: 'urlContext', config } ) );
198
+ getProvider.mockReturnValue( {
199
+ tools: {
200
+ googleSearch,
201
+ urlContext
202
+ }
203
+ } );
804
204
 
805
- it( 'throws when providerFn is null', () => {
806
- expect( () => registerProvider( 'bad', null ) ).toThrow( 'expected function, received null' );
807
- } );
808
- } );
205
+ const result = loadTools( {
206
+ config: {
207
+ provider: 'vertex',
208
+ tools: {
209
+ googleSearch: {
210
+ mode: 'MODE_DYNAMIC'
211
+ },
212
+ urlContext: {}
213
+ }
214
+ }
215
+ } );
809
216
 
810
- describe( 'getRegisteredProviders', () => {
811
- afterEach( () => {
812
- for ( const key of Object.keys( providers ) ) {
813
- delete providers[key];
814
- }
815
- Object.assign( providers, builtInProviders );
217
+ expect( Object.keys( result ) ).toEqual( [ 'googleSearch', 'urlContext' ] );
218
+ expect( googleSearch ).toHaveBeenCalledWith( { mode: 'MODE_DYNAMIC' } );
219
+ expect( urlContext ).toHaveBeenCalledWith( {} );
220
+ expect( result.googleSearch.type ).toBe( 'googleSearch' );
221
+ expect( result.urlContext.type ).toBe( 'urlContext' );
816
222
  } );
817
223
 
818
- it( 'returns default providers', () => {
819
- const providers = getRegisteredProviders();
224
+ it( 'throws when a configured tool is not supported by the provider', () => {
225
+ getProvider.mockReturnValue( {
226
+ tools: {
227
+ googleSearch: vi.fn(),
228
+ urlContext: vi.fn()
229
+ }
230
+ } );
820
231
 
821
- expect( providers ).toContain( 'anthropic' );
822
- expect( providers ).toContain( 'openai' );
823
- expect( providers ).toContain( 'azure' );
824
- expect( providers ).toContain( 'vertex' );
825
- expect( providers ).toContain( 'bedrock' );
826
- expect( providers ).toContain( 'perplexity' );
232
+ expect( () => loadTools( {
233
+ config: {
234
+ provider: 'vertex',
235
+ tools: {
236
+ unknownTool: {}
237
+ }
238
+ }
239
+ } ) ).toThrow( 'Invalid tool(s) unknownTool for provider "vertex". Available: googleSearch, urlContext.' );
827
240
  } );
828
241
 
829
- it( 'includes dynamically registered providers', () => {
830
- registerProvider( 'deepseek', vi.fn() );
831
-
832
- const providers = getRegisteredProviders();
242
+ it( 'reports all unsupported configured tools', () => {
243
+ getProvider.mockReturnValue( {
244
+ tools: {
245
+ googleSearch: vi.fn()
246
+ }
247
+ } );
833
248
 
834
- expect( providers ).toContain( 'deepseek' );
249
+ expect( () => loadTools( {
250
+ config: {
251
+ provider: 'vertex',
252
+ tools: {
253
+ unknownTool: {},
254
+ anotherUnknownTool: {}
255
+ }
256
+ }
257
+ } ) ).toThrow( 'Invalid tool(s) unknownTool, anotherUnknownTool for provider "vertex". Available: googleSearch.' );
835
258
  } );
836
259
  } );