Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
19
backend/internal/translator/antigravity/interactions/init.go
Normal file
19
backend/internal/translator/antigravity/interactions/init.go
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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"}]}}]}}`), ¶m)
|
||||
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"}}}]}}]}}`), ¶m)
|
||||
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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue