opentelemetry-instrumentation-vertexai 0.40.13__tar.gz → 0.41.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentelemetry-instrumentation-vertexai might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opentelemetry-instrumentation-vertexai
3
- Version: 0.40.13
3
+ Version: 0.41.0
4
4
  Summary: OpenTelemetry Vertex AI instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -17,7 +17,7 @@ Provides-Extra: instruments
17
17
  Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
18
18
  Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
19
19
  Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
20
- Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.9)
20
+ Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.10)
21
21
  Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-vertexai
22
22
  Description-Content-Type: text/markdown
23
23
 
@@ -1,26 +1,34 @@
1
1
  """OpenTelemetry Vertex AI instrumentation"""
2
2
 
3
3
  import logging
4
- import os
5
4
  import types
6
5
  from typing import Collection
7
- from opentelemetry.instrumentation.vertexai.config import Config
8
- from opentelemetry.instrumentation.vertexai.utils import dont_throw
9
- from wrapt import wrap_function_wrapper
10
6
 
11
7
  from opentelemetry import context as context_api
12
- from opentelemetry.trace import get_tracer, SpanKind
13
- from opentelemetry.trace.status import Status, StatusCode
14
-
8
+ from opentelemetry._events import get_event_logger
15
9
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
16
10
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
17
-
11
+ from opentelemetry.instrumentation.vertexai.config import Config
12
+ from opentelemetry.instrumentation.vertexai.event_emitter import (
13
+ emit_prompt_events,
14
+ emit_response_events,
15
+ )
16
+ from opentelemetry.instrumentation.vertexai.span_utils import (
17
+ set_input_attributes,
18
+ set_model_input_attributes,
19
+ set_model_response_attributes,
20
+ set_response_attributes,
21
+ )
22
+ from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_emit_events
23
+ from opentelemetry.instrumentation.vertexai.version import __version__
18
24
  from opentelemetry.semconv_ai import (
19
25
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
20
- SpanAttributes,
21
26
  LLMRequestTypeValues,
27
+ SpanAttributes,
22
28
  )
23
- from opentelemetry.instrumentation.vertexai.version import __version__
29
+ from opentelemetry.trace import SpanKind, get_tracer
30
+ from opentelemetry.trace.status import Status, StatusCode
31
+ from wrapt import wrap_function_wrapper
24
32
 
25
33
  logger = logging.getLogger(__name__)
26
34
 
@@ -114,12 +122,6 @@ WRAPPED_METHODS = [
114
122
  ]
115
123
 
116
124
 
117
- def should_send_prompts():
118
- return (
119
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
120
- ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
121
-
122
-
123
125
  def is_streaming_response(response):
124
126
  return isinstance(response, types.GeneratorType)
125
127
 
@@ -128,81 +130,18 @@ def is_async_streaming_response(response):
128
130
  return isinstance(response, types.AsyncGeneratorType)
129
131
 
130
132
 
131
- def _set_span_attribute(span, name, value):
132
- if value is not None:
133
- if value != "":
134
- span.set_attribute(name, value)
135
- return
136
-
137
-
138
- def _set_input_attributes(span, args, kwargs, llm_model):
139
- if should_send_prompts() and args is not None and len(args) > 0:
140
- prompt = ""
141
- for arg in args:
142
- if isinstance(arg, str):
143
- prompt = f"{prompt}{arg}\n"
144
- elif isinstance(arg, list):
145
- for subarg in arg:
146
- prompt = f"{prompt}{subarg}\n"
147
-
148
- _set_span_attribute(
149
- span,
150
- f"{SpanAttributes.LLM_PROMPTS}.0.user",
151
- prompt,
152
- )
153
-
154
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
155
- _set_span_attribute(
156
- span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
157
- )
158
- _set_span_attribute(
159
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
160
- )
161
- _set_span_attribute(
162
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
163
- )
164
- _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
165
- _set_span_attribute(span, SpanAttributes.LLM_TOP_K, kwargs.get("top_k"))
166
- _set_span_attribute(
167
- span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
168
- )
169
- _set_span_attribute(
170
- span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
171
- )
172
-
173
- return
174
-
175
-
176
133
  @dont_throw
177
- def _set_response_attributes(span, llm_model, generation_text, token_usage):
178
- _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, llm_model)
179
-
180
- if token_usage:
181
- _set_span_attribute(
182
- span,
183
- SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
184
- token_usage.total_token_count,
185
- )
186
- _set_span_attribute(
187
- span,
188
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
189
- token_usage.candidates_token_count,
190
- )
191
- _set_span_attribute(
192
- span,
193
- SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
194
- token_usage.prompt_token_count,
195
- )
196
-
197
- _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
198
- _set_span_attribute(
199
- span,
200
- f"{SpanAttributes.LLM_COMPLETIONS}.0.content",
201
- generation_text,
202
- )
134
+ def handle_streaming_response(span, event_logger, llm_model, response, token_usage):
135
+ set_model_response_attributes(span, llm_model, token_usage)
136
+ if should_emit_events():
137
+ emit_response_events(response, event_logger)
138
+ else:
139
+ set_response_attributes(span, llm_model, response)
140
+ if span.is_recording():
141
+ span.set_status(Status(StatusCode.OK))
203
142
 
204
143
 
205
- def _build_from_streaming_response(span, response, llm_model):
144
+ def _build_from_streaming_response(span, event_logger, response, llm_model):
206
145
  complete_response = ""
207
146
  token_usage = None
208
147
  for item in response:
@@ -213,13 +152,15 @@ def _build_from_streaming_response(span, response, llm_model):
213
152
 
214
153
  yield item_to_yield
215
154
 
216
- _set_response_attributes(span, llm_model, complete_response, token_usage)
155
+ handle_streaming_response(
156
+ span, event_logger, llm_model, complete_response, token_usage
157
+ )
217
158
 
218
159
  span.set_status(Status(StatusCode.OK))
219
160
  span.end()
220
161
 
221
162
 
222
- async def _abuild_from_streaming_response(span, response, llm_model):
163
+ async def _abuild_from_streaming_response(span, event_logger, response, llm_model):
223
164
  complete_response = ""
224
165
  token_usage = None
225
166
  async for item in response:
@@ -230,34 +171,39 @@ async def _abuild_from_streaming_response(span, response, llm_model):
230
171
 
231
172
  yield item_to_yield
232
173
 
233
- _set_response_attributes(span, llm_model, complete_response, token_usage)
174
+ handle_streaming_response(span, event_logger, llm_model, response, token_usage)
234
175
 
235
176
  span.set_status(Status(StatusCode.OK))
236
177
  span.end()
237
178
 
238
179
 
239
180
  @dont_throw
240
- def _handle_request(span, args, kwargs, llm_model):
241
- if span.is_recording():
242
- _set_input_attributes(span, args, kwargs, llm_model)
243
-
244
-
245
- @dont_throw
246
- def _handle_response(span, response, llm_model):
247
- if span.is_recording():
248
- _set_response_attributes(
249
- span, llm_model, response.candidates[0].text, response.usage_metadata
181
+ def _handle_request(span, event_logger, args, kwargs, llm_model):
182
+ set_model_input_attributes(span, kwargs, llm_model)
183
+ if should_emit_events():
184
+ emit_prompt_events(args, event_logger)
185
+ else:
186
+ set_input_attributes(span, args)
187
+
188
+
189
+ def _handle_response(span, event_logger, response, llm_model):
190
+ set_model_response_attributes(span, llm_model, response.usage_metadata)
191
+ if should_emit_events():
192
+ emit_response_events(response, event_logger)
193
+ else:
194
+ set_response_attributes(
195
+ span, llm_model, response.candidates[0].text if response.candidates else ""
250
196
  )
251
-
197
+ if span.is_recording():
252
198
  span.set_status(Status(StatusCode.OK))
253
199
 
254
200
 
255
201
  def _with_tracer_wrapper(func):
256
202
  """Helper for providing tracer for wrapper functions."""
257
203
 
258
- def _with_tracer(tracer, to_wrap):
204
+ def _with_tracer(tracer, event_logger, to_wrap):
259
205
  def wrapper(wrapped, instance, args, kwargs):
260
- return func(tracer, to_wrap, wrapped, instance, args, kwargs)
206
+ return func(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs)
261
207
 
262
208
  return wrapper
263
209
 
@@ -265,7 +211,7 @@ def _with_tracer_wrapper(func):
265
211
 
266
212
 
267
213
  @_with_tracer_wrapper
268
- async def _awrap(tracer, to_wrap, wrapped, instance, args, kwargs):
214
+ async def _awrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
269
215
  """Instruments and calls every function defined in TO_WRAP."""
270
216
  if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
271
217
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
@@ -283,29 +229,33 @@ async def _awrap(tracer, to_wrap, wrapped, instance, args, kwargs):
283
229
  name,
284
230
  kind=SpanKind.CLIENT,
285
231
  attributes={
286
- SpanAttributes.LLM_SYSTEM: "VertexAI",
232
+ SpanAttributes.LLM_SYSTEM: "Google",
287
233
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
288
234
  },
289
235
  )
290
236
 
291
- _handle_request(span, args, kwargs, llm_model)
237
+ _handle_request(span, event_logger, args, kwargs, llm_model)
292
238
 
293
239
  response = await wrapped(*args, **kwargs)
294
240
 
295
241
  if response:
296
242
  if is_streaming_response(response):
297
- return _build_from_streaming_response(span, response, llm_model)
243
+ return _build_from_streaming_response(
244
+ span, event_logger, response, llm_model
245
+ )
298
246
  elif is_async_streaming_response(response):
299
- return _abuild_from_streaming_response(span, response, llm_model)
247
+ return _abuild_from_streaming_response(
248
+ span, event_logger, response, llm_model
249
+ )
300
250
  else:
301
- _handle_response(span, response, llm_model)
251
+ _handle_response(span, event_logger, response, llm_model)
302
252
 
303
253
  span.end()
304
254
  return response
305
255
 
306
256
 
307
257
  @_with_tracer_wrapper
308
- def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
258
+ def _wrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
309
259
  """Instruments and calls every function defined in TO_WRAP."""
310
260
  if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY) or context_api.get_value(
311
261
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY
@@ -323,22 +273,26 @@ def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
323
273
  name,
324
274
  kind=SpanKind.CLIENT,
325
275
  attributes={
326
- SpanAttributes.LLM_SYSTEM: "VertexAI",
276
+ SpanAttributes.LLM_SYSTEM: "Google",
327
277
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
328
278
  },
329
279
  )
330
280
 
331
- _handle_request(span, args, kwargs, llm_model)
281
+ _handle_request(span, event_logger, args, kwargs, llm_model)
332
282
 
333
283
  response = wrapped(*args, **kwargs)
334
284
 
335
285
  if response:
336
286
  if is_streaming_response(response):
337
- return _build_from_streaming_response(span, response, llm_model)
287
+ return _build_from_streaming_response(
288
+ span, event_logger, response, llm_model
289
+ )
338
290
  elif is_async_streaming_response(response):
339
- return _abuild_from_streaming_response(span, response, llm_model)
291
+ return _abuild_from_streaming_response(
292
+ span, event_logger, response, llm_model
293
+ )
340
294
  else:
341
- _handle_response(span, response, llm_model)
295
+ _handle_response(span, event_logger, response, llm_model)
342
296
 
343
297
  span.end()
344
298
  return response
@@ -347,9 +301,10 @@ def _wrap(tracer, to_wrap, wrapped, instance, args, kwargs):
347
301
  class VertexAIInstrumentor(BaseInstrumentor):
348
302
  """An instrumentor for VertextAI's client library."""
349
303
 
350
- def __init__(self, exception_logger=None):
304
+ def __init__(self, exception_logger=None, use_legacy_attributes=True):
351
305
  super().__init__()
352
306
  Config.exception_logger = exception_logger
307
+ Config.use_legacy_attributes = use_legacy_attributes
353
308
 
354
309
  def instrumentation_dependencies(self) -> Collection[str]:
355
310
  return _instruments
@@ -357,6 +312,17 @@ class VertexAIInstrumentor(BaseInstrumentor):
357
312
  def _instrument(self, **kwargs):
358
313
  tracer_provider = kwargs.get("tracer_provider")
359
314
  tracer = get_tracer(__name__, __version__, tracer_provider)
315
+
316
+ event_logger = None
317
+
318
+ if should_emit_events():
319
+ event_logger_provider = kwargs.get("event_logger_provider")
320
+ event_logger = get_event_logger(
321
+ __name__,
322
+ __version__,
323
+ event_logger_provider=event_logger_provider,
324
+ )
325
+
360
326
  for wrapped_method in WRAPPED_METHODS:
361
327
  wrap_package = wrapped_method.get("package")
362
328
  wrap_object = wrapped_method.get("object")
@@ -366,9 +332,9 @@ class VertexAIInstrumentor(BaseInstrumentor):
366
332
  wrap_package,
367
333
  f"{wrap_object}.{wrap_method}",
368
334
  (
369
- _awrap(tracer, wrapped_method)
335
+ _awrap(tracer, event_logger, wrapped_method)
370
336
  if wrapped_method.get("is_async")
371
- else _wrap(tracer, wrapped_method)
337
+ else _wrap(tracer, event_logger, wrapped_method)
372
338
  ),
373
339
  )
374
340
 
@@ -0,0 +1,164 @@
1
+ from dataclasses import asdict
2
+ from enum import Enum
3
+ from typing import Union
4
+
5
+ from opentelemetry._events import Event
6
+ from opentelemetry.instrumentation.vertexai.event_models import (
7
+ ChoiceEvent,
8
+ MessageEvent,
9
+ )
10
+ from opentelemetry.instrumentation.vertexai.utils import (
11
+ dont_throw,
12
+ should_emit_events,
13
+ should_send_prompts,
14
+ )
15
+ from opentelemetry.semconv._incubating.attributes import (
16
+ gen_ai_attributes as GenAIAttributes,
17
+ )
18
+
19
+ from vertexai.generative_models import GenerationResponse
20
+
21
+
22
+ class Roles(Enum):
23
+ USER = "user"
24
+ ASSISTANT = "assistant"
25
+ SYSTEM = "system"
26
+ TOOL = "tool"
27
+
28
+
29
+ VALID_MESSAGE_ROLES = {role.value for role in Roles}
30
+ """The valid roles for naming the message event."""
31
+
32
+ EVENT_ATTRIBUTES = {
33
+ GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.VERTEX_AI.value
34
+ }
35
+ """The attributes to be used for the event."""
36
+
37
+
38
+ def _parse_vertex_finish_reason(reason):
39
+ if reason is None:
40
+ return "unknown"
41
+
42
+ finish_reason_map = {
43
+ 0: "unspecified",
44
+ 1: "stop",
45
+ 2: "max_tokens",
46
+ 3: "safety",
47
+ 4: "recitation",
48
+ 5: "other",
49
+ 6: "blocklist",
50
+ 7: "prohibited_content",
51
+ 8: "spii",
52
+ 9: "malformed_function_call",
53
+ }
54
+
55
+ if hasattr(reason, "value"):
56
+ reason_value = reason.value
57
+ else:
58
+ reason_value = reason
59
+
60
+ return finish_reason_map.get(reason_value, "unknown")
61
+
62
+
63
+ @dont_throw
64
+ def emit_prompt_events(args, event_logger):
65
+ prompt = ""
66
+ if args is not None and len(args) > 0:
67
+ for arg in args:
68
+ if isinstance(arg, str):
69
+ prompt = f"{prompt}{arg}\n"
70
+ elif isinstance(arg, list):
71
+ for subarg in arg:
72
+ prompt = f"{prompt}{subarg}\n"
73
+ emit_event(MessageEvent(content=prompt, role=Roles.USER.value), event_logger)
74
+
75
+
76
+ def emit_response_events(response, event_logger):
77
+ if isinstance(response, str):
78
+ emit_event(
79
+ ChoiceEvent(
80
+ index=0,
81
+ message={"content": response, "role": Roles.ASSISTANT.value},
82
+ finish_reason="unknown",
83
+ ),
84
+ event_logger,
85
+ )
86
+ elif isinstance(response, GenerationResponse):
87
+ for candidate in response.candidates:
88
+ emit_event(
89
+ ChoiceEvent(
90
+ index=candidate.index,
91
+ message={
92
+ "content": candidate.text,
93
+ "role": Roles.ASSISTANT.value,
94
+ },
95
+ finish_reason=_parse_vertex_finish_reason(candidate.finish_reason),
96
+ ),
97
+ event_logger,
98
+ )
99
+
100
+
101
+ def emit_event(event: Union[MessageEvent, ChoiceEvent], event_logger) -> None:
102
+ """
103
+ Emit an event to the OpenTelemetry SDK.
104
+
105
+ Args:
106
+ event: The event to emit.
107
+ """
108
+ if not should_emit_events() or event_logger is None:
109
+ return
110
+
111
+ if isinstance(event, MessageEvent):
112
+ _emit_message_event(event, event_logger)
113
+ elif isinstance(event, ChoiceEvent):
114
+ _emit_choice_event(event, event_logger)
115
+ else:
116
+ raise TypeError("Unsupported event type")
117
+
118
+
119
+ def _emit_message_event(event: MessageEvent, event_logger) -> None:
120
+ body = asdict(event)
121
+
122
+ if event.role in VALID_MESSAGE_ROLES:
123
+ name = "gen_ai.{}.message".format(event.role)
124
+ # According to the semantic conventions, the role is conditionally required if available
125
+ # and not equal to the "role" in the message name. So, remove the role from the body if
126
+ # it is the same as the in the event name.
127
+ body.pop("role", None)
128
+ else:
129
+ name = "gen_ai.user.message"
130
+
131
+ # According to the semantic conventions, only the assistant role has tool call
132
+ if event.role != Roles.ASSISTANT.value and event.tool_calls is not None:
133
+ del body["tool_calls"]
134
+ elif event.tool_calls is None:
135
+ del body["tool_calls"]
136
+
137
+ if not should_send_prompts():
138
+ del body["content"]
139
+ if body.get("tool_calls") is not None:
140
+ for tool_call in body["tool_calls"]:
141
+ tool_call["function"].pop("arguments", None)
142
+
143
+ event_logger.emit(Event(name=name, body=body, attributes=EVENT_ATTRIBUTES))
144
+
145
+
146
+ def _emit_choice_event(event: ChoiceEvent, event_logger) -> None:
147
+ body = asdict(event)
148
+ if event.message["role"] == Roles.ASSISTANT.value:
149
+ # According to the semantic conventions, the role is conditionally required if available
150
+ # and not equal to "assistant", so remove the role from the body if it is "assistant".
151
+ body["message"].pop("role", None)
152
+
153
+ if event.tool_calls is None:
154
+ del body["tool_calls"]
155
+
156
+ if not should_send_prompts():
157
+ body["message"].pop("content", None)
158
+ if body.get("tool_calls") is not None:
159
+ for tool_call in body["tool_calls"]:
160
+ tool_call["function"].pop("arguments", None)
161
+
162
+ event_logger.emit(
163
+ Event(name="gen_ai.choice", body=body, attributes=EVENT_ATTRIBUTES)
164
+ )
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, Literal, Optional, TypedDict
3
+
4
+
5
+ class _FunctionToolCall(TypedDict):
6
+ function_name: str
7
+ arguments: Optional[dict[str, Any]]
8
+
9
+
10
+ class ToolCall(TypedDict):
11
+ """Represents a tool call in the AI model."""
12
+
13
+ id: str
14
+ function: _FunctionToolCall
15
+ type: Literal["function"]
16
+
17
+
18
+ class CompletionMessage(TypedDict):
19
+ """Represents a message in the AI model."""
20
+
21
+ content: Any
22
+ role: str = "assistant"
23
+
24
+
25
+ @dataclass
26
+ class MessageEvent:
27
+ """Represents an input event for the AI model."""
28
+
29
+ content: Any
30
+ role: str = "user"
31
+ tool_calls: Optional[List[ToolCall]] = None
32
+
33
+
34
+ @dataclass
35
+ class ChoiceEvent:
36
+ """Represents a completion event for the AI model."""
37
+
38
+ index: int
39
+ message: CompletionMessage
40
+ finish_reason: str = "unknown"
41
+ tool_calls: Optional[List[ToolCall]] = None
@@ -0,0 +1,89 @@
1
+ from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_send_prompts
2
+ from opentelemetry.semconv_ai import SpanAttributes
3
+
4
+
5
+ def _set_span_attribute(span, name, value):
6
+ if value is not None:
7
+ if value != "":
8
+ span.set_attribute(name, value)
9
+ return
10
+
11
+
12
+ @dont_throw
13
+ def set_input_attributes(span, args):
14
+ if not span.is_recording():
15
+ return
16
+ if should_send_prompts() and args is not None and len(args) > 0:
17
+ prompt = ""
18
+ for arg in args:
19
+ if isinstance(arg, str):
20
+ prompt = f"{prompt}{arg}\n"
21
+ elif isinstance(arg, list):
22
+ for subarg in arg:
23
+ prompt = f"{prompt}{subarg}\n"
24
+
25
+ _set_span_attribute(
26
+ span,
27
+ f"{SpanAttributes.LLM_PROMPTS}.0.user",
28
+ prompt,
29
+ )
30
+
31
+
32
+ @dont_throw
33
+ def set_model_input_attributes(span, kwargs, llm_model):
34
+ if not span.is_recording():
35
+ return
36
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, llm_model)
37
+ _set_span_attribute(
38
+ span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
39
+ )
40
+ _set_span_attribute(
41
+ span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
42
+ )
43
+ _set_span_attribute(
44
+ span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
45
+ )
46
+ _set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
47
+ _set_span_attribute(span, SpanAttributes.LLM_TOP_K, kwargs.get("top_k"))
48
+ _set_span_attribute(
49
+ span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
50
+ )
51
+ _set_span_attribute(
52
+ span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
53
+ )
54
+
55
+
56
+ @dont_throw
57
+ def set_response_attributes(span, llm_model, generation_text):
58
+ if not span.is_recording() or not should_send_prompts():
59
+ return
60
+ _set_span_attribute(span, f"{SpanAttributes.LLM_COMPLETIONS}.0.role", "assistant")
61
+ _set_span_attribute(
62
+ span,
63
+ f"{SpanAttributes.LLM_COMPLETIONS}.0.content",
64
+ generation_text,
65
+ )
66
+
67
+
68
+ @dont_throw
69
+ def set_model_response_attributes(span, llm_model, token_usage):
70
+ if not span.is_recording():
71
+ return
72
+ _set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, llm_model)
73
+
74
+ if token_usage:
75
+ _set_span_attribute(
76
+ span,
77
+ SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
78
+ token_usage.total_token_count,
79
+ )
80
+ _set_span_attribute(
81
+ span,
82
+ SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
83
+ token_usage.candidates_token_count,
84
+ )
85
+ _set_span_attribute(
86
+ span,
87
+ SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
88
+ token_usage.prompt_token_count,
89
+ )
@@ -1,8 +1,18 @@
1
1
  import logging
2
+ import os
2
3
  import traceback
3
4
 
5
+ from opentelemetry import context as context_api
4
6
  from opentelemetry.instrumentation.vertexai.config import Config
5
7
 
8
+ TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"
9
+
10
+
11
+ def should_send_prompts():
12
+ return (
13
+ os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
14
+ ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
15
+
6
16
 
7
17
  def dont_throw(func):
8
18
  """
@@ -27,3 +37,7 @@ def dont_throw(func):
27
37
  Config.exception_logger(e)
28
38
 
29
39
  return wrapper
40
+
41
+
42
+ def should_emit_events():
43
+ return not Config.use_legacy_attributes
@@ -8,7 +8,7 @@ show_missing = true
8
8
 
9
9
  [tool.poetry]
10
10
  name = "opentelemetry-instrumentation-vertexai"
11
- version = "0.40.13"
11
+ version = "0.41.0"
12
12
  description = "OpenTelemetry Vertex AI instrumentation"
13
13
  authors = [
14
14
  "Gal Kleinman <gal@traceloop.com>",
@@ -28,7 +28,7 @@ python = ">=3.9,<4"
28
28
  opentelemetry-api = "^1.28.0"
29
29
  opentelemetry-instrumentation = ">=0.50b0"
30
30
  opentelemetry-semantic-conventions = ">=0.50b0"
31
- opentelemetry-semantic-conventions-ai = "0.4.9"
31
+ opentelemetry-semantic-conventions-ai = "0.4.10"
32
32
 
33
33
  [tool.poetry.group.dev.dependencies]
34
34
  autopep8 = "^2.2.0"