Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -440,12 +440,14 @@ else if (contentBlock.isWebSearchToolResult()) {
return null;
}

// Thinking chunk — emit with thinking metadata
// Thinking chunk — emit with thinking metadata. The text is exposed in
// metadata, not content, so it is not aggregated into the message text.
if (delta.isThinking()) {
String thinkingText = delta.asThinking().thinking();
streamingState.appendThinking(thinkingText);
Map<String, Object> thinkingProperties = new HashMap<>();
thinkingProperties.put("thinking", Boolean.TRUE);
thinkingProperties.put("thinkingText", thinkingText);
AssistantMessage assistantMessage = AssistantMessage.builder().properties(thinkingProperties).build();
return new ChatResponse(List.of(new Generation(assistantMessage)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,37 @@ void streamingThinkingBlockIsReplayedBeforeToolUseBlock() {
assertThat(replayedAssistantBlocks.get(1).isToolUse()).isTrue();
}

@Test
@SuppressWarnings("unchecked")
void streamingThinkingDeltasExposeThinkingTextInMetadata() {
List<RawMessageStreamEvent> events = List.of(messageStartEvent(), thinkingStartEvent(),
thinkingDeltaEvent("thinking "), thinkingDeltaEvent("text"), signatureDeltaEvent("thinking-signature"),
contentBlockStopEvent(0), messageDeltaEvent(StopReason.END_TURN));
StreamResponse<RawMessageStreamEvent> streamResponse = mock(StreamResponse.class);
given(streamResponse.stream()).willReturn(events.stream());

HttpResponseFor<StreamResponse<RawMessageStreamEvent>> rawResponse = mock(HttpResponseFor.class);
given(rawResponse.parse()).willReturn(streamResponse);
given(rawResponse.headers()).willReturn(Headers.builder().build());

given(this.anthropicClientAsync.messages()).willReturn(this.messageServiceAsync);
given(this.messageServiceAsync.withRawResponse()).willReturn(this.messageServiceAsyncWithRawResponse);
given(this.messageServiceAsyncWithRawResponse.createStreaming(any(MessageCreateParams.class),
any(RequestOptions.class)))
.willReturn(CompletableFuture.completedFuture(rawResponse));

List<ChatResponse> responses = this.chatModel.stream(new Prompt("Think about it.")).collectList().block();

assertThat(responses).isNotNull();
List<AssistantMessage> thinkingDeltas = responses.stream()
.map(response -> response.getResult().getOutput())
.filter(message -> message.getMetadata().containsKey("thinking"))
.toList();
assertThat(thinkingDeltas).extracting(message -> message.getMetadata().get("thinkingText"))
.containsExactly("thinking ", "text");
assertThat(thinkingDeltas).allSatisfy(message -> assertThat(message.getText()).isNull());
}

@Test
void cacheOptionsIsMergedFromRuntimePrompt() {
AnthropicChatModel model = AnthropicChatModel.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,10 +485,9 @@ stream.subscribe(response -> {
AssistantMessage message = generation.getOutput();

if (message.getMetadata().containsKey("thinking")) {
// Thinking delta — getText() returns null; the full accumulated thinking text
// is available via AnthropicAssistantMessage.getThinkingContents() on the
// aggregated response after streaming completes.
System.out.println("[thinking...]");
// Thinking delta — getText() returns null so thinking does not mix into the
// answer; the incremental thinking text is in the "thinkingText" metadata key.
System.out.print(message.getMetadata().get("thinkingText"));
}
else if (message.getMetadata().containsKey("signature")) {
// Thinking block signature (emitted at end of thinking)
Expand All @@ -512,7 +511,7 @@ When thinking is enabled, the response contains different types of content:
| **Thinking Block** | `signature` | Claude's reasoning text with a cryptographic signature. In sync mode, the thinking text is in `getText()` and the signature is in `getMetadata().get("signature")`.
| **Redacted Thinking** | `data` | Safety-redacted reasoning. Contains only a `data` marker, no visible text.
| **Signature (streaming)** | `signature` | In streaming mode, the signature arrives as a separate delta at the end of a thinking block.
| **Thinking Delta (streaming)** | `thinking` | Incremental thinking chunks during streaming. The `thinking` metadata key is set to `true`. `getText()` returns null on these chunks; the full thinking text is available via `AnthropicAssistantMessage.getThinkingContents()` on the aggregated response.
| **Thinking Delta (streaming)** | `thinking`, `thinkingText` | Incremental thinking chunks during streaming. The `thinking` metadata key is set to `true` and `thinkingText` holds the incremental thinking text. `getText()` returns null on these chunks, so thinking is not aggregated into the answer text; the full thinking text is also available via `AnthropicAssistantMessage.getThinkingContents()` on the aggregated response.
| **Text Block** | _(none)_ | The final answer text in `getText()`.
|====

Expand Down