mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-10 10:26:48 +08:00
test(translator, runtime): ensure empty text parts are skipped without null values
- Added tests for `ConvertOpenAIRequestToAntigravity` and `ConvertOpenAIRequestToGemini` to verify skipping of empty text parts while maintaining non-null structure. - Refactored part index logic to correctly handle placement of replay parts in `antigravity_reasoning_replay`. - Introduced helper functions `antigravityExistingReplayPartPath` and `antigravityReplayPartWritePath` for consistent replay part path management.
This commit is contained in:
@@ -368,10 +368,41 @@ func insertAntigravityModelFunctionCallBeforeContent(payload []byte, beforeIndex
|
||||
func antigravityRequestHasThoughtSignatureAt(payload []byte, itemResult gjson.Result) bool {
|
||||
ci := int(itemResult.Get("contentIndex").Int())
|
||||
pi := int(itemResult.Get("partIndex").Int())
|
||||
path := fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", ci, pi)
|
||||
partPath, ok := antigravityExistingReplayPartPath(payload, ci, pi)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
path := partPath + ".thoughtSignature"
|
||||
return strings.TrimSpace(gjson.GetBytes(payload, path).String()) != ""
|
||||
}
|
||||
|
||||
func antigravityExistingReplayPartPath(payload []byte, contentIndex int, partIndex int) (string, bool) {
|
||||
if contentIndex < 0 || partIndex < 0 {
|
||||
return "", false
|
||||
}
|
||||
partsPath := fmt.Sprintf("request.contents.%d.parts", contentIndex)
|
||||
parts := gjson.GetBytes(payload, partsPath)
|
||||
if !parts.IsArray() {
|
||||
return "", false
|
||||
}
|
||||
arr := parts.Array()
|
||||
if partIndex >= len(arr) || arr[partIndex].Type == gjson.Null {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("%s.%d", partsPath, partIndex), true
|
||||
}
|
||||
|
||||
func antigravityReplayPartWritePath(payload []byte, contentIndex int, partIndex int) string {
|
||||
if path, ok := antigravityExistingReplayPartPath(payload, contentIndex, partIndex); ok {
|
||||
return path
|
||||
}
|
||||
partsPath := fmt.Sprintf("request.contents.%d.parts", contentIndex)
|
||||
if gjson.GetBytes(payload, partsPath).IsArray() {
|
||||
return partsPath + ".-1"
|
||||
}
|
||||
return partsPath + ".0"
|
||||
}
|
||||
|
||||
func insertAntigravityReasoningReplayItems(payload []byte, items [][]byte) ([]byte, bool) {
|
||||
out := payload
|
||||
changed := false
|
||||
@@ -385,10 +416,14 @@ func insertAntigravityReasoningReplayItems(payload []byte, items [][]byte) ([]by
|
||||
if sig == "" {
|
||||
continue
|
||||
}
|
||||
path := fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", ci, pi)
|
||||
if strings.TrimSpace(gjson.GetBytes(out, path).String()) != "" {
|
||||
continue
|
||||
partPath, exists := antigravityExistingReplayPartPath(out, ci, pi)
|
||||
if exists {
|
||||
path := partPath + ".thoughtSignature"
|
||||
if strings.TrimSpace(gjson.GetBytes(out, path).String()) != "" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
path := antigravityReplayPartWritePath(out, ci, pi) + ".thoughtSignature"
|
||||
updated, err := sjson.SetBytes(out, path, sig)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -433,16 +468,41 @@ func mergeAntigravityFunctionCallPartReplay(payload []byte, itemResult gjson.Res
|
||||
|
||||
ci := antigravityReasoningReplayResolveContentIndex(payload, int(itemResult.Get("contentIndex").Int()))
|
||||
pi := int(itemResult.Get("partIndex").Int())
|
||||
pathSig := fmt.Sprintf("request.contents.%d.parts.%d.thoughtSignature", ci, pi)
|
||||
out := payload
|
||||
changed := false
|
||||
|
||||
partPath, exists := antigravityExistingReplayPartPath(out, ci, pi)
|
||||
if !exists {
|
||||
fc := map[string]any{"name": name}
|
||||
if callID != "" {
|
||||
fc["id"] = callID
|
||||
}
|
||||
if args.Type == gjson.String {
|
||||
fc["args"] = args.String()
|
||||
} else {
|
||||
var parsed any
|
||||
if json.Unmarshal([]byte(args.Raw), &parsed) == nil {
|
||||
fc["args"] = parsed
|
||||
}
|
||||
}
|
||||
part := map[string]any{"functionCall": fc}
|
||||
if sig != "" {
|
||||
part["thoughtSignature"] = sig
|
||||
}
|
||||
if updated, err := sjson.SetBytes(out, antigravityReplayPartWritePath(out, ci, pi), part); err == nil {
|
||||
return updated, true
|
||||
}
|
||||
return payload, false
|
||||
}
|
||||
|
||||
pathSig := partPath + ".thoughtSignature"
|
||||
if sig != "" && strings.TrimSpace(gjson.GetBytes(out, pathSig).String()) == "" {
|
||||
if updated, err := sjson.SetBytes(out, pathSig, sig); err == nil {
|
||||
out = updated
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
pathFC := fmt.Sprintf("request.contents.%d.parts.%d.functionCall", ci, pi)
|
||||
pathFC := partPath + ".functionCall"
|
||||
if !gjson.GetBytes(out, pathFC).Exists() {
|
||||
fc := map[string]any{"name": name}
|
||||
if callID != "" {
|
||||
|
||||
@@ -114,6 +114,36 @@ func TestMergeAntigravityFunctionCallPartReplayMergesSignatureIntoExistingFuncti
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareAntigravityGeminiReasoningReplayPayloadAppendsStaleThoughtSignatureWithoutNullParts(t *testing.T) {
|
||||
internalcache.ClearAntigravityReasoningReplayCache()
|
||||
t.Cleanup(internalcache.ClearAntigravityReasoningReplayCache)
|
||||
|
||||
item := []byte(`{"type":"thought_signature","contentIndex":8,"partIndex":3,"thoughtSignature":"sig-text"}`)
|
||||
internalcache.CacheAntigravityReasoningReplayItems("gemini-3-flash-agent", "session:sess-stale-text", [][]byte{item})
|
||||
|
||||
payload := []byte(`{"sessionId":"sess-stale-text","request":{"contents":[{"role":"user","parts":[{"text":"hi"}]},{"role":"model","parts":[{"text":"visible answer"}]},{"role":"user","parts":[{"text":"next"}]}]}}`)
|
||||
out, _, err := prepareAntigravityGeminiReasoningReplayPayload(context.Background(), "gemini-3-flash-agent", cliproxyexecutor.Request{}, cliproxyexecutor.Options{}, payload)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
parts := gjson.GetBytes(out, "request.contents.1.parts").Array()
|
||||
if len(parts) != 2 {
|
||||
t.Fatalf("parts length = %d, want 2; body=%s", len(parts), out)
|
||||
}
|
||||
for i, part := range parts {
|
||||
if part.Type == gjson.Null {
|
||||
t.Fatalf("parts.%d is null; body=%s", i, out)
|
||||
}
|
||||
}
|
||||
if got := parts[0].Get("text").String(); got != "visible answer" {
|
||||
t.Fatalf("text part = %q, want visible answer; body=%s", got, out)
|
||||
}
|
||||
if got := parts[1].Get("thoughtSignature").String(); got != "sig-text" {
|
||||
t.Fatalf("thoughtSignature = %q, want sig-text; body=%s", got, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAntigravityReasoningReplayScopeUsesStableSessionWithoutSessionId(t *testing.T) {
|
||||
payload := []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"stable-user-text"}]}]}}`)
|
||||
scope := antigravityReasoningReplayScopeFromPayload("gemini-3-flash-agent", payload)
|
||||
|
||||
+2
-2
@@ -183,8 +183,8 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
||||
text := item.Get("text").String()
|
||||
if text != "" {
|
||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||
p++
|
||||
}
|
||||
p++
|
||||
case "image_url":
|
||||
imageURL := item.Get("image_url.url").String()
|
||||
if len(imageURL) > 5 {
|
||||
@@ -257,8 +257,8 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
||||
text := item.Get("text").String()
|
||||
if text != "" {
|
||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||
p++
|
||||
}
|
||||
p++
|
||||
case "image_url":
|
||||
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
||||
imageURL := item.Get("image_url.url").String()
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package chat_completions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestConvertOpenAIRequestToAntigravitySkipsEmptyTextPartsWithoutNulls(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": ""},
|
||||
{"type": "input_audio", "input_audio": {"data": "SUQzBA==", "format": "mp3"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": ""}],
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "read_file", "arguments": "{\"path\":\"a.txt\"}"}
|
||||
}]
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "{\"output\":\"ok\"}"},
|
||||
{"role": "user", "content": "done"}
|
||||
]
|
||||
}`
|
||||
|
||||
result := ConvertOpenAIRequestToAntigravity("gemini-3-flash", []byte(inputJSON), false)
|
||||
userParts := gjson.GetBytes(result, "request.contents.0.parts").Array()
|
||||
if len(userParts) != 1 {
|
||||
t.Fatalf("user parts length = %d, want 1. Output: %s", len(userParts), result)
|
||||
}
|
||||
if userParts[0].Type == gjson.Null {
|
||||
t.Fatalf("user parts.0 is null. Output: %s", result)
|
||||
}
|
||||
if got := userParts[0].Get("inlineData.mime_type").String(); got != "audio/mpeg" {
|
||||
t.Fatalf("audio mime_type = %q, want audio/mpeg. Output: %s", got, result)
|
||||
}
|
||||
|
||||
assistantParts := gjson.GetBytes(result, "request.contents.1.parts").Array()
|
||||
if len(assistantParts) != 1 {
|
||||
t.Fatalf("assistant parts length = %d, want 1. Output: %s", len(assistantParts), result)
|
||||
}
|
||||
if assistantParts[0].Type == gjson.Null {
|
||||
t.Fatalf("assistant parts.0 is null. Output: %s", result)
|
||||
}
|
||||
if !assistantParts[0].Get("functionCall").Exists() {
|
||||
t.Fatalf("functionCall missing. Output: %s", result)
|
||||
}
|
||||
}
|
||||
@@ -181,8 +181,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
text := item.Get("text").String()
|
||||
if text != "" {
|
||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||
p++
|
||||
}
|
||||
p++
|
||||
case "image_url":
|
||||
imageURL := item.Get("image_url.url").String()
|
||||
if len(imageURL) > 5 {
|
||||
@@ -249,8 +249,8 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
||||
text := item.Get("text").String()
|
||||
if text != "" {
|
||||
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".text", text)
|
||||
p++
|
||||
}
|
||||
p++
|
||||
case "image_url":
|
||||
// If the assistant returned an inline data URL, preserve it for history fidelity.
|
||||
imageURL := item.Get("image_url.url").String()
|
||||
|
||||
@@ -91,6 +91,55 @@ func TestConvertOpenAIRequestToGeminiPreservesVideoURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIRequestToGeminiSkipsEmptyTextPartsWithoutNulls(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-3-flash",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": ""},
|
||||
{"type": "input_audio", "input_audio": {"data": "SUQzBA==", "format": "mp3"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [{"type": "text", "text": ""}],
|
||||
"tool_calls": [{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "read_file", "arguments": "{\"path\":\"a.txt\"}"}
|
||||
}]
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "{\"output\":\"ok\"}"},
|
||||
{"role": "user", "content": "done"}
|
||||
]
|
||||
}`
|
||||
|
||||
result := ConvertOpenAIRequestToGemini("gemini-3-flash", []byte(inputJSON), false)
|
||||
userParts := gjson.GetBytes(result, "contents.0.parts").Array()
|
||||
if len(userParts) != 1 {
|
||||
t.Fatalf("user parts length = %d, want 1. Output: %s", len(userParts), result)
|
||||
}
|
||||
if userParts[0].Type == gjson.Null {
|
||||
t.Fatalf("user parts.0 is null. Output: %s", result)
|
||||
}
|
||||
if got := userParts[0].Get("inlineData.mime_type").String(); got != "audio/mpeg" {
|
||||
t.Fatalf("audio mime_type = %q, want audio/mpeg. Output: %s", got, result)
|
||||
}
|
||||
|
||||
assistantParts := gjson.GetBytes(result, "contents.1.parts").Array()
|
||||
if len(assistantParts) != 1 {
|
||||
t.Fatalf("assistant parts length = %d, want 1. Output: %s", len(assistantParts), result)
|
||||
}
|
||||
if assistantParts[0].Type == gjson.Null {
|
||||
t.Fatalf("assistant parts.0 is null. Output: %s", result)
|
||||
}
|
||||
if !assistantParts[0].Get("functionCall").Exists() {
|
||||
t.Fatalf("functionCall missing. Output: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertOpenAIRequestToGeminiCleansToolSchemaRequiredFields(t *testing.T) {
|
||||
inputJSON := `{
|
||||
"model": "gemini-2.0-flash",
|
||||
|
||||
Reference in New Issue
Block a user