From 2b289cc848e45c272f3785a66254de599f32e2e0 Mon Sep 17 00:00:00 2001 From: Gauri Nigam Date: Fri, 3 Jul 2026 17:08:06 +0530 Subject: [PATCH 1/2] fix: stop overwriting multi-message INPUT_VALUE in OpenAI instrumentor --- .../openai/tests/test_span_io_handler.py | 87 +++++++++++++++++++ .../openai/traceai_openai/_span_io_handler.py | 17 +--- 2 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 python/frameworks/openai/tests/test_span_io_handler.py diff --git a/python/frameworks/openai/tests/test_span_io_handler.py b/python/frameworks/openai/tests/test_span_io_handler.py new file mode 100644 index 00000000..dc5aa0c0 --- /dev/null +++ b/python/frameworks/openai/tests/test_span_io_handler.py @@ -0,0 +1,87 @@ +""" +Regression test for issue #151: multi-turn conversation input was being +dropped from the span. _process_input_data set INPUT_VALUE three times +on the same span, and the last two writes overwrote the first, correct +one, leaving only the first message's text on the span. +""" + +import json + +import pytest +from opentelemetry import trace as trace_api +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import SimpleSpanProcessor +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) + +from fi_instrumentation.fi_types import SpanAttributes +from traceai_openai._span_io_handler import _process_input_data +from traceai_openai._with_span import _WithSpan + + +@pytest.fixture +def recorder(): + exporter = InMemorySpanExporter() + provider = TracerProvider() + provider.add_span_processor(SimpleSpanProcessor(exporter)) + tracer = provider.get_tracer(__name__) + return tracer, exporter + + +def _get_input_value(exporter): + spans = exporter.get_finished_spans() + assert len(spans) == 1 + return spans[0].attributes.get(SpanAttributes.INPUT_VALUE) + + +def test_multi_message_input_is_not_dropped(recorder): + tracer, exporter = recorder + messages = [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "What is the capital of France?"}, + {"role": "assistant", "content": "Paris."}, + {"role": "user", "content": "And Germany?"}, + ] + + with tracer.start_as_current_span("openai.chat") as span: + _process_input_data(messages, _WithSpan(span)) + + input_value = _get_input_value(exporter) + assert input_value is not None + + parsed = json.loads(input_value) + assert len(parsed) == len(messages), ( + "Expected all messages to survive in INPUT_VALUE, but only " + f"{len(parsed)} of {len(messages)} were present." + ) + assert [m["content"] for m in parsed] == [m["content"] for m in messages] + + +def test_multimodal_message_list_preserves_all_messages(recorder): + tracer, exporter = recorder + messages = [ + {"role": "user", "content": "Describe this image."}, + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this picture?"}, + { + "type": "image_url", + "image_url": {"url": "https://example.com/cat.png"}, + }, + ], + }, + ] + + with tracer.start_as_current_span("openai.chat") as span: + _process_input_data(messages, _WithSpan(span)) + + input_value = _get_input_value(exporter) + parsed = json.loads(input_value) + assert len(parsed) == 2 + + spans = exporter.get_finished_spans() + images_value = spans[0].attributes.get(SpanAttributes.INPUT_IMAGES) + assert images_value is not None + assert json.loads(images_value) == ["https://example.com/cat.png"] diff --git a/python/frameworks/openai/traceai_openai/_span_io_handler.py b/python/frameworks/openai/traceai_openai/_span_io_handler.py index 20c4ec47..e1e3fa5a 100644 --- a/python/frameworks/openai/traceai_openai/_span_io_handler.py +++ b/python/frameworks/openai/traceai_openai/_span_io_handler.py @@ -19,7 +19,6 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None: if isinstance(input_data, list): input_content = [] input_images = [] - eval_input = [] for msg in input_data: if isinstance(msg, dict): msg_content = msg.get("content", "") @@ -29,18 +28,11 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None: if isinstance(item, dict): if item.get("type") == "text": filtered_content.append(item) - eval_input.append(item.get("text", "")) elif item.get("type") == "image_url": url_data = item.get("image_url", {}) url = url_data.get("url") if url: input_images.append(url) - image_index = len(input_images) - 1 - eval_input.append( - "{{" - + f"{SpanAttributes.INPUT_IMAGES}.{image_index}" - + "}}" - ) if filtered_content: msg_dict = msg.copy() msg_dict["content"] = filtered_content @@ -49,18 +41,15 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None: continue else: input_content.append(msg) - eval_input.append(msg_content) + # INPUT_VALUE holds the full multi-message conversation as JSON. + # Do not overwrite it later with a single-message string; that was + # the cause of multi-turn context being dropped from the span (#151). if input_content: input_value = json.dumps(input_content, ensure_ascii=False) span.set_attribute(SpanAttributes.INPUT_VALUE, input_value) if input_images: images_value = json.dumps(input_images, ensure_ascii=False) span.set_attribute(SpanAttributes.INPUT_IMAGES, images_value) - if eval_input: - eval_input_str = " \n ".join(map(str, eval_input)) - span.set_attribute(SpanAttributes.INPUT_VALUE, eval_input_str) - if eval_input and len(eval_input) > 0: - span.set_attribute(SpanAttributes.INPUT_VALUE, eval_input[0]) else: try: input_str = json.dumps(input_data, ensure_ascii=False).strip() From 65c89f6aec229b2236636e6c923e49c0cdac2fed Mon Sep 17 00:00:00 2001 From: Gauri Nigam Date: Fri, 3 Jul 2026 17:17:33 +0530 Subject: [PATCH 2/2] chore: remove unused import in test file --- python/frameworks/openai/tests/test_span_io_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/frameworks/openai/tests/test_span_io_handler.py b/python/frameworks/openai/tests/test_span_io_handler.py index dc5aa0c0..39159691 100644 --- a/python/frameworks/openai/tests/test_span_io_handler.py +++ b/python/frameworks/openai/tests/test_span_io_handler.py @@ -8,7 +8,6 @@ import json import pytest -from opentelemetry import trace as trace_api from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import (