@@ -2154,6 +2154,58 @@ def test_get_response_attributes_with_usage():
21542154 assert result [OtelAttr .OUTPUT_TOKENS ] == 50
21552155
21562156
2157+ def test_get_response_attributes_with_additional_usage ():
2158+ """Test _get_response_attributes maps additional usage details to OTel attributes."""
2159+ from unittest .mock import Mock
2160+
2161+ from agent_framework .observability import OtelAttr , _get_response_attributes
2162+
2163+ response = Mock ()
2164+ response .response_id = None
2165+ response .finish_reason = None
2166+ response .raw_representation = None
2167+ response .usage_details = {
2168+ "input_token_count" : 0 ,
2169+ "output_token_count" : 50 ,
2170+ "cache_creation_input_token_count" : 10 ,
2171+ "cache_read_input_token_count" : 0 ,
2172+ "reasoning_output_token_count" : 30 ,
2173+ }
2174+
2175+ attrs = {}
2176+ result = _get_response_attributes (attrs , response )
2177+
2178+ assert result [OtelAttr .INPUT_TOKENS ] == 0
2179+ assert result [OtelAttr .OUTPUT_TOKENS ] == 50
2180+ assert result [OtelAttr .CACHE_CREATION_INPUT_TOKENS ] == 10
2181+ assert result [OtelAttr .CACHE_READ_INPUT_TOKENS ] == 0
2182+ assert result [OtelAttr .REASONING_OUTPUT_TOKENS ] == 30
2183+
2184+
2185+ def test_get_response_attributes_maps_legacy_usage_keys ():
2186+ """Test _get_response_attributes maps legacy provider usage keys to standard OTel attributes."""
2187+ from unittest .mock import Mock
2188+
2189+ from agent_framework .observability import OtelAttr , _get_response_attributes
2190+
2191+ response = Mock ()
2192+ response .response_id = None
2193+ response .finish_reason = None
2194+ response .raw_representation = None
2195+ response .usage_details = {
2196+ "anthropic.cache_creation_input_tokens" : 12 ,
2197+ "openai.cached_input_tokens" : 0 ,
2198+ "completion/reasoning_tokens" : 34 ,
2199+ }
2200+
2201+ attrs = {}
2202+ result = _get_response_attributes (attrs , response )
2203+
2204+ assert result [OtelAttr .CACHE_CREATION_INPUT_TOKENS ] == 12
2205+ assert result [OtelAttr .CACHE_READ_INPUT_TOKENS ] == 0
2206+ assert result [OtelAttr .REASONING_OUTPUT_TOKENS ] == 34
2207+
2208+
21572209def test_get_response_attributes_capture_usage_false ():
21582210 """Test _get_response_attributes skips usage when capture_usage is False."""
21592211 from unittest .mock import Mock
@@ -2164,13 +2216,22 @@ def test_get_response_attributes_capture_usage_false():
21642216 response .response_id = None
21652217 response .finish_reason = None
21662218 response .raw_representation = None
2167- response .usage_details = {"input_token_count" : 100 , "output_token_count" : 50 }
2219+ response .usage_details = {
2220+ "input_token_count" : 100 ,
2221+ "output_token_count" : 50 ,
2222+ "cache_creation_input_token_count" : 10 ,
2223+ "cache_read_input_token_count" : 20 ,
2224+ "reasoning_output_token_count" : 30 ,
2225+ }
21682226
21692227 attrs = {}
21702228 result = _get_response_attributes (attrs , response , capture_usage = False )
21712229
21722230 assert OtelAttr .INPUT_TOKENS not in result
21732231 assert OtelAttr .OUTPUT_TOKENS not in result
2232+ assert OtelAttr .CACHE_CREATION_INPUT_TOKENS not in result
2233+ assert OtelAttr .CACHE_READ_INPUT_TOKENS not in result
2234+ assert OtelAttr .REASONING_OUTPUT_TOKENS not in result
21742235
21752236
21762237def test_get_response_attributes_capture_response_id_false ():
@@ -2933,6 +2994,23 @@ def test_capture_response(span_exporter: InMemorySpanExporter):
29332994 assert spans [0 ].attributes .get (OtelAttr .OUTPUT_TOKENS ) == 50
29342995
29352996
2997+ def test_capture_response_records_zero_token_usage ():
2998+ """Test _capture_response records zero-valued token usage."""
2999+ from agent_framework .observability import OtelAttr , _capture_response
3000+
3001+ span = Mock ()
3002+ token_histogram = Mock ()
3003+ attrs = {
3004+ OtelAttr .INPUT_TOKENS : 0 ,
3005+ OtelAttr .OUTPUT_TOKENS : 0 ,
3006+ }
3007+
3008+ _capture_response (span = span , attributes = attrs , token_usage_histogram = token_histogram )
3009+
3010+ span .set_attributes .assert_called_once_with (attrs )
3011+ assert token_histogram .record .call_count == 2
3012+
3013+
29363014async def test_layer_ordering_span_sequence_with_function_calling (span_exporter : InMemorySpanExporter ):
29373015 """Test that with correct layer ordering, spans appear in the expected sequence.
29383016
@@ -3937,11 +4015,21 @@ class _InstrumentedAgent(AgentTelemetryLayer, RawAgent):
39374015 Content .from_function_call (call_id = "call_1" , name = "get_weather" , arguments = '{"city": "Seattle"}' )
39384016 ],
39394017 ),
3940- usage_details = UsageDetails (input_token_count = 2239 , output_token_count = 192 ),
4018+ usage_details = UsageDetails (
4019+ input_token_count = 2239 ,
4020+ output_token_count = 192 ,
4021+ cache_read_input_token_count = 100 ,
4022+ reasoning_output_token_count = 25 ,
4023+ ),
39414024 ),
39424025 ChatResponse (
39434026 messages = Message (role = "assistant" , contents = ["The weather in Seattle is sunny." ]),
3944- usage_details = UsageDetails (input_token_count = 2569 , output_token_count = 99 ),
4027+ usage_details = UsageDetails (
4028+ input_token_count = 2569 ,
4029+ output_token_count = 99 ,
4030+ cache_read_input_token_count = 200 ,
4031+ reasoning_output_token_count = 0 ,
4032+ ),
39454033 ),
39464034 ]
39474035
@@ -3965,12 +4053,18 @@ class _InstrumentedAgent(AgentTelemetryLayer, RawAgent):
39654053 # Individual chat spans retain their own usage
39664054 assert chat_spans [0 ].attributes .get (OtelAttr .INPUT_TOKENS ) == 2239
39674055 assert chat_spans [0 ].attributes .get (OtelAttr .OUTPUT_TOKENS ) == 192
4056+ assert chat_spans [0 ].attributes .get (OtelAttr .CACHE_READ_INPUT_TOKENS ) == 100
4057+ assert chat_spans [0 ].attributes .get (OtelAttr .REASONING_OUTPUT_TOKENS ) == 25
39684058 assert chat_spans [1 ].attributes .get (OtelAttr .INPUT_TOKENS ) == 2569
39694059 assert chat_spans [1 ].attributes .get (OtelAttr .OUTPUT_TOKENS ) == 99
4060+ assert chat_spans [1 ].attributes .get (OtelAttr .CACHE_READ_INPUT_TOKENS ) == 200
4061+ assert chat_spans [1 ].attributes .get (OtelAttr .REASONING_OUTPUT_TOKENS ) == 0
39704062
39714063 # The invoke_agent span must report the aggregate across all LLM round-trips
39724064 assert agent_span .attributes .get (OtelAttr .INPUT_TOKENS ) == 2239 + 2569
39734065 assert agent_span .attributes .get (OtelAttr .OUTPUT_TOKENS ) == 192 + 99
4066+ assert agent_span .attributes .get (OtelAttr .CACHE_READ_INPUT_TOKENS ) == 100 + 200
4067+ assert agent_span .attributes .get (OtelAttr .REASONING_OUTPUT_TOKENS ) == 25
39744068
39754069
39764070@pytest .mark .parametrize ("enable_sensitive_data" , [False ], indirect = True )
0 commit comments