Files
567-labs--instructor/tests/test_streaming_reasoning_events.py
T
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

214 lines
7.3 KiB
Python

"""Tests for reasoning summary event surfacing in RESPONSES_TOOLS streaming.
These tests verify the fix for issue #2291: ResponseReasoningSummaryTextDeltaEvent
and ResponseReasoningSummaryTextDoneEvent are forwarded to an on_event callback
instead of being silently dropped during create_partial streaming.
All tests use mock event objects — no API key required.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from instructor.dsl.partial import PartialBase
from instructor.mode import Mode
# ---------------------------------------------------------------------------
# Lightweight mock event classes matching openai.types.responses shapes
# ---------------------------------------------------------------------------
class _MockFunctionCallArgumentsDeltaEvent:
"""Mimics ResponseFunctionCallArgumentsDeltaEvent."""
type: str = "response.function_call_arguments.delta"
def __init__(self, delta: str) -> None:
self.delta = delta
class _MockReasoningSummaryTextDeltaEvent:
"""Mimics ResponseReasoningSummaryTextDeltaEvent."""
type: str = "response.reasoning_summary_text.delta"
def __init__(self, delta: str) -> None:
self.delta = delta
class _MockReasoningSummaryTextDoneEvent:
"""Mimics ResponseReasoningSummaryTextDoneEvent."""
type: str = "response.reasoning_summary_text.done"
def __init__(self, text: str) -> None:
self.text = text
# ---------------------------------------------------------------------------
# Monkeypatch helper: replace the openai import inside extract_json
# so our mocks are recognised by isinstance checks.
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def _patch_openai_response_types(monkeypatch: pytest.MonkeyPatch) -> None:
"""Replace openai.types.responses event classes with our mocks."""
import openai.types.responses as responses_mod
monkeypatch.setattr(
responses_mod,
"ResponseFunctionCallArgumentsDeltaEvent",
_MockFunctionCallArgumentsDeltaEvent,
)
monkeypatch.setattr(
responses_mod,
"ResponseReasoningSummaryTextDeltaEvent",
_MockReasoningSummaryTextDeltaEvent,
)
monkeypatch.setattr(
responses_mod,
"ResponseReasoningSummaryTextDoneEvent",
_MockReasoningSummaryTextDoneEvent,
)
# ---------------------------------------------------------------------------
# Sync tests — extract_json
# ---------------------------------------------------------------------------
class TestExtractJsonReasoningEvents:
"""Tests for PartialBase.extract_json with RESPONSES_TOOLS mode."""
def test_yields_function_call_deltas(self) -> None:
"""Existing behaviour: FunctionCallArgumentsDelta events yield JSON text."""
chunks = [
_MockFunctionCallArgumentsDeltaEvent('{"name":'),
_MockFunctionCallArgumentsDeltaEvent('"Alice"}'),
]
result = list(PartialBase.extract_json(iter(chunks), Mode.RESPONSES_TOOLS))
assert result == ['{"name":', '"Alice"}']
def test_forwards_reasoning_delta_to_on_event(self) -> None:
"""Reasoning delta events are forwarded to on_event callback."""
reasoning_chunk = _MockReasoningSummaryTextDeltaEvent("thinking...")
fn_chunk = _MockFunctionCallArgumentsDeltaEvent('{"a":1}')
chunks = [reasoning_chunk, fn_chunk]
callback = MagicMock()
json_parts = list(
PartialBase.extract_json(
iter(chunks), Mode.RESPONSES_TOOLS, on_event=callback
)
)
# JSON yield is preserved
assert json_parts == ['{"a":1}']
# Callback was invoked with the reasoning event
callback.assert_called_once_with(reasoning_chunk)
def test_forwards_reasoning_done_to_on_event(self) -> None:
"""Reasoning done events are forwarded to on_event callback."""
done_chunk = _MockReasoningSummaryTextDoneEvent("full reasoning text")
chunks = [done_chunk]
callback = MagicMock()
list(
PartialBase.extract_json(
iter(chunks), Mode.RESPONSES_TOOLS, on_event=callback
)
)
callback.assert_called_once_with(done_chunk)
def test_no_on_event_silently_ignores_reasoning_events(self) -> None:
"""Without on_event, reasoning events are silently dropped (backward compat)."""
chunks: list[Any] = [
_MockReasoningSummaryTextDeltaEvent("ignored"),
_MockFunctionCallArgumentsDeltaEvent('{"ok":true}'),
]
# Should not raise — reasoning events are simply skipped
result = list(PartialBase.extract_json(iter(chunks), Mode.RESPONSES_TOOLS))
assert result == ['{"ok":true}']
def test_works_with_inbuilt_tools_mode(self) -> None:
"""on_event also works with RESPONSES_TOOLS_WITH_INBUILT_TOOLS mode."""
reasoning_chunk = _MockReasoningSummaryTextDeltaEvent("reasoning")
fn_chunk = _MockFunctionCallArgumentsDeltaEvent('{"b":2}')
chunks = [reasoning_chunk, fn_chunk]
callback = MagicMock()
json_parts = list(
PartialBase.extract_json(
iter(chunks),
Mode.RESPONSES_TOOLS_WITH_INBUILT_TOOLS,
on_event=callback,
)
)
assert json_parts == ['{"b":2}']
callback.assert_called_once_with(reasoning_chunk)
# ---------------------------------------------------------------------------
# Async tests — extract_json_async
# ---------------------------------------------------------------------------
async def _async_iter(items: list[Any]):
"""Convert a list into an async generator."""
for item in items:
yield item
class TestExtractJsonAsyncReasoningEvents:
"""Tests for PartialBase.extract_json_async with RESPONSES_TOOLS mode."""
@pytest.mark.asyncio
async def test_forwards_reasoning_events_async(self) -> None:
"""Async path forwards reasoning events to sync callback."""
reasoning_chunk = _MockReasoningSummaryTextDeltaEvent("async thinking")
fn_chunk = _MockFunctionCallArgumentsDeltaEvent('{"c":3}')
callback = MagicMock()
json_parts: list[str] = []
async for part in PartialBase.extract_json_async(
_async_iter([reasoning_chunk, fn_chunk]),
Mode.RESPONSES_TOOLS,
on_event=callback,
):
json_parts.append(part)
assert json_parts == ['{"c":3}']
callback.assert_called_once_with(reasoning_chunk)
@pytest.mark.asyncio
async def test_async_on_event_callback_awaited(self) -> None:
"""Async callbacks are properly awaited in extract_json_async."""
received: list[Any] = []
async def async_callback(event: Any) -> None:
received.append(event)
reasoning_chunk = _MockReasoningSummaryTextDeltaEvent("awaited")
fn_chunk = _MockFunctionCallArgumentsDeltaEvent('{"d":4}')
json_parts: list[str] = []
async for part in PartialBase.extract_json_async(
_async_iter([reasoning_chunk, fn_chunk]),
Mode.RESPONSES_TOOLS,
on_event=async_callback,
):
json_parts.append(part)
assert json_parts == ['{"d":4}']
assert len(received) == 1
assert received[0] is reasoning_chunk