Files
567-labs--instructor/tests/test_streaming_reask_bug.py
wehub-resource-sync 97e91a83f3
Ruff / Ruff (push) Has been cancelled
Test / Core Tests (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.10) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.11) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.12) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.13) (push) Has been cancelled
Test / Offline Coverage Tests (Python 3.9) (push) Has been cancelled
Test / Full Coverage (Python 3.11) (push) Has been cancelled
Test / Core Provider Tests (OpenAI) (push) Has been cancelled
Test / Core Provider Tests (Anthropic) (push) Has been cancelled
Test / Core Provider Tests (Google) (push) Has been cancelled
Test / Core Provider Tests (Other) (push) Has been cancelled
Test / Anthropic Tests (push) Has been cancelled
Test / Gemini Tests (push) Has been cancelled
Test / Google GenAI Tests (push) Has been cancelled
Test / Vertex AI Tests (push) Has been cancelled
Test / OpenAI Tests (push) Has been cancelled
Test / Writer Tests (push) Has been cancelled
Test / Auto Client Tests (push) Has been cancelled
ty / type-check (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:38 +08:00

244 lines
7.3 KiB
Python

"""Test for streaming reask bug fix.
Bug: When using streaming mode with max_retries > 1, if validation fails,
the reask handlers crash with "'Stream' object has no attribute 'choices'"
because they expect a ChatCompletion but receive a Stream object.
GitHub Issue: https://github.com/jxnl/instructor/issues/1991
"""
from typing import Any, Optional
import pytest
from pydantic import ValidationError, BaseModel, field_validator
from instructor.mode import Mode
from instructor.processing.response import handle_reask_kwargs
class MockStream:
"""Mock Stream object that mimics openai.Stream behavior."""
def __iter__(self):
return iter([])
def __next__(self):
raise StopIteration
class MockResponsesToolCall:
"""Mock tool call item in a responses output list."""
def __init__(
self,
arguments: str,
name: Optional[str] = None,
call_id: Optional[str] = None,
item_type: str = "function_call",
) -> None:
self.arguments = arguments
self.name = name
self.call_id = call_id
self.type = item_type
class MockResponsesReasoningItem:
"""Mock reasoning item in a responses output list."""
type = "reasoning"
class MockResponsesResponse:
"""Mock Responses API response with output items."""
def __init__(self, output: list[Any]) -> None:
self.output = output
def create_mock_validation_error():
"""Create a real Pydantic ValidationError for testing."""
class TestModel(BaseModel):
name: str
@field_validator("name")
@classmethod
def must_have_space(cls, v):
if " " not in v:
raise ValueError("must contain space")
return v
try:
TestModel(name="John")
except ValidationError as e:
return e
class TestStreamingReaskBug:
"""Tests for the streaming reask bug fix."""
def test_reask_tools_with_stream_object_does_not_crash(self):
"""Test that reask_tools handles Stream objects without crashing.
Previously, this would crash with:
"'Stream' object has no attribute 'choices'"
"""
mock_stream = MockStream()
kwargs = {
"messages": [{"role": "user", "content": "test"}],
"tools": [{"type": "function", "function": {"name": "test"}}],
}
exception = create_mock_validation_error()
# This should not raise an AttributeError
result = handle_reask_kwargs(
kwargs=kwargs,
mode=Mode.TOOLS,
response=mock_stream,
exception=exception,
)
# Should return modified kwargs with error message
assert "messages" in result
assert len(result["messages"]) > 1 # Original + error message
def test_reask_anthropic_tools_with_stream_object(self):
"""Test that Anthropic reask handler handles Stream objects."""
mock_stream = MockStream()
kwargs = {
"messages": [{"role": "user", "content": "test"}],
}
exception = create_mock_validation_error()
result = handle_reask_kwargs(
kwargs=kwargs,
mode=Mode.ANTHROPIC_TOOLS,
response=mock_stream,
exception=exception,
)
assert "messages" in result
def test_reask_with_none_response(self):
"""Test that reask handlers handle None response gracefully."""
kwargs = {
"messages": [{"role": "user", "content": "test"}],
}
exception = create_mock_validation_error()
result = handle_reask_kwargs(
kwargs=kwargs,
mode=Mode.TOOLS,
response=None,
exception=exception,
)
assert "messages" in result
def test_reask_responses_tools_skips_reasoning_items_and_includes_details(self):
"""Test responses reask ignores reasoning items and adds tool details."""
mock_response = MockResponsesResponse(
output=[
MockResponsesReasoningItem(),
MockResponsesToolCall(
arguments='{"name": "Jane"}',
name="extract_person",
call_id="call_123",
),
]
)
kwargs = {
"messages": [{"role": "user", "content": "test"}],
}
exception = create_mock_validation_error()
result = handle_reask_kwargs(
kwargs=kwargs,
mode=Mode.RESPONSES_TOOLS,
response=mock_response,
exception=exception,
)
assert "messages" in result
assert len(result["messages"]) == 2
reask_content = result["messages"][-1]["content"]
assert "tool call name=extract_person, id=call_123" in reask_content
assert '{"name": "Jane"}' in reask_content
def test_reask_md_json_with_stream_object(self):
"""Test that MD_JSON reask handler handles Stream objects."""
mock_stream = MockStream()
kwargs = {
"messages": [{"role": "user", "content": "test"}],
}
exception = create_mock_validation_error()
result = handle_reask_kwargs(
kwargs=kwargs,
mode=Mode.MD_JSON,
response=mock_stream,
exception=exception,
)
assert "messages" in result
@pytest.mark.skipif(
not pytest.importorskip("openai", reason="openai not installed"),
reason="openai not installed",
)
class TestStreamingReaskIntegration:
"""Integration tests that require OpenAI API key."""
@pytest.fixture
def client(self):
"""Create instructor client if API key available."""
import os
if not os.getenv("OPENAI_API_KEY"):
pytest.skip(
"OPENAI_API_KEY not set" # ty: ignore[too-many-positional-arguments]
)
import instructor
from openai import OpenAI
return instructor.from_openai(OpenAI())
def test_streaming_with_retries_and_failing_validator(self, client):
"""Test that streaming with retries doesn't crash on validation failure.
This test verifies that the reask handler doesn't crash with
"'Stream' object has no attribute 'choices'" when validation fails
during streaming. The actual validation outcome depends on LLM behavior.
"""
class ImpossibleModel(BaseModel):
"""Model with a validator that always fails."""
value: str
@field_validator("value")
@classmethod
def always_fail(cls, v: str) -> str: # noqa: ARG003
raise ValueError("This validator always fails for testing")
# This should not crash with AttributeError about Stream.choices
# It should raise InstructorRetryException after retries are exhausted
from instructor.core.exceptions import InstructorRetryException
with pytest.raises(InstructorRetryException):
list(
client.chat.completions.create_partial(
model="gpt-4o-mini",
max_retries=2,
messages=[
{
"role": "user",
"content": "Return value='test'",
}
],
response_model=ImpossibleModel,
)
)