From 885b2ca5c55d5295f64c563da127cb2909e1fc30 Mon Sep 17 00:00:00 2001 From: Ahmed Allam Date: Thu, 30 Jul 2026 05:10:05 +0000 Subject: [PATCH] test(llm): cover the full run loop against a non-streaming gateway; drop README note Adds an integration test that drives Runner.run_streamed against a non-streaming gateway through _NonStreamingModel: the synthetic terminal event feeds the runner, which executes the tool call and continues to a final answer over two non-streaming turns. Removes the README env-var note. --- README.md | 1 - tests/test_disable_streaming.py | 70 ++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47fad3b2..982a635d 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,6 @@ export LLM_API_KEY="your-api-key" export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio export PERPLEXITY_API_KEY="your-api-key" # for search capabilities export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, quick scan: medium) -export LLM_DISABLE_STREAMING="true" # for OpenAI-compatible endpoints that don't support streaming ``` > [!NOTE] diff --git a/tests/test_disable_streaming.py b/tests/test_disable_streaming.py index 16afafbf..1d667e61 100644 --- a/tests/test_disable_streaming.py +++ b/tests/test_disable_streaming.py @@ -16,9 +16,11 @@ from http.server import BaseHTTPRequestHandler, HTTPServer from typing import TYPE_CHECKING, Any import pytest +from agents import Agent, Runner, function_tool from agents.model_settings import ModelSettings -from agents.models.interface import Model, ModelTracing +from agents.models.interface import Model, ModelProvider, ModelTracing from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel +from agents.run import RunConfig from openai import AsyncOpenAI, BadRequestError from openai.types.responses import ( ResponseCompletedEvent, @@ -205,6 +207,72 @@ async def test_wrapper_get_response_stays_non_streaming(gateway_url: str) -> Non assert tool_call.name == "do_thing" +_TURN_STREAM_FLAGS: list[bool] = [] + + +class _MultiTurnHandler(BaseHTTPRequestHandler): + """Non-streaming gateway: a tool call on turn 1, a final answer on turn 2.""" + + def log_message(self, *args: Any) -> None: + pass + + def do_POST(self) -> None: + length = int(self.headers.get("Content-Length", 0)) + body = json.loads(self.rfile.read(length) or b"{}") + _TURN_STREAM_FLAGS.append(bool(body.get("stream"))) + completion = _tool_call_completion() if len(_TURN_STREAM_FLAGS) == 1 else _text_completion() + if len(_TURN_STREAM_FLAGS) > 1: + completion["choices"][0]["message"]["content"] = "all done" + payload = json.dumps(completion).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + +@pytest.fixture +def multiturn_url() -> Iterator[str]: + _TURN_STREAM_FLAGS.clear() + server = HTTPServer(("127.0.0.1", 0), _MultiTurnHandler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_address[1]}/v1" + finally: + server.shutdown() + server.server_close() + + +@pytest.mark.asyncio +async def test_run_loop_executes_tool_and_completes_without_streaming(multiturn_url: str) -> None: + # The whole streamed agent loop runs against a non-streaming gateway: the + # synthetic terminal event feeds the runner, which executes the tool and + # continues the turn until a final answer. + calls: list[int] = [] + + @function_tool + def do_thing(n: int) -> str: + calls.append(n) + return f"did {n}" + + class _Provider(ModelProvider): + def get_model(self, model_name: str | None) -> Model: # noqa: ARG002 + return _NonStreamingModel(_model(multiturn_url)) + + agent = Agent(name="t", instructions="use the tool", tools=[do_thing], model="gw-model") + result = Runner.run_streamed( + agent, input="please", run_config=RunConfig(model_provider=_Provider()) + ) + async for _ in result.stream_events(): + pass + + assert calls == [1] # tool executed with the streamed tool-call args + assert result.final_output == "all done" + assert len(_TURN_STREAM_FLAGS) == 2 # two turns, both... + assert not any(_TURN_STREAM_FLAGS) # ...issued as non-streaming requests + + class _DummyModel(Model): async def get_response(self, *args: Any, **kwargs: Any) -> Any: raise NotImplementedError