-
Notifications
You must be signed in to change notification settings - Fork 453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add AddAzureOpenAIChatClient #6225
base: main
Are you sure you want to change the base?
Conversation
BTW does anyone know why |
Thank you 😄 |
Most likely no one requested it to be mirrored there. I can do so. But for future reference: https://github.com/dotnet/arcade/blob/main/Documentation/MirroringPackages.md |
@stephentoub, "/azp run"? |
{ | ||
builder.AddAzureOpenAIClient(connectionName, configureSettings, configureClientBuilder); | ||
|
||
builder.Services.AddSingleton(services => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume if we change AddChatClient to be a singleton, this would instead use that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it would be equivalent, so it might as well do.
Possibly. I'm used to that not being appropriate in dotnet/runtime. |
src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIChatClientExtensions.cs
Outdated
Show resolved
Hide resolved
src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIChatClientExtensions.cs
Outdated
Show resolved
Hide resolved
if (configuration.GetConnectionString(connectionName) is string connectionString) | ||
{ | ||
var connectionBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString }; | ||
deploymentName = (connectionBuilder[DeployentKey] ?? connectionBuilder[ModelKey])?.ToString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like DbConnectionStringBuilder's indexer actually throws rather than returning null if the keyword isn't found. Should these instead use TryGetValue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example: we use connectionBuilder.TryGetValue(ConnectionStringEndpoint, out var endpoint)
in AddOpenAIClientFromConfiguration
.
src/Components/Aspire.Azure.AI.OpenAI/AspireAzureOpenAIChatClientExtensions.cs
Outdated
Show resolved
Hide resolved
I think the builder alternative was suggested instead of a lambda in the extension method because we already have two optional lambdas in it (to customize option and settings). public static void AddAzureOpenAIClient(
this IHostApplicationBuilder builder,
string connectionName,
Action<AzureOpenAISettings>? configureSettings = null,
Action<IAzureClientBuilder<AzureOpenAIClient, AzureOpenAIClientOptions>>? configureClientBuilder = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Design looks reasonable to me.
<PackageReference Include="Microsoft.Extensions.Primitives" VersionOverride="9.0.0-*" /> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" VersionOverride="9.0.0-*" /> | ||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" VersionOverride="9.0.0-*" /> | ||
<PackageReference Include="System.Text.Json" VersionOverride="9.0.0-*" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR should simplify that, right? Or do we also want to use the package in apps targeting 8.0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or do we also want to use the package in apps targeting 8.0?
Yes. We have no choice, because M.E.AI.Abstractions targets the 9.x versions of these packages (even on net8.0
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We shouldn't be using wildcards *
s pacakge versions.
As for using STJ 9 on net8.0, I think that is something to think hard about in MEAI before shipping, since 9 is STS and 8 is LTS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stephentoub The current status for this is that MEAI.Abstractions will depend on STJ v8, whereas MEAI will depend on STJ v9, right?
If so then the Aspire integration package will still have to depend on STJv9 (even on net8.0
), because it needs to depend on MEAI (not .Abstractions).
Is this going to be a problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current status for this is that MEAI.Abstractions will depend on STJ v8, whereas MEAI will depend on STJ v9, right?
When targeting < net9.0, yes.
If so then the Aspire integration package will still have to depend on STJv9 (even on net8.0)... Is this going to be a problem?
Not sure. Who do we think will be concerned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@eerhardt Your comment above gave me the impression this was a concern. Can you let me know if it is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally there are 2 concerns with lifting libraries out of the shared framework:
- (technical) It is another .dll that needs to be distributed with your application. Also, it is no longer ReadyToRun'd because we aren't using the assembly in the shared framework (which is RID-specific and R2R'd).
- (policy) the servicing lifetime of System.Text.Json 9.0 ends before the servicing lifetime of System.Text.Json 8.0. If you are staying on
net8.0
TFM, you probably want to keep on a long-term support framework. But using MEAI (not .Abstractions) means that you are signing up for being on the short-term support System.Text.Json. Meaning you will need to move up to the v10 System.Text.Json faster than you would have if you stayed on the v8.
Can you give an example of how that would look? If you mean something like this, we can certainly look at going that way. However it would prohibit this sort of calling pattern: builder.AddAzureOpenAIClient("openai")
.AddChatClient()
.AddEmbeddingGenerator(); ... because if |
I'm keen to finish off this PR but to avoid any wasted work can someone from the Aspire side acknowledge if you're happy with the API shape so far? cc @sebastienros @eerhardt @davidfowl Specifically, this sort of registration usage: builder.AddAzureOpenAIChatClient("openai"); Or with custom pipeline: builder.AddAzureOpenAIChatClient("openai", pipeline => pipeline
.UseChatOptions(...)
.UseFunctionCalling()); // ... plus any other pipeline builder calls AlternativeThe only other main proposal is the following: builder.AddAzureOpenAIClient("openai").AddChatClient(pipeline => pipeline
.UseChatOptions(...)
.UseFunctionCalling()); // ... plus any other pipeline builder calls In this case we'd change the return type of |
The alternative looks better. EF has a similar problem of 2 lambdas (to configure EF and the underlying provider) and the API does not feel as great. |
@davidfowl Hold on, I updated the code above, after realising I was coupling together two separate design pivots. Can you re-check the code in that comment? |
As for the question of whether the IChatClient pipeline should be configured via a lambda or should be chained onto the In other words, even if that's our end goal we still have to decide whether there's a single |
We also need to revisit the Use method, which someone might chain at the end (assuming the previous methods return the same ChatClientBuilder) not realizing it's an expensive nop. Renaming it back to Build would help with that. |
I agree. We should remove that method completely and make it a constructor parameter to |
I like both alternatives (with or without builder). But if we expect users to use mostly use chat or embedding clients directly instead of the If we use a builder on |
Except this isn't an app host API. |
One concern I have is this method: aspire/src/Components/Aspire.Azure.AI.OpenAI/AspireConfigurableOpenAIExtensions.cs Lines 21 to 27 in 4f7dbd4
Following the pattern of
Where as if adding the OpenAIClient was separated from adding the "child" chat, embedding, assistant, audio, etc. clients we wouldn't need to duplicate the "from configuration" part. |
2551471
to
c565c53
Compare
OK, following the feedback from @davidfowl and @eerhardt, I've updated the implementation to chain onto the builder.AddAzureOpenAIClient("openai").AddChatClient("gpt-4o-mini", pipeline => pipeline
.AddFunctionCalling()
.AddSomeCustomMiddleware()); There's also a separate If people are happy with this API shape, I'll proceed with implementing all the other combinations:
|
Description
This is a starting point based on the discussion earlier this week with @eerhardt, @sebastienros, and @luisquintanilla.
It allows developers to register an
IChatClient
in client projects via:So far I only did this for Azure OpenAI but once we've agreed on the shape would do the equivalent for OpenAI too.
Design questions
Lifetime
Currently this registers the
IChatClient
as singleton. That differs from prior assumptions in the MEAI repo that it would be scoped (cc: @stephentoub). The reason I did singleton here is that the underlyingOpenAIClient
is singleton, and being singleton and thread-safe is cited as a core tenet of Azure client libraries.I suspect we should try to create an expectation that
IChatClient
implementations should be designed to be singleton and thread-safe. Anyone who finds they need per-usage-site state can store it inChatOptions
(e.g.,AdditionalProperties
). If this turns out to be impractical then we will have to reconsider this design. But it's better to start with singleton and later relax it to be scoped than to go in the other direction.Also filed dotnet/extensions#5499 to track on the MEAI side validating this for the existing
IChatClient
implementations.If I'm misjudging this and Aspire is totally OK with client libraries registering as scoped, we can reconsider.
APIs for chaining on
AddAzureOpenAIClient
It's also possible we might want to enable an API pattern like this:
... instead of requiring two top-level calls (
AddAzureOpenAIChatClient
andAddAzureOpenAIEmbeddingGenerator
). However that would involve changing the return type ofAddAzureOpenAIClient
fromvoid
to some builder we can putAddChatClient
andAddEmbeddingGenerator
extensions onto.I'm fine with doing this if we like it. There's an argument for still also supporting the top-level calls for equivalence with
AddOllamaChatClient
etc.Checklist
<remarks />
and<code />
elements on your triple slash comments?Microsoft Reviewers: Open in CodeFlow