mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-21 16:40:22 +00:00
Move base64 image data from Claude tool_result into functionResponse.parts as inlineData instead of outer sibling parts, preventing context bloat. Unify all inlineData field naming to camelCase mimeType across Claude, OpenAI, and Gemini translators. Add comprehensive edge case tests and Gemini-side regression test for functionResponse.parts preservation.
174 lines
5.0 KiB
Go
174 lines
5.0 KiB
Go
package gemini
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
func TestConvertGeminiRequestToAntigravity_PreserveValidSignature(t *testing.T) {
|
|
// Valid signature on functionCall should be preserved
|
|
validSignature := "abc123validSignature1234567890123456789012345678901234567890"
|
|
inputJSON := []byte(fmt.Sprintf(`{
|
|
"model": "gemini-3-pro-preview",
|
|
"contents": [
|
|
{
|
|
"role": "model",
|
|
"parts": [
|
|
{"functionCall": {"name": "test_tool", "args": {}}, "thoughtSignature": "%s"}
|
|
]
|
|
}
|
|
]
|
|
}`, validSignature))
|
|
|
|
output := ConvertGeminiRequestToAntigravity("gemini-3-pro-preview", inputJSON, false)
|
|
outputStr := string(output)
|
|
|
|
// Check that valid thoughtSignature is preserved
|
|
parts := gjson.Get(outputStr, "request.contents.0.parts").Array()
|
|
if len(parts) != 1 {
|
|
t.Fatalf("Expected 1 part, got %d", len(parts))
|
|
}
|
|
|
|
sig := parts[0].Get("thoughtSignature").String()
|
|
if sig != validSignature {
|
|
t.Errorf("Expected thoughtSignature '%s', got '%s'", validSignature, sig)
|
|
}
|
|
}
|
|
|
|
func TestConvertGeminiRequestToAntigravity_AddSkipSentinelToFunctionCall(t *testing.T) {
|
|
// functionCall without signature should get skip_thought_signature_validator
|
|
inputJSON := []byte(`{
|
|
"model": "gemini-3-pro-preview",
|
|
"contents": [
|
|
{
|
|
"role": "model",
|
|
"parts": [
|
|
{"functionCall": {"name": "test_tool", "args": {}}}
|
|
]
|
|
}
|
|
]
|
|
}`)
|
|
|
|
output := ConvertGeminiRequestToAntigravity("gemini-3-pro-preview", inputJSON, false)
|
|
outputStr := string(output)
|
|
|
|
// Check that skip_thought_signature_validator is added to functionCall
|
|
sig := gjson.Get(outputStr, "request.contents.0.parts.0.thoughtSignature").String()
|
|
expectedSig := "skip_thought_signature_validator"
|
|
if sig != expectedSig {
|
|
t.Errorf("Expected skip sentinel '%s', got '%s'", expectedSig, sig)
|
|
}
|
|
}
|
|
|
|
func TestConvertGeminiRequestToAntigravity_ParallelFunctionCalls(t *testing.T) {
|
|
// Multiple functionCalls should all get skip_thought_signature_validator
|
|
inputJSON := []byte(`{
|
|
"model": "gemini-3-pro-preview",
|
|
"contents": [
|
|
{
|
|
"role": "model",
|
|
"parts": [
|
|
{"functionCall": {"name": "tool_one", "args": {"a": "1"}}},
|
|
{"functionCall": {"name": "tool_two", "args": {"b": "2"}}}
|
|
]
|
|
}
|
|
]
|
|
}`)
|
|
|
|
output := ConvertGeminiRequestToAntigravity("gemini-3-pro-preview", inputJSON, false)
|
|
outputStr := string(output)
|
|
|
|
parts := gjson.Get(outputStr, "request.contents.0.parts").Array()
|
|
if len(parts) != 2 {
|
|
t.Fatalf("Expected 2 parts, got %d", len(parts))
|
|
}
|
|
|
|
expectedSig := "skip_thought_signature_validator"
|
|
for i, part := range parts {
|
|
sig := part.Get("thoughtSignature").String()
|
|
if sig != expectedSig {
|
|
t.Errorf("Part %d: Expected '%s', got '%s'", i, expectedSig, sig)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFixCLIToolResponse_PreservesFunctionResponseParts(t *testing.T) {
|
|
// When functionResponse contains a "parts" field with inlineData (from Claude
|
|
// translator's image embedding), fixCLIToolResponse should preserve it as-is.
|
|
// parseFunctionResponseRaw returns response.Raw for valid JSON objects,
|
|
// so extra fields like "parts" survive the pipeline.
|
|
input := `{
|
|
"model": "claude-opus-4-6-thinking",
|
|
"request": {
|
|
"contents": [
|
|
{
|
|
"role": "model",
|
|
"parts": [
|
|
{
|
|
"functionCall": {"name": "screenshot", "args": {}}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"role": "function",
|
|
"parts": [
|
|
{
|
|
"functionResponse": {
|
|
"id": "tool-001",
|
|
"name": "screenshot",
|
|
"response": {"result": "Screenshot taken"},
|
|
"parts": [
|
|
{"inlineData": {"mimeType": "image/png", "data": "iVBOR"}}
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}`
|
|
|
|
result, err := fixCLIToolResponse(input)
|
|
if err != nil {
|
|
t.Fatalf("fixCLIToolResponse failed: %v", err)
|
|
}
|
|
|
|
// Find the function response content (role=function)
|
|
contents := gjson.Get(result, "request.contents").Array()
|
|
var funcContent gjson.Result
|
|
for _, c := range contents {
|
|
if c.Get("role").String() == "function" {
|
|
funcContent = c
|
|
break
|
|
}
|
|
}
|
|
if !funcContent.Exists() {
|
|
t.Fatal("function role content should exist in output")
|
|
}
|
|
|
|
// The functionResponse should be preserved with its parts field
|
|
funcResp := funcContent.Get("parts.0.functionResponse")
|
|
if !funcResp.Exists() {
|
|
t.Fatal("functionResponse should exist in output")
|
|
}
|
|
|
|
// Verify the parts field with inlineData is preserved
|
|
inlineParts := funcResp.Get("parts").Array()
|
|
if len(inlineParts) != 1 {
|
|
t.Fatalf("Expected 1 inlineData part in functionResponse.parts, got %d", len(inlineParts))
|
|
}
|
|
if inlineParts[0].Get("inlineData.mimeType").String() != "image/png" {
|
|
t.Errorf("Expected mimeType 'image/png', got '%s'", inlineParts[0].Get("inlineData.mimeType").String())
|
|
}
|
|
if inlineParts[0].Get("inlineData.data").String() != "iVBOR" {
|
|
t.Errorf("Expected data 'iVBOR', got '%s'", inlineParts[0].Get("inlineData.data").String())
|
|
}
|
|
|
|
// Verify response.result is also preserved
|
|
if funcResp.Get("response.result").String() != "Screenshot taken" {
|
|
t.Errorf("Expected response.result 'Screenshot taken', got '%s'", funcResp.Get("response.result").String())
|
|
}
|
|
}
|