@@ -411,6 +411,91 @@ def test_create_span_non_component(self):
411411 # Verify start_as_current_span was called for the actual span creation (not just parent)
412412 assert mock_client .start_as_current_span .call_count == 2 # Once for parent, once for the span
413413
414+ def test_handle_embedder_with_openai_format (self ):
415+ """Test that embedder usage is extracted in OpenAI format."""
416+ mock_span = Mock ()
417+ mock_span .raw_span .return_value = mock_span
418+ mock_span .get_data .return_value = {
419+ "haystack.component.type" : "OpenAITextEmbedder" ,
420+ "haystack.component.output" : {
421+ "embedding" : [0.1 , 0.2 , 0.3 ],
422+ "meta" : {"model" : "custom-model" , "usage" : {"prompt_tokens" : 15 , "total_tokens" : 15 }},
423+ },
424+ }
425+
426+ handler = DefaultSpanHandler ()
427+ handler .handle (mock_span , component_type = "OpenAITextEmbedder" )
428+
429+ assert mock_span .update .call_count == 1
430+ assert mock_span .update .call_args_list [0 ][1 ] == {
431+ "usage_details" : {"prompt_tokens" : 15 , "total_tokens" : 15 },
432+ "model" : "custom-model" ,
433+ }
434+
435+ def test_handle_embedder_with_cohere_format (self ):
436+ """Test that embedder usage is extracted in Cohere billed_units format."""
437+ mock_span = Mock ()
438+ mock_span .raw_span .return_value = mock_span
439+ mock_span .get_data .return_value = {
440+ "haystack.component.type" : "CohereTextEmbedder" ,
441+ "haystack.component.output" : {
442+ "embedding" : [0.1 , 0.2 , 0.3 ],
443+ "meta" : {"api_version" : {"version" : "1" }, "billed_units" : {"input_tokens" : 4 }},
444+ },
445+ }
446+
447+ handler = DefaultSpanHandler ()
448+ handler .handle (mock_span , component_type = "CohereTextEmbedder" )
449+
450+ assert mock_span .update .call_count == 1
451+ assert mock_span .update .call_args_list [0 ][1 ] == {"usage_details" : {"input_tokens" : 4 }}
452+
453+ def test_handle_embedder_without_usage (self ):
454+ """Test that embedders without usage data are handled gracefully."""
455+ mock_span = Mock ()
456+ mock_span .raw_span .return_value = mock_span
457+ mock_span .get_data .return_value = {
458+ "haystack.component.type" : "SentenceTransformersTextEmbedder" ,
459+ "haystack.component.output" : {
460+ "embedding" : [0.1 , 0.2 , 0.3 ],
461+ "meta" : {}, # No usage data
462+ },
463+ }
464+
465+ handler = DefaultSpanHandler ()
466+ handler .handle (mock_span , component_type = "SentenceTransformersTextEmbedder" )
467+
468+ # Should not call update when no usage data is available
469+ assert mock_span .update .call_count == 0
470+
471+ def test_handle_embedder_with_nested_usage (self ):
472+ """Test that embedders with nested usage data are sanitized correctly."""
473+ mock_span = Mock ()
474+ mock_span .raw_span .return_value = mock_span
475+ mock_span .get_data .return_value = {
476+ "haystack.component.type" : "CustomEmbedder" ,
477+ "haystack.component.output" : {
478+ "embedding" : [0.1 , 0.2 , 0.3 ],
479+ "meta" : {
480+ "model" : "custom-model" ,
481+ "usage" : {
482+ "cache_creation" : {"input_tokens" : 10 },
483+ "cache_read" : {"input_tokens" : 5 },
484+ "total_tokens" : 15 ,
485+ },
486+ },
487+ },
488+ }
489+
490+ handler = DefaultSpanHandler ()
491+ handler .handle (mock_span , component_type = "CustomEmbedder" )
492+
493+ assert mock_span .update .call_count == 1
494+ assert mock_span .update .call_args_list [0 ][1 ] == {
495+ "usage_details" : {"cache_creation.input_tokens" : 10 , "cache_read.input_tokens" : 5 , "total_tokens" : 15 },
496+ "model" : "custom-model" ,
497+ }
498+
414499
415500class TestCustomSpanHandler :
416501 def test_handle (self ):
0 commit comments