spring-projects / spring-projects/spring-ai
[Bug] OpenAiChatModel internalCall missing reasoningContent in metadata
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Description
When using the non-streaming API (internalCall), the reasoningContent field is not included in the AssistantMessage metadata, but the streaming API (internalStream) correctly includes it.
Affected Versions
- 1.1.2
- 1.1.4
- 2.0.0-SNAPSHOT (main branch)
Root Cause
In OpenAiChatModel.java, the internalCall method builds metadata without reasoningContent:
// internalCall (non-streaming) - missing reasoningContent
Map<String, Object> metadata = Map.of(
"id", chatCompletion.id() != null ? chatCompletion.id() : "",
"role", choice.message().role() != null ? choice.message().role().name() : "",
"index", choice.index() != null ? choice.index() : 0,
"finishReason", getFinishReasonJson(choice.finishReason()),
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "",
"annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(Map.of()));
But internalStream (streaming) correctly includes it:
// internalStream (streaming) - has reasoningContent
Map<String, Object> metadata = Map.of(
"id", id,
"role", roleMap.getOrDefault(id, ""),
"index", choice.index() != null ? choice.index() : 0,
"finishReason", getFinishReasonJson(choice.finishReason()),
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "",
"annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(),
"reasoningContent", choice.message().reasoningContent() != null ? choice.message().reasoningContent() : "");
Expected Behavior
The internalCall method should also include reasoningContent in the metadata, consistent with the streaming API.
Proposed Fix
Add reasoningContent to the metadata map in internalCall:
Map<String, Object> metadata = Map.of(
"id", chatCompletion.id() != null ? chatCompletion.id() : "",
"role", choice.message().role() != null ? choice.message().role().name() : "",
"index", choice.index() != null ? choice.index() : 0,
"finishReason", getFinishReasonJson(choice.finishReason()),
"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "",
"annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(),
"reasoningContent", choice.message().reasoningContent() != null ? choice.message().reasoningContent() : "");
Workaround
Currently, users can use the streaming API (stream()) with .last().block() to get the reasoningContent, but this is not ideal for non-streaming use cases.
Related
This affects models like Qwen3.5-plus and DeepSeek-R1 that return reasoning content in API responses.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in OpenAiChatModel.java by comparing the internalCall metadata construction with the corresponding internalStream metadata. Confirm the non-streaming AssistantMessage metadata includes reasoningContent for responses from models such as Qwen3.5-plus and DeepSeek-R1, matching the streaming behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100