1- """Helpers for emitting GenAI metrics from LLM invocations."""
1+ """Helpers for emitting GenAI metrics from invocations."""
22
33from __future__ import annotations
44
1818 create_duration_histogram ,
1919 create_token_histogram ,
2020)
21- from opentelemetry .util .genai .types import LLMInvocation
21+ from opentelemetry .util .genai .types import GenAIInvocation , LLMInvocation
2222from opentelemetry .util .types import AttributeValue
2323
2424
@@ -29,57 +29,69 @@ def __init__(self, meter: Meter):
2929 self ._duration_histogram : Histogram = create_duration_histogram (meter )
3030 self ._token_histogram : Histogram = create_token_histogram (meter )
3131
32+ @staticmethod
33+ def _build_attributes (
34+ invocation : GenAIInvocation ,
35+ error_type : Optional [str ] = None ,
36+ ) -> Dict [str , AttributeValue ]:
37+ """Build metric attributes from an invocation."""
38+ attributes : Dict [str , AttributeValue ] = {}
39+
40+ # Set attributes using getattr for fields that may not exist on base class
41+ operation_name = getattr (invocation , "operation_name" , None )
42+ if operation_name :
43+ attributes [GenAI .GEN_AI_OPERATION_NAME ] = operation_name
44+
45+ request_model = getattr (invocation , "request_model" , None )
46+ if request_model :
47+ attributes [GenAI .GEN_AI_REQUEST_MODEL ] = request_model
48+
49+ provider = getattr (invocation , "provider" , None )
50+ if provider :
51+ attributes [GenAI .GEN_AI_PROVIDER_NAME ] = provider
52+
53+ response_model_name = getattr (invocation , "response_model_name" , None )
54+ if response_model_name :
55+ attributes [GenAI .GEN_AI_RESPONSE_MODEL ] = response_model_name
56+
57+ server_address = getattr (invocation , "server_address" , None )
58+ if server_address :
59+ attributes [server_attributes .SERVER_ADDRESS ] = server_address
60+
61+ server_port = getattr (invocation , "server_port" , None )
62+ if server_port is not None :
63+ attributes [server_attributes .SERVER_PORT ] = server_port
64+
65+ metric_attributes = getattr (invocation , "metric_attributes" , None )
66+ if metric_attributes :
67+ attributes .update (metric_attributes )
68+
69+ if error_type :
70+ attributes [error_attributes .ERROR_TYPE ] = error_type
71+
72+ return attributes
73+
3274 def record (
3375 self ,
3476 span : Optional [Span ],
35- invocation : LLMInvocation ,
77+ invocation : GenAIInvocation ,
3678 * ,
3779 error_type : Optional [str ] = None ,
3880 ) -> None :
39- """Record duration and token metrics for an invocation if possible."""
81+ """Record duration and token metrics for an invocation if possible.
82+
83+ For LLMInvocation: records duration and token (input/output) metrics.
84+ For EmbeddingInvocation: records duration only.
85+ """
4086
4187 # pylint: disable=too-many-branches
4288
4389 if span is None :
4490 return
4591
46- token_counts : list [tuple [int , str ]] = []
47- if invocation .input_tokens is not None :
48- token_counts .append (
49- (
50- invocation .input_tokens ,
51- GenAI .GenAiTokenTypeValues .INPUT .value ,
52- )
53- )
54- if invocation .output_tokens is not None :
55- token_counts .append (
56- (
57- invocation .output_tokens ,
58- GenAI .GenAiTokenTypeValues .OUTPUT .value ,
59- )
60- )
92+ attributes = self ._build_attributes (invocation , error_type )
6193
62- attributes : Dict [str , AttributeValue ] = {
63- GenAI .GEN_AI_OPERATION_NAME : GenAI .GenAiOperationNameValues .CHAT .value
64- }
65- if invocation .request_model :
66- attributes [GenAI .GEN_AI_REQUEST_MODEL ] = invocation .request_model
67- if invocation .provider :
68- attributes [GenAI .GEN_AI_PROVIDER_NAME ] = invocation .provider
69- if invocation .response_model_name :
70- attributes [GenAI .GEN_AI_RESPONSE_MODEL ] = (
71- invocation .response_model_name
72- )
73- if invocation .server_address :
74- attributes [server_attributes .SERVER_ADDRESS ] = (
75- invocation .server_address
76- )
77- if invocation .server_port is not None :
78- attributes [server_attributes .SERVER_PORT ] = invocation .server_port
79- if invocation .metric_attributes :
80- attributes .update (invocation .metric_attributes )
81-
82- # Calculate duration from span timing or invocation monotonic start
94+ # Calculate duration from invocation monotonic start
8395 duration_seconds : Optional [float ] = None
8496 if invocation .monotonic_start_s is not None :
8597 duration_seconds = max (
@@ -88,8 +100,6 @@ def record(
88100 )
89101
90102 span_context = set_span_in_context (span )
91- if error_type :
92- attributes [error_attributes .ERROR_TYPE ] = error_type
93103
94104 if duration_seconds is not None :
95105 self ._duration_histogram .record (
@@ -98,12 +108,31 @@ def record(
98108 context = span_context ,
99109 )
100110
101- for token_count , token_type in token_counts :
102- self ._token_histogram .record (
103- token_count ,
104- attributes = attributes | {GenAI .GEN_AI_TOKEN_TYPE : token_type },
105- context = span_context ,
106- )
111+ # Only record token metrics for LLMInvocation
112+ if isinstance (invocation , LLMInvocation ):
113+ token_counts : list [tuple [int , str ]] = []
114+ if invocation .input_tokens is not None :
115+ token_counts .append (
116+ (
117+ invocation .input_tokens ,
118+ GenAI .GenAiTokenTypeValues .INPUT .value ,
119+ )
120+ )
121+ if invocation .output_tokens is not None :
122+ token_counts .append (
123+ (
124+ invocation .output_tokens ,
125+ GenAI .GenAiTokenTypeValues .OUTPUT .value ,
126+ )
127+ )
128+
129+ for token_count , token_type in token_counts :
130+ self ._token_histogram .record (
131+ token_count ,
132+ attributes = attributes
133+ | {GenAI .GEN_AI_TOKEN_TYPE : token_type },
134+ context = span_context ,
135+ )
107136
108137
109138__all__ = ["InvocationMetricsRecorder" ]
0 commit comments