| @@ -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 | |
| | 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 | func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) { | 1166 | func TestConvertClaudeRequestToAntigravity_ToolAndThinking_NoExistingSystem(t *testing.T) { |
| 744 | // When tools + thinking but no system instruction, should create one with hint | 1167 | // When tools + thinking but no system instruction, should create one with hint |
| 745 | inputJSON := []byte(`{ | 1168 | inputJSON := []byte(`{ |