diff --git a/NuGet.config b/NuGet.config index 0fedd015e82..2aab6faa83e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -17,6 +17,8 @@ + + @@ -39,6 +41,10 @@ + + + + diff --git a/eng/packages/General.props b/eng/packages/General.props index d30eedcfefe..28206e5652b 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -17,7 +17,7 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 38695ae16f0..80ece797929 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -17,7 +17,6 @@ using System.Threading.Tasks; using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; -using OpenAI; using OpenAI.Responses; #pragma warning disable S1226 // Method parameters, caught exceptions and foreach variables' initial values should not be ignored @@ -464,6 +463,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => case FunctionCallResponseItem fcri: anyFunctions = true; + lastMessageId = outputItemAddedUpdate.Item.Id; lastRole = ChatRole.Assistant; break; } @@ -735,7 +735,9 @@ private static bool IsStoredOutputDisabled(CreateResponseOptions? options, Respo case HostedToolSearchTool: // Workaround: The OpenAI .NET SDK doesn't yet expose a ToolSearchTool type. // See https://github.com/openai/openai-dotnet/issues/1053 - return ModelReaderWriter.Read(BinaryData.FromString("""{"type": "tool_search"}"""), ModelReaderWriterOptions.Json, OpenAIContext.Default)!; +#pragma warning disable OPENAI001 // OpenAIResponsesContext is experimental + return ModelReaderWriter.Read(BinaryData.FromString("""{"type": "tool_search"}"""), ModelReaderWriterOptions.Json, OpenAIResponsesContext.Default)!; +#pragma warning restore OPENAI001 case HostedWebSearchTool webSearchTool: return new WebSearchTool @@ -895,7 +897,9 @@ internal static ResponseTool ToNamespaceResponseTool(string name, string? descri writer.WriteStartArray("tools"u8); foreach (var namespacedTool in namespacedTools) { - var toolData = ModelReaderWriter.Write(namespacedTool, ModelReaderWriterOptions.Json, OpenAIContext.Default); +#pragma warning disable OPENAI001 // OpenAIResponsesContext is experimental + var toolData = ModelReaderWriter.Write(namespacedTool, ModelReaderWriterOptions.Json, OpenAIResponsesContext.Default); +#pragma warning restore OPENAI001 using var doc = JsonDocument.Parse(toolData); doc.RootElement.WriteTo(writer); } @@ -904,7 +908,9 @@ internal static ResponseTool ToNamespaceResponseTool(string name, string? descri writer.WriteEndObject(); } - return ModelReaderWriter.Read(BinaryData.FromBytes(stream.ToArray()), ModelReaderWriterOptions.Json, OpenAIContext.Default)!; +#pragma warning disable OPENAI001 // OpenAIResponsesContext is experimental + return ModelReaderWriter.Read(BinaryData.FromBytes(stream.ToArray()), ModelReaderWriterOptions.Json, OpenAIResponsesContext.Default)!; +#pragma warning restore OPENAI001 } /// Creates a from a . diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 90f8343d8f0..73297dfad23 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -5974,6 +5974,7 @@ public async Task StreamingResponseWithFunctionCallOutput_YieldsFunctionResultCo var functionCallUpdate = updates.FirstOrDefault(u => u.Contents.OfType().Any()); Assert.NotNull(functionCallUpdate); Assert.Equal(ChatRole.Assistant, functionCallUpdate.Role); + Assert.Equal("fc_001", functionCallUpdate.MessageId); // Verify MessageID is set for function calls (issue #7479) var fcc = functionCallUpdate.Contents.OfType().Single(); Assert.Equal("call_123", fcc.CallId); Assert.Equal("get_weather", fcc.Name); @@ -5994,14 +5995,23 @@ public async Task StreamingResponseWithFunctionCallOutput_YieldsFunctionResultCo var response = updates.ToChatResponse(); Assert.Equal("resp_001", response.ResponseId); Assert.Equal("gpt-4o-mini", response.ModelId); - Assert.Single(response.Messages); - var message = response.Messages[0]; - Assert.Equal(ChatRole.Assistant, message.Role); - // Message should contain: FunctionCallContent, FunctionResultContent, and TextContent - Assert.Single(message.Contents.OfType()); - Assert.Single(message.Contents.OfType()); - var textContent = Assert.Single(message.Contents.OfType()); + // With the MessageID fix, function calls now have their own MessageId ("fc_001") and + // the text message has a different MessageId ("msg_001"), resulting in 2 separate messages. + Assert.Equal(2, response.Messages.Count); + + // First message contains the function call and result items (MessageId="fc_001") + var functionMessage = response.Messages[0]; + Assert.Equal(ChatRole.Assistant, functionMessage.Role); + Assert.Equal("fc_001", functionMessage.MessageId); + Assert.Single(functionMessage.Contents.OfType()); + Assert.Single(functionMessage.Contents.OfType()); + + // Second message contains the text response (MessageId="msg_001") + var textMessage = response.Messages[1]; + Assert.Equal(ChatRole.Assistant, textMessage.Role); + Assert.Equal("msg_001", textMessage.MessageId); + var textContent = Assert.Single(textMessage.Contents.OfType()); Assert.Equal("It's 25°C and sunny in Paris.", textContent.Text); // Verify usage