andrewyng--aisuite
7a7e2f90cd
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).
156 行
5.1 KiB
Python
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,
|
|
)
|