""" Tests for chat completions streaming wrapper. Tests that the wrapper correctly: - Wraps Responses API - Enables reasoning automatically - Converts reasoning to tags - Streams both reasoning and content """ import json import pytest from httpx import AsyncClient from src.chat import constants @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_enables_reasoning(async_client: AsyncClient): """Test that streaming wrapper automatically enables reasoning.""" request_data = { "model": "lorem-tester", "messages": [ {"role": "user", "content": "Test message"} ], "stream": True } chunks_received = [] think_tags_found = False async with async_client.stream( "POST", "/v1/chat/completions", json=request_data, timeout=20.0, ) as response: assert response.status_code == 200 assert response.headers["content-type"] == "text/event-stream; charset=utf-8" async for line in response.aiter_lines(): if not line.strip(): continue if line.startswith("data: "): data_str = line[6:].strip() if data_str == "[DONE]": break try: chunk = json.loads(data_str) chunks_received.append(chunk) # Check for tags in delta content if "choices" in chunk and len(chunk["choices"]) > 0: delta = chunk["choices"][0].get("delta", {}) content = delta.get("content") if content and ("" in content or "" in content): think_tags_found = True except json.JSONDecodeError: pass # Should have received chunks assert len(chunks_received) > 0 # Should have found tags (reasoning enabled automatically) assert think_tags_found, "Expected tags in streaming output" @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_reasoning_before_content(async_client: AsyncClient): """Test that reasoning ( tags) comes before actual content.""" request_data = { "model": "lorem-tester", "messages": [ {"role": "user", "content": "Explain something"} ], "stream": True } all_content = [] found_think_opening = False found_think_closing = False found_content_after_think = False async with async_client.stream( "POST", "/v1/chat/completions", json=request_data, timeout=20.0, ) as response: assert response.status_code == 200 async for line in response.aiter_lines(): if not line.strip(): continue if line.startswith("data: "): data_str = line[6:].strip() if data_str == "[DONE]": break try: chunk = json.loads(data_str) if "choices" in chunk and len(chunk["choices"]) > 0: delta = chunk["choices"][0].get("delta", {}) content = delta.get("content", "") if content: all_content.append(content) if "" in content: found_think_opening = True if "" in content: found_think_closing = True # Content after closing think tag if found_think_closing and content.strip() and "" not in content and "" not in content: found_content_after_think = True except json.JSONDecodeError: pass # Verify ordering full_text = "".join(all_content) if found_think_opening and found_think_closing: # Reasoning should come before main content think_start = full_text.index("") think_end = full_text.index("") assert think_start < think_end, "Opening should come before closing " @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_proper_chunk_structure(async_client: AsyncClient): """Test that streaming chunks have proper structure.""" request_data = { "model": "lorem-tester", "messages": [ {"role": "user", "content": "Hello"} ], "temperature": 0.8, "stream": True } first_chunk = None last_chunk = None chunk_count = 0 async with async_client.stream( "POST", "/v1/chat/completions", json=request_data, timeout=20.0, ) as response: assert response.status_code == 200 async for line in response.aiter_lines(): if not line.strip(): continue if line.startswith("data: "): data_str = line[6:].strip() if data_str == "[DONE]": break try: chunk = json.loads(data_str) chunk_count += 1 # Verify chunk structure assert "id" in chunk assert "object" in chunk assert chunk["object"] == constants.CHAT_COMPLETION_CHUNK_OBJECT assert "created" in chunk assert "model" in chunk assert chunk["model"] == "lorem-tester" assert "choices" in chunk assert len(chunk["choices"]) == 1 choice = chunk["choices"][0] assert "index" in choice assert choice["index"] == 0 assert "delta" in choice if first_chunk is None: first_chunk = chunk last_chunk = chunk except json.JSONDecodeError: pass # Verify we got chunks assert chunk_count > 0 assert first_chunk is not None assert last_chunk is not None # First chunk should have role assert first_chunk["choices"][0]["delta"].get("role") == constants.ROLE_ASSISTANT # Last chunk should have finish_reason assert last_chunk["choices"][0].get("finish_reason") == constants.FINISH_REASON_STOP @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_with_system_message(async_client: AsyncClient): """Test streaming with system message.""" request_data = { "model": "lorem-tester", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello"} ], "stream": True } chunks_received = [] async with async_client.stream( "POST", "/v1/chat/completions", json=request_data, timeout=20.0, ) as response: assert response.status_code == 200 async for line in response.aiter_lines(): if not line.strip(): continue if line.startswith("data: "): data_str = line[6:].strip() if data_str == "[DONE]": break try: chunk = json.loads(data_str) chunks_received.append(chunk) except json.JSONDecodeError: pass # Should handle system message properly assert len(chunks_received) > 0 # First chunk should still have assistant role assert chunks_received[0]["choices"][0]["delta"].get("role") == constants.ROLE_ASSISTANT @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_pipeline_prefix(async_client: AsyncClient): """Test streaming with pipeline prefix in model name.""" request_data = { "model": "some_pipeline.lorem-tester", "messages": [ {"role": "user", "content": "Test"} ], "stream": True } chunks_received = [] async with async_client.stream( "POST", "/v1/chat/completions", json=request_data, timeout=20.0, ) as response: assert response.status_code == 200 async for line in response.aiter_lines(): if not line.strip(): continue if line.startswith("data: "): data_str = line[6:].strip() if data_str == "[DONE]": break try: chunk = json.loads(data_str) chunks_received.append(chunk) # Model should keep original name (with prefix) assert chunk["model"] == "some_pipeline.lorem-tester" except json.JSONDecodeError: pass assert len(chunks_received) > 0 @pytest.mark.unit @pytest.mark.asyncio async def test_streaming_wrapper_non_streaming_fallback(async_client: AsyncClient): """Test that non-streaming request works through wrapper.""" request_data = { "model": "lorem-tester", "messages": [ {"role": "user", "content": "Hello"} ], "stream": False # Non-streaming } response = await async_client.post( "/v1/chat/completions", json=request_data, timeout=20.0 ) assert response.status_code == 200 data = response.json() # Verify structure assert "id" in data assert "object" in data assert data["object"] == constants.CHAT_COMPLETION_OBJECT assert "choices" in data assert len(data["choices"]) == 1 choice = data["choices"][0] assert "message" in choice assert choice["message"]["role"] == constants.ROLE_ASSISTANT assert choice["message"]["content"] # Should have content # Should have tags in content (reasoning enabled) assert "" in choice["message"]["content"] assert "" in choice["message"]["content"]