| @@ -413,8 +413,8 @@ func TestConvertClaudeRequestToAntigravity_ImageContent(t *testing.T) { · 412 unmodified lines |
| 413 | 413 | if !inlineData.Exists() { |
| 414 | 414 | t.Error("inlineData should exist") |
| 415 | 415 | } |
| 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") |
| 418 | 418 | } |
| 419 | 419 | if !strings.Contains(inlineData.Get("data").String(), "iVBORw0KGgo") { |
| 420 | 420 | t.Error("data mismatch") |
| @@ -740,6 +740,429 @@ func TestConvertClaudeRequestToAntigravity_ToolResultNullContent(t *testing.T) { · 319 unmodified lines |
| 740 | 740 | } |
| 741 | 741 | } |
| 742 | 742 | |
| 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 | + |
| 743 | 1166 | func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) { |
| 744 | 1167 | // When tools + thinking but no system instruction, should create one with hint |
| 745 | 1168 | inputJSON := []byte(`{ |