@output.ai/llm 0.2.13 → 0.3.0-dev.pr341-daa6878

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.
@@ -15,9 +15,15 @@ vi.mock( './ai_model.js', () => ( {
15
15
  loadTools: ( ...values ) => loadToolsImpl( ...values )
16
16
  } ) );
17
17
 
18
+ const OutputMock = {
19
+ object: vi.fn( opts => ( { _type: 'object', ...opts } ) ),
20
+ array: vi.fn( opts => ( { _type: 'array', ...opts } ) ),
21
+ choice: vi.fn( opts => ( { _type: 'choice', ...opts } ) )
22
+ };
23
+
18
24
  const aiFns = {
19
25
  generateText: vi.fn(),
20
- generateObject: vi.fn()
26
+ Output: OutputMock
21
27
  };
22
28
  vi.mock( 'ai', () => ( aiFns ) );
23
29
 
@@ -47,10 +53,6 @@ const basePrompt = {
47
53
  };
48
54
 
49
55
  beforeEach( () => {
50
- tracingSpies.addEventStart.mockClear();
51
- tracingSpies.addEventEnd.mockClear();
52
- tracingSpies.addEventError.mockClear();
53
-
54
56
  loadModelImpl.mockReset().mockReturnValue( 'MODEL' );
55
57
  loadPromptImpl.mockReset().mockReturnValue( basePrompt );
56
58
 
@@ -60,16 +62,6 @@ beforeEach( () => {
60
62
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
61
63
  finishReason: 'stop'
62
64
  } );
63
- aiFns.generateObject.mockReset().mockResolvedValue( {
64
- object: 'OBJECT',
65
- usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
66
- finishReason: 'stop'
67
- } );
68
-
69
- validators.validateGenerateTextArgs.mockClear();
70
- validators.validateGenerateObjectArgs.mockClear();
71
- validators.validateGenerateArrayArgs.mockClear();
72
- validators.validateGenerateEnumArgs.mockClear();
73
65
  } );
74
66
 
75
67
  afterEach( async () => {
@@ -100,10 +92,11 @@ describe( 'ai_sdk', () => {
100
92
  expect( result.finishReason ).toBe( 'stop' );
101
93
  } );
102
94
 
103
- it( 'generateObject: validates, traces, calls AI with output object and returns object', async () => {
95
+ it( 'generateObject: validates, traces, calls generateText with Output.object', async () => {
104
96
  const { generateObject } = await importSut();
105
- aiFns.generateObject.mockResolvedValueOnce( {
106
- object: { a: 1 },
97
+ aiFns.generateText.mockResolvedValueOnce( {
98
+ output: { a: 1 },
99
+ text: '',
107
100
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
108
101
  finishReason: 'stop'
109
102
  } );
@@ -121,23 +114,30 @@ describe( 'ai_sdk', () => {
121
114
  expect( tracingSpies.addEventStart ).toHaveBeenCalledTimes( 1 );
122
115
  expect( tracingSpies.addEventEnd ).toHaveBeenCalledTimes( 1 );
123
116
 
124
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
125
- output: 'object',
117
+ expect( OutputMock.object ).toHaveBeenCalledWith( {
126
118
  schema,
127
- schemaName: 'Thing',
128
- schemaDescription: 'A thing',
129
- model: 'MODEL',
130
- messages: basePrompt.messages,
131
- temperature: 0.3,
132
- providerOptions: basePrompt.config.providerOptions
119
+ name: 'Thing',
120
+ description: 'A thing'
133
121
  } );
122
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
123
+ expect.objectContaining( {
124
+ output: expect.objectContaining( { _type: 'object' } ),
125
+ model: 'MODEL',
126
+ messages: basePrompt.messages,
127
+ temperature: 0.3,
128
+ providerOptions: basePrompt.config.providerOptions
129
+ } )
130
+ );
131
+ expect( result.output ).toEqual( { a: 1 } );
132
+ // Backward compat: .object still works via Proxy
134
133
  expect( result.object ).toEqual( { a: 1 } );
135
134
  } );
136
135
 
137
- it( 'generateArray: validates, traces, calls AI (item schema) and returns array', async () => {
136
+ it( 'generateArray: validates, traces, calls generateText with Output.array', async () => {
138
137
  const { generateArray } = await importSut();
139
- aiFns.generateObject.mockResolvedValueOnce( {
140
- object: [ 1, 2 ],
138
+ aiFns.generateText.mockResolvedValueOnce( {
139
+ output: [ 1, 2 ],
140
+ text: '',
141
141
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
142
142
  finishReason: 'stop'
143
143
  } );
@@ -155,23 +155,30 @@ describe( 'ai_sdk', () => {
155
155
  expect( tracingSpies.addEventStart ).toHaveBeenCalledTimes( 1 );
156
156
  expect( tracingSpies.addEventEnd ).toHaveBeenCalledTimes( 1 );
157
157
 
158
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
159
- output: 'array',
160
- schema,
161
- schemaName: 'Numbers',
162
- schemaDescription: 'Two numbers',
163
- model: 'MODEL',
164
- messages: basePrompt.messages,
165
- temperature: 0.3,
166
- providerOptions: basePrompt.config.providerOptions
158
+ expect( OutputMock.array ).toHaveBeenCalledWith( {
159
+ element: schema,
160
+ name: 'Numbers',
161
+ description: 'Two numbers'
167
162
  } );
163
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
164
+ expect.objectContaining( {
165
+ output: expect.objectContaining( { _type: 'array' } ),
166
+ model: 'MODEL',
167
+ messages: basePrompt.messages,
168
+ temperature: 0.3,
169
+ providerOptions: basePrompt.config.providerOptions
170
+ } )
171
+ );
172
+ expect( result.output ).toEqual( [ 1, 2 ] );
173
+ // Backward compat: .object still works via Proxy
168
174
  expect( result.object ).toEqual( [ 1, 2 ] );
169
175
  } );
170
176
 
171
- it( 'generateEnum: validates, traces, calls AI with output enum and returns value', async () => {
177
+ it( 'generateEnum: validates, traces, calls generateText with Output.choice', async () => {
172
178
  const { generateEnum } = await importSut();
173
- aiFns.generateObject.mockResolvedValueOnce( {
174
- object: 'B',
179
+ aiFns.generateText.mockResolvedValueOnce( {
180
+ output: 'B',
181
+ text: '',
175
182
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
176
183
  finishReason: 'stop'
177
184
  } );
@@ -183,14 +190,20 @@ describe( 'ai_sdk', () => {
183
190
  expect( tracingSpies.addEventStart ).toHaveBeenCalledTimes( 1 );
184
191
  expect( tracingSpies.addEventEnd ).toHaveBeenCalledTimes( 1 );
185
192
 
186
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
187
- output: 'enum',
188
- enum: [ 'A', 'B', 'C' ],
189
- model: 'MODEL',
190
- messages: basePrompt.messages,
191
- temperature: 0.3,
192
- providerOptions: basePrompt.config.providerOptions
193
+ expect( OutputMock.choice ).toHaveBeenCalledWith( {
194
+ options: [ 'A', 'B', 'C' ]
193
195
  } );
196
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
197
+ expect.objectContaining( {
198
+ output: expect.objectContaining( { _type: 'choice' } ),
199
+ model: 'MODEL',
200
+ messages: basePrompt.messages,
201
+ temperature: 0.3,
202
+ providerOptions: basePrompt.config.providerOptions
203
+ } )
204
+ );
205
+ expect( result.output ).toBe( 'B' );
206
+ // Backward compat: .object still works via Proxy
194
207
  expect( result.object ).toBe( 'B' );
195
208
  } );
196
209
 
@@ -257,21 +270,20 @@ describe( 'ai_sdk', () => {
257
270
  const schema = z.object( { result: z.string() } );
258
271
  await generateObject( { prompt: 'test_prompt@v1', schema } );
259
272
 
260
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
261
- output: 'object',
262
- schema,
263
- schemaName: undefined,
264
- schemaDescription: undefined,
265
- model: 'MODEL',
266
- messages: promptWithOpenAIOptions.messages,
267
- temperature: 0.8,
268
- providerOptions: {
269
- openai: {
270
- reasoningEffort: 'high',
271
- reasoningSummary: 'detailed'
273
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
274
+ expect.objectContaining( {
275
+ output: expect.objectContaining( { _type: 'object' } ),
276
+ model: 'MODEL',
277
+ messages: promptWithOpenAIOptions.messages,
278
+ temperature: 0.8,
279
+ providerOptions: {
280
+ openai: {
281
+ reasoningEffort: 'high',
282
+ reasoningSummary: 'detailed'
283
+ }
272
284
  }
273
- }
274
- } );
285
+ } )
286
+ );
275
287
  } );
276
288
 
277
289
  it( 'generateArray: passes azure-specific options to AI SDK', async () => {
@@ -295,21 +307,20 @@ describe( 'ai_sdk', () => {
295
307
  const schema = z.string();
296
308
  await generateArray( { prompt: 'test_prompt@v1', schema } );
297
309
 
298
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
299
- output: 'array',
300
- schema,
301
- schemaName: undefined,
302
- schemaDescription: undefined,
303
- model: 'MODEL',
304
- messages: promptWithAzureOptions.messages,
305
- maxOutputTokens: 2000,
306
- providerOptions: {
307
- azure: {
308
- deploymentName: 'my-deployment',
309
- apiVersion: '2023-12-01-preview'
310
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
311
+ expect.objectContaining( {
312
+ output: expect.objectContaining( { _type: 'array' } ),
313
+ model: 'MODEL',
314
+ messages: promptWithAzureOptions.messages,
315
+ maxOutputTokens: 2000,
316
+ providerOptions: {
317
+ azure: {
318
+ deploymentName: 'my-deployment',
319
+ apiVersion: '2023-12-01-preview'
320
+ }
310
321
  }
311
- }
312
- } );
322
+ } )
323
+ );
313
324
  } );
314
325
 
315
326
  it( 'generateEnum: passes mixed provider options to AI SDK', async () => {
@@ -335,23 +346,24 @@ describe( 'ai_sdk', () => {
335
346
  const { generateEnum } = await importSut();
336
347
  await generateEnum( { prompt: 'test_prompt@v1', enum: [ 'A', 'B', 'C' ] } );
337
348
 
338
- expect( aiFns.generateObject ).toHaveBeenCalledWith( {
339
- output: 'enum',
340
- enum: [ 'A', 'B', 'C' ],
341
- model: 'MODEL',
342
- messages: promptWithMixedOptions.messages,
343
- maxOutputTokens: 64000,
344
- providerOptions: {
345
- thinking: {
346
- type: 'enabled',
347
- budgetTokens: 3000
348
- },
349
- anthropic: {
350
- effort: 'high'
351
- },
352
- customField: { nested: 'value' }
353
- }
354
- } );
349
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
350
+ expect.objectContaining( {
351
+ output: expect.objectContaining( { _type: 'choice' } ),
352
+ model: 'MODEL',
353
+ messages: promptWithMixedOptions.messages,
354
+ maxOutputTokens: 64000,
355
+ providerOptions: {
356
+ thinking: {
357
+ type: 'enabled',
358
+ budgetTokens: 3000
359
+ },
360
+ anthropic: {
361
+ effort: 'high'
362
+ },
363
+ customField: { nested: 'value' }
364
+ }
365
+ } )
366
+ );
355
367
  } );
356
368
 
357
369
  it( 'generateText: passes through providerMetadata', async () => {
@@ -394,10 +406,11 @@ describe( 'ai_sdk', () => {
394
406
  expect( response.result ).toBe( response.text );
395
407
  } );
396
408
 
397
- it( 'generateObject: includes unified result field that matches object', async () => {
409
+ it( 'generateObject: includes unified result field that matches output', async () => {
398
410
  const { generateObject } = await importSut();
399
- aiFns.generateObject.mockResolvedValueOnce( {
400
- object: { a: 1, b: 'test' },
411
+ aiFns.generateText.mockResolvedValueOnce( {
412
+ output: { a: 1, b: 'test' },
413
+ text: '',
401
414
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
402
415
  finishReason: 'stop'
403
416
  } );
@@ -406,13 +419,16 @@ describe( 'ai_sdk', () => {
406
419
  const response = await generateObject( { prompt: 'test_prompt@v1', schema } );
407
420
 
408
421
  expect( response.result ).toEqual( { a: 1, b: 'test' } );
409
- expect( response.result ).toEqual( response.object );
422
+ expect( response.result ).toEqual( response.output );
423
+ // Backward compat
424
+ expect( response.object ).toEqual( response.output );
410
425
  } );
411
426
 
412
- it( 'generateArray: includes unified result field that matches object', async () => {
427
+ it( 'generateArray: includes unified result field that matches output', async () => {
413
428
  const { generateArray } = await importSut();
414
- aiFns.generateObject.mockResolvedValueOnce( {
415
- object: [ 'item1', 'item2', 'item3' ],
429
+ aiFns.generateText.mockResolvedValueOnce( {
430
+ output: [ 'item1', 'item2', 'item3' ],
431
+ text: '',
416
432
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
417
433
  finishReason: 'stop'
418
434
  } );
@@ -421,13 +437,16 @@ describe( 'ai_sdk', () => {
421
437
  const response = await generateArray( { prompt: 'test_prompt@v1', schema } );
422
438
 
423
439
  expect( response.result ).toEqual( [ 'item1', 'item2', 'item3' ] );
424
- expect( response.result ).toEqual( response.object );
440
+ expect( response.result ).toEqual( response.output );
441
+ // Backward compat
442
+ expect( response.object ).toEqual( response.output );
425
443
  } );
426
444
 
427
- it( 'generateEnum: includes unified result field that matches object', async () => {
445
+ it( 'generateEnum: includes unified result field that matches output', async () => {
428
446
  const { generateEnum } = await importSut();
429
- aiFns.generateObject.mockResolvedValueOnce( {
430
- object: 'yes',
447
+ aiFns.generateText.mockResolvedValueOnce( {
448
+ output: 'yes',
449
+ text: '',
431
450
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
432
451
  finishReason: 'stop'
433
452
  } );
@@ -435,7 +454,9 @@ describe( 'ai_sdk', () => {
435
454
  const response = await generateEnum( { prompt: 'test_prompt@v1', enum: [ 'yes', 'no' ] } );
436
455
 
437
456
  expect( response.result ).toBe( 'yes' );
438
- expect( response.result ).toBe( response.object );
457
+ expect( response.result ).toBe( response.output );
458
+ // Backward compat
459
+ expect( response.object ).toBe( response.output );
439
460
  } );
440
461
 
441
462
  it( 'generateText: traces error and rethrows when AI SDK fails', async () => {
@@ -451,7 +472,7 @@ describe( 'ai_sdk', () => {
451
472
 
452
473
  it( 'generateObject: traces error and rethrows when AI SDK fails', async () => {
453
474
  const error = new Error( 'Invalid schema' );
454
- aiFns.generateObject.mockRejectedValueOnce( error );
475
+ aiFns.generateText.mockRejectedValueOnce( error );
455
476
  const { generateObject } = await importSut();
456
477
 
457
478
  const schema = z.object( { a: z.number() } );
@@ -482,21 +503,24 @@ describe( 'ai_sdk', () => {
482
503
 
483
504
  it( 'generateObject: Proxy correctly handles AI SDK response with getter', async () => {
484
505
  const responseWithGetter = {
485
- _internalObject: { value: 42 },
486
- get object() {
487
- return this._internalObject;
506
+ _internalOutput: { value: 42 },
507
+ get output() {
508
+ return this._internalOutput;
488
509
  },
510
+ text: '',
489
511
  usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
490
512
  finishReason: 'stop'
491
513
  };
492
- aiFns.generateObject.mockResolvedValueOnce( responseWithGetter );
514
+ aiFns.generateText.mockResolvedValueOnce( responseWithGetter );
493
515
 
494
516
  const { generateObject } = await importSut();
495
517
  const schema = z.object( { value: z.number() } );
496
518
  const response = await generateObject( { prompt: 'test_prompt@v1', schema } );
497
519
 
498
- expect( response.object ).toEqual( { value: 42 } );
520
+ expect( response.output ).toEqual( { value: 42 } );
499
521
  expect( response.result ).toEqual( { value: 42 } );
522
+ // Backward compat via Proxy
523
+ expect( response.object ).toEqual( { value: 42 } );
500
524
  } );
501
525
 
502
526
  it( 'generateText: passes through AI SDK options like tools and maxRetries', async () => {
@@ -561,6 +585,13 @@ describe( 'ai_sdk', () => {
561
585
  const { generateObject } = await importSut();
562
586
  const schema = z.object( { a: z.number() } );
563
587
 
588
+ aiFns.generateText.mockResolvedValueOnce( {
589
+ output: { a: 1 },
590
+ text: '',
591
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
592
+ finishReason: 'stop'
593
+ } );
594
+
564
595
  await generateObject( {
565
596
  prompt: 'test_prompt@v1',
566
597
  schema,
@@ -569,7 +600,7 @@ describe( 'ai_sdk', () => {
569
600
  topP: 0.9
570
601
  } );
571
602
 
572
- expect( aiFns.generateObject ).toHaveBeenCalledWith(
603
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
573
604
  expect.objectContaining( {
574
605
  maxRetries: 3,
575
606
  seed: 123,
@@ -583,6 +614,13 @@ describe( 'ai_sdk', () => {
583
614
  const schema = z.string();
584
615
  const controller = new AbortController();
585
616
 
617
+ aiFns.generateText.mockResolvedValueOnce( {
618
+ output: [ 'a' ],
619
+ text: '',
620
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
621
+ finishReason: 'stop'
622
+ } );
623
+
586
624
  await generateArray( {
587
625
  prompt: 'test_prompt@v1',
588
626
  schema,
@@ -590,7 +628,7 @@ describe( 'ai_sdk', () => {
590
628
  headers: { 'X-Custom': 'value' }
591
629
  } );
592
630
 
593
- expect( aiFns.generateObject ).toHaveBeenCalledWith(
631
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
594
632
  expect.objectContaining( {
595
633
  abortSignal: controller.signal,
596
634
  headers: { 'X-Custom': 'value' }
@@ -601,6 +639,13 @@ describe( 'ai_sdk', () => {
601
639
  it( 'generateEnum: passes through AI SDK options', async () => {
602
640
  const { generateEnum } = await importSut();
603
641
 
642
+ aiFns.generateText.mockResolvedValueOnce( {
643
+ output: 'A',
644
+ text: '',
645
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
646
+ finishReason: 'stop'
647
+ } );
648
+
604
649
  await generateEnum( {
605
650
  prompt: 'test_prompt@v1',
606
651
  enum: [ 'A', 'B' ],
@@ -608,7 +653,7 @@ describe( 'ai_sdk', () => {
608
653
  presencePenalty: 0.5
609
654
  } );
610
655
 
611
- expect( aiFns.generateObject ).toHaveBeenCalledWith(
656
+ expect( aiFns.generateText ).toHaveBeenCalledWith(
612
657
  expect.objectContaining( {
613
658
  stopSequences: [ 'END' ],
614
659
  presencePenalty: 0.5
@@ -616,6 +661,15 @@ describe( 'ai_sdk', () => {
616
661
  );
617
662
  } );
618
663
 
664
+ it( 'generateText: .object returns undefined instead of leaking text', async () => {
665
+ const { generateText } = await importSut();
666
+ const result = await generateText( { prompt: 'test_prompt@v1' } );
667
+
668
+ expect( result.object ).toBeUndefined();
669
+ expect( result.text ).toBe( 'TEXT' );
670
+ expect( result.result ).toBe( 'TEXT' );
671
+ } );
672
+
619
673
  it( 'generateText: passes through unknown future options for forward compatibility', async () => {
620
674
  const { generateText } = await importSut();
621
675