From ee735d05671bd6ded8e59157d39f36297e8b08a7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:25:40 +0000 Subject: [PATCH 1/3] Fix OpenAI stream wrapper attribute delegation - Add __getattr__ method to OpenAIAsyncStreamWrapper and OpenaiStreamWrapper - Fixes 'choices' attribute error when accessing stream attributes - Follows existing pattern from agno instrumentor for attribute delegation - Enables transparent access to underlying stream properties like choices, model, id Co-Authored-By: Alex --- .../instrumentation/providers/openai/stream_wrapper.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agentops/instrumentation/providers/openai/stream_wrapper.py b/agentops/instrumentation/providers/openai/stream_wrapper.py index 15e541d82..8fd829a7d 100644 --- a/agentops/instrumentation/providers/openai/stream_wrapper.py +++ b/agentops/instrumentation/providers/openai/stream_wrapper.py @@ -84,6 +84,10 @@ def __exit__(self, exc_type, exc_val, exc_tb): context_api.detach(self._token) return False + def __getattr__(self, name): + """Delegate attribute access to the original stream.""" + return getattr(self._stream, name) + def _process_chunk(self, chunk: Any) -> None: """Process a single chunk from the stream. @@ -320,6 +324,10 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): context_api.detach(self._token) return False + def __getattr__(self, name): + """Delegate attribute access to the original stream.""" + return getattr(self._stream, name) + @_with_tracer_wrapper def chat_completion_stream_wrapper(tracer, wrapped, instance, args, kwargs): From 63055f1a955444d44d55a9732152dc34ab5f8efb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:28:53 +0000 Subject: [PATCH 2/3] Add unit tests for OpenAI stream wrapper attribute delegation - Test __getattr__ method in both OpenaiStreamWrapper and OpenAIAsyncStreamWrapper - Verify proper delegation of choices, model, id, and usage attributes - Test AttributeError handling for missing attributes - Addresses codecov/patch coverage requirement for PR #1098 Co-Authored-By: Alex --- .../openai_core/test_stream_wrapper.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/unit/instrumentation/openai_core/test_stream_wrapper.py diff --git a/tests/unit/instrumentation/openai_core/test_stream_wrapper.py b/tests/unit/instrumentation/openai_core/test_stream_wrapper.py new file mode 100644 index 000000000..18f7c97c8 --- /dev/null +++ b/tests/unit/instrumentation/openai_core/test_stream_wrapper.py @@ -0,0 +1,97 @@ +""" +Tests for OpenAI Stream Wrapper Attribute Delegation + +This module contains tests for the OpenAI stream wrapper classes to ensure +proper attribute delegation to the underlying stream objects. +""" + +import pytest +from unittest.mock import Mock, AsyncMock + +from agentops.instrumentation.providers.openai.stream_wrapper import ( + OpenAIAsyncStreamWrapper, + OpenaiStreamWrapper +) + + +class TestOpenaiStreamWrapper: + """Tests for sync OpenAI stream wrapper attribute delegation""" + + def test_getattr_delegates_to_stream(self): + """Test that __getattr__ properly delegates to underlying stream""" + mock_stream = Mock() + mock_stream.choices = [{"delta": {"content": "test content"}}] + mock_stream.model = "gpt-4" + mock_stream.id = "test-stream-id" + mock_stream.usage = {"prompt_tokens": 10, "completion_tokens": 5} + + mock_span = Mock() + + wrapper = OpenaiStreamWrapper(mock_stream, mock_span, {}) + + assert wrapper.choices == mock_stream.choices + assert wrapper.model == mock_stream.model + assert wrapper.id == mock_stream.id + assert wrapper.usage == mock_stream.usage + + def test_getattr_raises_attributeerror_for_missing_attributes(self): + """Test that __getattr__ raises AttributeError for missing attributes""" + mock_stream = Mock() + del mock_stream.nonexistent_attribute # Ensure it doesn't exist + + mock_span = Mock() + + wrapper = OpenaiStreamWrapper(mock_stream, mock_span, {}) + + with pytest.raises(AttributeError): + _ = wrapper.nonexistent_attribute + + +class TestOpenAIAsyncStreamWrapper: + """Tests for async OpenAI stream wrapper attribute delegation""" + + def test_getattr_delegates_to_stream(self): + """Test that __getattr__ properly delegates to underlying stream""" + mock_stream = AsyncMock() + mock_stream.choices = [{"delta": {"content": "test content"}}] + mock_stream.model = "gpt-4" + mock_stream.id = "test-async-stream-id" + mock_stream.usage = {"prompt_tokens": 15, "completion_tokens": 8} + + mock_span = Mock() + + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) + + assert wrapper.choices == mock_stream.choices + assert wrapper.model == mock_stream.model + assert wrapper.id == mock_stream.id + assert wrapper.usage == mock_stream.usage + + def test_getattr_raises_attributeerror_for_missing_attributes(self): + """Test that __getattr__ raises AttributeError for missing attributes""" + mock_stream = AsyncMock() + del mock_stream.nonexistent_attribute # Ensure it doesn't exist + + mock_span = Mock() + + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) + + with pytest.raises(AttributeError): + _ = wrapper.nonexistent_attribute + + def test_choices_attribute_specifically(self): + """Test the specific 'choices' attribute that was causing the original error""" + mock_stream = AsyncMock() + mock_stream.choices = [ + Mock(delta=Mock(content="Hello")), + Mock(delta=Mock(content=" world")) + ] + + mock_span = Mock() + + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) + + choices = wrapper.choices + assert len(choices) == 2 + assert choices[0].delta.content == "Hello" + assert choices[1].delta.content == " world" From f6e72f9513b890c6c4a189aa2a2cdfc0eaae07ca Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:30:35 +0000 Subject: [PATCH 3/3] Apply pre-commit formatting fixes to test_stream_wrapper.py - Consolidate multi-line imports to single line - Remove trailing whitespace - Format list definitions for consistency - Addresses pre-commit checks failure in PR #1098 Co-Authored-By: Alex --- .../openai_core/test_stream_wrapper.py | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/tests/unit/instrumentation/openai_core/test_stream_wrapper.py b/tests/unit/instrumentation/openai_core/test_stream_wrapper.py index 18f7c97c8..ad7b26a13 100644 --- a/tests/unit/instrumentation/openai_core/test_stream_wrapper.py +++ b/tests/unit/instrumentation/openai_core/test_stream_wrapper.py @@ -8,10 +8,7 @@ import pytest from unittest.mock import Mock, AsyncMock -from agentops.instrumentation.providers.openai.stream_wrapper import ( - OpenAIAsyncStreamWrapper, - OpenaiStreamWrapper -) +from agentops.instrumentation.providers.openai.stream_wrapper import OpenAIAsyncStreamWrapper, OpenaiStreamWrapper class TestOpenaiStreamWrapper: @@ -24,11 +21,11 @@ def test_getattr_delegates_to_stream(self): mock_stream.model = "gpt-4" mock_stream.id = "test-stream-id" mock_stream.usage = {"prompt_tokens": 10, "completion_tokens": 5} - + mock_span = Mock() - + wrapper = OpenaiStreamWrapper(mock_stream, mock_span, {}) - + assert wrapper.choices == mock_stream.choices assert wrapper.model == mock_stream.model assert wrapper.id == mock_stream.id @@ -38,11 +35,11 @@ def test_getattr_raises_attributeerror_for_missing_attributes(self): """Test that __getattr__ raises AttributeError for missing attributes""" mock_stream = Mock() del mock_stream.nonexistent_attribute # Ensure it doesn't exist - + mock_span = Mock() - + wrapper = OpenaiStreamWrapper(mock_stream, mock_span, {}) - + with pytest.raises(AttributeError): _ = wrapper.nonexistent_attribute @@ -57,11 +54,11 @@ def test_getattr_delegates_to_stream(self): mock_stream.model = "gpt-4" mock_stream.id = "test-async-stream-id" mock_stream.usage = {"prompt_tokens": 15, "completion_tokens": 8} - + mock_span = Mock() - + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) - + assert wrapper.choices == mock_stream.choices assert wrapper.model == mock_stream.model assert wrapper.id == mock_stream.id @@ -71,26 +68,23 @@ def test_getattr_raises_attributeerror_for_missing_attributes(self): """Test that __getattr__ raises AttributeError for missing attributes""" mock_stream = AsyncMock() del mock_stream.nonexistent_attribute # Ensure it doesn't exist - + mock_span = Mock() - + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) - + with pytest.raises(AttributeError): _ = wrapper.nonexistent_attribute def test_choices_attribute_specifically(self): """Test the specific 'choices' attribute that was causing the original error""" mock_stream = AsyncMock() - mock_stream.choices = [ - Mock(delta=Mock(content="Hello")), - Mock(delta=Mock(content=" world")) - ] - + mock_stream.choices = [Mock(delta=Mock(content="Hello")), Mock(delta=Mock(content=" world"))] + mock_span = Mock() - + wrapper = OpenAIAsyncStreamWrapper(mock_stream, mock_span, {}) - + choices = wrapper.choices assert len(choices) == 2 assert choices[0].delta.content == "Hello"