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.
This commit is contained in:
Ahmed Allam
2026-07-30 08:30:06 +03:00
committed by Ahmed Allam
parent 980216860e
commit 885b2ca5c5
2 changed files with 69 additions and 2 deletions
-1
View File
@@ -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]
+69 -1
View File
@@ -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