Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,900 @@
// Package gemini provides request translation functionality for Antigravity to Gemini API compatibility.
// It handles parsing and transforming Antigravity API requests into Gemini API format,
// extracting model information, system instructions, message contents, and tool declarations.
// The package performs JSON data transformation to ensure compatibility
// between Antigravity API format and Gemini API's expected format.
package gemini
import (
"encoding/json"
"fmt"
"strings"
"github.com/router-for-me/CLIProxyAPI/v7/internal/signature"
translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common"
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/gemini/common"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
log "github.com/sirupsen/logrus"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)
// ConvertGeminiRequestToAntigravity parses and transforms a Antigravity API request into Gemini API format.
// It extracts the model name, system instruction, message contents, and tool declarations
// from the raw JSON request and returns them in the format expected by the Gemini API.
// The function performs the following transformations:
// 1. Extracts the model information from the request
// 2. Restructures the JSON to match Gemini API format
// 3. Converts system instructions to the expected format
// 4. Fixes CLI tool response format and grouping
//
// Parameters:
// - modelName: The name of the model to use for the request (unused in current implementation)
// - rawJSON: The raw JSON request data from the Antigravity API
// - stream: A boolean indicating if the request is for a streaming response (unused in current implementation)
//
// Returns:
// - []byte: The transformed request data in Gemini API format
func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
rawJSON := inputRawJSON
functionNameMap := util.SanitizedFunctionNameMap(inputRawJSON)
// Keep the envelope in []byte form. Round-tripping through string copies the
// entire request, which dominates allocations for large inline data. Fill the
// small envelope fields first so the payload is only spliced in once.
envelope, _ := sjson.SetBytes([]byte(`{"project":"","request":{},"model":""}`), "model", modelName)
rawJSON, _ = sjson.SetRawBytes(envelope, "request", rawJSON)
if util.GetGJSONBytesNoCopy(rawJSON, "request.model").Exists() {
rawJSON, _ = sjson.DeleteBytes(rawJSON, "request.model")
}
fixedJSON, errFixCLIToolResponse := fixCLIToolResponse(rawJSON)
if errFixCLIToolResponse != nil {
return []byte{}
}
rawJSON = fixedJSON
if systemInstructionResult := util.GetGJSONBytesNoCopy(rawJSON, "request.system_instruction"); systemInstructionResult.Exists() {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.systemInstruction", []byte(systemInstructionResult.Raw))
rawJSON, _ = sjson.DeleteBytes(rawJSON, "request.system_instruction")
}
// Normalize roles in request.contents: default to valid values if missing/invalid.
// The contents array is only materialized when a role actually changes; copying
// every content up front duplicates the whole payload for large inline data.
contents := util.GetGJSONBytesNoCopy(rawJSON, "request.contents")
if contents.IsArray() && geminiContentRolesNeedNormalization(contents) {
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
previousRole := ""
contents.ForEach(func(_, value gjson.Result) bool {
role := value.Get("role").String()
content := []byte(value.Raw)
if role != "user" && role != "model" {
if previousRole == "" || previousRole == "model" {
role = "user"
} else {
role = "model"
}
content, _ = sjson.SetBytes(content, "role", role)
}
previousRole = role
contentItems = append(contentItems, content)
return true
})
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", translatorcommon.JoinRawArray(contentItems))
}
toolsResult := util.GetGJSONBytesNoCopy(rawJSON, "request.tools")
if toolsResult.IsArray() {
seenFunctionNames := make(map[string]struct{})
toolsChanged := false
var toolItems [][]byte
toolsResult.ForEach(func(toolIndex, tool gjson.Result) bool {
toolJSON := []byte(tool.Raw)
toolChanged := false
for _, key := range []string{"functionDeclarations", "function_declarations"} {
declarations := tool.Get(key)
if !declarations.IsArray() {
continue
}
declarationsChanged := false
var declarationItems [][]byte
declarations.ForEach(func(_, declaration gjson.Result) bool {
nameResult := declaration.Get("name")
originalName := nameResult.String()
mappedName := util.MapSanitizedFunctionName(functionNameMap, originalName)
if mappedName != "" {
if _, exists := seenFunctionNames[mappedName]; exists {
declarationsChanged = true
return true
}
seenFunctionNames[mappedName] = struct{}{}
}
declarationJSON := []byte(declaration.Raw)
if nameResult.Type != gjson.String || mappedName != originalName {
declarationJSON, _ = sjson.SetBytes(declarationJSON, "name", mappedName)
declarationsChanged = true
}
if parameters := declaration.Get("parameters"); parameters.Exists() {
declarationJSON, _ = sjson.SetRawBytes(declarationJSON, "parametersJsonSchema", []byte(parameters.Raw))
declarationJSON, _ = sjson.DeleteBytes(declarationJSON, "parameters")
declarationsChanged = true
}
declarationItems = append(declarationItems, declarationJSON)
return true
})
if declarationsChanged {
var errSet error
toolJSON, errSet = sjson.SetRawBytes(toolJSON, key, translatorcommon.JoinRawArray(declarationItems))
if errSet != nil {
log.Warnf("failed to normalize function declarations in tool %d: %v", toolIndex.Int(), errSet)
} else {
toolChanged = true
}
}
}
toolsChanged = toolsChanged || toolChanged
toolItems = append(toolItems, toolJSON)
return true
})
if toolsChanged {
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.tools", translatorcommon.JoinRawArray(toolItems))
}
rawJSON = removeEmptyGeminiFunctionTools(rawJSON)
}
rawJSON = rewriteGeminiFunctionNames(rawJSON, functionNameMap)
if strings.Contains(strings.ToLower(modelName), "claude") {
rawJSON = SanitizeAntigravityClaudeGeminiRequestSignatures(modelName, rawJSON)
} else {
rawJSON = signature.SanitizeGeminiRequestThoughtSignatures(rawJSON, "request.contents")
}
return common.AttachDefaultSafetySettings(rawJSON, "request.safetySettings")
}
// geminiContentRolesNeedNormalization reports whether any content role is missing
// or invalid and therefore requires rebuilding the contents array.
func geminiContentRolesNeedNormalization(contents gjson.Result) bool {
needsNormalization := false
contents.ForEach(func(_, value gjson.Result) bool {
role := value.Get("role").String()
if role != "user" && role != "model" {
needsNormalization = true
return false
}
return true
})
return needsNormalization
}
func removeEmptyGeminiFunctionTools(rawJSON []byte) []byte {
tools := util.GetGJSONBytesNoCopy(rawJSON, "request.tools")
if tools.IsArray() && len(tools.Array()) == 0 {
rawJSON, _ = sjson.DeleteBytes(rawJSON, "request.tools")
return rawJSON
}
changed := false
var cleanedTools [][]byte
for _, tool := range tools.Array() {
toolJSON := []byte(tool.Raw)
if tool.IsObject() {
for _, key := range []string{"functionDeclarations", "function_declarations"} {
if declarations := tool.Get(key); declarations.IsArray() && len(declarations.Array()) == 0 {
toolJSON, _ = sjson.DeleteBytes(toolJSON, key)
changed = true
}
}
if len(util.ParseGJSONBytesNoCopy(toolJSON).Map()) == 0 {
changed = true
continue
}
}
cleanedTools = append(cleanedTools, toolJSON)
}
if !changed {
return rawJSON
}
if len(cleanedTools) == 0 {
rawJSON, _ = sjson.DeleteBytes(rawJSON, "request.tools")
return rawJSON
}
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.tools", translatorcommon.JoinRawArray(cleanedTools))
return rawJSON
}
// geminiFunctionNameFields lists the part fields that can carry a function name.
var geminiFunctionNameFields = []string{"functionCall", "functionResponse", "function_call", "function_response"}
// geminiFunctionNamesNeedRewrite reports whether any part carries a function name
// that must be remapped or coerced to a string.
func geminiFunctionNamesNeedRewrite(contents gjson.Result, functionNameMap map[string]string) bool {
needsRewrite := false
contents.ForEach(func(_, content gjson.Result) bool {
content.Get("parts").ForEach(func(_, part gjson.Result) bool {
for _, field := range geminiFunctionNameFields {
nameResult := part.Get(field + ".name")
name := nameResult.String()
if name == "" {
continue
}
if nameResult.Type == gjson.String && util.MapSanitizedFunctionName(functionNameMap, name) == name {
continue
}
needsRewrite = true
return false
}
return true
})
return !needsRewrite
})
return needsRewrite
}
func rewriteGeminiFunctionNames(rawJSON []byte, functionNameMap map[string]string) []byte {
contents := util.GetGJSONBytesNoCopy(rawJSON, "request.contents")
canBatchContents := contents.IsArray()
if canBatchContents {
contents.ForEach(func(_, content gjson.Result) bool {
parts := content.Get("parts")
if parts.Exists() && !parts.IsArray() {
canBatchContents = false
return false
}
return true
})
}
// Rebuilding the contents array copies every content and part, so only pay for
// it once a name actually needs rewriting.
if canBatchContents && geminiFunctionNamesNeedRewrite(contents, functionNameMap) {
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
contents.ForEach(func(_, content gjson.Result) bool {
contentJSON := []byte(content.Raw)
partsChanged := false
partItems := make([][]byte, 0, 4)
content.Get("parts").ForEach(func(_, part gjson.Result) bool {
partJSON := []byte(part.Raw)
for _, field := range geminiFunctionNameFields {
nameResult := part.Get(field + ".name")
name := nameResult.String()
if name == "" {
continue
}
mappedName := util.MapSanitizedFunctionName(functionNameMap, name)
if nameResult.Type == gjson.String && mappedName == name {
continue
}
partJSON, _ = sjson.SetBytes(partJSON, field+".name", mappedName)
partsChanged = true
}
partItems = append(partItems, partJSON)
return true
})
if partsChanged {
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts", translatorcommon.JoinRawArray(partItems))
}
contentItems = append(contentItems, contentJSON)
return true
})
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", translatorcommon.JoinRawArray(contentItems))
} else if !canBatchContents {
for contentIndex, content := range contents.Array() {
for partIndex, part := range content.Get("parts").Array() {
for _, field := range geminiFunctionNameFields {
nameResult := part.Get(field + ".name")
name := nameResult.String()
if name == "" {
continue
}
mappedName := util.MapSanitizedFunctionName(functionNameMap, name)
if nameResult.Type == gjson.String && mappedName == name {
continue
}
path := fmt.Sprintf("request.contents.%d.parts.%d.%s.name", contentIndex, partIndex, field)
rawJSON, _ = sjson.SetBytes(rawJSON, path, mappedName)
}
}
}
}
for _, allowedPath := range []string{
"request.toolConfig.functionCallingConfig.allowedFunctionNames",
"request.tool_config.function_calling_config.allowed_function_names",
} {
allowedNames := util.GetGJSONBytesNoCopy(rawJSON, allowedPath)
if allowedNames.IsArray() {
namesChanged := false
nameItems := make([][]byte, 0, 4)
allowedNames.ForEach(func(_, name gjson.Result) bool {
mappedName := util.MapSanitizedFunctionName(functionNameMap, name.String())
namesChanged = namesChanged || name.Type != gjson.String || mappedName != name.String()
mappedNameJSON, _ := json.Marshal(mappedName)
nameItems = append(nameItems, mappedNameJSON)
return true
})
if namesChanged {
rawJSON, _ = sjson.SetRawBytes(rawJSON, allowedPath, translatorcommon.JoinRawArray(nameItems))
}
} else {
for index, name := range allowedNames.Array() {
mappedName := util.MapSanitizedFunctionName(functionNameMap, name.String())
if name.Type == gjson.String && mappedName == name.String() {
continue
}
path := fmt.Sprintf("%s.%d", allowedPath, index)
rawJSON, _ = sjson.SetBytes(rawJSON, path, mappedName)
}
}
}
return rawJSON
}
func SanitizeAntigravityClaudeGeminiRequestSignatures(modelName string, rawJSON []byte) []byte {
contents := util.GetGJSONBytesNoCopy(rawJSON, "request.contents")
if !contents.IsArray() {
return rawJSON
}
contentsArray := contents.Array()
changed := false
rewrittenContents := make([][]byte, 0, len(contentsArray))
for contentIndex, content := range contentsArray {
parts := content.Get("parts")
if !parts.IsArray() {
rewrittenContents = append(rewrittenContents, []byte(content.Raw))
continue
}
isModelTurn := content.Get("role").String() == "model"
partsArray := parts.Array()
contentChanged := false
rewrittenParts := make([][]byte, 0, len(partsArray))
for partIndex, partResult := range partsArray {
var part map[string]any
decoder := json.NewDecoder(strings.NewReader(partResult.Raw))
decoder.UseNumber()
if err := decoder.Decode(&part); err != nil {
rewrittenParts = append(rewrittenParts, []byte(partResult.Raw))
continue
}
rawSignature, hasStringSignature := antigravityClaudeGeminiPartThoughtSignature(part)
hasSignatureKey := hasStringSignature || antigravityClaudeGeminiPartHasThoughtSignatureKey(part) || antigravityClaudeGeminiPartHasThoughtSignatureKeyInRaw(partResult.Raw)
if hasFunctionResponsePart(part) {
if hasSignatureKey {
changed = true
contentChanged = true
deleteAntigravityClaudeGeminiPartThoughtSignatureFields(part)
logAntigravityClaudeGeminiSignatureSanitize(modelName, "drop_signature", "functionResponse parts cannot replay Claude thinking signatures", contentIndex, partIndex, rawSignature)
partBytes, _ := json.Marshal(part)
rewrittenParts = append(rewrittenParts, partBytes)
} else {
rewrittenParts = append(rewrittenParts, []byte(partResult.Raw))
}
continue
}
if !isModelTurn {
if hasSignatureKey {
changed = true
contentChanged = true
deleteAntigravityClaudeGeminiPartThoughtSignatureFields(part)
logAntigravityClaudeGeminiSignatureSanitize(modelName, "drop_signature", "non-model parts cannot replay Claude thinking signatures", contentIndex, partIndex, rawSignature)
partBytes, _ := json.Marshal(part)
rewrittenParts = append(rewrittenParts, partBytes)
} else {
rewrittenParts = append(rewrittenParts, []byte(partResult.Raw))
}
continue
}
if part["thought"] == true {
normalized, compatible := signature.CompatibleAntigravityClaudeThinkingSignature(rawSignature)
if !compatible {
changed = true
contentChanged = true
logAntigravityClaudeGeminiSignatureSanitize(modelName, "drop_thinking_block", "missing_or_incompatible_signature", contentIndex, partIndex, rawSignature)
continue
}
text, _ := part["text"].(string)
if strings.TrimSpace(text) == "" {
changed = true
contentChanged = true
logAntigravityClaudeGeminiSignatureSanitize(modelName, "drop_thinking_block", "empty_thinking_text", contentIndex, partIndex, rawSignature)
continue
}
if normalized != rawSignature {
changed = true
contentChanged = true
logAntigravityClaudeGeminiSignatureSanitize(modelName, "normalize_signature", "compatible_claude_signature", contentIndex, partIndex, rawSignature)
}
deleteAntigravityClaudeGeminiPartThoughtSignatureFields(part)
part["thoughtSignature"] = normalized
partBytes, _ := json.Marshal(part)
rewrittenParts = append(rewrittenParts, partBytes)
continue
}
if hasSignatureKey {
changed = true
contentChanged = true
deleteAntigravityClaudeGeminiPartThoughtSignatureFields(part)
logAntigravityClaudeGeminiSignatureSanitize(modelName, "drop_signature", "non-thinking parts should not carry Claude thinking signatures", contentIndex, partIndex, rawSignature)
partBytes, _ := json.Marshal(part)
rewrittenParts = append(rewrittenParts, partBytes)
} else {
rewrittenParts = append(rewrittenParts, []byte(partResult.Raw))
}
}
if len(rewrittenParts) == 0 {
changed = true
continue
}
if contentChanged || len(rewrittenParts) != len(partsArray) {
contentBytes := []byte(content.Raw)
contentBytes, _ = sjson.SetRawBytes(contentBytes, "parts", translatorcommon.JoinRawArray(rewrittenParts))
rewrittenContents = append(rewrittenContents, contentBytes)
} else {
rewrittenContents = append(rewrittenContents, []byte(content.Raw))
}
}
if !changed {
return rawJSON
}
out, errSet := sjson.SetRawBytes(rawJSON, "request.contents", translatorcommon.JoinRawArray(rewrittenContents))
if errSet != nil {
return rawJSON
}
return out
}
func antigravityClaudeGeminiPartHasThoughtSignatureKeyInRaw(raw string) bool {
dec := json.NewDecoder(strings.NewReader(raw))
dec.UseNumber()
var stack []bool
expectKey := false
for {
t, err := dec.Token()
if err != nil {
break
}
switch v := t.(type) {
case json.Delim:
switch v {
case '{':
stack = append(stack, true)
expectKey = true
case '}':
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
if len(stack) > 0 && stack[len(stack)-1] {
expectKey = true
} else {
expectKey = false
}
case '[':
stack = append(stack, false)
expectKey = false
case ']':
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
if len(stack) > 0 && stack[len(stack)-1] {
expectKey = true
} else {
expectKey = false
}
}
case string:
if expectKey && len(stack) > 0 && stack[len(stack)-1] {
if v == "thoughtSignature" || v == "thought_signature" {
return true
}
expectKey = false
} else {
if len(stack) > 0 && stack[len(stack)-1] {
expectKey = true
}
}
default:
if len(stack) > 0 && stack[len(stack)-1] {
expectKey = true
}
}
}
return false
}
func antigravityClaudeGeminiPartHasThoughtSignatureKey(part map[string]any) bool {
for _, path := range [][]string{
{"thoughtSignature"},
{"thought_signature"},
{"functionCall", "thoughtSignature"},
{"functionCall", "thought_signature"},
{"functionResponse", "thoughtSignature"},
{"functionResponse", "thought_signature"},
{"extra_content", "google", "thought_signature"},
} {
if hasKeyAtPath(part, path...) {
return true
}
}
return false
}
func hasKeyAtPath(value map[string]any, path ...string) bool {
var current any = value
for _, key := range path {
m, ok := current.(map[string]any)
if !ok {
return false
}
if _, exists := m[key]; !exists {
return false
}
current = m[key]
}
return true
}
func antigravityClaudeGeminiPartThoughtSignature(part map[string]any) (string, bool) {
for _, path := range [][]string{
{"thoughtSignature"},
{"thought_signature"},
{"functionCall", "thoughtSignature"},
{"functionCall", "thought_signature"},
{"functionResponse", "thoughtSignature"},
{"functionResponse", "thought_signature"},
{"extra_content", "google", "thought_signature"},
} {
if value, ok := stringAtPath(part, path...); ok {
return value, true
}
}
return "", false
}
func deleteAntigravityClaudeGeminiPartThoughtSignatureFields(part map[string]any) {
for _, path := range [][]string{
{"thoughtSignature"},
{"thought_signature"},
{"functionCall", "thoughtSignature"},
{"functionCall", "thought_signature"},
{"functionResponse", "thoughtSignature"},
{"functionResponse", "thought_signature"},
{"extra_content", "google", "thought_signature"},
} {
deleteAtPath(part, path...)
}
}
func hasFunctionResponsePart(part map[string]any) bool {
if _, ok := part["functionResponse"]; ok {
return true
}
_, ok := part["function_response"]
return ok
}
func stringAtPath(value map[string]any, path ...string) (string, bool) {
var current any = value
for _, key := range path {
m, ok := current.(map[string]any)
if !ok {
return "", false
}
current, ok = m[key]
if !ok {
return "", false
}
}
s, ok := current.(string)
return s, ok
}
func deleteAtPath(value map[string]any, path ...string) {
if len(path) == 0 {
return
}
current := value
for _, key := range path[:len(path)-1] {
next, ok := current[key].(map[string]any)
if !ok {
return
}
current = next
}
delete(current, path[len(path)-1])
}
func logAntigravityClaudeGeminiSignatureSanitize(modelName, action, reason string, contentIndex, partIndex int, rawSignature string) {
fields := log.Fields{
"component": "signature_sanitizer",
"translator": "antigravity_gemini",
"target_provider": string(signature.SignatureProviderClaude),
"action": action,
"reason": reason,
"model": modelName,
"content_index": contentIndex,
"part_index": partIndex,
"has_signature": strings.TrimSpace(rawSignature) != "",
"signature_length": len(strings.TrimSpace(rawSignature)),
"detected_provider": string(signature.DetectSignatureProviderForBlock(rawSignature, signature.SignatureBlockKindClaudeThinking)),
}
log.WithFields(fields).Debug("antigravity gemini translator: sanitized Claude target thoughtSignature before upstream")
}
// 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
}
func normalizeAntigravityInlineDataPart(part gjson.Result) ([]byte, bool) {
inline := part.Get("inlineData")
if !inline.Exists() {
inline = part.Get("inline_data")
}
if !inline.Exists() {
return nil, false
}
data := inline.Get("data").String()
if data == "" {
return nil, false
}
mimeType := inline.Get("mimeType").String()
if mimeType == "" {
mimeType = inline.Get("mime_type").String()
}
if mimeType == "" {
// Cloud Code Assist ignores inlineData without mimeType.
mimeType = "image/png"
}
out := []byte(`{"inlineData":{"mimeType":"","data":""}}`)
out, _ = sjson.SetBytes(out, "inlineData.mimeType", mimeType)
out, _ = sjson.SetBytes(out, "inlineData.data", data)
return out, true
}
func attachInlineDataToFunctionResponse(response gjson.Result, images [][]byte) gjson.Result {
if len(images) == 0 {
return response
}
target := []byte(response.Raw)
for _, img := range images {
target, _ = sjson.SetRawBytes(target, "functionResponse.parts.-1", img)
}
return gjson.ParseBytes(target)
}
// collectFunctionResponsesWithSiblingInlineData keeps functionResponse parts and
// moves sibling inline_data/inlineData onto the nearest preceding functionResponse.
// Leading images before the first functionResponse attach to that first response.
func collectFunctionResponsesWithSiblingInlineData(parts gjson.Result) []gjson.Result {
responses := make([]gjson.Result, 0)
leadingImages := make([][]byte, 0)
current := -1
parts.ForEach(func(_, part gjson.Result) bool {
if part.Get("functionResponse").Exists() {
responses = append(responses, part)
current = len(responses) - 1
if len(leadingImages) > 0 {
responses[current] = attachInlineDataToFunctionResponse(responses[current], leadingImages)
leadingImages = nil
}
return true
}
imagePart, ok := normalizeAntigravityInlineDataPart(part)
if !ok {
return true
}
if current >= 0 {
responses[current] = attachInlineDataToFunctionResponse(responses[current], [][]byte{imagePart})
return true
}
leadingImages = append(leadingImages, imagePart)
return true
})
return responses
}
// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string.
// Falls back to a minimal "functionResponse" object when parsing fails.
// fallbackName is used when the response's own name is empty.
func parseFunctionResponseRaw(response gjson.Result, fallbackName string) string {
if response.IsObject() && gjson.Valid(response.Raw) {
raw := response.Raw
name := response.Get("functionResponse.name").String()
if strings.TrimSpace(name) == "" && fallbackName != "" {
updated, _ := sjson.SetBytes([]byte(raw), "functionResponse.name", fallbackName)
raw = string(updated)
}
return raw
}
log.Debugf("parse function response failed, using fallback")
funcResp := response.Get("functionResponse")
if funcResp.Exists() {
fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
name := funcResp.Get("name").String()
if strings.TrimSpace(name) == "" {
name = fallbackName
}
fr, _ = sjson.SetBytes(fr, "functionResponse.name", name)
fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", funcResp.Get("response").String())
if id := funcResp.Get("id").String(); id != "" {
fr, _ = sjson.SetBytes(fr, "functionResponse.id", id)
}
return string(fr)
}
useName := fallbackName
if useName == "" {
useName = "unknown"
}
fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
fr, _ = sjson.SetBytes(fr, "functionResponse.name", useName)
fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", response.String())
return string(fr)
}
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
// This function transforms the CLI tool response format by intelligently grouping function calls
// with their corresponding responses, ensuring proper conversation flow and API compatibility.
// It converts from a linear format (1.json) to a grouped format (2.json) where function calls
// and their responses are properly associated and structured.
//
// Parameters:
// - input: The input JSON string to be processed
//
// Returns:
// - string: The processed JSON string with grouped function calls and responses
// - error: An error if the processing fails
func fixCLIToolResponse(input []byte) ([]byte, error) {
// Parse the input JSON to extract the conversation structure.
// The parsed result references input directly; input must not be mutated
// while the result and its raw slices are still in use.
parsed := util.ParseGJSONBytesNoCopy(input)
// Extract the contents array which contains the conversation messages
contents := parsed.Get("request.contents")
if !contents.Exists() {
// log.Debugf(input)
return input, fmt.Errorf("contents not found in input")
}
needsGrouping := false
allContentsAreObjects := true
contents.ForEach(func(_, content gjson.Result) bool {
if !content.IsObject() {
allContentsAreObjects = false
return true
}
content.Get("parts").ForEach(func(_, part gjson.Result) bool {
if part.Get("functionResponse").Exists() {
needsGrouping = true
return false
}
return true
})
return !needsGrouping
})
if contents.IsArray() && allContentsAreObjects && !needsGrouping {
return input, nil
}
// Initialize data structures for processing and grouping
contentItems := translatorcommon.NewRawArrayItems(contents.Get("#").Int())
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
var collectedResponses []gjson.Result // Standalone responses to be matched
appendFunctionResponses := func(responses []gjson.Result, callNames []string) {
partItems := make([][]byte, 0, len(responses))
for responseIndex, response := range responses {
partRaw := parseFunctionResponseRaw(response, callNames[responseIndex])
if partRaw != "" {
partItems = append(partItems, []byte(partRaw))
}
}
if len(partItems) > 0 {
functionResponseContent := []byte(`{"parts":[],"role":"function"}`)
functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts", translatorcommon.JoinRawArray(partItems))
contentItems = append(contentItems, functionResponseContent)
}
}
// Process each content object in the conversation
// This iterates through messages and groups function calls with their responses
contents.ForEach(func(key, value gjson.Result) bool {
role := value.Get("role").String()
parts := value.Get("parts")
// Collect function responses and attach sibling inlineData to the nearest one.
responsePartsInThisContent := collectFunctionResponsesWithSiblingInlineData(parts)
// If this content has function responses, collect them
if len(responsePartsInThisContent) > 0 {
collectedResponses = append(collectedResponses, responsePartsInThisContent...)
// 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:]
// Take the needed responses for this group
groupResponses := collectedResponses[:group.ResponsesNeeded]
collectedResponses = collectedResponses[group.ResponsesNeeded:]
appendFunctionResponses(groupResponses, group.CallNames)
}
return true // Skip adding this content, responses are merged
}
// If this is a model with function calls, create a new group
if role == "model" {
var callNames []string
parts.ForEach(func(_, part gjson.Result) bool {
if part.Get("functionCall").Exists() {
callNames = append(callNames, part.Get("functionCall.name").String())
}
return true
})
if len(callNames) > 0 {
// Add the model content
if !value.IsObject() {
log.Warnf("failed to parse model content")
return true
}
contentItems = append(contentItems, []byte(value.Raw))
// Create a new group for tracking responses
group := &FunctionCallGroup{
ResponsesNeeded: len(callNames),
CallNames: callNames,
}
pendingGroups = append(pendingGroups, group)
} else {
// Regular model content without function calls
if !value.IsObject() {
log.Warnf("failed to parse content")
return true
}
contentItems = append(contentItems, []byte(value.Raw))
}
} else {
// Non-model content (user, etc.)
if !value.IsObject() {
log.Warnf("failed to parse content")
return true
}
contentItems = append(contentItems, []byte(value.Raw))
}
return true
})
// Handle any remaining pending groups with remaining responses
for _, group := range pendingGroups {
if len(collectedResponses) >= group.ResponsesNeeded {
groupResponses := collectedResponses[:group.ResponsesNeeded]
collectedResponses = collectedResponses[group.ResponsesNeeded:]
appendFunctionResponses(groupResponses, group.CallNames)
}
}
// Update the original JSON with the new contents
result, _ := sjson.SetRawBytes(input, "request.contents", translatorcommon.JoinRawArray(contentItems))
return result, nil
}