History

fix(claude): preserve non-result block positions during tool result alignment

- Maintain non-tool-result content blocks at their original indices in `AlignClaudeToolResults` instead of moving them after results. - Reorder tool results in-place into their original slot indices based on preceding tool use IDs.

Luis Pater committed 2 months ago

Commit 85641421 parent 5dc428f4 files · +63 −30Browse files

Unified · Split

4 files
  1. internal/translator/antigravity/claude/antigravity_claude_request_test.go +14 −8
  2. internal/translator/common/claude_messages.go +15 −11
  3. internal/translator/common/claude_messages_test.go +25 −5
  4. internal/translator/gemini/claude/gemini_claude_request_test.go +9 −6
internal/translator/antigravity/claude/antigravity_claude_request_test.go+14−8
@@ -2253,19 +2253,25 @@ func TestConvertClaudeRequestToAntigravity_AlignsPermutedParallelToolResultsWith · 2252 unmodified lines
2253 output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false)2253 output := ConvertClaudeRequestToAntigravity("gemini-3.7-flash-high", inputJSON, false)
2254 parts := gjson.GetBytes(output, "request.contents.1.parts").Array()2254 parts := gjson.GetBytes(output, "request.contents.1.parts").Array()
2255 if len(parts) != 8 {2255 if len(parts) != 8 {
2256 t.Fatalf("parts = %d, want six responses followed by two text parts; output=%s", len(parts), output)2256 t.Fatalf("parts = %d, want eight parts; output=%s", len(parts), output)
2257 }2257 }
2258 for index := 0; index < 6; index++ {2258 if got := parts[0].Get("text").String(); got != "Tool results follow." {
2259 t.Fatalf("leading text = %q; output=%s", got, output)
2260 }
2261 for index := 0; index < 3; index++ {
2259 wantID := fmt.Sprintf("call_162060%d", index+3)2262 wantID := fmt.Sprintf("call_162060%d", index+3)
2260 if gotID := parts[index].Get("functionResponse.id").String(); gotID != wantID {2263 if gotID := parts[index+1].Get("functionResponse.id").String(); gotID != wantID {
2261 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)2264 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+1, gotID, wantID, output)
2262 }2265 }
2263 }2266 }
2264 if got := parts[6].Get("text").String(); got != "Tool results follow." {2267 if got := parts[4].Get("text").String(); got != "Continue after reading." {
2265 t.Fatalf("first trailing text = %q; output=%s", got, output)2268 t.Fatalf("middle text = %q; output=%s", got, output)
2266 }2269 }
2267 if got := parts[7].Get("text").String(); got != "Continue after reading." {2270 for index := 3; index < 6; index++ {
2268 t.Fatalf("second trailing text = %q; output=%s", got, output)2271 wantID := fmt.Sprintf("call_162060%d", index+3)
2272 if gotID := parts[index+2].Get("functionResponse.id").String(); gotID != wantID {
2273 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index+2, gotID, wantID, output)
2274 }
2269 }2275 }
2270 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {2276 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {
2271 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)2277 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)
internal/translator/common/claude_messages.go+15−11
@@ -76,9 +76,9 @@ func (a *ClaudeMessageAccumulator) Messages() [][]byte { · 75 unmodified lines
76 return a.messages76 return a.messages
77}77}
78 78
79// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs.79// AlignClaudeToolResults orders tool_result blocks by the preceding tool_use IDs,
80// Other content blocks retain their relative order after the tool results. If a80// preserving non-result content blocks at their existing indexes. If a complete
81// complete one-to-one match is unavailable, the original content is returned.81// one-to-one match is unavailable, the original content is returned.
82func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result {82func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Result {
83 if !content.IsArray() || len(toolUseIDs) == 0 {83 if !content.IsArray() || len(toolUseIDs) == 0 {
84 return content84 return content
@@ -86,19 +86,18 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 1 unmodified line
86 86
87 parts := content.Array()87 parts := content.Array()
88 toolResults := make([]gjson.Result, 0, len(toolUseIDs))88 toolResults := make([]gjson.Result, 0, len(toolUseIDs))
89 otherParts := make([]gjson.Result, 0, len(parts))89 toolResultIndices := make([]int, 0, len(toolUseIDs))
90 for _, part := range parts {90 for i, part := range parts {
91 if part.Get("type").String() == "tool_result" {91 if part.Get("type").String() == "tool_result" {
92 toolResults = append(toolResults, part)92 toolResults = append(toolResults, part)
93 continue93 toolResultIndices = append(toolResultIndices, i)
94 }94 }
95 otherParts = append(otherParts, part)
96 }95 }
97 if len(toolResults) != len(toolUseIDs) {96 if len(toolResults) != len(toolUseIDs) {
98 return content97 return content
99 }98 }
100 99
101 ordered := make([][]byte, 0, len(parts))100 reorderedResults := make([]gjson.Result, 0, len(toolUseIDs))
102 used := make([]bool, len(toolResults))101 used := make([]bool, len(toolResults))
103 for _, toolUseID := range toolUseIDs {102 for _, toolUseID := range toolUseIDs {
104 matched := -1103 matched := -1
@@ -112,10 +111,15 @@ func AlignClaudeToolResults(content gjson.Result, toolUseIDs []string) gjson.Res · 7 unmodified lines
112 return content111 return content
113 }112 }
114 used[matched] = true113 used[matched] = true
115 ordered = append(ordered, []byte(toolResults[matched].Raw))114 reorderedResults = append(reorderedResults, toolResults[matched])
115 }
116
117 ordered := make([][]byte, len(parts))
118 for i, part := range parts {
119 ordered[i] = []byte(part.Raw)
116 }120 }
117 for _, part := range otherParts {121 for i, slotIndex := range toolResultIndices {
118 ordered = append(ordered, []byte(part.Raw))122 ordered[slotIndex] = []byte(reorderedResults[i].Raw)
119 }123 }
120 return gjson.ParseBytes(JoinRawArray(ordered))124 return gjson.ParseBytes(JoinRawArray(ordered))
121}125}
internal/translator/common/claude_messages_test.go+25−5
@@ -110,7 +110,7 @@ func TestClaudeMessageAccumulatorPreservesBlockCacheControl(t *testing.T) { · 109 unmodified lines
110}110}
111 111
112func TestAlignClaudeToolResults(t *testing.T) {112func TestAlignClaudeToolResults(t *testing.T) {
113 t.Run("reorders permuted results and keeps other parts after", func(t *testing.T) {113 t.Run("reorders permuted results while preserving non-result slots", func(t *testing.T) {
114 input := gjson.Parse(`[114 input := gjson.Parse(`[
115 {"type":"tool_result","tool_use_id":"call_2","content":"two"},115 {"type":"tool_result","tool_use_id":"call_2","content":"two"},
116 {"type":"text","text":"extra user text"},116 {"type":"text","text":"extra user text"},
@@ -124,11 +124,31 @@ func TestAlignClaudeToolResults(t *testing.T) { · 7 unmodified lines
124 if parts[0].Get("tool_use_id").String() != "call_1" {124 if parts[0].Get("tool_use_id").String() != "call_1" {
125 t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String())125 t.Fatalf("parts[0].tool_use_id = %q, want call_1", parts[0].Get("tool_use_id").String())
126 }126 }
127 if parts[1].Get("tool_use_id").String() != "call_2" {127 if parts[1].Get("type").String() != "text" || parts[1].Get("text").String() != "extra user text" {
128 t.Fatalf("parts[1].tool_use_id = %q, want call_2", parts[1].Get("tool_use_id").String())128 t.Fatalf("parts[1] = %s, want extra user text", parts[1].Raw)
129 }129 }
130 if parts[2].Get("type").String() != "text" || parts[2].Get("text").String() != "extra user text" {130 if parts[2].Get("tool_use_id").String() != "call_2" {
131 t.Fatalf("parts[2] = %s, want extra user text", parts[2].Raw)131 t.Fatalf("parts[2].tool_use_id = %q, want call_2", parts[2].Get("tool_use_id").String())
132 }
133
134 leadingTextInput := gjson.Parse(`[
135 {"type":"text","text":"leading text"},
136 {"type":"tool_result","tool_use_id":"call_2","content":"two"},
137 {"type":"tool_result","tool_use_id":"call_1","content":"one"}
138 ]`)
139 leadingAligned := AlignClaudeToolResults(leadingTextInput, []string{"call_1", "call_2"})
140 leadingParts := leadingAligned.Array()
141 if len(leadingParts) != 3 {
142 t.Fatalf("len(leadingParts) = %d, want 3", len(leadingParts))
143 }
144 if leadingParts[0].Get("type").String() != "text" || leadingParts[0].Get("text").String() != "leading text" {
145 t.Fatalf("leadingParts[0] = %s, want leading text", leadingParts[0].Raw)
146 }
147 if leadingParts[1].Get("tool_use_id").String() != "call_1" {
148 t.Fatalf("leadingParts[1].tool_use_id = %q, want call_1", leadingParts[1].Get("tool_use_id").String())
149 }
150 if leadingParts[2].Get("tool_use_id").String() != "call_2" {
151 t.Fatalf("leadingParts[2].tool_use_id = %q, want call_2", leadingParts[2].Get("tool_use_id").String())
132 }152 }
133 })153 })
134 154
internal/translator/gemini/claude/gemini_claude_request_test.go+9−6
@@ -389,18 +389,21 @@ func TestConvertClaudeRequestToGemini_AlignsPermutedParallelToolResultsWithMixed · 388 unmodified lines
389 if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID {389 if gotID := callParts[index].Get("functionCall.id").String(); gotID != wantID {
390 t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)390 t.Fatalf("functionCall[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)
391 }391 }
392 if gotID := responseParts[index].Get("functionResponse.id").String(); gotID != wantID {392 }
393 if got := responseParts[0].Get("text").String(); got != "Results arrived." {
394 t.Fatalf("leading text = %q; output=%s", got, output)
395 }
396 for index, wantID := range []string{"call_1", "call_2", "call_3"} {
397 responsePart := responseParts[index+1]
398 if gotID := responsePart.Get("functionResponse.id").String(); gotID != wantID {
393 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)399 t.Fatalf("functionResponse[%d].id = %q, want %q; output=%s", index, gotID, wantID, output)
394 }400 }
395 if gotName := responseParts[index].Get("functionResponse.name").String(); gotName != "Read" {401 if gotName := responsePart.Get("functionResponse.name").String(); gotName != "Read" {
396 t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output)402 t.Fatalf("functionResponse[%d].name = %q, want Read; output=%s", index, gotName, output)
397 }403 }
398 }404 }
399 if got := responseParts[3].Get("text").String(); got != "Results arrived." {
400 t.Fatalf("first trailing text = %q; output=%s", got, output)
401 }
402 if got := responseParts[4].Get("text").String(); got != "Continue." {405 if got := responseParts[4].Get("text").String(); got != "Continue." {
403 t.Fatalf("second trailing text = %q; output=%s", got, output)406 t.Fatalf("trailing text = %q; output=%s", got, output)
404 }407 }
405 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {408 if errPairing := internalsignature.ValidateGeminiFunctionCallPairing(output); errPairing != nil {
406 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)409 t.Fatalf("translated parallel tool history is invalid: %v; output=%s", errPairing, output)