@@ -653,6 +653,117 @@ describe('Langfuse integration', () => {
653653 } )
654654 } )
655655
656+ describe ( 'convertToolsToLangfuse' , ( ) => {
657+ test ( 'converts Anthropic tool schema to OpenAI-style format' , async ( ) => {
658+ const { convertToolsToLangfuse } = await import ( '../convert.js' )
659+ const tools = [
660+ {
661+ name : 'BashTool' ,
662+ description : 'Execute a bash command' ,
663+ input_schema : {
664+ type : 'object' ,
665+ properties : { command : { type : 'string' } } ,
666+ required : [ 'command' ] ,
667+ } ,
668+ } ,
669+ ]
670+ const result = convertToolsToLangfuse ( tools ) as Array < Record < string , unknown > >
671+ expect ( result ) . toHaveLength ( 1 )
672+ expect ( result [ 0 ] ) . toEqual ( {
673+ type : 'function' ,
674+ function : {
675+ name : 'BashTool' ,
676+ description : 'Execute a bash command' ,
677+ parameters : {
678+ type : 'object' ,
679+ properties : { command : { type : 'string' } } ,
680+ required : [ 'command' ] ,
681+ } ,
682+ } ,
683+ } )
684+ } )
685+
686+ test ( 'converts multiple tools' , async ( ) => {
687+ const { convertToolsToLangfuse } = await import ( '../convert.js' )
688+ const tools = [
689+ { name : 'ReadTool' , description : 'Read a file' , input_schema : { type : 'object' } } ,
690+ { name : 'WriteTool' , description : 'Write a file' , input_schema : { type : 'object' } } ,
691+ ]
692+ const result = convertToolsToLangfuse ( tools ) as Array < Record < string , unknown > >
693+ expect ( result ) . toHaveLength ( 2 )
694+ expect ( ( result [ 0 ] ! . function as Record < string , unknown > ) . name ) . toBe ( 'ReadTool' )
695+ expect ( ( result [ 1 ] ! . function as Record < string , unknown > ) . name ) . toBe ( 'WriteTool' )
696+ } )
697+
698+ test ( 'falls back to parameters when input_schema is missing' , async ( ) => {
699+ const { convertToolsToLangfuse } = await import ( '../convert.js' )
700+ const tools = [
701+ { name : 'Tool1' , description : 'desc' , parameters : { type : 'object' , properties : { a : { type : 'string' } } } } ,
702+ ]
703+ const result = convertToolsToLangfuse ( tools ) as Array < Record < string , unknown > >
704+ expect ( ( result [ 0 ] ! . function as Record < string , unknown > ) . parameters ) . toEqual ( {
705+ type : 'object' ,
706+ properties : { a : { type : 'string' } } ,
707+ } )
708+ } )
709+
710+ test ( 'uses empty object when neither input_schema nor parameters exist' , async ( ) => {
711+ const { convertToolsToLangfuse } = await import ( '../convert.js' )
712+ const tools = [ { name : 'Tool1' , description : 'desc' } ]
713+ const result = convertToolsToLangfuse ( tools ) as Array < Record < string , unknown > >
714+ expect ( ( result [ 0 ] ! . function as Record < string , unknown > ) . parameters ) . toEqual ( { } )
715+ } )
716+
717+ test ( 'returns empty array for empty input' , async ( ) => {
718+ const { convertToolsToLangfuse } = await import ( '../convert.js' )
719+ expect ( convertToolsToLangfuse ( [ ] ) ) . toEqual ( [ ] )
720+ } )
721+ } )
722+
723+ describe ( 'recordLLMObservation with tools' , ( ) => {
724+ test ( 'wraps input into { messages, tools } when tools provided' , async ( ) => {
725+ process . env . LANGFUSE_PUBLIC_KEY = 'pk-test'
726+ process . env . LANGFUSE_SECRET_KEY = 'sk-test'
727+ const { createTrace, recordLLMObservation } = await import ( '../tracing.js' )
728+ const span = createTrace ( { sessionId : 's1' , model : 'claude-3' , provider : 'firstParty' } )
729+ mockStartObservation . mockClear ( )
730+ const messages = [ { role : 'user' , content : 'hello' } ]
731+ const tools = [ { type : 'function' , function : { name : 'Bash' , description : 'Run' , parameters : { } } } ]
732+ recordLLMObservation ( span , {
733+ model : 'claude-3' ,
734+ provider : 'firstParty' ,
735+ input : messages ,
736+ output : [ ] ,
737+ usage : { input_tokens : 10 , output_tokens : 5 } ,
738+ tools,
739+ } )
740+ expect ( mockStartObservation ) . toHaveBeenCalledWith ( 'ChatAnthropic' , expect . objectContaining ( {
741+ input : { messages, tools } ,
742+ } ) , expect . objectContaining ( {
743+ asType : 'generation' ,
744+ } ) )
745+ } )
746+
747+ test ( 'keeps input as-is when tools not provided' , async ( ) => {
748+ process . env . LANGFUSE_PUBLIC_KEY = 'pk-test'
749+ process . env . LANGFUSE_SECRET_KEY = 'sk-test'
750+ const { createTrace, recordLLMObservation } = await import ( '../tracing.js' )
751+ const span = createTrace ( { sessionId : 's1' , model : 'claude-3' , provider : 'firstParty' } )
752+ mockStartObservation . mockClear ( )
753+ const messages = [ { role : 'user' , content : 'hello' } ]
754+ recordLLMObservation ( span , {
755+ model : 'claude-3' ,
756+ provider : 'firstParty' ,
757+ input : messages ,
758+ output : [ ] ,
759+ usage : { input_tokens : 10 , output_tokens : 5 } ,
760+ } )
761+ expect ( mockStartObservation ) . toHaveBeenCalledWith ( 'ChatAnthropic' , expect . objectContaining ( {
762+ input : messages ,
763+ } ) , expect . any ( Object ) )
764+ } )
765+ } )
766+
656767 describe ( 'SDK exceptions do not affect main flow' , ( ) => {
657768 test ( 'createTrace returns null on SDK error' , async ( ) => {
658769 process . env . LANGFUSE_PUBLIC_KEY = 'pk-test'
0 commit comments