diff --git a/agentops/instrumentation/common/token_counting.py b/agentops/instrumentation/common/token_counting.py index c467a78c9..7d2f19f07 100644 --- a/agentops/instrumentation/common/token_counting.py +++ b/agentops/instrumentation/common/token_counting.py @@ -23,25 +23,29 @@ class TokenUsage: reasoning_tokens: Optional[int] = None def to_attributes(self) -> Dict[str, int]: - """Convert to span attributes dictionary.""" + """Convert to span attributes dictionary. + + Only metrics greater than zero are included so that non‑LLM spans do + not contain empty token usage fields. + """ attributes = {} - if self.prompt_tokens is not None: + if self.prompt_tokens: attributes[SpanAttributes.LLM_USAGE_PROMPT_TOKENS] = self.prompt_tokens - if self.completion_tokens is not None: + if self.completion_tokens: attributes[SpanAttributes.LLM_USAGE_COMPLETION_TOKENS] = self.completion_tokens - if self.total_tokens is not None: + if self.total_tokens: attributes[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] = self.total_tokens - if self.cached_prompt_tokens is not None: + if self.cached_prompt_tokens: attributes[SpanAttributes.LLM_USAGE_CACHE_CREATION_INPUT_TOKENS] = self.cached_prompt_tokens - if self.cached_read_tokens is not None: + if self.cached_read_tokens: attributes[SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS] = self.cached_read_tokens - if self.reasoning_tokens is not None: + if self.reasoning_tokens: attributes[SpanAttributes.LLM_USAGE_REASONING_TOKENS] = self.reasoning_tokens return attributes diff --git a/agentops/sdk/attributes.py b/agentops/sdk/attributes.py index bf77db7ce..a4e4b5697 100644 --- a/agentops/sdk/attributes.py +++ b/agentops/sdk/attributes.py @@ -60,8 +60,8 @@ def get_global_resource_attributes( """ Get all global resource attributes for telemetry. - Combines service metadata, system information, and imported libraries - into a complete resource attributes dictionary. + Combines service metadata and imported libraries into a complete + resource attributes dictionary. Args: service_name: Name of the service @@ -73,7 +73,6 @@ def get_global_resource_attributes( # Start with service attributes attributes: dict[str, Any] = { ResourceAttributes.SERVICE_NAME: service_name, - **get_system_resource_attributes(), } if project_id: diff --git a/agentops/sdk/core.py b/agentops/sdk/core.py index f4c09f23e..3e165c1b0 100644 --- a/agentops/sdk/core.py +++ b/agentops/sdk/core.py @@ -23,6 +23,7 @@ get_trace_attributes, get_span_attributes, get_session_end_attributes, + get_system_resource_attributes, ) from agentops.semconv import SpanKind from agentops.helpers.dashboard import log_trace_url @@ -354,6 +355,9 @@ def start_trace( # Build trace attributes attributes = get_trace_attributes(tags=tags) + # Include system metadata only for the default session trace + if trace_name == "session": + attributes.update(get_system_resource_attributes()) # make_span creates and starts the span, and activates it in the current context # It returns: span, context_object, context_token diff --git a/tests/unit/instrumentation/common/test_token_counting.py b/tests/unit/instrumentation/common/test_token_counting.py new file mode 100644 index 000000000..2f00561b9 --- /dev/null +++ b/tests/unit/instrumentation/common/test_token_counting.py @@ -0,0 +1,26 @@ +from agentops.instrumentation.common.token_counting import TokenUsage +from agentops.semconv import SpanAttributes + + +class TestTokenUsageToAttributes: + def test_skips_zero_values(self): + usage = TokenUsage( + prompt_tokens=0, + completion_tokens=0, + total_tokens=0, + cached_prompt_tokens=0, + cached_read_tokens=0, + reasoning_tokens=0, + ) + + attrs = usage.to_attributes() + assert attrs == {} + + def test_includes_positive_values_only(self): + usage = TokenUsage(prompt_tokens=5, completion_tokens=0, total_tokens=5) + attrs = usage.to_attributes() + assert SpanAttributes.LLM_USAGE_PROMPT_TOKENS in attrs + assert attrs[SpanAttributes.LLM_USAGE_PROMPT_TOKENS] == 5 + assert SpanAttributes.LLM_USAGE_COMPLETION_TOKENS not in attrs + assert SpanAttributes.LLM_USAGE_TOTAL_TOKENS in attrs + assert attrs[SpanAttributes.LLM_USAGE_TOTAL_TOKENS] == 5 diff --git a/tests/unit/sdk/test_resource_attributes.py b/tests/unit/sdk/test_resource_attributes.py new file mode 100644 index 000000000..f406ecb91 --- /dev/null +++ b/tests/unit/sdk/test_resource_attributes.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + + +from agentops.sdk.core import tracer +from agentops.sdk.attributes import get_global_resource_attributes +from agentops.semconv.resource import ResourceAttributes + + +@patch("agentops.sdk.attributes.get_imported_libraries", return_value=["agentops"]) +def test_global_resource_attributes_no_system(mock_libs): + attrs = get_global_resource_attributes("svc", project_id="proj") + assert attrs[ResourceAttributes.SERVICE_NAME] == "svc" + assert attrs[ResourceAttributes.PROJECT_ID] == "proj" + assert ResourceAttributes.IMPORTED_LIBRARIES in attrs + assert ResourceAttributes.HOST_MACHINE not in attrs + assert ResourceAttributes.CPU_COUNT not in attrs + + +@patch("agentops.sdk.core.get_system_resource_attributes") +def test_system_metadata_only_for_session(mock_sys_attrs, instrumentation): + mock_sys_attrs.return_value = {ResourceAttributes.HOST_MACHINE: "test"} + + ctx = tracer.start_trace("session") + tracer.end_trace(ctx, end_state="Success") + spans = instrumentation.get_finished_spans() + assert len(spans) == 1 + assert spans[0].attributes.get(ResourceAttributes.HOST_MACHINE) == "test" + + instrumentation.clear_spans() + + ctx = tracer.start_trace("custom") + tracer.end_trace(ctx, end_state="Success") + spans = instrumentation.get_finished_spans() + assert len(spans) == 1 + assert ResourceAttributes.HOST_MACHINE not in spans[0].attributes