spring-projects / spring-projects/spring-ai

[Bug] OpenAiChatModel internalCall missing reasoningContent in metadata

Open
#5,693 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

status: waiting-for-triage
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.