mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-11 14:47:58 +08:00
- Ensure Gemini, AI Studio, Vertex, and Antigravity requests end with a user turn to prevent upstream errors when trailing with model content. - Preserve trailing turns that contain a function response and skip trailing adjustments for `countTokens` actions or Claude models. - Introduce boundary normalization helpers to validate both leading and trailing turn roles. Closes: #5358
92 lines
2.9 KiB
Go
92 lines
2.9 KiB
Go
package helps
|
|
|
|
import (
|
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
var emptyGeminiUserTurnJSON = []byte(`{"role":"user","parts":[{"text":""}]}`)
|
|
|
|
// EnsureGeminiLeadingUserContent ensures that the contents array at the given path
|
|
// starts with a user turn when sending to Gemini/Antigravity upstreams.
|
|
func EnsureGeminiLeadingUserContent(payload []byte, path string) []byte {
|
|
firstRole := gjson.GetBytes(payload, path+".0.role")
|
|
if firstRole.String() != "model" {
|
|
return payload
|
|
}
|
|
contents := util.GetGJSONBytesNoCopy(payload, path)
|
|
if !contents.IsArray() {
|
|
return payload
|
|
}
|
|
contentArray := contents.Array()
|
|
if len(contentArray) == 0 {
|
|
return payload
|
|
}
|
|
|
|
contentItems := make([][]byte, 0, len(contentArray)+1)
|
|
contentItems = append(contentItems, emptyGeminiUserTurnJSON)
|
|
for _, content := range contentArray {
|
|
contentItems = append(contentItems, []byte(content.Raw))
|
|
}
|
|
|
|
out, errSet := sjson.SetRawBytes(payload, path, translatorcommon.JoinRawArray(contentItems))
|
|
if errSet != nil {
|
|
return payload
|
|
}
|
|
return out
|
|
}
|
|
|
|
func contentHasFunctionResponse(content gjson.Result) bool {
|
|
parts := content.Get("parts")
|
|
if !parts.IsArray() {
|
|
return false
|
|
}
|
|
for _, part := range parts.Array() {
|
|
if part.Get("functionResponse").Exists() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// EnsureGeminiTrailingUserContent ensures that the contents array at the given path
|
|
// ends with a user turn when sending to Gemini/Antigravity upstreams.
|
|
// If the final turn contains a functionResponse, it is preserved without appending
|
|
// an empty user turn because upstream expects the model to generate the response.
|
|
func EnsureGeminiTrailingUserContent(payload []byte, path string) []byte {
|
|
contents := util.GetGJSONBytesNoCopy(payload, path)
|
|
if !contents.IsArray() {
|
|
return payload
|
|
}
|
|
contentArray := contents.Array()
|
|
if len(contentArray) == 0 {
|
|
return payload
|
|
}
|
|
lastContent := contentArray[len(contentArray)-1]
|
|
lastRole := lastContent.Get("role").String()
|
|
if (lastRole != "model" && lastRole != "assistant") || contentHasFunctionResponse(lastContent) {
|
|
return payload
|
|
}
|
|
|
|
contentItems := make([][]byte, 0, len(contentArray)+1)
|
|
for _, content := range contentArray {
|
|
contentItems = append(contentItems, []byte(content.Raw))
|
|
}
|
|
contentItems = append(contentItems, emptyGeminiUserTurnJSON)
|
|
|
|
out, errSet := sjson.SetRawBytes(payload, path, translatorcommon.JoinRawArray(contentItems))
|
|
if errSet != nil {
|
|
return payload
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EnsureGeminiBoundaryUserContent ensures that the contents array at the given path
|
|
// both starts and ends with a user turn when sending to Gemini/Antigravity upstreams.
|
|
func EnsureGeminiBoundaryUserContent(payload []byte, path string) []byte {
|
|
payload = EnsureGeminiLeadingUserContent(payload, path)
|
|
return EnsureGeminiTrailingUserContent(payload, path)
|
|
}
|