fix: prevent empty reasoning messages in streaming interfaces (#7207)

* fix: prevent empty reasoning messages in streaming interfaces

Prevents empty "Thinking..." indicators from appearing in clients by
filtering out reasoning messages with no content at the source.

Changes:
- Gemini: Don't emit ReasoningMessage when only thought_signature exists
- Gemini: Only emit reasoning content if text is non-empty
- Anthropic: Don't emit ReasoningMessage for BetaSignatureDelta
- Anthropic: Only emit reasoning content if thinking text is non-empty

This fixes the issue where providers send signature metadata before
actual thinking content, causing empty reasoning blocks to appear
in the UI after responses complete.

Affects: Gemini reasoning, Anthropic extended thinking

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>

* fix: handle Anthropic thinking signature correctly

- Only include 'signature' in Anthropic message payload if it is not None (fixes BadRequestError).
- Capture and attach 'signature' to ReasoningMessage in streaming interface.

* fix(anthropic): attach signature to last reasoning message in stream

---------

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2025-12-17 20:51:52 -05:00
committed by Caren Thomas
parent 0585f013f4
commit 82e5d70807
3 changed files with 61 additions and 71 deletions

View File

@@ -88,6 +88,7 @@ class SimpleAnthropicStreamingInterface:
self.tool_call_name = None
self.accumulated_tool_call_args = ""
self.previous_parse = {}
self.thinking_signature = None
# usage trackers
self.input_tokens = 0
@@ -426,20 +427,23 @@ class SimpleAnthropicStreamingInterface:
f"Streaming integrity failed - received BetaThinkingBlock object while not in THINKING EventMode: {delta}"
)
if prev_message_type and prev_message_type != "reasoning_message":
message_index += 1
reasoning_message = ReasoningMessage(
id=self.letta_message_id,
source="reasoner_model",
reasoning=delta.thinking,
date=datetime.now(timezone.utc).isoformat(),
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
run_id=self.run_id,
step_id=self.step_id,
)
self.reasoning_messages.append(reasoning_message)
prev_message_type = reasoning_message.message_type
yield reasoning_message
# Only emit reasoning message if we have actual content
if delta.thinking and delta.thinking.strip():
if prev_message_type and prev_message_type != "reasoning_message":
message_index += 1
reasoning_message = ReasoningMessage(
id=self.letta_message_id,
source="reasoner_model",
reasoning=delta.thinking,
signature=self.thinking_signature,
date=datetime.now(timezone.utc).isoformat(),
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
run_id=self.run_id,
step_id=self.step_id,
)
self.reasoning_messages.append(reasoning_message)
prev_message_type = reasoning_message.message_type
yield reasoning_message
elif isinstance(delta, BetaSignatureDelta):
# Safety check
@@ -448,21 +452,15 @@ class SimpleAnthropicStreamingInterface:
f"Streaming integrity failed - received BetaSignatureDelta object while not in THINKING EventMode: {delta}"
)
if prev_message_type and prev_message_type != "reasoning_message":
message_index += 1
reasoning_message = ReasoningMessage(
id=self.letta_message_id,
source="reasoner_model",
reasoning="",
date=datetime.now(timezone.utc).isoformat(),
signature=delta.signature,
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
run_id=self.run_id,
step_id=self.step_id,
)
self.reasoning_messages.append(reasoning_message)
prev_message_type = reasoning_message.message_type
yield reasoning_message
# Store signature but don't emit empty reasoning message
# Signature will be attached when actual thinking content arrives
self.thinking_signature = delta.signature
# Update the last reasoning message with the signature so it gets persisted
if self.reasoning_messages:
last_msg = self.reasoning_messages[-1]
if isinstance(last_msg, ReasoningMessage):
last_msg.signature = delta.signature
elif isinstance(event, BetaRawMessageStartEvent):
self.message_id = event.message.id