From ef3c35a8b543e426bdbfab586e3598af5a173556 Mon Sep 17 00:00:00 2001 From: viettranx Date: Fri, 27 Feb 2026 20:53:53 +0700 Subject: [PATCH] fix: Drop tool_calls without thought_signature instead of converting to text, fix trace detail word-wrap Gemini 2.5 flash doesn't return thought_signature in OpenAI-compat responses, causing collapseToolCallsWithoutSig to convert ALL tool_calls to "(Used tool ...)" text. Gemini then imitates this pattern instead of making actual tool calls. Now drops tool_call cycles entirely, preserving only the assistant's text content. Also fixes word-wrap in trace detail dialog (break-words instead of break-all) and uses
 with whitespace-pre-wrap for trace-level input/output previews.

Co-Authored-By: Claude Opus 4.6 
---
 internal/providers/openai_gemini.go           | 60 ++++---------------
 .../src/pages/traces/trace-detail-dialog.tsx  |  8 +--
 2 files changed, 16 insertions(+), 52 deletions(-)

diff --git a/internal/providers/openai_gemini.go b/internal/providers/openai_gemini.go
index 20388e7e..30ee3621 100644
--- a/internal/providers/openai_gemini.go
+++ b/internal/providers/openai_gemini.go
@@ -1,20 +1,11 @@
 package providers
 
-import (
-	"encoding/json"
-	"fmt"
-	"strings"
-)
-
-// collapseToolCallsWithoutSig rewrites assistant messages whose tool_calls lack
-// thought_signature (required by Gemini 2.5+) into plain text. This handles old
-// session history stored before the thought_signature capture fix.
-// Tool call cycles (assistant + tool results) are collapsed into:
-//   - assistant: original content + summary
-//   - user: tool result content
+// collapseToolCallsWithoutSig strips tool_call cycles that lack thought_signature
+// (required by Gemini 2.5+). Old session history stored before the thought_signature
+// capture fix doesn't have it, and Gemini rejects those messages with HTTP 400.
 //
-// The collapsed format deliberately avoids looking like a tool call invocation
-// to prevent Gemini from imitating the pattern instead of using structured tool_calls.
+// The assistant's original text content (if any) is preserved; only the tool_calls
+// and their corresponding tool-result messages are dropped.
 func collapseToolCallsWithoutSig(msgs []Message) []Message {
 	// Collect tool_call IDs that need collapsing.
 	collapseIDs := make(map[string]bool)
@@ -24,7 +15,6 @@ func collapseToolCallsWithoutSig(msgs []Message) []Message {
 		}
 		for _, tc := range m.ToolCalls {
 			if tc.Metadata["thought_signature"] == "" {
-				// If any tool_call in the message is missing sig, collapse all of them.
 				for _, tc2 := range m.ToolCalls {
 					collapseIDs[tc2.ID] = true
 				}
@@ -40,49 +30,23 @@ func collapseToolCallsWithoutSig(msgs []Message) []Message {
 	for i := 0; i < len(msgs); i++ {
 		m := msgs[i]
 
-		// Collapse assistant with tool_calls → plain text assistant
+		// Strip tool_calls from assistant message, keep original content only.
 		if m.Role == "assistant" && len(m.ToolCalls) > 0 && collapseIDs[m.ToolCalls[0].ID] {
-			var sb strings.Builder
 			if m.Content != "" {
-				sb.WriteString(m.Content)
-				sb.WriteString("\n\n")
+				result = append(result, Message{
+					Role:    "assistant",
+					Content: m.Content,
+				})
 			}
-			for _, tc := range m.ToolCalls {
-				argsJSON, _ := json.Marshal(tc.Arguments)
-				fmt.Fprintf(&sb, "(Used tool \"%s\" with %s)\n", tc.Name, string(argsJSON))
-			}
-			result = append(result, Message{
-				Role:    "assistant",
-				Content: strings.TrimSpace(sb.String()),
-			})
 
-			// Collect consecutive tool results → one user message
-			var toolBuf strings.Builder
+			// Skip consecutive tool results belonging to these tool_calls.
 			for i+1 < len(msgs) && msgs[i+1].Role == "tool" && collapseIDs[msgs[i+1].ToolCallID] {
 				i++
-				toolMsg := msgs[i]
-				toolName := toolMsg.ToolCallID
-				for _, tc := range m.ToolCalls {
-					if tc.ID == toolMsg.ToolCallID {
-						toolName = tc.Name
-						break
-					}
-				}
-				if toolBuf.Len() > 0 {
-					toolBuf.WriteString("\n\n")
-				}
-				fmt.Fprintf(&toolBuf, "Result from %s: %s", toolName, toolMsg.Content)
-			}
-			if toolBuf.Len() > 0 {
-				result = append(result, Message{
-					Role:    "user",
-					Content: toolBuf.String(),
-				})
 			}
 			continue
 		}
 
-		// Skip orphaned tool results whose assistant was already collapsed
+		// Skip orphaned tool results whose assistant was already collapsed.
 		if m.Role == "tool" && collapseIDs[m.ToolCallID] {
 			continue
 		}
diff --git a/ui/web/src/pages/traces/trace-detail-dialog.tsx b/ui/web/src/pages/traces/trace-detail-dialog.tsx
index 5ab253ff..d36ce0fd 100644
--- a/ui/web/src/pages/traces/trace-detail-dialog.tsx
+++ b/ui/web/src/pages/traces/trace-detail-dialog.tsx
@@ -132,14 +132,14 @@ export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace
             {trace.input_preview && (
               

Input

-

{trace.input_preview}

+
{trace.input_preview}
)} {trace.output_preview && (

Output

-

{trace.output_preview}

+
{trace.output_preview}
)} @@ -255,7 +255,7 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) { {span.input_preview && (

Input:

-
+                
                   {span.input_preview}
                 
@@ -263,7 +263,7 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) { {span.output_preview && (

Output:

-
+                
                   {span.output_preview}