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:
Luis Pater
2026-06-23 08:07:03 +08:00
parent 5d9ea1667b
commit bd646819ed
6 changed files with 205 additions and 10 deletions
@@ -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)
@@ -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()
@@ -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",