"""Tests for the async client path (Completions.acreate).""" from types import SimpleNamespace from unittest.mock import AsyncMock, Mock, patch import pytest from aisuite import Client from aisuite.framework.message import ChatCompletionMessageToolCall, Function, Message def _chat_response(content=None, tool_calls=None): message = Message(role="assistant", content=content, tool_calls=tool_calls) return SimpleNamespace(choices=[SimpleNamespace(message=message)]) def _tool_call(name, arguments, call_id="call_1"): return ChatCompletionMessageToolCall( id=call_id, type="function", function=Function(name=name, arguments=arguments) ) @pytest.mark.asyncio @patch("aisuite.provider.ProviderFactory.create_provider") async def test_acreate_runs_full_tool_loop(mock_create_provider): provider = Mock() provider.achat_completions_create = AsyncMock( side_effect=[ _chat_response( tool_calls=[_tool_call("get_weather", '{"location": "San Francisco"}')] ), _chat_response(content="It is sunny in San Francisco."), ] ) mock_create_provider.return_value = provider def get_weather(location: str): """Get the weather for a location.""" return {"location": location, "condition": "sunny"} client = Client() response = await client.chat.completions.acreate( model="openai:gpt-4o", messages=[{"role": "user", "content": "What is the weather?"}], tools=[get_weather], max_turns=2, ) assert response.choices[0].message.content == "It is sunny in San Francisco." assert provider.achat_completions_create.await_count == 2 @pytest.mark.asyncio @patch("aisuite.provider.ProviderFactory.create_provider") async def test_acreate_without_tools(mock_create_provider): provider = Mock() provider.achat_completions_create = AsyncMock( return_value=_chat_response(content="hello") ) mock_create_provider.return_value = provider client = Client() response = await client.chat.completions.acreate( model="openai:gpt-4o", messages=[{"role": "user", "content": "hi"}], ) assert response.choices[0].message.content == "hello" provider.achat_completions_create.assert_awaited_once() @pytest.mark.asyncio @patch("aisuite.provider.ProviderFactory.create_provider") async def test_acreate_manual_tool_calling_passes_tools(mock_create_provider): """Manual mode (no max_turns): the provider must receive the tool schemas. Async twin of the #266 regression: tools are popped from kwargs, so the manual path must re-add provider-ready specs before delegating. """ provider = Mock() provider.achat_completions_create = AsyncMock( return_value=_chat_response(content="ok") ) 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() await client.chat.completions.acreate( model="openai:gpt-4o", messages=[{"role": "user", "content": "hi"}], tools=[schema, get_weather], # no max_turns → manual mode ) sent = provider.achat_completions_create.await_args.kwargs["tools"] assert sent[0] == schema # dicts pass through untouched assert sent[1]["function"]["name"] == "get_weather" # callables become specs @pytest.mark.asyncio @patch("aisuite.provider.ProviderFactory.create_provider") async def test_acreate_awaits_async_tool(mock_create_provider): """An async def tool is awaited inside the async tool loop.""" provider = Mock() provider.achat_completions_create = AsyncMock( side_effect=[ _chat_response(tool_calls=[_tool_call("lookup", '{"q": "x"}')]), _chat_response(content="done"), ] ) mock_create_provider.return_value = provider async def lookup(q: str): """An async tool.""" return {"q": q, "value": 42} client = Client() response = await client.chat.completions.acreate( model="openai:gpt-4o", messages=[{"role": "user", "content": "look it up"}], tools=[lookup], max_turns=3, ) assert response.choices[0].message.content == "done"