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,19 @@
package interactions
import (
. "github.com/router-for-me/CLIProxyAPI/v7/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v7/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v7/internal/translator/translator"
)
func init() {
translator.Register(
Interactions,
Antigravity,
ConvertInteractionsRequestToAntigravity,
interfaces.TranslateResponse{
Stream: ConvertAntigravityResponseToInteractions,
NonStream: ConvertAntigravityResponseToInteractionsNonStream,
},
)
}

View file

@ -0,0 +1,20 @@
package interactions
import (
"testing"
"github.com/tidwall/gjson"
)
func TestConvertInteractionsRequestToAntigravityNormalizesOpenAIFileDataURL(t *testing.T) {
input := []byte(`{"model":"gemini-3.5-flash","input":[{"type":"user_input","content":[{"type":"file","file":{"filename":"test.pdf","file_data":"data:application/pdf;base64,JVBERi0xLjQK"}}]}]}`)
out := ConvertInteractionsRequestToAntigravity("gemini-3.5-flash", input, false)
inlineData := gjson.GetBytes(out, "request.contents.0.parts.0.inlineData")
if got := inlineData.Get("mimeType").String(); got != "application/pdf" {
t.Fatalf("inlineData.mimeType = %q, want application/pdf. Output: %s", got, out)
}
if got := inlineData.Get("data").String(); got != "JVBERi0xLjQK" {
t.Fatalf("inlineData.data = %q, want raw base64 payload. Output: %s", got, out)
}
}

View file

@ -0,0 +1,793 @@
package interactions
import (
"encoding/json"
"fmt"
"strings"
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"
)
func ConvertInteractionsRequestToAntigravity(modelName string, inputRawJSON []byte, stream bool) []byte {
root := gjson.ParseBytes(inputRawJSON)
functionNameMap := util.SanitizedFunctionNameMap(inputRawJSON)
out := []byte(`{"project":"","request":{"contents":[]},"model":""}`)
out, _ = sjson.SetBytes(out, "model", modelName)
if stream || root.Get("stream").Bool() {
out, _ = sjson.SetBytes(out, "request.stream", true)
}
out = copyInteractionsSystemToAntigravity(out, root)
out = copyInteractionsGenerationConfigToAntigravity(out, root)
contentItems := translatorcommon.NewRawArrayItems(root.Get("input.#").Int())
appendInteractionsInputToAntigravity(&contentItems, root.Get("input"))
out = translatorcommon.SetRawArrayItems(out, "request.contents", contentItems)
out = copyInteractionsToolsToAntigravity(out, root, functionNameMap)
out = rewriteInteractionsFunctionNames(out, functionNameMap)
out = attachDefaultAntigravitySafetySettings(out)
return out
}
func rewriteInteractionsFunctionNames(out []byte, functionNameMap map[string]string) []byte {
contents := gjson.GetBytes(out, "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
})
}
if canBatchContents {
contentsChanged := false
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 []string{"functionCall", "functionResponse"} {
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))
contentsChanged = true
}
contentItems = append(contentItems, contentJSON)
return true
})
if contentsChanged {
out, _ = sjson.SetRawBytes(out, "request.contents", translatorcommon.JoinRawArray(contentItems))
}
} else {
for contentIndex, content := range contents.Array() {
for partIndex, part := range content.Get("parts").Array() {
for _, field := range []string{"functionCall", "functionResponse"} {
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)
out, _ = sjson.SetBytes(out, path, mappedName)
}
}
}
}
allowedPath := "request.toolConfig.functionCallingConfig.allowedFunctionNames"
allowedNames := gjson.GetBytes(out, 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 {
out, _ = sjson.SetRawBytes(out, 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)
out, _ = sjson.SetBytes(out, path, mappedName)
}
}
return out
}
func copyInteractionsSystemToAntigravity(out []byte, root gjson.Result) []byte {
sys := root.Get("system_instruction")
if !sys.Exists() {
return out
}
if sys.Type == gjson.String {
instr := []byte(`{"parts":[{"text":""}]}`)
instr, _ = sjson.SetBytes(instr, "parts.0.text", sys.String())
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", instr)
return out
}
if text := sys.Get("text"); text.Exists() && !sys.Get("parts").Exists() {
instr := []byte(`{"parts":[{"text":""}]}`)
instr, _ = sjson.SetBytes(instr, "parts.0.text", text.String())
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", instr)
return out
}
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", []byte(sys.Raw))
return out
}
func copyInteractionsGenerationConfigToAntigravity(out []byte, root gjson.Result) []byte {
if cfg := root.Get("generation_config"); cfg.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig", convertSnakeCaseKeysToCamelCaseForAntigravity([]byte(cfg.Raw)))
} else if cfg := root.Get("generationConfig"); cfg.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig", []byte(cfg.Raw))
}
out = normalizeInteractionsGenerationConfigForAntigravity(out)
out = copyInteractionsReasoningToAntigravity(out, root)
out = copyInteractionsResponseModalitiesToAntigravity(out, root)
out = copyInteractionsToolChoiceToAntigravity(out, root)
return out
}
func normalizeInteractionsGenerationConfigForAntigravity(out []byte) []byte {
if thinkingLevel := gjson.GetBytes(out, "request.generationConfig.thinkingLevel"); thinkingLevel.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", []byte(thinkingLevel.Raw))
out, _ = sjson.DeleteBytes(out, "request.generationConfig.thinkingLevel")
}
if thinkingBudget := gjson.GetBytes(out, "request.generationConfig.thinkingBudget"); thinkingBudget.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", []byte(thinkingBudget.Raw))
out, _ = sjson.DeleteBytes(out, "request.generationConfig.thinkingBudget")
}
if includeThoughts := gjson.GetBytes(out, "request.generationConfig.includeThoughts"); includeThoughts.Exists() {
out, _ = sjson.SetRawBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", []byte(includeThoughts.Raw))
out, _ = sjson.DeleteBytes(out, "request.generationConfig.includeThoughts")
}
if summaries := gjson.GetBytes(out, "request.generationConfig.thinkingSummaries"); summaries.Exists() {
if includeThoughts, ok := antigravityThinkingSummariesIncludeThoughts(summaries); ok {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
}
out, _ = sjson.DeleteBytes(out, "request.generationConfig.thinkingSummaries")
}
if toolChoice := gjson.GetBytes(out, "request.generationConfig.toolChoice"); toolChoice.Exists() {
out, _ = sjson.DeleteBytes(out, "request.generationConfig.toolChoice")
}
return out
}
func copyInteractionsReasoningToAntigravity(out []byte, root gjson.Result) []byte {
reasoning := root.Get("reasoning")
if !reasoning.Exists() {
return out
}
effort := strings.ToLower(strings.TrimSpace(reasoning.Get("effort").String()))
if effort == "" {
effort = strings.ToLower(strings.TrimSpace(reasoning.Get("thinking_level").String()))
}
if effort != "" {
// Thinking amount and summary visibility are independent. This OpenAI-style
// compatibility alias controls only the amount; includeThoughts is written
// below only for an explicit Interactions summary selector.
if effort == "auto" {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
} else {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
}
}
if summary := reasoning.Get("summary"); summary.Exists() {
if includeThoughts, ok := antigravityThinkingSummariesIncludeThoughts(summary); ok {
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", includeThoughts)
}
}
return out
}
func copyInteractionsResponseModalitiesToAntigravity(out []byte, root gjson.Result) []byte {
mods := root.Get("response_modalities")
if !mods.Exists() {
mods = root.Get("responseModalities")
}
if !mods.Exists() || !mods.IsArray() {
return out
}
var responseMods []string
mods.ForEach(func(_, mod gjson.Result) bool {
switch strings.ToLower(strings.TrimSpace(mod.String())) {
case "text":
responseMods = append(responseMods, "TEXT")
case "image":
responseMods = append(responseMods, "IMAGE")
case "audio":
responseMods = append(responseMods, "AUDIO")
}
return true
})
if len(responseMods) > 0 {
out, _ = sjson.SetBytes(out, "request.generationConfig.responseModalities", responseMods)
}
return out
}
func copyInteractionsToolChoiceToAntigravity(out []byte, root gjson.Result) []byte {
toolChoice := root.Get("tool_choice")
if !toolChoice.Exists() {
toolChoice = root.Get("generation_config.tool_choice")
}
if !toolChoice.Exists() {
toolChoice = root.Get("generationConfig.toolChoice")
}
if !toolChoice.Exists() {
return out
}
mode := ""
var allowedNames []string
if toolChoice.Type == gjson.String {
switch strings.ToLower(strings.TrimSpace(toolChoice.String())) {
case "none":
mode = "NONE"
case "auto":
mode = "AUTO"
case "required", "any":
mode = "ANY"
}
} else if toolChoice.IsObject() {
switch strings.ToLower(strings.TrimSpace(toolChoice.Get("type").String())) {
case "none":
mode = "NONE"
case "auto":
mode = "AUTO"
case "required", "any":
mode = "ANY"
case "function":
mode = "ANY"
if name := toolChoice.Get("function.name").String(); strings.TrimSpace(name) != "" {
allowedNames = append(allowedNames, name)
}
case "tool":
mode = "ANY"
if name := toolChoice.Get("name").String(); strings.TrimSpace(name) != "" {
allowedNames = append(allowedNames, name)
}
}
}
if mode == "" {
return out
}
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", mode)
if len(allowedNames) > 0 {
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", allowedNames)
}
return out
}
func appendInteractionsInputToAntigravity(items *[][]byte, input gjson.Result) {
if !input.Exists() {
return
}
if input.Type == gjson.String {
appendAntigravityTextContent(items, "user", input.String())
return
}
if input.IsArray() {
input.ForEach(func(_, item gjson.Result) bool {
appendInteractionsStepToAntigravity(items, item, "user")
return true
})
return
}
if steps := input.Get("steps"); steps.Exists() && steps.IsArray() {
defaultRole := "user"
if role := input.Get("role").String(); role == "model" || role == "assistant" {
defaultRole = "model"
}
steps.ForEach(func(_, step gjson.Result) bool {
appendInteractionsStepToAntigravity(items, step, defaultRole)
return true
})
return
}
appendInteractionsStepToAntigravity(items, input, "user")
}
func appendInteractionsStepToAntigravity(items *[][]byte, step gjson.Result, defaultRole string) {
if step.Type == gjson.String {
appendAntigravityTextContent(items, defaultRole, step.String())
return
}
if steps := step.Get("steps"); steps.Exists() && steps.IsArray() {
role := defaultRole
if itemRole := step.Get("role").String(); itemRole == "model" || itemRole == "assistant" {
role = "model"
} else if itemRole == "user" {
role = "user"
}
steps.ForEach(func(_, child gjson.Result) bool {
appendInteractionsStepToAntigravity(items, child, role)
return true
})
return
}
switch step.Get("type").String() {
case "model_output":
appendInteractionsStepContentToAntigravity(items, "model", step, false)
case "thought":
appendInteractionsStepContentToAntigravity(items, "model", step, true)
case "function_call":
appendInteractionsFunctionCallToAntigravity(items, step)
case "function_result":
appendInteractionsFunctionResultToAntigravity(items, step)
case "user_input", "":
if step.Get("parts").Exists() {
appendInteractionsNativeContentToAntigravity(items, step, defaultRole)
} else {
appendInteractionsContentListToAntigravity(items, defaultRole, step.Get("content"))
}
default:
if step.Get("parts").Exists() {
appendInteractionsNativeContentToAntigravity(items, step, defaultRole)
} else if step.Get("content").Exists() {
appendInteractionsContentListToAntigravity(items, defaultRole, step.Get("content"))
} else if text := step.Get("text"); text.Exists() {
appendAntigravityTextContent(items, defaultRole, text.String())
}
}
}
func appendInteractionsNativeContentToAntigravity(items *[][]byte, step gjson.Result, defaultRole string) {
parts := step.Get("parts")
if !parts.Exists() || !parts.IsArray() {
return
}
partItems := make([][]byte, 0, 4)
parts.ForEach(func(_, part gjson.Result) bool {
if partJSON := interactionsNativeAntigravityPart(part); len(partJSON) > 0 {
partItems = append(partItems, partJSON)
}
return true
})
if len(partItems) > 0 {
role := antigravityContentRole(step.Get("role").String(), defaultRole)
*items = append(*items, antigravityContent(role, partItems))
}
}
func appendInteractionsStepContentToAntigravity(items *[][]byte, role string, step gjson.Result, thought bool) {
content := step.Get("content")
if !content.Exists() {
return
}
partItems := make([][]byte, 0, 4)
if content.IsArray() {
content.ForEach(func(_, part gjson.Result) bool {
if partJSON := appendInteractionsContentToAntigravityPart(nil, part, thought); len(partJSON) > 0 {
partItems = append(partItems, partJSON)
}
return true
})
} else if content.IsObject() {
if partJSON := appendInteractionsContentToAntigravityPart(nil, content, thought); len(partJSON) > 0 {
partItems = append(partItems, partJSON)
}
} else if content.Type == gjson.String {
partItems = append(partItems, antigravityTextPartJSON(content.String(), thought))
}
if len(partItems) > 0 {
*items = append(*items, antigravityContent(role, partItems))
}
}
func appendInteractionsContentListToAntigravity(items *[][]byte, role string, content gjson.Result) {
if !content.Exists() {
return
}
if content.IsArray() {
content.ForEach(func(_, part gjson.Result) bool {
appendInteractionsContentPartToAntigravity(items, role, part)
return true
})
return
}
if content.IsObject() {
appendInteractionsContentPartToAntigravity(items, role, content)
} else if content.Type == gjson.String {
appendAntigravityTextContent(items, role, content.String())
}
}
func appendInteractionsContentPartToAntigravity(items *[][]byte, role string, part gjson.Result) {
partJSON := appendInteractionsContentToAntigravityPart(nil, part, false)
if len(partJSON) > 0 {
*items = append(*items, antigravityContent(role, [][]byte{partJSON}))
}
}
func appendInteractionsContentToAntigravityPart(_ []byte, content gjson.Result, thought bool) []byte {
if text := content.Get("text"); text.Exists() {
return antigravityTextPartJSON(text.String(), thought)
}
if inline := content.Get("inline_data"); inline.Exists() {
return antigravityInlineDataPartJSON(inline)
}
if inline := content.Get("inlineData"); inline.Exists() {
return antigravityInlineDataPartJSON(inline)
}
switch strings.ToLower(strings.TrimSpace(content.Get("type").String())) {
case "text":
if text := content.Get("text"); text.Exists() {
return antigravityTextPartJSON(text.String(), thought)
}
case "image", "audio", "video", "document":
if mime := content.Get("mime_type"); mime.Exists() || content.Get("mimeType").Exists() {
mimeType := mime.String()
if mimeType == "" {
mimeType = content.Get("mimeType").String()
}
if data := content.Get("data").String(); data != "" {
return antigravityInlineDataPartJSON(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, data)))
}
}
if uri := content.Get("file_uri"); uri.Exists() || content.Get("fileUri").Exists() {
fileURI := uri.String()
if fileURI == "" {
fileURI = content.Get("fileUri").String()
}
mimeType := content.Get("mime_type").String()
if mimeType == "" {
mimeType = content.Get("mimeType").String()
}
return antigravityFileDataPartJSON(gjson.Parse(fmt.Sprintf(`{"mimeType":%q,"fileUri":%q}`, mimeType, fileURI)))
}
if url := content.Get("url"); url.Exists() {
return antigravityInlineDataPartFromDataURL(url.String())
}
case "image_url":
return antigravityInlineDataPartFromDataURL(content.Get("image_url.url").String())
case "input_audio":
mimeType := antigravityInputAudioMimeType(content.Get("input_audio.format").String())
return antigravityInlineDataPartJSON(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, content.Get("input_audio.data").String())))
case "file":
filename := content.Get("file.filename").String()
fileData := content.Get("file.file_data").String()
if mimeType, data, ok := translatorcommon.NormalizeOpenAIFileData(filename, "", fileData); ok {
return antigravityInlineDataPartJSON(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, mimeType, data)))
}
}
return nil
}
func appendInteractionsFunctionCallToAntigravity(items *[][]byte, step gjson.Result) {
part := []byte(`{"functionCall":{"name":"","args":{}}}`)
part, _ = sjson.SetBytes(part, "functionCall.name", step.Get("name").String())
if callID := step.Get("call_id"); callID.Exists() {
part, _ = sjson.SetBytes(part, "functionCall.id", callID.String())
} else if id := step.Get("id"); id.Exists() {
part, _ = sjson.SetBytes(part, "functionCall.id", id.String())
}
if args := step.Get("arguments"); args.Exists() {
part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(args.Raw))
}
*items = append(*items, antigravityContent("model", [][]byte{part}))
}
func appendInteractionsFunctionResultToAntigravity(items *[][]byte, step gjson.Result) {
part := []byte(`{"functionResponse":{"name":"","response":{}}}`)
part, _ = sjson.SetBytes(part, "functionResponse.name", step.Get("name").String())
if callID := step.Get("call_id"); callID.Exists() {
part, _ = sjson.SetBytes(part, "functionResponse.id", callID.String())
} else if id := step.Get("id"); id.Exists() {
part, _ = sjson.SetBytes(part, "functionResponse.id", id.String())
}
if result := step.Get("result"); result.Exists() {
part, _ = sjson.SetRawBytes(part, "functionResponse.response", []byte(result.Raw))
}
*items = append(*items, antigravityContent("user", [][]byte{part}))
}
func copyInteractionsToolsToAntigravity(out []byte, root gjson.Result, functionNameMap map[string]string) []byte {
tools := root.Get("tools")
if !tools.Exists() {
return out
}
if !tools.IsArray() {
out, _ = sjson.SetRawBytes(out, "request.tools", []byte(tools.Raw))
return out
}
var functionDeclarations [][]byte
var otherTools [][]byte
tools.ForEach(func(_, tool gjson.Result) bool {
if decls := tool.Get("functionDeclarations"); decls.Exists() && decls.IsArray() {
decls.ForEach(func(_, decl gjson.Result) bool {
if converted := antigravityFunctionDeclarationJSON(decl, functionNameMap); len(converted) > 0 {
functionDeclarations = append(functionDeclarations, converted)
}
return true
})
return true
}
if decls := tool.Get("function_declarations"); decls.Exists() && decls.IsArray() {
decls.ForEach(func(_, decl gjson.Result) bool {
if converted := antigravityFunctionDeclarationJSON(decl, functionNameMap); len(converted) > 0 {
functionDeclarations = append(functionDeclarations, converted)
}
return true
})
return true
}
if tool.Get("type").String() == "function" || tool.Get("name").Exists() {
if converted := antigravityFunctionDeclarationJSON(tool, functionNameMap); len(converted) > 0 {
functionDeclarations = append(functionDeclarations, converted)
}
return true
}
otherTools = append(otherTools, []byte(tool.Raw))
return true
})
deduplicated := util.DeduplicateFunctionDeclarations(translatorcommon.JoinRawArray(functionDeclarations))
hasFunction := len(deduplicated) > 2
if hasFunction || len(otherTools) > 0 {
toolItems := make([][]byte, 0, 1+len(otherTools))
if hasFunction {
functionToolNode := []byte(`{"functionDeclarations":[]}`)
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", deduplicated)
toolItems = append(toolItems, functionToolNode)
}
toolItems = append(toolItems, otherTools...)
out, _ = sjson.SetRawBytes(out, "request.tools", translatorcommon.JoinRawArray(toolItems))
}
return out
}
func antigravityFunctionDeclarationJSON(decl gjson.Result, functionNameMap map[string]string) []byte {
fn := decl
if nested := decl.Get("function"); nested.Exists() && nested.IsObject() {
fn = nested
}
name := fn.Get("name").String()
if strings.TrimSpace(name) == "" {
return nil
}
out := []byte(`{"name":"","parametersJsonSchema":{"type":"object","properties":{}}}`)
out, _ = sjson.SetBytes(out, "name", util.MapSanitizedFunctionName(functionNameMap, name))
if desc := fn.Get("description"); desc.Exists() {
out, _ = sjson.SetBytes(out, "description", desc.String())
}
if params := fn.Get("parametersJsonSchema"); params.Exists() {
out, _ = sjson.SetRawBytes(out, "parametersJsonSchema", []byte(params.Raw))
} else if params := fn.Get("parameters"); params.Exists() {
out, _ = sjson.SetRawBytes(out, "parametersJsonSchema", []byte(params.Raw))
}
if response := fn.Get("response"); response.Exists() {
out, _ = sjson.SetRawBytes(out, "response", []byte(response.Raw))
}
if responseSchema := fn.Get("responseJsonSchema"); responseSchema.Exists() {
out, _ = sjson.SetRawBytes(out, "responseJsonSchema", []byte(responseSchema.Raw))
}
return out
}
func interactionsNativeAntigravityPart(part gjson.Result) []byte {
switch {
case part.Get("text").Exists(), part.Get("functionCall").Exists(), part.Get("functionResponse").Exists():
return []byte(part.Raw)
case part.Get("inlineData").Exists():
return antigravityInlineDataPartJSON(part.Get("inlineData"))
case part.Get("fileData").Exists():
return antigravityFileDataPartJSON(part.Get("fileData"))
case part.Get("inline_data").Exists():
return antigravityInlineDataPartJSON(part.Get("inline_data"))
case part.Get("file_data").Exists():
return antigravityFileDataPartJSON(part.Get("file_data"))
}
return nil
}
func antigravityTextPartJSON(text string, thought bool) []byte {
partJSON := []byte(`{"text":""}`)
partJSON, _ = sjson.SetBytes(partJSON, "text", text)
if thought {
partJSON, _ = sjson.SetBytes(partJSON, "thought", true)
}
return partJSON
}
func antigravityInlineDataPartJSON(inline gjson.Result) []byte {
mimeType := inline.Get("mimeType").String()
if mimeType == "" {
mimeType = inline.Get("mime_type").String()
}
data := inline.Get("data").String()
if mimeType == "" || data == "" {
return nil
}
partJSON := []byte(`{"inlineData":{"mimeType":"","data":""}}`)
partJSON, _ = sjson.SetBytes(partJSON, "inlineData.mimeType", mimeType)
partJSON, _ = sjson.SetBytes(partJSON, "inlineData.data", data)
return partJSON
}
func antigravityFileDataPartJSON(fileData gjson.Result) []byte {
mimeType := fileData.Get("mimeType").String()
if mimeType == "" {
mimeType = fileData.Get("mime_type").String()
}
fileURI := fileData.Get("fileUri").String()
if fileURI == "" {
fileURI = fileData.Get("file_uri").String()
}
if mimeType == "" || fileURI == "" {
return nil
}
partJSON := []byte(`{"fileData":{"mimeType":"","fileUri":""}}`)
partJSON, _ = sjson.SetBytes(partJSON, "fileData.mimeType", mimeType)
partJSON, _ = sjson.SetBytes(partJSON, "fileData.fileUri", fileURI)
return partJSON
}
func antigravityInlineDataPartFromDataURL(dataURL string) []byte {
if !strings.HasPrefix(dataURL, "data:") {
return nil
}
payload := dataURL[5:]
pieces := strings.SplitN(payload, ";", 2)
if len(pieces) != 2 || !strings.HasPrefix(pieces[1], "base64,") {
return nil
}
return antigravityInlineDataPartJSON(gjson.Parse(fmt.Sprintf(`{"mime_type":%q,"data":%q}`, pieces[0], pieces[1][7:])))
}
func appendAntigravityTextContent(items *[][]byte, role, text string) {
part := antigravityTextPartJSON(text, false)
*items = append(*items, antigravityContent(antigravityContentRole(role, "user"), [][]byte{part}))
}
func antigravityContent(role string, parts [][]byte) []byte {
content := []byte(`{"role":"","parts":[]}`)
content, _ = sjson.SetBytes(content, "role", role)
content, _ = sjson.SetRawBytes(content, "parts", translatorcommon.JoinRawArray(parts))
return content
}
func antigravityContentRole(role, defaultRole string) string {
switch strings.ToLower(strings.TrimSpace(role)) {
case "model", "assistant":
return "model"
case "user":
return "user"
}
if defaultRole == "model" {
return "model"
}
return "user"
}
func antigravityInputAudioMimeType(format string) string {
switch strings.ToLower(strings.TrimSpace(format)) {
case "wav":
return "audio/wav"
case "mp3":
return "audio/mpeg"
case "flac":
return "audio/flac"
case "opus":
return "audio/opus"
case "pcm16":
return "audio/pcm"
default:
return "audio/mpeg"
}
}
func antigravityThinkingSummariesIncludeThoughts(summary gjson.Result) (bool, bool) {
if summary.Type != gjson.String {
return false, false
}
switch strings.ToLower(strings.TrimSpace(summary.String())) {
case "auto":
return true, true
case "none":
return false, true
default:
return false, false
}
}
func convertSnakeCaseKeysToCamelCaseForAntigravity(raw []byte) []byte {
root := gjson.ParseBytes(raw)
if !root.Exists() {
return raw
}
out := []byte(`{}`)
out = copySnakeCaseValueToCamelCaseForAntigravity(out, "", root)
return out
}
func copySnakeCaseValueToCamelCaseForAntigravity(out []byte, path string, node gjson.Result) []byte {
if node.IsObject() {
node.ForEach(func(key, value gjson.Result) bool {
childPath := joinAntigravityJSONPath(path, toAntigravityCamelCase(key.String()))
out = copySnakeCaseValueToCamelCaseForAntigravity(out, childPath, value)
return true
})
return out
}
if node.IsArray() {
node.ForEach(func(_, value gjson.Result) bool {
out = copySnakeCaseValueToCamelCaseForAntigravity(out, path+".-1", value)
return true
})
return out
}
out, _ = sjson.SetRawBytes(out, path, []byte(node.Raw))
return out
}
func joinAntigravityJSONPath(path, key string) string {
if path == "" {
return key
}
return path + "." + key
}
func toAntigravityCamelCase(s string) string {
parts := strings.Split(s, "_")
if len(parts) == 0 {
return s
}
out := parts[0]
for _, part := range parts[1:] {
if part == "" {
continue
}
out += strings.ToUpper(part[:1]) + part[1:]
}
return out
}
func attachDefaultAntigravitySafetySettings(out []byte) []byte {
if gjson.GetBytes(out, "request.safetySettings").Exists() {
return out
}
settings := []map[string]string{
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"},
{"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"},
}
raw, errMarshal := json.Marshal(settings)
if errMarshal != nil {
return out
}
out, _ = sjson.SetRawBytes(out, "request.safetySettings", raw)
return out
}

View file

@ -0,0 +1,494 @@
package interactions
import (
"bytes"
"context"
"fmt"
"strings"
"time"
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"
)
type antigravityToInteractionsStreamState struct {
Started bool
Finished bool
Completed bool
Done bool
ActiveStepOpen bool
ID string
StepID string
ActiveStepType string
ActiveStepIndex int
StepIndex int
ToolNameMap map[string]string
}
func ConvertAntigravityResponseToInteractions(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
_ = ctx
_ = originalRequestRawJSON
_ = requestRawJSON
if param == nil {
var local any
param = &local
}
if *param == nil {
*param = &antigravityToInteractionsStreamState{
ID: fmt.Sprintf("interaction_%d", time.Now().UnixNano()),
ToolNameMap: util.DisambiguatedToolNameMap(originalRequestRawJSON),
}
}
st := (*param).(*antigravityToInteractionsStreamState)
payloads := antigravityStreamPayloads(rawJSON)
out := make([][]byte, 0)
for _, payload := range payloads {
if bytes.Equal(bytes.TrimSpace(payload), []byte("[DONE]")) {
if !st.Completed {
out = appendAntigravityInteractionsStepStop(out, st)
out = appendAntigravityInteractionsCompleted(out, st, modelName, gjson.Result{})
}
out = appendAntigravityInteractionsDone(out, st)
continue
}
root := unwrapAntigravityResponse(gjson.ParseBytes(payload))
root = restoreInteractionsFunctionNames(root, st.ToolNameMap)
if !root.Exists() {
continue
}
if !st.Started {
out = appendAntigravityInteractionsCreated(out, st, modelName)
out = appendAntigravityInteractionsStatusUpdate(out, st)
st.Started = true
}
root.Get("candidates.0.content.parts").ForEach(func(_, part gjson.Result) bool {
out = appendAntigravityPartToInteractionsStream(out, st, part)
return true
})
hasFinish := root.Get("candidates.0.finishReason").Exists()
hasUsage := hasAntigravityStreamUsage(root)
if hasFinish && !st.Finished {
out = appendAntigravityInteractionsStepStop(out, st)
st.Finished = true
}
if hasUsage && st.Finished && !st.Completed {
out = appendAntigravityInteractionsCompleted(out, st, modelName, root)
}
}
return out
}
func ConvertAntigravityResponseToInteractionsNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
_ = ctx
_ = originalRequestRawJSON
_ = requestRawJSON
root := unwrapAntigravityResponse(gjson.ParseBytes(rawJSON))
root = restoreInteractionsFunctionNames(root, util.DisambiguatedToolNameMap(originalRequestRawJSON))
out := []byte(`{"id":"","object":"interaction","status":"completed","model":"","steps":[]}`)
id := root.Get("responseId").String()
if id == "" {
id = fmt.Sprintf("interaction_%d", time.Now().UnixNano())
}
out, _ = sjson.SetBytes(out, "id", id)
out, _ = sjson.SetBytes(out, "model", modelName)
var steps [][]byte
root.Get("candidates.0.content.parts").ForEach(func(_, part gjson.Result) bool {
if step := antigravityPartToInteractionsStep(part); len(step) > 0 {
steps = append(steps, step)
}
return true
})
if len(steps) > 0 {
out = translatorcommon.SetRawArrayItems(out, "steps", steps)
}
out = setInteractionsUsageFromAntigravity(out, "usage", root)
return out
}
func antigravityStreamPayloads(rawJSON []byte) [][]byte {
trimmed := bytes.TrimSpace(rawJSON)
if bytes.HasPrefix(trimmed, []byte("data:")) {
return [][]byte{bytes.TrimSpace(trimmed[5:])}
}
root := gjson.ParseBytes(trimmed)
if root.IsArray() {
payloads := make([][]byte, 0)
root.ForEach(func(_, item gjson.Result) bool {
if response := item.Get("response"); response.Exists() {
payloads = append(payloads, []byte(response.Raw))
} else if item.Exists() {
payloads = append(payloads, []byte(item.Raw))
}
return true
})
if len(payloads) > 0 {
return payloads
}
}
return [][]byte{trimmed}
}
func unwrapAntigravityResponse(root gjson.Result) gjson.Result {
if response := root.Get("response"); response.Exists() {
response = restoreAntigravityUsageMetadata(response)
return response
}
return restoreAntigravityUsageMetadata(root)
}
func restoreInteractionsFunctionNames(root gjson.Result, nameMap map[string]string) gjson.Result {
if !root.Exists() || len(nameMap) == 0 {
return root
}
raw := []byte(root.Raw)
candidates := root.Get("candidates")
for candidateIndex, candidate := range candidates.Array() {
for partIndex, part := range candidate.Get("content.parts").Array() {
for _, field := range []string{"functionCall", "functionResponse"} {
nameResult := part.Get(field + ".name")
name := nameResult.String()
if name == "" {
continue
}
restoredName := util.RestoreSanitizedToolName(nameMap, name)
if nameResult.Type == gjson.String && restoredName == name {
continue
}
path := fmt.Sprintf("candidates.%d.content.parts.%d.%s.name", candidateIndex, partIndex, field)
raw, _ = sjson.SetBytes(raw, path, restoredName)
}
}
}
return gjson.ParseBytes(raw)
}
func restoreAntigravityUsageMetadata(root gjson.Result) gjson.Result {
if !root.Get("usageMetadata").Exists() {
if cpaUsage := root.Get("cpaUsageMetadata"); cpaUsage.Exists() {
raw, _ := sjson.SetRawBytes([]byte(root.Raw), "usageMetadata", []byte(cpaUsage.Raw))
raw, _ = sjson.DeleteBytes(raw, "cpaUsageMetadata")
return gjson.ParseBytes(raw)
}
}
return root
}
func appendAntigravityInteractionsCreated(out [][]byte, st *antigravityToInteractionsStreamState, modelName string) [][]byte {
created := []byte(`{"interaction":{"id":"","status":"in_progress","object":"interaction","model":""},"event_type":"interaction.created"}`)
created, _ = sjson.SetBytes(created, "interaction.id", st.ID)
created, _ = sjson.SetBytes(created, "interaction.model", modelName)
return append(out, translatorcommon.SSEEventData("interaction.created", created))
}
func appendAntigravityInteractionsStatusUpdate(out [][]byte, st *antigravityToInteractionsStreamState) [][]byte {
statusUpdate := []byte(`{"interaction_id":"","status":"in_progress","event_type":"interaction.status_update"}`)
statusUpdate, _ = sjson.SetBytes(statusUpdate, "interaction_id", st.ID)
return append(out, translatorcommon.SSEEventData("interaction.status_update", statusUpdate))
}
func appendAntigravityInteractionsCompleted(out [][]byte, st *antigravityToInteractionsStreamState, modelName string, root gjson.Result) [][]byte {
now := time.Now().UTC().Format(time.RFC3339)
completed := []byte(`{"interaction":{"id":"","status":"completed","usage":{},"created":"","updated":"","service_tier":"standard","object":"interaction","model":""},"event_type":"interaction.completed"}`)
completed, _ = sjson.SetBytes(completed, "interaction.id", st.ID)
completed, _ = sjson.SetBytes(completed, "interaction.created", now)
completed, _ = sjson.SetBytes(completed, "interaction.updated", now)
completed, _ = sjson.SetBytes(completed, "interaction.model", modelName)
if root.Exists() {
completed = setInteractionsStreamUsageFromAntigravity(completed, "interaction.usage", root)
}
out = append(out, translatorcommon.SSEEventData("interaction.completed", completed))
st.Completed = true
return out
}
func appendAntigravityInteractionsDone(out [][]byte, st *antigravityToInteractionsStreamState) [][]byte {
if st.Done {
return out
}
out = append(out, translatorcommon.SSEEventData("done", []byte("[DONE]")))
st.Done = true
return out
}
func appendAntigravityInteractionsStepStart(out [][]byte, st *antigravityToInteractionsStreamState, stepType string, part gjson.Result) [][]byte {
st.StepID = fmt.Sprintf("step_%d", time.Now().UnixNano())
st.ActiveStepIndex = st.StepIndex
st.StepIndex++
st.ActiveStepType = stepType
st.ActiveStepOpen = true
stepStart := []byte(`{"index":0,"step":{"type":""},"event_type":"step.start"}`)
stepStart, _ = sjson.SetBytes(stepStart, "index", st.ActiveStepIndex)
stepStart, _ = sjson.SetBytes(stepStart, "step.type", stepType)
if stepType == "function_call" {
id := antigravityFunctionPartID(part)
if id == "" {
id = st.StepID
}
stepStart, _ = sjson.SetBytes(stepStart, "step.id", id)
stepStart, _ = sjson.SetBytes(stepStart, "step.call_id", id)
stepStart, _ = sjson.SetBytes(stepStart, "step.name", part.Get("name").String())
stepStart, _ = sjson.SetRawBytes(stepStart, "step.arguments", []byte(`{}`))
}
return append(out, translatorcommon.SSEEventData("step.start", stepStart))
}
func appendAntigravityInteractionsStepStop(out [][]byte, st *antigravityToInteractionsStreamState) [][]byte {
if !st.ActiveStepOpen {
return out
}
stepStop := []byte(`{"index":0,"event_type":"step.stop"}`)
stepStop, _ = sjson.SetBytes(stepStop, "index", st.ActiveStepIndex)
out = append(out, translatorcommon.SSEEventData("step.stop", stepStop))
st.ActiveStepOpen = false
st.ActiveStepType = ""
return out
}
func ensureAntigravityInteractionsStep(out [][]byte, st *antigravityToInteractionsStreamState, stepType string, part gjson.Result) [][]byte {
if st.ActiveStepOpen && st.ActiveStepType == stepType {
return out
}
out = appendAntigravityInteractionsStepStop(out, st)
return appendAntigravityInteractionsStepStart(out, st, stepType, part)
}
func appendAntigravityPartToInteractionsStream(out [][]byte, st *antigravityToInteractionsStreamState, part gjson.Result) [][]byte {
if text := part.Get("text"); text.Exists() && text.String() != "" {
if part.Get("thought").Bool() {
out = ensureAntigravityInteractionsStep(out, st, "thought", gjson.Result{})
delta := []byte(`{"index":0,"delta":{"content":{"text":"","type":"text"},"type":"thought_summary"},"event_type":"step.delta"}`)
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
delta, _ = sjson.SetBytes(delta, "delta.content.text", text.String())
out = append(out, translatorcommon.SSEEventData("step.delta", delta))
return appendAntigravityThoughtSignature(out, st, part)
}
out = ensureAntigravityInteractionsStep(out, st, "model_output", gjson.Result{})
delta := []byte(`{"index":0,"delta":{"text":"","type":"text"},"event_type":"step.delta"}`)
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
delta, _ = sjson.SetBytes(delta, "delta.text", text.String())
return append(out, translatorcommon.SSEEventData("step.delta", delta))
}
if fc := part.Get("functionCall"); fc.Exists() {
out = appendAntigravityThoughtSignature(out, st, part)
out = ensureAntigravityInteractionsStep(out, st, "function_call", fc)
delta := []byte(`{"index":0,"delta":{"arguments":"","type":"arguments_delta"},"event_type":"step.delta"}`)
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
arguments := `{}`
if args := fc.Get("args"); args.Exists() {
arguments = args.Raw
}
delta, _ = sjson.SetBytes(delta, "delta.arguments", arguments)
out = append(out, translatorcommon.SSEEventData("step.delta", delta))
return appendAntigravityInteractionsStepStop(out, st)
}
if fr := part.Get("functionResponse"); fr.Exists() {
out = ensureAntigravityInteractionsStep(out, st, "function_result", fr)
delta := []byte(`{"index":0,"delta":{"type":"function_result","name":"","result":{}},"event_type":"step.delta"}`)
delta, _ = sjson.SetBytes(delta, "index", st.ActiveStepIndex)
delta, _ = sjson.SetBytes(delta, "delta.name", fr.Get("name").String())
if response := fr.Get("response"); response.Exists() {
delta, _ = sjson.SetRawBytes(delta, "delta.result", []byte(response.Raw))
}
out = append(out, translatorcommon.SSEEventData("step.delta", delta))
return appendAntigravityInteractionsStepStop(out, st)
}
return out
}
func appendAntigravityThoughtSignature(out [][]byte, st *antigravityToInteractionsStreamState, part gjson.Result) [][]byte {
if signature := antigravityThoughtSignature(part); signature != "" {
out = ensureAntigravityInteractionsStep(out, st, "thought", gjson.Result{})
signatureDelta := []byte(`{"index":0,"delta":{"signature":"","type":"thought_signature"},"event_type":"step.delta"}`)
signatureDelta, _ = sjson.SetBytes(signatureDelta, "index", st.ActiveStepIndex)
signatureDelta, _ = sjson.SetBytes(signatureDelta, "delta.signature", signature)
return append(out, translatorcommon.SSEEventData("step.delta", signatureDelta))
}
return out
}
func antigravityPartToInteractionsStep(part gjson.Result) []byte {
if fc := part.Get("functionCall"); fc.Exists() {
step := []byte(`{"type":"function_call","name":"","arguments":{}}`)
step, _ = sjson.SetBytes(step, "name", fc.Get("name").String())
if id := fc.Get("id"); id.Exists() {
step, _ = sjson.SetBytes(step, "call_id", id.String())
} else if callID := fc.Get("call_id"); callID.Exists() {
step, _ = sjson.SetBytes(step, "call_id", callID.String())
}
if args := fc.Get("args"); args.Exists() {
step, _ = sjson.SetRawBytes(step, "arguments", []byte(args.Raw))
}
return step
}
if fr := part.Get("functionResponse"); fr.Exists() {
step := []byte(`{"type":"function_result","name":"","result":{}}`)
step, _ = sjson.SetBytes(step, "name", fr.Get("name").String())
if id := fr.Get("id"); id.Exists() {
step, _ = sjson.SetBytes(step, "call_id", id.String())
} else if callID := fr.Get("call_id"); callID.Exists() {
step, _ = sjson.SetBytes(step, "call_id", callID.String())
}
if response := fr.Get("response"); response.Exists() {
step, _ = sjson.SetRawBytes(step, "result", []byte(response.Raw))
}
return step
}
if text := part.Get("text"); text.Exists() {
step := []byte(`{"type":"model_output","content":[]}`)
if part.Get("thought").Bool() {
step, _ = sjson.SetBytes(step, "type", "thought")
}
item := []byte(`{"type":"text","text":""}`)
item, _ = sjson.SetBytes(item, "text", text.String())
step = translatorcommon.SetRawArrayItems(step, "content", [][]byte{item})
return step
}
if inline := part.Get("inlineData"); inline.Exists() {
return antigravityInlineDataToInteractionsStep(inline)
}
if inline := part.Get("inline_data"); inline.Exists() {
return antigravityInlineDataToInteractionsStep(inline)
}
return nil
}
func antigravityInlineDataToInteractionsStep(inline gjson.Result) []byte {
mimeType := inline.Get("mimeType").String()
if mimeType == "" {
mimeType = inline.Get("mime_type").String()
}
data := inline.Get("data").String()
if mimeType == "" || data == "" {
return nil
}
contentType := "document"
lower := strings.ToLower(mimeType)
switch {
case strings.HasPrefix(lower, "image/"):
contentType = "image"
case strings.HasPrefix(lower, "audio/"):
contentType = "audio"
case strings.HasPrefix(lower, "video/"):
contentType = "video"
}
item := []byte(`{"type":"","mime_type":"","data":""}`)
item, _ = sjson.SetBytes(item, "type", contentType)
item, _ = sjson.SetBytes(item, "mime_type", mimeType)
item, _ = sjson.SetBytes(item, "data", data)
step := []byte(`{"type":"model_output","content":[]}`)
step, _ = sjson.SetRawBytes(step, "content.-1", item)
return step
}
func hasAntigravityStreamUsage(root gjson.Result) bool {
usage := antigravityUsageNode(root)
if !usage.Exists() {
return false
}
for _, path := range []string{
"promptTokenCount",
"candidatesTokenCount",
"totalTokenCount",
"thoughtsTokenCount",
"cachedContentTokenCount",
"prompt_token_count",
"candidates_token_count",
"total_token_count",
"thoughts_token_count",
"cached_content_token_count",
} {
if usage.Get(path).Exists() {
return true
}
}
return false
}
func setInteractionsUsageFromAntigravity(out []byte, path string, root gjson.Result) []byte {
usage := antigravityUsageNode(root)
if !usage.Exists() {
return out
}
out, _ = sjson.SetBytes(out, path+".input_tokens", firstAntigravityUsageInt(usage, "promptTokenCount", "prompt_token_count"))
out, _ = sjson.SetBytes(out, path+".output_tokens", firstAntigravityUsageInt(usage, "candidatesTokenCount", "candidates_token_count"))
if antigravityUsagePathExists(usage, "thoughtsTokenCount", "thoughts_token_count") {
out, _ = sjson.SetBytes(out, path+".reasoning_tokens", firstAntigravityUsageInt(usage, "thoughtsTokenCount", "thoughts_token_count"))
}
out, _ = sjson.SetBytes(out, path+".total_tokens", firstAntigravityUsageInt(usage, "totalTokenCount", "total_token_count"))
if antigravityUsagePathExists(usage, "cachedContentTokenCount", "cached_content_token_count") {
out, _ = sjson.SetBytes(out, path+".cached_tokens", firstAntigravityUsageInt(usage, "cachedContentTokenCount", "cached_content_token_count"))
}
return out
}
func setInteractionsStreamUsageFromAntigravity(out []byte, path string, root gjson.Result) []byte {
usage := antigravityUsageNode(root)
if !usage.Exists() {
return out
}
inputTokens := firstAntigravityUsageInt(usage, "promptTokenCount", "prompt_token_count")
outputTokens := firstAntigravityUsageInt(usage, "candidatesTokenCount", "candidates_token_count")
totalTokens := firstAntigravityUsageInt(usage, "totalTokenCount", "total_token_count")
thoughtTokens := firstAntigravityUsageInt(usage, "thoughtsTokenCount", "thoughts_token_count")
cachedTokens := firstAntigravityUsageInt(usage, "cachedContentTokenCount", "cached_content_token_count")
out, _ = sjson.SetBytes(out, path+".total_tokens", totalTokens)
out, _ = sjson.SetBytes(out, path+".total_input_tokens", inputTokens)
out, _ = sjson.SetRawBytes(out, path+".input_tokens_by_modality", []byte(fmt.Sprintf(`[{"modality":"text","tokens":%d}]`, inputTokens)))
out, _ = sjson.SetBytes(out, path+".total_cached_tokens", cachedTokens)
out, _ = sjson.SetBytes(out, path+".total_output_tokens", outputTokens)
out, _ = sjson.SetBytes(out, path+".total_tool_use_tokens", 0)
out, _ = sjson.SetBytes(out, path+".total_thought_tokens", thoughtTokens)
return out
}
func antigravityUsageNode(root gjson.Result) gjson.Result {
if usage := root.Get("usageMetadata"); usage.Exists() {
return usage
}
if usage := root.Get("usage_metadata"); usage.Exists() {
return usage
}
if usage := root.Get("cpaUsageMetadata"); usage.Exists() {
return usage
}
return gjson.Result{}
}
func firstAntigravityUsageInt(usage gjson.Result, paths ...string) int64 {
for _, path := range paths {
if value := usage.Get(path); value.Exists() {
return value.Int()
}
}
return 0
}
func antigravityUsagePathExists(usage gjson.Result, paths ...string) bool {
for _, path := range paths {
if usage.Get(path).Exists() {
return true
}
}
return false
}
func antigravityFunctionPartID(part gjson.Result) string {
if id := part.Get("id"); id.Exists() {
return id.String()
}
if callID := part.Get("call_id"); callID.Exists() {
return callID.String()
}
return ""
}
func antigravityThoughtSignature(part gjson.Result) string {
for _, path := range []string{"thoughtSignature", "thought_signature", "extra_content.google.thought_signature"} {
if signature := strings.TrimSpace(part.Get(path).String()); signature != "" {
return signature
}
}
return ""
}

View file

@ -0,0 +1,216 @@
package interactions
import (
"bytes"
"context"
"testing"
"github.com/router-for-me/CLIProxyAPI/v7/internal/util"
"github.com/tidwall/gjson"
)
func TestConvertInteractionsRequestToAntigravityWithToolMessagesDirect(t *testing.T) {
out := ConvertInteractionsRequestToAntigravity("antigravity-test", []byte(`{"model":"antigravity-test","system_instruction":"be brief","input":[{"type":"user_input","content":[{"type":"text","text":"hi"}]},{"type":"function_call","name":"lookup","call_id":"call_1","arguments":{"q":"x"}},{"type":"function_result","name":"lookup","call_id":"call_1","result":{"ok":true}}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object","properties":{"q":{"type":"string"}}}}]}`), false)
if got := gjson.GetBytes(out, "request.systemInstruction.parts.0.text").String(); got != "be brief" {
t.Fatalf("request.systemInstruction.parts.0.text = %q, want be brief. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.contents.0.parts.0.text").String(); got != "hi" {
t.Fatalf("request.contents.0.parts.0.text = %q, want hi. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.contents.1.parts.0.functionCall.name").String(); got != "lookup" {
t.Fatalf("functionCall.name = %q, want lookup. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.contents.2.parts.0.functionResponse.name").String(); got != "lookup" {
t.Fatalf("functionResponse.name = %q, want lookup. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.name").String(); got != "lookup" {
t.Fatalf("request.tools.0.functionDeclarations.0.name = %q, want lookup. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.parametersJsonSchema.properties.q.type").String(); got != "string" {
t.Fatalf("tool parameters schema was not preserved. Output: %s", string(out))
}
}
func TestConvertInteractionsRequestToAntigravityPreservesGenerationConfig(t *testing.T) {
out := ConvertInteractionsRequestToAntigravity("antigravity-test", []byte(`{"model":"antigravity-test","input":"hi","generation_config":{"max_output_tokens":16,"top_p":0.8,"tool_choice":"auto","thinking_level":"high","thinking_summaries":"auto"},"reasoning":{"summary":"auto"},"stream":true}`), true)
if gjson.GetBytes(out, "input").Exists() {
t.Fatalf("raw interactions input exists in translated request. Output: %s", string(out))
}
for _, path := range []string{
"request.generationConfig.toolChoice",
"request.generationConfig.thinkingLevel",
"request.generationConfig.thinkingSummaries",
} {
if gjson.GetBytes(out, path).Exists() {
t.Fatalf("%s exists, want omitted. Output: %s", path, string(out))
}
}
if got := gjson.GetBytes(out, "request.stream").Bool(); !got {
t.Fatalf("request.stream = false, want true. Output: %s", string(out))
}
if got := gjson.GetBytes(out, "request.contents.0.parts.0.text").String(); got != "hi" {
t.Fatalf("request.contents.0.parts.0.text = %q, want hi. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.generationConfig.maxOutputTokens").Int(); got != 16 {
t.Fatalf("request.generationConfig.maxOutputTokens = %d, want 16. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.generationConfig.topP").Float(); got != 0.8 {
t.Fatalf("request.generationConfig.topP = %v, want 0.8. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel").String(); got != "high" {
t.Fatalf("request.generationConfig.thinkingConfig.thinkingLevel = %q, want high. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts").Bool(); !got {
t.Fatalf("request.generationConfig.thinkingConfig.includeThoughts = false, want true. Output: %s", string(out))
}
if got := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.mode").String(); got != "AUTO" {
t.Fatalf("request.toolConfig.functionCallingConfig.mode = %q, want AUTO. Output: %s", got, string(out))
}
}
func TestConvertInteractionsReasoningToAntigravityKeepsSummaryIndependent(t *testing.T) {
tests := []struct {
name string
reasoning string
want bool
wantExists bool
}{
{name: "effort only leaves summaries unspecified", reasoning: `{"effort":"high"}`},
{name: "explicit auto enables summaries", reasoning: `{"effort":"high","summary":"auto"}`, want: true, wantExists: true},
{name: "explicit none disables summaries", reasoning: `{"effort":"high","summary":"none"}`, wantExists: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
body := []byte(`{"model":"antigravity-test","input":"hi","reasoning":` + test.reasoning + `}`)
out := ConvertInteractionsRequestToAntigravity("antigravity-test", body, false)
if got := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel").String(); got != "high" {
t.Fatalf("thinkingLevel = %q, want high. Output: %s", got, out)
}
includeThoughts := gjson.GetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts")
if includeThoughts.Exists() != test.wantExists {
t.Fatalf("includeThoughts exists = %v, want %v. Output: %s", includeThoughts.Exists(), test.wantExists, out)
}
if test.wantExists && includeThoughts.Bool() != test.want {
t.Fatalf("includeThoughts = %v, want %v. Output: %s", includeThoughts.Bool(), test.want, out)
}
})
}
}
func TestConvertAntigravityResponseToInteractionsNonStream(t *testing.T) {
raw := []byte(`{"response":{"responseId":"resp_1","candidates":[{"content":{"role":"model","parts":[{"text":"ok"},{"functionCall":{"name":"lookup","id":"call_1","args":{"q":"x"}}}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":3,"candidatesTokenCount":2,"totalTokenCount":5}}}`)
out := ConvertAntigravityResponseToInteractionsNonStream(context.Background(), "antigravity-test", nil, nil, raw, nil)
if got := gjson.GetBytes(out, "steps.0.content.0.text").String(); got != "ok" {
t.Fatalf("steps.0.content.0.text = %q, want ok. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "steps.1.type").String(); got != "function_call" {
t.Fatalf("steps.1.type = %q, want function_call. Output: %s", got, string(out))
}
if got := gjson.GetBytes(out, "usage.total_tokens").Int(); got != 5 {
t.Fatalf("usage.total_tokens = %d, want 5. Output: %s", got, string(out))
}
}
func TestConvertAntigravityResponseToInteractionsStream(t *testing.T) {
ctx := context.WithValue(context.Background(), "alt", "")
var param any
events := ConvertAntigravityResponseToInteractions(ctx, "antigravity-test", nil, nil, []byte(`data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]}}]}}`), &param)
payload := findAntigravityInteractionsEventPayload(events, "step.delta")
if len(payload) == 0 {
t.Fatalf("step.delta event not found: %q", events)
}
if got := gjson.GetBytes(payload, "delta.text").String(); got != "ok" {
t.Fatalf("delta.text = %q, want ok. Payload: %s", got, string(payload))
}
}
func TestConvertAntigravityResponseToInteractionsStreamFunctionCallStartHasCallID(t *testing.T) {
var param any
events := ConvertAntigravityResponseToInteractions(context.Background(), "antigravity-test", nil, nil, []byte(`data: {"response":{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"lookup","id":"call_1","args":{"q":"x"}}}]}}]}}`), &param)
payload := findAntigravityInteractionsEventPayload(events, "step.start")
if got := gjson.GetBytes(payload, "step.call_id").String(); got != "call_1" {
t.Fatalf("step.call_id = %q, want call_1. Payload: %s", got, string(payload))
}
}
func TestConvertInteractionsRequestToAntigravityDeduplicatesAndDisambiguatesTools(t *testing.T) {
first := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build"
second := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build_logs"
inputJSON := []byte(`{
"input":[
{"type":"function_call","name":"` + second + `","call_id":"call_1","arguments":{}},
{"type":"function_result","name":"` + second + `","call_id":"call_1","result":{}}
],
"tools":[
{"functionDeclarations":[{"name":"lookup"},{"name":"` + first + `"}]},
{"function_declarations":[{"name":"lookup"},{"name":"` + second + `"}]}
],
"tool_choice":{"type":"function","function":{"name":"` + second + `"}}
}`)
out := ConvertInteractionsRequestToAntigravity("antigravity-test", inputJSON, false)
declarations := gjson.GetBytes(out, "request.tools.0.functionDeclarations").Array()
if len(declarations) != 3 {
t.Fatalf("declaration count = %d, want 3. Output: %s", len(declarations), out)
}
firstMapped := declarations[1].Get("name").String()
secondMapped := declarations[2].Get("name").String()
if firstMapped == secondMapped || len(secondMapped) > 64 {
t.Fatalf("collision names = %q and %q, want distinct names <= 64 chars", firstMapped, secondMapped)
}
if got := gjson.GetBytes(out, "request.contents.0.parts.0.functionCall.name").String(); got != secondMapped {
t.Fatalf("functionCall.name = %q, want %q. Output: %s", got, secondMapped, out)
}
if got := gjson.GetBytes(out, "request.contents.1.parts.0.functionResponse.name").String(); got != secondMapped {
t.Fatalf("functionResponse.name = %q, want %q. Output: %s", got, secondMapped, out)
}
if got := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames.0").String(); got != secondMapped {
t.Fatalf("allowedFunctionNames.0 = %q, want %q. Output: %s", got, secondMapped, out)
}
}
func TestConvertInteractionsRequestToAntigravityPreservesNameMappingWhitespace(t *testing.T) {
inputJSON := []byte(`{
"input":[{"type":"function_call","name":" read/file ","arguments":{}}],
"tools":[{"type":"function","name":" read/file ","parameters":{"type":"object"}}],
"tool_choice":{"type":"function","function":{"name":" read/file "}}
}`)
out := ConvertInteractionsRequestToAntigravity("antigravity-test", inputJSON, false)
declarationName := gjson.GetBytes(out, "request.tools.0.functionDeclarations.0.name").String()
callName := gjson.GetBytes(out, "request.contents.0.parts.0.functionCall.name").String()
allowedName := gjson.GetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames.0").String()
if declarationName == "" || callName != declarationName || allowedName != declarationName {
t.Fatalf("mapped names declaration=%q call=%q allowed=%q. Output: %s", declarationName, callName, allowedName, out)
}
}
func TestConvertAntigravityResponseToInteractionsRestoresDisambiguatedName(t *testing.T) {
first := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build"
second := "mcp__plugin_cloudflare_cloudflare-builds__workers_builds_get_build_logs"
original := []byte(`{"tools":[{"name":"` + first + `"},{"name":"` + second + `"}]}`)
mapped := util.SanitizedFunctionNameMap(original)[second]
raw := []byte(`{"response":{"candidates":[{"content":{"parts":[{"functionCall":{"name":"` + mapped + `","args":{}}}]}}]}}`)
out := ConvertAntigravityResponseToInteractionsNonStream(context.Background(), "antigravity-test", original, nil, raw, nil)
if got := gjson.GetBytes(out, "steps.0.name").String(); got != second {
t.Fatalf("function call name = %q, want %q. Output: %s", got, second, out)
}
}
func findAntigravityInteractionsEventPayload(events [][]byte, eventType string) []byte {
prefix := []byte("data:")
for _, event := range events {
for _, line := range bytes.Split(event, []byte("\n")) {
line = bytes.TrimSpace(line)
if !bytes.HasPrefix(line, prefix) {
continue
}
payload := bytes.TrimSpace(line[len(prefix):])
if gjson.GetBytes(payload, "type").String() == eventType || gjson.GetBytes(payload, "event_type").String() == eventType {
return payload
}
}
}
return nil
}

View file

@ -0,0 +1,30 @@
package interactions
import (
"testing"
"github.com/tidwall/gjson"
)
func TestRewriteInteractionsFunctionNamesReusesNormalizedPayload(t *testing.T) {
input := []byte(`{"request":{"contents":[{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{}}}]},{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"ok"}}}]}],"toolConfig":{"functionCallingConfig":{"allowedFunctionNames":["lookup"]}}}}`)
output := rewriteInteractionsFunctionNames(input, nil)
if &output[0] != &input[0] {
t.Fatal("normalized function names caused a payload copy")
}
}
func TestRewriteInteractionsFunctionNamesNormalizesNonStringNames(t *testing.T) {
input := []byte(`{"request":{"contents":[{"role":"model","parts":[{"functionCall":{"name":true,"args":{}}}]}],"toolConfig":{"functionCallingConfig":{"allowedFunctionNames":[true]}}}}`)
output := rewriteInteractionsFunctionNames(input, nil)
if name := gjson.GetBytes(output, "request.contents.0.parts.0.functionCall.name"); name.Type != gjson.String || name.String() != "true" {
t.Fatalf("functionCall.name = %s, want string true", name.Raw)
}
if name := gjson.GetBytes(output, "request.toolConfig.functionCallingConfig.allowedFunctionNames.0"); name.Type != gjson.String || name.String() != "true" {
t.Fatalf("allowedFunctionNames.0 = %s, want string true", name.Raw)
}
}