diff --git a/sdk/cs/src/FoundryLocalManager.cs b/sdk/cs/src/FoundryLocalManager.cs index 737c965..6224585 100644 --- a/sdk/cs/src/FoundryLocalManager.cs +++ b/sdk/cs/src/FoundryLocalManager.cs @@ -258,30 +258,38 @@ public async Task> ListCachedModelsAsync(CancellationToken ct = IgnorePipeReport = true }; - var response = await _serviceClient!.PostAsJsonAsync("/openai/download", request, ct); - response.EnsureSuccessStatusCode(); - var responseBody = await response.Content.ReadAsStringAsync(ct); - - // Find the last '{' to get the start of the JSON object - var jsonStart = responseBody.LastIndexOf('{'); - if (jsonStart == -1) + try { - throw new InvalidOperationException("No JSON object found in response."); - } + var response = await _serviceClient!.PostAsJsonAsync("/openai/download", request, ct); + response.EnsureSuccessStatusCode(); + var responseBody = await response.Content.ReadAsStringAsync(ct); - var jsonPart = responseBody[jsonStart..]; + // Find the last '{' to get the start of the JSON object + var jsonStart = responseBody.LastIndexOf('{'); + if (jsonStart == -1) + { + throw new InvalidOperationException("No JSON object found in response."); + } - // Parse the JSON part - using var jsonDoc = JsonDocument.Parse(jsonPart); - var success = jsonDoc.RootElement.GetProperty("success").GetBoolean(); - var errorMessage = jsonDoc.RootElement.GetProperty("errorMessage").GetString(); + var jsonPart = responseBody[jsonStart..]; - if (!success) + // Parse the JSON part + using var jsonDoc = JsonDocument.Parse(jsonPart); + var success = jsonDoc.RootElement.GetProperty("success").GetBoolean(); + var errorMessage = jsonDoc.RootElement.GetProperty("errorMessage").GetString(); + + if (!success) + { + throw new InvalidOperationException($"Failed to download model: {errorMessage}"); + } + + return modelInfo; + } + catch (Exception) { - throw new InvalidOperationException($"Failed to download model: {errorMessage}"); + Console.WriteLine($"Failed to download model from: {modelInfo.Uri}"); + throw; } - - return modelInfo; } public async Task IsModelUpgradeableAsync(string aliasOrModelId, DeviceType? device = null, CancellationToken ct = default) diff --git a/sdk/cs/test/FoundryLocal.Tests/DownloadExceptionTest.cs b/sdk/cs/test/FoundryLocal.Tests/DownloadExceptionTest.cs new file mode 100644 index 0000000..5ef5807 --- /dev/null +++ b/sdk/cs/test/FoundryLocal.Tests/DownloadExceptionTest.cs @@ -0,0 +1,64 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft. All rights reserved. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.AI.Foundry.Local.Tests; + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AI.Foundry.Local; +using RichardSzalay.MockHttp; +using Xunit; + +public class DownloadExceptionTest +{ + [Fact] + public async Task DownloadModelAsync_RethrowsException_AndPrintsUrl() + { + // Arrange + using var mockHttp = new MockHttpMessageHandler(); + var client = mockHttp.ToHttpClient(); + client.BaseAddress = new Uri("http://localhost:5272"); + + using var manager = new FoundryLocalManager(); + + // Inject the mock client + typeof(FoundryLocalManager) + .GetField("_serviceUri", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(manager, client.BaseAddress); + + typeof(FoundryLocalManager) + .GetField("_serviceClient", BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(manager, client); + + // Mock catalog to return a model + var modelInfo = new ModelInfo + { + ModelId = "test-model:1", + Uri = "https://example.com/model.onnx", + Alias = "test-model", + Runtime = new Runtime { DeviceType = DeviceType.CPU } + }; + var catalog = new List { modelInfo }; + var catalogJson = JsonSerializer.Serialize(catalog, ModelGenerationContext.Default.ListModelInfo); + + mockHttp.When(HttpMethod.Get, "/foundry/list").Respond("application/json", catalogJson); + mockHttp.When(HttpMethod.Get, "/openai/models").Respond("application/json", "[]"); + + // Mock download to throw an exception + mockHttp.When(HttpMethod.Post, "/openai/download").Throw(new HttpRequestException("SSL connection failed")); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => manager.DownloadModelAsync("test-model")); + Assert.Equal("SSL connection failed", ex.Message); + + // Note: Verifying Console.WriteLine is difficult in this setup without redirecting Console.Out, + // but we verified the exception is rethrown. + } +}