Skip to content

Commit 3f8cb82

Browse files
committed
Commit changes
1 parent 29dfd56 commit 3f8cb82

20 files changed

+647
-318
lines changed

instrumentation-genai/opentelemetry-instrumentation-vertexai/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ classifiers = [
2525
]
2626
dependencies = [
2727
"opentelemetry-api ~= 1.28",
28-
"opentelemetry-instrumentation ~= 0.49b0",
29-
"opentelemetry-semantic-conventions ~= 0.49b0",
28+
"opentelemetry-instrumentation == 0.58b0dev",
29+
"opentelemetry-semantic-conventions == 0.58b0dev",
3030
]
3131

3232
[project.optional-dependencies]

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@
4848
)
4949

5050
from opentelemetry._events import get_event_logger
51+
from opentelemetry.instrumentation._semconv import (
52+
_OpenTelemetrySemanticConventionStability,
53+
_OpenTelemetryStabilitySignalType,
54+
)
5155
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5256
from opentelemetry.instrumentation.utils import unwrap
5357
from opentelemetry.instrumentation.vertexai.package import _instruments
@@ -118,9 +122,12 @@ def _instrument(self, **kwargs: Any):
118122
schema_url=Schemas.V1_28_0.value,
119123
event_logger_provider=event_logger_provider,
120124
)
125+
sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
126+
_OpenTelemetryStabilitySignalType.GEN_AI,
127+
)
121128

122129
method_wrappers = MethodWrappers(
123-
tracer, event_logger, is_content_enabled()
130+
tracer, event_logger, is_content_enabled(), sem_conv_opt_in_mode
124131
)
125132
for client_class, method_name, wrapper in _methods_to_wrap(
126133
method_wrappers

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/patch.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@
2424
)
2525

2626
from opentelemetry._events import EventLogger
27+
from opentelemetry.instrumentation._semconv import (
28+
_StabilityMode,
29+
)
2730
from opentelemetry.instrumentation.vertexai.utils import (
2831
GenerateContentParams,
32+
create_operation_details_event,
2933
get_genai_request_attributes,
3034
get_genai_response_attributes,
3135
get_server_attributes,
@@ -91,14 +95,72 @@ def _extract_params(
9195

9296
class MethodWrappers:
9397
def __init__(
94-
self, tracer: Tracer, event_logger: EventLogger, capture_content: bool
98+
self,
99+
tracer: Tracer,
100+
event_logger: EventLogger,
101+
capture_content: bool,
102+
sem_conv_opt_in_mode: _StabilityMode,
95103
) -> None:
96104
self.tracer = tracer
97105
self.event_logger = event_logger
98106
self.capture_content = capture_content
107+
self.sem_conv_opt_in_mode = sem_conv_opt_in_mode
108+
109+
# Deprecations:
110+
# - `gen_ai.system.message` event - use `gen_ai.system_instructions` or
111+
# `gen_ai.input.messages` attributes instead.
112+
# - `gen_ai.user.message`, `gen_ai.assistant.message`, `gen_ai.tool.message` events
113+
# (use `gen_ai.input.messages` attribute instead)
114+
# - `gen_ai.choice` event (use `gen_ai.output.messages` attribute instead)
115+
116+
@contextmanager
117+
def _with_new_instrumentation(
118+
self,
119+
instance: client.PredictionServiceClient
120+
| client_v1beta1.PredictionServiceClient,
121+
args: Any,
122+
kwargs: Any,
123+
):
124+
params = _extract_params(*args, **kwargs)
125+
api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType]
126+
span_attributes = {
127+
**get_genai_request_attributes(False, params),
128+
**get_server_attributes(api_endpoint),
129+
}
130+
131+
span_name = get_span_name(span_attributes)
132+
133+
with self.tracer.start_as_current_span(
134+
name=span_name,
135+
kind=SpanKind.CLIENT,
136+
attributes=span_attributes,
137+
) as span:
138+
139+
def handle_response(
140+
response: prediction_service.GenerateContentResponse
141+
| prediction_service_v1beta1.GenerateContentResponse,
142+
) -> None:
143+
if span.is_recording():
144+
# When streaming, this is called multiple times so attributes would be
145+
# overwritten. In practice, it looks the API only returns the interesting
146+
# attributes on the last streamed response. However, I couldn't find
147+
# documentation for this and setting attributes shouldn't be too expensive.
148+
span.set_attributes(
149+
get_genai_response_attributes(response)
150+
)
151+
self.event_logger.emit(
152+
create_operation_details_event(
153+
api_endpoint = api_endpoint,
154+
params=params,
155+
capture_content=self.capture_content,
156+
response=response,
157+
)
158+
)
159+
160+
yield handle_response
99161

100162
@contextmanager
101-
def _with_instrumentation(
163+
def _with_default_instrumentation(
102164
self,
103165
instance: client.PredictionServiceClient
104166
| client_v1beta1.PredictionServiceClient,
@@ -108,7 +170,7 @@ def _with_instrumentation(
108170
params = _extract_params(*args, **kwargs)
109171
api_endpoint: str = instance.api_endpoint # type: ignore[reportUnknownMemberType]
110172
span_attributes = {
111-
**get_genai_request_attributes(params),
173+
**get_genai_request_attributes(False, params),
112174
**get_server_attributes(api_endpoint),
113175
}
114176

@@ -162,9 +224,11 @@ def generate_content(
162224
prediction_service.GenerateContentResponse
163225
| prediction_service_v1beta1.GenerateContentResponse
164226
):
165-
with self._with_instrumentation(
166-
instance, args, kwargs
167-
) as handle_response:
227+
if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
228+
_instrumentation = self._with_default_instrumentation
229+
else:
230+
_instrumentation = self._with_new_instrumentation
231+
with _instrumentation(instance, args, kwargs) as handle_response:
168232
response = wrapped(*args, **kwargs)
169233
handle_response(response)
170234
return response
@@ -186,9 +250,11 @@ async def agenerate_content(
186250
prediction_service.GenerateContentResponse
187251
| prediction_service_v1beta1.GenerateContentResponse
188252
):
189-
with self._with_instrumentation(
190-
instance, args, kwargs
191-
) as handle_response:
253+
if self.sem_conv_opt_in_mode == _StabilityMode.DEFAULT:
254+
_instrumentation = self._with_default_instrumentation
255+
else:
256+
_instrumentation = self._with_new_instrumentation
257+
with _instrumentation(instance, args, kwargs) as handle_response:
192258
response = await wrapped(*args, **kwargs)
193259
handle_response(response)
194260
return response

instrumentation-genai/opentelemetry-instrumentation-vertexai/src/opentelemetry/instrumentation/vertexai/utils.py

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,20 @@ def get_server_attributes(
104104

105105

106106
def get_genai_request_attributes(
107+
use_latest_semconvs: bool,
107108
params: GenerateContentParams,
108109
operation_name: GenAIAttributes.GenAiOperationNameValues = GenAIAttributes.GenAiOperationNameValues.CHAT,
109110
):
110111
model = _get_model_name(params.model)
111112
generation_config = params.generation_config
112113
attributes: dict[str, AttributeValue] = {
113114
GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name.value,
114-
GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.VERTEX_AI.value,
115115
GenAIAttributes.GEN_AI_REQUEST_MODEL: model,
116116
}
117+
if not use_latest_semconvs:
118+
attributes[GenAIAttributes.GEN_AI_SYSTEM] = (
119+
GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
120+
)
117121

118122
if not generation_config:
119123
return attributes
@@ -140,12 +144,10 @@ def get_genai_request_attributes(
140144
attributes[GenAIAttributes.GEN_AI_REQUEST_FREQUENCY_PENALTY] = (
141145
generation_config.frequency_penalty
142146
)
143-
# Uncomment once GEN_AI_REQUEST_SEED is released in 1.30
144-
# https://github.com/open-telemetry/semantic-conventions/pull/1710
145-
# if "seed" in generation_config:
146-
# attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] = (
147-
# generation_config.seed
148-
# )
147+
if "seed" in generation_config and use_latest_semconvs:
148+
attributes[GenAIAttributes.GEN_AI_REQUEST_SEED] = (
149+
generation_config.seed
150+
)
149151
if "stop_sequences" in generation_config:
150152
attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] = (
151153
generation_config.stop_sequences
@@ -256,6 +258,92 @@ def request_to_events(
256258
yield user_event(role=content.role, content=request_content)
257259

258260

261+
def create_operation_details_event(
262+
*,
263+
api_endpoint: str,
264+
response: prediction_service.GenerateContentResponse
265+
| prediction_service_v1beta1.GenerateContentResponse,
266+
params: GenerateContentParams,
267+
capture_content: bool,
268+
) -> Event:
269+
event = Event(name="gen_ai.client.inference.operation.details")
270+
attributes: dict[str, AnyValue] = {
271+
**get_genai_request_attributes(True, params),
272+
**get_server_attributes(api_endpoint),
273+
}
274+
event.attributes = attributes
275+
if not capture_content:
276+
return event
277+
278+
attributes["gen_ai.system_instructions"] = [
279+
{
280+
"type": "text",
281+
"content": "\n".join(
282+
part.text for part in params.system_instruction.parts
283+
),
284+
}
285+
]
286+
if params.contents:
287+
attributes["gen_ai.input.messages"] = [
288+
_convert_content_to_message(content) for content in params.contents
289+
]
290+
if response.candidates:
291+
attributes["gen_ai.output.messages"] = (
292+
_convert_response_to_output_messages(response)
293+
)
294+
return event
295+
296+
297+
def _convert_response_to_output_messages(
298+
response: prediction_service.GenerateContentResponse
299+
| prediction_service_v1beta1.GenerateContentResponse,
300+
) -> list:
301+
output_messages = []
302+
for candidate in response.candidates:
303+
message = _convert_content_to_message(candidate.content)
304+
message["finish_reason"] = _map_finish_reason(candidate.finish_reason)
305+
output_messages.append(message)
306+
return output_messages
307+
308+
309+
def _convert_content_to_message(content: content.Content) -> dict:
310+
message = {}
311+
message["role"] = content.role
312+
message["parts"] = []
313+
for part in content.parts:
314+
if "function_response" in part:
315+
part = part.function_response
316+
message["parts"].append(
317+
{
318+
"type": "tool_call_response",
319+
"id": part.id,
320+
"response": json_format.MessageToDict(part._pb.response),
321+
}
322+
)
323+
elif "function_call" in part:
324+
part = part.function_call
325+
message["parts"].append(
326+
{
327+
"type": "tool_call",
328+
"id": part.id,
329+
"name": part.name,
330+
# TODO: support partial_args/streaming here?
331+
"response": json_format.MessageToDict(
332+
part._pb.args,
333+
),
334+
}
335+
)
336+
elif "text" in part:
337+
message["parts"].append({"type": "text", "content": part.text})
338+
part = part.text
339+
else:
340+
message["parts"].append(
341+
type(part).to_dict(part, including_default_value_fields=False)
342+
)
343+
message["parts"][-1]["type"] = type(part)
344+
return message
345+
346+
259347
def response_to_events(
260348
*,
261349
response: prediction_service.GenerateContentResponse

instrumentation-genai/opentelemetry-instrumentation-vertexai/tests/cassettes/test_function_call_choice.yaml

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ interactions:
4949
User-Agent:
5050
- python-requests/2.32.3
5151
method: POST
52-
uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-1.5-flash-002:generateContent?%24alt=json%3Benum-encoding%3Dint
52+
uri: https://us-central1-aiplatform.googleapis.com/v1/projects/fake-project/locations/us-central1/publishers/google/models/gemini-2.5-pro:generateContent?%24alt=json%3Benum-encoding%3Dint
5353
response:
5454
body:
5555
string: |-
@@ -65,7 +65,8 @@ interactions:
6565
"args": {
6666
"location": "New Delhi"
6767
}
68-
}
68+
},
69+
"thoughtSignature": "CuUGAcu98PA0CgDJVjnZvciC3yQjuC6R/hJiEIzLX88DIJjiqSv2B+3ZhTR62dWOcR8xHdVOUjJB+LgQO/KTzyZ9FZBM5OdkXsXMC+KsrEsjnjDq0wSAtDe8zDhztTD2mmgFT7i0asHN60aoxdkgM3myRHyzmaNjs+F2Ua3wS9MiAqzYdKoPry5kzkL/KQBI3dpsL/JiiGKFgXQ7y+hL3epdjU4ySjVw/x5zKC/MyHHpQIhHwhzKYZR3I1TUR3b0ziBZJknQHFaxNckeDKfY3aXuawPqqqf+RrUd7MEQOE0nnJF/jl7zwhxevPglX9R//MTpzV8bZIBLKu84GYIM1h7x2iHN0t5arER3VDk0kgIG0FsOaMrv+BqRykSsQc/9K2gLdB+ouGSlLJLQ8amnyiuxQC25RHRPlGvJKJCq2wCl2pEAhd7HUoCbiN145pbdoYi+L4vigSIsob7NvGHW/RJiZd1lHXAVJUCmzmhyNMj3iYW3qvO5+H41q5EZjlUT8RNBwN9Lv4V71CRdQnw5oYfH1WncPoVdbtQwPqIzhqJ93JKLJGdSkkNrODdZlt96iOHDchD0PLLz/vGg90La+v+sAvwnKRX/SGXruySlTDKntua4UBFauFD7Cyv/VQvyNpqiP39jnTdjW3J42vKLEtpf/Ldagf1JUcZ/BXQUYamhzuEdRVBg3XB0kbXA/e59vSsqXHPDFurbWzv8q56Jxu/lE8H416iPlWuR25J8SqWahHO039YtOrjGW9+/C69bFxh9HwSJtmUkoX0P0cZmRuYkOl3mpl4sRz/GfebNu7+G4+q9O5SSnpmzxEGUNccRiiTiZK89YPj3vqnODydK5qYTyMaTkYGkInxqCoJbCvXuNnYArwnVuWGKMHHJfmlV9et24zipKdMfb/ZTfdSy9uni4mgMOqd6b4er08Qft1NKktxqmU87izBd5Vt6VSfYsJqW4lrRs4f+28TfFXl2znfKt16Pi+8AuyH+49zS/xkZS5J48rGg0xWvowPkT2vSz/nYGb5xlEDPDABDvfwwib+T32d7cj6dQgZHzQWnPjZgSlSfOYuU2QSZtO5nx8oS2jnF+sb2Ywst0ZMV+ECOsH5OSjQ31Nmd6/PIHGVtFI6veBK78Xon2iuoHNwwqYZj8+hAXRMTkyY="
6970
},
7071
{
7172
"functionCall": {
@@ -78,29 +79,31 @@ interactions:
7879
]
7980
},
8081
"finishReason": 1,
81-
"avgLogprobs": -0.00018152029952034354
82+
"avgLogprobs": -1.0146095752716064
8283
}
8384
],
8485
"usageMetadata": {
85-
"promptTokenCount": 72,
86+
"promptTokenCount": 74,
8687
"candidatesTokenCount": 16,
87-
"totalTokenCount": 88,
88+
"totalTokenCount": 290,
89+
"trafficType": 1,
8890
"promptTokensDetails": [
8991
{
9092
"modality": 1,
91-
"tokenCount": 72
93+
"tokenCount": 74
9294
}
9395
],
9496
"candidatesTokensDetails": [
9597
{
9698
"modality": 1,
9799
"tokenCount": 16
98100
}
99-
]
101+
],
102+
"thoughtsTokenCount": 200
100103
},
101-
"modelVersion": "gemini-1.5-flash-002",
102-
"createTime": "2025-02-06T04:26:30.610859Z",
103-
"responseId": "9jmkZ6ukJb382PgPrp7zsQw"
104+
"modelVersion": "gemini-2.5-pro",
105+
"createTime": "2025-08-19T14:55:25.379701Z",
106+
"responseId": "XZCkaLWWF8qm1dkP7rOm6AM"
104107
}
105108
headers:
106109
Content-Type:
@@ -112,7 +115,7 @@ interactions:
112115
- X-Origin
113116
- Referer
114117
content-length:
115-
- '1029'
118+
- '2273'
116119
status:
117120
code: 200
118121
message: OK

0 commit comments

Comments
 (0)