@@ -47,6 +47,43 @@ type LangfuseInputMessage =
4747 | AssistantMessage
4848 | LangfuseChatMessage
4949
50+ function isRecord ( value : unknown ) : value is Record < string , unknown > {
51+ return value !== null && typeof value === 'object' && ! Array . isArray ( value )
52+ }
53+
54+ function isLangfuseToolCall ( value : unknown ) : value is LangfuseToolCall {
55+ if ( ! isRecord ( value ) ) return false
56+ const fn = value . function
57+ return (
58+ typeof value . id === 'string' &&
59+ value . type === 'function' &&
60+ isRecord ( fn ) &&
61+ typeof fn . name === 'string' &&
62+ typeof fn . arguments === 'string'
63+ )
64+ }
65+
66+ function getToolCalls ( value : unknown ) : LangfuseToolCall [ ] {
67+ return Array . isArray ( value ) ? value . filter ( isLangfuseToolCall ) : [ ]
68+ }
69+
70+ function getContentToolCalls ( content : unknown [ ] ) : LangfuseToolCall [ ] {
71+ return content . flatMap ( block =>
72+ isRecord ( block ) ? getToolCalls ( block . tool_calls ) : [ ] ,
73+ )
74+ }
75+
76+ function mergeToolCalls (
77+ ...groups : readonly LangfuseToolCall [ ] [ ]
78+ ) : LangfuseToolCall [ ] {
79+ const merged = new Map < string , LangfuseToolCall > ( )
80+ for ( const toolCall of groups . flat ( ) ) {
81+ const key = toolCall . id || `${ toolCall . function . name } :${ toolCall . function . arguments } `
82+ if ( ! merged . has ( key ) ) merged . set ( key , toolCall )
83+ }
84+ return [ ...merged . values ( ) ]
85+ }
86+
5087/** Normalize a content block into a LangfuseContentPart (non-tool_use, non-tool_result) */
5188function toContentPart ( block : Record < string , unknown > ) : LangfuseContentPart | null {
5289 const type = block . type as string | undefined
@@ -163,30 +200,34 @@ export function convertMessagesToLangfuse(
163200 isLangfuseRole ( inner . role ) ? inner . role : isWrappedMessage ? toRole ( msg ) : 'user'
164201 const rawContent = inner . content
165202 if ( typeof rawContent === 'string' || ! Array . isArray ( rawContent ) ) {
203+ const toolCalls = getToolCalls ( inner . tool_calls )
166204 result . push ( {
167205 role,
168206 content : String ( rawContent ?? '' ) ,
169207 ...( 'tool_call_id' in inner && typeof inner . tool_call_id === 'string'
170208 ? { tool_call_id : inner . tool_call_id }
171209 : { } ) ,
172- ...( 'tool_calls' in inner && Array . isArray ( inner . tool_calls )
173- ? { tool_calls : inner . tool_calls as LangfuseToolCall [ ] }
174- : { } ) ,
210+ ...( toolCalls . length > 0 ? { tool_calls : toolCalls } : { } ) ,
175211 } )
176212 continue
177213 }
178214
179215 if ( role === 'assistant' ) {
180216 // Extract tool_use → tool_calls at message level
181217 const { tool_calls, rest } = extractToolCalls ( rawContent )
218+ const allToolCalls = mergeToolCalls (
219+ tool_calls ,
220+ getToolCalls ( inner . tool_calls ) ,
221+ getContentToolCalls ( rest ) ,
222+ )
182223 const parts = rest
183224 . filter ( ( b ) : b is Record < string , unknown > => b != null && typeof b === 'object' )
184225 . map ( b => toContentPart ( b ) )
185226 . filter ( ( p ) : p is LangfuseContentPart => p !== null )
186227 result . push ( {
187228 role : 'assistant' ,
188229 content : collapseContent ( parts ) ,
189- ...( tool_calls . length > 0 && { tool_calls } ) ,
230+ ...( allToolCalls . length > 0 && { tool_calls : allToolCalls } ) ,
190231 } )
191232 } else {
192233 // User messages: extract tool_result → separate tool messages
@@ -196,15 +237,17 @@ export function convertMessagesToLangfuse(
196237 . map ( b => toContentPart ( b ) )
197238 . filter ( ( p ) : p is LangfuseContentPart => p !== null )
198239 if ( parts . length > 0 || toolMessages . length === 0 ) {
240+ const toolCalls = mergeToolCalls (
241+ getToolCalls ( inner . tool_calls ) ,
242+ getContentToolCalls ( rest ) ,
243+ )
199244 result . push ( {
200245 role,
201246 content : collapseContent ( parts ) ,
202247 ...( 'tool_call_id' in inner && typeof inner . tool_call_id === 'string'
203248 ? { tool_call_id : inner . tool_call_id }
204249 : { } ) ,
205- ...( 'tool_calls' in inner && Array . isArray ( inner . tool_calls )
206- ? { tool_calls : inner . tool_calls as LangfuseToolCall [ ] }
207- : { } ) ,
250+ ...( toolCalls . length > 0 ? { tool_calls : toolCalls } : { } ) ,
208251 } )
209252 }
210253 result . push ( ...toolMessages )
0 commit comments