项目文件夹

文件
Rohit C Prasad 7a7e2f90cd Add streaming support to chat completions (stream=True on create/acreate)
Providers yield unified OpenAI-shaped chunks: OpenAI/Ollama stream natively,
Anthropic events are normalized, and other providers get an async bridge over
any sync stream. Streaming is manual tool calling only (no max_turns/MCP).
2026-07-14 22:36:30 -07:00

156 行
5.1 KiB
Python

"""Tests for the streaming client path (create/acreate with stream=True)."""
from unittest.mock import Mock, patch
import pytest
from aisuite import Client
def _stream_provider(chunks):
provider = Mock()
provider.chat_completions_create_stream = Mock(return_value=iter(chunks))
return provider
@patch("aisuite.provider.ProviderFactory.create_provider")
def test_create_stream_routes_to_provider_stream(mock_create_provider):
chunks = [object(), object()]
provider = _stream_provider(chunks)
mock_create_provider.return_value = provider
client = Client()
stream = client.chat.completions.create(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
stream=True,
temperature=0.2,
)
assert list(stream) == chunks
call = provider.chat_completions_create_stream.call_args
assert call.args == ("gpt-4o", [{"role": "user", "content": "hi"}])
# `stream` is consumed by the client; extra settings pass through.
assert call.kwargs == {"temperature": 0.2}
provider.chat_completions_create.assert_not_called()
@patch("aisuite.provider.ProviderFactory.create_provider")
def test_create_stream_converts_tools(mock_create_provider):
"""Streaming is manual tool calling: schemas pass through, callables
become specs — same contract as the non-streaming manual path (#266)."""
provider = _stream_provider([])
mock_create_provider.return_value = provider
def get_weather(location: str):
"""Get the weather for a location."""
return {"location": location}
schema = {
"type": "function",
"function": {
"name": "manual_tool",
"parameters": {"type": "object", "properties": {}},
},
}
client = Client()
client.chat.completions.create(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
tools=[schema, get_weather],
stream=True,
)
sent = provider.chat_completions_create_stream.call_args.kwargs["tools"]
assert sent[0] == schema
assert sent[1]["function"]["name"] == "get_weather"
@patch("aisuite.provider.ProviderFactory.create_provider")
def test_create_stream_with_max_turns_raises(mock_create_provider):
mock_create_provider.return_value = _stream_provider([])
client = Client()
with pytest.raises(ValueError, match="max_turns"):
client.chat.completions.create(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
tools=[{"type": "function", "function": {"name": "t", "parameters": {}}}],
max_turns=3,
stream=True,
)
@patch("aisuite.provider.ProviderFactory.create_provider")
def test_create_stream_rejects_mcp_configs(mock_create_provider):
mock_create_provider.return_value = _stream_provider([])
client = Client()
with pytest.raises(ValueError, match="MCP"):
client.chat.completions.create(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
tools=[{"type": "mcp", "name": "fs", "command": "npx", "args": []}],
stream=True,
)
@patch("aisuite.provider.ProviderFactory.create_provider")
def test_stream_false_keeps_non_streaming_path(mock_create_provider):
provider = Mock()
provider.chat_completions_create = Mock(return_value=Mock(choices=[]))
mock_create_provider.return_value = provider
client = Client()
client.chat.completions.create(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
stream=False,
)
provider.chat_completions_create.assert_called_once()
provider.chat_completions_create_stream.assert_not_called()
@pytest.mark.asyncio
@patch("aisuite.provider.ProviderFactory.create_provider")
async def test_acreate_stream_returns_async_iterator(mock_create_provider):
chunks = [object(), object()]
async def astream(model, messages, **kwargs):
for chunk in chunks:
yield chunk
provider = Mock()
provider.achat_completions_create_stream = astream
mock_create_provider.return_value = provider
client = Client()
stream = await client.chat.completions.acreate(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
stream=True,
)
received = [chunk async for chunk in stream]
assert received == chunks
provider.achat_completions_create.assert_not_called()
@pytest.mark.asyncio
@patch("aisuite.provider.ProviderFactory.create_provider")
async def test_acreate_stream_with_max_turns_raises_eagerly(mock_create_provider):
"""The guard fires at the acreate call, not on first iteration."""
mock_create_provider.return_value = Mock()
client = Client()
with pytest.raises(ValueError, match="max_turns"):
await client.chat.completions.acreate(
model="openai:gpt-4o",
messages=[{"role": "user", "content": "hi"}],
tools=[{"type": "function", "function": {"name": "t", "parameters": {}}}],
max_turns=2,
stream=True,
)