mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
foundry stuff
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
|
||||||
@@ -39,9 +39,11 @@
|
|||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||||
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
<PackageVersion Include="Microsoft.Windows.CppWinRT" Version="2.0.240111.5" />
|
||||||
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
<PackageVersion Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.1.16" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.9.1-preview.1.25474.6" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.9" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.9" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.WindowsServices" Version="9.0.8" />
|
||||||
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.64.0" />
|
<PackageVersion Include="Microsoft.SemanticKernel" Version="1.64.0" />
|
||||||
@@ -71,7 +73,7 @@
|
|||||||
<PackageVersion Include="NLog" Version="5.2.8" />
|
<PackageVersion Include="NLog" Version="5.2.8" />
|
||||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||||
<PackageVersion Include="OpenAI" Version="2.3.0" />
|
<PackageVersion Include="OpenAI" Version="2.5.0" />
|
||||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||||
|
|||||||
@@ -825,6 +825,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightSwitch.UITests", "src\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests\Microsoft.CmdPal.Ext.ClipboardHistory.UnitTests.csproj", "{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LanguageModelProvider", "src\common\LanguageModelProvider\LanguageModelProvider.csproj", "{45354F4F-1414-45CE-B600-51CD1209FD19}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|ARM64 = Debug|ARM64
|
Debug|ARM64 = Debug|ARM64
|
||||||
@@ -2995,6 +2997,14 @@ Global
|
|||||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.ActiveCfg = Release|x64
|
||||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA}.Release|x64.Build.0 = Release|x64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|ARM64.Build.0 = Release|ARM64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19}.Release|x64.Build.0 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -3323,6 +3333,7 @@ Global
|
|||||||
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
{3DCCD936-D085-4869-A1DE-CA6A64152C94} = {5B201255-53C8-490B-A34F-01F05D48A477}
|
||||||
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
{F5333ED7-06D8-4AB3-953A-36D63F08CB6F} = {3DCCD936-D085-4869-A1DE-CA6A64152C94}
|
||||||
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
{4E0FCF69-B06B-D272-76BF-ED3A559B4EDA} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
|
||||||
|
{45354F4F-1414-45CE-B600-51CD1209FD19} = {1AFB6476-670D-4E80-A464-657E01DFF482}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||||
|
|||||||
14
src/common/LanguageModelProvider/AppUtils.cs
Normal file
14
src/common/LanguageModelProvider/AppUtils.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
internal static class AppUtils
|
||||||
|
{
|
||||||
|
public static string GetThemeAssetSuffix()
|
||||||
|
{
|
||||||
|
// Default suffix for assets that are theme-agnostic today.
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record FoundryCachedModel(string Name, string? Id);
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record FoundryCatalogModel
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("displayName")]
|
||||||
|
public string DisplayName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("providerType")]
|
||||||
|
public string ProviderType { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("uri")]
|
||||||
|
public string Uri { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string Version { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("modelType")]
|
||||||
|
public string ModelType { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("promptTemplate")]
|
||||||
|
public PromptTemplate PromptTemplate { get; init; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("publisher")]
|
||||||
|
public string Publisher { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("task")]
|
||||||
|
public string Task { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("runtime")]
|
||||||
|
public Runtime Runtime { get; init; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("fileSizeMb")]
|
||||||
|
public long FileSizeMb { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("modelSettings")]
|
||||||
|
public ModelSettings ModelSettings { get; init; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("alias")]
|
||||||
|
public string Alias { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("supportsToolCalling")]
|
||||||
|
public bool SupportsToolCalling { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("license")]
|
||||||
|
public string License { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("licenseDescription")]
|
||||||
|
public string LicenseDescription { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("parentModelUri")]
|
||||||
|
public string ParentModelUri { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
214
src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
Normal file
214
src/common/LanguageModelProvider/FoundryLocal/FoundryClient.cs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed class FoundryClient
|
||||||
|
{
|
||||||
|
public static async Task<FoundryClient?> CreateAsync()
|
||||||
|
{
|
||||||
|
var serviceManager = FoundryServiceManager.TryCreate();
|
||||||
|
if (serviceManager is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await serviceManager.IsRunning().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (!await serviceManager.StartService().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceUrl = await serviceManager.GetServiceUrl().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(serviceUrl))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FoundryClient(serviceUrl, serviceManager, new HttpClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
public FoundryServiceManager ServiceManager { get; }
|
||||||
|
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private readonly List<FoundryCatalogModel> _catalogModels = [];
|
||||||
|
|
||||||
|
private FoundryClient(string baseUrl, FoundryServiceManager serviceManager, HttpClient httpClient)
|
||||||
|
{
|
||||||
|
ServiceManager = serviceManager;
|
||||||
|
_baseUrl = baseUrl;
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FoundryCatalogModel>> ListCatalogModels()
|
||||||
|
{
|
||||||
|
if (_catalogModels.Count > 0)
|
||||||
|
{
|
||||||
|
return _catalogModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync($"{_baseUrl}/foundry/list").ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var models = await JsonSerializer.DeserializeAsync(
|
||||||
|
response.Content.ReadAsStream(),
|
||||||
|
FoundryJsonContext.Default.ListFoundryCatalogModel).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (models is { Count: > 0 })
|
||||||
|
{
|
||||||
|
models.ForEach(_catalogModels.Add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Surfacing errors here prevents listing other providers; swallow and return cached list instead.
|
||||||
|
}
|
||||||
|
|
||||||
|
return _catalogModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<FoundryCachedModel>> ListCachedModels()
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync($"{_baseUrl}/openai/models").ConfigureAwait(false);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var catalogModels = await ListCatalogModels().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||||
|
var modelIds = content
|
||||||
|
.Trim('[', ']')
|
||||||
|
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(id => id.Trim('"'));
|
||||||
|
|
||||||
|
List<FoundryCachedModel> models = [];
|
||||||
|
|
||||||
|
foreach (var id in modelIds)
|
||||||
|
{
|
||||||
|
var model = catalogModels.FirstOrDefault(m => m.Name == id);
|
||||||
|
models.Add(model != null ? new FoundryCachedModel(id, model.Alias) : new FoundryCachedModel(id, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FoundryDownloadResult> DownloadModel(FoundryCatalogModel model, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var models = await ListCachedModels().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (models.Any(m => m.Name == model.Name))
|
||||||
|
{
|
||||||
|
return new(true, "Model already downloaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.Run(
|
||||||
|
async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var modelDownload = new FoundryModelDownload(
|
||||||
|
Name: model.Name,
|
||||||
|
Uri: model.Uri,
|
||||||
|
Path: await GetModelPath(model.Uri).ConfigureAwait(false), // temporary
|
||||||
|
ProviderType: model.ProviderType,
|
||||||
|
PromptTemplate: model.PromptTemplate);
|
||||||
|
|
||||||
|
var uploadBody = new FoundryDownloadBody(modelDownload, IgnorePipeReport: true);
|
||||||
|
|
||||||
|
var downloadBodyContext = FoundryJsonContext.Default.FoundryDownloadBody;
|
||||||
|
string body = JsonSerializer.Serialize(uploadBody, downloadBodyContext);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/openai/download")
|
||||||
|
{
|
||||||
|
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||||
|
};
|
||||||
|
|
||||||
|
using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
using var reader = new StreamReader(stream);
|
||||||
|
|
||||||
|
string? finalJson = null;
|
||||||
|
|
||||||
|
while (!reader.EndOfStream && !cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
line = line.Trim();
|
||||||
|
|
||||||
|
if (finalJson != null || line.StartsWith('{'))
|
||||||
|
{
|
||||||
|
finalJson += line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(line, @"\d+(\.\d+)?%");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var percentage = match.Value;
|
||||||
|
if (float.TryParse(percentage.TrimEnd('%'), out float progressValue))
|
||||||
|
{
|
||||||
|
progress?.Report(progressValue / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadResultContext = FoundryJsonContext.Default.FoundryDownloadResult;
|
||||||
|
var result = finalJson is not null
|
||||||
|
? JsonSerializer.Deserialize(finalJson, downloadResultContext)!
|
||||||
|
: new FoundryDownloadResult(false, "Missing final result from server.");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return new FoundryDownloadResult(false, e.Message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is a temporary function to get the model path from the blob storage
|
||||||
|
// it will be removed once the tag is available in the list response
|
||||||
|
private async Task<string> GetModelPath(string assetId)
|
||||||
|
{
|
||||||
|
var registryUri =
|
||||||
|
$"https://eastus.api.azureml.ms/modelregistry/v1.0/registry/models/nonazureaccount?assetId={Uri.EscapeDataString(assetId)}";
|
||||||
|
|
||||||
|
using var resp = await _httpClient.GetAsync(registryUri).ConfigureAwait(false);
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
await using var jsonStream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||||
|
var jsonRoot = await JsonDocument.ParseAsync(jsonStream).ConfigureAwait(false);
|
||||||
|
var blobSasUri = jsonRoot.RootElement.GetProperty("blobSasUri").GetString()!;
|
||||||
|
|
||||||
|
var uriBuilder = new UriBuilder(blobSasUri);
|
||||||
|
var existingQuery = string.IsNullOrWhiteSpace(uriBuilder.Query)
|
||||||
|
? string.Empty
|
||||||
|
: uriBuilder.Query.TrimStart('?') + "&";
|
||||||
|
|
||||||
|
uriBuilder.Query = existingQuery + "restype=container&comp=list&delimiter=/";
|
||||||
|
|
||||||
|
var listXml = await _httpClient.GetStringAsync(uriBuilder.Uri).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var match = Regex.Match(listXml, @"<Name>(.*?)\/<\/Name>");
|
||||||
|
return match.Success ? match.Groups[1].Value : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record FoundryDownloadBody(FoundryModelDownload Model, bool IgnorePipeReport);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record FoundryDownloadResult(bool Success, string? ErrorMessage);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(
|
||||||
|
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = false)]
|
||||||
|
[JsonSerializable(typeof(FoundryCatalogModel))]
|
||||||
|
[JsonSerializable(typeof(List<FoundryCatalogModel>))]
|
||||||
|
[JsonSerializable(typeof(FoundryDownloadResult))]
|
||||||
|
[JsonSerializable(typeof(FoundryDownloadBody))]
|
||||||
|
internal sealed partial class FoundryJsonContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record FoundryModelDownload(
|
||||||
|
string Name,
|
||||||
|
string Uri,
|
||||||
|
string Path,
|
||||||
|
string ProviderType,
|
||||||
|
PromptTemplate PromptTemplate);
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed class FoundryServiceManager
|
||||||
|
{
|
||||||
|
public static FoundryServiceManager? TryCreate()
|
||||||
|
{
|
||||||
|
return IsAvailable() ? new FoundryServiceManager() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAvailable()
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo.FileName = "where";
|
||||||
|
process.StartInfo.Arguments = "foundry";
|
||||||
|
process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
process.StartInfo.RedirectStandardError = true;
|
||||||
|
process.StartInfo.UseShellExecute = false;
|
||||||
|
process.StartInfo.CreateNoWindow = true;
|
||||||
|
process.Start();
|
||||||
|
process.WaitForExit();
|
||||||
|
return process.ExitCode == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetUrl(string output)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(output, @"https?:\/\/[^\/]+:\d+");
|
||||||
|
return match.Success ? match.Value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetServiceUrl()
|
||||||
|
{
|
||||||
|
var status = await Utils.RunFoundryWithArguments("service status").ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (status.ExitCode != 0 || string.IsNullOrWhiteSpace(status.Output))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetUrl(status.Output);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsRunning()
|
||||||
|
{
|
||||||
|
var url = await GetServiceUrl().ConfigureAwait(false);
|
||||||
|
return url is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> StartService()
|
||||||
|
{
|
||||||
|
if (await IsRunning().ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = await Utils.RunFoundryWithArguments("service start").ConfigureAwait(false);
|
||||||
|
if (status.ExitCode != 0 || string.IsNullOrWhiteSpace(status.Output))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetUrl(status.Output) is not null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record ModelSettings
|
||||||
|
{
|
||||||
|
// The sample shows an empty array; keep it open-ended.
|
||||||
|
[JsonPropertyName("parameters")]
|
||||||
|
public List<JsonElement> Parameters { get; init; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record PromptTemplate
|
||||||
|
{
|
||||||
|
[JsonPropertyName("assistant")]
|
||||||
|
public string Assistant { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("prompt")]
|
||||||
|
public string Prompt { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
16
src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
Normal file
16
src/common/LanguageModelProvider/FoundryLocal/Runtime.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal sealed record Runtime
|
||||||
|
{
|
||||||
|
[JsonPropertyName("deviceType")]
|
||||||
|
public string DeviceType { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("executionProvider")]
|
||||||
|
public string ExecutionProvider { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
37
src/common/LanguageModelProvider/FoundryLocal/Utils.cs
Normal file
37
src/common/LanguageModelProvider/FoundryLocal/Utils.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider.FoundryLocal;
|
||||||
|
|
||||||
|
internal static class Utils
|
||||||
|
{
|
||||||
|
public static async Task<(string? Output, string? Error, int ExitCode)> RunFoundryWithArguments(string arguments)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var process = new Process();
|
||||||
|
process.StartInfo.FileName = "foundry";
|
||||||
|
process.StartInfo.Arguments = arguments;
|
||||||
|
process.StartInfo.RedirectStandardOutput = true;
|
||||||
|
process.StartInfo.RedirectStandardError = true;
|
||||||
|
process.StartInfo.UseShellExecute = false;
|
||||||
|
process.StartInfo.CreateNoWindow = true;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
string? output = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
string? error = await process.StandardError.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
await process.WaitForExitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return (output, error, process.ExitCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (null, null, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
Normal file
210
src/common/LanguageModelProvider/FoundryLocalModelProvider.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ClientModel;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using LanguageModelProvider.FoundryLocal;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
using OpenAI;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
public sealed class FoundryLocalModelProvider : ILanguageModelProvider
|
||||||
|
{
|
||||||
|
private IEnumerable<ModelDetails>? _downloadedModels;
|
||||||
|
private IEnumerable<ModelDetails>? _catalogModels;
|
||||||
|
private FoundryClient? _foundryManager;
|
||||||
|
private string? _serviceUrl;
|
||||||
|
|
||||||
|
public static FoundryLocalModelProvider Instance { get; } = new();
|
||||||
|
|
||||||
|
public string Name => "FoundryLocal";
|
||||||
|
|
||||||
|
public HardwareAccelerator ModelHardwareAccelerator => HardwareAccelerator.FOUNDRYLOCAL;
|
||||||
|
|
||||||
|
public List<string> NugetPackageReferences { get; } = ["Microsoft.Extensions.AI.OpenAI"];
|
||||||
|
|
||||||
|
public string ProviderDescription => "The model will run locally via Foundry Local";
|
||||||
|
|
||||||
|
public string UrlPrefix => "fl://";
|
||||||
|
|
||||||
|
public string Icon => $"fl{AppUtils.GetThemeAssetSuffix()}.svg";
|
||||||
|
|
||||||
|
public string Url => _serviceUrl ?? string.Empty;
|
||||||
|
|
||||||
|
public string IChatClientImplementationNamespace { get; } = "OpenAI";
|
||||||
|
|
||||||
|
public string GetDetailsUrl(ModelDetails details)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChatClient? GetIChatClient(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_serviceUrl))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelId = url.Split('/').LastOrDefault();
|
||||||
|
if (string.IsNullOrWhiteSpace(modelId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenAIClient(
|
||||||
|
new ApiKeyCredential("none"),
|
||||||
|
new OpenAIClientOptions { Endpoint = new Uri($"{_serviceUrl}/v1") })
|
||||||
|
.GetChatClient(modelId)
|
||||||
|
.AsIChatClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetIChatClientString(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InitializeAsync().GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelId = url.Split('/').LastOrDefault();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_serviceUrl) || string.IsNullOrWhiteSpace(modelId))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"new OpenAIClient(new ApiKeyCredential(\"none\"), new OpenAIClientOptions{{ Endpoint = new Uri(\"{_serviceUrl}/v1\") }}).GetChatClient(\"{modelId}\").AsIChatClient()";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default)
|
||||||
|
{
|
||||||
|
if (ignoreCached)
|
||||||
|
{
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
await InitializeAsync(cancelationToken);
|
||||||
|
|
||||||
|
return _downloadedModels ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ModelDetails> GetAllModelsInCatalog()
|
||||||
|
{
|
||||||
|
return _catalogModels ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DownloadModel(ModelDetails modelDetails, IProgress<float>? progress, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_foundryManager == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelDetails.ProviderModelDetails is not FoundryCatalogModel model)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await _foundryManager.DownloadModel(model, progress, cancellationToken)).Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
_downloadedModels = null;
|
||||||
|
_ = InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync(CancellationToken cancelationToken = default)
|
||||||
|
{
|
||||||
|
if (_foundryManager != null && _downloadedModels != null && _downloadedModels.Any())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_foundryManager ??= await FoundryClient.CreateAsync();
|
||||||
|
|
||||||
|
if (_foundryManager == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_serviceUrl ??= await _foundryManager.ServiceManager.GetServiceUrl();
|
||||||
|
|
||||||
|
if (_catalogModels == null || !_catalogModels.Any())
|
||||||
|
{
|
||||||
|
_catalogModels = (await _foundryManager.ListCatalogModels()).Select(ToModelDetails).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cachedModels = await _foundryManager.ListCachedModels();
|
||||||
|
|
||||||
|
List<ModelDetails> downloadedModels = [];
|
||||||
|
|
||||||
|
foreach (var model in _catalogModels)
|
||||||
|
{
|
||||||
|
var cachedModel = cachedModels.FirstOrDefault(m => m.Name == model.Name);
|
||||||
|
|
||||||
|
if (cachedModel != default)
|
||||||
|
{
|
||||||
|
model.Id = $"{UrlPrefix}{cachedModel.Id}";
|
||||||
|
downloadedModels.Add(model);
|
||||||
|
cachedModels.Remove(cachedModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var model in cachedModels)
|
||||||
|
{
|
||||||
|
downloadedModels.Add(new ModelDetails
|
||||||
|
{
|
||||||
|
Id = $"fl-{model.Name}",
|
||||||
|
Name = model.Name,
|
||||||
|
Url = $"{UrlPrefix}{model.Name}",
|
||||||
|
Description = $"{model.Name} running locally with Foundry Local",
|
||||||
|
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||||
|
SupportedOnQualcomm = true,
|
||||||
|
ProviderModelDetails = model,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_downloadedModels = downloadedModels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ModelDetails ToModelDetails(FoundryCatalogModel model)
|
||||||
|
{
|
||||||
|
return new ModelDetails
|
||||||
|
{
|
||||||
|
Id = $"fl-{model.Name}",
|
||||||
|
Name = model.Name,
|
||||||
|
Url = $"{UrlPrefix}{model.Name}",
|
||||||
|
Description = $"{model.Alias} running locally with Foundry Local",
|
||||||
|
HardwareAccelerators = [HardwareAccelerator.FOUNDRYLOCAL],
|
||||||
|
Size = model.FileSizeMb * 1024 * 1024,
|
||||||
|
SupportedOnQualcomm = true,
|
||||||
|
License = model.License?.ToLowerInvariant() ?? string.Empty,
|
||||||
|
ProviderModelDetails = model,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> IsAvailable()
|
||||||
|
{
|
||||||
|
await InitializeAsync();
|
||||||
|
return _foundryManager != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/common/LanguageModelProvider/HardwareAccelerator.cs
Normal file
22
src/common/LanguageModelProvider/HardwareAccelerator.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
public enum HardwareAccelerator
|
||||||
|
{
|
||||||
|
CPU,
|
||||||
|
DML,
|
||||||
|
QNN,
|
||||||
|
WCRAPI,
|
||||||
|
OLLAMA,
|
||||||
|
OPENAI,
|
||||||
|
FOUNDRYLOCAL,
|
||||||
|
LEMONADE,
|
||||||
|
NPU,
|
||||||
|
GPU,
|
||||||
|
VitisAI,
|
||||||
|
OpenVINO,
|
||||||
|
NvTensorRT,
|
||||||
|
}
|
||||||
34
src/common/LanguageModelProvider/ILanguageModelProvider.cs
Normal file
34
src/common/LanguageModelProvider/ILanguageModelProvider.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
public interface ILanguageModelProvider
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
string UrlPrefix { get; }
|
||||||
|
|
||||||
|
string Icon { get; }
|
||||||
|
|
||||||
|
HardwareAccelerator ModelHardwareAccelerator { get; }
|
||||||
|
|
||||||
|
List<string> NugetPackageReferences { get; }
|
||||||
|
|
||||||
|
string ProviderDescription { get; }
|
||||||
|
|
||||||
|
Task<IEnumerable<ModelDetails>> GetModelsAsync(bool ignoreCached = false, CancellationToken cancelationToken = default);
|
||||||
|
|
||||||
|
IChatClient? GetIChatClient(string url);
|
||||||
|
|
||||||
|
string IChatClientImplementationNamespace { get; }
|
||||||
|
|
||||||
|
string GetIChatClientString(string url);
|
||||||
|
|
||||||
|
string GetDetailsUrl(ModelDetails details);
|
||||||
|
|
||||||
|
string Url { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||||
|
<Import Project="..\..\Common.Dotnet.CsWinRT.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.AI" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
|
||||||
|
<PackageReference Include="OpenAI" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
108
src/common/LanguageModelProvider/LanguageModelService.cs
Normal file
108
src/common/LanguageModelProvider/LanguageModelService.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Extensions.AI;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
public sealed class LanguageModelService
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, ILanguageModelProvider> _providersByPrefix;
|
||||||
|
|
||||||
|
public LanguageModelService(IEnumerable<ILanguageModelProvider> providers)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(providers);
|
||||||
|
|
||||||
|
_providersByPrefix = new ConcurrentDictionary<string, ILanguageModelProvider>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var provider in providers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||||
|
{
|
||||||
|
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LanguageModelService CreateDefault()
|
||||||
|
{
|
||||||
|
return new LanguageModelService(new[]
|
||||||
|
{
|
||||||
|
FoundryLocalModelProvider.Instance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyCollection<ILanguageModelProvider> Providers => _providersByPrefix.Values.ToArray();
|
||||||
|
|
||||||
|
public bool RegisterProvider(ILanguageModelProvider provider)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(provider);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(provider.UrlPrefix))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Provider must supply a URL prefix.", nameof(provider));
|
||||||
|
}
|
||||||
|
|
||||||
|
_providersByPrefix[provider.UrlPrefix] = provider;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILanguageModelProvider? GetProviderFor(string? modelReference)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(modelReference))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var provider in _providersByPrefix.Values)
|
||||||
|
{
|
||||||
|
if (modelReference.StartsWith(provider.UrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ModelDetails>> GetModelsAsync(bool refresh = false, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
List<ModelDetails> models = [];
|
||||||
|
|
||||||
|
foreach (var provider in _providersByPrefix.Values)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
var providerModels = await provider.GetModelsAsync(refresh, cancellationToken).ConfigureAwait(false);
|
||||||
|
models.AddRange(providerModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChatClient? GetClient(ModelDetails model)
|
||||||
|
{
|
||||||
|
if (model is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reference = !string.IsNullOrWhiteSpace(model.Url) ? model.Url : model.Id;
|
||||||
|
return GetClient(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChatClient? GetClient(string? modelReference)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(modelReference))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = GetProviderFor(modelReference);
|
||||||
|
|
||||||
|
return provider?.GetIChatClient(modelReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/common/LanguageModelProvider/ModelDetails.cs
Normal file
32
src/common/LanguageModelProvider/ModelDetails.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace LanguageModelProvider;
|
||||||
|
|
||||||
|
public class ModelDetails
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
public bool IsUserAdded { get; set; }
|
||||||
|
|
||||||
|
public string Icon { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public List<HardwareAccelerator> HardwareAccelerators { get; set; } = [];
|
||||||
|
|
||||||
|
public bool SupportedOnQualcomm { get; set; }
|
||||||
|
|
||||||
|
public string License { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public object? ProviderModelDetails { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user