Skip to content

Commit 0ea4e53

Browse files
authored
Merge pull request #180 from giovanimds/fix/google-reasoning-content-pollution
fix: Google reasoning text leaking into plaintext content
2 parents bc8b0dd + 980d24d commit 0ea4e53

2 files changed

Lines changed: 85 additions & 3 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System.Text;
2+
using LlmTornado.Chat;
3+
using LlmTornado.Chat.Vendors.Google;
4+
5+
namespace LlmTornado.Tests;
6+
7+
/// <summary>
8+
/// Regression tests for the Google/Gemini/Gemma vendor part mapper. Does not require API credentials.
9+
/// </summary>
10+
[TestFixture]
11+
public class GoogleReasoningPartTests
12+
{
13+
[Test]
14+
public void ThoughtPart_WithText_IsClassifiedAsReasoning_AndNotAppendedToPlaintext()
15+
{
16+
VendorGoogleChatRequestMessagePart part = new VendorGoogleChatRequestMessagePart
17+
{
18+
Text = "the model is thinking about the answer",
19+
Thought = true
20+
};
21+
22+
StringBuilder sb = new StringBuilder();
23+
ChatMessagePart result = part.ToMessagePart(sb);
24+
25+
Assert.That(result.Type, Is.EqualTo(ChatMessageTypes.Reasoning));
26+
Assert.That(result.Reasoning?.Content, Is.EqualTo("the model is thinking about the answer"));
27+
28+
// The bug: sb (which becomes ChatMessage.Content / the streamed "content" field)
29+
// used to receive the thought text unconditionally, mixing reasoning into the
30+
// user-visible answer even though the part was correctly flagged as a thought.
31+
Assert.That(sb.ToString(), Is.Empty);
32+
}
33+
34+
[Test]
35+
public void NonThoughtPart_WithText_IsClassifiedAsText_AndAppendedToPlaintext()
36+
{
37+
VendorGoogleChatRequestMessagePart part = new VendorGoogleChatRequestMessagePart
38+
{
39+
Text = "42",
40+
Thought = false
41+
};
42+
43+
StringBuilder sb = new StringBuilder();
44+
ChatMessagePart result = part.ToMessagePart(sb);
45+
46+
Assert.That(result.Type, Is.EqualTo(ChatMessageTypes.Text));
47+
Assert.That(result.Text, Is.EqualTo("42"));
48+
Assert.That(sb.ToString(), Is.EqualTo("42"));
49+
}
50+
51+
[Test]
52+
public void ThoughtPart_WithoutText_StillReportsReasoningSignature()
53+
{
54+
// Redacted-thought blocks: content can be empty while a signature is still present.
55+
VendorGoogleChatRequestMessagePart part = new VendorGoogleChatRequestMessagePart
56+
{
57+
Text = null,
58+
Thought = true,
59+
ThoughtSignature = "opaque-signature"
60+
};
61+
62+
StringBuilder sb = new StringBuilder();
63+
ChatMessagePart result = part.ToMessagePart(sb);
64+
65+
Assert.That(result.Type, Is.EqualTo(ChatMessageTypes.Reasoning));
66+
Assert.That(result.Reasoning?.Signature, Is.EqualTo("opaque-signature"));
67+
Assert.That(sb.ToString(), Is.Empty);
68+
}
69+
}

src/LlmTornado/Chat/Vendors/Google/VendorGoogleChatRequest.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,22 @@ public ChatMessagePart ToMessagePart(StringBuilder sb)
543543
}
544544
else if (Text is not null)
545545
{
546-
part.Type = ChatMessageTypes.Text;
547-
part.Text = Text;
548-
sb.Append(Text);
546+
if (Thought ?? false)
547+
{
548+
part.Type = ChatMessageTypes.Reasoning;
549+
part.Reasoning = new ChatMessageReasoningData
550+
{
551+
Provider = LLmProviders.Google,
552+
Content = Text,
553+
Signature = ThoughtSignature
554+
};
555+
}
556+
else
557+
{
558+
part.Type = ChatMessageTypes.Text;
559+
part.Text = Text;
560+
sb.Append(Text);
561+
}
549562
}
550563
else if (InlineData is not null)
551564
{

0 commit comments

Comments
 (0)