opentelemetry-instrumentation-vertexai 0.46.0__tar.gz → 0.46.2__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.

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