Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Enhanced streaming support
- Performance optimizations

### Python

#### fi-instrumentation-otel (Core)
- 🐛 Fix issue where SpanAttributes.INPUT_VALUE was overwritten multiple times, causing loss of context for multi-message inputs (fixes #151)

---

## [2025-07-08]
Expand Down
16 changes: 7 additions & 9 deletions python/frameworks/openai/traceai_openai/_span_io_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,20 @@ def _process_input_data(input_data: Any, span: _WithSpan) -> None:
else:
input_content.append(msg)
eval_input.append(msg_content)
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])
Comment on lines 59 to -63
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just Deleting these lines should solve the issue of data being overwritten , no need to introduce any new span attributes

else:
try:
input_str = json.dumps(input_data, ensure_ascii=False).strip()
except (TypeError, ValueError):
input_str = str(input_data).strip()
if isinstance(input_data, str):
input_str = input_data.strip()
else:
try:
input_str = json.dumps(input_data, ensure_ascii=False).strip()
except (TypeError, ValueError):
input_str = str(input_data).strip()
span.set_attribute(SpanAttributes.INPUT_VALUE, input_str)


Expand Down
62 changes: 61 additions & 1 deletion python/tests/test_framework_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,64 @@ def test_instrumentor_uninstrumentation(self):

# Methods should be restored (back to functions)
assert openai.OpenAI.request == original_request
assert openai.AsyncOpenAI.request == original_async_request
assert openai.AsyncOpenAI.request == original_async_request


class TestSpanIOHandler:
"""Test the span I/O handler functions."""

def test_process_input_data_multi_message(self):
"""Test _process_input_data with multiple messages preserves full context."""
from traceai_openai._span_io_handler import _process_input_data
from fi_instrumentation.fi_types import SpanAttributes
from unittest.mock import MagicMock

# Create mock span
mock_span = MagicMock()

# Test input with multiple messages
input_data = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "How are you?"}
]

_process_input_data(input_data, mock_span)

# Verify INPUT_VALUE contains full joined text
expected_value = "You are a helpful assistant. \n Hello \n Hi there! \n How are you?"
mock_span.set_attribute.assert_any_call(SpanAttributes.INPUT_VALUE, expected_value)

def test_process_input_data_single_message(self):
"""Test _process_input_data with single message for regression."""
from traceai_openai._span_io_handler import _process_input_data
from fi_instrumentation.fi_types import SpanAttributes
from unittest.mock import MagicMock

mock_span = MagicMock()

input_data = [
{"role": "user", "content": "Hello world"}
]

_process_input_data(input_data, mock_span)

# Should still set INPUT_VALUE to the single message
expected_value = "Hello world"
mock_span.set_attribute.assert_any_call(SpanAttributes.INPUT_VALUE, expected_value)

def test_process_input_data_non_list(self):
"""Test _process_input_data with non-list input (string prompt)."""
from traceai_openai._span_io_handler import _process_input_data
from fi_instrumentation.fi_types import SpanAttributes
from unittest.mock import MagicMock

mock_span = MagicMock()

input_data = "What is the capital of France?"

_process_input_data(input_data, mock_span)

# Should set INPUT_VALUE to the string
mock_span.set_attribute.assert_called_once_with(SpanAttributes.INPUT_VALUE, "What is the capital of France?")