@@ -356,3 +356,149 @@ describe('result.object', () => {
356
356
} ) ;
357
357
} ) ;
358
358
} ) ;
359
+
360
+ describe ( 'onFinish callback' , ( ) => {
361
+ describe ( 'with successfully validated object' , ( ) => {
362
+ let result : Parameters <
363
+ Required < Parameters < typeof streamObject > [ 0 ] > [ 'onFinish' ]
364
+ > [ 0 ] ;
365
+
366
+ beforeEach ( async ( ) => {
367
+ const { partialObjectStream } = await streamObject ( {
368
+ model : new MockLanguageModelV1 ( {
369
+ doStream : async ( { prompt, mode } ) => {
370
+ assert . deepStrictEqual ( mode , { type : 'object-json' } ) ;
371
+ assert . deepStrictEqual ( prompt , [
372
+ {
373
+ role : 'system' ,
374
+ content :
375
+ 'JSON schema:\n' +
376
+ '{"type":"object","properties":{"content":{"type":"string"}},"required":["content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}\n' +
377
+ 'You MUST answer with a JSON object that matches the JSON schema above.' ,
378
+ } ,
379
+ { role : 'user' , content : [ { type : 'text' , text : 'prompt' } ] } ,
380
+ ] ) ;
381
+
382
+ return {
383
+ stream : convertArrayToReadableStream ( [
384
+ { type : 'text-delta' , textDelta : '{ ' } ,
385
+ { type : 'text-delta' , textDelta : '"content": ' } ,
386
+ { type : 'text-delta' , textDelta : `"Hello, ` } ,
387
+ { type : 'text-delta' , textDelta : `world` } ,
388
+ { type : 'text-delta' , textDelta : `!"` } ,
389
+ { type : 'text-delta' , textDelta : ' }' } ,
390
+ {
391
+ type : 'finish' ,
392
+ finishReason : 'stop' ,
393
+ usage : { completionTokens : 10 , promptTokens : 3 } ,
394
+ } ,
395
+ ] ) ,
396
+ rawCall : { rawPrompt : 'prompt' , rawSettings : { } } ,
397
+ } ;
398
+ } ,
399
+ } ) ,
400
+ schema : z . object ( { content : z . string ( ) } ) ,
401
+ mode : 'json' ,
402
+ prompt : 'prompt' ,
403
+ onFinish : async event => {
404
+ result = event as unknown as typeof result ;
405
+ } ,
406
+ } ) ;
407
+
408
+ // consume stream
409
+ await convertAsyncIterableToArray ( partialObjectStream ) ;
410
+ } ) ;
411
+
412
+ it ( 'should contain token usage' , async ( ) => {
413
+ assert . deepStrictEqual ( result . usage , {
414
+ completionTokens : 10 ,
415
+ promptTokens : 3 ,
416
+ totalTokens : 13 ,
417
+ } ) ;
418
+ } ) ;
419
+
420
+ it ( 'should contain the full object' , async ( ) => {
421
+ assert . deepStrictEqual ( result . object , {
422
+ content : 'Hello, world!' ,
423
+ } ) ;
424
+ } ) ;
425
+
426
+ it ( 'should not contain an error object' , async ( ) => {
427
+ assert . deepStrictEqual ( result . error , undefined ) ;
428
+ } ) ;
429
+ } ) ;
430
+
431
+ describe ( "with object that doesn't match the schema" , ( ) => {
432
+ let result : Parameters <
433
+ Required < Parameters < typeof streamObject > [ 0 ] > [ 'onFinish' ]
434
+ > [ 0 ] ;
435
+
436
+ beforeEach ( async ( ) => {
437
+ const { partialObjectStream, object } = await streamObject ( {
438
+ model : new MockLanguageModelV1 ( {
439
+ doStream : async ( { prompt, mode } ) => {
440
+ assert . deepStrictEqual ( mode , { type : 'object-json' } ) ;
441
+ assert . deepStrictEqual ( prompt , [
442
+ {
443
+ role : 'system' ,
444
+ content :
445
+ 'JSON schema:\n' +
446
+ '{"type":"object","properties":{"content":{"type":"string"}},"required":["content"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}\n' +
447
+ 'You MUST answer with a JSON object that matches the JSON schema above.' ,
448
+ } ,
449
+ { role : 'user' , content : [ { type : 'text' , text : 'prompt' } ] } ,
450
+ ] ) ;
451
+
452
+ return {
453
+ stream : convertArrayToReadableStream ( [
454
+ { type : 'text-delta' , textDelta : '{ ' } ,
455
+ { type : 'text-delta' , textDelta : '"invalid": ' } ,
456
+ { type : 'text-delta' , textDelta : `"Hello, ` } ,
457
+ { type : 'text-delta' , textDelta : `world` } ,
458
+ { type : 'text-delta' , textDelta : `!"` } ,
459
+ { type : 'text-delta' , textDelta : ' }' } ,
460
+ {
461
+ type : 'finish' ,
462
+ finishReason : 'stop' ,
463
+ usage : { completionTokens : 10 , promptTokens : 3 } ,
464
+ } ,
465
+ ] ) ,
466
+ rawCall : { rawPrompt : 'prompt' , rawSettings : { } } ,
467
+ } ;
468
+ } ,
469
+ } ) ,
470
+ schema : z . object ( { content : z . string ( ) } ) ,
471
+ mode : 'json' ,
472
+ prompt : 'prompt' ,
473
+ onFinish : async event => {
474
+ result = event as unknown as typeof result ;
475
+ } ,
476
+ } ) ;
477
+
478
+ // consume stream
479
+ await convertAsyncIterableToArray ( partialObjectStream ) ;
480
+
481
+ // consume expected error rejection
482
+ await object . catch ( ( ) => { } ) ;
483
+ } ) ;
484
+
485
+ it ( 'should contain token usage' , async ( ) => {
486
+ assert . deepStrictEqual ( result . usage , {
487
+ completionTokens : 10 ,
488
+ promptTokens : 3 ,
489
+ totalTokens : 13 ,
490
+ } ) ;
491
+ } ) ;
492
+
493
+ it ( 'should not contain a full object' , async ( ) => {
494
+ assert . deepStrictEqual ( result . object , undefined ) ;
495
+ } ) ;
496
+
497
+ it ( 'should contain an error object' , async ( ) => {
498
+ assert . deepStrictEqual (
499
+ TypeValidationError . isTypeValidationError ( result . error ) ,
500
+ true ,
501
+ ) ;
502
+ } ) ;
503
+ } ) ;
504
+ } ) ;
0 commit comments