1515 emit_message_events ,
1616)
1717from opentelemetry .instrumentation .google_generativeai .span_utils import (
18+ _collect_finish_reasons_from_response ,
19+ set_input_attributes ,
1820 set_input_attributes_sync ,
1921 set_model_request_attributes ,
2022 set_model_response_attributes ,
3234)
3335from opentelemetry .semconv_ai import (
3436 SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY ,
35- LLMRequestTypeValues ,
36- SpanAttributes ,
37- Meters
37+ Meters ,
3838)
3939from opentelemetry .metrics import Meter , get_meter
4040from opentelemetry .trace import SpanKind , get_tracer , StatusCode
4141from wrapt import wrap_function_wrapper
4242
43+ _GCP_GEN_AI = GenAIAttributes .GenAiProviderNameValues .GCP_GEN_AI .value
44+ _GEN_CONTENT = GenAIAttributes .GenAiOperationNameValues .GENERATE_CONTENT .value
45+
4346logger = logging .getLogger (__name__ )
4447
4548WRAPPED_METHODS = [
4649 {
4750 "package" : "google.genai.models" ,
4851 "object" : "Models" ,
4952 "method" : "generate_content" ,
50- "span_name" : "gemini.generate_content" ,
5153 },
5254 {
5355 "package" : "google.genai.models" ,
5456 "object" : "AsyncModels" ,
5557 "method" : "generate_content" ,
56- "span_name" : "gemini.generate_content" ,
5758 },
5859 {
5960 "package" : "google.genai.models" ,
6061 "object" : "Models" ,
6162 "method" : "generate_content_stream" ,
62- "span_name" : "gemini.generate_content" ,
6363 },
6464 {
6565 "package" : "google.genai.models" ,
6666 "object" : "AsyncModels" ,
6767 "method" : "generate_content_stream" ,
68- "span_name" : "gemini.generate_content" ,
6968 },
7069]
7170
@@ -85,43 +84,79 @@ def _build_from_streaming_response(
8584 event_logger ,
8685 token_histogram ,
8786):
88- complete_response = ""
87+ emit_events = should_emit_events () and event_logger
88+ text_parts = []
8989 last_chunk = None
9090 for item in response :
9191 item_to_yield = item
9292 last_chunk = item
93- complete_response += str (item .text )
93+ if not emit_events :
94+ t = getattr (item , "text" , None )
95+ if isinstance (t , str ):
96+ text_parts .append (t )
9497
9598 yield item_to_yield
9699
97- if should_emit_events () and event_logger :
100+ complete_response = "" .join (text_parts )
101+
102+ if emit_events :
98103 emit_choice_events (response , event_logger )
99104 else :
100- set_response_attributes (span , complete_response , llm_model )
105+ if last_chunk is not None and getattr (last_chunk , "candidates" , None ):
106+ set_response_attributes (span , last_chunk , llm_model )
107+ else :
108+ set_response_attributes (
109+ span , complete_response , llm_model , stream_last_chunk = last_chunk
110+ )
111+
112+ # Finish reasons from the final chunk — Gemini SDK aggregates candidates per chunk,
113+ # so the last chunk reflects all candidates without deduplication artifacts.
114+ stream_reasons = _collect_finish_reasons_from_response (last_chunk ) if last_chunk else None
101115 set_model_response_attributes (
102- span , last_chunk or response , llm_model , token_histogram
116+ span ,
117+ last_chunk or response ,
118+ llm_model ,
119+ token_histogram ,
120+ stream_finish_reasons = stream_reasons or None ,
103121 )
104122 span .end ()
105123
106124
107125async def _abuild_from_streaming_response (
108126 span , response : GenerateContentResponse , llm_model , event_logger , token_histogram
109127):
110- complete_response = ""
128+ emit_events = should_emit_events () and event_logger
129+ text_parts = []
111130 last_chunk = None
112131 async for item in response :
113132 item_to_yield = item
114133 last_chunk = item
115- complete_response += str (item .text )
134+ if not emit_events :
135+ t = getattr (item , "text" , None )
136+ if isinstance (t , str ):
137+ text_parts .append (t )
116138
117139 yield item_to_yield
118140
119- if should_emit_events () and event_logger :
141+ complete_response = "" .join (text_parts )
142+
143+ if emit_events :
120144 emit_choice_events (response , event_logger )
121145 else :
122- set_response_attributes (span , complete_response , llm_model )
146+ if last_chunk is not None and getattr (last_chunk , "candidates" , None ):
147+ set_response_attributes (span , last_chunk , llm_model )
148+ else :
149+ set_response_attributes (
150+ span , complete_response , llm_model , stream_last_chunk = last_chunk
151+ )
152+
153+ stream_reasons = _collect_finish_reasons_from_response (last_chunk ) if last_chunk else None
123154 set_model_response_attributes (
124- span , last_chunk if last_chunk else response , llm_model , token_histogram
155+ span ,
156+ last_chunk if last_chunk else response ,
157+ llm_model ,
158+ token_histogram ,
159+ stream_finish_reasons = stream_reasons or None ,
125160 )
126161 span .end ()
127162
@@ -136,6 +171,16 @@ def _handle_request(span, args, kwargs, llm_model, event_logger):
136171 set_model_request_attributes (span , kwargs , llm_model )
137172
138173
174+ @dont_throw
175+ async def _handle_request_async (span , args , kwargs , llm_model , event_logger ):
176+ if should_emit_events () and event_logger :
177+ emit_message_events (args , kwargs , event_logger )
178+ else :
179+ await set_input_attributes (span , args , kwargs , llm_model )
180+
181+ set_model_request_attributes (span , kwargs , llm_model )
182+
183+
139184@dont_throw
140185def _handle_response (span , response , llm_model , event_logger , token_histogram ):
141186 if should_emit_events () and event_logger :
@@ -200,17 +245,17 @@ async def _awrap(
200245 if "model" in kwargs :
201246 llm_model = kwargs ["model" ].replace ("models/" , "" )
202247
203- name = to_wrap .get ("span_name" )
204248 span = tracer .start_span (
205- name ,
249+ f" { _GEN_CONTENT } { llm_model } " ,
206250 kind = SpanKind .CLIENT ,
207251 attributes = {
208- GenAIAttributes .GEN_AI_SYSTEM : "Google" ,
209- SpanAttributes .LLM_REQUEST_TYPE : LLMRequestTypeValues .COMPLETION .value ,
252+ GenAIAttributes .GEN_AI_PROVIDER_NAME : _GCP_GEN_AI ,
253+ GenAIAttributes .GEN_AI_OPERATION_NAME : _GEN_CONTENT ,
254+ GenAIAttributes .GEN_AI_REQUEST_MODEL : llm_model ,
210255 },
211256 )
212257 start_time = time .perf_counter ()
213- _handle_request (span , args , kwargs , llm_model , event_logger )
258+ await _handle_request_async (span , args , kwargs , llm_model , event_logger )
214259 try :
215260 response = await wrapped (* args , ** kwargs )
216261 except Exception as e :
@@ -224,7 +269,9 @@ async def _awrap(
224269 duration_histogram .record (
225270 duration ,
226271 attributes = {
227- GenAIAttributes .GEN_AI_PROVIDER_NAME : "Google" ,
272+ GenAIAttributes .GEN_AI_PROVIDER_NAME : _GCP_GEN_AI ,
273+ GenAIAttributes .GEN_AI_OPERATION_NAME : _GEN_CONTENT ,
274+ GenAIAttributes .GEN_AI_REQUEST_MODEL : llm_model ,
228275 GenAIAttributes .GEN_AI_RESPONSE_MODEL : llm_model ,
229276 },
230277 )
@@ -276,13 +323,13 @@ def _wrap(
276323 if "model" in kwargs :
277324 llm_model = kwargs ["model" ].replace ("models/" , "" )
278325
279- name = to_wrap .get ("span_name" )
280326 span = tracer .start_span (
281- name ,
327+ f" { _GEN_CONTENT } { llm_model } " ,
282328 kind = SpanKind .CLIENT ,
283329 attributes = {
284- GenAIAttributes .GEN_AI_SYSTEM : "Google" ,
285- SpanAttributes .LLM_REQUEST_TYPE : LLMRequestTypeValues .COMPLETION .value ,
330+ GenAIAttributes .GEN_AI_PROVIDER_NAME : _GCP_GEN_AI ,
331+ GenAIAttributes .GEN_AI_OPERATION_NAME : _GEN_CONTENT ,
332+ GenAIAttributes .GEN_AI_REQUEST_MODEL : llm_model ,
286333 },
287334 )
288335
@@ -301,7 +348,9 @@ def _wrap(
301348 duration_histogram .record (
302349 duration ,
303350 attributes = {
304- GenAIAttributes .GEN_AI_PROVIDER_NAME : "Google" ,
351+ GenAIAttributes .GEN_AI_PROVIDER_NAME : _GCP_GEN_AI ,
352+ GenAIAttributes .GEN_AI_OPERATION_NAME : _GEN_CONTENT ,
353+ GenAIAttributes .GEN_AI_REQUEST_MODEL : llm_model ,
305354 GenAIAttributes .GEN_AI_RESPONSE_MODEL : llm_model ,
306355 },
307356 )
0 commit comments