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

> Explore aisuite's async vs sync clients. Understand their differences and discover when to use each to optimize your applications. Learn how aisuite ensures consistent behavior.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: deep-dive
- Published: 2026-07-27

---

**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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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:

```python
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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/aisuite/mcp/client.py) leverages `httpx.AsyncClient`, yet its teardown remains compatible with this same stack.

## Practical Code Examples

### Basic Synchronous Chat

```python
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

```python
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

```python
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

```python
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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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`](https://github.com/andrewyng/aisuite/blob/main/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.