diff --git a/.gitignore b/.gitignore index 1b1f680..406a20e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ Cargo.lock bin/ obj/ /src/cs/samples/ConsoleClient/test.http +logs/ \ No newline at end of file diff --git a/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj index 113bebd..105dd0e 100644 --- a/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj +++ b/sdk_v2/cs/src/Microsoft.AI.Foundry.Local.csproj @@ -98,9 +98,8 @@ $(FoundryLocalCoreVersion) - 0.9.0.2-dev-20260226T191541-2b332047 - 0.9.0.4-dev-20260226T191638-2b332047 - + 0.9.0-dev-20260227T230631-2a3af92 + 0.9.0-dev-20260227T222239-2a3af92 True @@ -109,6 +108,11 @@ True + + + $(NoWarn);NU1604 + diff --git a/sdk_v2/cs/src/OpenAI/AudioClient.cs b/sdk_v2/cs/src/OpenAI/AudioClient.cs index 245fbdf..5475185 100644 --- a/sdk_v2/cs/src/OpenAI/AudioClient.cs +++ b/sdk_v2/cs/src/OpenAI/AudioClient.cs @@ -138,7 +138,7 @@ private async IAsyncEnumerable TranscribeAudio { var failed = false; - await _coreInterop.ExecuteCommandWithCallbackAsync( + var res = await _coreInterop.ExecuteCommandWithCallbackAsync( "audio_transcribe", request, async (callbackData) => @@ -163,6 +163,16 @@ await _coreInterop.ExecuteCommandWithCallbackAsync( ct ).ConfigureAwait(false); + // If the native layer returned an error (e.g. missing audio file, invalid model) + // without invoking any callbacks, propagate it so the caller sees an exception + // instead of an empty stream. + if (res.Error != null) + { + channel.Writer.TryComplete( + new FoundryLocalException($"Error from audio_transcribe command: {res.Error}", _logger)); + return; + } + // use TryComplete as an exception in the callback may have already closed the channel _ = channel.Writer.TryComplete(); } diff --git a/sdk_v2/cs/src/OpenAI/ChatClient.cs b/sdk_v2/cs/src/OpenAI/ChatClient.cs index e237ee3..b9f889f 100644 --- a/sdk_v2/cs/src/OpenAI/ChatClient.cs +++ b/sdk_v2/cs/src/OpenAI/ChatClient.cs @@ -171,7 +171,7 @@ private async IAsyncEnumerable ChatStreamingImplAs { var failed = false; - await _coreInterop.ExecuteCommandWithCallbackAsync( + var response = await _coreInterop.ExecuteCommandWithCallbackAsync( "chat_completions", request, async (callbackData) => @@ -196,6 +196,17 @@ await _coreInterop.ExecuteCommandWithCallbackAsync( ct ).ConfigureAwait(false); + // If the native layer returned an error (e.g. missing model, invalid input) + // without invoking any callbacks, propagate it so the caller sees an exception + // instead of an empty stream. + if (!failed && response.Error != null) + { + channel.Writer.TryComplete( + new FoundryLocalException($"Error from chat_completions command: {response.Error}", _logger)); + failed = true; + return; + } + // use TryComplete as an exception in the callback may have already closed the channel _ = channel.Writer.TryComplete(); } diff --git a/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs index c887563..ec4ab4c 100644 --- a/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs +++ b/sdk_v2/cs/test/FoundryLocal.Tests/AudioClientTests.cs @@ -67,6 +67,33 @@ public async Task AudioTranscription_NoStreaming_Succeeds_WithTemperature() Console.WriteLine($"Response: {content}"); } + [Test] + public async Task AudioTranscription_NoStreaming_InValidFile() + { + var audioClient = await model!.GetAudioClientAsync(); + await Assert.That(audioClient).IsNotNull(); + + audioClient.Settings.Language = "en"; + + var audioFilePath = Path.Combine(AppContext.BaseDirectory, "testdata/non_exist_Recording.mp3"); + + FoundryLocalException? caught = null; + try + { + await audioClient.TranscribeAudioAsync(audioFilePath).ConfigureAwait(false); + } + catch (FoundryLocalException ex) + { + caught = ex; + } + + // Assert: a FoundryLocalException must have been thrown + await Assert.That(caught).IsNotNull(); + Console.WriteLine($"Caught exception: {caught}"); + await Assert.That(caught!.Message).Contains("Audio file not found"); + + } + [Test] public async Task AudioTranscription_Streaming_Succeeds() { @@ -123,4 +150,33 @@ public async Task AudioTranscription_Streaming_Succeeds_WithTemperature() } + + [Test] + public async Task AudioTranscription_Streaming_InvalidFiles() + { + var audioClient = await model!.GetAudioClientAsync(); + await Assert.That(audioClient).IsNotNull(); + + audioClient.Settings.Language = "en"; + + var audioFilePath = Path.Combine(AppContext.BaseDirectory, "testdata/Record.mp3"); + + FoundryLocalException? caught = null; + try + { + await foreach (var _ in audioClient.TranscribeAudioStreamingAsync(audioFilePath, CancellationToken.None).ConfigureAwait(false)) + { + } + } + catch (FoundryLocalException ex) + { + caught = ex; + } + + // Assert: a FoundryLocalException must have been thrown + await Assert.That(caught).IsNotNull(); + Console.WriteLine($"Caught exception: {caught}"); + await Assert.That(caught!.Message).Contains("Audio file not found"); + + } } diff --git a/sdk_v2/js/logs/foundry.core20260226.log b/sdk_v2/js/logs/foundry.core20260226.log deleted file mode 100644 index 4f44c4f..0000000 --- a/sdk_v2/js/logs/foundry.core20260226.log +++ /dev/null @@ -1,16 +0,0 @@ -2026-02-26 15:53:10.767 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 15:53:12.159 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:00:39.395 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:00:40.683 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:03:53.744 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:03:55.150 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:05:42.755 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:05:44.077 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:08:42.903 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:08:44.319 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:11:14.418 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:11:15.709 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:12:34.632 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:12:39.679 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:15:46.477 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"} -2026-02-26 16:15:47.873 -08:00 [WRN] {"Params":{"OpenAICreateRequest":"{\"Model\":\"openai-whisper-tiny-generic-cpu:2\",\"FileName\":\"C:\\\\foundry-local\\\\Foundry-Local\\\\sdk_v2\\\\testdata\\\\Recording.mp3\",\"Language\":\"en\",\"Temperature\":0,\"metadata\":{\"language\":\"en\",\"temperature\":\"0\"}}"}