From 4805d2311e0df7b09958a14ec718611b2076f30a Mon Sep 17 00:00:00 2001 From: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> Date: Wed, 4 Nov 2026 18:14:00 +0530 Subject: [PATCH] fix(openai): default upload_base64_image to None and support sync uploaders Signed-off-by: Mohd Quamar Tyagi <104281681+Tyagiquamar@users.noreply.github.com> --- .../instrumentation/openai/__init__.py | 2 +- .../openai/shared/chat_wrappers.py | 7 +- .../instrumentation/openai/shared/config.py | 7 +- .../tests/traces/test_vision.py | 104 ++++++++++++++++++ .../uv.lock | 6 +- 5 files changed, 118 insertions(+), 8 deletions(-) diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py index 5f6de1b099..6f1397e360 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/__init__.py @@ -19,7 +19,7 @@ def __init__( get_common_metrics_attributes: Callable[[], dict] = lambda: {}, upload_base64_image: Optional[ Callable[[str, str, str, str], Coroutine[None, None, str]] - ] = lambda *args: "", + ] = None, enable_trace_context_propagation: bool = True, use_attributes: Optional[bool] = None, use_legacy_attributes: Optional[bool] = None, diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py index a7d6c11a0e..1f709c595b 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/chat_wrappers.py @@ -1,4 +1,5 @@ import copy +import inspect import logging import threading import time @@ -438,7 +439,11 @@ async def _process_image_item(item, trace_id, span_id, message_index, content_in image_name = f"message_{message_index}_content_{content_index}.{image_format}" base64_string = item["image_url"]["url"].split(",")[1] # Convert trace_id and span_id to strings as expected by upload function - url = await Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) + upload_result = Config.upload_base64_image(str(trace_id), str(span_id), image_name, base64_string) + if inspect.isawaitable(upload_result): + url = await upload_result + else: + url = upload_result return {"type": "image_url", "image_url": {"url": url}} diff --git a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py index 088a9a8f8a..1f96580956 100644 --- a/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py +++ b/packages/opentelemetry-instrumentation-openai/opentelemetry/instrumentation/openai/shared/config.py @@ -1,15 +1,16 @@ from typing import Callable, Optional from opentelemetry._logs import Logger +from typing_extensions import Coroutine class Config: enrich_assistant = False exception_logger = None 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 - ) + upload_base64_image: Optional[ + Callable[[str, str, str, str], Coroutine[None, None, str]] + ] = None enable_trace_context_propagation: bool = True use_legacy_attributes = True event_logger: Optional[Logger] = None diff --git a/packages/opentelemetry-instrumentation-openai/tests/traces/test_vision.py b/packages/opentelemetry-instrumentation-openai/tests/traces/test_vision.py index 449950ae08..7a36e5f55b 100644 --- a/packages/opentelemetry-instrumentation-openai/tests/traces/test_vision.py +++ b/packages/opentelemetry-instrumentation-openai/tests/traces/test_vision.py @@ -404,3 +404,107 @@ def assert_message_in_logs(log: ReadableLogRecord, event_name: str, expected_con else: assert log.log_record.body assert dict(log.log_record.body) == expected_content + + +@pytest.mark.asyncio +async def test_vision_default_uploader_config(): + from opentelemetry.instrumentation.openai import OpenAIInstrumentor + from opentelemetry.instrumentation.openai.shared.config import Config + + original = Config.upload_base64_image + try: + OpenAIInstrumentor() + assert Config.upload_base64_image is None + finally: + Config.upload_base64_image = original + + +@pytest.mark.asyncio +async def test_process_image_item_uploader_handling(): + from opentelemetry.instrumentation.openai.shared.chat_wrappers import ( + _process_image_item, + ) + from opentelemetry.instrumentation.openai.shared.config import Config + + original = Config.upload_base64_image + item = { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}, + } + + try: + # 1. No uploader configured (None) -> returns original item + Config.upload_base64_image = None + res_none = await _process_image_item(item, "trace1", "span1", 0, 0) + assert res_none == item + + # 2. Synchronous uploader -> returns url without TypeError + Config.upload_base64_image = ( + lambda trace_id, span_id, name, b64: "https://cdn.example.com/sync.png" + ) + res_sync = await _process_image_item(item, "trace1", "span1", 0, 0) + assert res_sync == { + "type": "image_url", + "image_url": {"url": "https://cdn.example.com/sync.png"}, + } + + # 3. Asynchronous uploader -> awaited and returns url + async def async_uploader(trace_id, span_id, name, b64): + return "https://cdn.example.com/async.png" + + Config.upload_base64_image = async_uploader + res_async = await _process_image_item(item, "trace1", "span1", 0, 0) + assert res_async == { + "type": "image_url", + "image_url": {"url": "https://cdn.example.com/async.png"}, + } + finally: + Config.upload_base64_image = original + + +@pytest.mark.asyncio +async def test_set_prompts_preserves_input_messages_without_uploader( + tracer_provider, span_exporter +): + from opentelemetry.instrumentation.openai.shared.chat_wrappers import _set_prompts + from opentelemetry.instrumentation.openai.shared.config import Config + + original = Config.upload_base64_image + tracer = tracer_provider.get_tracer("test") + span = tracer.start_span("test_vision_no_uploader_span") + + messages = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image"}, + { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,iVBORw0KGgo="}, + }, + ], + } + ] + + try: + Config.upload_base64_image = None + await _set_prompts(span, messages) + span.end() + + spans = span_exporter.get_finished_spans() + target_span = [s for s in spans if s.name == "test_vision_no_uploader_span"][-1] + input_messages = get_input_messages(target_span) + + assert len(input_messages) == 1 + assert input_messages[0]["role"] == "user" + parts = input_messages[0]["parts"] + assert len(parts) == 2 + assert parts[0] == {"type": "text", "content": "Describe this image"} + assert parts[1] == { + "type": "blob", + "modality": "image", + "mime_type": "image/png", + "content": "iVBORw0KGgo=", + } + finally: + Config.upload_base64_image = original diff --git a/packages/opentelemetry-instrumentation-openai/uv.lock b/packages/opentelemetry-instrumentation-openai/uv.lock index 40a60b6a95..df2c8718d9 100644 --- a/packages/opentelemetry-instrumentation-openai/uv.lock +++ b/packages/opentelemetry-instrumentation-openai/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <4" resolution-markers = [ "python_full_version >= '3.12'", @@ -170,7 +170,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -533,7 +533,7 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.61.0" +version = "0.62.4" source = { editable = "." } dependencies = [ { name = "opentelemetry-api" },