History

fix(antigravity): place tool_result images in functionResponse.parts and unify mimeType

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.

sususu98 committed 8 months ago

Commit 4e261821 parent 713388d4 files · +562 −10Browse files

Unified · Split

4 files
  1. internal/translator/antigravity/claude/antigravity_claude_request.go +56 −5
  2. internal/translator/antigravity/claude/antigravity_claude_request_test.go +425 −2
  3. internal/translator/antigravity/gemini/antigravity_gemini_request_test.go +78 −0
  4. internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +3 −3
internal/translator/antigravity/claude/antigravity_claude_request.go+56−5
@@ -223,14 +223,65 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 222 unmodified lines
223223 functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", responseData)
224224 } else if functionResponseResult.IsArray() {
225225 frResults := functionResponseResult.Array()
226− if len(frResults) == 1 {
227− functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", frResults[0].Raw)
226+ nonImageCount := 0
227+ lastNonImageRaw := ""
228+ filteredJSON := "[]"
229+ imagePartsJSON := "[]"
230+ for _, fr := range frResults {
231+ if fr.Get("type").String() == "image" && fr.Get("source.type").String() == "base64" {
232+ inlineDataJSON := `{}`
233+ if mimeType := fr.Get("source.media_type").String(); mimeType != "" {
234+ inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType)
235+ }
236+ if data := fr.Get("source.data").String(); data != "" {
237+ inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
238+ }
239+
240+ imagePartJSON := `{}`
241+ imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON)
242+ imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON)
243+ continue
244+ }
245+
246+ nonImageCount++
247+ lastNonImageRaw = fr.Raw
248+ filteredJSON, _ = sjson.SetRaw(filteredJSON, "-1", fr.Raw)
249+ }
250+
251+ if nonImageCount == 1 {
252+ functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", lastNonImageRaw)
253+ } else if nonImageCount > 1 {
254+ functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", filteredJSON)
228255 } else {
229− functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
256+ functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "")
257+ }
258+
259+ // Place image data inside functionResponse.parts as inlineData
260+ // instead of as sibling parts in the outer content, to avoid
261+ // base64 data bloating the text context.
262+ if gjson.Get(imagePartsJSON, "#").Int() > 0 {
263+ functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON)
230264 }
231265
232266 } else if functionResponseResult.IsObject() {
233− functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
267+ if functionResponseResult.Get("type").String() == "image" && functionResponseResult.Get("source.type").String() == "base64" {
268+ inlineDataJSON := `{}`
269+ if mimeType := functionResponseResult.Get("source.media_type").String(); mimeType != "" {
270+ inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType)
271+ }
272+ if data := functionResponseResult.Get("source.data").String(); data != "" {
273+ inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
274+ }
275+
276+ imagePartJSON := `{}`
277+ imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON)
278+ imagePartsJSON := "[]"
279+ imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON)
280+ functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON)
281+ functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "")
282+ } else {
283+ functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
284+ }
234285 } else if functionResponseResult.Raw != "" {
235286 functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
236287 } else {
@@ -248,7 +299,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 11 unmodified lines
248299 if sourceResult.Get("type").String() == "base64" {
249300 inlineDataJSON := `{}`
250301 if mimeType := sourceResult.Get("media_type").String(); mimeType != "" {
251− inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mime_type", mimeType)
302+ inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType)
252303 }
253304 if data := sourceResult.Get("data").String(); data != "" {
254305 inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
internal/translator/antigravity/claude/antigravity_claude_request_test.go+425−2
@@ -413,8 +413,8 @@ func TestConvertClaudeRequestToAntigravity_ImageContent(t *testing.T) { · 412 unmodified lines
413413 if !inlineData.Exists() {
414414 t.Error("inlineData should exist")
415415 }
416− if inlineData.Get("mime_type").String() != "image/png" {
417− t.Error("mime_type mismatch")
416+ if inlineData.Get("mimeType").String() != "image/png" {
417+ t.Error("mimeType mismatch")
418418 }
419419 if !strings.Contains(inlineData.Get("data").String(), "iVBORw0KGgo") {
420420 t.Error("data mismatch")
@@ -740,6 +740,429 @@ func TestConvertClaudeRequestToAntigravity_ToolResultNullContent(t *testing.T) { · 319 unmodified lines
740740 }
741741 }
742742
743+func TestConvertClaudeRequestToAntigravity_ToolResultWithImage(t *testing.T) {
744+ // tool_result with array content containing text + image should place
745+ // image data inside functionResponse.parts as inlineData, not as a
746+ // sibling part in the outer content (to avoid base64 context bloat).
747+ inputJSON := []byte(`{
748+ "model": "claude-3-5-sonnet-20240620",
749+ "messages": [
750+ {
751+ "role": "user",
752+ "content": [
753+ {
754+ "type": "tool_result",
755+ "tool_use_id": "Read-123-456",
756+ "content": [
757+ {
758+ "type": "text",
759+ "text": "File content here"
760+ },
761+ {
762+ "type": "image",
763+ "source": {
764+ "type": "base64",
765+ "media_type": "image/png",
766+ "data": "iVBORw0KGgoAAAANSUhEUg=="
767+ }
768+ }
769+ ]
770+ }
771+ ]
772+ }
773+ ]
774+ }`)
775+
776+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
777+ outputStr := string(output)
778+
779+ if !gjson.Valid(outputStr) {
780+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
781+ }
782+
783+ // Image should be inside functionResponse.parts, not as outer sibling part
784+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
785+ if !funcResp.Exists() {
786+ t.Fatal("functionResponse should exist")
787+ }
788+
789+ // Text content should be in response.result
790+ resultText := funcResp.Get("response.result.text").String()
791+ if resultText != "File content here" {
792+ t.Errorf("Expected response.result.text = 'File content here', got '%s'", resultText)
793+ }
794+
795+ // Image should be in functionResponse.parts[0].inlineData
796+ inlineData := funcResp.Get("parts.0.inlineData")
797+ if !inlineData.Exists() {
798+ t.Fatal("functionResponse.parts[0].inlineData should exist")
799+ }
800+ if inlineData.Get("mimeType").String() != "image/png" {
801+ t.Errorf("Expected mimeType 'image/png', got '%s'", inlineData.Get("mimeType").String())
802+ }
803+ if !strings.Contains(inlineData.Get("data").String(), "iVBORw0KGgo") {
804+ t.Error("data mismatch")
805+ }
806+
807+ // Image should NOT be in outer parts (only functionResponse part should exist)
808+ outerParts := gjson.Get(outputStr, "request.contents.0.parts")
809+ if outerParts.IsArray() && len(outerParts.Array()) > 1 {
810+ t.Errorf("Expected only 1 outer part (functionResponse), got %d", len(outerParts.Array()))
811+ }
812+}
813+
814+func TestConvertClaudeRequestToAntigravity_ToolResultWithSingleImage(t *testing.T) {
815+ // tool_result with single image object as content should place
816+ // image data inside functionResponse.parts, not as outer sibling part.
817+ inputJSON := []byte(`{
818+ "model": "claude-3-5-sonnet-20240620",
819+ "messages": [
820+ {
821+ "role": "user",
822+ "content": [
823+ {
824+ "type": "tool_result",
825+ "tool_use_id": "Read-789-012",
826+ "content": {
827+ "type": "image",
828+ "source": {
829+ "type": "base64",
830+ "media_type": "image/jpeg",
831+ "data": "/9j/4AAQSkZJRgABAQ=="
832+ }
833+ }
834+ }
835+ ]
836+ }
837+ ]
838+ }`)
839+
840+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
841+ outputStr := string(output)
842+
843+ if !gjson.Valid(outputStr) {
844+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
845+ }
846+
847+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
848+ if !funcResp.Exists() {
849+ t.Fatal("functionResponse should exist")
850+ }
851+
852+ // response.result should be empty (image only)
853+ if funcResp.Get("response.result").String() != "" {
854+ t.Errorf("Expected empty response.result for image-only content, got '%s'", funcResp.Get("response.result").String())
855+ }
856+
857+ // Image should be in functionResponse.parts[0].inlineData
858+ inlineData := funcResp.Get("parts.0.inlineData")
859+ if !inlineData.Exists() {
860+ t.Fatal("functionResponse.parts[0].inlineData should exist")
861+ }
862+ if inlineData.Get("mimeType").String() != "image/jpeg" {
863+ t.Errorf("Expected mimeType 'image/jpeg', got '%s'", inlineData.Get("mimeType").String())
864+ }
865+
866+ // Image should NOT be in outer parts
867+ outerParts := gjson.Get(outputStr, "request.contents.0.parts")
868+ if outerParts.IsArray() && len(outerParts.Array()) > 1 {
869+ t.Errorf("Expected only 1 outer part, got %d", len(outerParts.Array()))
870+ }
871+}
872+
873+func TestConvertClaudeRequestToAntigravity_ToolResultWithMultipleImagesAndTexts(t *testing.T) {
874+ // tool_result with array content: 2 text items + 2 images
875+ // All images go into functionResponse.parts, texts into response.result array
876+ inputJSON := []byte(`{
877+ "model": "claude-3-5-sonnet-20240620",
878+ "messages": [
879+ {
880+ "role": "user",
881+ "content": [
882+ {
883+ "type": "tool_result",
884+ "tool_use_id": "Multi-001",
885+ "content": [
886+ {"type": "text", "text": "First text"},
887+ {
888+ "type": "image",
889+ "source": {"type": "base64", "media_type": "image/png", "data": "AAAA"}
890+ },
891+ {"type": "text", "text": "Second text"},
892+ {
893+ "type": "image",
894+ "source": {"type": "base64", "media_type": "image/jpeg", "data": "BBBB"}
895+ }
896+ ]
897+ }
898+ ]
899+ }
900+ ]
901+ }`)
902+
903+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
904+ outputStr := string(output)
905+
906+ if !gjson.Valid(outputStr) {
907+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
908+ }
909+
910+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
911+ if !funcResp.Exists() {
912+ t.Fatal("functionResponse should exist")
913+ }
914+
915+ // Multiple text items => response.result is an array
916+ resultArr := funcResp.Get("response.result")
917+ if !resultArr.IsArray() {
918+ t.Fatalf("Expected response.result to be an array, got: %s", resultArr.Raw)
919+ }
920+ results := resultArr.Array()
921+ if len(results) != 2 {
922+ t.Fatalf("Expected 2 result items, got %d", len(results))
923+ }
924+
925+ // Both images should be in functionResponse.parts
926+ imgParts := funcResp.Get("parts").Array()
927+ if len(imgParts) != 2 {
928+ t.Fatalf("Expected 2 image parts in functionResponse.parts, got %d", len(imgParts))
929+ }
930+ if imgParts[0].Get("inlineData.mimeType").String() != "image/png" {
931+ t.Errorf("Expected first image mimeType 'image/png', got '%s'", imgParts[0].Get("inlineData.mimeType").String())
932+ }
933+ if imgParts[0].Get("inlineData.data").String() != "AAAA" {
934+ t.Errorf("Expected first image data 'AAAA', got '%s'", imgParts[0].Get("inlineData.data").String())
935+ }
936+ if imgParts[1].Get("inlineData.mimeType").String() != "image/jpeg" {
937+ t.Errorf("Expected second image mimeType 'image/jpeg', got '%s'", imgParts[1].Get("inlineData.mimeType").String())
938+ }
939+ if imgParts[1].Get("inlineData.data").String() != "BBBB" {
940+ t.Errorf("Expected second image data 'BBBB', got '%s'", imgParts[1].Get("inlineData.data").String())
941+ }
942+
943+ // Only 1 outer part (the functionResponse itself)
944+ outerParts := gjson.Get(outputStr, "request.contents.0.parts").Array()
945+ if len(outerParts) != 1 {
946+ t.Errorf("Expected 1 outer part, got %d", len(outerParts))
947+ }
948+}
949+
950+func TestConvertClaudeRequestToAntigravity_ToolResultWithOnlyMultipleImages(t *testing.T) {
951+ // tool_result with only images (no text) — response.result should be empty string
952+ inputJSON := []byte(`{
953+ "model": "claude-3-5-sonnet-20240620",
954+ "messages": [
955+ {
956+ "role": "user",
957+ "content": [
958+ {
959+ "type": "tool_result",
960+ "tool_use_id": "ImgOnly-001",
961+ "content": [
962+ {
963+ "type": "image",
964+ "source": {"type": "base64", "media_type": "image/png", "data": "PNG1"}
965+ },
966+ {
967+ "type": "image",
968+ "source": {"type": "base64", "media_type": "image/gif", "data": "GIF1"}
969+ }
970+ ]
971+ }
972+ ]
973+ }
974+ ]
975+ }`)
976+
977+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
978+ outputStr := string(output)
979+
980+ if !gjson.Valid(outputStr) {
981+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
982+ }
983+
984+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
985+ if !funcResp.Exists() {
986+ t.Fatal("functionResponse should exist")
987+ }
988+
989+ // No text => response.result should be empty string
990+ if funcResp.Get("response.result").String() != "" {
991+ t.Errorf("Expected empty response.result, got '%s'", funcResp.Get("response.result").String())
992+ }
993+
994+ // Both images in functionResponse.parts
995+ imgParts := funcResp.Get("parts").Array()
996+ if len(imgParts) != 2 {
997+ t.Fatalf("Expected 2 image parts, got %d", len(imgParts))
998+ }
999+ if imgParts[0].Get("inlineData.mimeType").String() != "image/png" {
1000+ t.Error("first image mimeType mismatch")
1001+ }
1002+ if imgParts[1].Get("inlineData.mimeType").String() != "image/gif" {
1003+ t.Error("second image mimeType mismatch")
1004+ }
1005+
1006+ // Only 1 outer part
1007+ outerParts := gjson.Get(outputStr, "request.contents.0.parts").Array()
1008+ if len(outerParts) != 1 {
1009+ t.Errorf("Expected 1 outer part, got %d", len(outerParts))
1010+ }
1011+}
1012+
1013+func TestConvertClaudeRequestToAntigravity_ToolResultImageNotBase64(t *testing.T) {
1014+ // image with source.type != "base64" should be treated as non-image (falls through)
1015+ inputJSON := []byte(`{
1016+ "model": "claude-3-5-sonnet-20240620",
1017+ "messages": [
1018+ {
1019+ "role": "user",
1020+ "content": [
1021+ {
1022+ "type": "tool_result",
1023+ "tool_use_id": "NotB64-001",
1024+ "content": [
1025+ {"type": "text", "text": "some output"},
1026+ {
1027+ "type": "image",
1028+ "source": {"type": "url", "url": "https://example.com/img.png"}
1029+ }
1030+ ]
1031+ }
1032+ ]
1033+ }
1034+ ]
1035+ }`)
1036+
1037+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
1038+ outputStr := string(output)
1039+
1040+ if !gjson.Valid(outputStr) {
1041+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
1042+ }
1043+
1044+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
1045+ if !funcResp.Exists() {
1046+ t.Fatal("functionResponse should exist")
1047+ }
1048+
1049+ // Non-base64 image is treated as non-image, so it goes into the filtered results
1050+ // along with the text item. Since there are 2 non-image items, result is array.
1051+ resultArr := funcResp.Get("response.result")
1052+ if !resultArr.IsArray() {
1053+ t.Fatalf("Expected response.result to be an array (2 non-image items), got: %s", resultArr.Raw)
1054+ }
1055+ results := resultArr.Array()
1056+ if len(results) != 2 {
1057+ t.Fatalf("Expected 2 result items, got %d", len(results))
1058+ }
1059+
1060+ // No functionResponse.parts (no base64 images collected)
1061+ if funcResp.Get("parts").Exists() {
1062+ t.Error("functionResponse.parts should NOT exist when no base64 images")
1063+ }
1064+}
1065+
1066+func TestConvertClaudeRequestToAntigravity_ToolResultImageMissingData(t *testing.T) {
1067+ // image with source.type=base64 but missing data field
1068+ inputJSON := []byte(`{
1069+ "model": "claude-3-5-sonnet-20240620",
1070+ "messages": [
1071+ {
1072+ "role": "user",
1073+ "content": [
1074+ {
1075+ "type": "tool_result",
1076+ "tool_use_id": "NoData-001",
1077+ "content": [
1078+ {"type": "text", "text": "output"},
1079+ {
1080+ "type": "image",
1081+ "source": {"type": "base64", "media_type": "image/png"}
1082+ }
1083+ ]
1084+ }
1085+ ]
1086+ }
1087+ ]
1088+ }`)
1089+
1090+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
1091+ outputStr := string(output)
1092+
1093+ if !gjson.Valid(outputStr) {
1094+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
1095+ }
1096+
1097+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
1098+ if !funcResp.Exists() {
1099+ t.Fatal("functionResponse should exist")
1100+ }
1101+
1102+ // The image is still classified as base64 image (type check passes),
1103+ // but data field is missing => inlineData has mimeType but no data
1104+ imgParts := funcResp.Get("parts").Array()
1105+ if len(imgParts) != 1 {
1106+ t.Fatalf("Expected 1 image part, got %d", len(imgParts))
1107+ }
1108+ if imgParts[0].Get("inlineData.mimeType").String() != "image/png" {
1109+ t.Error("mimeType should still be set")
1110+ }
1111+ if imgParts[0].Get("inlineData.data").Exists() {
1112+ t.Error("data should not exist when source.data is missing")
1113+ }
1114+}
1115+
1116+func TestConvertClaudeRequestToAntigravity_ToolResultImageMissingMediaType(t *testing.T) {
1117+ // image with source.type=base64 but missing media_type field
1118+ inputJSON := []byte(`{
1119+ "model": "claude-3-5-sonnet-20240620",
1120+ "messages": [
1121+ {
1122+ "role": "user",
1123+ "content": [
1124+ {
1125+ "type": "tool_result",
1126+ "tool_use_id": "NoMime-001",
1127+ "content": [
1128+ {"type": "text", "text": "output"},
1129+ {
1130+ "type": "image",
1131+ "source": {"type": "base64", "data": "AAAA"}
1132+ }
1133+ ]
1134+ }
1135+ ]
1136+ }
1137+ ]
1138+ }`)
1139+
1140+ output := ConvertClaudeRequestToAntigravity("claude-sonnet-4-5", inputJSON, false)
1141+ outputStr := string(output)
1142+
1143+ if !gjson.Valid(outputStr) {
1144+ t.Fatalf("Result is not valid JSON:\n%s", outputStr)
1145+ }
1146+
1147+ funcResp := gjson.Get(outputStr, "request.contents.0.parts.0.functionResponse")
1148+ if !funcResp.Exists() {
1149+ t.Fatal("functionResponse should exist")
1150+ }
1151+
1152+ // The image is still classified as base64 image,
1153+ // but media_type is missing => inlineData has data but no mimeType
1154+ imgParts := funcResp.Get("parts").Array()
1155+ if len(imgParts) != 1 {
1156+ t.Fatalf("Expected 1 image part, got %d", len(imgParts))
1157+ }
1158+ if imgParts[0].Get("inlineData.mimeType").Exists() {
1159+ t.Error("mimeType should not exist when media_type is missing")
1160+ }
1161+ if imgParts[0].Get("inlineData.data").String() != "AAAA" {
1162+ t.Error("data should still be set")
1163+ }
1164+}
1165+
7431166 func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) {
7441167 // When tools + thinking but no system instruction, should create one with hint
7451168 inputJSON := []byte(`{
internal/translator/antigravity/gemini/antigravity_gemini_request_test.go+78−0
@@ -93,3 +93,81 @@ func TestConvertGeminiRequestToAntigravity_ParallelFunctionCalls(t *testing.T) { · 92 unmodified lines
9393 }
9494 }
9595 }
96+
97+func TestFixCLIToolResponse_PreservesFunctionResponseParts(t *testing.T) {
98+ // When functionResponse contains a "parts" field with inlineData (from Claude
99+ // translator's image embedding), fixCLIToolResponse should preserve it as-is.
100+ // parseFunctionResponseRaw returns response.Raw for valid JSON objects,
101+ // so extra fields like "parts" survive the pipeline.
102+ input := `{
103+ "model": "claude-opus-4-6-thinking",
104+ "request": {
105+ "contents": [
106+ {
107+ "role": "model",
108+ "parts": [
109+ {
110+ "functionCall": {"name": "screenshot", "args": {}}
111+ }
112+ ]
113+ },
114+ {
115+ "role": "function",
116+ "parts": [
117+ {
118+ "functionResponse": {
119+ "id": "tool-001",
120+ "name": "screenshot",
121+ "response": {"result": "Screenshot taken"},
122+ "parts": [
123+ {"inlineData": {"mimeType": "image/png", "data": "iVBOR"}}
124+ ]
125+ }
126+ }
127+ ]
128+ }
129+ ]
130+ }
131+ }`
132+
133+ result, err := fixCLIToolResponse(input)
134+ if err != nil {
135+ t.Fatalf("fixCLIToolResponse failed: %v", err)
136+ }
137+
138+ // Find the function response content (role=function)
139+ contents := gjson.Get(result, "request.contents").Array()
140+ var funcContent gjson.Result
141+ for _, c := range contents {
142+ if c.Get("role").String() == "function" {
143+ funcContent = c
144+ break
145+ }
146+ }
147+ if !funcContent.Exists() {
148+ t.Fatal("function role content should exist in output")
149+ }
150+
151+ // The functionResponse should be preserved with its parts field
152+ funcResp := funcContent.Get("parts.0.functionResponse")
153+ if !funcResp.Exists() {
154+ t.Fatal("functionResponse should exist in output")
155+ }
156+
157+ // Verify the parts field with inlineData is preserved
158+ inlineParts := funcResp.Get("parts").Array()
159+ if len(inlineParts) != 1 {
160+ t.Fatalf("Expected 1 inlineData part in functionResponse.parts, got %d", len(inlineParts))
161+ }
162+ if inlineParts[0].Get("inlineData.mimeType").String() != "image/png" {
163+ t.Errorf("Expected mimeType 'image/png', got '%s'", inlineParts[0].Get("inlineData.mimeType").String())
164+ }
165+ if inlineParts[0].Get("inlineData.data").String() != "iVBOR" {
166+ t.Errorf("Expected data 'iVBOR', got '%s'", inlineParts[0].Get("inlineData.data").String())
167+ }
168+
169+ // Verify response.result is also preserved
170+ if funcResp.Get("response.result").String() != "Screenshot taken" {
171+ t.Errorf("Expected response.result 'Screenshot taken', got '%s'", funcResp.Get("response.result").String())
172+ }
173+}
internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go+3−3
@@ -187,7 +187,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 186 unmodified lines
187187 if len(pieces) == 2 && len(pieces[1]) > 7 {
188188 mime := pieces[0]
189189 data := pieces[1][7:]
190− node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mime_type", mime)
190+ node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mimeType", mime)
191191 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)
192192 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
193193 p++
@@ -201,7 +201,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 7 unmodified lines
201201 ext = sp[len(sp)-1]
202202 }
203203 if mimeType, ok := misc.MimeTypes[ext]; ok {
204− node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mime_type", mimeType)
204+ node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mimeType", mimeType)
205205 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", fileData)
206206 p++
207207 } else {
@@ -235,7 +235,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 27 unmodified lines
235235 if len(pieces) == 2 && len(pieces[1]) > 7 {
236236 mime := pieces[0]
237237 data := pieces[1][7:]
238− node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mime_type", mime)
238+ node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.mimeType", mime)
239239 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)
240240 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
241241 p++