mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-11 14:47:58 +08:00
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. Closes: #5484
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user