opentelemetry-instrumentation-vertexai 0.45.0__tar.gz → 0.49.6__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.
Files changed (13) hide show
  1. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/PKG-INFO +7 -6
  2. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/opentelemetry/instrumentation/vertexai/__init__.py +32 -11
  3. opentelemetry_instrumentation_vertexai-0.49.6/opentelemetry/instrumentation/vertexai/config.py +9 -0
  4. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/opentelemetry/instrumentation/vertexai/event_emitter.py +13 -4
  5. opentelemetry_instrumentation_vertexai-0.49.6/opentelemetry/instrumentation/vertexai/span_utils.py +313 -0
  6. opentelemetry_instrumentation_vertexai-0.49.6/opentelemetry/instrumentation/vertexai/version.py +1 -0
  7. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/pyproject.toml +6 -6
  8. opentelemetry_instrumentation_vertexai-0.45.0/opentelemetry/instrumentation/vertexai/config.py +0 -3
  9. opentelemetry_instrumentation_vertexai-0.45.0/opentelemetry/instrumentation/vertexai/span_utils.py +0 -89
  10. opentelemetry_instrumentation_vertexai-0.45.0/opentelemetry/instrumentation/vertexai/version.py +0 -1
  11. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/README.md +0 -0
  12. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/opentelemetry/instrumentation/vertexai/event_models.py +0 -0
  13. {opentelemetry_instrumentation_vertexai-0.45.0 → opentelemetry_instrumentation_vertexai-0.49.6}/opentelemetry/instrumentation/vertexai/utils.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-vertexai
3
- Version: 0.45.0
3
+ Version: 0.49.6
4
4
  Summary: OpenTelemetry Vertex AI instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -13,11 +13,12 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
16
17
  Provides-Extra: instruments
17
- Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
18
- Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
19
- Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
20
- Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.11)
18
+ Requires-Dist: opentelemetry-api (>=1.38.0,<2.0.0)
19
+ Requires-Dist: opentelemetry-instrumentation (>=0.59b0)
20
+ Requires-Dist: opentelemetry-semantic-conventions (>=0.59b0)
21
+ Requires-Dist: opentelemetry-semantic-conventions-ai (>=0.4.13,<0.5.0)
21
22
  Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-vertexai
22
23
  Description-Content-Type: text/markdown
23
24
 
@@ -5,7 +5,7 @@ import types
5
5
  from typing import Collection
6
6
 
7
7
  from opentelemetry import context as context_api
8
- from opentelemetry._events import get_event_logger
8
+ from opentelemetry._logs import get_logger
9
9
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
10
10
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
11
11
  from opentelemetry.instrumentation.vertexai.config import Config
@@ -15,12 +15,16 @@ from opentelemetry.instrumentation.vertexai.event_emitter import (
15
15
  )
16
16
  from opentelemetry.instrumentation.vertexai.span_utils import (
17
17
  set_input_attributes,
18
+ set_input_attributes_sync,
18
19
  set_model_input_attributes,
19
20
  set_model_response_attributes,
20
21
  set_response_attributes,
21
22
  )
22
23
  from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_emit_events
23
24
  from opentelemetry.instrumentation.vertexai.version import __version__
25
+ from opentelemetry.semconv._incubating.attributes import (
26
+ gen_ai_attributes as GenAIAttributes,
27
+ )
24
28
  from opentelemetry.semconv_ai import (
25
29
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
26
30
  LLMRequestTypeValues,
@@ -178,12 +182,12 @@ async def _abuild_from_streaming_response(span, event_logger, response, llm_mode
178
182
 
179
183
 
180
184
  @dont_throw
181
- def _handle_request(span, event_logger, args, kwargs, llm_model):
185
+ async def _handle_request(span, event_logger, args, kwargs, llm_model):
182
186
  set_model_input_attributes(span, kwargs, llm_model)
183
187
  if should_emit_events():
184
188
  emit_prompt_events(args, event_logger)
185
189
  else:
186
- set_input_attributes(span, args)
190
+ await set_input_attributes(span, args)
187
191
 
188
192
 
189
193
  def _handle_response(span, event_logger, response, llm_model):
@@ -223,18 +227,23 @@ async def _awrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs)
223
227
  llm_model = instance._model_id
224
228
  if hasattr(instance, "_model_name"):
225
229
  llm_model = instance._model_name.replace("publishers/google/models/", "")
230
+ # For ChatSession, try to get model from the parent model object
231
+ if hasattr(instance, "_model") and hasattr(instance._model, "_model_name"):
232
+ llm_model = instance._model._model_name.replace("publishers/google/models/", "")
233
+ elif hasattr(instance, "_model") and hasattr(instance._model, "_model_id"):
234
+ llm_model = instance._model._model_id
226
235
 
227
236
  name = to_wrap.get("span_name")
228
237
  span = tracer.start_span(
229
238
  name,
230
239
  kind=SpanKind.CLIENT,
231
240
  attributes={
232
- SpanAttributes.LLM_SYSTEM: "Google",
241
+ GenAIAttributes.GEN_AI_SYSTEM: "Google",
233
242
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
234
243
  },
235
244
  )
236
245
 
237
- _handle_request(span, event_logger, args, kwargs, llm_model)
246
+ await _handle_request(span, event_logger, args, kwargs, llm_model)
238
247
 
239
248
  response = await wrapped(*args, **kwargs)
240
249
 
@@ -267,18 +276,28 @@ def _wrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
267
276
  llm_model = instance._model_id
268
277
  if hasattr(instance, "_model_name"):
269
278
  llm_model = instance._model_name.replace("publishers/google/models/", "")
279
+ # For ChatSession, try to get model from the parent model object
280
+ if hasattr(instance, "_model") and hasattr(instance._model, "_model_name"):
281
+ llm_model = instance._model._model_name.replace("publishers/google/models/", "")
282
+ elif hasattr(instance, "_model") and hasattr(instance._model, "_model_id"):
283
+ llm_model = instance._model._model_id
270
284
 
271
285
  name = to_wrap.get("span_name")
272
286
  span = tracer.start_span(
273
287
  name,
274
288
  kind=SpanKind.CLIENT,
275
289
  attributes={
276
- SpanAttributes.LLM_SYSTEM: "Google",
290
+ GenAIAttributes.GEN_AI_SYSTEM: "Google",
277
291
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
278
292
  },
279
293
  )
280
294
 
281
- _handle_request(span, event_logger, args, kwargs, llm_model)
295
+ # Use sync version for non-async wrapper to avoid image processing for now
296
+ set_model_input_attributes(span, kwargs, llm_model)
297
+ if should_emit_events():
298
+ emit_prompt_events(args, event_logger)
299
+ else:
300
+ set_input_attributes_sync(span, args)
282
301
 
283
302
  response = wrapped(*args, **kwargs)
284
303
 
@@ -301,10 +320,12 @@ def _wrap(tracer, event_logger, to_wrap, wrapped, instance, args, kwargs):
301
320
  class VertexAIInstrumentor(BaseInstrumentor):
302
321
  """An instrumentor for VertextAI's client library."""
303
322
 
304
- def __init__(self, exception_logger=None, use_legacy_attributes=True):
323
+ def __init__(self, exception_logger=None, use_legacy_attributes=True, upload_base64_image=None):
305
324
  super().__init__()
306
325
  Config.exception_logger = exception_logger
307
326
  Config.use_legacy_attributes = use_legacy_attributes
327
+ if upload_base64_image:
328
+ Config.upload_base64_image = upload_base64_image
308
329
 
309
330
  def instrumentation_dependencies(self) -> Collection[str]:
310
331
  return _instruments
@@ -316,11 +337,11 @@ class VertexAIInstrumentor(BaseInstrumentor):
316
337
  event_logger = None
317
338
 
318
339
  if should_emit_events():
319
- event_logger_provider = kwargs.get("event_logger_provider")
320
- event_logger = get_event_logger(
340
+ logger_provider = kwargs.get("logger_provider")
341
+ event_logger = get_logger(
321
342
  __name__,
322
343
  __version__,
323
- event_logger_provider=event_logger_provider,
344
+ logger_provider=logger_provider,
324
345
  )
325
346
 
326
347
  for wrapped_method in WRAPPED_METHODS:
@@ -0,0 +1,9 @@
1
+ from typing import Callable
2
+
3
+
4
+ class Config:
5
+ exception_logger = None
6
+ use_legacy_attributes = True
7
+ upload_base64_image: Callable[[str, str, str, str], str] = (
8
+ lambda trace_id, span_id, image_name, base64_string: str
9
+ )
@@ -2,7 +2,7 @@ from dataclasses import asdict
2
2
  from enum import Enum
3
3
  from typing import Union
4
4
 
5
- from opentelemetry._events import Event
5
+ from opentelemetry._logs import LogRecord
6
6
  from opentelemetry.instrumentation.vertexai.event_models import (
7
7
  ChoiceEvent,
8
8
  MessageEvent,
@@ -140,7 +140,12 @@ def _emit_message_event(event: MessageEvent, event_logger) -> None:
140
140
  for tool_call in body["tool_calls"]:
141
141
  tool_call["function"].pop("arguments", None)
142
142
 
143
- event_logger.emit(Event(name=name, body=body, attributes=EVENT_ATTRIBUTES))
143
+ log_record = LogRecord(
144
+ body=body,
145
+ attributes=EVENT_ATTRIBUTES,
146
+ event_name=name
147
+ )
148
+ event_logger.emit(log_record)
144
149
 
145
150
 
146
151
  def _emit_choice_event(event: ChoiceEvent, event_logger) -> None:
@@ -159,6 +164,10 @@ def _emit_choice_event(event: ChoiceEvent, event_logger) -> None:
159
164
  for tool_call in body["tool_calls"]:
160
165
  tool_call["function"].pop("arguments", None)
161
166
 
162
- event_logger.emit(
163
- Event(name="gen_ai.choice", body=body, attributes=EVENT_ATTRIBUTES)
167
+ log_record = LogRecord(
168
+ body=body,
169
+ attributes=EVENT_ATTRIBUTES,
170
+ event_name="gen_ai.choice"
171
+
164
172
  )
173
+ event_logger.emit(log_record)
@@ -0,0 +1,313 @@
1
+ import copy
2
+ import json
3
+ import base64
4
+ import logging
5
+ import asyncio
6
+ import threading
7
+ from opentelemetry.instrumentation.vertexai.utils import dont_throw, should_send_prompts
8
+ from opentelemetry.instrumentation.vertexai.config import Config
9
+ from opentelemetry.semconv._incubating.attributes import (
10
+ gen_ai_attributes as GenAIAttributes,
11
+ )
12
+ from opentelemetry.semconv_ai import SpanAttributes
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def _set_span_attribute(span, name, value):
19
+ if value is not None:
20
+ if value != "":
21
+ span.set_attribute(name, value)
22
+ return
23
+
24
+
25
+ def _is_base64_image_part(item):
26
+ """Check if item is a VertexAI Part object containing image data"""
27
+ try:
28
+ # Check if it has the Part attributes we expect
29
+ if not hasattr(item, 'inline_data') or not hasattr(item, 'mime_type'):
30
+ return False
31
+
32
+ # Check if it's an image mime type and has inline data
33
+ if item.mime_type and 'image/' in item.mime_type and item.inline_data:
34
+ # Check if the inline_data has actual data
35
+ if hasattr(item.inline_data, 'data') and item.inline_data.data:
36
+ return True
37
+
38
+ return False
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ async def _process_image_part(item, trace_id, span_id, content_index):
44
+ """Process a VertexAI Part object containing image data"""
45
+ if not Config.upload_base64_image:
46
+ return None
47
+
48
+ try:
49
+ # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg')
50
+ image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown'
51
+ image_name = f"content_{content_index}.{image_format}"
52
+
53
+ # Convert binary data to base64 string for upload
54
+ binary_data = item.inline_data.data
55
+ base64_string = base64.b64encode(binary_data).decode('utf-8')
56
+
57
+ # Upload the base64 data - convert IDs to strings
58
+ url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
59
+
60
+ # Return OpenAI-compatible format for consistency across LLM providers
61
+ return {
62
+ "type": "image_url",
63
+ "image_url": {"url": url}
64
+ }
65
+ except Exception as e:
66
+ logger.warning(f"Failed to process image part: {e}")
67
+ # Return None to skip adding this image to the span
68
+ return None
69
+
70
+
71
+ def run_async(method):
72
+ """Handle async method in sync context, following OpenAI's battle-tested approach"""
73
+ try:
74
+ loop = asyncio.get_running_loop()
75
+ except RuntimeError:
76
+ loop = None
77
+
78
+ if loop and loop.is_running():
79
+ thread = threading.Thread(target=lambda: asyncio.run(method))
80
+ thread.start()
81
+ thread.join()
82
+ else:
83
+ asyncio.run(method)
84
+
85
+
86
+ def _process_image_part_sync(item, trace_id, span_id, content_index):
87
+ """Synchronous version of image part processing using OpenAI's pattern"""
88
+ if not Config.upload_base64_image:
89
+ return None
90
+
91
+ try:
92
+ # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg')
93
+ image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown'
94
+ image_name = f"content_{content_index}.{image_format}"
95
+
96
+ # Convert binary data to base64 string for upload
97
+ binary_data = item.inline_data.data
98
+ base64_string = base64.b64encode(binary_data).decode('utf-8')
99
+
100
+ # Use OpenAI's run_async pattern to handle the async upload function
101
+ url = None
102
+
103
+ async def upload_task():
104
+ nonlocal url
105
+ url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
106
+
107
+ run_async(upload_task())
108
+
109
+ return {
110
+ "type": "image_url",
111
+ "image_url": {"url": url}
112
+ }
113
+ except Exception as e:
114
+ logger.warning(f"Failed to process image part sync: {e}")
115
+ # Return None to skip adding this image to the span
116
+ return None
117
+
118
+
119
+ async def _process_vertexai_argument(argument, span):
120
+ """Process a single argument for VertexAI, handling different types"""
121
+ if isinstance(argument, str):
122
+ # Simple text argument in OpenAI format
123
+ return [{"type": "text", "text": argument}]
124
+
125
+ elif isinstance(argument, list):
126
+ # List of mixed content (text strings and Part objects) - deep copy and process
127
+ content_list = copy.deepcopy(argument)
128
+ processed_items = []
129
+
130
+ for item_index, content_item in enumerate(content_list):
131
+ processed_item = await _process_content_item_vertexai(content_item, span, item_index)
132
+ if processed_item is not None:
133
+ processed_items.append(processed_item)
134
+
135
+ return processed_items
136
+
137
+ else:
138
+ # Single Part object - convert to OpenAI format
139
+ processed_item = await _process_content_item_vertexai(argument, span, 0)
140
+ return [processed_item] if processed_item is not None else []
141
+
142
+
143
+ async def _process_content_item_vertexai(content_item, span, item_index):
144
+ """Process a single content item for VertexAI"""
145
+ if isinstance(content_item, str):
146
+ # Convert text to OpenAI format
147
+ return {"type": "text", "text": content_item}
148
+
149
+ elif _is_base64_image_part(content_item):
150
+ # Process image part
151
+ return await _process_image_part(
152
+ content_item, span.context.trace_id, span.context.span_id, item_index
153
+ )
154
+
155
+ elif hasattr(content_item, 'text'):
156
+ # Text part to OpenAI format
157
+ return {"type": "text", "text": content_item.text}
158
+
159
+ else:
160
+ # Other types as text
161
+ return {"type": "text", "text": str(content_item)}
162
+
163
+
164
+ def _process_vertexai_argument_sync(argument, span):
165
+ """Synchronous version of argument processing for VertexAI"""
166
+ if isinstance(argument, str):
167
+ # Simple text argument in OpenAI format
168
+ return [{"type": "text", "text": argument}]
169
+
170
+ elif isinstance(argument, list):
171
+ # List of mixed content (text strings and Part objects) - deep copy and process
172
+ content_list = copy.deepcopy(argument)
173
+ processed_items = []
174
+
175
+ for item_index, content_item in enumerate(content_list):
176
+ processed_item = _process_content_item_vertexai_sync(content_item, span, item_index)
177
+ if processed_item is not None:
178
+ processed_items.append(processed_item)
179
+
180
+ return processed_items
181
+
182
+ else:
183
+ # Single Part object - convert to OpenAI format
184
+ processed_item = _process_content_item_vertexai_sync(argument, span, 0)
185
+ return [processed_item] if processed_item is not None else []
186
+
187
+
188
+ def _process_content_item_vertexai_sync(content_item, span, item_index):
189
+ """Synchronous version of content item processing for VertexAI"""
190
+ if isinstance(content_item, str):
191
+ # Convert text to OpenAI format
192
+ return {"type": "text", "text": content_item}
193
+
194
+ elif _is_base64_image_part(content_item):
195
+ # Process image part
196
+ return _process_image_part_sync(
197
+ content_item, span.context.trace_id, span.context.span_id, item_index
198
+ )
199
+
200
+ elif hasattr(content_item, 'text'):
201
+ # Text part to OpenAI format
202
+ return {"type": "text", "text": content_item.text}
203
+
204
+ else:
205
+ # Other types as text
206
+ return {"type": "text", "text": str(content_item)}
207
+
208
+
209
+ @dont_throw
210
+ async def set_input_attributes(span, args):
211
+ """Process input arguments, handling both text and image content"""
212
+ if not span.is_recording():
213
+ return
214
+ if should_send_prompts() and args is not None and len(args) > 0:
215
+ # Process each argument using extracted helper methods
216
+ for arg_index, argument in enumerate(args):
217
+ processed_content = await _process_vertexai_argument(argument, span)
218
+
219
+ if processed_content:
220
+ _set_span_attribute(
221
+ span,
222
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{arg_index}.role",
223
+ "user"
224
+ )
225
+ _set_span_attribute(
226
+ span,
227
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{arg_index}.content",
228
+ json.dumps(processed_content)
229
+ )
230
+
231
+
232
+ # Sync version with image processing support
233
+ @dont_throw
234
+ def set_input_attributes_sync(span, args):
235
+ """Synchronous version with image processing support"""
236
+ if not span.is_recording():
237
+ return
238
+ if should_send_prompts() and args is not None and len(args) > 0:
239
+ # Process each argument using extracted helper methods
240
+ for arg_index, argument in enumerate(args):
241
+ processed_content = _process_vertexai_argument_sync(argument, span)
242
+
243
+ if processed_content:
244
+ _set_span_attribute(
245
+ span,
246
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{arg_index}.role",
247
+ "user"
248
+ )
249
+ _set_span_attribute(
250
+ span,
251
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{arg_index}.content",
252
+ json.dumps(processed_content)
253
+ )
254
+
255
+
256
+ @dont_throw
257
+ def set_model_input_attributes(span, kwargs, llm_model):
258
+ if not span.is_recording():
259
+ return
260
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, llm_model)
261
+ _set_span_attribute(
262
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.0.user", kwargs.get("prompt")
263
+ )
264
+ _set_span_attribute(
265
+ span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, kwargs.get("temperature")
266
+ )
267
+ _set_span_attribute(
268
+ span, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, kwargs.get("max_output_tokens")
269
+ )
270
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_P, kwargs.get("top_p"))
271
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_K, kwargs.get("top_k"))
272
+ _set_span_attribute(
273
+ span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
274
+ )
275
+ _set_span_attribute(
276
+ span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
277
+ )
278
+
279
+
280
+ @dont_throw
281
+ def set_response_attributes(span, llm_model, generation_text):
282
+ if not span.is_recording() or not should_send_prompts():
283
+ return
284
+ _set_span_attribute(span, f"{GenAIAttributes.GEN_AI_COMPLETION}.0.role", "assistant")
285
+ _set_span_attribute(
286
+ span,
287
+ f"{GenAIAttributes.GEN_AI_COMPLETION}.0.content",
288
+ generation_text,
289
+ )
290
+
291
+
292
+ @dont_throw
293
+ def set_model_response_attributes(span, llm_model, token_usage):
294
+ if not span.is_recording():
295
+ return
296
+ _set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, llm_model)
297
+
298
+ if token_usage:
299
+ _set_span_attribute(
300
+ span,
301
+ SpanAttributes.LLM_USAGE_TOTAL_TOKENS,
302
+ token_usage.total_token_count,
303
+ )
304
+ _set_span_attribute(
305
+ span,
306
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
307
+ token_usage.candidates_token_count,
308
+ )
309
+ _set_span_attribute(
310
+ span,
311
+ GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
312
+ token_usage.prompt_token_count,
313
+ )
@@ -8,7 +8,7 @@ show_missing = true
8
8
 
9
9
  [tool.poetry]
10
10
  name = "opentelemetry-instrumentation-vertexai"
11
- version = "0.45.0"
11
+ version = "0.49.6"
12
12
  description = "OpenTelemetry Vertex AI instrumentation"
13
13
  authors = [
14
14
  "Gal Kleinman <gal@traceloop.com>",
@@ -25,10 +25,10 @@ include = "opentelemetry/instrumentation/vertexai"
25
25
 
26
26
  [tool.poetry.dependencies]
27
27
  python = ">=3.9,<4"
28
- opentelemetry-api = "^1.28.0"
29
- opentelemetry-instrumentation = ">=0.50b0"
30
- opentelemetry-semantic-conventions = ">=0.50b0"
31
- opentelemetry-semantic-conventions-ai = "0.4.11"
28
+ opentelemetry-api = "^1.38.0"
29
+ opentelemetry-instrumentation = ">=0.59b0"
30
+ opentelemetry-semantic-conventions = ">=0.59b0"
31
+ opentelemetry-semantic-conventions-ai = "^0.4.13"
32
32
 
33
33
  [tool.poetry.group.dev.dependencies]
34
34
  autopep8 = "^2.2.0"
@@ -41,7 +41,7 @@ pytest = "^8.2.2"
41
41
  pytest-sugar = "1.0.0"
42
42
  vcrpy = "^6.0.1"
43
43
  pytest-recording = "^0.13.1"
44
- opentelemetry-sdk = "^1.27.0"
44
+ opentelemetry-sdk = "^1.38.0"
45
45
  google-cloud-aiplatform = "^1.54.0"
46
46
 
47
47
  [build-system]
@@ -1,3 +0,0 @@
1
- class Config:
2
- exception_logger = None
3
- use_legacy_attributes = True
@@ -1,89 +0,0 @@
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
- )