diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index b37c475508a..eb39754d5fd 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -466,10 +466,38 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => break; case StreamingResponseErrorUpdate errorUpdate: - yield return CreateUpdate(new ErrorContent(errorUpdate.Message) + string? errorMessage = errorUpdate.Message; + string? errorCode = errorUpdate.Code; + string? errorParam = errorUpdate.Param; + + // Workaround for https://github.com/openai/openai-dotnet/issues/849. + // The OpenAI service is sending down error information in a different format + // than is documented and thus a different format from what the OpenAI client + // library deserializes. Until that's addressed such that the data is correctly + // propagated through the OpenAI library, if it looks like the update doesn't + // contain the properly deserialized error information, try accessing it + // directly from the underlying JSON. { - ErrorCode = errorUpdate.Code, - Details = errorUpdate.Param, + if (string.IsNullOrEmpty(errorMessage)) + { + _ = errorUpdate.Patch.TryGetValue("$.error.message"u8, out errorMessage); + } + + if (string.IsNullOrEmpty(errorCode)) + { + _ = errorUpdate.Patch.TryGetValue("$.error.code"u8, out errorCode); + } + + if (string.IsNullOrEmpty(errorParam)) + { + _ = errorUpdate.Patch.TryGetValue("$.error.param"u8, out errorParam); + } + } + + yield return CreateUpdate(new ErrorContent(errorMessage) + { + ErrorCode = errorCode, + Details = errorParam, }); break; diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 91b111aa782..94d767f67d4 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -4889,6 +4889,135 @@ [new ChatMessage(ChatRole.User, "test")], }); } + [Fact] + public async Task StreamingErrorUpdate_DocumentedFormat_ParsesCorrectly() + { + const string Input = """ + { + "model":"gpt-4o-mini", + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"test"}]}], + "stream":true + } + """; + + const string Output = """ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} + + event: error + data: {"type":"error","sequence_number":1,"message":"Rate limit exceeded","code":"rate_limit_exceeded","param":"requests"} + + event: response.failed + data: {"type":"response.failed","sequence_number":2,"response":{"id":"resp_001","object":"response","created_at":1741892091,"status":"failed","model":"gpt-4o-mini","output":[],"error":{"code":"rate_limit_exceeded","message":"Rate limit exceeded"}}} + + + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + List updates = []; + await foreach (var update in client.GetStreamingResponseAsync("test")) + { + updates.Add(update); + } + + var errorUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is ErrorContent)); + Assert.NotNull(errorUpdate); + + var errorContent = errorUpdate.Contents.OfType().First(); + Assert.Equal("Rate limit exceeded", errorContent.Message); + Assert.Equal("rate_limit_exceeded", errorContent.ErrorCode); + Assert.Equal("requests", errorContent.Details); + } + + [Fact] + public async Task StreamingErrorUpdate_ActualErroneousFormat_ParsesCorrectly() + { + const string Input = """ + { + "model":"gpt-4o-mini", + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"test"}]}], + "stream":true + } + """; + + const string Output = """ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_002","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} + + event: error + data: {"type":"error","sequence_number":1,"error":{"message":"Content filter triggered","code":"content_filter","param":"safety"}} + + event: response.failed + data: {"type":"response.failed","sequence_number":2,"response":{"id":"resp_002","object":"response","created_at":1741892091,"status":"failed","model":"gpt-4o-mini","output":[],"error":{"code":"content_filter","message":"Content filter triggered"}}} + + + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + List updates = []; + await foreach (var update in client.GetStreamingResponseAsync("test")) + { + updates.Add(update); + } + + var errorUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is ErrorContent)); + Assert.NotNull(errorUpdate); + + var errorContent = errorUpdate.Contents.OfType().First(); + Assert.Equal("Content filter triggered", errorContent.Message); + Assert.Equal("content_filter", errorContent.ErrorCode); + Assert.Equal("safety", errorContent.Details); + } + + [Fact] + public async Task StreamingErrorUpdate_NoErrorInformation_HandlesGracefully() + { + const string Input = """ + { + "model":"gpt-4o-mini", + "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"test"}]}], + "stream":true + } + """; + + const string Output = """ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_003","object":"response","created_at":1741892091,"status":"in_progress","model":"gpt-4o-mini","output":[]}} + + event: error + data: {"type":"error","sequence_number":1} + + event: response.failed + data: {"type":"response.failed","sequence_number":2,"response":{"id":"resp_003","object":"response","created_at":1741892091,"status":"failed","model":"gpt-4o-mini","output":[]}} + + + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + List updates = []; + await foreach (var update in client.GetStreamingResponseAsync("test")) + { + updates.Add(update); + } + + var errorUpdate = updates.FirstOrDefault(u => u.Contents.Any(c => c is ErrorContent)); + Assert.NotNull(errorUpdate); + + var errorContent = errorUpdate.Contents.OfType().First(); + Assert.True(string.IsNullOrEmpty(errorContent.Message)); + Assert.True(string.IsNullOrEmpty(errorContent.ErrorCode)); + Assert.True(string.IsNullOrEmpty(errorContent.Details)); + } + [Fact] public async Task StreamingResponseWithAnnotations_HandlesCorrectly() {