diff --git a/internal/translator/antigravity/claude/antigravity_claude_request_test.go b/internal/translator/antigravity/claude/antigravity_claude_request_test.go index 6f3ea636c..da3c75a89 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request_test.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request_test.go @@ -2253,19 +2253,25 @@ func TestConvertClaudeRequestToAntigravity_AlignsPermutedParallelToolResultsWith output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false) parts := gjson.GetBytes(output, "request.contents.1.parts").Array() if len(parts) != 8 { - t.Fatalf("parts = %d, want six responses followed by two text parts; output=%s", len(parts), output) + t.Fatalf("parts = %d, want eight parts; output=%s", len(parts), output) } - for index := 0; index < 6; index++ { + if got := parts[0].Get("text").String(); got != "Tool results follow." { + t.Fatalf("leading text = %q; output=%s", got, output) + } + for index := 0; index < 3; index++ { wantID := fmt.Sprintf("call_162060%d", index+3) - if gotID := parts[index].Get("functionResponse.id").String(); gotID != wantID { - t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) + if gotID := parts[index+1].Get("functionResponse.id").String(); gotID != wantID { + t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+1, gotID, wantID, output) } } - if got := parts[6].Get("text").String(); got != "Tool results follow." { - t.Fatalf("first trailing text = %q; output=%s", got, output) + if got := parts[4].Get("text").String(); got != "Continue after reading." { + t.Fatalf("middle text = %q; output=%s", got, output) } - if got := parts[7].Get("text").String(); got != "Continue after reading." { - t.Fatalf("second trailing text = %q; output=%s", got, output) + for index := 3; index < 6; index++ { + wantID := fmt.Sprintf("call_162060%d", index+3) + if gotID := parts[index+2].Get("functionResponse.id").String(); gotID != wantID { + t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+2, gotID, wantID, output) + } } if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output) diff --git a/internal/translator/common/claude_messages.go b/internal/translator/common/claude_messages.go index 36e930200..158fb11c1 100644 --- a/internal/translator/common/claude_messages.go +++ b/internal/translator/common/claude_messages.go @@ -76,9 +76,9 @@ func (a *ClaudeMessageAccumulator) Messages() [][]byte { return a.messages } -// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs. -// Other content blocks retain their relative order after the tool results. If a -// complete one-to-one match is unavailable, the original content is returned. +// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs, +// preserving non-result content blocks at their existing indexes. If a complete +// one-to-one match is unavailable, the original content is returned. func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result { if !content.IsArray() || len(toolUseIDs) == 0 { return content @@ -86,19 +86,18 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res parts := content.Array() toolResults := make([]gjson.Result, 0, len(toolUseIDs)) - otherParts := make([]gjson.Result, 0, len(parts)) - for _, part := range parts { + toolResultIndices := make([]int, 0, len(toolUseIDs)) + for i, part := range parts { if part.Get("type").String() == "tool_result" { toolResults = append(toolResults, part) - continue + toolResultIndices = append(toolResultIndices, i) } - otherParts = append(otherParts, part) } if len(toolResults) != len(toolUseIDs) { return content } - ordered := make([][]byte, 0, len(parts)) + reorderedResults := make([]gjson.Result, 0, len(toolUseIDs)) used := make([]bool, len(toolResults)) for _, toolUseID := range toolUseIDs { matched := -1 @@ -112,10 +111,15 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res return content } used[matched] = true - ordered = append(ordered, []byte(toolResults[matched].Raw)) + reorderedResults = append(reorderedResults, toolResults[matched]) } - for _, part := range otherParts { - ordered = append(ordered, []byte(part.Raw)) + + ordered := make([][]byte, len(parts)) + for i, part := range parts { + ordered[i] = []byte(part.Raw) + } + for i, slotIndex := range toolResultIndices { + ordered[slotIndex] = []byte(reorderedResults[i].Raw) } return gjson.ParseBytes(JoinRawArray(ordered)) } diff --git a/internal/translator/common/claude_messages_test.go b/internal/translator/common/claude_messages_test.go index 042b35265..5358f33cc 100644 --- a/internal/translator/common/claude_messages_test.go +++ b/internal/translator/common/claude_messages_test.go @@ -110,7 +110,7 @@ func TestClaudeMessageAccumulatorPreservesBlockCacheControl(t *testing.T) { } func TestAlignClaudeToolResults(t *testing.T) { - t.Run("reorders permuted results and keeps other parts after", func(t *testing.T) { + t.Run("reorders permuted results while preserving non-result slots", func(t *testing.T) { input := gjson.Parse(`[ {"type":"tool_result","tool_use_id":"call_2","content":"two"}, {"type":"text","text":"extra user text"}, @@ -124,11 +124,31 @@ func TestAlignClaudeToolResults(t *testing.T) { if parts[0].Get("tool_use_id").String() != "call_1" { t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String()) } - if parts[1].Get("tool_use_id").String() != "call_2" { - t.Fatalf("parts[1].tool_use_id = %q, want call_2", parts[1].Get("tool_use_id").String()) + if parts[1].Get("type").String() != "text" || parts[1].Get("text").String() != "extra user text" { + t.Fatalf("parts[1] = %s, want extra user text", parts[1].Raw) } - if parts[2].Get("type").String() != "text" || parts[2].Get("text").String() != "extra user text" { - t.Fatalf("parts[2] = %s, want extra user text", parts[2].Raw) + if parts[2].Get("tool_use_id").String() != "call_2" { + t.Fatalf("parts[2].tool_use_id = %q, want call_2", parts[2].Get("tool_use_id").String()) + } + + leadingTextInput := gjson.Parse(`[ + {"type":"text","text":"leading text"}, + {"type":"tool_result","tool_use_id":"call_2","content":"two"}, + {"type":"tool_result","tool_use_id":"call_1","content":"one"} + ]`) + leadingAligned := AlignClaudeToolResults(leadingTextInput, []string{"call_1", "call_2"}) + leadingParts := leadingAligned.Array() + if len(leadingParts) != 3 { + t.Fatalf("len(leadingParts) = %d, want 3", len(leadingParts)) + } + if leadingParts[0].Get("type").String() != "text" || leadingParts[0].Get("text").String() != "leading text" { + t.Fatalf("leadingParts[0] = %s, want leading text", leadingParts[0].Raw) + } + if leadingParts[1].Get("tool_use_id").String() != "call_1" { + t.Fatalf("leadingParts[1].tool_use_id = %q, want call_1", leadingParts[1].Get("tool_use_id").String()) + } + if leadingParts[2].Get("tool_use_id").String() != "call_2" { + t.Fatalf("leadingParts[2].tool_use_id = %q, want call_2", leadingParts[2].Get("tool_use_id").String()) } }) diff --git a/internal/translator/gemini/claude/gemini_claude_request_test.go b/internal/translator/gemini/claude/gemini_claude_request_test.go index 232c23516..72210bf3c 100644 --- a/internal/translator/gemini/claude/gemini_claude_request_test.go +++ b/internal/translator/gemini/claude/gemini_claude_request_test.go @@ -389,18 +389,21 @@ func TestConvertClaudeRequestToGemini_AlignsPermutedParallelToolResultsWithMixed if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID { t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) } - if gotID := responseParts[index].Get("functionResponse.id").String(); gotID != wantID { + } + if got := responseParts[0].Get("text").String(); got != "Results arrived." { + t.Fatalf("leading text = %q; output=%s", got, output) + } + for index, wantID := range []string{"call_1", "call_2", "call_3"} { + responsePart := responseParts[index+1] + if gotID := responsePart.Get("functionResponse.id").String(); gotID != wantID { t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output) } - if gotName := responseParts[index].Get("functionResponse.name").String(); gotName != "Read" { + if gotName := responsePart.Get("functionResponse.name").String(); gotName != "Read" { t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output) } } - if got := responseParts[3].Get("text").String(); got != "Results arrived." { - t.Fatalf("first trailing text = %q; output=%s", got, output) - } if got := responseParts[4].Get("text").String(); got != "Continue." { - t.Fatalf("second trailing text = %q; output=%s", got, output) + t.Fatalf("trailing text = %q; output=%s", got, output) } if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil { t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)