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
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""
|
|
Tests that IncompleteOutputException propagates directly from retry_sync/retry_async
|
|
without being wrapped in InstructorRetryException.
|
|
|
|
Regression tests for issue #2273.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
from pydantic import BaseModel
|
|
|
|
import instructor
|
|
from instructor.core.exceptions import IncompleteOutputException
|
|
from instructor.mode import Mode
|
|
|
|
|
|
class User(BaseModel):
|
|
name: str
|
|
age: int
|
|
|
|
|
|
def _make_truncated_response() -> Mock:
|
|
"""Return a mock response that simulates a MAX_TOKENS truncation."""
|
|
mock_response = Mock()
|
|
mock_response.choices = [Mock()]
|
|
mock_response.choices[0].message = Mock()
|
|
mock_response.choices[0].message.content = '{"name": "Alice"' # truncated JSON
|
|
mock_response.choices[0].finish_reason = "length"
|
|
mock_response.usage = None
|
|
return mock_response
|
|
|
|
|
|
def _raise_incomplete(*_args, **_kwargs):
|
|
raise IncompleteOutputException(last_completion=None)
|
|
|
|
|
|
def test_incomplete_output_exception_is_catchable_sync():
|
|
"""IncompleteOutputException must be catchable directly, not wrapped."""
|
|
mock_client = Mock()
|
|
mock_client.chat = Mock()
|
|
mock_client.chat.completions = Mock()
|
|
mock_client.chat.completions.create = Mock(side_effect=_raise_incomplete)
|
|
|
|
client = instructor.patch(mock_client, mode=Mode.TOOLS)
|
|
|
|
with pytest.raises(IncompleteOutputException):
|
|
client.chat.completions.create( # ty: ignore[no-matching-overload] - runtime-patched API
|
|
model="gpt-4o-mini",
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "test"}],
|
|
max_retries=0,
|
|
)
|
|
|
|
|
|
def test_incomplete_output_not_wrapped_in_instructor_retry_exception_sync():
|
|
"""IncompleteOutputException must NOT be wrapped in InstructorRetryException."""
|
|
mock_client = Mock()
|
|
mock_client.chat = Mock()
|
|
mock_client.chat.completions = Mock()
|
|
mock_client.chat.completions.create = Mock(side_effect=_raise_incomplete)
|
|
|
|
client = instructor.patch(mock_client, mode=Mode.TOOLS)
|
|
|
|
with pytest.raises(IncompleteOutputException):
|
|
client.chat.completions.create( # ty: ignore[no-matching-overload] - runtime-patched API
|
|
model="gpt-4o-mini",
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "test"}],
|
|
max_retries=3,
|
|
)
|
|
|
|
|
|
def test_incomplete_output_exception_with_max_retries_zero():
|
|
"""IncompleteOutputException is raised immediately when max_retries=0."""
|
|
call_count = {"n": 0}
|
|
|
|
def _raise_incomplete_and_count(*_args, **_kwargs):
|
|
call_count["n"] += 1
|
|
raise IncompleteOutputException(last_completion=None)
|
|
|
|
mock_client = Mock()
|
|
mock_client.chat = Mock()
|
|
mock_client.chat.completions = Mock()
|
|
mock_client.chat.completions.create = _raise_incomplete_and_count
|
|
|
|
client = instructor.patch(mock_client, mode=Mode.TOOLS)
|
|
|
|
with pytest.raises(IncompleteOutputException):
|
|
client.chat.completions.create( # ty: ignore[no-matching-overload] - runtime-patched API
|
|
model="gpt-4o-mini",
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "test"}],
|
|
max_retries=0,
|
|
)
|
|
|
|
# Should only have been called once (no retries for IncompleteOutputException)
|
|
assert call_count["n"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_incomplete_output_exception_is_catchable_async():
|
|
"""Async path: IncompleteOutputException must be directly catchable."""
|
|
mock_client = Mock()
|
|
mock_client.chat = Mock()
|
|
mock_client.chat.completions = Mock()
|
|
mock_client.chat.completions.create = AsyncMock(side_effect=_raise_incomplete)
|
|
|
|
client = instructor.patch(mock_client, mode=Mode.TOOLS)
|
|
|
|
with pytest.raises(IncompleteOutputException):
|
|
await client.chat.completions.create( # ty: ignore[no-matching-overload] - runtime-patched API
|
|
model="gpt-4o-mini",
|
|
response_model=User,
|
|
messages=[{"role": "user", "content": "test"}],
|
|
max_retries=0,
|
|
)
|