Skip to content

Commit 5785c87

Browse files
adr for python client constructors
1 parent f9a94ce commit 5785c87

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
status: proposed
3+
contact: eavanvalkenburg
4+
date: 2025-11-18
5+
deciders: markwallace-microsoft, dmytrostruk, sphenry, alliscode
6+
consulted: taochenosu, moonbox3, giles17
7+
---
8+
9+
# Python Client Constructors
10+
11+
## Context and Problem Statement
12+
13+
We have multiple Chat Client implementations that can be used with different servers, the most important example is OpenAI, where we have a separate client for OpenAI and for Azure OpenAI. The constructors for the underlying OpenAI client has now enabled both, so it might make sense to have a single Chat Client for both, the same also applies to other Chat Clients, such as Anthropic, which has a Anthropic client, but also AnthropicBedrock and AnthropicVertex, currently we don't support creating a AF AnthropicClient with those by default, but if you pass them in as a parameter, it will work. This is not the case for OpenAI, where we have a separate client for Azure OpenAI and OpenAI, the OpenAI clients still accept any OpenAI Client (or a subclass thereof) as a parameter, so it can already be used with different servers, including Azure OpenAI, but it is not the default.
14+
15+
We have a preference of creating clients inside of our code because then we can add a user-agent string to allow us to track usage of our clients with different services. This is most useful for Azure, but could also be a strong signal for other vendors to invest in first party support for Agent Framework. And we also make sure to not alter clients that are passed in, as that is often meant for a specific use case, such as setting up httpx clients with proxies, or other customizations that are not relevant for the Agent Framework.
16+
17+
There is likely not a single best solution, the goal here is consistency across clients, and with that ease of use for users of Agent Framework.
18+
19+
## Decision Drivers
20+
21+
- Reduce client sprawl and different clients that only have one or more different parameters.
22+
- Make client creation inside our classes the default and cover as many backends as possible.
23+
- Make clients easy to use and discover, so that users can easily find the right client for their use case.
24+
- Allow client creation based on environment variables, so that users can easily configure their clients without having to pass in parameters.
25+
26+
## Considered Options
27+
28+
- Separate clients for each backend, such as OpenAI and Azure OpenAI, Anthropic and AnthropicBedrock, etc.
29+
- Separate parameter set per backend with a single client, such as OpenAIClient with parameters, for endpoint/base_url, api_key, and entra auth.
30+
- Single client with a explicit parameter for the backend to use, such as OpenAIClient(backend="azure") or AnthropicClient(backend="vertex").
31+
- Single client with a customized `__new__` method that can create the right client based on the parameters passed in, such as OpenAIClient(api_key="...", backend="azure") which returns a AzureOpenAIClient.
32+
- Map clients to underlying SDK clients, OpenAI's SDK client allows both OpenAI and Azure OpenAI, so would be a single client, while Anthropic's SDK has explicit clients for Bedrock and Vertex, so would be a separate client for AnthropicBedrock and AnthropicVertex.
33+
34+
## Pros and Cons of the Options
35+
36+
### Separate clients for each backend, such as OpenAI and Azure OpenAI, Anthropic and AnthropicBedrock, etc.
37+
This option would entail potentially a large number of clients, and keeping track of additional backend implementation being created by vendors.
38+
- Good, because it is clear which client is used
39+
- Good, because we can easily have aliases of parameters, that are then mapped internally, such as `deployment_name` for Azure OpenAI mapping to `model_id` internally
40+
- Good, because it is easy to map environment variables to the right client
41+
- Good, because any customization of the behavior can be done in the subclass
42+
- Good, because we can expose the classes in different places, currently the `AzureOpenAIClient` is exposed in the `azure` module, while the `OpenAIClient` is exposed in the `openai` module, the same could be done with Anthropic, exposed from `anthropic`, while `AnthropicBedrock` would be exposed from `agent_framework.amazon`.
43+
- Good, stable clients per backend, as changes to one client do not affect the other clients.
44+
- Bad, because it creates a lot of clients that are very similar (even if they subclass from one base client class)
45+
- Bad, because it is hard to keep track of all the clients and their parameters
46+
- Bad, because it is hard to discover the right client for a specific use case
47+
48+
Example code:
49+
```python
50+
from agent_framework.openai import OpenAIClient # using a fictional OpenAIClient, to illustrate the point
51+
from agent_framework.azure import AzureOpenAIClient
52+
53+
openai_client = OpenAIClient(api_key="...")
54+
azure_client = AzureOpenAIClient(deployment_name="...", ad_token_provider=...)
55+
```
56+
57+
### Separate parameter set per backend with a single client, such as OpenAIClient with parameters, for endpoint/base_url, api_key, and entra auth.
58+
This option would entail a single client that can be used with different backends, but requires the user to pass in the right parameters.
59+
- Good, because it reduces the number of clients and makes it easier to discover the right client with the right parameters
60+
- Good, because it allows for a single client to be used with different backends and additional backends can be added easily
61+
- Good, because the user does not have to worry about which client to use, they can just use the `OpenAIClient` or `AnthropicClient` and pass in the right parameters, and we create the right client for them, if that client changes, then we do that in the code, without any changes to the api.
62+
- Good, because in many cases, the differences between the backends are just a few parameters, such as endpoint/base_url and authentication method.
63+
- Good, because client resolution logic could be encapsulated in a factory method, making it easier to maintain and even extend by users.
64+
- Bad, because it requires the user to know which parameters to pass in for the specific backend and when using environment variables, it is not always clear which parameters are used for which backend, or what the order of precedence is.
65+
- Bad, because it can lead to confusion if the user passes in the wrong parameters for the specific backend
66+
- Bad, because the name for a parameter that is similar but not the same between backends can be confusing, such as `deployment_name` for Azure OpenAI and `model_id` for OpenAI, would we then only have `model_id` for both, or have both parameters?
67+
- Bad, because it can lead to a lot of parameters that are not used for a specific backend, such as `entra_auth` for Azure OpenAI, but not for OpenAI
68+
- Bad, less stable per client, as changes to the parameter change all clients.
69+
- Bad, because customized behavior per backend is harder to implement, as it requires more conditional logic in the client code.
70+
71+
Example code:
72+
```python
73+
from agent_framework.openai import OpenAIClient
74+
openai_client = OpenAIClient(
75+
model_id="...",
76+
api_key="...",
77+
)
78+
azure_client = OpenAIClient(
79+
deployment_name="...", # or model_id, depending on the decision
80+
ad_token_provider=...,
81+
)
82+
```
83+
84+
### Single client with a explicit parameter for the backend to use, such as OpenAIClient(backend="azure") or AnthropicClient(backend="vertex").
85+
This option would entail a single client that can be used with different backends, but requires the user to pass in the right backend as a parameter.
86+
- Same list as the option above, and:
87+
- Good, because it is explicit about which backend to try and target, including for environment variables
88+
- Bad, because adding a new backend would require a change to the client (the backend param would change from i.e. `Literal["openai", "azure"]` to `Literal["openai", "azure", "newbackend"]`)
89+
90+
91+
Example code:
92+
```python
93+
from agent_framework.openai import OpenAIClient
94+
openai_client = OpenAIClient(
95+
backend="openai", # probably optional, since this would be the default
96+
model_id="...",
97+
api_key="...",
98+
)
99+
azure_client = OpenAIClient(
100+
backend="azure",
101+
deployment_name="...", # could also become `model_id` to make it a bit simpler
102+
ad_token_provider=...,
103+
)
104+
```
105+
106+
### Single client with a customized `__new__` method that can create the right client based on the parameters passed in, such as OpenAIClient(backend="azure") which returns a AzureOpenAIClient.
107+
This option would entail a single client that can be used with different backends, and the right client is created based on the parameters passed in.
108+
- Good, because the entry point for a user is very clear
109+
- Good, because it allows for customization of the client based on the parameters passed in
110+
- Bad, because it still needs all the extra client classes for the different backends
111+
- Bad, because there might be confusion between using the subclasses or the main class with the customized `__new__` method
112+
- Bad, because adding a new backend is still work that is required to be built
113+
114+
Example code:
115+
```python
116+
from agent_framework.openai import OpenAIClient
117+
openai_client = OpenAIClient(
118+
backend="openai", # probably optional, since this would be the default
119+
model_id="...",
120+
api_key="...",
121+
)
122+
azure_client = OpenAIClient(
123+
backend="azure",
124+
model_id="...",
125+
ad_token_provider=...,
126+
)
127+
print(type(openai_client)) # OpenAIClient
128+
print(type(azure_client)) # AzureOpenAIClient
129+
```
130+
131+
### Map clients to underlying SDK clients, OpenAI's SDK client allows both OpenAI and Azure OpenAI, so would be a single client, while Anthropic's SDK has explicit clients for Bedrock and Vertex, so would be a separate client for AnthropicBedrock and AnthropicVertex.
132+
This option would entail a mix of the above options, depending on the underlying SDK clients.
133+
- Good, because it aligns with the underlying SDK clients and their capabilities
134+
- Good, because it reduces the number of clients where possible
135+
- Bad, because it can lead to inconsistency between clients, some being separate per backend, while others are combined
136+
- Bad, because it can lead to confusion for users if they expect a consistent approach across all clients
137+
138+
Example code:
139+
```python
140+
from agent_framework.anthropic import AnthropicClient, AnthropicBedrockClient
141+
from agent_framework.openai import OpenAIClient
142+
openai_client = OpenAIClient(
143+
model_id="...",
144+
api_key="...",
145+
)
146+
azure_client = OpenAIClient(
147+
model_id="...",
148+
api_key=lambda: get_azure_ad_token(...),
149+
)
150+
anthropic_client = AnthropicClient(
151+
model_id="...",
152+
api_key="...",
153+
)
154+
anthropic_bedrock_client = AnthropicBedrockClient(
155+
model_id="...",
156+
aws_secret_key="...",
157+
aws_access_key="...",
158+
base_url="...",
159+
)
160+
```
161+
162+
## Decision Outcome
163+
164+
TBD

0 commit comments

Comments
 (0)