Skip to content

Commit a24c205

Browse files
committed
Fix bug where 'gen_ai.response.finish_reasons' was not being correctly populated.
1 parent 6587485 commit a24c205

File tree

2 files changed

+114
-0
lines changed

2 files changed

+114
-0
lines changed

instrumentation-genai/opentelemetry-instrumentation-google-genai/src/opentelemetry/instrumentation/google_genai/generate_content.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ def process_response(self, response: GenerateContentResponse):
252252
# need to be reflected back into the span attributes.
253253
#
254254
# See also: TODOS.md.
255+
self._update_finish_reasons(response)
255256
self._maybe_update_token_counts(response)
256257
self._maybe_update_error_type(response)
257258
self._maybe_log_response(response)
@@ -275,6 +276,16 @@ def finalize_processing(self):
275276
self._record_token_usage_metric()
276277
self._record_duration_metric()
277278

279+
def _update_finish_reasons(self, response):
280+
if not response.candidates:
281+
return
282+
for candidate in response.candidates:
283+
finish_reason = candidate.finish_reason
284+
if finish_reason is None:
285+
continue
286+
finish_reason_str = finish_reason.name.lower().removeprefix('finish_reason_')
287+
self._finish_reasons_set.add(finish_reason_str)
288+
278289
def _maybe_update_token_counts(self, response: GenerateContentResponse):
279290
input_tokens = _get_response_property(
280291
response, "usage_metadata.prompt_token_count"
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
from google.genai import types as genai_types
18+
from .base import TestCase
19+
20+
class FinishReasonsTestCase(TestCase):
21+
22+
def generate_and_get_span_finish_reasons(self):
23+
self.client.models.generate_content(
24+
model='gemini-2.5-flash-001',
25+
contents='Some prompt')
26+
span = self.otel.get_span_named("generate_content gemini-2.5-flash-001")
27+
assert span is not None
28+
assert "gen_ai.response.finish_reasons" in span.attributes
29+
return list(span.attributes["gen_ai.response.finish_reasons"])
30+
31+
def test_single_candidate_with_valid_reason(self):
32+
self.configure_valid_response(candidate=genai_types.Candidate(
33+
finish_reason=genai_types.FinishReason.STOP
34+
))
35+
self.assertEqual(
36+
self.generate_and_get_span_finish_reasons(),
37+
["stop"])
38+
39+
def test_single_candidate_with_safety_reason(self):
40+
self.configure_valid_response(candidate=genai_types.Candidate(
41+
finish_reason=genai_types.FinishReason.SAFETY
42+
))
43+
self.assertEqual(
44+
self.generate_and_get_span_finish_reasons(),
45+
["safety"])
46+
47+
def test_single_candidate_with_max_tokens_reason(self):
48+
self.configure_valid_response(candidate=genai_types.Candidate(
49+
finish_reason=genai_types.FinishReason.MAX_TOKENS
50+
))
51+
self.assertEqual(
52+
self.generate_and_get_span_finish_reasons(),
53+
["max_tokens"])
54+
55+
def test_single_candidate_with_no_reason(self):
56+
self.configure_valid_response(candidate=genai_types.Candidate(
57+
finish_reason=None
58+
))
59+
self.assertEqual(
60+
self.generate_and_get_span_finish_reasons(),
61+
[])
62+
63+
def test_single_candidate_with_unspecified_reason(self):
64+
self.configure_valid_response(candidate=genai_types.Candidate(
65+
finish_reason=genai_types.FinishReason.FINISH_REASON_UNSPECIFIED
66+
))
67+
self.assertEqual(
68+
self.generate_and_get_span_finish_reasons(),
69+
["unspecified"])
70+
71+
def test_multiple_candidates_with_valid_reasons(self):
72+
self.configure_valid_response(candidates=[
73+
genai_types.Candidate(finish_reason=genai_types.FinishReason.MAX_TOKENS),
74+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
75+
])
76+
self.assertEqual(
77+
self.generate_and_get_span_finish_reasons(),
78+
["max_tokens", "stop"])
79+
80+
def test_sorts_finish_reasons(self):
81+
self.configure_valid_response(candidates=[
82+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
83+
genai_types.Candidate(finish_reason=genai_types.FinishReason.MAX_TOKENS),
84+
genai_types.Candidate(finish_reason=genai_types.FinishReason.SAFETY),
85+
])
86+
self.assertEqual(
87+
self.generate_and_get_span_finish_reasons(),
88+
["max_tokens", "safety", "stop"])
89+
90+
def test_deduplicates_finish_reasons(self):
91+
self.configure_valid_response(candidates=[
92+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
93+
genai_types.Candidate(finish_reason=genai_types.FinishReason.MAX_TOKENS),
94+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
95+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
96+
genai_types.Candidate(finish_reason=genai_types.FinishReason.SAFETY),
97+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
98+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
99+
genai_types.Candidate(finish_reason=genai_types.FinishReason.STOP),
100+
])
101+
self.assertEqual(
102+
self.generate_and_get_span_finish_reasons(),
103+
["max_tokens", "safety", "stop"])

0 commit comments

Comments
 (0)