diff --git a/eng/packages/ProjectTemplates.props b/eng/packages/ProjectTemplates.props
index eb053b5270d..385410facee 100644
--- a/eng/packages/ProjectTemplates.props
+++ b/eng/packages/ProjectTemplates.props
@@ -19,6 +19,7 @@
+
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json
index 4058affe004..51a6aac70d4 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/.template.config/template.json
@@ -128,18 +128,13 @@
"type": "parameter",
"displayName": "_AI service provider",
"datatype": "choice",
- "defaultValue": "githubmodels",
+ "defaultValue": "foundrylocal",
"choices": [
{
"choice": "azureopenai",
"displayName": "Azure OpenAI",
"description": "Uses Azure OpenAI service"
},
- {
- "choice": "githubmodels",
- "displayName": "GitHub Models",
- "description": "Uses GitHub Models"
- },
{
"choice": "ollama",
"displayName": "Ollama (for local development)",
@@ -149,6 +144,11 @@
"choice": "openai",
"displayName": "OpenAI Platform",
"description": "Uses the OpenAI Platform"
+ },
+ {
+ "choice": "foundrylocal",
+ "displayName": "Foundry Local (on-device)",
+ "description": "Uses Foundry Local with the qwen3-4b and qwen3-embedding-0.6b models"
}
// {
// "choice": "azureaifoundry",
@@ -215,14 +215,14 @@
"type": "computed",
"value": "(AiServiceProvider == \"openai\")"
},
- "IsGHModels": {
- "type": "computed",
- "value": "(AiServiceProvider == \"githubmodels\")"
- },
"IsOllama": {
"type": "computed",
"value": "(AiServiceProvider == \"ollama\")"
},
+ "IsFoundryLocal": {
+ "type": "computed",
+ "value": "(AiServiceProvider == \"foundrylocal\")"
+ },
"IsAzureAIFoundry": {
"type": "computed",
"value": "(AiServiceProvider == \"azureaifoundry\")"
@@ -317,6 +317,38 @@
},
"replaces": "all-minilm"
},
+ "FoundryLocalChatModelDefault": {
+ "type": "generated",
+ "generator": "constant",
+ "parameters": {
+ "value": "qwen3-4b"
+ }
+ },
+ "FoundryLocalEmbeddingModelDefault": {
+ "type": "generated",
+ "generator": "constant",
+ "parameters": {
+ "value": "qwen3-embedding-0.6b"
+ }
+ },
+ "FoundryLocalChatModel": {
+ "type": "generated",
+ "generator": "coalesce",
+ "parameters": {
+ "sourceVariableName": "ChatModel",
+ "fallbackVariableName": "FoundryLocalChatModelDefault"
+ },
+ "replaces": "qwen3-4b"
+ },
+ "FoundryLocalEmbeddingModel": {
+ "type": "generated",
+ "generator": "coalesce",
+ "parameters": {
+ "sourceVariableName": "EmbeddingModel",
+ "fallbackVariableName": "FoundryLocalEmbeddingModelDefault"
+ },
+ "replaces": "qwen3-embedding-0.6b"
+ },
"webHttpPort": {
"type": "parameter",
"datatype": "integer",
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs
index 3304884fa78..fd577fedc2c 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.AppHost/AppHost.cs
@@ -1,17 +1,11 @@
var builder = DistributedApplication.CreateBuilder(args);
#if (IsOllama) // ASPIRE PARAMETERS
-#elif (IsOpenAI || IsGHModels)
+#elif (IsOpenAI)
// You will need to set the connection string to your own value
// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line:
// cd this-project-directory
-#if (IsOpenAI)
// dotnet user-secrets set ConnectionStrings:openai "Key=YOUR-API-KEY"
-#elif (IsGHModels)
-// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
-#else // IsAzureOpenAI
-// dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://YOUR-DEPLOYMENT-NAME.openai.azure.com;Key=YOUR-API-KEY"
-#endif
var openai = builder.AddConnectionString("openai");
#else // IsAzureOpenAI
@@ -62,7 +56,7 @@
.WithReference(embeddings)
.WaitFor(chat)
.WaitFor(embeddings);
-#elif (IsOpenAI || IsGHModels)
+#elif (IsOpenAI)
webApp.WithReference(openai);
#else // IsAzureOpenAI
webApp
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in
index 3d8811c209e..e490183ab4b 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/AIChatWeb-CSharp.Web.csproj-in
@@ -7,19 +7,29 @@
d5681fae-b21b-4114-b781-48180f08c0c4
+
+
+
+ win-x64;win-arm64;linux-x64;linux-arm64;osx-arm64
+
+
+
-
+
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs
index 31442718f1f..7ce9d732627 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.Aspire.cs
@@ -1,5 +1,9 @@
+#if (IsFoundryLocal)
+// Foundry Local has no .NET Aspire hosting integration and is only supported without Aspire.
+#error Foundry Local is not supported with .NET Aspire. Re-create the project without --aspire and with a non-Qdrant vector store (Qdrant requires Aspire).
+#endif
using Microsoft.Extensions.AI;
-#if (IsOpenAI || IsGHModels)
+#if (IsOpenAI)
using OpenAI;
#endif
using AIChatWeb_CSharp.Web.Components;
@@ -19,7 +23,7 @@
builder.AddOllamaApiClient("embeddings")
.AddEmbeddingGenerator();
#elif (IsAzureAIFoundry)
-#else // (IsOpenAI || IsAzureOpenAI || IsGHModels)
+#else // (IsOpenAI || IsAzureOpenAI)
#if (IsOpenAI)
var openai = builder.AddOpenAIClient("openai");
#else
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs
index 89a231105e9..c386794ad7b 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Program.cs
@@ -1,8 +1,12 @@
-#if (IsGHModels || IsOpenAI || (IsAzureOpenAI && !IsManagedIdentity))
+#if (IsOpenAI || IsFoundryLocal || (IsAzureOpenAI && !IsManagedIdentity))
using System.ClientModel;
#elif (IsAzureOpenAI && IsManagedIdentity)
using System.ClientModel.Primitives;
#endif
+#if (IsFoundryLocal)
+using Microsoft.AI.Foundry.Local;
+using Microsoft.Extensions.Logging.Abstractions;
+#endif
#if (IsAzureAISearch && !IsManagedIdentity)
using Azure;
#elif (IsManagedIdentity)
@@ -11,7 +15,7 @@
using Microsoft.Extensions.AI;
#if (IsOllama)
using OllamaSharp;
-#elif (IsGHModels || IsOpenAI || IsAzureOpenAI)
+#elif (IsOpenAI || IsFoundryLocal || IsAzureOpenAI)
using OpenAI;
#endif
using AIChatWeb_CSharp.Web.Components;
@@ -21,25 +25,52 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
-#if (IsGHModels)
-// You will need to set the endpoint and key to your own values
-// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line:
-// cd this-project-directory
-// dotnet user-secrets set GitHubModels:Token YOUR-GITHUB-TOKEN
-var credential = new ApiKeyCredential(builder.Configuration["GitHubModels:Token"] ?? throw new InvalidOperationException("Missing configuration: GitHubModels:Token. See the README for details."));
-var openAIOptions = new OpenAIClientOptions()
-{
- Endpoint = new Uri("https://models.inference.ai.azure.com")
-};
-
-var ghModelsClient = new OpenAIClient(credential, openAIOptions);
-var chatClient = ghModelsClient.GetChatClient("gpt-4o-mini").AsIChatClient();
-var embeddingGenerator = ghModelsClient.GetEmbeddingClient("text-embedding-3-small").AsIEmbeddingGenerator();
-#elif (IsOllama)
+#if (IsOllama)
IChatClient chatClient = new OllamaApiClient(new Uri("http://localhost:11434"),
"llama3.2");
IEmbeddingGenerator> embeddingGenerator = new OllamaApiClient(new Uri("http://localhost:11434"),
"all-minilm");
+#elif (IsFoundryLocal)
+var chatAlias = builder.Configuration["FoundryLocal:ChatModel"] ?? "qwen3-4b";
+var embeddingAlias = builder.Configuration["FoundryLocal:EmbeddingModel"] ?? "qwen3-embedding-0.6b";
+var foundryServiceUrl = builder.Configuration["FoundryLocal:ServiceUrl"] ?? "http://127.0.0.1:5273";
+
+await FoundryLocalManager.CreateAsync(new Configuration
+{
+ AppName = "AIChatWeb-CSharp",
+ Web = new Configuration.WebService { Urls = foundryServiceUrl }
+}, NullLogger.Instance);
+var foundryManager = FoundryLocalManager.Instance;
+await foundryManager.StartWebServiceAsync();
+var foundryCatalog = await foundryManager.GetCatalogAsync();
+
+async Task EnsureFoundryModelAsync(string modelAlias)
+{
+ var model = await foundryCatalog.GetModelAsync(modelAlias)
+ ?? throw new InvalidOperationException(
+ $"Foundry Local model '{modelAlias}' was not found in the catalog. Run 'foundry model list' to see available models.");
+ if (!await model.IsCachedAsync())
+ {
+ Console.WriteLine($"Foundry Local: downloading model '{modelAlias}' (first run only)...");
+ await model.DownloadAsync(_ => { });
+ }
+ if (!await model.IsLoadedAsync())
+ {
+ await model.LoadAsync();
+ }
+ return model.Id;
+}
+
+var chatModelId = await EnsureFoundryModelAsync(chatAlias);
+var embeddingModelId = await EnsureFoundryModelAsync(embeddingAlias);
+
+var foundryEndpointUrl = foundryManager.Urls?.FirstOrDefault() ?? foundryServiceUrl;
+var foundryEndpoint = new Uri($"{foundryEndpointUrl.TrimEnd('/')}/v1");
+var foundryClient = new OpenAIClient(
+ new ApiKeyCredential("unused"),
+ new OpenAIClientOptions { Endpoint = foundryEndpoint });
+var chatClient = foundryClient.GetChatClient(chatModelId).AsIChatClient();
+var embeddingGenerator = foundryClient.GetEmbeddingClient(embeddingModelId).AsIEmbeddingGenerator();
#elif (IsOpenAI)
// You will need to set the endpoint and key to your own values
// You can do this using Visual Studio's "Manage User Secrets" UI, or on the command line:
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md
index 14677ca5372..0290f72ceef 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/README.md
@@ -21,32 +21,6 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See
#### ---#endif
# Configure the AI Model Provider
-#### ---#if (IsGHModels)
-To use models hosted by GitHub Models, you will need to create a GitHub personal access token with `models:read` permissions, but no other scopes or permissions. See [Prototyping with AI models](https://docs.github.com/github-models/prototyping-with-ai-models) and [Managing your personal access tokens](https://docs.github.com/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) in the GitHub Docs for more information.
-
-#### ---#if (hostIdentifier == "vs")
-Configure your token for this project using .NET User Secrets:
-
-1. In Visual Studio, right-click on your project in the Solution Explorer and select "Manage User Secrets".
-2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following key and value:
-
- ```json
- {
- "GitHubModels:Token": "YOUR-TOKEN"
- }
- ```
-#### ---#else
-From the command line, configure your token for this project using .NET User Secrets by running the following commands:
-
-```sh
-cd <>
-dotnet user-secrets set GitHubModels:Token YOUR-TOKEN
-```
-#### ---#endif
-
-Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models).
-
-#### ---#endif
#### ---#if (IsOpenAI)
## Using OpenAI
@@ -92,6 +66,34 @@ ollama pull all-minilm
### 3. Learn more about Ollama
Once the models are installed, you can start using them in your application. Refer to the [Ollama documentation](https://github.com/ollama/ollama/blob/main/docs/README.md) for detailed instructions on how to explore models locally.
+#### ---#endif
+#### ---#if (IsFoundryLocal)
+## Setting up a local environment using Foundry Local
+This project is configured to use Foundry Local, which runs models on your workstation through a local OpenAI-compatible endpoint. It does not need an API key.
+
+### 1. Install Foundry Local
+Install Foundry Local for your operating system by following the [Foundry Local documentation](https://learn.microsoft.com/azure/ai-foundry/foundry-local/).
+
+### 2. Run the app
+The app starts the Foundry Local service for you. On first run, it downloads the configured models, then loads them into the local service.
+
+The default chat model alias is `qwen3-4b`. The default embedding model alias is `qwen3-embedding-0.6b`.
+
+### 3. Override model aliases or the service URL
+You can change the defaults in `appsettings.json`, `appsettings.Development.json`, or user secrets:
+
+```json
+{
+ "FoundryLocal": {
+ "ChatModel": "qwen3-4b",
+ "EmbeddingModel": "qwen3-embedding-0.6b",
+ "ServiceUrl": "http://127.0.0.1:5273"
+ }
+}
+```
+
+Use `FoundryLocal:ServiceUrl` if another local process already uses the default port.
+
#### ---#endif
#### ---#if (IsAzureOpenAI)
## Using Azure OpenAI
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs
index 60e6b5684e4..3acc0ba26de 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/Services/IngestedChunk.cs
@@ -7,6 +7,8 @@ public class IngestedChunk
{
#if (IsOllama)
public const int VectorDimensions = 384; // 384 is the default vector size for the all-minilm embedding model
+#elif (IsFoundryLocal)
+ public const int VectorDimensions = 1024; // 1024 is the default vector size for the qwen3-embedding-0.6b embedding model
#else
public const int VectorDimensions = 1536; // 1536 is the default vector size for the OpenAI text-embedding-3-small model
#endif
@@ -17,9 +19,15 @@ public class IngestedChunk
#endif
public const string CollectionName = "data-AIChatWeb-CSharp.Web-chunks";
+#if (IsQdrant)
[VectorStoreKey(StorageName = "key")]
[JsonPropertyName("key")]
public required Guid Key { get; set; }
+#else
+ [VectorStoreKey(StorageName = "key")]
+ [JsonPropertyName("key")]
+ public required string Key { get; set; }
+#endif
[VectorStoreData(StorageName = "documentid")]
[JsonPropertyName("documentid")]
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json
index d7b2fc5dca0..5dde9fc2245 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.Development.json
@@ -6,4 +6,11 @@
"Microsoft.EntityFrameworkCore": "Warning"
}
}
+//#if (IsFoundryLocal)
+ ,"FoundryLocal": {
+ "ChatModel": "qwen3-4b",
+ "EmbeddingModel": "qwen3-embedding-0.6b",
+ "ServiceUrl": "http://127.0.0.1:5273"
+ }
+//#endif
}
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json
index 46bdb452246..ca99e211be7 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/AIChatWeb-CSharp.Web/appsettings.json
@@ -6,5 +6,12 @@
"Microsoft.EntityFrameworkCore": "Warning"
}
},
+//#if (IsFoundryLocal)
+ "FoundryLocal": {
+ "ChatModel": "qwen3-4b",
+ "EmbeddingModel": "qwen3-embedding-0.6b",
+ "ServiceUrl": "http://127.0.0.1:5273"
+ },
+//#endif
"AllowedHosts": "*"
}
diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md
index 0fff810489c..fc3071a5e80 100644
--- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md
+++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/templates/AIChatWeb-CSharp/README.Aspire.md
@@ -20,32 +20,6 @@ This incompatibility can be addressed by upgrading to Docker Desktop 4.41.1. See
# Configure the AI Model Provider
-#### ---#if (IsGHModels)
-## Using GitHub Models
-To use models hosted by GitHub Models, you will need to create a GitHub personal access token. The token should not have any scopes or permissions. See [Managing your personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens).
-
-#### ---#if (hostIdentifier == "vs")
-Configure your token for this project using .NET User Secrets:
-
-1. In Visual Studio, right-click on the AIChatWeb-CSharp.AppHost project in the Solution Explorer and select "Manage User Secrets".
-2. This opens a `secrets.json` file where you can store your API keys without them being tracked in source control. Add the following key and value:
-
- ```json
- {
- "ConnectionStrings:openai": "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
- }
- ```
-#### ---#else
-From the command line, configure your token for this project using .NET User Secrets by running the following commands:
-
-```sh
-cd AIChatWeb-CSharp.AppHost
-dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
-```
-#### ---#endif
-
-Learn more about [prototyping with AI models using GitHub Models](https://docs.github.com/github-models/prototyping-with-ai-models).
-#### ---#endif
#### ---#if (IsOpenAI)
## Using OpenAI
diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs
index 513f4cf0394..b16ab3c4fc9 100644
--- a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs
+++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/AIChatWebExecutionTests.cs
@@ -38,7 +38,7 @@ public AIChatWebExecutionTests(TemplateExecutionTestFixture fixture, ITestOutput
public static IEnumerable