From fb4bdce2fd9d024cee6053108f8d6a40396096b4 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 15 May 2026 03:06:59 +0800 Subject: [PATCH 1/2] .NET: fix: preserve Responses logprobs field --- .../Responses/Models/CreateResponse.cs | 6 ++ .../Responses/Models/Response.cs | 6 ++ .../OpenAIResponsesSerializationTests.cs | 60 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs index 9a8059bd4a2..7b9a82c5882 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/CreateResponse.cs @@ -127,6 +127,12 @@ internal sealed class CreateResponse [JsonPropertyName("max_tool_calls")] public int? MaxToolCalls { get; init; } + /// + /// Whether to return log probabilities of the output tokens. + /// + [JsonPropertyName("logprobs")] + public bool? Logprobs { get; init; } + /// /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position. /// diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs index 3f9c50e9333..ee00c3dafc4 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/Models/Response.cs @@ -228,6 +228,12 @@ internal sealed record Response [JsonPropertyName("max_tool_calls")] public int? MaxToolCalls { get; init; } + /// + /// Whether log probabilities were requested for the output tokens. + /// + [JsonPropertyName("logprobs")] + public bool? Logprobs { get; init; } + /// /// An integer between 0 and 20 specifying the number of most likely tokens to return at each token position. /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs index d3a23a382dc..558f6ca0c5f 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs @@ -192,6 +192,30 @@ public void Serialize_CreateRequestWithOptions_IncludesAllFields() Assert.Equal("value2", metadata.GetProperty("key2").GetString()); } + [Fact] + public void Deserialize_CreateRequestWithLogprobs_RoundTripsFields() + { + const string Json = """ + { + "model": "gpt-4o-mini", + "input": "Return token probabilities.", + "logprobs": true, + "top_logprobs": 3 + } + """; + + CreateResponse? request = JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.CreateResponse); + string reserializedJson = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); + using var doc = JsonDocument.Parse(reserializedJson); + var root = doc.RootElement; + + Assert.NotNull(request); + Assert.True(request.Logprobs); + Assert.Equal(3, request.TopLogprobs); + Assert.True(root.GetProperty("logprobs").GetBoolean()); + Assert.Equal(3, root.GetProperty("top_logprobs").GetInt32()); + } + [Fact] public void Serialize_NullableFields_AreOmittedWhenNull() { @@ -535,6 +559,42 @@ public void Deserialize_MetadataResponse_HasModelParameters() Assert.Equal(150, response.MaxOutputTokens); } + [Fact] + public void Deserialize_ResponseWithLogprobs_RoundTripsFields() + { + const string Json = """ + { + "id": "resp_logprobs", + "object": "response", + "created_at": 1715000000, + "model": "gpt-4o-mini", + "status": "completed", + "output": [], + "usage": { + "input_tokens": 0, + "input_tokens_details": { "cached_tokens": 0 }, + "output_tokens": 0, + "output_tokens_details": { "reasoning_tokens": 0 }, + "total_tokens": 0 + }, + "tools": [], + "logprobs": true, + "top_logprobs": 3 + } + """; + + Response? response = JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.Response); + string reserializedJson = JsonSerializer.Serialize(response, OpenAIHostingJsonContext.Default.Response); + using var doc = JsonDocument.Parse(reserializedJson); + var root = doc.RootElement; + + Assert.NotNull(response); + Assert.True(response.Logprobs); + Assert.Equal(3, response.TopLogprobs); + Assert.True(root.GetProperty("logprobs").GetBoolean()); + Assert.Equal(3, root.GetProperty("top_logprobs").GetInt32()); + } + [Fact] public void Deserialize_ToolCallResponse_HasFunctionCall() { From 8ed968b480c73b9c8a246532d1e48a6a08243136 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:25:04 +0800 Subject: [PATCH 2/2] .NET: propagate Responses logprobs metadata --- .../Responses/AgentResponseExtensions.cs | 1 + .../Responses/AgentResponseUpdateExtensions.cs | 1 + .../Responses/InMemoryResponsesService.cs | 1 + .../OpenAIResponsesSerializationTests.cs | 16 +++++++++------- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseExtensions.cs index 2734fad4272..e1863d802cc 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseExtensions.cs @@ -53,6 +53,7 @@ public static Response ToResponse( Error = null, Id = context.ResponseId, Instructions = request.Instructions, + Logprobs = request.Logprobs, MaxOutputTokens = request.MaxOutputTokens, MaxToolCalls = request.MaxToolCalls, Metadata = request.Metadata is IReadOnlyDictionary metadata ? new Dictionary(metadata) : [], diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs index ac63237d568..7930cba8380 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/AgentResponseUpdateExtensions.cs @@ -250,6 +250,7 @@ Response CreateResponse(ResponseStatus status = ResponseStatus.Completed, IEnume Error = null, Id = context.ResponseId, Instructions = request.Instructions, + Logprobs = request.Logprobs, MaxOutputTokens = request.MaxOutputTokens, MaxToolCalls = request.MaxToolCalls, Metadata = request.Metadata != null ? new Dictionary(request.Metadata) : [], diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs index 6224120ac9c..c0f9c0a015d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs +++ b/dotnet/src/Microsoft.Agents.AI.Hosting.OpenAI/Responses/InMemoryResponsesService.cs @@ -366,6 +366,7 @@ private ResponseState InitializeResponse(string responseId, CreateResponse reque Id = responseId, IncompleteDetails = null, Instructions = request.Instructions, + Logprobs = request.Logprobs, MaxOutputTokens = request.MaxOutputTokens, MaxToolCalls = request.MaxToolCalls, Metadata = metadata, diff --git a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs index 558f6ca0c5f..98e3fb43541 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Hosting.OpenAI.UnitTests/OpenAIResponsesSerializationTests.cs @@ -193,7 +193,7 @@ public void Serialize_CreateRequestWithOptions_IncludesAllFields() } [Fact] - public void Deserialize_CreateRequestWithLogprobs_RoundTripsFields() + public void Deserialize_CreateResponseWithLogprobs_RoundTripsFields() { const string Json = """ { @@ -204,14 +204,15 @@ public void Deserialize_CreateRequestWithLogprobs_RoundTripsFields() } """; - CreateResponse? request = JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.CreateResponse); - string reserializedJson = JsonSerializer.Serialize(request, OpenAIHostingJsonContext.Default.CreateResponse); + CreateResponse? createResponse = JsonSerializer.Deserialize(Json, OpenAIHostingJsonContext.Default.CreateResponse); + string reserializedJson = JsonSerializer.Serialize(createResponse, OpenAIHostingJsonContext.Default.CreateResponse); using var doc = JsonDocument.Parse(reserializedJson); var root = doc.RootElement; - Assert.NotNull(request); - Assert.True(request.Logprobs); - Assert.Equal(3, request.TopLogprobs); + Assert.NotNull(createResponse); + Assert.True(createResponse.Logprobs.HasValue); + Assert.True(createResponse.Logprobs.Value); + Assert.Equal(3, createResponse.TopLogprobs); Assert.True(root.GetProperty("logprobs").GetBoolean()); Assert.Equal(3, root.GetProperty("top_logprobs").GetInt32()); } @@ -589,7 +590,8 @@ public void Deserialize_ResponseWithLogprobs_RoundTripsFields() var root = doc.RootElement; Assert.NotNull(response); - Assert.True(response.Logprobs); + Assert.True(response.Logprobs.HasValue); + Assert.True(response.Logprobs.Value); Assert.Equal(3, response.TopLogprobs); Assert.True(root.GetProperty("logprobs").GetBoolean()); Assert.Equal(3, root.GetProperty("top_logprobs").GetInt32());