-
Notifications
You must be signed in to change notification settings - Fork 868
fix(google-generativeai,vertexai): image support for Gemini models #3340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Implement comprehensive image support for both Google Generative AI and Vertex AI instrumentations:
**Google Generative AI Instrumentation:**
- Add image detection for Part objects with inline_data containing binary image data
- Implement async and sync image processing functions
- Convert images to OpenAI-compatible format for consistency across LLM providers
- Support mixed text/image content in spans with proper JSON structure
- Add upload_base64_image configuration hook integration
**Vertex AI Instrumentation:**
- Add image detection for Part objects with base64 image data
- Implement async and sync image processing with OpenAI format output
- Handle mixed content arrays with text and image parts
- Add upload_base64_image configuration hook integration
**Traceloop SDK Integration:**
- Update Google GenAI instrumentor initialization to accept base64_image_uploader
- Pass image upload hook to both instrumentations for proper image URL replacement
**Sample Applications:**
- Add google_genai_image_example.py demonstrating Google GenAI with images
- Clean up test files and improve sample app structure
**Key Features:**
- Consistent OpenAI format: {"type": "image_url", "image_url": {"url": "..."}}
- Support for PNG, JPEG, GIF, WebP image formats
- Graceful fallback URLs when upload fails
- Proper async/sync handling with asyncio.run() in sync contexts
- Full backward compatibility with existing text-only workflows
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds base64 image-part handling to prompt processing with async and sync code paths across Google Generative AI, Vertex AI, and OpenAI instrumentations. Exposes an upload_base64_image configuration/constructor parameter propagated from SDK init. Introduces JSON-encoded prompt content, new sample vision scripts, and control-flow changes to call async/sync attribute setters. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor App
participant SDK as Traceloop SDK
participant Instr as Instrumentor
participant Span as Span Utils
participant Upl as Config.upload_base64_image
participant API as Provider API
App->>SDK: init(..., base64_image_uploader)
SDK->>Instr: construct(upload_base64_image=callable)
App->>Instr: request(model, contents)
alt async flow
Instr->>Span: await set_input_attributes(span, args, kwargs, model)
loop per part
alt image part
Span->>Upl: upload(str(trace_id), str(span_id), name, b64)
Upl-->>Span: url or failure
Span-->>Instr: add image_url entry
else text/other
Span-->>Instr: add text entry
end
end
else sync flow
Instr->>Span: set_input_attributes_sync(span, args, kwargs, model)
end
Instr->>API: generate_content(...)
API-->>Instr: response
Instr-->>App: response
sequenceDiagram
autonumber
participant SyncWrap as Sync Wrapper
participant AsyncWrap as Async Wrapper
participant Span as Span Utils
Note over SyncWrap: Non-emitting sync path
SyncWrap->>Span: set_input_attributes_sync(...)
Span-->>SyncWrap: attributes set
Note over AsyncWrap: Non-emitting async path
AsyncWrap->>Span: await set_input_attributes(...)
Span-->>AsyncWrap: attributes set
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed everything up to 20231b2 in 1 minute and 42 seconds. Click for details.
- Reviewed
987lines of code in9files - Skipped
0files when reviewing. - Skipped posting
7draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py:7
- Draft comment:
Default lambda for upload_base64_image returns the type 'str' instead of a string value. Verify if this placeholder is intended. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =50%<= threshold50%The comment is pointing out a potential issue with the return type of a lambda function. It suggests that the lambda returns a type instead of a value, which could be a mistake. The comment is asking for confirmation if this is intended, which is not allowed. However, it is also indirectly suggesting that there might be an issue with the code. The comment could be rephrased to directly suggest a code change or clarification without asking for confirmation.
2. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:45
- Draft comment:
The check 'if not Config.upload_base64_image:' may never trigger because the default lambda is truthy. This could lead to always processing images even when no uploader is configured. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% Without seeing the Config class implementation, we can't verify if there's actually a default lambda that would make this check problematic. The current code's behavior of checking truthiness seems reasonable - it would handle None, False, or any other falsy value by skipping image processing. The suggested change to explicitly check for None would be more restrictive and might break existing behavior. I could be wrong about the Config implementation - maybe there is a default lambda that makes this check problematic. However, without seeing that code, I can't verify the claim. Even if there is a default lambda, the current code's behavior of checking truthiness is a valid pattern that would work correctly in most cases. The burden of proof is on the commenter to show why this is actually problematic. Delete this comment. Without evidence of the Config implementation and why the current truthiness check is problematic, this seems like an unnecessary change that could break existing behavior.
3. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:88
- Draft comment:
Using asyncio.run in _process_image_part_sync may block or raise errors if an event loop is already running. Consider handling such cases or documenting its limitations. - Reason this comment was not posted:
Comment looked like it was already resolved.
4. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py:7
- Draft comment:
Similar to Google config, the default lambda for upload_base64_image returns 'str' (the type) rather than a string value. Confirm if this placeholder is acceptable. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 30% vs. threshold = 50% This appears to be a legitimate issue - the lambda is returning the type 'str' instead of a string value, which would cause a runtime error. However, the comment asks for confirmation rather than directly stating the problem. The issue itself is real and needs fixing, but the way it's phrased violates our rules about asking for confirmation. The code might be intentionally written this way as a placeholder. Maybe there's a pattern in the codebase where returning types is acceptable. Even if it's intentional, returning a type instead of a value is incorrect Python and would cause runtime errors. This needs to be fixed regardless of intent. The core issue (returning type vs value) should be flagged, but the comment should be rewritten to be more direct rather than asking for confirmation.
5. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:84
- Draft comment:
In the synchronous image processing (_process_image_part_sync), asyncio.run is used. Confirm that blocking behavior is acceptable and consider alternatives if running in environments with an active event loop. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 30% vs. threshold = 50% The comment raises a valid technical concern - asyncio.run can cause issues if used in an environment with an existing event loop. However, the comment is phrased as "confirm that..." and "consider alternatives" which violates our rule about not asking for confirmations. The code already has error handling and fallbacks, suggesting the authors considered failure cases. The use of asyncio.run could genuinely cause runtime errors in some environments. Maybe this is important enough to keep despite being phrased as a question? While the technical concern is valid, the comment violates our rule about not asking for confirmations or verifications. If there was a specific better alternative being suggested, that would be different. Delete the comment. While it raises a valid technical point, it's phrased as a request for confirmation rather than providing a clear actionable suggestion for improvement.
6. packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py:170
- Draft comment:
The method 'aupload_base64_image' is being passed as the uploader. Verify if the naming ('aupload') is intentional or a typo for 'upload_base64_image'. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
7. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py:308
- Draft comment:
Typographical error: The docstring mentions "VertextAI's" which appears to be a typo. Consider updating it to "VertexAI's". - Reason this comment was not posted:
Comment was on unchanged code.
Workflow ID: wflow_1G0dXbVZksprOK7H
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Outdated
Show resolved
Hide resolved
…mage uploads Remove misleading fallback URLs when image uploads fail. Instead of adding placeholder URLs like "/fallback/async_image" to spans, simply skip adding the image to the span if upload fails. **Changes:** - _process_image_part() and _process_image_part_sync() now return None when upload fails - Updated all callers to check for None and skip adding failed images to processed_content - Removed nested try/catch for upload failures - let the main exception handler catch all failures - Images that can't be uploaded won't appear in spans rather than showing fake URLs This provides cleaner span data - if an image couldn't be uploaded and stored, it shouldn't be referenced in the span attributes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 535e612 in 1 minute and 27 seconds. Click for details.
- Reviewed
347lines of code in2files - Skipped
0files when reviewing. - Skipped posting
5draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:45
- Draft comment:
The fallback behavior has been changed: instead of returning a fallback URL, the function now returns None if upload_base64_image is not set or fails. Confirm that skipping the image (i.e. returning None) is intended, as the PR description mentions graceful fallbacks. - Reason this comment was not posted:
Comment looked like it was already resolved.
2. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:86
- Draft comment:
The synchronous function uses asyncio.run to call the async uploader. Be cautious as asyncio.run can cause issues if an event loop is already running. Consider alternative approaches if this function might be called from such contexts. - Reason this comment was not posted:
Comment was on unchanged code.
3. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:41
- Draft comment:
Similarly, the VertexAI image processing functions now return None instead of a fallback URL. Confirm that this behavior—omitting the image part on failure—is intended, as the PR details mention graceful fallback URLs. - Reason this comment was not posted:
Comment looked like it was already resolved.
4. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:81
- Draft comment:
The synchronous method uses asyncio.run for the async upload call. As with the Google instrumentation, this may cause issues in environments with a running event loop. Consider verifying the usage context for potential conflicts. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 30% vs. threshold = 50% This is a legitimate technical concern - asyncio.run() can't be used when there's already a running event loop. However, the comment is speculative ("may cause issues") and asks for verification rather than suggesting a clear fix. The code is part of an instrumentation library where the context of usage isn't fully known. The issue raised is technically valid and could cause real problems in production. Maybe we're being too strict about speculative comments when they point out genuine architectural risks? While the concern is valid, the comment violates our rules by being speculative and asking for verification rather than providing a clear, actionable solution. If there was a specific fix suggested, it would be different. Delete the comment. While it raises a valid technical concern, it's speculative and asks for verification rather than providing a concrete solution.
5. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:70
- Draft comment:
There is noticeable duplication between the async and sync image processing functions. Consider refactoring the common logic (e.g., extracting the image format, converting data to base64) to reduce maintenance overhead. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
Workflow ID: wflow_jklSErDpaYh5f5hi
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (1)
175-176: Bug: Async streaming finalizer passes the generator instead of the accumulated text
_abuild_from_streaming_responseshould passcomplete_responsetohandle_streaming_response, just like the sync builder. Passing the async generator object (response) will causeset_response_attributesto record a generator repr instead of the model text and can break downstream consumers.Apply this diff:
- handle_streaming_response(span, event_logger, llm_model, response, token_usage) + handle_streaming_response( + span, event_logger, llm_model, complete_response, token_usage + )packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (2)
424-429: Unify uploader callable type (3-arg vs 4-arg) and make it optional.init_instrumentations currently types base64_image_uploader as Callable[[str, str, str], str], but Gemini/Vertex pass a 4th base64 argument. This mismatch will confuse type checkers and risks incorrect adapters down the line. Also allow None.
Apply:
-def init_instrumentations( - should_enrich_metrics: bool, - base64_image_uploader: Callable[[str, str, str], str], +from typing import Awaitable, Any + +def init_instrumentations( + should_enrich_metrics: bool, + base64_image_uploader: Optional[Callable[..., Awaitable[str]]] = None, instruments: Optional[Set[Instruments]] = None, block_instruments: Optional[Set[Instruments]] = None, ):
168-173: Guard None and attribute drift on image_uploader; pass a safe callable.image_uploader can be None and not all implementations may expose aupload_base64_image. This will raise AttributeError at import/init time.
Apply:
- init_instrumentations( - should_enrich_metrics, - image_uploader.aupload_base64_image, - instruments, - block_instruments, - ) + uploader_callable = ( + getattr(image_uploader, "aupload_base64_image", None) if image_uploader else None + ) + init_instrumentations( + should_enrich_metrics, + uploader_callable, + instruments, + block_instruments, + )Optionally, adapt 3-arg vs 4-arg at the source by wrapping:
+ # Optional adapter to tolerate both 3-arg and 4-arg uploaders + if uploader_callable: + async def _adapted_uploader(trace_id, span_id, image_name, base64_string): + try: + return await uploader_callable(trace_id, span_id, image_name, base64_string) + except TypeError: + return await uploader_callable(trace_id, span_id, image_name) + uploader_callable = _adapted_uploaderpackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py (1)
281-289: Async wrapper selection bug: wrong condition prevents using _awrap.WRAPPED_METHODS uses "generate_content" for both Models and AsyncModels, but the code checks for "generate_content_async". As a result, async paths will be wrapped with the sync wrapper.
Apply:
- wrap_function_wrapper( - wrap_package, - f"{wrap_object}.{wrap_method}", - ( - _awrap(tracer, event_logger, wrapped_method) - if wrap_method == "generate_content_async" - else _wrap(tracer, event_logger, wrapped_method) - ), - ) + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _awrap(tracer, event_logger, wrapped_method) + if wrap_object == "AsyncModels" + else _wrap(tracer, event_logger, wrapped_method), + )
🧹 Nitpick comments (12)
packages/sample-app/sample_app/google_genai_image_example.py (1)
35-44: Simplify and harden MIME-type detectionYou already call
.lower()on the path, so listing uppercase variants is redundant. Also, using the stdlibmimetypesavoids manual extension lists and supports more formats out of the box.Apply this diff:
- # Determine mime type from file extension - if image_path.lower().endswith(('.png', '.PNG')): - mime_type = "image/png" - elif image_path.lower().endswith(('.jpg', '.jpeg', '.JPG', '.JPEG')): - mime_type = "image/jpeg" - elif image_path.lower().endswith(('.gif', '.GIF')): - mime_type = "image/gif" - elif image_path.lower().endswith(('.webp', '.WEBP')): - mime_type = "image/webp" - else: - mime_type = "image/jpeg" # Default fallback + # Determine mime type from filename using stdlib + import mimetypes + mime_type, _ = mimetypes.guess_type(image_path) + if not mime_type or not mime_type.startswith("image/"): + mime_type = "image/jpeg" # Default fallbackpackages/sample-app/sample_app/vertex_gemini_vision_example.py (1)
29-34: Optional: detect MIME type dynamicallyRight now every local image is sent as
image/jpeg. If the sample may handle PNG/GIF/WebP, consider detecting from filename or bytes.Example using stdlib
mimetypes:- image_part = Part.from_data( - data=image_data, - mime_type="image/jpeg" - ) + import mimetypes + mime_type, _ = mimetypes.guess_type(image_path) + if not mime_type or not mime_type.startswith("image/"): + mime_type = "image/jpeg" + image_part = Part.from_data(data=image_data, mime_type=mime_type)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (2)
282-288: Outdated comment: code does perform image processing in sync pathThe comment says “avoid image processing for now,” but
set_input_attributes_syncincludes image processing and upload hook usage. This can confuse future maintainers.Apply this diff:
- # Use sync version for non-async wrapper to avoid image processing for now + # Use the synchronous attribute path for non-async wrappers
310-316: Type-hint the new constructor arg and document behaviorFor clarity and IDE help, add a precise type hint to
upload_base64_image, matching Config. Also briefly document that, if provided, it overrides the global Config uploader.Apply this diff:
- def __init__(self, exception_logger=None, use_legacy_attributes=True, upload_base64_image=None): + def __init__( + self, + exception_logger=None, + use_legacy_attributes: bool = True, + upload_base64_image: "Callable[[str, str, str, str], str]" = None, + ):Note: using a forward string reference avoids adding a new import in this file. Alternatively, import
Callablelocally.packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (2)
680-693: Match Gemini init signature to widened callable type.Stay consistent with the Optional async callable; avoids typing noise and future adapters.
-def init_google_generativeai_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] -): +def init_google_generativeai_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Optional[Callable[..., Awaitable[str]]] = None +):
943-955: Match Vertex init signature to widened callable type.Same rationale as for Gemini.
-def init_vertexai_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] -): +def init_vertexai_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Optional[Callable[..., Awaitable[str]]] = None +):packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (3)
70-101: Same fixes for sync processing; avoid losing uploads under running event loop.Currently asyncio.run() under an active loop will raise and force a fallback URL. Consider a safer bridge and stringified IDs.
- image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' + mime = getattr(getattr(item, "inline_data", None), "mime_type", None) or getattr(item, "mime_type", None) + image_format = mime.split('/')[1] if mime else 'unknown' @@ - try: - url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) + tid, sid = str(trace_id), str(span_id) + try: + # Prefer running in a new loop when one is already active to avoid RuntimeError + loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + pass + if loop and loop.is_running(): + # Run in a worker thread to avoid interfering with the existing loop + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: + url = ex.submit(lambda: asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string))).result() + else: + url = asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string)) except Exception as upload_error: logger.warning(f"Failed to upload image: {upload_error}") url = f"/image/{image_name}" # Fallback URLIf adding concurrent.futures is undesirable, at minimum stringify IDs:
- url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) + url = asyncio.run(Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string))
105-153: Set role attribute alongside content for consistency with OpenAI/GenAI paths.You build content but never set f"{prefix}.role". GenAI and OpenAI code sets role="user". Aligning improves downstream parsing consistency.
- _set_span_attribute(span, f"{prefix}.content", json.dumps(processed_content)) + _set_span_attribute(span, f"{prefix}.content", json.dumps(processed_content)) + _set_span_attribute(span, f"{prefix}.role", "user")
21-35: Flake8: avoid bare except (E722).Addressed in the detection function change above. If you keep any bare excepts, switch to
except Exception:.packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (3)
19-23: Micro: simplify guard.Combine nested checks.
-def _set_span_attribute(span, name, value): - if value is not None: - if value != "": - span.set_attribute(name, value) +def _set_span_attribute(span, name, value): + if value is not None and value != "": + span.set_attribute(name, value)
26-41: Tighten detector and exceptions.Good structure; minor simplification and explicit Exception.
- try: - # Check if it has the Part attributes we expect for new Google GenAI SDK - if hasattr(item, 'inline_data') and item.inline_data is not None: - # Check if it's an image mime type and has data - if (hasattr(item.inline_data, 'mime_type') and - item.inline_data.mime_type and - 'image/' in item.inline_data.mime_type and - hasattr(item.inline_data, 'data') and - item.inline_data.data): - return True - return False - except Exception: + try: + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) + has_data = hasattr(inline, "data") and bool(inline.data) + return bool(mime and mime.startswith("image/") and has_data) + except Exception: return False
74-106: Safer sync upload under running event loops; stringify IDs.asyncio.run inside a running loop raises RuntimeError leading to fallback URL.
- try: - url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) + tid, sid = str(trace_id), str(span_id) + try: + loop = None + try: + loop = asyncio.get_running_loop() + except RuntimeError: + pass + if loop and loop.is_running(): + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex: + url = ex.submit(lambda: asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string))).result() + else: + url = asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (9)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py(3 hunks)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py(1 hunks)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py(2 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py(5 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py(1 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py(1 hunks)packages/sample-app/sample_app/google_genai_image_example.py(1 hunks)packages/sample-app/sample_app/vertex_gemini_vision_example.py(1 hunks)packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py(5 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules
Files:
packages/sample-app/sample_app/google_genai_image_example.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.pypackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.pypackages/sample-app/sample_app/vertex_gemini_vision_example.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.pypackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.pypackages/traceloop-sdk/traceloop/sdk/tracing/tracing.pypackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py
🧬 Code graph analysis (9)
packages/sample-app/sample_app/google_genai_image_example.py (3)
packages/traceloop-sdk/traceloop/sdk/__init__.py (2)
Traceloop(37-274)init(49-206)packages/traceloop-sdk/traceloop/sdk/client/client.py (1)
Client(12-66)packages/sample-app/sample_app/vertex_gemini_vision_example.py (1)
describe_image_from_local_path(20-41)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (2)
set_input_attributes_sync(157-204)set_input_attributes(105-152)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py (1)
should_emit_events(42-43)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/event_emitter.py (1)
emit_prompt_events(64-73)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/utils.py (2)
dont_throw(13-35)should_send_prompts(38-41)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
Config(4-9)
packages/sample-app/sample_app/vertex_gemini_vision_example.py (2)
packages/traceloop-sdk/traceloop/sdk/__init__.py (2)
Traceloop(37-274)init(49-206)packages/sample-app/sample_app/google_genai_image_example.py (1)
describe_image_from_local_path(27-75)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)
set_input_attributes_sync(220-326)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
Config(4-9)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py (2)
dont_throw(17-39)should_send_prompts(11-14)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
SpanAttributes(64-261)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (4)
_set_span_attribute(19-23)_process_image_part(43-71)_process_image_part_sync(74-105)set_input_attributes(109-215)
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (5)
packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
upload_base64_image(137-138)packages/opentelemetry-instrumentation-anthropic/tests/conftest.py (3)
upload_base64_image(84-85)upload_base64_image(107-108)upload_base64_image(133-134)packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py (1)
upload_base64_image(14-15)packages/traceloop-sdk/traceloop/sdk/telemetry.py (3)
Telemetry(12-94)capture(66-73)log_exception(75-86)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (1)
VertexAIInstrumentor(307-356)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (3)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
Config(4-9)packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (1)
Config(6-15)packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/config.py (1)
Config(6-13)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
20-21: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
30-36: Use a single if statement instead of nested if statements
(SIM102)
packages/sample-app/sample_app/vertex_gemini_vision_example.py
7-7: google.cloud.storage imported but unused
Remove unused import: google.cloud.storage
(F401)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
15-16: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
29-31: Use a single if statement instead of nested if statements
(SIM102)
35-35: Do not use bare except
(E722)
🪛 Flake8 (7.2.0)
packages/sample-app/sample_app/vertex_gemini_vision_example.py
[error] 7-7: 'google.cloud.storage' imported but unused
(F401)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
[error] 35-35: do not use bare 'except'
(E722)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Test Packages (3.10)
- GitHub Check: Test Packages (3.11)
- GitHub Check: Test Packages (3.12)
- GitHub Check: Lint
- GitHub Check: Build Packages (3.11)
🔇 Additional comments (8)
packages/sample-app/sample_app/google_genai_image_example.py (1)
24-25: Verify model and location values for your environmentUsing
location="global"and the model IDgemini-2.0-flash-expmay vary by region and SDK version. If the target Vertex/GenAI endpoint in your project doesn’t expose this model or acceptglobal, requests will fail.Would you like me to check the latest supported model IDs and regions for the
google-genaiSDK and Vertex endpoints you’re using?Also applies to: 65-68
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (1)
181-188: Good: request handling is now truly asyncMaking
_handle_requestasync and awaiting it in_awrapensures image-part processing (which may await uploads) happens before the model call and keeps the async path consistent with the new span_utils behavior.Also applies to: 238-238
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (1)
461-463: OK to forward uploader to Gemini instrumentor (post-type fix).Once the callable type is widened to Optional[Callable[..., Awaitable[str]]], this pass-through looks good.
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py (3)
16-20: Import switch to set_input_attributes_sync looks good.Keeps sync flow consistent with event-less path.
252-258: Config wiring for upload_base64_image is sound.Passing the callable through the constructor and assigning to Config is consistent with other instrumentors.
106-106: OK to route non-event path through sync attribute setter.Matches the synchronous wrapping being used here.
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)
117-172: LGTM: mixed-content handling for contents[] with text and image Parts.The per-item JSON payload with role defaulting to "user" aligns with OpenAI-compatible formatting.
173-216: LGTM: args[] path mirrors contents[]; role set to "user".Consistent with the new content structure.
| upload_base64_image: Callable[[str, str, str, str], str] = ( | ||
| lambda trace_id, span_id, image_name, base64_string: str | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix default upload_base64_image: current lambda returns the str type, not a URL string
The default should return a valid string (ideally a harmless placeholder URL) rather than the str class object. As-is, any call site that uses the default will get <class 'str'>, which breaks downstream JSON formatting for image_url.url and can crash or silently misbehave.
Apply this diff:
- upload_base64_image: Callable[[str, str, str, str], str] = (
- lambda trace_id, span_id, image_name, base64_string: str
- )
+ # Default: do not upload, but return a harmless placeholder URL.
+ upload_base64_image: Callable[[str, str, str, str], str] = (
+ lambda trace_id, span_id, image_name, base64_string: "about:blank"
+ )Optionally, consider letting this be Optional[Callable] with None default and handling the fallback in span_utils for more explicit behavior. I can draft that change across both google_generativeai and vertexai configs to keep them consistent.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| upload_base64_image: Callable[[str, str, str, str], str] = ( | |
| lambda trace_id, span_id, image_name, base64_string: str | |
| ) | |
| # Default: do not upload, but return a harmless placeholder URL. | |
| upload_base64_image: Callable[[str, str, str, str], str] = ( | |
| lambda trace_id, span_id, image_name, base64_string: "about:blank" | |
| ) |
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py
around lines 7-9, the default upload_base64_image lambda currently returns the
str class object rather than a URL string; replace the default implementation so
it returns a harmless placeholder URL string (e.g.
"https://example.com/placeholder.png") instead of str; alternatively, if you
prefer the Optional approach, change the type to Optional[Callable[[str, str,
str, str], str]] = None and handle the fallback where upload_base64_image is
invoked (e.g., in span_utils) to return a placeholder URL when None.
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Show resolved
Hide resolved
| upload_base64_image: Callable[[str, str, str, str], str] = ( | ||
| lambda trace_id, span_id, image_name, base64_string: str | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix default upload_base64_image: returning str type is a bug
Same issue as the Google GenAI config: the default currently returns the str class object, not a URL string. This will break OpenAI-compatible JSON image content.
Apply this diff:
- upload_base64_image: Callable[[str, str, str, str], str] = (
- lambda trace_id, span_id, image_name, base64_string: str
- )
+ # Default: do not upload, but return a harmless placeholder URL.
+ upload_base64_image: Callable[[str, str, str, str], str] = (
+ lambda trace_id, span_id, image_name, base64_string: "about:blank"
+ )If you’d like, I can also convert this to Optional[Callable] with a None default and wire an explicit fallback in span_utils’ image processing paths.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| upload_base64_image: Callable[[str, str, str, str], str] = ( | |
| lambda trace_id, span_id, image_name, base64_string: str | |
| ) | |
| # Default: do not upload, but return a harmless placeholder URL. | |
| upload_base64_image: Callable[[str, str, str, str], str] = ( | |
| lambda trace_id, span_id, image_name, base64_string: "about:blank" | |
| ) |
🤖 Prompt for AI Agents
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py
lines 7-9: the default upload_base64_image currently returns the Python str
class object instead of a URL string; replace the default lambda so it returns
an actual empty string (or a sensible default URL string) instead of the str
type object, ensuring the Callable signature remains the same; optionally
consider switching the annotation to Optional[Callable] with a None default and
handle the fallback in span_utils if you prefer explicit wiring.
.../opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
Show resolved
Hide resolved
.../opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
Show resolved
Hide resolved
**Config fixes:** - Set upload_base64_image default to None instead of returning str type - Properly handle None check in image processing functions **Asyncio handling improvements:** - Add proper error handling for asyncio.run() in sync contexts - Handle "cannot be called from a running event loop" gracefully - Skip image upload and return None when event loop conflicts occur These changes address code review feedback about potential runtime errors and improve robustness in different execution environments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 29bbfde in 47 seconds. Click for details.
- Reviewed
62lines of code in4files - Skipped
0files when reviewing. - Skipped posting
4draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py:7
- Draft comment:
Changed default hook from a lambda to None. Ensure documentation makes it clear that upload_base64_image must be explicitly configured. - Reason this comment was not posted:
Confidence changes required:50%<= threshold50%None
2. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:86
- Draft comment:
Catching RuntimeError with a string check can be brittle. Consider using a more robust method for detecting a running event loop or centralizing this logic. - Reason this comment was not posted:
Confidence changes required:50%<= threshold50%None
3. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py:7
- Draft comment:
Default hook changed to None; ensure that users are aware upload_base64_image must be provided in configuration. - Reason this comment was not posted:
Confidence changes required:50%<= threshold50%None
4. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:83
- Draft comment:
Similar to the Google instrument, using a string match in the RuntimeError handling is somewhat brittle. Consider refactoring this shared pattern if possible. - Reason this comment was not posted:
Confidence changes required:50%<= threshold50%None
Workflow ID: wflow_Uqn4YCIZzrsUClqP
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
…sage" This reverts commit 29bbfde.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed 144929b in 1 minute and 45 seconds. Click for details.
- Reviewed
62lines of code in4files - Skipped
0files when reviewing. - Skipped posting
2draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py:8
- Draft comment:
The lambda here returns the type 'str' instead of an actual string value. Consider providing a proper default async/no-op implementation returning a valid string or None. - Reason this comment was not posted:
Marked as duplicate.
2. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:82
- Draft comment:
Directly calling asyncio.run() without error handling can trigger a RuntimeError when an event loop is active. Consider adding a try/except block similar to previous implementation. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The original code had specific handling for the "event loop already running" error, which is a real issue that can occur when using asyncio.run() in a sync context. However, the function already has a general try/except that will catch this error and handle it gracefully by logging and returning None. The specific error handling wasn't providing much additional value beyond what the general error handler already does. The comment identifies a real potential issue with asyncio.run(), but may be overlooking the existing error handling that would catch this case. While the concern about asyncio.run() is valid, the existing general error handler on lines 88-91 already provides adequate protection against this and other errors. The comment should be deleted since the code already has sufficient error handling through its general try/except block that will catch and handle the asyncio.run() error appropriately.
Workflow ID: wflow_IJez43PxxtRN6gBY
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
| exception_logger = None | ||
| use_legacy_attributes = True | ||
| upload_base64_image: Callable[[str, str, str, str], str] = ( | ||
| lambda trace_id, span_id, image_name, base64_string: str |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The lambda returns the type 'str' instead of a string value. This default no-op doesn't match expected behavior. Consider returning a valid string (or an async no-op function) instead of the type.
| lambda trace_id, span_id, image_name, base64_string: str | |
| lambda trace_id, span_id, image_name, base64_string: "" |
| base64_string = base64.b64encode(binary_data).decode('utf-8') | ||
|
|
||
| # Use asyncio.run to call the async upload function in sync context | ||
| url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the try/except block around asyncio.run() may lead to RuntimeError if an event loop is already running. Consider reintroducing proper error handling or using a workaround for nested event loops.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
♻️ Duplicate comments (4)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (2)
57-59: Normalize OTel IDs to strings before upload (matches Config type and uploader expectations)
span.context.trace_idandspan.context.span_idare integers in OTel.Config.upload_base64_imageis typed to receive strings. Normalize IDs to strings to avoid type surprises.Apply:
# Upload the base64 data - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) + tid, sid = str(trace_id), str(span_id) + url = await Config.upload_base64_image(tid, sid, image_name, base64_string)- url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) + tid, sid = str(trace_id), str(span_id) + url = asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string))Also applies to: 85-87
85-87: Guard asyncio.run in sync path to avoid RuntimeError when an event loop is already running; provide fallbackCalling
asyncio.runinside an already-running event loop raisesRuntimeError. Add a guard and graceful fallback URL.- # Use asyncio.run to call the async upload function in sync context - url = asyncio.run(Config.upload_base64_image(trace_id, span_id, image_name, base64_string)) + # Use asyncio.run to call the async upload function in sync context + try: + url = asyncio.run(Config.upload_base64_image(tid, sid, image_name, base64_string)) + except RuntimeError: + # Event loop is already running (e.g., notebooks, async frameworks) + logger.warning("Event loop already running; cannot use asyncio.run in sync path, using fallback URL.") + fallback_url = getattr( + Config, + "fallback_sync_image_url", + getattr(Config, "fallback_image_url", None), + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return Nonepackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (2)
21-36: Fix Vertex Part detection; prefer inline_data.mime_type and check data presence.As implemented, this will miss valid Parts where mime_type lives under inline_data (common in Vertex). Also retains unnecessary nesting. This causes image parts to be skipped entirely.
def _is_base64_image_part(item): """Check if item is a VertexAI Part object containing image data""" try: - # Check if it has the Part attributes we expect - if not hasattr(item, 'inline_data') or not hasattr(item, 'mime_type'): - return False - - # Check if it's an image mime type and has inline data - if item.mime_type and 'image/' in item.mime_type and item.inline_data: - # Check if the inline_data has actual data - if hasattr(item.inline_data, 'data') and item.inline_data.data: - return True - - return False + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) or getattr(item, "mime_type", None) + has_data = hasattr(inline, "data") and bool(inline.data) + return bool(mime and mime.startswith("image/") and has_data) except Exception: return FalseIf you want to restrict formats to PNG, JPEG/JPG, GIF, WebP as per PR goal, add this near the top of the module (optional) and gate on it:
ALLOWED_IMAGE_MIME_TYPES = {"image/png", "image/jpeg", "image/jpg", "image/webp", "image/gif"}and then:
return bool(mime in ALLOWED_IMAGE_MIME_TYPES and has_data)
45-55: Normalize IDs to strings and use robust mime extraction.Prefer inline_data.mime_type, split defensively, and pass string IDs to the uploader to match its signature.
- # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') - image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' + # Extract format from mime type (prefer inline_data.mime_type) + mime = ( + getattr(getattr(item, "inline_data", None), "mime_type", None) + or getattr(item, "mime_type", None) + ) + image_format = mime.split('/')[1] if (mime and '/' in mime) else 'unknown' image_name = f"content_{content_index}.{image_format}" @@ - # Upload the base64 data - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) + # Upload the base64 data + tid, sid = str(trace_id), str(span_id) + url = await Config.upload_base64_image(tid, sid, image_name, base64_string)
🧹 Nitpick comments (13)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (3)
26-41: Tighten and simplify image-part detection; flatten nested conditionalsFlatten the nested checks per SIM102 and avoid attribute chains that can raise if intermediates are None. Optionally restrict to the supported set of mime types.
-def _is_image_part(item): - """Check if item is a Google GenAI Part object containing image data""" - try: - # Check if it has the Part attributes we expect for new Google GenAI SDK - if hasattr(item, 'inline_data') and item.inline_data is not None: - # Check if it's an image mime type and has data - if (hasattr(item.inline_data, 'mime_type') and - item.inline_data.mime_type and - 'image/' in item.inline_data.mime_type and - hasattr(item.inline_data, 'data') and - item.inline_data.data): - return True - return False - except Exception: - return False +def _is_image_part(item): + """Check if item is a Google GenAI Part object containing image data""" + try: + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) + data = getattr(inline, "data", None) + return bool(inline and mime and mime.startswith("image/") and data) + except Exception: + return FalseIf you want to strictly align with “PNG/JPEG/GIF/WebP only”, you can add:
ALLOWED_MIME = {"image/png","image/jpeg","image/jpg","image/gif","image/webp"} return bool(inline and mime in ALLOWED_MIME and data)
43-69: Reduce duplication between async and sync image-processing pathsThe async/sync functions duplicate extraction, naming, encoding, and return-shape logic. Factor out common pieces to reduce maintenance risk.
Sketch:
def _extract_image_name_and_b64(item, content_index): image_format = item.inline_data.mime_type.split("/")[1] if item.inline_data.mime_type else "unknown" image_name = f"content_{content_index}.{image_format}" base64_string = base64.b64encode(item.inline_data.data).decode("utf-8") return image_name, base64_string def _as_image_url(url): return {"type": "image_url", "image_url": {"url": url}}Then each path only differs at the “upload” line (await vs sync or fallback).
Also applies to: 71-96
48-56: Minor: avoid repeating base64 encode/name creation logic; precompute oncePrecompute
image_nameandbase64_stringvia a helper to keep both paths in lockstep and reduce drift.Also applies to: 76-84
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (10)
14-18: Simplify guard; avoid nested if (Ruff SIM102).Current nested checks are equivalent to a single guard; keeps intent while reducing branches.
def _set_span_attribute(span, name, value): - if value is not None: - if value != "": - span.set_attribute(name, value) - return + if value is None or value == "": + return + span.set_attribute(name, value)
61-64: Add a graceful fallback URL when upload fails (PR objective).The PR summary mentions “graceful fallback URLs when uploads fail,” but this path currently returns None, dropping the image. Consider returning a small, deterministic placeholder URL (e.g., an app-handled openllmetry: scheme) or a configurable fallback.
For example:
- # Return None to skip adding this image to the span - return None + # Graceful fallback: keep content shape even if upload fails + fallback = getattr(Config, "fallback_image_url", None) + if callable(fallback): + try: + return {"type": "image_url", "image_url": {"url": fallback()}} + except Exception: + logger.debug("fallback_image_url failed; skipping image.") + return NoneWould you like me to wire a simple
Config.fallback_image_urlwith a default like "openllmetry:upload_failed"?
101-107: Set explicit role for prompt entries ("user") to align with semconv usage.Adds parity with other instrumentations that emit role alongside content.
for i, arg in enumerate(args): prefix = f"{SpanAttributes.LLM_PROMPTS}.{i}" + _set_span_attribute(span, f"{prefix}.role", "user") @@ for i, arg in enumerate(args): prefix = f"{SpanAttributes.LLM_PROMPTS}.{i}" + _set_span_attribute(span, f"{prefix}.role", "user")Also applies to: 155-161
108-114: Avoid unnecessary deepcopy; iterate read-only and drop import.Deep copying Part objects is costly and may fail. We don't mutate; iterate directly. Then remove the copy import.
- # List of mixed content (text strings and Part objects) - deep copy and process - content = copy.deepcopy(arg) - - # Process each item in the content list using OpenAI-compatible format - processed_content = [] - for j, item in enumerate(content): + # List of mixed content (text strings and Part objects) - process read-only + processed_content = [] + for j, item in enumerate(arg):- # List of mixed content (text strings and Part objects) - deep copy and process - content = copy.deepcopy(arg) - - # Process each item in the content list using OpenAI-compatible format - processed_content = [] - for j, item in enumerate(content): + # List of mixed content (text strings and Part objects) - process read-only + processed_content = [] + for j, item in enumerate(arg):Outside-range cleanup:
-import copyAlso applies to: 162-167, 1-1
118-124: Defensive: skip empty/failed image uploads before appending.Ensure we don't append placeholders when uploader returns falsy URL.
- if processed_item is not None: + if processed_item and processed_item.get("image_url", {}).get("url"): processed_content.append(processed_item)Apply similarly in the sync branch.
Also applies to: 188-195
39-60: Optional: validate image mime types against allowed set.If strict validation is desired, reject unsupported images early to protect attributes from unexpected content types.
async def _process_image_part(item, trace_id, span_id, content_index): @@ - # Convert binary data to base64 string for upload + # Validate mime type if policy is configured + allowed = globals().get("ALLOWED_IMAGE_MIME_TYPES") + if allowed is not None and mime not in allowed: + logger.debug("Skipping image with unsupported mime type: %s", mime) + return None + + # Convert binary data to base64 string for upload binary_data = item.inline_data.dataRepeat the same check in the sync variant.
Also applies to: 67-92
39-60: Telemetry hygiene: avoid embedding large data URLs in attributes.If you implement a fallback using data: URLs, cap base64 length (e.g., 8–16KB) to respect typical exporter limits and avoid bloating spans.
I can add a small helper to truncate base64 for any data-URL fallback and document the attribute size trade-offs.
Also applies to: 67-92
41-43: Early-exit condition should also handle empty uploader callable.Guard for a misconfigured Config where upload_base64_image is set but falsy/non-callable.
- if not Config.upload_base64_image: + if not getattr(Config, "upload_base64_image", None): return NoneAlso applies to: 69-71
95-145: End-to-end check: inputs with only images can yield empty content arrays.If every image fails to upload, we set an empty JSON list. Consider skipping attribute in that case to reduce noise.
- _set_span_attribute(span, f"{prefix}.content", json.dumps(processed_content)) + if processed_content: + _set_span_attribute(span, f"{prefix}.content", json.dumps(processed_content))Apply in both async and sync paths.
Also applies to: 149-199
39-60: Unit-test request: add coverage for inline_data mime vs. item mime, and sync path in running loop.These code paths are fragile across SDK versions. Add tests to lock behavior.
I can contribute tests that:
- Assert _is_base64_image_part accepts Parts where mime lives on inline_data.
- Verify async uploader is called with str IDs.
- Validate sync path returns None (or placeholder) when a loop is already running.
Would you like me to draft these?
Also applies to: 67-92
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py(2 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules
Files:
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
🧬 Code graph analysis (2)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py (2)
dont_throw(17-39)should_send_prompts(11-14)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
SpanAttributes(64-261)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (5)
_set_span_attribute(19-23)_process_image_part(43-68)_process_image_part_sync(71-95)set_input_attributes(99-209)set_input_attributes_sync(214-324)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (3)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/utils.py (2)
dont_throw(13-35)should_send_prompts(38-41)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
Config(4-9)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (4)
_set_span_attribute(14-18)_process_image_part(39-64)_process_image_part_sync(67-91)set_input_attributes(95-144)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
15-16: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
29-31: Use a single if statement instead of nested if statements
(SIM102)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
20-21: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
30-36: Use a single if statement instead of nested if statements
(SIM102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Test Packages (3.12)
- GitHub Check: Test Packages (3.11)
- GitHub Check: Test Packages (3.10)
- GitHub Check: Build Packages (3.11)
- GitHub Check: Lint
| except Exception as e: | ||
| logger.warning(f"Failed to process image part: {e}") | ||
| # Return None to skip adding this image to the span | ||
| return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Return a configurable fallback image URL on upload failure (aligns with PR intent and prior review)
On failures you currently return None, which drops the image entirely. PR text claims “graceful fallback URLs when uploads fail.” Use configurable fallbacks instead of dropping the item.
except Exception as e:
logger.warning(f"Failed to process image part: {e}")
- # Return None to skip adding this image to the span
- return None
+ # Graceful fallback
+ fallback_url = getattr(
+ Config,
+ "fallback_async_image_url",
+ getattr(Config, "fallback_image_url", None),
+ )
+ if fallback_url:
+ return {"type": "image_url", "image_url": {"url": fallback_url}}
+ return None except Exception as e:
logger.warning(f"Failed to process image part sync: {e}")
- # Return None to skip adding this image to the span
- return None
+ # Graceful fallback
+ fallback_url = getattr(
+ Config,
+ "fallback_sync_image_url",
+ getattr(Config, "fallback_image_url", None),
+ )
+ if fallback_url:
+ return {"type": "image_url", "image_url": {"url": fallback_url}}
+ return NoneNote: This keeps fallback configuration centralized in Config (avoids hard-coded URLs). It also addresses the earlier feedback about making fallbacks configurable rather than hard-coded.
Also applies to: 92-95
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
around lines 65-68 (and similarly lines 92-95) the current except blocks return
None which drops the image; update them to return a configurable fallback image
URL from the project's Config instead of None. Replace the None return with
something like Config.get().fallback_image_url (or the appropriate Config
accessor used elsewhere), ensure Config has a fallback_image_url entry (with
sensible default), and add a brief debug log noting the fallback was used so the
span still contains an image URL when upload processing fails.
| @dont_throw | ||
| def set_input_attributes(span, args, kwargs, llm_model): | ||
| async def set_input_attributes(span, args, kwargs, llm_model): | ||
| """Process input arguments, handling both text and image content""" | ||
| if not span.is_recording(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dont_throw wraps an async function incorrectly — exceptions inside the coroutine won’t be caught
The current dont_throw decorator is synchronous and won’t catch exceptions raised inside async def set_input_attributes(...). This silently disables the intended error shielding/logging for this async path.
Two viable fixes (prefer A for reuse):
- A) Make the decorator support coroutine functions globally (utils.py). Keep the decorator here unchanged.
- B) Remove the decorator from the async function and handle try/except inside the function body.
A) Update decorator (utils.py), then keep using @dont_throw here:
# utils.py
import functools, inspect, logging, traceback
def dont_throw(func):
logger = logging.getLogger(func.__module__)
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return async_wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return wrapperB) Alternatively, drop the decorator here and wrap the function body with try/except.
🤖 Prompt for AI Agents
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
around lines 98-101: the dont_throw decorator is synchronous so exceptions
raised inside this async function won’t be caught; fix by updating dont_throw in
utils.py to detect coroutine functions (inspect.iscoroutinefunction) and return
an async wrapper that awaits the function and catches/logs exceptions
(preferred, then keep @dont_throw here), or as an alternative remove the
decorator here and add an internal try/except in set_input_attributes to
catch/log exceptions.
| async def set_input_attributes(span, args, kwargs, llm_model): | ||
| """Process input arguments, handling both text and image content""" | ||
| if not span.is_recording(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Signature change to async: audit call sites to ensure proper awaiting or use of the sync wrapper
set_input_attributes is now async. Ensure instrumentors call await set_input_attributes(...) or use set_input_attributes_sync(...) in synchronous code paths.
Run:
Expected: All real invocations in async contexts are awaited; sync contexts use set_input_attributes_sync.
Also applies to: 212-216
🏁 Script executed:
#!/bin/bash
# Find all call sites
rg -nP --type=py -C2 '\bset_input_attributes\s*\('
echo
echo "Calls that are awaited:"
rg -nP --type=py -C2 'await\s+set_input_attributes\s*\('
echo
echo "Suspicious un-awaited calls (may be false positives if they call the _sync version):"
rg -nP --type=py -C2 '(?<!await\s)\bset_input_attributes\s*\(' | rg -v '_sync\s*\('Length of output: 17620
🏁 Script executed:
#!/bin/bash
# Display the head of the async span_utils implementation
sed -n '1,200p' packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
echo
echo "— Sync wrapper definitions in span_utils.py —"
rg -nP --type=py '_sync' -n packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
echo
echo "— Calls to async set_input_attributes in __init__.py —"
rg -nP --type=py -C2 '\bset_input_attributes\s*\(' packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py
echo
echo "— Calls to sync set_input_attributes_sync in __init__.py —"
rg -nP --type=py -C2 '\bset_input_attributes_sync\s*\(' packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.pyLength of output: 9700
🏁 Script executed:
#!/bin/bash
# Show the first 200 lines of the google_generativeai instrumentation __init__.py for context
sed -n '1,200p' packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.pyLength of output: 5948
Fix async wrapper to use the async set_input_attributes instead of the sync shim
The async instrumentation in
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py
still calls the synchronous helper (_handle_request → set_input_attributes_sync), which invokes asyncio.run(...) inside an already-running event loop. In a live async context this will raise RuntimeError: asyncio.run() cannot be called from a running event loop, breaking prompt-attribute recording.
• Location: async wrapper _awrap (around line 155)
• Current code:
@_with_tracer_wrapper
async def _awrap(...):
...
_handle_request(span, args, kwargs, llm_model, event_logger)
response = await wrapped(*args, **kwargs)
...• Proposed change: call the async setter directly (and preserve the dont_throw behavior):
- _handle_request(span, args, kwargs, llm_model, event_logger)
+ # use async setter in async contexts
+ try:
+ if should_emit_events() and event_logger:
+ emit_message_events(args, kwargs, event_logger)
+ else:
+ await set_input_attributes(span, args, kwargs, llm_model)
+ except Exception:
+ # swallow per @dont_throw contract
+ pass
response = await wrapped(*args, **kwargs)• Remove or deprecate _handle_request for async use, or split into separate sync/async handlers.
• Verify the sync wrapper (_handle_request → set_input_attributes_sync) remains used in the sync _wrap.
This change ensures that in async flows we properly await set_input_attributes, and only use the sync shim in purely synchronous instrumentation.
| async def _process_image_part(item, trace_id, span_id, content_index): | ||
| """Process a VertexAI Part object containing image data""" | ||
| if not Config.upload_base64_image: | ||
| return None | ||
|
|
||
| try: | ||
| # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') | ||
| image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' | ||
| image_name = f"content_{content_index}.{image_format}" | ||
|
|
||
| # Convert binary data to base64 string for upload | ||
| binary_data = item.inline_data.data | ||
| base64_string = base64.b64encode(binary_data).decode('utf-8') | ||
|
|
||
| # Upload the base64 data | ||
| url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) | ||
|
|
||
| # Return OpenAI-compatible format for consistency across LLM providers | ||
| return { | ||
| "type": "image_url", | ||
| "image_url": {"url": url} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Repository-wide verification script for uploader signatures and usages.
Confirm all uploaders expect string IDs and identify any sync implementations.
Also applies to: 67-92
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Config definitions of uploaders:"
rg -nP 'class\s+Config\b.*\n(?:.|\n)*?upload_base64_image' -C2 packages
echo
echo "All references to upload_base64_image/base64_image_uploader:"
rg -n --hidden -S 'upload_base64_image|base64_image_uploader' -C2 packages
echo
echo "Check async definitions (expect async def for uploaders unless explicitly sync):"
rg -nP 'async\s+def\s+\w*upload_base64_image' -C1 packages || trueLength of output: 168
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Inspect span_utils.py imports (to locate Config source):"
sed -n '1,30p' packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
echo
echo "Search for Config class definitions repository-wide:"
rg -nP 'class\s+Config\b' -C2 .
echo
echo "Locate any definitions or assignments of upload_base64_image:"
rg -n --hidden -g '*.py' 'upload_base64_image' -C3Length of output: 46405
Standardize and Correct upload_base64_image Defaults and Signatures
The default stubs in several instrumentation packages are inconsistent with their async usage and will break at runtime. We need to align signatures, defaults, and type annotations so that:
- By default, no uploader is configured (so code early-returns rather than trying to
awaita non-awaitable). - When provided, the uploader is always an async function returning
str. - Sync wrappers only call truly async uploaders via
asyncio.run.
Please apply the following critical fixes:
• In packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py
- Change
to
upload_base64_image: Callable[[str, str, str, str], str] = ( lambda trace_id, span_id, image_name, base64_string: str )
from typing import Optional, Callable, Awaitable upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Update import for
Awaitable.
• In packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
- Before awaiting, guard on
if Config.upload_base64_image is None: return None. - Remove any assumptions about default lambdas returning
str.
• In packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (and similarly in other packages like Google-GenAI)
- Ensure the default
upload_base64_imagematches the four-argument async signature:from typing import Optional, Callable, Awaitable upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Align any sync wrappers to call
asyncio.runonly ifupload_base64_imageis set.
• Audit all instrumentor constructors (__init__) across instrumentations to confirm they accept upload_base64_image: Optional[Callable[..., Awaitable[str]]] and assign it directly to Config.upload_base64_image.
These changes will prevent runtime TypeError when awaiting a non-awaitable and ensure consistent, predictable behavior when no uploader is supplied.
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
around lines 39-60, the code awaits Config.upload_base64_image without
guaranteeing it's an async callable or set; change the logic to early-return
when Config.upload_base64_image is None (i.e., if Config.upload_base64_image is
None: return None) and stop assuming a default lambda exists. Also update the
related config files to declare upload_base64_image as
Optional[Callable[[str,str,str,str], Awaitable[str]]] = None, ensure
instrumentor constructors accept and assign that Optional async callable
directly to Config.upload_base64_image, and make any synchronous wrappers call
upload functions via asyncio.run only when upload_base64_image is set.
.../opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
Show resolved
Hide resolved
…mage processing Replace custom asyncio.run usage with OpenAI's proven threading approach for handling async image uploads in sync contexts. This prevents event loop conflicts when an existing loop is already running. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caution
Changes requested ❌
Reviewed 009db80 in 1 minute and 32 seconds. Click for details.
- Reviewed
108lines of code in2files - Skipped
0files when reviewing. - Skipped posting
2draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:104
- Draft comment:
Good use of 'nonlocal' to capture the URL result from the async upload task. Confirm that returning a URL of None in failure cases is the intended fallback behavior. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
2. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:68
- Draft comment:
Consider renaming 'method' to 'coro' for clarity and think about extracting run_async into a shared utility to avoid duplicating this pattern across instrumentations. - Reason this comment was not posted:
Marked as duplicate.
Workflow ID: wflow_pjFOaLAuPzPhJDUi
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
| return None | ||
|
|
||
|
|
||
| def run_async(method): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider renaming the 'method' parameter to 'coro' (or similar) to clarify that a coroutine is expected. Also, note that any exceptions inside the threaded asyncio.run may be lost; adding explicit exception handling could improve debuggability.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (6)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (3)
44-69: Normalize IDs to strings and use configurable fallbacks for missing/failed uploads (async path)Two issues:
- OTel IDs are often ints; normalize to str before handing to uploaders. This was raised earlier.
- When uploads are disabled or fail, return a configurable fallback URL instead of dropping the image. This matches the PR’s “graceful fallback” promise and prior feedback.
async def _process_image_part(item, trace_id, span_id, content_index): """Process a Google GenAI Part object containing image data""" - if not Config.upload_base64_image: - return None + if not Config.upload_base64_image: + fallback_url = getattr( + Config, "fallback_async_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None try: # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') image_format = item.inline_data.mime_type.split('/')[1] if item.inline_data.mime_type else 'unknown' image_name = f"content_{content_index}.{image_format}" # Convert binary data to base64 string for upload binary_data = item.inline_data.data base64_string = base64.b64encode(binary_data).decode('utf-8') # Upload the base64 data - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) + # Normalize IDs to strings (OTel may provide ints) + tid, sid = str(trace_id), str(span_id) + url = await Config.upload_base64_image(tid, sid, image_name, base64_string) + if not url: + fallback_url = getattr( + Config, "fallback_async_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None # Return OpenAI-compatible format for consistency across LLM providers return { "type": "image_url", "image_url": {"url": url} } except Exception as e: - logger.warning(f"Failed to process image part: {e}") - # Return None to skip adding this image to the span - return None + logger.warning("Failed to process image part; using fallback if configured: %s", e) + fallback_url = getattr( + Config, "fallback_async_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None
87-118: Sync image processing: use run_async return value, normalize IDs, and honor fallbacksCurrently, exceptions in the spawned thread aren’t surfaced and may yield
url=None. Also, IDs aren’t normalized and missing/failed uploads don’t use fallbacks.def _process_image_part_sync(item, trace_id, span_id, content_index): """Synchronous version of image part processing using OpenAI's pattern""" - if not Config.upload_base64_image: - return None + if not Config.upload_base64_image: + fallback_url = getattr( + Config, "fallback_sync_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None try: # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') image_format = item.inline_data.mime_type.split('/')[1] if item.inline_data.mime_type else 'unknown' image_name = f"content_{content_index}.{image_format}" # Convert binary data to base64 string for upload binary_data = item.inline_data.data base64_string = base64.b64encode(binary_data).decode('utf-8') - # Use OpenAI's run_async pattern to handle the async upload function - url = None - - async def upload_task(): - nonlocal url - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) - - run_async(upload_task()) + # Normalize IDs to strings and run async uploader from sync code + tid, sid = str(trace_id), str(span_id) + async def upload_task(): + return await Config.upload_base64_image(tid, sid, image_name, base64_string) + url = run_async(upload_task()) - return { + if not url: + fallback_url = getattr( + Config, "fallback_sync_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None + + return { "type": "image_url", "image_url": {"url": url} } except Exception as e: - logger.warning(f"Failed to process image part sync: {e}") - # Return None to skip adding this image to the span - return None + logger.warning("Failed to process image part sync; using fallback if configured: %s", e) + fallback_url = getattr( + Config, "fallback_sync_image_url", getattr(Config, "fallback_image_url", None) + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None
120-123: dont_throw likely won’t catch exceptions in async function — update decorator or handle locallyPer prior review, the decorator is synchronous and won’t catch exceptions inside this
async def. Either upgradedont_throwto wrap coroutine functions or remove the decorator here and add a try/except in the function.Option A (preferred): update the decorator (in utils.py) to detect coroutine functions and return an async wrapper using
inspect.iscoroutinefunctionandawait(see earlier suggestion).Option B (local change here): drop
@dont_throwand wrap the body in try/except.Would you like me to generate a focused patch for Option B in this file, or a utils.py patch for Option A?
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (3)
83-109: Sync upload: robust MIME parsing, normalize IDs to strings, and support both async and sync uploadersMirror the async improvements and allow a synchronous uploader without forcing a thread-hopping coroutine.
- try: - # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') - image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' + try: + # Extract format from mime type (prefer inline_data.mime_type) + mime = ( + getattr(getattr(item, "inline_data", None), "mime_type", None) + or getattr(item, "mime_type", None) + ) or "" + image_format = mime.split("/", 1)[1].lower() if "/" in mime else "unknown" image_name = f"content_{content_index}.{image_format}" @@ - # Use OpenAI's run_async pattern to handle the async upload function - url = None - - async def upload_task(): - nonlocal url - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) - - run_async(upload_task()) + # Support both async and sync uploaders + uploader = Config.upload_base64_image + tid, sid = str(trace_id), str(span_id) + if asyncio.iscoroutinefunction(uploader): + url = run_async(uploader(tid, sid, image_name, base64_string)) + else: + url = uploader(tid, sid, image_name, base64_string)
22-37: Fix VertexAI Part image detection: prefer inline_data.mime_type and simplify checksCurrent logic relies on item.mime_type and nested conditions, which misses the common inline_data.mime_type path and can fail to detect valid image Parts.
-def _is_base64_image_part(item): - """Check if item is a VertexAI Part object containing image data""" - try: - # Check if it has the Part attributes we expect - if not hasattr(item, 'inline_data') or not hasattr(item, 'mime_type'): - return False - - # Check if it's an image mime type and has inline data - if item.mime_type and 'image/' in item.mime_type and item.inline_data: - # Check if the inline_data has actual data - if hasattr(item.inline_data, 'data') and item.inline_data.data: - return True - - return False - except Exception: - return False +def _is_base64_image_part(item): + """Return True when `item` is a VertexAI Part with inline image bytes.""" + try: + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) or getattr(item, "mime_type", None) + data = getattr(inline, "data", None) + return bool(mime and mime.startswith("image/") and data) + except Exception: + return False
45-61: Async upload: robust MIME parsing and normalize IDs to strings; support sync uploaders via to_threadUse inline_data.mime_type when present, split safely, and cast trace/span IDs to strings. If a synchronous uploader is configured, offload it with asyncio.to_thread to avoid blocking the loop.
- # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') - image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' + # Extract format from mime type (prefer inline_data.mime_type) + mime = ( + getattr(getattr(item, "inline_data", None), "mime_type", None) + or getattr(item, "mime_type", None) + ) or "" + image_format = mime.split("/", 1)[1].lower() if "/" in mime else "unknown" image_name = f"content_{content_index}.{image_format}" @@ - # Upload the base64 data - url = await Config.upload_base64_image(trace_id, span_id, image_name, base64_string) + # Upload the base64 data (support async or sync uploader) + uploader = Config.upload_base64_image + tid, sid = str(trace_id), str(span_id) + if asyncio.iscoroutinefunction(uploader): + url = await uploader(tid, sid, image_name, base64_string) + else: + url = await asyncio.to_thread(uploader, tid, sid, image_name, base64_string)
🧹 Nitpick comments (6)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (6)
62-66: Log full traceback on upload failureUse logger.exception to retain stack traces for support/debuggability.
- except Exception as e: - logger.warning(f"Failed to process image part: {e}") - # Return None to skip adding this image to the span - return None + except Exception: + logger.exception("Failed to process image part") + return None
68-81: Return the coroutine result from run_asyncReturning the result simplifies callers and avoids reliance on outer nonlocal variables.
-def run_async(method): - """Handle async method in sync context, following OpenAI's battle-tested approach""" +def run_async(coro): + """Run an async coroutine from sync code and return its result.""" try: loop = asyncio.get_running_loop() except RuntimeError: loop = None - if loop and loop.is_running(): - thread = threading.Thread(target=lambda: asyncio.run(method)) + if loop and loop.is_running(): + result = None + def _runner(): + nonlocal result + result = asyncio.run(coro) + thread = threading.Thread(target=_runner) thread.start() thread.join() - else: - asyncio.run(method) + return result + else: + return asyncio.run(coro)
110-113: Log full traceback on sync upload failureSame rationale as async path.
- except Exception as e: - logger.warning(f"Failed to process image part sync: {e}") - # Return None to skip adding this image to the span - return None + except Exception: + logger.exception("Failed to process image part sync") + return None
130-136: Avoid deepcopy of content listsDeep copying provider objects is unnecessary (we don’t mutate items), can be expensive, and may fail on non-serializable Parts. Iterate the original list.
- # List of mixed content (text strings and Part objects) - deep copy and process - content = copy.deepcopy(arg) + # List of mixed content (text strings and Part objects) + content = argAlso remove the now-unused import:
-import copyAlso applies to: 187-193
15-19: Simplify nested conditionals per Ruff SIM102No functional change; reduces nesting.
-def _set_span_attribute(span, name, value): - if value is not None: - if value != "": - span.set_attribute(name, value) - return +def _set_span_attribute(span, name, value): + if value is not None and value != "": + span.set_attribute(name, value)
62-66: Confirm fallback behavior when uploads failPR objective mentions “graceful fallback URLs when uploads fail,” but both paths return None on failure, which drops the image entirely. If fallback is expected at this layer, consider a configurable placeholder (e.g., Config.fallback_image_url) or a data: URL stub to preserve structure.
Also applies to: 110-113
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py(2 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules
Files:
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
16-17: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
30-32: Use a single if statement instead of nested if statements
(SIM102)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
21-22: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
31-37: Use a single if statement instead of nested if statements
(SIM102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Test Packages (3.11)
- GitHub Check: Test Packages (3.12)
- GitHub Check: Test Packages (3.10)
- GitHub Check: Build Packages (3.11)
- GitHub Check: Lint
🔇 Additional comments (5)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (5)
148-174: Mixed-content processing (text + images) looks correct and resilientAccurately iterates
content.parts, preserves text, processes images, and stringifies other parts. Good use of role defaulting later.
176-186: Span attribute construction for per-item JSON payloads is coherentUsing JSON with explicit types aligns with your OpenAI-compatible shape and keeps attributes uniform across providers.
187-226: Args-path handling mirrors contents-path semanticsSymmetry between
argsandcontentshandling is good; indices are stable and image parts go through the same pipeline.
226-232: Prompt kwarg path is straightforward and consistentSingle-string prompt is correctly wrapped into the JSON content format and tagged with role "user".
234-341: Sync and async attribute setters correctly separatedI’ve confirmed that:
- All async wrappers invoke the async setter with
await set_input_attributes(span, args)(e.g. invertexai/__init__.pyat line 187).- All sync wrappers call the sync setter
set_input_attributes_sync(span, …)(e.g. invertexai/__init__.pyat line 287 and ingoogle_generativeai/__init__.pyat line 106).The sync version remains available for backward compatibility, and no async contexts call the sync setter—avoiding nested event‐loop issues.
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Show resolved
Hide resolved
Remove unused imports, fix whitespace issues, and ensure proper newlines at end of files for both Google GenAI and Vertex AI image sample apps. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed ef1b148 in 34 seconds. Click for details.
- Reviewed
228lines of code in2files - Skipped
0files when reviewing. - Skipped posting
2draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/sample-app/sample_app/google_genai_image_example.py:35
- Draft comment:
Consider using Python's built-in 'mimetypes' module for determining the image's MIME type instead of manual string checks. This approach improves maintainability and handles more extensions automatically. - Reason this comment was not posted:
Confidence changes required:33%<= threshold50%None
2. packages/sample-app/sample_app/vertex_gemini_vision_example.py:4
- Draft comment:
Good cleanup: the unnecessary import 'from google.cloud import storage' has been removed. - Reason this comment was not posted:
Confidence changes required:0%<= threshold50%None
Workflow ID: wflow_3cVldB6yspUDybb1
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
avivhalfon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easy to get lost in all of this else if, its very hard to read
I think there is a place to clean a bit, DRY some key logic and separate to methods.
Also the bots comments seems relevance.
Let me know after the changes and I'll hope on again to dive in closer
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Outdated
Show resolved
Hide resolved
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Outdated
Show resolved
Hide resolved
...entation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
Show resolved
Hide resolved
…vements 1. Fix trace_id/span_id type conversion across all instrumentations - Convert OpenTelemetry integer IDs to strings before upload calls - Standardize all signatures to use 4-parameter format consistently - Fix OpenAI config signature mismatch (was expecting 3 params, using 4) 2. Improve code readability (Issue #4 from PR review) - Extract helper methods to reduce complex nested if/else blocks - Use descriptive variable names (arg_index, content_item vs i, arg) - Apply consistent patterns across VertexAI and Google GenAI instrumentations - Maintain both async and sync versions with shared logic 3. Fix VertexAI ChatSession model name extraction - Add support for getting model name from ChatSession._model reference - Ensures send_message spans show correct model instead of "unknown" All changes maintain backward compatibility and pass linting checks. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Important
Looks good to me! 👍
Reviewed 0c80bc9 in 1 minute and 19 seconds. Click for details.
- Reviewed
540lines of code in6files - Skipped
0files when reviewing. - Skipped posting
3draft comments. View those below. - Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py:59
- Draft comment:
Converting trace_id and span_id to strings before invoking the upload hook is a good defensive measure. Please ensure that these IDs are always defined and correctly formatted. - Reason this comment was not posted:
Comment did not seem useful. Confidence is useful =50%<= threshold50%The comment starts with a suggestion about converting IDs to strings, which is a valid code suggestion. However, it then asks the author to ensure that the IDs are always defined and correctly formatted, which violates the rule against asking the author to ensure behavior. The first part of the comment is useful, but the second part is not allowed.
2. packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py:124
- Draft comment:
The newly added helper functions (e.g. _process_vertexai_argument and _process_content_item_vertexai) use deep copies of input lists. Consider evaluating performance impacts when handling large content lists. - Reason this comment was not posted:
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The deep copy behavior isn't new - it was present in the original code and was just moved to helper functions. The comment suggests evaluating performance but doesn't identify a specific issue. Without evidence of an actual performance problem, this seems like speculation. The code comment explicitly states this follows OpenAI's pattern, suggesting it's an intentional design choice. The comment does raise a valid concern about performance with large lists. Deep copying can be expensive for large data structures. While deep copying can be expensive, this is an existing pattern that was just refactored. Without evidence of actual performance issues, suggesting changes would be premature optimization. Delete the comment. It's speculative, doesn't identify a concrete issue, and the deep copy behavior is an intentional pattern that existed before this change.
3. packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py:169
- Draft comment:
The initializer passes 'image_uploader.aupload_base64_image' to init_instrumentations. Verify if 'aupload_base64_image' is intentionally named or if it was meant to be 'upload_base64_image' to match the configured hook signature. - Reason this comment was not posted:
Comment was not on a location in the diff, so it can't be submitted as a review comment.
Workflow ID: wflow_mhZfBYu6CSG1aZB2
You can customize by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
♻️ Duplicate comments (2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)
61-65: Ensure URL is not None and return a configurable fallback on upload failuresToday, if the upload coroutine fails in the background thread (sync path) or raises (async path), we can end up returning
{"image_url":{"url": None}}or drop the image entirely by returningNone. The PR summary promises “graceful fallback URLs when uploads fail”; make this behavior explicit and avoid emitting invalid URLs.Apply these diffs:
@@ - # Return OpenAI-compatible format for consistency across LLM providers - return { - "type": "image_url", - "image_url": {"url": url} - } + # Return OpenAI-compatible format for consistency across LLM providers + if not url: + logger.debug("Image upload returned empty URL; using fallback (async)") + fallback_url = getattr( + Config, "fallback_async_image_url", + getattr(Config, "fallback_image_url", None), + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None + return {"type": "image_url", "image_url": {"url": url}} @@ - except Exception as e: - logger.warning(f"Failed to process image part: {e}") - # Return None to skip adding this image to the span - return None + except Exception as e: + logger.warning(f"Failed to process image part: {e}") + # Graceful fallback + fallback_url = getattr( + Config, "fallback_async_image_url", + getattr(Config, "fallback_image_url", None), + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None @@ - return { - "type": "image_url", - "image_url": {"url": url} - } + if not url: + logger.debug("Image upload returned empty URL; using fallback (sync)") + fallback_url = getattr( + Config, "fallback_sync_image_url", + getattr(Config, "fallback_image_url", None), + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return None + return {"type": "image_url", "image_url": {"url": url}} @@ - except Exception as e: - logger.warning(f"Failed to process image part sync: {e}") - # Return None to skip adding this image to the span - return None + except Exception as e: + logger.warning(f"Failed to process image part sync: {e}") + # Graceful fallback + fallback_url = getattr( + Config, "fallback_sync_image_url", + getattr(Config, "fallback_image_url", None), + ) + if fallback_url: + return {"type": "image_url", "image_url": {"url": fallback_url}} + return NoneNote: You may want to add the optional
fallback_*_image_urlfields to the relevant Config class (or a singlefallback_image_url) if not already present.Also applies to: 110-113, 66-69, 114-117
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (1)
22-37: Fix Vertex Part image detection: prefer inline_data.mime_type and tighten checks.Current logic incorrectly requires item.mime_type on the Part, which will miss images because Vertex places mime_type on inline_data. Also collapse nested ifs. This was raised previously; applying here ensures images are actually detected.
-def _is_base64_image_part(item): - """Check if item is a VertexAI Part object containing image data""" - try: - # Check if it has the Part attributes we expect - if not hasattr(item, 'inline_data') or not hasattr(item, 'mime_type'): - return False - - # Check if it's an image mime type and has inline data - if item.mime_type and 'image/' in item.mime_type and item.inline_data: - # Check if the inline_data has actual data - if hasattr(item.inline_data, 'data') and item.inline_data.data: - return True - - return False - except Exception: - return False +def _is_base64_image_part(item): + """Check if item is a VertexAI Part object containing image data""" + try: + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) or getattr(item, "mime_type", None) + has_data = hasattr(inline, "data") and bool(inline.data) + return bool(mime and mime.startswith("image/") and has_data) + except Exception: + return False
🧹 Nitpick comments (13)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)
27-41: Constrain image detection to supported MIME types and simplify nested conditionalsThe current
_is_image_partaccepts anyimage/*and uses deeply nested checks. Tighten to the formats you intend to support (PNG/JPEG/GIF/WebP) and flatten the predicate for readability and correctness (SIM102).Apply within this range:
-def _is_image_part(item): - """Check if item is a Google GenAI Part object containing image data""" - try: - # Check if it has the Part attributes we expect for new Google GenAI SDK - if hasattr(item, 'inline_data') and item.inline_data is not None: - # Check if it's an image mime type and has data - if (hasattr(item.inline_data, 'mime_type') and - item.inline_data.mime_type and - 'image/' in item.inline_data.mime_type and - hasattr(item.inline_data, 'data') and - item.inline_data.data): - return True - return False - except Exception: - return False +def _is_image_part(item): + """Check if item is a Google GenAI Part object containing supported image data""" + try: + inline = getattr(item, "inline_data", None) + mime = getattr(inline, "mime_type", None) + data = getattr(inline, "data", None) + return bool( + inline + and isinstance(mime, str) + and mime in ALLOWED_IMAGE_MIME_TYPES + and data + ) + except Exception: + return FalseAdd near the logger (outside this hunk) to centralize the allowlist:
# Supported image MIME types (align with PR scope) ALLOWED_IMAGE_MIME_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"}packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (1)
318-326: Minor typo in class docstring“VertextAI” → “VertexAI”.
No functional impact, but worth fixing for polish.
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (4)
424-429: Type hints: uploader is awaited downstream; annotate as awaitableAcross instrumentors,
upload_base64_imageis awaited. Update the type to an awaitable to reflect reality and help static analysis.-def init_instrumentations( - should_enrich_metrics: bool, - base64_image_uploader: Callable[[str, str, str, str], str], +from typing import Awaitable +def init_instrumentations( + should_enrich_metrics: bool, + base64_image_uploader: Callable[[str, str, str, str], Awaitable[str]],Remember to import
Awaitableat the top (with the other typing imports).
561-563: Align OpenAI/Anthropic initializers with awaitable uploader signaturePropagate the awaitable type to these initializers to maintain consistency.
-def init_openai_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] +def init_openai_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], Awaitable[str]] @@ -def init_anthropic_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] +def init_anthropic_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], Awaitable[str]]Also applies to: 585-587
680-682: Gemini initializer: annotate uploader as awaitableSame rationale as above for consistency and tooling.
-def init_google_generativeai_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] +def init_google_generativeai_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], Awaitable[str]] @@ instrumentor = GoogleGenerativeAiInstrumentor( exception_logger=lambda e: Telemetry().log_exception(e), - upload_base64_image=base64_image_uploader, + upload_base64_image=base64_image_uploader, )Also applies to: 690-693
943-945: Vertex initializer: annotate uploader as awaitableMirror the awaitable type here as well.
-def init_vertexai_instrumentor( - should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], str] +def init_vertexai_instrumentor( + should_enrich_metrics: bool, base64_image_uploader: Callable[[str, str, str, str], Awaitable[str]] @@ instrumentor = VertexAIInstrumentor( exception_logger=lambda e: Telemetry().log_exception(e), - upload_base64_image=base64_image_uploader, + upload_base64_image=base64_image_uploader, )Also applies to: 951-954
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (7)
16-18: Consolidate nested conditionals in _set_span_attribute for clarity.Simplify to a single guard; keeps behavior identical and satisfies SIM102.
- if value is not None: - if value != "": - span.set_attribute(name, value) + if value not in (None, ""): + span.set_attribute(name, value)
75-78: Harden run_async: log coroutine exceptions and avoid non-daemon thread.Capture and log exceptions inside the worker; mark the helper thread as daemon to avoid shutdown hangs.
- thread = threading.Thread(target=lambda: asyncio.run(method)) - thread.start() - thread.join() + def _runner(): + try: + asyncio.run(method) + except Exception as e: + logger.debug("run_async coroutine failed: %s", e) + thread = threading.Thread(target=_runner, daemon=True) + thread.start() + thread.join()
122-132: Avoid deepcopy for argument lists; iterate directly or take a shallow copy.We don’t mutate items; deepcopy adds avoidable CPU and memory overhead for large prompt payloads.
- # List of mixed content (text strings and Part objects) - deep copy and process - content_list = copy.deepcopy(argument) + # List of mixed content (text strings and Part objects) - no mutation required + content_list = list(argument)
156-158: Restrict fallback text size to prevent leaking huge object reprs.Serializing arbitrary objects via str(content_item) risks very large attributes and inadvertent PII. Truncate defensively.
- return {"type": "text", "text": str(content_item)} + text = str(content_item) + return {"type": "text", "text": text[:1000]}
167-178: Mirror shallow-copy optimization in sync path.Same rationale as async path: avoid deepcopy.
- # List of mixed content (text strings and Part objects) - deep copy and process - content_list = copy.deepcopy(argument) + # List of mixed content (text strings and Part objects) - no mutation required + content_list = list(argument)
201-203: Truncate fallback text in sync path as well.Keep consistency and avoid oversized attributes.
- return {"type": "text", "text": str(content_item)} + text = str(content_item) + return {"type": "text", "text": text[:1000]}
243-265: Mixed “.content” vs legacy “.user” prompt attributes—confirm intended dual format.You now store JSON content at gen_ai.prompt.{i}.content while still setting gen_ai.prompt.0.user. If both are meant to coexist for back-compat, consider gating legacy fields behind Config.use_legacy_attributes to avoid duplication.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/__init__.py(3 hunks)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py(2 hunks)packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py(1 hunks)packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py(1 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py(7 hunks)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py(1 hunks)packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/init.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Store API keys only in environment variables/secure vaults; never hardcode secrets in code
Use Flake8 for code linting and adhere to its rules
Files:
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.pypackages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.pypackages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.pypackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.pypackages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.pypackages/traceloop-sdk/traceloop/sdk/tracing/tracing.py
🧬 Code graph analysis (6)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (5)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (1)
Config(6-15)packages/opentelemetry-instrumentation-anthropic/opentelemetry/instrumentation/anthropic/config.py (1)
Config(6-13)packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
upload_base64_image(137-138)packages/opentelemetry-instrumentation-anthropic/tests/conftest.py (3)
upload_base64_image(84-85)upload_base64_image(107-108)upload_base64_image(133-134)packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py (1)
upload_base64_image(14-15)
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (2)
packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
upload_base64_image(137-138)packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py (1)
upload_base64_image(14-15)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (4)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/utils.py (2)
dont_throw(13-35)should_send_prompts(38-41)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (1)
Config(4-9)packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
SpanAttributes(64-261)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (7)
_set_span_attribute(15-19)_process_image_part(40-65)run_async(68-80)_process_image_part_sync(83-113)upload_task(100-102)set_input_attributes(207-221)set_input_attributes_sync(226-240)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py (2)
dont_throw(17-39)should_send_prompts(11-14)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)packages/opentelemetry-semantic-conventions-ai/opentelemetry/semconv_ai/__init__.py (1)
SpanAttributes(64-261)packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (4)
_process_image_part(44-69)run_async(72-84)_process_image_part_sync(87-117)upload_task(104-106)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (4)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py (3)
set_input_attributes_sync(226-240)set_model_input_attributes(244-264)set_input_attributes(207-221)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py (1)
should_emit_events(42-43)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/event_emitter.py (1)
emit_prompt_events(64-73)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (1)
Config(4-9)
packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (3)
packages/opentelemetry-instrumentation-openai/tests/conftest.py (1)
upload_base64_image(137-138)packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py (1)
upload_base64_image(14-15)packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (1)
VertexAIInstrumentor(317-366)
🪛 Ruff (0.12.2)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py
21-22: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
31-37: Use a single if statement instead of nested if statements
(SIM102)
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
16-17: Use a single if statement instead of nested if statements
Combine if statements using and
(SIM102)
30-32: Use a single if statement instead of nested if statements
(SIM102)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: Test Packages (3.10)
- GitHub Check: Build Packages (3.11)
- GitHub Check: Test Packages (3.12)
- GitHub Check: Test Packages (3.11)
- GitHub Check: Lint
🔇 Additional comments (6)
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/span_utils.py (1)
120-201: Nice modularization of mixed-content handlingThe split into
_process_content_item,_process_content_part, and_process_argumentmakes the logic readable and reusable across async/sync paths. The role handling via_set_prompt_attributesis clean.packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py (1)
432-434: LGTM on stringifying IDs for the uploaderConverting
trace_id/span_idto strings before callingupload_base64_imageavoids implicit type handling by uploaders and aligns with the rest of the PR.packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/__init__.py (3)
181-188: Async request path now awaits prompt attribute population — goodUsing
await set_input_attributes(...)in the non-emitting async path fixes the previous mismatch and avoidsasyncio.run()pitfalls under a running loop.
227-232: Robust LLM model resolution for ChatSessionDeriving
llm_modelfrom the parent model object (instance._model) covers ChatSession-based flows. Good defensive addition.
292-298: Appropriate use of sync setter in the sync wrapperCalling
set_input_attributes_syncfrom the synchronous wrapper keeps the async upload logic out of the sync path and avoids nested loop issues.packages/traceloop-sdk/traceloop/sdk/tracing/tracing.py (1)
169-173: Confirmedaupload_base64_imageis defined and correctly referenced
- The async method is declared at
packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py:17so passingasync def aupload_base64_image(self, trace_id, span_id, image_name, image_file): …image_uploader.aupload_base64_imageis valid.- There is also a sync wrapper at
packages/traceloop-sdk/traceloop/sdk/images/image_uploader.py:14If your intent was to use the synchronous entrypoint, you can passdef upload_base64_image(self, trace_id, span_id, image_name, image_file): asyncio.run(self.aupload_image_file(trace_id, span_id, image_name, image_file))image_uploader.upload_base64_imageinstead.
| def run_async(method): | ||
| """Handle async method in sync context, following OpenAI's battle-tested approach""" | ||
| try: | ||
| loop = asyncio.get_running_loop() | ||
| except RuntimeError: | ||
| loop = None | ||
|
|
||
| if loop and loop.is_running(): | ||
| thread = threading.Thread(target=lambda: asyncio.run(method)) | ||
| thread.start() | ||
| thread.join() | ||
| else: | ||
| asyncio.run(method) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run_async swallows exceptions and does not return results; fix and use the return value
In the sync path, exceptions raised inside the coroutine terminate the thread and are lost; url remains None, and the caller returns an invalid image_url. Make run_async return the coroutine result and propagate exceptions. Then simplify the sync uploader to use the returned value rather than a nonlocal side channel.
Apply this diff:
@@
-def run_async(method):
- """Handle async method in sync context, following OpenAI's battle-tested approach"""
+def run_async(coro):
+ """Run a coroutine from sync code; return its result and propagate exceptions."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
- if loop and loop.is_running():
- thread = threading.Thread(target=lambda: asyncio.run(method))
- thread.start()
- thread.join()
- else:
- asyncio.run(method)
+ if loop and loop.is_running():
+ result = {"value": None, "exc": None}
+
+ def runner():
+ try:
+ result["value"] = asyncio.run(coro)
+ except Exception as e:
+ result["exc"] = e
+
+ thread = threading.Thread(target=runner, daemon=True)
+ thread.start()
+ thread.join()
+ if result["exc"]:
+ raise result["exc"]
+ return result["value"]
+ else:
+ return asyncio.run(coro)
@@
- # Use OpenAI's run_async pattern to handle the async upload function
- url = None
-
- async def upload_task():
- nonlocal url
- url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
-
- run_async(upload_task())
+ # Use run_async to bridge the async upload function in a sync context
+ async def upload_task():
+ return await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
+ url = run_async(upload_task())Also applies to: 101-109
| @dont_throw | ||
| def set_input_attributes(span, args, kwargs, llm_model): | ||
| async def set_input_attributes(span, args, kwargs, llm_model): | ||
| """Process input arguments, handling both text and image content""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dont_throw on an async function won’t catch in-coroutine exceptions
@dont_throw as implemented in utils.py is synchronous and won’t catch exceptions raised inside this async def set_input_attributes(...). This silently bypasses the intended shielding/logging.
Two viable fixes (A preferred):
- A) Update the decorator in
utils.pyto handle coroutine functions. - B) Remove the decorator here and add an internal try/except.
A) Example (utils.py):
import functools, inspect, logging, traceback
def dont_throw(func):
logger = logging.getLogger(func.__module__)
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return async_wrapper
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return wrapperWould you like me to open a follow-up PR to implement (A) and sweep for other async uses of @dont_throw?
| upload_base64_image: Callable[[str, str, str, str], str] = ( | ||
| lambda trace_id, span_id, image_name, base64_string: str | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default uploader must be awaitable or None; current lambda will break awaiting callers
_process_image_item awaits Config.upload_base64_image. Here, the default is a plain lambda returning the type object str, which is truthy and not awaitable. This will cause a TypeError: object str can't be used in 'await' expression when the default is used.
Change the type to Optional coroutine callable and default to None (matches Anthropic config and your call-site guard if not Config.upload_base64_image:). Alternatively, provide an async no-op default.
Apply this diff:
-from typing import Callable, Optional
+from typing import Callable, Optional, Coroutine, Any
@@
-class Config:
+class Config:
@@
- get_common_metrics_attributes: Callable[[], dict] = lambda: {}
- upload_base64_image: Callable[[str, str, str, str], str] = (
- lambda trace_id, span_id, image_name, base64_string: str
- )
+ get_common_metrics_attributes: Callable[[], dict] = lambda: {}
+ # Awaitable uploader; None disables upload (call sites guard on truthiness)
+ upload_base64_image: Optional[
+ Callable[[str, str, str, str], Coroutine[Any, Any, str]]
+ ] = NoneIf you prefer a no-op default instead of None, define:
async def _noop_upload_base64_image(trace_id: str, span_id: str, image_name: str, base64_string: str) -> str:
return ""
# upload_base64_image = _noop_upload_base64_imageBut None is cleaner given your existing guard.
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py
around lines 10-12, the default upload_base64_image is a synchronous lambda
returning the type object `str`, but callers await this value; change its type
to Optional[Callable[[str, str, str, str], Awaitable[str]]] (import Optional and
Awaitable) and set the default to None to match the call-site guard, or
alternatively provide an async no-op function that returns an empty string and
use that as the default.
| async def _process_image_part(item, trace_id, span_id, content_index): | ||
| """Process a VertexAI Part object containing image data""" | ||
| if not Config.upload_base64_image: | ||
| return None | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Uploader contract: ensure Config.upload_base64_image is async Optional and consistently configured.
Both code paths assume an async uploader is set (awaited directly or via run_async). However, Config in this package still declares a synchronous default lambda (and not Optional), which will cause a TypeError at runtime when awaited.
Actions:
- Update packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py to:
- Declare: upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Provide no default lambda; treat absence as “no upload”.
- Audit other providers (Google GenAI, OpenAI, Anthropic) to align on the same four-arg async signature.
- In this file you already guard on a missing uploader; keep that logic.
Script to audit repository-wide consistency:
Also applies to: 83-87
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "Uploader definitions:"
rg -nP --glob '**/*.py' 'upload_base64_image\s*:\s*' -C2
echo; echo "Awaited call sites (should pass 4 args and cast IDs to str):"
rg -nP --glob '**/*.py' 'await\s+Config\.upload_base64_image\(' -C2
echo; echo "Sync call sites (should use run_async or not exist):"
rg -nP --glob '**/*.py' '(?<!await\s)Config\.upload_base64_image\(' -C2Length of output: 15945
Ensure upload_base64_image is declared as an optional async uploader and remove all default sync lambdas
The current defaults for upload_base64_image across several providers use synchronous lambdas (or non-optional types), which will lead to runtime TypeErrors when code paths await them. We need to align on an Optional[Callable[[str, str, str, str], Awaitable[str]]] = None signature everywhere and remove the built-in sync defaults.
Please update the following locations:
-
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/config.py (lines 7–9):
- Change
to
upload_base64_image: Callable[[str, str, str, str], str] = ( lambda trace_id, span_id, image_name, base64_string: str )
upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Change
-
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/init.py (around line 19):
- Replace
with
upload_base64_image: Optional[ Callable[[str, str, str, str], Coroutine[None, None, str]] ] = lambda *args: ""
upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Replace
-
packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py (lines 10–12):
- Replace the sync default
with
upload_base64_image: Callable[[str, str, str, str], str] = ( lambda trace_id, span_id, image_name, base64_string: str )
upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Replace the sync default
-
packages/opentelemetry-instrumentation-google-generativeai/opentelemetry/instrumentation/google_generativeai/config.py (lines 7–9):
- Change
to
upload_base64_image: Callable[[str, str, str, str], str] = ( lambda trace_id, span_id, image_name, base64_string: str )
upload_base64_image: Optional[Callable[[str, str, str, str], Awaitable[str]]] = None
- Change
The Anthropic instrumentation already uses Optional[..., Coroutine[..., str]] = None and requires no change. All existing call sites correctly guard on if Config.upload_base64_image and either await or use run_async, so no updates there.
| try: | ||
| # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') | ||
| image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' | ||
| image_name = f"content_{content_index}.{image_format}" | ||
|
|
||
| # Convert binary data to base64 string for upload | ||
| binary_data = item.inline_data.data | ||
| base64_string = base64.b64encode(binary_data).decode('utf-8') | ||
|
|
||
| # Upload the base64 data - convert IDs to strings | ||
| url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) | ||
|
|
||
| # Return OpenAI-compatible format for consistency across LLM providers | ||
| return { | ||
| "type": "image_url", | ||
| "image_url": {"url": url} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use inline_data.mime_type, validate binary data, and guard against empty upload URL.
Relying on item.mime_type will break with Vertex Parts. Also ensure we skip if no binary data or uploader returns a falsey URL.
- # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg')
- image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown'
- image_name = f"content_{content_index}.{image_format}"
+ # Extract format from mime type (prefer inline_data.mime_type)
+ mime = (
+ getattr(getattr(item, "inline_data", None), "mime_type", None)
+ or getattr(item, "mime_type", None)
+ )
+ image_format = mime.split("/")[1] if (mime and "/" in mime) else "unknown"
+ image_name = f"content_{content_index}.{image_format}"
@@
- binary_data = item.inline_data.data
- base64_string = base64.b64encode(binary_data).decode('utf-8')
+ inline = getattr(item, "inline_data", None)
+ binary_data = getattr(inline, "data", None)
+ if not binary_data:
+ return None
+ base64_string = base64.b64encode(binary_data).decode("utf-8")
@@
- url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
-
- # Return OpenAI-compatible format for consistency across LLM providers
- return {
- "type": "image_url",
- "image_url": {"url": url}
- }
+ url = await Config.upload_base64_image(
+ str(trace_id), str(span_id), image_name, base64_string
+ )
+ if not url:
+ return None
+ # Return OpenAI-compatible format for consistency across LLM providers
+ return {"type": "image_url", "image_url": {"url": url}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') | |
| image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' | |
| image_name = f"content_{content_index}.{image_format}" | |
| # Convert binary data to base64 string for upload | |
| binary_data = item.inline_data.data | |
| base64_string = base64.b64encode(binary_data).decode('utf-8') | |
| # Upload the base64 data - convert IDs to strings | |
| url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) | |
| # Return OpenAI-compatible format for consistency across LLM providers | |
| return { | |
| "type": "image_url", | |
| "image_url": {"url": url} | |
| } | |
| try: | |
| # Extract format from mime type (prefer inline_data.mime_type) | |
| mime = ( | |
| getattr(getattr(item, "inline_data", None), "mime_type", None) | |
| or getattr(item, "mime_type", None) | |
| ) | |
| image_format = mime.split("/") [1] if (mime and "/" in mime) else "unknown" | |
| image_name = f"content_{content_index}.{image_format}" | |
| # Convert binary data to base64 string for upload, skipping if missing | |
| inline = getattr(item, "inline_data", None) | |
| binary_data = getattr(inline, "data", None) | |
| if not binary_data: | |
| return None | |
| base64_string = base64.b64encode(binary_data).decode("utf-8") | |
| # Upload the base64 data - convert IDs to strings, skip if uploader returns empty URL | |
| url = await Config.upload_base64_image( | |
| str(trace_id), str(span_id), image_name, base64_string | |
| ) | |
| if not url: | |
| return None | |
| # Return OpenAI-compatible format for consistency across LLM providers | |
| return {"type": "image_url", "image_url": {"url": url}} |
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
around lines 45 to 61, change the logic to read the MIME type from
item.inline_data.mime_type (not item.mime_type), validate that inline_data.data
exists and is non-empty bytes before encoding/attempting upload, and after
calling Config.upload_base64_image guard against a falsey/empty URL (log or
return None/skip) instead of returning an object with an empty URL; ensure IDs
are stringified as before and that the function returns nothing or a safe
sentinel when validation fails.
| try: | ||
| # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') | ||
| image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' | ||
| image_name = f"content_{content_index}.{image_format}" | ||
|
|
||
| # Convert binary data to base64 string for upload | ||
| binary_data = item.inline_data.data | ||
| base64_string = base64.b64encode(binary_data).decode('utf-8') | ||
|
|
||
| # Use OpenAI's run_async pattern to handle the async upload function | ||
| url = None | ||
|
|
||
| async def upload_task(): | ||
| nonlocal url | ||
| url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) | ||
|
|
||
| run_async(upload_task()) | ||
|
|
||
| return { | ||
| "type": "image_url", | ||
| "image_url": {"url": url} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Mirror async fixes in the sync path and guard against missing URL.
Same mime-type and binary-data issues exist here; also skip returning an image block if upload failed and URL is None/empty.
- # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg')
- image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown'
- image_name = f"content_{content_index}.{image_format}"
+ # Extract format from mime type (prefer inline_data.mime_type)
+ mime = (
+ getattr(getattr(item, "inline_data", None), "mime_type", None)
+ or getattr(item, "mime_type", None)
+ )
+ image_format = mime.split("/")[1] if (mime and "/" in mime) else "unknown"
+ image_name = f"content_{content_index}.{image_format}"
@@
- binary_data = item.inline_data.data
- base64_string = base64.b64encode(binary_data).decode('utf-8')
+ inline = getattr(item, "inline_data", None)
+ binary_data = getattr(inline, "data", None)
+ if not binary_data:
+ return None
+ base64_string = base64.b64encode(binary_data).decode("utf-8")
@@
async def upload_task():
nonlocal url
url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string)
run_async(upload_task())
- return {
- "type": "image_url",
- "image_url": {"url": url}
- }
+ if not url:
+ return None
+ return {"type": "image_url", "image_url": {"url": url}}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| # Extract format from mime type (e.g., 'image/jpeg' -> 'jpeg') | |
| image_format = item.mime_type.split('/')[1] if item.mime_type else 'unknown' | |
| image_name = f"content_{content_index}.{image_format}" | |
| # Convert binary data to base64 string for upload | |
| binary_data = item.inline_data.data | |
| base64_string = base64.b64encode(binary_data).decode('utf-8') | |
| # Use OpenAI's run_async pattern to handle the async upload function | |
| url = None | |
| async def upload_task(): | |
| nonlocal url | |
| url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) | |
| run_async(upload_task()) | |
| return { | |
| "type": "image_url", | |
| "image_url": {"url": url} | |
| } | |
| try: | |
| # Extract format from mime type (prefer inline_data.mime_type) | |
| mime = ( | |
| getattr(getattr(item, "inline_data", None), "mime_type", None) | |
| or getattr(item, "mime_type", None) | |
| ) | |
| image_format = mime.split("/")[1] if (mime and "/" in mime) else "unknown" | |
| image_name = f"content_{content_index}.{image_format}" | |
| # Convert binary data to base64 string for upload | |
| inline = getattr(item, "inline_data", None) | |
| binary_data = getattr(inline, "data", None) | |
| if not binary_data: | |
| return None | |
| base64_string = base64.b64encode(binary_data).decode("utf-8") | |
| # Use OpenAI's run_async pattern to handle the async upload function | |
| url = None | |
| async def upload_task(): | |
| nonlocal url | |
| url = await Config.upload_base64_image( | |
| str(trace_id), str(span_id), image_name, base64_string | |
| ) | |
| run_async(upload_task()) | |
| if not url: | |
| return None | |
| return {"type": "image_url", "image_url": {"url": url}} |
🤖 Prompt for AI Agents
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
around lines 88 to 109: mirror the async-path fixes in this sync upload path by
defensively extracting the mime subtype (use 'unknown' if item.mime_type is
falsy or split fails), guard that item.inline_data and item.inline_data.data
exist before base64-encoding (skip/return None if missing), ensure
base64.b64encode receives bytes, and after run_async(upload_task()) check that
url is truthy before returning the image block (if url is None/empty, do not
return an image_url block—return None or skip adding it). Ensure variables use
nonlocal url as shown and include these guards so failed uploads or missing data
don’t produce empty image_url entries.
| @dont_throw | ||
| def set_input_attributes(span, args): | ||
| async def set_input_attributes(span, args): | ||
| """Process input arguments, handling both text and image content""" | ||
| if not span.is_recording(): | ||
| return | ||
| if should_send_prompts() and args is not None and len(args) > 0: | ||
| prompt = "" | ||
| for arg in args: | ||
| if isinstance(arg, str): | ||
| prompt = f"{prompt}{arg}\n" | ||
| elif isinstance(arg, list): | ||
| for subarg in arg: | ||
| prompt = f"{prompt}{subarg}\n" | ||
| # Process each argument using extracted helper methods | ||
| for arg_index, argument in enumerate(args): | ||
| processed_content = await _process_vertexai_argument(argument, span) | ||
|
|
||
| _set_span_attribute( | ||
| span, | ||
| f"{SpanAttributes.LLM_PROMPTS}.0.user", | ||
| prompt, | ||
| ) | ||
| if processed_content: | ||
| _set_span_attribute( | ||
| span, | ||
| f"{SpanAttributes.LLM_PROMPTS}.{arg_index}.content", | ||
| json.dumps(processed_content) | ||
| ) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dont_throw is not async-aware; exceptions inside set_input_attributes won’t be caught.
The decorator in utils.py wraps async functions with a sync wrapper that returns the coroutine without awaiting it; exceptions raised during await won’t be caught/logged by the decorator. That defeats the purpose of “don’t throw” for this async API.
Two options:
- Make dont_throw async-aware (preferred; see snippet below).
- Or remove @dont_throw here and rely on callers to handle exceptions (not recommended for instrumentation).
Proposed async-aware decorator (outside this file, in packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py):
# drop-in replacement
import inspect
import functools
import logging
import traceback
from .config import Config
def dont_throw(func):
logger = logging.getLogger(func.__module__)
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def aw(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return aw
@functools.wraps(func)
def sw(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
logger.debug(
"OpenLLMetry failed to trace in %s, error: %s",
func.__name__,
traceback.format_exc(),
)
if Config.exception_logger:
Config.exception_logger(e)
return swHappy to push a follow-up PR if you want.
🤖 Prompt for AI Agents
In
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/span_utils.py
lines 206-222, the @dont_throw decorator applied to async set_input_attributes
is not async-aware so exceptions raised during await are not caught; update the
dont_throw implementation in
packages/opentelemetry-instrumentation-vertexai/opentelemetry/instrumentation/vertexai/utils.py
to detect coroutine functions (inspect.iscoroutinefunction), return an async
wrapper that awaits the target and catches/logs exceptions (using the module
logger and Config.exception_logger if set), and preserve functools.wraps
behavior; alternatively remove the decorator from this async function, but
preferred fix is to make dont_throw handle async functions and update
imports/tests accordingly.
Summary
Changes Made
Google Generative AI Instrumentation
inline_datacontaining binary image data{"type": "image_url", "image_url": {"url": "..."}}upload_base64_imageconfiguration hook integrationVertex AI Instrumentation
upload_base64_imageconfiguration hook integrationTraceloop SDK Integration
base64_image_uploaderSample Applications
google_genai_image_example.pydemonstrating Google GenAI with imagesvertex_gemini_vision_example.pydemonstrating Vertex AI with imagesKey Features
asyncio.run()in sync contextsTest Plan
Technical Details
The implementation follows the same patterns established for OpenAI instrumentation:
This ensures consistent image handling across all LLM provider instrumentations.
🤖 Generated with Claude Code
Important
Adds image support for Google Generative AI and Vertex AI, including async/sync processing, URL replacement, and Traceloop SDK integration.
GoogleGenerativeAiInstrumentorandVertexAIInstrumentorupdated to acceptupload_base64_imageconfiguration.set_input_attributes_syncandset_input_attributesinspan_utils.pyhandle image data.google_genai_image_example.pyandvertex_gemini_vision_example.pyto demonstrate image handling.tracing.pyto pass image upload hooks to instrumentations.This description was created by
for 0c80bc9. You can customize this summary. It will automatically update as commits are pushed.
Summary by CodeRabbit
New Features
Documentation