diff --git a/.gitignore b/.gitignore index e66cd370..bee4b907 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,4 @@ cython_debug/ # CAI files .cai/ .vscode/ -cai_env/ + diff --git a/tests/core/test_openai_chatcompletions.py b/tests/core/test_openai_chatcompletions.py index 89a00627..afb45526 100644 --- a/tests/core/test_openai_chatcompletions.py +++ b/tests/core/test_openai_chatcompletions.py @@ -174,3 +174,119 @@ async def test_get_response_with_tool_call(monkeypatch) -> None: assert fn_call_item.name == "do_thing" assert fn_call_item.arguments == "{'x':1}" +@pytest.mark.asyncio +async def test_fetch_response_non_stream(monkeypatch) -> None: + """ + Verify that `_fetch_response` builds the correct OpenAI API call when not + streaming and returns the ChatCompletion object directly. We supply a + dummy ChatCompletion through a stubbed OpenAI client and inspect the + captured kwargs. + """ + + # Dummy completions to record kwargs + class DummyCompletions: + def __init__(self) -> None: + self.kwargs: dict[str, Any] = {} + + async def create(self, **kwargs: Any) -> Any: + self.kwargs = kwargs + return chat + + class DummyClient: + def __init__(self, completions: DummyCompletions) -> None: + self.chat = type("_Chat", (), {"completions": completions})() + self.base_url = httpx.URL("http://fake") + + msg = ChatCompletionMessage(role="assistant", content="ignored") + choice = Choice(index=0, finish_reason="stop", message=msg) + chat = ChatCompletion( + id="resp-id", + created=0, + model="fake", + object="chat.completion", + choices=[choice], + ) + completions = DummyCompletions() + dummy_client = DummyClient(completions) + model = OpenAIChatCompletionsModel(model=cai_model, openai_client=dummy_client) # type: ignore + # Execute the private fetch with a system instruction and simple string input. + with generation_span(disabled=True) as span: + result = await model._fetch_response( + system_instructions="sys", + input="hi", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + span=span, + tracing=ModelTracing.DISABLED, + stream=False, + ) + + # Ensure expected args were passed through to OpenAI client. + kwargs = completions.kwargs + assert kwargs["stream"] is False + assert kwargs["store"] is True + assert kwargs["model"] == cai_model + assert kwargs["messages"][0]["role"] == "system" + assert kwargs["messages"][0]["content"] == "sys" + assert kwargs["messages"][1]["role"] == "user" + # Defaults for optional fields become the NOT_GIVEN sentinel + assert kwargs["tools"] is NOT_GIVEN + assert kwargs["tool_choice"] is NOT_GIVEN + assert kwargs["response_format"] is NOT_GIVEN + assert kwargs["stream_options"] is NOT_GIVEN + + +@pytest.mark.asyncio +async def test_fetch_response_stream(monkeypatch) -> None: + """ + When `stream=True`, `_fetch_response` should return a bare `Response` + object along with the underlying async stream. The OpenAI client call + should include `stream_options` to request usage-delimited chunks. + """ + os.environ['CAI_STREAM'] = 'true' + async def event_stream() -> AsyncIterator[ChatCompletionChunk]: + if False: # pragma: no cover + yield # pragma: no cover + + class DummyCompletions: + def __init__(self) -> None: + self.kwargs: dict[str, Any] = {} + + async def create(self, **kwargs: Any) -> Any: + self.kwargs = kwargs + return event_stream() + + class DummyClient: + def __init__(self, completions: DummyCompletions) -> None: + self.chat = type("_Chat", (), {"completions": completions})() + self.base_url = httpx.URL("http://fake") + + completions = DummyCompletions() + dummy_client = DummyClient(completions) + model = OpenAIChatCompletionsModel(model=cai_model, openai_client=dummy_client) # type: ignore + with generation_span(disabled=True) as span: + response, stream = await model._fetch_response( + system_instructions=None, + input="hi", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + span=span, + tracing=ModelTracing.DISABLED, + stream=True, + ) + # Check OpenAI client was called for streaming + assert completions.kwargs["stream"] is True + assert completions.kwargs["store"] is True + assert completions.kwargs["stream_options"] == {"include_usage": True} + # Response is a proper openai Response + assert isinstance(response, Response) + assert response.id == FAKE_RESPONSES_ID + assert response.model == cai_model + assert response.object == "response" + assert response.output == [] + # We returned the async iterator produced by our dummy. + assert hasattr(stream, "__aiter__") \ No newline at end of file