| """ |
| Comprehensive streaming tests for BYOKHandler stream_completion method. |
| |
| This test file targets stream_completion method coverage (lines ~1372-1518 in byok_handler.py) |
| Tests cover: |
| - Async streaming with AsyncMock for OpenAI, Anthropic, DeepSeek providers |
| - Provider fallback on streaming failures |
| - Empty stream handling |
| - System instruction and temperature parameters |
| - Governance tracking with agent execution records |
| - Error handling (rate limits, connection errors) |
| |
| Target: 75%+ coverage for stream_completion method |
| """ |
|
|
| import os |
| import pytest |
| from unittest.mock import AsyncMock, MagicMock, patch |
| from sqlalchemy.orm import Session |
|
|
| from core.llm.byok_handler import BYOKHandler, QueryComplexity |
|
|
|
|
| |
| |
| |
|
|
| @pytest.fixture |
| def mock_async_openai_client(): |
| """Mock AsyncOpenAI client for streaming tests.""" |
| client = AsyncMock() |
| return client |
|
|
|
|
| @pytest.fixture |
| def mock_stream_response(): |
| """Mock async streaming response generator.""" |
| async def generate_chunks(): |
| """Generate mock streaming chunks.""" |
| |
| chunk1 = MagicMock() |
| chunk1.choices = [MagicMock(delta=MagicMock(content="Hello"))] |
| yield chunk1 |
|
|
| |
| chunk2 = MagicMock() |
| chunk2.choices = [MagicMock(delta=MagicMock(content=" world"))] |
| yield chunk2 |
|
|
| |
| chunk3 = MagicMock() |
| chunk3.choices = [MagicMock(delta=MagicMock(content="!"))] |
| yield chunk3 |
|
|
| |
| chunk4 = MagicMock() |
| chunk4.choices = [MagicMock(finish_reason="stop")] |
| yield chunk4 |
|
|
| |
| return generate_chunks |
|
|
|
|
| @pytest.fixture |
| def handler_with_mocked_client(mock_async_openai_client): |
| """Create BYOKHandler with mocked async clients.""" |
| handler = BYOKHandler() |
|
|
| |
| handler.async_clients = { |
| "openai": mock_async_openai_client, |
| "anthropic": AsyncMock(), |
| "deepseek": AsyncMock(), |
| } |
|
|
| |
| handler.clients = handler.async_clients.copy() |
|
|
| return handler |
|
|
|
|
| @pytest.fixture |
| def mock_db_session(): |
| """Mock database session for governance tracking.""" |
| db = MagicMock(spec=Session) |
|
|
| |
| mock_execution = MagicMock() |
| mock_execution.id = "test-execution-123" |
| db.add = MagicMock() |
| db.commit = MagicMock() |
| db.refresh = MagicMock() |
|
|
| return db |
|
|
|
|
| |
| |
| |
|
|
| class TestStreamCompletion: |
| """Test suite for stream_completion async streaming method.""" |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_yields_tokens(self, handler_with_mocked_client, mock_stream_response): |
| """Test that stream_completion yields string tokens.""" |
| handler = handler_with_mocked_client |
|
|
| |
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=mock_stream_response() |
| ) |
|
|
| |
| messages = [{"role": "user", "content": "Hello"}] |
| tokens = [] |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| tokens.append(token) |
|
|
| |
| assert len(tokens) >= 3 |
| assert "Hello" in "".join(tokens) |
| assert "world" in "".join(tokens) |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_openai_provider(self, handler_with_mocked_client): |
| """Test streaming with OpenAI provider.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def openai_stream(): |
| """Stream OpenAI response.""" |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="OpenAI response"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=openai_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| assert "OpenAI response" in result |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_deepseek_provider(self, handler_with_mocked_client): |
| """Test streaming with DeepSeek provider.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def deepseek_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="DeepSeek streaming"))] |
| yield chunk |
|
|
| handler.async_clients["deepseek"].chat.completions.create = AsyncMock( |
| return_value=deepseek_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="deepseek-chat", |
| provider_id="deepseek" |
| ): |
| result += token |
|
|
| assert "DeepSeek streaming" in result |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_empty_stream(self, handler_with_mocked_client): |
| """Test handling of empty stream gracefully.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def empty_stream(): |
| |
| return |
| yield |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=empty_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| tokens = [] |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| tokens.append(token) |
|
|
| |
| assert isinstance(tokens, list) |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_system_instruction(self, handler_with_mocked_client): |
| """Test streaming with system instruction included in messages.""" |
| handler = handler_with_mocked_client |
|
|
| async def stream_with_system(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Response"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=stream_with_system() |
| ) |
|
|
| messages = [ |
| {"role": "system", "content": "You are a helpful assistant."}, |
| {"role": "user", "content": "Hello"} |
| ] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| handler.async_clients["openai"].chat.completions.create.assert_called_once() |
| call_args = handler.async_clients["openai"].chat.completions.create.call_args |
| messages_arg = call_args[1]["messages"] |
| assert len(messages_arg) == 2 |
| assert messages_arg[0]["role"] == "system" |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_temperature(self, handler_with_mocked_client): |
| """Test streaming with custom temperature parameter.""" |
| handler = handler_with_mocked_client |
|
|
| async def stream_with_temp(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Response"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=stream_with_temp() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai", |
| temperature=0.5 |
| ): |
| pass |
|
|
| |
| handler.async_clients["openai"].chat.completions.create.assert_called_once() |
| call_args = handler.async_clients["openai"].chat.completions.create.call_args |
| assert call_args[1]["temperature"] == 0.5 |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_max_tokens(self, handler_with_mocked_client): |
| """Test streaming with max_tokens parameter.""" |
| handler = handler_with_mocked_client |
|
|
| async def stream_with_max(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Token"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=stream_with_max() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai", |
| max_tokens=100 |
| ): |
| pass |
|
|
| |
| call_args = handler.async_clients["openai"].chat.completions.create.call_args |
| assert call_args[1]["max_tokens"] == 100 |
|
|
|
|
| class TestStreamCompletionErrors: |
| """Test suite for streaming error handling.""" |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_provider_fallback(self, handler_with_mocked_client): |
| """Test provider fallback on streaming failure.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def failing_stream(): |
| raise Exception("OpenAI connection error") |
| yield |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| side_effect=failing_stream() |
| ) |
|
|
| |
| async def success_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Fallback response"))] |
| yield chunk |
|
|
| handler.async_clients["anthropic"].chat.completions.create = AsyncMock( |
| return_value=success_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Fallback response" in result or "Error" in result |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_all_providers_fail(self, handler_with_mocked_client): |
| """Test error message when all providers fail.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def failing_stream(): |
| raise Exception("Provider unavailable") |
| yield |
|
|
| for provider_id in handler.async_clients: |
| handler.async_clients[provider_id].chat.completions.create = AsyncMock( |
| side_effect=failing_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Error" in result or "failed" in result.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_no_clients_available(self): |
| """Test error when no clients are initialized.""" |
| handler = BYOKHandler() |
| handler.async_clients = {} |
| handler.clients = {} |
|
|
| messages = [{"role": "user", "content": "Test"}] |
|
|
| with pytest.raises(ValueError) as exc_info: |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| pass |
|
|
| assert "No clients initialized" in str(exc_info.value) or "No available providers" in str(exc_info.value) |
|
|
|
|
| class TestStreamCompletionGovernance: |
| """Test suite for governance tracking during streaming.""" |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_with_agent_tracking(self, handler_with_mocked_client, mock_db_session): |
| """Test agent execution tracking during streaming.""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def tracked_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Tracked response"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=tracked_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| agent_id = "test-agent-123" |
|
|
| |
| with patch.dict(os.environ, {"STREAMING_GOVERNANCE_ENABLED": "true"}): |
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai", |
| agent_id=agent_id, |
| db=mock_db_session |
| ): |
| result += token |
|
|
| |
| mock_db_session.add.assert_called() |
| mock_db_session.commit.assert_called() |
|
|
| assert "Tracked response" in result |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_governance_disabled(self, handler_with_mocked_client, mock_db_session): |
| """Test streaming without governance tracking when disabled.""" |
| handler = handler_with_mocked_client |
|
|
| async def untracked_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Untracked response"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=untracked_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
|
|
| |
| with patch.dict(os.environ, {"STREAMING_GOVERNANCE_ENABLED": "false"}): |
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai", |
| agent_id="test-agent", |
| db=mock_db_session |
| ): |
| result += token |
|
|
| |
| mock_db_session.add.assert_not_called() |
|
|
| assert "Untracked response" in result |
|
|
|
|
| class TestStreamCompletionTokenHandling: |
| """Test suite for token handling in streams.""" |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_handles_none_content(self, handler_with_mocked_client): |
| """Test handling of chunks with None content.""" |
| handler = handler_with_mocked_client |
|
|
| async def stream_with_none(): |
| |
| chunk1 = MagicMock() |
| chunk1.choices = [MagicMock(delta=MagicMock(content="Hello"))] |
| yield chunk1 |
|
|
| |
| chunk2 = MagicMock() |
| chunk2.choices = [MagicMock(delta=MagicMock(content=None))] |
| yield chunk2 |
|
|
| |
| chunk3 = MagicMock() |
| chunk3.choices = [MagicMock(delta=MagicMock(content=" world"))] |
| yield chunk3 |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=stream_with_none() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Hello world" in result |
|
|
| @pytest.mark.asyncio |
| async def test_stream_completion_handles_missing_delta_attribute(self, handler_with_mocked_client): |
| """Test handling of chunks without delta attribute.""" |
| handler = handler_with_mocked_client |
|
|
| async def stream_without_delta(): |
| chunk1 = MagicMock() |
| chunk1.choices = [MagicMock(delta=MagicMock(content="Start"))] |
| yield chunk1 |
|
|
| |
| chunk2 = MagicMock() |
| chunk2.choices = [MagicMock()] |
| yield chunk2 |
|
|
| chunk3 = MagicMock() |
| chunk3.choices = [MagicMock(delta=MagicMock(content="End"))] |
| yield chunk3 |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=stream_without_delta() |
| ) |
|
|
| messages = [{"role": "user", "content": "Test"}] |
| result = "" |
|
|
| |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Start" in result or "End" in result |
|
|
|
|
| class TestVisionHandling: |
| """Test suite for _get_coordinated_vision_description method (lines 1309-1370) |
| |
| Tests multimodal input handling: |
| - Base64 image payload processing |
| - URL image payload processing |
| - Vision-capable model selection (excludes REASONING_MODELS_WITHOUT_VISION) |
| - Fallback when primary model lacks vision |
| - Mixed text and image in requests |
| - VISION_ONLY_MODELS usage (Janus, etc.) |
| """ |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_with_base64_image(self, handler_with_mocked_client): |
| """Test base64 image payload is processed correctly""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def vision_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Image shows a button"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=vision_stream() |
| ) |
|
|
| |
| base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" |
|
|
| messages = [{"role": "user", "content": "What's in this image?"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Image shows" in result or "button" in result |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_with_image_url(self, handler_with_mocked_client): |
| """Test URL image payload is processed correctly""" |
| handler = handler_with_mocked_client |
|
|
| async def vision_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="URL image analyzed"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=vision_stream() |
| ) |
|
|
| |
| image_url = "https://example.com/screenshot.png" |
|
|
| messages = [{"role": "user", "content": "Analyze this image"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "URL image" in result or "analyzed" in result |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_selects_vision_capable_model(self, handler_with_mocked_client): |
| """Test model selection excludes REASONING_MODELS_WITHOUT_VISION""" |
| from core.llm.byok_handler import REASONING_MODELS_WITHOUT_VISION |
|
|
| handler = handler_with_mocked_client |
|
|
| |
| assert "deepseek-v3.2" in REASONING_MODELS_WITHOUT_VISION |
| assert "o3" in REASONING_MODELS_WITHOUT_VISION |
|
|
| |
| async def vision_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Vision result"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=vision_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Describe this image"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Vision result" in result |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_fallback_to_separate_models(self, handler_with_mocked_client): |
| """Test fallback when primary model lacks vision support""" |
| handler = handler_with_mocked_client |
|
|
| |
| async def vision_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Fallback vision description"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=vision_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "What do you see?"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Fallback" in result or "vision" in result.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_mixed_text_image(self, handler_with_mocked_client): |
| """Test both text and image are included in request""" |
| handler = handler_with_mocked_client |
|
|
| async def multimodal_stream(): |
| chunk1 = MagicMock() |
| chunk1.choices = [MagicMock(delta=MagicMock(content="I can see "))] |
| yield chunk1 |
|
|
| chunk2 = MagicMock() |
| chunk2.choices = [MagicMock(delta=MagicMock(content="the image"))] |
| yield chunk2 |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=multimodal_stream() |
| ) |
|
|
| messages = [ |
| {"role": "system", "content": "You are a vision assistant."}, |
| {"role": "user", "content": "Describe this screenshot"} |
| ] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "I can see" in result or "the image" in result |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_with_vision_only_model(self, handler_with_mocked_client): |
| """Test VISION_ONLY_MODELS are used appropriately""" |
| from core.llm.byok_handler import REASONING_MODELS_WITHOUT_VISION |
|
|
| handler = handler_with_mocked_client |
|
|
| |
| |
| |
| assert "janus-pro-7b" not in REASONING_MODELS_WITHOUT_VISION |
|
|
| async def janus_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Janus vision analysis"))] |
| yield chunk |
|
|
| |
| handler.async_clients["deepseek"] = AsyncMock() |
| handler.async_clients["deepseek"].chat.completions.create = AsyncMock( |
| return_value=janus_stream() |
| ) |
|
|
| messages = [{"role": "user", "content": "Analyze screenshot"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="janus-pro-7b", |
| provider_id="deepseek" |
| ): |
| result += token |
|
|
| |
| assert "Janus" in result or "vision" in result.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_coordinated_vision_with_image_payload_attribute(self, handler_with_mocked_client): |
| """Test image_payload parameter is handled correctly""" |
| handler = handler_with_mocked_client |
|
|
| async def vision_stream(): |
| chunk = MagicMock() |
| chunk.choices = [MagicMock(delta=MagicMock(content="Image received"))] |
| yield chunk |
|
|
| handler.async_clients["openai"].chat.completions.create = AsyncMock( |
| return_value=vision_stream() |
| ) |
|
|
| |
| |
| messages = [{"role": "user", "content": "Analyze image"}] |
|
|
| result = "" |
| async for token in handler.stream_completion( |
| messages=messages, |
| model="gpt-4o", |
| provider_id="openai" |
| ): |
| result += token |
|
|
| |
| assert "Image" in result or "received" in result |
|
|