Roundtrip OpenAI Responses reasoning item id for stateless (store=false) encrypted reasoning - #7629
Conversation
…e resume The streaming path dropped the reasoning item's service assigned id, so on a stateless (store=false) resume the reconstructed reasoning item had no id and the Azure AI Foundry project scoped Responses endpoint rejected the request with HTTP 400 invalid_payload. RawRepresentation could not carry the id because it is [JsonIgnore] and does not survive session serialization between turns. Carry the reasoning item id in the reasoning content's AdditionalProperties, which survives both content coalescing and JSON serialization, and restore it on the outgoing request (handling both string and JsonElement values). Adds a streaming round-trip test that serializes and rehydrates the history to model a persisted human in the loop approval session. Refs dotnet#7628, microsoft/agent-framework#7067
Replaces the AdditionalProperties based carrier from the previous commit with a first class optional TextReasoningContent.ItemId property, marked [Experimental] (MEAI001). Content coalescing preserves it, and the OpenAI Responses client populates and consumes it, so the reasoning item id survives both coalescing and JSON serialization of the chat history and is sent back on stateless (store=false) resume. The typed, serializable property also removes the need to special case JsonElement values when reading the id back. The AdditionalProperties approach is kept in history (previous commit) for backtrack. Refs dotnet#7628, microsoft/agent-framework#7067
There was a problem hiding this comment.
Pull request overview
This PR fixes a stateless (store=false) OpenAI Responses resume failure when encrypted reasoning is enabled by ensuring the service-assigned reasoning item id is preserved through streaming coalescing and through chat-history JSON serialization/rehydration.
Changes:
- Introduces an experimental
TextReasoningContent.ItemIdproperty (diagnosticMEAI001) to carry provider-assigned reasoning item identifiers across turns. - Preserves
ItemIdduring streamed content coalescing and restores it when rebuilding outgoingReasoningResponseItempayloads inOpenAIResponsesChatClient. - Adds a regression test that streams reasoning +
encrypted_content, persists/rehydrates history, then asserts the subsequent request includes bothidandencrypted_content.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs | Adds a streaming + persisted-history roundtrip test asserting reasoning id and encrypted_content are preserved on the next request. |
| src/Shared/DiagnosticIds/DiagnosticIds.cs | Adds a new experiments constant for the TextReasoningContent.ItemId experimental API. |
| src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs | Captures reasoning item ids into TextReasoningContent while parsing streaming/non-streaming responses and rehydrates them into outgoing ReasoningResponseItem.Id. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/TextReasoningContent.cs | Adds experimental ItemId property for provider-assigned reasoning identifiers. |
| src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs | Preserves TextReasoningContent.ItemId when coalescing streamed reasoning deltas into a single content instance. |
The roundtrip test comment still referenced the AdditionalProperties carrier from the earlier commit; the current implementation roundtrips the reasoning item id via the TextReasoningContent.ItemId property. Addresses PR review feedback. Refs dotnet#7628, microsoft/agent-framework#7067
|
Updated comment: applying #7629 (comment) will make no need to update the API baseline and the issue raised here should be ignored at that time The new Running { "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ItemId { get; set; }", "Stage": "Experimental" } |
…se) encrypted reasoning (#7629) * Fix OpenAI Responses encrypted reasoning id round-trip for store=false resume The streaming path dropped the reasoning item's service assigned id, so on a stateless (store=false) resume the reconstructed reasoning item had no id and the Azure AI Foundry project scoped Responses endpoint rejected the request with HTTP 400 invalid_payload. RawRepresentation could not carry the id because it is [JsonIgnore] and does not survive session serialization between turns. Carry the reasoning item id in the reasoning content's AdditionalProperties, which survives both content coalescing and JSON serialization, and restore it on the outgoing request (handling both string and JsonElement values). Adds a streaming round-trip test that serializes and rehydrates the history to model a persisted human in the loop approval session. Refs #7628, microsoft/agent-framework#7067 * Use first-class TextReasoningContent.ItemId to roundtrip reasoning id Replaces the AdditionalProperties based carrier from the previous commit with a first class optional TextReasoningContent.ItemId property, marked [Experimental] (MEAI001). Content coalescing preserves it, and the OpenAI Responses client populates and consumes it, so the reasoning item id survives both coalescing and JSON serialization of the chat history and is sent back on stateless (store=false) resume. The typed, serializable property also removes the need to special case JsonElement values when reading the id back. The AdditionalProperties approach is kept in history (previous commit) for backtrack. Refs #7628, microsoft/agent-framework#7067 * Fix stale test comment to reference TextReasoningContent.ItemId The roundtrip test comment still referenced the AdditionalProperties carrier from the earlier commit; the current implementation roundtrips the reasoning item id via the TextReasoningContent.ItemId property. Addresses PR review feedback. Refs #7628, microsoft/agent-framework#7067 * Revert "Fix stale test comment to reference TextReasoningContent.ItemId" This reverts commit bc42d22. * Revert "Use first-class TextReasoningContent.ItemId to roundtrip reasoning id" This reverts commit 6fe9943. * Use named arguments for CreateReasoningContent encrypted-content call Pass protectedData and itemId by name at the streaming encrypted-content call site to avoid misreading the positional encrypted-content argument. No behavior change. --------- Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
Problem
When resuming a stateless (
store = false) OpenAI Responses conversation that carries encrypted reasoning (include: ["reasoning.encrypted_content"]),Microsoft.Extensions.AI.OpenAIdrops the reasoning item's service-assignedidwhile streaming. On the next request the reconstructed reasoning item has noid, and the Azure AI Foundry project-scoped Responses endpoint rejects it withHTTP 400 invalid_payload.This is the root cause behind microsoft/agent-framework#7067 (GPT-5.6 stateless approval resume failing intermittently). The failure only occurs on turns where the model emitted a reasoning item, which is why it appeared intermittent.
Key findings:
idis required on the project-scoped endpoint (/api/projects/{project}/openai/v1/responses). The resource-scoped endpoint (/openai/v1/responses) is lenient about a missingid, masking the problem in direct testing.AIContent.RawRepresentationcannot carry theidacross turns because it is[JsonIgnore], so it does not survive chat-history serialization and rehydration between turns, such as a hosted human-in-the-loop approval flow.Implementation
The reasoning item ID is stored in the reasoning content's
AdditionalProperties, which survives content coalescing and JSON serialization, and is restored when creating the outgoingReasoningResponseItem. The read handles both an in-memorystringand aJsonElementproduced by serialization and rehydration.This keeps the provider-specific ID out of the shared abstraction and introduces no public API change. Reviewers confirmed that the equivalent reasoning blocks from Anthropic, Gemini, and Bedrock carry signatures or encrypted blobs already represented by
ProtectedData, while the per-item ID is currently specific to OpenAI Responses.Tests
Adds
EncryptedReasoning_Streaming_RoundTripsReasoningItemIdtoOpenAIResponseClientTests. It streams a reasoning item (text deltas plusencrypted_contentplusid), coalesces it into history, serializes and rehydrates the history to mimic a persisted session, sends it back, and asserts the outgoing reasoning item still carries itsidandencrypted_content.The test fails before the fix and passes after it across all target frameworks.
Microsoft Reviewers: Open in CodeFlow