fix: backfill empty functionResponse.name from preceding functionCall
when Amp or Claude Code sends functionResponse with an empty name in Gemini conversation history, the Gemini API rejects the request with 400 "Name cannot be empty". this fix backfills empty names from the corresponding preceding functionCall parts using positional matching. covers all three Gemini translator paths: - gemini/gemini (direct API key) - antigravity/gemini (OAuth) - gemini-cli/gemini (Gemini CLI) also switches fixCLIToolResponse pending group matching from LIFO to FIFO to correctly handle multiple sequential tool call groups. fixes #1903
This commit is contained in:
@@ -7,6 +7,7 @@ package gemini
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
@@ -116,6 +117,17 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
|
||||
// FunctionCallGroup represents a group of function calls and their responses
|
||||
type FunctionCallGroup struct {
|
||||
ResponsesNeeded int
|
||||
CallNames []string // ordered function call names for backfilling empty response names
|
||||
}
|
||||
|
||||
// backfillFunctionResponseName ensures that a functionResponse JSON object has a non-empty name,
|
||||
// falling back to fallbackName if the original is empty.
|
||||
func backfillFunctionResponseName(raw string, fallbackName string) string {
|
||||
name := gjson.Get(raw, "functionResponse.name").String()
|
||||
if strings.TrimSpace(name) == "" && fallbackName != "" {
|
||||
raw, _ = sjson.Set(raw, "functionResponse.name", fallbackName)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
|
||||
@@ -165,31 +177,28 @@ func fixCLIToolResponse(input string) (string, error) {
|
||||
if len(responsePartsInThisContent) > 0 {
|
||||
collectedResponses = append(collectedResponses, responsePartsInThisContent...)
|
||||
|
||||
// Check if any pending groups can be satisfied
|
||||
for i := len(pendingGroups) - 1; i >= 0; i-- {
|
||||
group := pendingGroups[i]
|
||||
if len(collectedResponses) >= group.ResponsesNeeded {
|
||||
// Take the needed responses for this group
|
||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||
// Check if pending groups can be satisfied (FIFO: oldest group first)
|
||||
for len(pendingGroups) > 0 && len(collectedResponses) >= pendingGroups[0].ResponsesNeeded {
|
||||
group := pendingGroups[0]
|
||||
pendingGroups = pendingGroups[1:]
|
||||
|
||||
// Create merged function response content
|
||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||
for _, response := range groupResponses {
|
||||
if !response.IsObject() {
|
||||
log.Warnf("failed to parse function response")
|
||||
continue
|
||||
}
|
||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
|
||||
// Take the needed responses for this group
|
||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||
|
||||
// Create merged function response content
|
||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||
for ri, response := range groupResponses {
|
||||
if !response.IsObject() {
|
||||
log.Warnf("failed to parse function response")
|
||||
continue
|
||||
}
|
||||
raw := backfillFunctionResponseName(response.Raw, group.CallNames[ri])
|
||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", raw)
|
||||
}
|
||||
|
||||
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||
}
|
||||
|
||||
// Remove this group as it's been satisfied
|
||||
pendingGroups = append(pendingGroups[:i], pendingGroups[i+1:]...)
|
||||
break
|
||||
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,15 +207,15 @@ func fixCLIToolResponse(input string) (string, error) {
|
||||
|
||||
// If this is a model with function calls, create a new group
|
||||
if role == "model" {
|
||||
functionCallsCount := 0
|
||||
var callNames []string
|
||||
parts.ForEach(func(_, part gjson.Result) bool {
|
||||
if part.Get("functionCall").Exists() {
|
||||
functionCallsCount++
|
||||
callNames = append(callNames, part.Get("functionCall.name").String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if functionCallsCount > 0 {
|
||||
if len(callNames) > 0 {
|
||||
// Add the model content
|
||||
if !value.IsObject() {
|
||||
log.Warnf("failed to parse model content")
|
||||
@@ -216,7 +225,8 @@ func fixCLIToolResponse(input string) (string, error) {
|
||||
|
||||
// Create a new group for tracking responses
|
||||
group := &FunctionCallGroup{
|
||||
ResponsesNeeded: functionCallsCount,
|
||||
ResponsesNeeded: len(callNames),
|
||||
CallNames: callNames,
|
||||
}
|
||||
pendingGroups = append(pendingGroups, group)
|
||||
} else {
|
||||
@@ -246,12 +256,16 @@ func fixCLIToolResponse(input string) (string, error) {
|
||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||
|
||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
||||
for _, response := range groupResponses {
|
||||
for ri, response := range groupResponses {
|
||||
if !response.IsObject() {
|
||||
log.Warnf("failed to parse function response")
|
||||
continue
|
||||
}
|
||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
|
||||
raw := response.Raw
|
||||
if ri < len(group.CallNames) {
|
||||
raw = backfillFunctionResponseName(raw, group.CallNames[ri])
|
||||
}
|
||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", raw)
|
||||
}
|
||||
|
||||
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
||||
|
||||
Reference in New Issue
Block a user