Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
128 commits
Select commit Hold shift + click to select a range
d484ccf
Update
Dec 12, 2025
83ccfcc
delete useless files
Dec 12, 2025
6b70841
rename
Dec 12, 2025
2b960aa
rename
Dec 12, 2025
27773b8
rename
Dec 12, 2025
5c60cf1
remove acceleratorInfo
Dec 12, 2025
271f610
update
Dec 12, 2025
1199d02
update
Dec 12, 2025
98196fb
test nuget source
Dec 12, 2025
90eb03f
test nuget source
Dec 12, 2025
970ab4d
fix format
Dec 12, 2025
ef5c00b
fix format
Dec 12, 2025
378100c
CI/CD error
Dec 12, 2025
6811b13
CI/CD error
Dec 12, 2025
061806e
CI/CD error
Dec 12, 2025
2e5bcf1
Fix APPX1101 duplicate onnxruntime.dll error by adopting Foundry Loca…
Dec 12, 2025
32b52a7
CI/CD error
Dec 12, 2025
e555a51
Add telemetry
Dec 12, 2025
4733634
Add telemetry
Dec 12, 2025
a024a97
format
Dec 13, 2025
a46aed9
chatClient use SDK
Dec 13, 2025
ecb30f7
Fix
Dec 15, 2025
bc8c498
Fix
Dec 15, 2025
76b78cd
UPDATE
Dec 15, 2025
649d8cb
UPDATE
Dec 15, 2025
05fe712
revert
Dec 15, 2025
f88bdb1
update
Dec 15, 2025
e9aa10e
update
Dec 15, 2025
b5ca5b6
fix format
Dec 15, 2025
56841a3
fix format
Dec 15, 2025
2c3352e
Merge branch 'main' into milly/flmerge
weiyuanyue Dec 15, 2025
8db9866
Merge branch 'main' into milly/flmerge
weiyuanyue Dec 16, 2025
b5a9d59
Merge branch 'main' into milly/flmerge
wang563681252 Dec 17, 2025
0714600
Add test infra
Dec 17, 2025
2660efe
update
Dec 17, 2025
08bed0a
update
Dec 17, 2025
349f4e4
format
Dec 17, 2025
6d09f55
update
Dec 17, 2025
efeda2b
format
Dec 17, 2025
f2a265f
format
Dec 17, 2025
6c1e5e0
fail intention
Dec 17, 2025
8ce1623
revert
Dec 17, 2025
19a764c
fail intention
Dec 17, 2025
e95f57e
fix
Dec 17, 2025
e65e61f
update
Dec 17, 2025
89c7866
finish
Dec 17, 2025
6793e77
update
Dec 18, 2025
c8bb9e4
update
Dec 18, 2025
c81d470
rename
Dec 18, 2025
4e62906
temp: add pull_request trigger for testing
Dec 18, 2025
e3c324e
update
Dec 18, 2025
c41b01b
update
Dec 18, 2025
fa7a526
Fix: Move MSIX deployment after build steps
Dec 18, 2025
dfdc4fd
Remove pull_request trigger to avoid parallel execution with CI
Dec 18, 2025
49307e8
Merge remote-tracking branch 'origin/main' into milly/uiamerge
Dec 18, 2025
916230b
Fix test report path format
Dec 18, 2025
bfe802d
Remove CI trigger
Dec 18, 2025
416d8c2
remove Upload Build Artifacts for Testing
Dec 18, 2025
d96d43f
update
Dec 18, 2025
d3a39db
update
Dec 18, 2025
2c92bbf
optimize unit test workflow with coverage and conditional artifacts
Dec 18, 2025
6c1eb70
update
Dec 18, 2025
928117c
update
Dec 18, 2025
6d476b8
format
Dec 18, 2025
2ad691d
format
Dec 18, 2025
9571287
test
Dec 18, 2025
006a956
test
Dec 18, 2025
b9c4301
test
Dec 18, 2025
de18197
Merge branch 'main' into milly/uiamerge
weiyuanyue Dec 19, 2025
f389025
Merge branch 'main' into milly/flmerge
weiyuanyue Dec 19, 2025
4293866
remove useless file
Dec 19, 2025
7fdc8da
Merge branch 'main' into milly/uiamerge
wang563681252 Dec 19, 2025
81ce59f
Use AsyncLocal to isolate test performance metrics
Dec 24, 2025
4b821de
Merge remote-tracking branch 'origin/milly/uiamerge' into milly/uiamerge
Dec 24, 2025
5b03266
Clear measurements after save to prevent memory leak
Dec 24, 2025
947b70e
Add cleanup failure warnings and refactor MSIX deployment
Dec 24, 2025
a752bad
ci: Include Directory.Packages.props in NuGet cache key
Dec 24, 2025
0944d37
format
Dec 24, 2025
d91b38a
Replace Thread.Sleep with explicit waits in NavigationViewTests
Dec 24, 2025
240a2e4
remove comment
Dec 24, 2025
dd5a143
Fix
Dec 24, 2025
e257423
update
Dec 24, 2025
b59608a
update
Dec 24, 2025
5ac1416
update
Dec 24, 2025
6f2b456
Merge remote-tracking branch 'origin/main' into milly/flmerge
Dec 25, 2025
8677327
Merge remote-tracking branch 'origin/milly/uiamerge' into milly/flmerge
Dec 25, 2025
546b64e
Merge remote-tracking branch 'origin/milly/flmerge' into milly/flmerge
Dec 25, 2025
b5858e1
Add empty stream detection with informative error message
Dec 25, 2025
4d314ba
Merge remote-tracking branch 'origin/main' into milly/flmerge
Dec 25, 2025
0567fe7
format
Dec 25, 2025
9d013a2
format
Dec 25, 2025
fbccca6
Add FL clean cache
Dec 25, 2025
be62c3a
update
Dec 25, 2025
f7ef6f6
update
Dec 25, 2025
0ec0332
update
Dec 25, 2025
dfe7ee5
update
Dec 25, 2025
d68faef
update
Dec 25, 2025
f4ac479
update
Dec 25, 2025
f74f01d
format
Dec 26, 2025
d4d105b
Add Telemetry
Dec 26, 2025
a920b21
Update FoundryLocal not available message for SDK-based implementation
Dec 26, 2025
b7618db
Add retry button
Dec 26, 2025
c4188ae
use ModelCacheDir
Dec 26, 2025
792a743
remove web
Dec 26, 2025
8b1fa6f
update
Dec 26, 2025
2727c98
remove url
Dec 26, 2025
28feede
update
Dec 26, 2025
a5330e6
update
Dec 26, 2025
85c0db7
Add task-based filtering for Foundry Local model picker
Dec 27, 2025
3e3caa2
Add task-based filtering for Foundry Local model picker
Dec 27, 2025
6b38291
code clean
Dec 27, 2025
66cd8f1
format
Dec 27, 2025
2fdefb7
Add UT
Dec 27, 2025
48b4714
format
Dec 27, 2025
ed6b6ea
format
Dec 27, 2025
d472fad
rename
Dec 27, 2025
d3a5762
rename
Dec 27, 2025
1e2deec
Eliminated sync-over-async anti-pattern
Dec 28, 2025
cda55e4
update file path
Dec 28, 2025
9ccc8f9
revert
Dec 29, 2025
c7c4682
update
Dec 29, 2025
c91bd28
remove useless telemetry
Dec 29, 2025
b65b45b
format
Dec 29, 2025
f7fd545
Add UT
Dec 29, 2025
bcb8917
update comment
Dec 29, 2025
459cea9
update
Dec 29, 2025
8858893
format
Dec 29, 2025
88e4ab0
UT format
Dec 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chatClient use SDK
  • Loading branch information
Milly Wei (from Dev Box) committed Dec 13, 2025
commit a46aed9882e5a0ca6e8b10143e00b4a297b3dfa5
26 changes: 8 additions & 18 deletions AIDevGallery/ExternalModelUtils/FoundryLocal/FoundryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace AIDevGallery.ExternalModelUtils.FoundryLocal;

internal class FoundryClient
{
private readonly Dictionary<string, (string ServiceUrl, string ModelId)> _preparedModels = new();
private readonly Dictionary<string, IModel> _preparedModels = new();
private readonly SemaphoreSlim _prepareLock = new(1, 1);
private FoundryLocalManager? _manager;
private ICatalog? _catalog;
Expand Down Expand Up @@ -130,7 +130,7 @@ await model.DownloadAsync(
}

/// <summary>
/// Prepares a model for use by loading it and starting the web service.
/// Prepares a model for use by loading it (no web service needed).
/// Should be called after download or when first accessing a cached model.
/// Thread-safe: multiple concurrent calls for the same alias will only prepare once.
/// </summary>
Expand Down Expand Up @@ -168,18 +168,8 @@ public async Task PrepareModelAsync(string alias, CancellationToken cancellation
await model.LoadAsync(cancellationToken);
}

if (_manager.Urls == null || _manager.Urls.Length == 0)
{
await _manager.StartWebServiceAsync(cancellationToken);
}

var serviceUrl = _manager.Urls?.FirstOrDefault();
if (string.IsNullOrEmpty(serviceUrl))
{
throw new InvalidOperationException("Failed to start Foundry Local web service");
}

_preparedModels[alias] = (serviceUrl, model.Id);
// Store the model directly - no web service needed
_preparedModels[alias] = model;
}
finally
{
Expand All @@ -188,13 +178,13 @@ public async Task PrepareModelAsync(string alias, CancellationToken cancellation
}

/// <summary>
/// Gets the service URL and model ID for a prepared model.
/// Gets the prepared model.
/// Returns null if the model hasn't been prepared yet.
/// </summary>
/// <returns>A tuple containing the service URL and model ID, or null if not prepared.</returns>
public (string ServiceUrl, string ModelId)? GetPreparedModel(string alias)
/// <returns>The IModel instance, or null if not prepared.</returns>
public IModel? GetPreparedModel(string alias)
{
return _preparedModels.TryGetValue(alias, out var info) ? info : null;
return _preparedModels.TryGetValue(alias, out var model) ? model : null;
}

public Task<string?> GetServiceUrl()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Extensions.AI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

namespace AIDevGallery.ExternalModelUtils.FoundryLocal;

/// <summary>
/// Adapter that wraps FoundryLocal SDK's native OpenAIChatClient to work with Microsoft.Extensions.AI.IChatClient.
/// Uses the SDK's direct model API (no web service) to avoid SSE compatibility issues.
/// </summary>
internal class FoundryLocalChatClientAdapter : IChatClient
{
private readonly Microsoft.AI.Foundry.Local.OpenAIChatClient _chatClient;
private readonly string _modelId;

public FoundryLocalChatClientAdapter(Microsoft.AI.Foundry.Local.OpenAIChatClient chatClient, string modelId)
{
_modelId = modelId;
_chatClient = chatClient;

// CRITICAL: MaxTokens must be set, otherwise the model won't generate any output
if (_chatClient.Settings.MaxTokens == null)
{
_chatClient.Settings.MaxTokens = 512;
}

if (_chatClient.Settings.Temperature == null)
{
_chatClient.Settings.Temperature = 0.7f;
}
}

public ChatClientMetadata Metadata => new("FoundryLocal", new Uri($"foundrylocal:///{_modelId}"), _modelId);

public Task<ChatResponse> GetResponseAsync(
IEnumerable<Microsoft.Extensions.AI.ChatMessage> chatMessages,
ChatOptions? options = null,
CancellationToken cancellationToken = default) =>
GetStreamingResponseAsync(chatMessages, options, cancellationToken).ToChatResponseAsync(cancellationToken: cancellationToken);

public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<Microsoft.Extensions.AI.ChatMessage> chatMessages,
ChatOptions? options = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var messageList = chatMessages.ToList();
var openAIMessages = ConvertToFoundryMessages(messageList);

// Use FoundryLocal SDK's native streaming API - direct in-memory communication, no HTTP/SSE
var streamingResponse = _chatClient.CompleteChatStreamingAsync(openAIMessages, cancellationToken);

string responseId = Guid.NewGuid().ToString("N");
await foreach (var chunk in streamingResponse)
{
cancellationToken.ThrowIfCancellationRequested();

if (chunk.Choices != null && chunk.Choices.Count > 0)
{
var content = chunk.Choices[0].Message?.Content;
if (!string.IsNullOrEmpty(content))
{
yield return new ChatResponseUpdate(ChatRole.Assistant, content)
{
ResponseId = responseId
};
}
}
}
}

public object? GetService(Type serviceType, object? serviceKey = null)
{
return serviceType?.IsInstanceOfType(this) == true ? this : null;
}

public void Dispose()
{
// ChatClient doesn't need disposal
}

private static List<Betalgo.Ranul.OpenAI.ObjectModels.RequestModels.ChatMessage> ConvertToFoundryMessages(IList<Microsoft.Extensions.AI.ChatMessage> messages)
{
return messages.Select(m => new Betalgo.Ranul.OpenAI.ObjectModels.RequestModels.ChatMessage
{
Role = m.Role.Value,
Content = m.Text ?? string.Empty
}).ToList();
}
}
36 changes: 9 additions & 27 deletions AIDevGallery/ExternalModelUtils/FoundryLocalModelProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
using AIDevGallery.Telemetry.Events;
using AIDevGallery.Utils;
using Microsoft.Extensions.AI;
using OpenAI;
using System;
using System.ClientModel;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
Expand Down Expand Up @@ -56,19 +54,18 @@ internal class FoundryLocalModelProvider : IExternalModelProvider
}

// Must be prepared beforehand via EnsureModelReadyAsync to avoid deadlock
var preparedInfo = _foundryManager.GetPreparedModel(alias);
if (preparedInfo == null)
var model = _foundryManager.GetPreparedModel(alias);
if (model == null)
{
throw new InvalidOperationException(
$"Model '{alias}' is not ready yet. The model is being loaded in the background. Please wait a moment and try again.");
}

var (serviceUrl, modelId) = preparedInfo.Value;
// Get the native FoundryLocal chat client - direct SDK usage, no web service needed
var chatClient = model.GetChatClientAsync().Result;

return new OpenAIClient(new ApiKeyCredential("none"), new OpenAIClientOptions
{
Endpoint = new Uri($"{serviceUrl}/v1")
}).GetChatClient(modelId).AsIChatClient();
// Wrap it in our adapter to implement IChatClient interface
return new FoundryLocal.FoundryLocalChatClientAdapter(chatClient, model.Id);
}

public string? GetIChatClientString(string url)
Expand All @@ -80,14 +77,13 @@ internal class FoundryLocalModelProvider : IExternalModelProvider
return null;
}

var preparedInfo = _foundryManager.GetPreparedModel(alias);
if (preparedInfo == null)
var model = _foundryManager.GetPreparedModel(alias);
if (model == null)
{
return null;
}

var (serviceUrl, modelId) = preparedInfo.Value;
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
return $"var model = await catalog.GetModelAsync(\"{alias}\"); await model.LoadAsync(); var chatClient = await model.GetChatClientAsync(); /* Use chatClient.CompleteChatStreamingAsync() */";
}

public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
Expand Down Expand Up @@ -168,20 +164,6 @@ private async Task InitializeAsync(CancellationToken cancelationToken = default)
if (hasCachedVariant)
{
downloadedModels.Add(firstModel);

_ = Task.Run(
async () =>
{
try
{
await _foundryManager.PrepareModelAsync(catalogModel.Alias, cancelationToken);
}
catch
{
// Silently fail - user will see "not ready" error when attempting to use the model
}
},
cancelationToken);
}
}

Expand Down