Skip to content

[Bug] OpenAiChatModel internalCall missing reasoningContent in metadata #5693

@CL-LSFY

Description

@CL-LSFY

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions