Skip to content

OpenAIResponsesChatClient.ToChatMessages never surfaces reasoning.content — only reads GetSummaryText() #7714

Description

@bitorel

Hi team,

Ran into this building a Microsoft.Extensions.AI agent against
OpenRouter (tested Qwen and Nvidia Nemotron models via the Responses API).
The models' actual reasoning text never showed up anywhere —
not in TextReasoningContent.Text, not in OTel traces.

Opened a ticket with the openai-dotnet (1329) team first, since I assumed
it was a client-model gap there. Their answer (pasted below) clarified
it's actually expected on their side, and pointed at the real mechanism:

▎ Although reasoning.content exists in the official Responses API schema,
▎ it is never returned by the OpenAI service... The field was added to the
▎ schema specifically for raw chain of thought from the open-weight gpt-oss
▎ models... That said, this is a good fit for the JsonPatch support that
▎ we added starting in v2.6.0... For your scenario, you can read the
▎ reasoning content directly:

▎ string reasoningText = reasoningItem.Patch.GetString("$.content[0].text"u8);

▎ The field is also preserved by the patch when the item is used in
▎ a subsequent request.

That's a real, working fix — but only if you're calling
OpenAIResponseClient directly.
Going through Microsoft.Extensions.AI's AIAgent/IChatClient abstraction, you never get a ReasoningResponseItem to call .Patch on — OpenAIResponsesChatClient.ToChatMessages (line ~207) already converts it before your code ever sees it:

case ReasoningResponseItem reasoningItem:
message.Contents.Add(CreateReasoningContent(reasoningItem.GetSummaryText(), reasoningItem.EncryptedContent, reasoningItem.Id, outputItem));
break;

GetSummaryText() only reads SummaryParts — it never sees content,
so it comes back empty and TextReasoningContent.Text stays empty
too. Same story for OTel: OtelMessageSerializer reads
TextReasoningContent.Text directly, so it never sees this either.

Worth noting: this exact same file already gets it right for streaming.
StreamingResponseReasoningTextDeltaUpdate handles a real, separate
response.reasoning_text.delta event and reads
reasoningTextDeltaUpdate.Delta straight into CreateReasoningContent,
no gap at all. So this isn't a question of whether the concept fits —
it's already implemented and working one code path over, in the same
class. This is purely the non-streaming branch not having the same
parity yet.

I also noticed #7295 already landed a similar pattern — JsonPatch
read of a non-standard field, surfaced as TextReasoningContent — for
OpenAIChatClient (Chat Completions), reading reasoning_content.
This issue is the equivalent gap on the Responses API side, reading
content instead.

Also related: #7525, which covers the Chat Completions side not
writing reasoning_content back out on replay. This one's
narrower — no write-back needed here at all, since the Responses API's
content field already round-trips for free through the existing
generic Patch mechanism (confirmed by the OpenAI SDK team's reply
above). Just the read side needs the fallback.

Suggested fix, matching #7295's approach and the existing
streaming branch, at OpenAIResponsesChatClient.cs:

case ReasoningResponseItem reasoningItem:
string reasoningText = reasoningItem.GetSummaryText();
if (string.IsNullOrEmpty(reasoningText))
{
reasoningText = reasoningItem.Patch.GetString("$.content[0].text"u8);
}
message.Contents.Add(CreateReasoningContent(reasoningText, reasoningItem.EncryptedContent, reasoningItem.Id, outputItem));
break;

Would be happy for any direction on this,

Thank you!

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions