From 2ccb447ad7f2948bccc0c80724148567edc7a108 Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 08:51:40 +0530 Subject: [PATCH 1/2] Skip empty Gemini thought parts instead of emitting empty reasoning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildResponsePart appended a TextReasoningContent for every part with Thought=true, unconditionally — unlike the text branch, which guards on part.Text != "". A thought part carrying neither thinking text nor a ThoughtSignature therefore produced an empty, information-free TextReasoningContent{Text:"", ProtectedData:""} in the response. Guard the thought branch on part.Text != "" || len(part.ThoughtSignature) > 0, mirroring the text guard. A signature-only thought is still emitted so its signature round-trips via ProtectedData in multi-turn requests; only the fully-empty thought part is skipped. --- provider/geminiprovider/agent.go | 25 +++++++++------- provider/geminiprovider/agent_test.go | 42 ++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/provider/geminiprovider/agent.go b/provider/geminiprovider/agent.go index 16e9c5b4..9144b067 100644 --- a/provider/geminiprovider/agent.go +++ b/provider/geminiprovider/agent.go @@ -382,17 +382,22 @@ func buildResponsePart(part *genai.Part, contents []message.Content) ([]message. if part.Thought { // Thinking model: emit TextReasoningContent. Encode ThoughtSignature as // base64 in ProtectedData so it can be passed back in multi-turn requests. - protectedData := "" - if len(part.ThoughtSignature) > 0 { - protectedData = base64.StdEncoding.EncodeToString(part.ThoughtSignature) + // Skip a thought part that carries neither thinking text nor a signature, + // consistent with how empty text parts are skipped below; such a part + // would otherwise add an empty, information-free reasoning content. + if part.Text != "" || len(part.ThoughtSignature) > 0 { + protectedData := "" + if len(part.ThoughtSignature) > 0 { + protectedData = base64.StdEncoding.EncodeToString(part.ThoughtSignature) + } + contents = append(contents, &message.TextReasoningContent{ + Text: part.Text, + ProtectedData: protectedData, + ContentHeader: message.ContentHeader{ + RawRepresentation: part, + }, + }) } - contents = append(contents, &message.TextReasoningContent{ - Text: part.Text, - ProtectedData: protectedData, - ContentHeader: message.ContentHeader{ - RawRepresentation: part, - }, - }) } else if part.Text != "" { contents = append(contents, &message.TextContent{ Text: part.Text, diff --git a/provider/geminiprovider/agent_test.go b/provider/geminiprovider/agent_test.go index 66377c5c..775c05d0 100644 --- a/provider/geminiprovider/agent_test.go +++ b/provider/geminiprovider/agent_test.go @@ -763,6 +763,45 @@ func TestFunctionResultMissingCallID(t *testing.T) { } } +// TestResponseWithEmptyThoughtPartOmitted verifies that a thought part carrying +// neither text nor a signature does not produce an empty TextReasoningContent. +func TestResponseWithEmptyThoughtPartOmitted(t *testing.T) { + resp := map[string]any{ + "candidates": []any{ + map[string]any{ + "content": map[string]any{ + "role": "model", + "parts": []any{ + map[string]any{"thought": true}, // empty thought: no text, no signature + map[string]any{"text": "The answer is 42."}, + }, + }, + "finishReason": "STOP", + }, + }, + "usageMetadata": map[string]any{"promptTokenCount": 10, "candidatesTokenCount": 15, "totalTokenCount": 25}, + } + respBody, _ := json.Marshal(resp) + + server := httptest.NewServer(captureAndRespond(t, make(chan []byte, 1), "application/json", string(respBody))) + defer server.Close() + + a := newTestClient(t, server) + + result, err := a.RunText(t.Context(), "hi").Collect() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + for _, msg := range result.Messages { + for _, c := range msg.Contents { + if rc, ok := c.(*message.TextReasoningContent); ok { + t.Errorf("expected no reasoning content for an empty thought part, got Text=%q ProtectedData=%q", rc.Text, rc.ProtectedData) + } + } + } +} + // TestResponseWithThinkingContent verifies that response parts with thought=true // are translated into TextReasoningContent. func TestResponseWithThinkingContent(t *testing.T) { @@ -1592,7 +1631,8 @@ func TestGenerateContentConfigOption(t *testing.T) { temp := float32(0.5) topP := float32(0.9) topK := float32(20) - _, err := a.RunText(t.Context(), "Test", + _, err := a.RunText( + t.Context(), "Test", geminiprovider.GenerateContentConfig(genai.GenerateContentConfig{ Temperature: &temp, TopP: &topP, From 459f9656cc6768e3a3ae9debf37a46f350bb6cfb Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sat, 18 Jul 2026 09:50:31 +0530 Subject: [PATCH 2/2] Assert the non-thought text part survives in empty-thought test Address review feedback: the test asserted only the absence of reasoning content, so it would also pass if the provider dropped all response parts. Also assert the normal text part is still emitted. --- provider/geminiprovider/agent_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/provider/geminiprovider/agent_test.go b/provider/geminiprovider/agent_test.go index 775c05d0..e2ce874c 100644 --- a/provider/geminiprovider/agent_test.go +++ b/provider/geminiprovider/agent_test.go @@ -793,13 +793,23 @@ func TestResponseWithEmptyThoughtPartOmitted(t *testing.T) { t.Fatalf("unexpected error: %v", err) } + var foundText string for _, msg := range result.Messages { for _, c := range msg.Contents { - if rc, ok := c.(*message.TextReasoningContent); ok { - t.Errorf("expected no reasoning content for an empty thought part, got Text=%q ProtectedData=%q", rc.Text, rc.ProtectedData) + switch content := c.(type) { + case *message.TextReasoningContent: + t.Errorf("expected no reasoning content for an empty thought part, got Text=%q ProtectedData=%q", content.Text, content.ProtectedData) + case *message.TextContent: + foundText += content.Text } } } + // The normal (non-thought) text part must still be emitted; asserting it + // guards against a regression that drops all parts rather than just the + // empty thought. + if foundText != "The answer is 42." { + t.Errorf("text content = %q, want %q", foundText, "The answer is 42.") + } } // TestResponseWithThinkingContent verifies that response parts with thought=true