aisuite Async vs Sync Clients: Key Differences and When to Use Each

aisuite provides a single Client class where synchronous and asynchronous operations are exposed as parallel method pairs—create blocks on I/O while acreate returns an awaitable coroutine—so the functional behavior, tool loops, and error handling remain identical across both paths.

The andrewyng/aisuite library unifies multiple LLM providers behind one interface. When evaluating the difference between aisuite async and sync clients, you are not choosing separate objects; you are selecting between blocking and non-blocking I/O models within the same Client instance. Both paths share the same validation logic, thinking-content extraction, and MCP cleanup helpers, differing only in how they suspend execution while waiting for the LLM or external tool calls.

Core Design: One Class, Two Execution Models

aisuite does not split its API into discrete synchronous and asynchronous client classes. Instead, aisuite/client.py defines one Client that routes requests through matching method pairs on the Completions class.

Synchronous Entry Points

The synchronous path starts at Client.chat.completions.create(...), which delegates to Completions.create. According to the source code in aisuite/client.py (lines 85-112), this method prepares the provider, extracts parameters such as max_turns and tools, and then either calls provider.chat_completions_create(...) directly or enters the blocking tool loop _tool_runner. The final response is normalized through _extract_thinking_content before it is returned.

Asynchronous Entry Points

The asynchronous counterpart is Client.chat.completions.acreate(...), implemented in aisuite/client.py (lines 84-124) as Completions.acreate. This coroutine mirrors the sync setup exactly but awaits provider.achat_completions_create(...) and, when tools are enabled, delegates to _atool_runner instead. It also applies _extract_thinking_content to the final result.

How the Sync Client Works

Understanding the sync path in andrewyng/aisuite helps clarify what the async variant is replacing.

Blocking Provider Calls and Tool Loops

Inside aisuite/client.py, Completions.create handles streaming by invoking provider.chat_completions_create_stream(...) or, for standard requests, provider.chat_completions_create(...). When max_turns and tools are supplied, control passes to _tool_runner (lines 83-119). This helper runs a regular while loop that blocks until each tool execution via tools_instance.execute_tool finishes before requesting the next model turn.

Synchronous Streaming

If stream=True, the sync method returns a conventional iterator. You consume the OpenAI-shaped chunks with a standard for loop, blocking between each network read.

How the Async Client Works

The async variant keeps the exact same semantics but replaces every blocking call with an awaitable equivalent.

Awaitable Provider Calls

Completions.acreate awaits the underlying coroutine:

response = await provider.achat_completions_create(...)

This allows the Python event loop to schedule other tasks while aisuite waits for the provider's HTTP response.

Asynchronous Tool Execution

Multi-turn tool loops are handled by _atool_runner in aisuite/client.py (lines 95-144). Like its sync sibling, it manages turns and reasoning extraction, but it awaits both provider.achat_completions_create(...) and tools_instance.aexecute_tool(...). The tool abstraction in aisuite/utils/tools.py exposes both execute_tool (sync) and aexecute_tool (async) methods, which the respective runners consume. Consequently, the async loop yields control during each external call instead of monopolizing the thread.

Async Streaming

When stream=True is passed to acreate, the method returns an async iterator from provider.achat_completions_create_stream(...). You consume it with async for, letting the event loop interleave other coroutines between chunks.

Error Handling and Resource Cleanup

Both execution paths rely on the same validation and tracing utilities. They also share the _emit_model_error helper for consistent exception formatting. Notably, even the async path uses the synchronous ExitStack for resource management because aisuite/client.py only needs to invoke synchronous __exit__ cleanup actions after the async call finishes. The async MCP HTTP client in aisuite/mcp/client.py leverages httpx.AsyncClient, yet its teardown remains compatible with this same stack.

Practical Code Examples

Basic Synchronous Chat

import aisuite

# Initialize the client (provider config omitted for brevity)

client = aisuite.Client(provider_configs={"openai": {"api_key": "…"}})

# Simple chat completion (blocking)

response = client.chat.completions.create(
    model="openai:gpt-4o",
    messages=[{"role": "user", "content": "Hello"}],
    max_turns=3,               # Enable multi-turn tool loop

    tools=[my_tool_function],  # Callable tools or MCP configs

)

print(response.choices[0].message.content)

Basic Asynchronous Chat

import asyncio
import aisuite

async def main():
    client = aisuite.Client(provider_configs={"openai": {"api_key": "…"}})

    # Async chat completion – note the await

    response = await client.chat.completions.acreate(
        model="openai:gpt-4o",
        messages=[{"role": "user", "content": "Hello"}],
        max_turns=3,
        tools=[my_async_tool],
    )

    print(response.choices[0].message.content)

asyncio.run(main())

Synchronous Streaming

for chunk in client.chat.completions.create(
    model="openai:gpt-4o-mini",
    messages=[{"role": "user", "content": "Tell me a story"}],
    stream=True,
):
    print(chunk.choices[0].delta.content, end="")

Asynchronous Streaming

async for chunk in client.chat.completions.acreate(
    model="openai:gpt-4o-mini",
    messages=[{"role": "user", "content": "Tell me a story"}],
    stream=True,
):
    print(chunk.choices[0].delta.content, end="")

Choosing Between aisuite Async and Sync Clients

The decision depends entirely on your runtime environment and concurrency requirements.

  • Use the synchronous client when your code is already blocking, such as in scripts, CLI tools, or legacy applications where introducing an event loop adds unnecessary complexity.
  • Use the asynchronous client when you operate inside an async ecosystem like FastAPI, asyncio-based bots, or high-throughput services. The async version lets you interleave other coroutines while waiting for the LLM or external tool calls, maximizing I/O concurrency without extra threads.

Summary

  • aisuite exposes async and sync behavior through parallel method pairs on a single Client class: create and acreate.
  • The sync path in aisuite/client.py uses blocking provider methods and _tool_runner, while the async path awaits provider.achat_completions_create(...) and _atool_runner.
  • Tool-loop logic is functionally identical; only the I/O primitives change between execute_tool and aexecute_tool as defined in aisuite/utils/tools.py.
  • Both paths share the same validation, error emission (_emit_model_error), and ExitStack cleanup.
  • Sync streaming returns a conventional iterator; async streaming returns an async iterator consumed with async for.
  • Choose sync for simple scripts and async for event-loop frameworks where non-blocking I/O improves throughput.

Frequently Asked Questions

Does aisuite have a separate AsyncClient class?

No. The andrewyng/aisuite source code defines only one Client class. The async interface is exposed through acreate methods on the same Completions object, not through a separate async-specific class hierarchy.

How does tool execution differ between aisuite async and sync clients?

It does not differ logically. Completions._tool_runner and Completions._atool_runner implement the same multi-turn workflow. The only distinction is that the sync variant calls tools_instance.execute_tool in a blocking loop, whereas the async variant awaits tools_instance.aexecute_tool so the event loop remains unblocked.

Why does the async client use a synchronous ExitStack?

According to aisuite/client.py, the ExitStack manages context objects whose cleanup actions (__exit__) are synchronous. Because aisuite performs this cleanup after the awaited provider call completes, the synchronous stack is still safe and correct in the async path. The underlying MCP transport in aisuite/mcp/client.py may use httpx.AsyncClient, but its teardown integrates with this same stack.

Can I use the same Client instance for both sync and async requests?

Yes. Because aisuite async and sync clients are simply method pairs on a single Client, you can call create in one part of your application and acreate in another. You must ensure that coroutines are properly awaited inside an async function, but the instance itself requires no special separation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →