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
223 functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", responseData)223 functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", responseData)
224 } else if functionResponseResult.IsArray() {224 } else if functionResponseResult.IsArray() {
225 frResults := functionResponseResult.Array()225 frResults := functionResponseResult.Array()
226 if len(frResults) == 1 {226 nonImageCount := 0
227 functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", frResults[0].Raw)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)
228 } else {255 } 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)
230 }264 }
231 265
232 } else if functionResponseResult.IsObject() {266 } 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 }
234 } else if functionResponseResult.Raw != "" {285 } else if functionResponseResult.Raw != "" {
235 functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)286 functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
236 } else {287 } else {
@@ -248,7 +299,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 11 unmodified lines
248 if sourceResult.Get("type").String() == "base64" {299 if sourceResult.Get("type").String() == "base64" {
249 inlineDataJSON := `{}`300 inlineDataJSON := `{}`
250 if mimeType := sourceResult.Get("media_type").String(); mimeType != "" {301 if mimeType := sourceResult.Get("media_type").String(); mimeType != "" {
251 inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mime_type", mimeType)302 inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType)
252 }303 }
253 if data := sourceResult.Get("data").String(); data != "" {304 if data := sourceResult.Get("data").String(); data != "" {
254 inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)305 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
413 if !inlineData.Exists() {413 if !inlineData.Exists() {
414 t.Error("inlineData should exist")414 t.Error("inlineData should exist")
415 }415 }
416 if inlineData.Get("mime_type").String() != "image/png" {416 if inlineData.Get("mimeType").String() != "image/png" {
417 t.Error("mime_type mismatch")417 t.Error("mimeType mismatch")
418 }418 }
419 if !strings.Contains(inlineData.Get("data").String(), "iVBORw0KGgo") {419 if !strings.Contains(inlineData.Get("data").String(), "iVBORw0KGgo") {
420 t.Error("data mismatch")420 t.Error("data mismatch")
@@ -740,6 +740,429 @@ func TestConvertClaudeRequestToAntigravity_ToolResultNullContent(t *testing.T) { · 319 unmodified lines
740 }740 }
741}741}
742 742
743func 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
814func 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
873func 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
950func 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
1013func 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
1066func 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
1116func 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
743func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) {1166func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) {
744 // When tools + thinking but no system instruction, should create one with hint1167 // When tools + thinking but no system instruction, should create one with hint
745 inputJSON := []byte(`{1168 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
93 }93 }
94 }94 }
95}95}
96
97func 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
187 if len(pieces) == 2 && len(pieces[1]) > 7 {187 if len(pieces) == 2 && len(pieces[1]) > 7 {
188 mime := pieces[0]188 mime := pieces[0]
189 data := pieces[1][7:]189 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)
191 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)191 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)
192 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)192 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
193 p++193 p++
@@ -201,7 +201,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 7 unmodified lines
201 ext = sp[len(sp)-1]201 ext = sp[len(sp)-1]
202 }202 }
203 if mimeType, ok := misc.MimeTypes[ext]; ok {203 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)
205 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", fileData)205 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", fileData)
206 p++206 p++
207 } else {207 } else {
@@ -235,7 +235,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 27 unmodified lines
235 if len(pieces) == 2 && len(pieces[1]) > 7 {235 if len(pieces) == 2 && len(pieces[1]) > 7 {
236 mime := pieces[0]236 mime := pieces[0]
237 data := pieces[1][7:]237 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)
239 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)239 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".inlineData.data", data)
240 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)240 node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
241 p++241 p++