mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 22:15:00 +00:00
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 <pre> with whitespace-pre-wrap for trace-level input/output previews. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e5cf4a9c9
commit
ef3c35a8b5
2 files changed
+16
-52
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
@@ -132,14 +132,14 @@ export function TraceDetailDialog({ traceId, onClose, getTrace, onNavigateTrace
|
||||
{trace.input_preview && (
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Input</p>
|
||||
<p className="text-sm">{trace.input_preview}</p>
|
||||
<pre className="max-h-[40vh] overflow-y-auto whitespace-pre-wrap break-words text-sm">{trace.input_preview}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{trace.output_preview && (
|
||||
<div className="rounded-md border p-3">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground">Output</p>
|
||||
<p className="text-sm">{trace.output_preview}</p>
|
||||
<pre className="max-h-[40vh] overflow-y-auto whitespace-pre-wrap break-words text-sm">{trace.output_preview}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -255,7 +255,7 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) {
|
||||
{span.input_preview && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Input:</p>
|
||||
<pre className="mt-1 max-h-[40vh] overflow-y-auto break-all whitespace-pre-wrap rounded bg-muted/50 p-2 text-xs">
|
||||
<pre className="mt-1 max-h-[40vh] overflow-y-auto break-words whitespace-pre-wrap rounded bg-muted/50 p-2 text-xs">
|
||||
{span.input_preview}
|
||||
</pre>
|
||||
</div>
|
||||
@@ -263,7 +263,7 @@ function SpanTreeNode({ node, depth }: { node: SpanNode; depth: number }) {
|
||||
{span.output_preview && (
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground">Output:</p>
|
||||
<pre className="mt-1 max-h-[40vh] overflow-y-auto break-all whitespace-pre-wrap rounded bg-muted/50 p-2 text-xs">
|
||||
<pre className="mt-1 max-h-[40vh] overflow-y-auto break-words whitespace-pre-wrap rounded bg-muted/50 p-2 text-xs">
|
||||
{span.output_preview}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user