Choosing the right Microsoft Agent Framework client
If you have followed this series so far, you have already seen us use a handful of different clients to create agents in Microsoft Agent Framework (MAF). The first hands-on article used FoundryAgent. The persistent agents article used FoundryChatClient and called it the recommended approach for Foundry persistence. The workflow articles switched to OpenAIChatClient. That is a lot of clients for what looks, on the surface, like the same thing — “give me an agent that talks to a model.”
The reason for the variety is simple. MAF deliberately separates which model surface you talk to from how the agent and its conversation are managed. Each client class targets a different combination of those two questions. Picking the right one up front saves a surprising amount of refactoring later, especially when you start adding hosted tools, adding persistence, or moving an agent to production.
In this article, we will pull all of these clients into one place, explain what each one is for, and build the same simple weather agent three different ways so you can see what changes and what stays the same.
The mental model: client first, agent second
In MAF, an agent is a thin object. Almost everything that determines runtime behavior, which API surface you call, which tools you can attach, where conversation state lives, is decided by the client you give the agent.
When you write code like this:
|
|
the chat_client on the left of .as_agent is doing more work than it looks. It carries:
- the model endpoint and the deployed model name
- the credential used to authenticate every call
- the API surface (Chat Completions, Responses, Foundry Agent Service)
- the set of hosted tools available to the agent
- the strategy for storing or persisting conversations
If you swap the client, all of those change at once. Swap the agent definition, and only the persona changes. That is why the rest of this article is organized around clients rather than agents.
Before you pick a client, three questions decide most of the answers for you.
Where does your model live? If you are deploying models through Microsoft Foundry, your client choice is between FoundryChatClient (use the Foundry-deployed model directly) or FoundryAgent (let Foundry Agent Service own the agent lifecycle). If you are pointing directly at Azure OpenAI or to OpenAI, OpenAIChatClient covers both — it switches paths based on whether you pass an Azure credential or an OpenAI API key. If you need richer hosted tools, the Responses API has its own client. If you are running a model locally, Ollama, LM Studio, Foundry Local, you bring your own IChatClient and use the generic agent path.
Do you need hosted tools? Hosted tools like web search, file search, and code interpreter are tied to specific API surfaces. The Responses API path (Azure or OpenAI) gives you the richest set today. Chat Completions clients support fewer hosted tools, but they are stable and broadly available. The Foundry Agent Service exposes its own hosted tools through FoundryAgent.
Who owns conversation state? If you want the platform to track threads, store messages, and survive process restarts, the Foundry Agent Service path (FoundryAgent) is the natural fit. If your application owns the conversation in its own database and just wants a stateless model call, any of the chat or response clients will work. We will revisit this trade-off later in the series, when we cover AgentSession for multi-turn conversations.
These three questions almost always narrow you down to one or two choices. The sections below walk through each client in turn.
OpenAIChatClient
This is the client we have used in the workflow articles. It lives in agent_framework.openai and targets the Chat Completions API. The same class covers both Azure OpenAI and OpenAI direct: pass DefaultAzureCredential for the Azure path, or api_key=... for OpenAI direct.
|
|
The constructor takes an explicit model= parameter rather than reading the deployment name from a single environment variable, so the four-getenv chain in the example resolves the model name from whichever variable your environment uses. Chat Completions is the most stable and widely available surface; if your team has an Azure OpenAI resource, this is the safest starting point. Hosted tools available here include function tools and a limited set of others; richer tools belong on the Responses path described next.
For OpenAI direct, swap the credential for an API key:
|
|
The decision logic is identical: pick OpenAI direct when you are prototyping outside Azure or for projects that have not gone through Azure procurement; pick the Azure path otherwise.
OpenAIResponsesClient
The Responses API is the newer surface for both Azure OpenAI and OpenAI direct. It exposes more hosted tools out of the box (web search, file search, code interpreter) and carries server-side conversation state via previous_response_id. The trade-off is that support for the Responses API is uneven across regions and model families. Check that the model you want is available in the region you need before committing.
|
|
If your agent needs hosted file search or code interpreter, this is the client to reach for. If you only need function tools, stay on Chat Completions.
FoundryChatClient
This is the client we used in the persistent agents article. FoundryChatClient connects to a Microsoft Foundry project and uses one of the models you have deployed there. It is the recommended path when you want Foundry to manage your model deployments — including governance, content filters, and shared quotas — but you do not want the full Foundry Agent Service runtime managing your agents.
|
|
The endpoint and model are set by AZURE_AI_PROJECT_ENDPOINT and AZURE_AI_FOUNDRY_MODEL_DEPLOYMENT_NAME. Use this when you want Foundry’s deployment story without taking on the Foundry Agent Service.
FoundryAgent
FoundryAgent is what we used in our first hands-on article. It is also what binds you to the Foundry Agent Service runtime: the agent itself becomes a Foundry resource, conversations are stored as Foundry threads, and the platform handles persistence, identity, and observability.
|
|
This is the right choice when you want the platform to own the agent: long-running threads that survive process restarts, centralized identity and audit, and access to the broader Foundry tool catalog. The trade-off is coupling — your agent now depends on the Foundry Agent Service being available, and moving to a different runtime later is more work.
ChatClientAgent
Everything above assumes a Microsoft- or OpenAI-hosted endpoint. For local models or third-party endpoints, MAF provides a generic path: implement (or import) a class that satisfies the IChatClient contract and pass it to a ChatClientAgent. This is the path for Foundry Local, Ollama, LM Studio, vLLM, or any OpenAI-compatible local server. It is also the path for Anthropic models when you do not want to go through a hosted gateway.
The exact import surface here has been moving as MAF stabilizes across SDK versions, so I am keeping the example deliberately schematic. The structure to keep in mind is: any chat client that satisfies the protocol can be wrapped into an agent the same way the Azure and OpenAI clients are.
A side-by-side cheat sheet
Here is the same information collapsed into one table you can paste into a design doc.
| Client | Model lives in | API surface | Hosted tools | Conversation state | Typical use |
|---|---|---|---|---|---|
OpenAIChatClient |
Azure OpenAI or OpenAI direct (chosen by credential type) | Chat Completions | Function + limited hosted | App-managed | Stable default; the workhorse |
OpenAIResponsesClient |
Azure OpenAI or OpenAI direct | Responses | Function + richer hosted | Server-side via previous_response_id |
Need web/file/code-interpreter |
FoundryChatClient |
Microsoft Foundry deployment | Chat Completions-compatible | Function tools | App-managed | Foundry-deployed models, agent runs in your process |
FoundryAgent |
Foundry Agent Service | Foundry Agent runtime | Foundry tool catalog (incl. Code Interpreter) | Platform-managed (Foundry threads) | Persistent, platform-owned agents |
ChatClientAgent over a custom client |
Anywhere | Whatever the client implements | Whatever the client exposes | App-managed | Local models, third-party providers |
The two columns that decide the most are Conversation state and Hosted tools. Once you have committed to where the state lives, the rest of your client choice is essentially fixed.
The same agent, three ways
To make the differences concrete, here is the weather agent from our second article implemented across three clients. The agent’s behavior — its instructions, its get_weather tool, and the prompt — is identical across all versions. Only the client changes.
|
|
Version A — OpenAIChatClient. The most portable. No platform coupling, conversation lives in the application.
|
|
Version B — FoundryAgent. Same agent, but now Foundry Agent Service owns the agent and its conversation threads. Notice the async with blocks because the client needs explicit lifecycle management.
|
|
Version C — FoundryChatClient. Foundry-deployed model, but the agent runtime is local — the same shape as Version A, just talking to a model in a Foundry project.
|
|
Three things are worth noticing across the three versions:
| What changed | Versions A, B, C |
|---|---|
Agent definition (name, instructions, tools) |
Identical |
| Import path | Different |
| Credential type | Sync DefaultAzureCredential in A; async AzureCliCredential in B and C |
| Lifecycle | Plain in A; async with blocks in B and C |
| Where the agent lives | Local in A and C; in Foundry Agent Service in B |
| Where conversation history lives | Application memory in A and C; Foundry threads in B |
The agent code does not change. The runtime characteristics — what survives a restart, where logs land, what tools you have access to — change a lot. That is the whole point of the client abstraction.
Pitfalls
A few things that have caught people out.
The Responses API does not have feature parity with Chat Completions in every region or for every model family. If you swap from OpenAIChatClient to OpenAIResponsesClient and a model you were using stops responding, region or model availability is the first thing to check.
FoundryChatClient and FoundryAgent look similar, and you can recreate most of one from the other, but they have different conversation-state semantics. If you start with FoundryChatClient and later migrate to FoundryAgent, your existing application-managed conversation history does not move automatically — you have to bring it across into Foundry threads yourself.
Hosted tools are tied to clients. A HostedCodeInterpreterTool that works behind FoundryAgent may need a different setup behind OpenAIResponsesClient, and may not be available at all behind OpenAIChatClient. When you change clients, audit your tool list.
Chat clients construct an agent via chat_client.as_agent(...), which wraps the client into a local agent. FoundryAgent is constructed directly with FoundryAgent(project_endpoint=..., agent_name=..., instructions=..., tools=[...]) and materializes a resource on the Foundry side. The difference reflects what the call actually does: chat clients build an agent locally, while FoundryAgent creates a platform resource.
Credentials matter more than they look. azure.identity (sync) and azure.identity.aio (async) export different classes with the same names. The async clients use the aio variants; mixing them silently leads to confusing errors deep inside an async with.
Finally, the import surface has shifted across MAF versions. The early-preview AzureChatClient was renamed to AzureOpenAIChatClient, and that has since been consolidated into OpenAIChatClient (in agent_framework.openai), which now serves both Azure OpenAI and OpenAI direct depending on the credential or API key you pass in. The Foundry path moved similarly: AzureAIClient and AzureAIAgentClient (formerly in agent_framework.azure) became FoundryChatClient and FoundryAgent in agent_framework.foundry. When in doubt, pin your agent-framework version and check the imports against the version you are running.
Wrap-up and what is next
The agents we have written throughout this series have been deceptively portable — the persona, the tools, and the workflow code carry over between clients with little trouble. What does not carry across is the runtime: where state lives, which hosted tools you can reach, and how the platform manages your agent. Picking the right client is, more than anything, picking the runtime profile you want for the agent.
In the next article, we will take this a step deeper and look at function tools. We have used them casually in earlier examples; now we will look at how MAF turns a Python function into a tool the model can call, what the schema generation actually does with your type hints, and how to handle errors and structured returns cleanly.
AzureOpenAIChatClient and AzureOpenAIResponsesClient (formerly in agent_framework.azure) were consolidated into OpenAIChatClient and OpenAIResponsesClient (in agent_framework.openai), which now serve both Azure OpenAI and OpenAI direct depending on the credential or API key. The Azure path requires either an azure_endpoint= argument or an AZURE_OPENAI_ENDPOINT environment variable. AzureAIClient and AzureAIAgentClient (also formerly in agent_framework.azure) became FoundryChatClient and FoundryAgent in agent_framework.foundry. Chat clients use chat_client.as_agent(...) rather than chat_client.create_agent(...). The cheat sheet, the per-section examples, and the worked “same agent, three ways” example have been updated to match.
Comments
Comments Require Consent
The comment system (Giscus) uses GitHub and may set authentication cookies. Enable comments to join the discussion.