Skip to content

Roundtrip OpenAI Responses reasoning item id for stateless (store=false) encrypted reasoning#7629

Open
rogerbarreto wants to merge 3 commits into
dotnet:mainfrom
rogerbarreto:issues/7628-fix-responses-encrypted-reasoning-resume
Open

Roundtrip OpenAI Responses reasoning item id for stateless (store=false) encrypted reasoning#7629
rogerbarreto wants to merge 3 commits into
dotnet:mainfrom
rogerbarreto:issues/7628-fix-responses-encrypted-reasoning-resume

Conversation

@rogerbarreto

@rogerbarreto rogerbarreto commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

When resuming a stateless (store = false) OpenAI Responses conversation that carries encrypted reasoning (include: ["reasoning.encrypted_content"]), Microsoft.Extensions.AI.OpenAI drops the reasoning item's service assigned id while streaming. On the next request the reconstructed reasoning item has no id, and the Azure AI Foundry project scoped Responses endpoint rejects it with HTTP 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 actually emitted a reasoning item, which is why it looked intermittent.

Key findings:

  • The id is required on the project scoped endpoint (/api/projects/{project}/openai/v1/responses). The resource scoped endpoint (/openai/v1/responses) is lenient about a missing id, which masks the problem when testing directly against it. The summary shape does not matter; the id is the deciding factor.
  • AIContent.RawRepresentation cannot carry the id across turns because it is [JsonIgnore], so it does not survive the chat history being serialized and rehydrated between turns (for example a hosted human in the loop approval flow where the approval returns in a later request or on a different instance).

Tests

Adds EncryptedReasoning_Streaming_RoundTripsReasoningItemId to OpenAIResponseClientTests. It streams a reasoning item (text deltas plus encrypted_content plus id), 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 its id and encrypted_content. The serialization step is what makes the test represent the real hosted scenario, and it is why a RawRepresentation only fix is not enough.

The test fails before the fix (all target frameworks) and passes after it. Full Microsoft.Extensions.AI.OpenAI and Microsoft.Extensions.AI.Abstractions suites pass.

Two approaches (separate commits)

This PR keeps both approaches as distinct commits so reviewers can compare and pick a direction. The branch tip is the second (first class property) approach.

Commit 1: carry the id in AdditionalProperties (no public API change).
Stashes the reasoning item id in the reasoning content's AdditionalProperties, which survives both content coalescing and JSON serialization, then restores it on the way out. Contained to OpenAIResponsesChatClient.cs. The read has to accept both a string (in memory) and a JsonElement (after a serialize and rehydrate round trip).

Commit 2 (branch tip): first class TextReasoningContent.ItemId property.
Introduces an optional, provider assigned identifier on TextReasoningContent, marked [Experimental] (MEAI001), in the same spirit as ProtectedData. This is consistent with other content types that carry their own provider ids (FunctionCallContent.CallId, HostedFileContent.FileId, HostedVectorStoreContent.VectorStoreId). Content coalescing preserves it, and the OpenAI Responses client populates and consumes it. Being a typed, serialized property it survives serialization cleanly and removes the need to special case JsonElement on read.

Considerations addressed by the property approach:

  • Serialization: a normal property serializes and rehydrates cleanly.
  • Coalescing: ChatResponseExtensions preserves the id when merging streamed reasoning deltas, alongside how it already carries ProtectedData from the terminal content.
  • Cross provider applicability: OpenAI Responses uses a reasoning item id. Providers that do not have one leave it null, exactly as ProtectedData is provider specific today.

If reviewers prefer the no API change route, commit 1 can stand alone. If the first class property is preferred, commit 1 is a stepping stone and can be squashed away.

Naming

The new property is named ItemId rather than ReasoningItemId: it matches the provider "output item id" terminology, avoids stuttering with the TextReasoningContent type name, and is consistent with the existing ItemId usage in the image generation path. Happy to rename if maintainers prefer something else.

Microsoft Reviewers: Open in CodeFlow

…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
@rogerbarreto rogerbarreto marked this pull request as ready for review July 14, 2026 20:07
@rogerbarreto rogerbarreto requested review from a team as code owners July 14, 2026 20:07
Copilot AI review requested due to automatic review settings July 14, 2026 20:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ItemId property (diagnostic MEAI001) to carry provider-assigned reasoning item identifiers across turns.
  • Preserves ItemId during streamed content coalescing and restores it when rebuilding outgoing ReasoningResponseItem payloads in OpenAIResponsesChatClient.
  • Adds a regression test that streams reasoning + encrypted_content, persists/rehydrates history, then asserts the subsequent request includes both id and encrypted_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.

Comment on lines +643 to +645
// Simulate persisting and rehydrating the session between turns (as a hosted, human-in-the-loop
// approval flow does). This drops RawRepresentation (which is [JsonIgnore]), so the reasoning item's
// id must survive via a serialized field (AdditionalProperties) to roundtrip.
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
@jozkee jozkee added the area-ai Microsoft.Extensions.AI libraries label Jul 15, 2026
@jozkee jozkee requested review from Copilot July 15, 2026 16:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comment on lines +313 to +317
// Preserve the provider-assigned reasoning item id (required to roundtrip encrypted reasoning in
// stateless scenarios). All contents in a coalesced run belong to the same reasoning item, so use
// the first available id.
for (int i = start; i < end; i++)
{
// encrypted reasoning is rejected by the service without it.
case ReasoningResponseItem rri when rri.EncryptedContent is { Length: > 0 } encryptedContent:
yield return CreateUpdate(new TextReasoningContent(null) { ProtectedData = encryptedContent });
yield return CreateUpdate(CreateReasoningContent(text: null, encryptedContent, rri.Id));
/// </para>
/// </remarks>
[Experimental(DiagnosticIds.Experiments.AIReasoningItemId, UrlFormat = DiagnosticIds.UrlFormat)]
public string? ItemId { get; set; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lean towards an additional property since looks like OpenAI is the only provider with an id in their item.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I checked the other providers and their reasoning blocks only carry a signature/encrypted blob (Anthropic signature, Gemini thought signature, Bedrock reasoningText.signature), which we already model as ProtectedData. The per-item id is specific to OpenAI Responses today. Let's keep it in AdditionalProperties for now and revisit promoting it to the abstraction if/when more providers expose an equivalent id.

@tarekgh

tarekgh commented Jul 15, 2026

Copy link
Copy Markdown
Member

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 TextReasoningContent.ItemId property is missing from the API baseline. src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.json currently only lists Text and ProtectedData under TextReasoningContent, so the API check will fail on the new member.

Running ./scripts/MakeApiBaselines.ps1 should add it:

{ "Member": "string? Microsoft.Extensions.AI.TextReasoningContent.ItemId { get; set; }", "Stage": "Experimental" }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-ai Microsoft.Extensions.AI libraries

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenAI Responses: reasoning item id dropped for encrypted reasoning, breaking (store=false) resume

4 participants