8000 Add Azure AI Foundry support by sebastienros · Pull Request #9974 · dotnet/aspire · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add Azure AI Foundry support #9974

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

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open

Add Azure AI Foundry support #9974

wants to merge 29 commits into from

Conversation

sebastienros
Copy link
Member
@sebastienros sebastienros commented Jun 20, 2025

Description

Add support for Azure AI Foundry models and Foundry Local.
Fixes #9012 #9568

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

sebastienros and others added 26 commits June 2, 2025 17:36
Bug in the FoundryLocalManager where the model is always downloaded (not doing case-insensitive checks for local models)

Bug that the WaitFor isn't propogating across and the web app in playground won't start
… requires

Not sure this is 100% right as the ModelInfo value isn't available until the model is downloaded, so if you don't do WaitFor Aspire will build the connection string with some null values. Also, the WaitFor, while it pauses the web app from starting, doesn't then release it to start for some reason
This should be delegated to the hosting resource to provide a valid endpoint, either generated by something like Foundry Local, or provided as expected using an endpoint from the Azure resource
@Copilot Copilot AI review requested due to automatic review settings June 20, 2025 22:14
@github-actions github-actions bot added the area-integrations Issues pertaining to Aspire Integrations packages label Jun 20, 2025
@sebastienros sebastienros requested a review from aaronpowell June 20, 2025 22:27
<IsPackable>true</IsPackable>
<PackageTags>aspire integration hosting azure openai ai aifoundry foundry</PackageTags>
<Description>Azure AI Foundry resource types for .NET Aspire.</Description>
<PackageIconFullPath>$(SharedDir)AzureOpenAI_256x.png</PackageIconFullPath>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong icon, need to use the foundry one

@sebastienros
Copy link
Member Author

Apparently the SDK just got on NuGet yesterday: https://www.nuget.org/packages/Microsoft.AI.Foundry.Local/0.1.0

We should be able to remove the /Internal folder with the SDK files that were copied

@@ -0,0 +1,552 @@
// Licensed to the .NET Foundation under one or more agreements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of untested logic...

/// <summary>
/// Gets the "connectionString" output reference from the Azure AI Services resource.
/// </summary>
public BicepOutputReference AIFoundryApiEndpoint => new("aiFoundryApiEndpoint", this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to expose the "NameOutputReference" property like the other bicep resources

/// <summary>
/// Gets the "connectionString" output reference from the Azure AI Services resource.
/// </summary>
public BicepOutputReference AIFoundryApiEndpoint => new("aiFoundryApiEndpoint", this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just call this endpoint?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to just call this "Endpoint" because the way Azure does this is there are multiple endpoints on each CognitiveServices account.

See https://github.com/dotnet/aspire/pull/9974/files#diff-f0de67f9e4b87d93c59940d40d62126b8d97f5885a18509710f7d9451a14481eR59-R65

There is a collection of endpoints that are indexed by name.

/// <summary>
/// Gets or sets a value indicating whether the resource is local.
/// </summary>
public bool IsLocal { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we have IsLocal, IsContainer, IsEmulator 😭

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use IsEmulator rather than IsLocal, since it's pretty analogous to an emulator in the way we use Foundry Local.

Comment on lines +13 to +16
public class AzureAIFoundryResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString,
IResourceWithEndpoints
Copy link
9E88
Member
@davidfowl davidfowl Jun 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't do this anywhere. Usually you still need to get the underlying emulator to get the endpoints. I think if we do this for foundry local, we should consider doing it for the other resources that have emulators.

Suggested change
public class AzureAIFoundryResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString,
IResourceWithEndpoints
public class AzureAIFoundryResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString


namespace Aspire.Hosting.Azure.AIFoundry;

internal sealed class FoundryHealthCheck(FoundryLocalManager manager) : IHealthCheck
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like this should be an http health check, but I understand the manager handles it.

QNNExecutionProvider
}

internal sealed partial class FoundryLocalManager : IDisposable, IAsyncDisposable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of logic that doesn't seem to have any logging. Make me wonder if it should be a global resource that shows up in the dashboard.


resourceBuilder
.WithHttpEndpoint(env: "PORT", isProxied: false, port: 6914, name: AzureAIFoundryResource.PrimaryEndpointName)
.WithExternalHttpEndpoints()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?? Doesn't matter for local.

}
else
{
await rns.PublishUpdateAsync(resource, state => state with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No way to restart? No way to see logs as to why it's not starting...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, there's no way to push logs from a "background task" such as this up to the parent resource log stream. But also, the SDK we use to manage Foundry Local allow us to hook into that anyway, as it manages the calls to the CLI or control plane (depending on the call you're making), so a failure is opaque to us.

As for the ability to restart/etc. let's tackle that via a future PR - this one can focus on MVP of Foundry Local + Azure AI Foundry, then add the nice-to-haves.

var rns = @event.Services.GetRequiredService<ResourceNotificationService>();
var manager = @event.Services.GetRequiredService<FoundryLocalManager>();

await rns.WaitForResourceAsync(@event.Resource.Name, KnownResourceStates.Finished, ct).ConfigureAwait(false);
Copy link
< 10000 /details-menu>
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't do anything if you are using a custom resource. Who marks the resource as finished?


await rns.WaitForResourceAsync(@event.Resource.Name, KnownResourceStates.Finished, ct).ConfigureAwait(false);

await manager.StopServiceAsync(ct).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this shared on the machine? What happens when you have multiple of these apps running at the same time?

logger.LogInformation("Failed to start {Model}. Error: {Error}", model, progress.ErrorMessage);
await rns.PublishUpdateAsync(deployment, state => state with
{
State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
State = new ResourceStateSnapshot(KnownResourceStates.FailedToStart, KnownResourceStateStyles.Error)
State = KnownResourceStates.FailedToStart


await rns.PublishUpdateAsync(deployment, state => state with
{
State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
State = new ResourceStateSnapshot(KnownResourceStates.Running, KnownResourceStateStyles.Success)
State = KnownResourceStates.Running

Properties = [.. state.Properties, new(CustomResourceKnownProperties.Source, model)]
}).ConfigureAwait(false);

var result = manager.DownloadModelWithProgressAsync(model, ct: ct) ?? throw new InvalidOperationException($"Failed to download model {model}.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When model download fails where will it show up? Who is logging this error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check the latest iteration of the management SDK (we've got a snapshot of the code until their NuGet package ships) but I think it can't return null anymore, so it'll always return a result that we handle in the await foreach below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never throw from a background thread like this. Log to the resource logger.


resourceBuilder
.WithHttpEndpoint(env: "PORT", isProxied: false, port: 6914, name: AzureAIFoundryResource.PrimaryEndpointName)
.WithExternalHttpEndpoints()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.WithExternalHttpEndpoints()

True, I think it was there from when I brute-forced some stuff originally.

}
else
{
await rns.PublishUpdateAsync(resource, state => state with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my testing, there's no way to push logs from a "background task" such as this up to the parent resource log stream. But also, the SDK we use to manage Foundry Local allow us to hook into that anyway, as it manages the calls to the CLI or control plane (depending on the call you're making), so a failure is opaque to us.

As for the ability to restart/etc. let's tackle that via a future PR - this one can focus on MVP of Foundry Local + Azure AI Foundry, then add the nice-to-haves.

/// </summary>
/// <param name="resource">The resource builder for the Foundry Local resource.</param>
/// <returns>The updated resource builder.</returns>
private static IResourceBuilder<AzureAIFoundryResource> EnsureResourceStops(this IResourceBuilder<AzureAIFoundryResource> resource)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this method entirely. I originally wrote it with the theory that you could run multiple instances of Foundry Local (since it was an executable as the entry point) but have since learnt that that isn't possible, so trying to force a stop will just cause problems.

Properties = [.. state.Properties, new(CustomResourceKnownProperties.Source, model)]
}).ConfigureAwait(false);

var result = manager.DownloadModelWithProgressAsync(model, ct: ct) ?? throw new InvalidOperationException($"Failed to download model {model}.");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check the latest iteration of the management SDK (we've got a snapshot of the code until their NuGet package ships) but I think it can't return null anymore, so it'll always return a result that we handle in the await foreach below.

/// <summary>
/// Gets or sets a value indicating whether the resource is local.
/// </summary>
public bool IsLocal { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use IsEmulator rather than IsLocal, since it's pretty analogous to an emulator in the way we use Foundry Local.


namespace Aspire.Hosting.Azure.AIFoundry;

internal sealed class FoundryHealthCheck(FoundryLocalManager manager) : IHealthCheck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal sealed class FoundryHealthCheck(FoundryLocalManager manager) : IHealthCheck
internal sealed class FoundryLocalHealthCheck(FoundryLocalManager manager) : IHealthCheck

@@ -0,0 +1,552 @@
// Licensed to the .NET Foundation under one or more agreements.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should note that this file comes from https://github.com/microsoft/Foundry-Local/tree/main/sdk/cs/src and is copied locally until the NuGet package for the management SDK ships.


namespace Aspire.Hosting.Azure.AIFoundry;

internal sealed class ModelHealthCheck(string modelAlias, FoundryLocalManager manager) : IHealthCheck
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
internal sealed class ModelHealthCheck(string modelAlias, FoundryLocalManager manager) : IHealthCheck
internal sealed class LocalModelHealthCheck(string modelAlias, FoundryLocalManager manager) : IHealthCheck


var localBuilder = resourceBuilder.RunAsFoundryLocal();
var localResource = Assert.Single(builder.Resources.OfType<AzureAIFoundryResource>());
Assert.True(localResource.IsLocal);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to assert on the ApiKey?

@dotnet-policy-service dotnet-policy-service bot added the needs-author-action An issue or pull request that requires more info or actions from the author. label Jun 23, 2025
/// <summary>
/// Provides extension methods for configuring and managing Foundry Local resources.
/// </summary>
public static partial class AzureAIFoundryLocalResourceExtensions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a separate class?

}

var azureResource = builder.Resource;
builder.ApplicationBuilder.Resources.Remove(azureResource);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove and add here? Can't we just set IsLocal = true on the current AzureAIFoundryResource?

// The .NET Foundation licenses this file to you under the MIT license.

// --------------------------------------------------------------------------------------------------------------------
// <copyright company="Microsoft">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just add Microsoft copyrighted code to this repo?

resourceBuilder
.WithHttpEndpoint(env: "PORT", isProxied: false, port: 6914, name: AzureAIFoundryResource.PrimaryEndpointName)
.WithExternalHttpEndpoints()
//.WithAIFoundryLocalDefaults()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it can be deleted.

var builder = DistributedApplication.CreateBuilder(args);

var foundry = builder.AddAzureAIFoundry("foundry")
.RunAsFoundryLocal()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I try to F5 this app, I'm getting an error that says my Azure provisioning information is missing.

We shouldn't require Azure provisioning info when the app is running as local.


<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Azure.AI.Inference" />
<AspireProjectOrPackageReference Include="Aspire.OpenAI" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is OpenAI used at all?


```csharp
var chat = builder.AddAzureAIFoundry("foundry")
.AddDeployment("chat", "phi-3.5-mini", "1", "Microsoft");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This didn't work for me. I got:

{"error":{"code":"InvalidTemplateDeployment","message":"The template deployment 'foundry' is not valid according to the validation procedure. The tracking id is 'a63a7811-3833-40bd-85fc-8df85c2360dc'. See inner errors for details.","details":[{"code":"DeploymentModelNotSupported","message":"The model 'Format:Microsoft,Name:phi-3.5-mini,Version:1' of account deployment is not supported."}]}}

@@ -103,17 +103,17 @@ void IConnectionStringSettings.ParseConnectionString(string? connectionString)
ConnectionString = connectionString
};

if (connectionBuilder.TryGetValue(nameof(DeploymentId), out var modelId))
if (connectionBuilder.TryGetValue("DeploymentId", out var modelId))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

@@ -113,15 +114,29 @@ protected override IAzureClientBuilder<ChatCompletionsClient, AzureAIInferenceCl
}
else
{
var endpoint = settings.Endpoint;

if (endpoint.Host.EndsWith(".ai.azure.com", StringComparison.OrdinalIgnoreCase) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be the responsibility of the Hosting side. It should guarantee it uses the right URL and the client integration shouldn't need to fix the URL up.

< 629A input type="hidden" name="authenticity_token" value="isWQ7vi-0F2goN39q3VoUK8czNTk8db5DCTpR0IPf44iQUYZbUDevAOtQW6jzTDeDy0jtAKEGS3tuIL0w2iayg" autocomplete="off" />

return new ChatCompletionsClient(settings.Endpoint, settings.TokenCredential ?? new DefaultAzureCredential(), options);
var credential = settings.TokenCredential ?? new DefaultAzureCredential();

BearerTokenAuthenticationPolicy tokenPolicy = new(credential, ["https://cognitiveservices.azure.com/.default"]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this the default in the library?

Can we add a comment here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-integrations Issues pertaining to Aspire Integrations packages needs-author-action An issue or pull request that requires more info or actions from the author.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Hosting integration for Azure AI Foundry
4 participants
0