fix(dotnet/connectors/openai): deduplicate top-level JSON keys when using ExtraBody - #14264
fix(dotnet/connectors/openai): deduplicate top-level JSON keys when using ExtraBody#14264nithin42 wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses OpenAI/Azure OpenAI request failures caused by duplicate top-level JSON keys when OpenAIPromptExecutionSettings.ExtraBody is applied via System.ClientModel’s JsonPatch, by adding a pipeline-level sanitization step that rewrites outgoing JSON to ensure unique top-level property names (last-write-wins).
Changes:
- Added a per-call
DeduplicateJsonKeysPipelinePolicyinClientCoreto rewrite JSON request bodies with deduplicated top-level keys. - Added unit tests to assert that
toolsand other top-level keys (e.g.,temperature) are not duplicated in the final request payload.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs | Registers and implements an HTTP pipeline policy that deduplicates top-level JSON object keys before sending requests. |
| dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionExtraBodyTests.cs | Adds tests validating that ExtraBody patching does not produce duplicate top-level keys in the request body. |
Suppressed comments (2)
dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionExtraBodyTests.cs:34
ITestOutputHelperis injected and stored but never used. This introduces unnecessary dependencies and can trigger compiler warnings (assigned but never used). Either use it (e.g., to log the request JSON on failure) or remove it and restore the parameterless constructor.
private readonly ITestOutputHelper _output;
public OpenAIChatCompletionExtraBodyTests(ITestOutputHelper output)
{
this._output = output;
dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionExtraBodyTests.cs:16
- After removing the unused
ITestOutputHelperinjection,using Xunit.Abstractions;becomes unused as well and should be removed to avoid warnings.
using Xunit;
using Xunit.Abstractions;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| using var memoryStream = new System.IO.MemoryStream(); | ||
| message.Request.Content.WriteTo(memoryStream, default); | ||
| byte[] bytes = memoryStream.ToArray(); | ||
| if (bytes.Length == 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| string rawJson = System.Text.Encoding.UTF8.GetString(bytes); | ||
| if (!rawJson.StartsWith('{')) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| string cleanJson = DeduplicateTopLevelJsonKeys(rawJson); | ||
| if (!string.Equals(rawJson, cleanJson, StringComparison.Ordinal)) | ||
| { | ||
| message.Request.Content = System.ClientModel.BinaryContent.Create(BinaryData.FromString(cleanJson)); | ||
| } |
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Net; | ||
| using System.Net.Http; |
| var dictionary = new Dictionary<string, System.Text.Json.JsonElement>(StringComparer.Ordinal); | ||
| foreach (var prop in root.EnumerateObject()) | ||
| { | ||
| dictionary[prop.Name] = prop.Value.Clone(); | ||
| } | ||
|
|
||
| using var stream = new System.IO.MemoryStream(); |
@microsoft-github-policy-service agree |
…y and add exception safety
Fixes #14156
Motivation and Context
When developers use
OpenAIPromptExecutionSettings.ExtraBodyto pass custom or preview parameters to OpenAI/Azure OpenAI models (such as web search tools viatools,temperature,reasoning_effort, or custom vendor fields),System.ClientModel'sJsonPatchappends patched properties onto the outgoing JSON request body.Because
ChatCompletionOptions's default JSON model serializer already writes built-in fields (such astools: []),JsonPatchappends a secondtoolsproperty at the end of the JSON object. This creates duplicate top-level keys in the serialized HTTP request payload (e.g.,{"messages":[...],"tools":[],"tools":[{"type":"web_search"}]}), causing OpenAI/Azure OpenAI API gateways to reject the request with400 Bad Request.Solution: Generic Pipeline Policy (Non-Hardcoded)
Instead of hardcoding
if (key == "tools")workarounds inOpenAIPromptExecutionSettings.cs, this PR introduces a generic, non-intrusive solution at the HTTP pipeline level:DeduplicateJsonKeysPipelinePolicy: Added a newPipelinePolicyinsideClientCore.csregistered atPipelinePosition.PerCall.tools,$.tools,$.tools[0],temperature, or vendor extensions), it deduplicates top-level object properties (last-write-wins) using standardJsonDocument/Utf8JsonWriterparsing before transmitting the request.Verification & Testing
ExtraBodyToolsDoesNotEmitDuplicateToolsKeyInRequestBodyAsyncandExtraBodyDoesNotEmitDuplicateTopLevelKeysInRequestBodyAsyncinOpenAIChatCompletionExtraBodyTests.cs.Connectors.OpenAI.UnitTestspass cleanly (dotnet test)."tools"key and valid JSON.