-
Notifications
You must be signed in to change notification settings - Fork 462
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.
@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.
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 Thanks for clarifying those points. Sorry it's taken a while to get back to this (partly vacation, partly other stuff).
We don't have any proposed plan that I know of for not needing the STJv9 dependency here, since:
- It's fundamental to these helpers that they integrate with DI
- MEAI's way of doing that requires referencing Microsoft.Extensions.AI since that's where its DI integration lives
- Microsoft.Extensions.AI references STJv9
Give this, and the fact that MEAI itself is not going to be in any LTS release prior to .NET 10, we can't provide an LTS-only set of dependencies.
The two concerns you raise certainly both sound valid, but do you see them just as concerns or as blockers in some sense? If you think neither of them actually block us from proceeding then we will do so. But if you think it's not that simple then perhaps we can set up a meeting to discuss.
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.
do you see them just as concerns or as blockers in some sense?
They are concerns, not necessarily blockers. If MEAI decides that it requires STJv9 there isn't anything .NET Aspire can do about it (other than not take the dependency, which I'm not advocating for).
I do think this is an area that needs attention/thought across the whole org. In the past we've had different strategies for dependencies. And our current lifetime/servicing policies make it hard for libraries to depend on STS versions of OOB packages.
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.
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
.UseFunctionCalling()
.UseSomeCustomMiddleware()); There's also a separate If people are happy with this API shape, I'll proceed with implementing all the other combinations:
|
UpdatesFollowing discussion here, the latest proposed shape for the Aspire integration is: builder.AddAzureOpenAIClient("openai").AddChatClient("gpt-4o-mini", pipeline => pipeline
.UseFunctionCalling()
.UseOpenTelemetry()); I think that's fine but am still keen to consider the non-lambda alternative: builder.AddAzureOpenAIClient("openai").AddChatClient("gpt-4o-mini")
.UseFunctionCalling()
.UseOpenTelemetry(); ... or with @kzu's proposed simplification: builder.AddAzureOpenAIClient("openai").AddChatClient("gpt-4o-mini")
.UseDefaultPipeline(); I like More complex caseArguably this is also better even if you want to register multiple services. Before you'd have: builder.AddAzureOpenAIClient("openai")
.AddChatClient("gpt-4o-mini", pipeline => pipeline
.UseFunctionCalling()
.UseOpenTelemetry())
.AddEmbeddingGenerator("deployment-name", pipeline => pipeline
.UseOpenTelemetry()); That's a bit scary but still works as a single logical statement. It could conceptually be reduced to something like this to reduce the sense of nested lambdas: builder.AddAzureOpenAIClient("openai")
.AddChatClient("gpt-4o-mini", ChatClient.DefaultPipeline)
.AddEmbeddingGenerator("deployment-name", EmbeddingGenerator.DefaultPipeline); With the chaining above, you'd need a further variable: var openai = builder.AddAzureOpenAIClient("openai");
openai.AddChatClient("gpt-4o-mini").UseDefaultPipeline();
openai.AddEmbeddingGenerator("deployment-name").UseDefaultPipeline(); I think this is at least equally clean, and given how this makes clear how you can chain in custom stuff before and after the default pipeline pieces, it feels good to me. |
I think we want to move forwards with this as soon as we can, so I'm particularly looking for feedback from @eerhardt on the STJv9 dependency question and @stephentoub on the API shape. |
I like the non-lambda alternative personally. Yes it means you may have to capture something in a variable in order to do multiple, but IMO that is OK, you can only be so fluent.
A principle of .NET Aspire is that telemetry is enabled by default, so I'm a little concerned that you would need to opt-in to telemetry by calling UseDefaultPipeline. We don't have anything else like this in .NET Aspire. Instead everything else has an opt-out. Would it be possible to have a builder.AddAzureOpenAIClient("openai").AddChatClient("gpt-4o-mini")
.ClearPipeline()
.UseFunctionCalling(); would turn off telemetry, but keep function calling.
Have we given any more thought to the Hosting integration APIs? Specifically @sebastienros's proposal at Configure OpenAI models in the app host (dotnet/aspire#6577). |
4587b4b
to
a29740a
Compare
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