diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py index 0054c99d59..1404b74f8d 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/__init__.py @@ -29,6 +29,7 @@ count_prompt_tokens_from_request, dont_throw, error_metrics_attributes, + get_reasoning_tokens, run_async, set_span_attribute, shared_metrics_attributes, @@ -197,6 +198,7 @@ async def _aset_token_usage( token_histogram: Histogram = None, choice_counter: Counter = None, ): + """Record token usage from an asynchronous Anthropic response.""" import inspect # If we get a coroutine, await it @@ -296,6 +298,12 @@ async def _aset_token_usage( ) set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + set_span_attribute( + span, + SpanAttributes.GEN_AI_USAGE_REASONING_TOKENS, + get_reasoning_tokens(usage), + ) + set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cache_read_tokens ) @@ -316,6 +324,7 @@ def _set_token_usage( token_histogram: Histogram = None, choice_counter: Counter = None, ): + """Record token usage from a synchronous Anthropic response.""" import inspect # If we get a coroutine, we cannot process it in sync context @@ -411,7 +420,11 @@ def _set_token_usage( span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens ) set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) - + set_span_attribute( + span, + SpanAttributes.GEN_AI_USAGE_REASONING_TOKENS, + get_reasoning_tokens(usage), + ) set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cache_read_tokens ) diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py index bd2d6dc15d..346b66de0c 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/streaming.py @@ -15,6 +15,7 @@ count_prompt_tokens_from_request, dont_throw, error_metrics_attributes, + get_reasoning_tokens, set_span_attribute, shared_metrics_attributes, should_emit_events, @@ -82,6 +83,7 @@ def _set_token_usage( token_histogram: Histogram = None, choice_counter: Counter = None, ): + """Record token usage collected from a completed streaming response.""" cache_read_tokens = ( complete_response.get("usage", {}).get("cache_read_input_tokens", 0) or 0 ) @@ -98,6 +100,12 @@ def _set_token_usage( ) set_span_attribute(span, SpanAttributes.GEN_AI_USAGE_TOTAL_TOKENS, total_tokens) + set_span_attribute( + span, + SpanAttributes.GEN_AI_USAGE_REASONING_TOKENS, + get_reasoning_tokens(complete_response.get("usage")), + ) + set_span_attribute( span, GenAIAttributes.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, cache_read_tokens ) diff --git a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py index a060031017..ccb25306fa 100644 --- a/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py +++ b/packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/utils.py @@ -27,6 +27,21 @@ def set_span_attribute(span, name, value): return +def get_reasoning_tokens(usage): + """Return reasoning tokens from an Anthropic usage object or dictionary.""" + if not usage: + return None + + details = ( + usage.get("output_tokens_details") + if isinstance(usage, dict) + else getattr(usage, "output_tokens_details", None) + ) + if isinstance(details, dict): + return details.get("reasoning_tokens") + return getattr(details, "reasoning_tokens", None) + + def should_send_prompts(): return ( os.getenv(TRACELOOP_TRACE_CONTENT) or "true" diff --git a/packages/opentelemetry-instrumentation-anthropic/tests/test_thinking.py b/packages/opentelemetry-instrumentation-anthropic/tests/test_thinking.py index f54b96e06c..5bfe1915a9 100644 --- a/packages/opentelemetry-instrumentation-anthropic/tests/test_thinking.py +++ b/packages/opentelemetry-instrumentation-anthropic/tests/test_thinking.py @@ -1,5 +1,12 @@ import json +from types import SimpleNamespace +from unittest.mock import Mock + import pytest +from opentelemetry.instrumentation.anthropic import _set_token_usage +from opentelemetry.instrumentation.anthropic.streaming import ( + _set_token_usage as _set_stream_token_usage, +) from opentelemetry.sdk._logs import ReadableLogRecord from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, @@ -8,6 +15,46 @@ from .utils import verify_metrics +def test_reasoning_tokens_are_recorded_for_non_streaming_response(): + """Verify reasoning tokens are recorded for a regular response.""" + span = Mock() + response = SimpleNamespace( + usage=SimpleNamespace( + input_tokens=10, + output_tokens=20, + output_tokens_details=SimpleNamespace(reasoning_tokens=7), + ), + content=[], + completion=None, + stop_reason=None, + ) + + _set_token_usage(span, None, {}, response) + + span.set_attribute.assert_any_call( + GenAIAttributes.GEN_AI_USAGE_REASONING_TOKENS, 7 + ) + + +def test_reasoning_tokens_are_recorded_for_streaming_response(): + """Verify reasoning tokens are recorded for a streaming response.""" + span = Mock() + complete_response = { + "usage": { + "input_tokens": 10, + "output_tokens": 20, + "output_tokens_details": {"reasoning_tokens": 7}, + }, + "events": [], + } + + _set_stream_token_usage(span, complete_response, 10, 20) + + span.set_attribute.assert_any_call( + GenAIAttributes.GEN_AI_USAGE_REASONING_TOKENS, 7 + ) + + @pytest.mark.vcr def test_anthropic_thinking_legacy( instrument_legacy, anthropic_client, span_exporter, log_exporter, reader