diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch.py b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch.py index 3782ca61c..9c5eb1afc 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch.py +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/src/opentelemetry/instrumentation/genai/openai/patch.py @@ -63,6 +63,22 @@ def chat_completions_create_v_new( capture_content = handler.should_capture_content() def traced_method(wrapped, instance, args, kwargs): + # Materialize generator/map-backed messages/tools before they are + # consumed by either instrumentation or the SDK (fixes #825). + messages_raw = kwargs.get("messages") + if messages_raw is not None and not isinstance(messages_raw, (list, tuple)): + try: + messages_raw = list(messages_raw) + except TypeError: + pass + kwargs = {**kwargs, "messages": messages_raw} + tools_raw = kwargs.get("tools") + if tools_raw is not None and not isinstance(tools_raw, (list, tuple)): + try: + tools_raw = list(tools_raw) + except TypeError: + pass + kwargs = {**kwargs, "tools": tools_raw} chat_invocation = create_chat_invocation( handler, kwargs, instance, capture_content=capture_content ) @@ -94,6 +110,22 @@ def async_chat_completions_create_v_new( capture_content = handler.should_capture_content() async def traced_method(wrapped, instance, args, kwargs): + # Materialize generator/map-backed messages/tools before they are + # consumed by either instrumentation or the SDK (fixes #825). + messages_raw = kwargs.get("messages") + if messages_raw is not None and not isinstance(messages_raw, (list, tuple)): + try: + messages_raw = list(messages_raw) + except TypeError: + pass + kwargs = {**kwargs, "messages": messages_raw} + tools_raw = kwargs.get("tools") + if tools_raw is not None and not isinstance(tools_raw, (list, tuple)): + try: + tools_raw = list(tools_raw) + except TypeError: + pass + kwargs = {**kwargs, "tools": tools_raw} chat_invocation = create_chat_invocation( handler, kwargs, instance, capture_content=capture_content ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_issue_825_regression.py b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_issue_825_regression.py new file mode 100644 index 000000000..ff87e9549 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-openai/tests/test_issue_825_regression.py @@ -0,0 +1,61 @@ +"""Regression test for issue #825: instrumented wrapper materializes generators.""" +import os +os.environ.setdefault("OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT", "SPAN_ONLY") + +from unittest.mock import MagicMock, AsyncMock, patch + +# Import the instrumented wrappers (production code unchanged) +from opentelemetry.instrumentation.genai.openai.patch import ( + chat_completions_create_v_new, + async_chat_completions_create_v_new, +) + +def test_sync_wrapper_materializes_generator_messages(): + mock_create = MagicMock(return_value={"choices": []}) + handler = MagicMock() + handler.should_capture_content.return_value = False + wrapper = chat_completions_create_v_new(handler) + generator = (msg for msg in [{"role":"user","content":"hi"}]) + wrapper(mock_create, None, [], {"messages": generator}) + call_kwargs = mock_create.call_args.kwargs + assert isinstance(call_kwargs["messages"], list) + assert call_kwargs["messages"] == [{"role":"user","content":"hi"}] + +def test_sync_wrapper_materializes_generator_tools(): + mock_create = MagicMock(return_value={"choices": []}) + handler = MagicMock() + handler.should_capture_content.return_value = False + wrapper = chat_completions_create_v_new(handler) + generator = (t for t in [{"type":"function","function":{"name":"f"}}]) + wrapper(mock_create, None, [], {"messages": [{"role":"user","content":"hi"}], "tools": generator}) + call_kwargs = mock_create.call_args.kwargs + assert isinstance(call_kwargs["tools"], list) + assert call_kwargs["tools"] == [{"type":"function","function":{"name":"f"}}] + +def test_async_wrapper_materializes_generator_messages(): + import asyncio + mock_create = AsyncMock(return_value={"choices": []}) + handler = MagicMock() + handler.should_capture_content.return_value = False + wrapper = async_chat_completions_create_v_new(handler) + async def run(): + generator = (msg for msg in [{"role":"user","content":"hi"}]) + await wrapper(mock_create, None, [], {"messages": generator}) + asyncio.run(run()) + call_kwargs = mock_create.call_args.kwargs + assert isinstance(call_kwargs["messages"], list) + assert call_kwargs["messages"] == [{"role":"user","content":"hi"}] + +def test_async_wrapper_materializes_generator_tools(): + import asyncio + mock_create = AsyncMock(return_value={"choices": []}) + handler = MagicMock() + handler.should_capture_content.return_value = False + wrapper = async_chat_completions_create_v_new(handler) + async def run(): + generator = (t for t in [{"type":"function","function":{"name":"f"}}]) + await wrapper(mock_create, None, [], {"messages": [{"role":"user","content":"hi"}], "tools": generator}) + asyncio.run(run()) + call_kwargs = mock_create.call_args.kwargs + assert isinstance(call_kwargs["tools"], list) + assert call_kwargs["tools"] == [{"type":"function","function":{"name":"f"}}]