diff --git a/src/main/java/com/google/genai/ReplayApiClient.java b/src/main/java/com/google/genai/ReplayApiClient.java index c2f379fc0e6..0fe233764ed 100644 --- a/src/main/java/com/google/genai/ReplayApiClient.java +++ b/src/main/java/com/google/genai/ReplayApiClient.java @@ -352,6 +352,14 @@ private static Object normalizeKeyCase(Object obj) { entry -> { String newKey = Common.snakeToCamel(entry.getKey()); Object normalizedValue = normalizeKeyCase(entry.getValue()); + if (newKey.equals("data") || newKey.equals("imageBytes")) { + if (normalizedValue instanceof JsonNode && ((JsonNode) normalizedValue).isTextual()) { + String base64Str = ((JsonNode) normalizedValue).asText(); + // Normalize URL-safe Base64 to Standard Base64 and remove padding for comparison + base64Str = base64Str.replace('-', '+').replace('_', '/').replaceAll("=", ""); + normalizedValue = JsonSerializable.toJsonNode(base64Str); + } + } normalizedNode.set(newKey, JsonSerializable.toJsonNode(normalizedValue)); }); return normalizedNode; diff --git a/src/test/java/com/google/genai/TableTest.java b/src/test/java/com/google/genai/TableTest.java index ba93d29191e..c699cf91aa2 100644 --- a/src/test/java/com/google/genai/TableTest.java +++ b/src/test/java/com/google/genai/TableTest.java @@ -194,16 +194,6 @@ private static Collection createTestCases( String msg = " => Test skipped: parameters contain unsupported union type"; return Collections.singletonList(DynamicTest.dynamicTest(testName + msg, () -> {})); } - // Edit image ReferenceImages are not correctly deserialized for replay tests - if (testName.contains("models.edit_image") - || testName.contains("batches.create.test_with_image_blob")) { // TODO(b/431798111) - String msg = " => Test skipped: replay tests are not supported for edit_image"; - return Collections.singletonList(DynamicTest.dynamicTest(testName + msg, () -> {})); - } - if (testName.contains("models.embed_content.test_new_api_inline_pdf")) { - String msg = " => Test skipped: inline byte deserialization fails"; - return Collections.singletonList(DynamicTest.dynamicTest(testName + msg, () -> {})); - } // TODO(b/457846189): Support models.list filter parameter if (testName.contains("models.list.test_tuned_models_with_filter") || testName.contains("models.list.test_tuned_models.vertex")) { @@ -224,7 +214,8 @@ private static Collection createTestCases( Object fromValue = fromParameters.getOrDefault(parameterName, null); // May throw IllegalArgumentException here Object parameter = - JsonSerializable.objectMapper.convertValue(fromValue, method.getParameterTypes()[i]); + TestUtils.getTestObjectMapper().convertValue( + fromValue, TestUtils.getTestObjectMapper().constructType(method.getGenericParameterTypes()[i])); if (method.getName().equals("embedContent") && parameter instanceof List) { throw new IllegalArgumentException(); } @@ -337,6 +328,38 @@ private static Map prepareParameters(TestTableItem testTableItem "source.scribbleImage.image.imageBytes", new ReplayBase64Sanitizer(), false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "[]referenceImages.referenceImage.imageBytes", + new ReplayBase64Sanitizer(), + false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "referenceImages.[]referenceImage.imageBytes", + new ReplayBase64Sanitizer(), + false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, "[]contents.[]parts.inlineData.data", new ReplayBase64Sanitizer(), false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "contents.[]parts.inlineData.data", + new ReplayBase64Sanitizer(), + false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "[]contents.[]parts.functionResponse.[]parts.inlineData.data", + new ReplayBase64Sanitizer(), + false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "contents.[]parts.functionResponse.[]parts.inlineData.data", + new ReplayBase64Sanitizer(), + false); + ReplaySanitizer.sanitizeMapByPath( + fromParameters, + "src.[]inlinedRequests.[]contents.[]parts.inlineData.data", + new ReplayBase64Sanitizer(), + false); return fromParameters; } diff --git a/src/test/java/com/google/genai/TestUtils.java b/src/test/java/com/google/genai/TestUtils.java index 9e2313db3c2..04da5bbd511 100644 --- a/src/test/java/com/google/genai/TestUtils.java +++ b/src/test/java/com/google/genai/TestUtils.java @@ -16,11 +16,91 @@ package com.google.genai; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.genai.types.ContentReferenceImage; +import com.google.genai.types.ControlReferenceImage; +import com.google.genai.types.MaskReferenceImage; +import com.google.genai.types.RawReferenceImage; +import com.google.genai.types.ReferenceImage; +import com.google.genai.types.StyleReferenceImage; +import com.google.genai.types.SubjectReferenceImage; +import java.io.IOException; + public final class TestUtils { static final String API_KEY = "api-key"; static final String PROJECT = "project"; static final String LOCATION = "location"; + private static ObjectMapper testObjectMapper; + + public static ObjectMapper getTestObjectMapper() { + if (testObjectMapper == null) { + testObjectMapper = JsonSerializable.objectMapper.copy(); + SimpleModule customModule = new SimpleModule(); + customModule.addDeserializer(ReferenceImage.class, new ReferenceImageDeserializer()); + testObjectMapper.registerModule(customModule); + } + return testObjectMapper; + } + + private static class ReferenceImageDeserializer extends StdDeserializer { + public ReferenceImageDeserializer() { + this(null); + } + + public ReferenceImageDeserializer(Class vc) { + super(vc); + } + + @Override + public ReferenceImage deserialize(JsonParser jp, DeserializationContext ctxt) + throws IOException { + JsonNode node = jp.getCodec().readTree(jp); + if (node.isObject()) { + com.fasterxml.jackson.databind.node.ObjectNode objNode = + (com.fasterxml.jackson.databind.node.ObjectNode) node; + if (objNode.has("maskImageConfig")) { + objNode.set("config", objNode.get("maskImageConfig")); + } + if (objNode.has("styleImageConfig")) { + objNode.set("config", objNode.get("styleImageConfig")); + } + if (objNode.has("controlImageConfig")) { + objNode.set("config", objNode.get("controlImageConfig")); + } + if (objNode.has("subjectImageConfig")) { + objNode.set("config", objNode.get("subjectImageConfig")); + } + if (objNode.has("contentImageConfig")) { + objNode.set("config", objNode.get("contentImageConfig")); + } + } + + if (node.has("referenceType")) { + String type = node.get("referenceType").asText(); + if ("REFERENCE_TYPE_RAW".equals(type)) { + return jp.getCodec().treeToValue(node, RawReferenceImage.class); + } else if ("REFERENCE_TYPE_MASK".equals(type)) { + return jp.getCodec().treeToValue(node, MaskReferenceImage.class); + } else if ("REFERENCE_TYPE_CONTROL".equals(type)) { + return jp.getCodec().treeToValue(node, ControlReferenceImage.class); + } else if ("REFERENCE_TYPE_STYLE".equals(type)) { + return jp.getCodec().treeToValue(node, StyleReferenceImage.class); + } else if ("REFERENCE_TYPE_SUBJECT".equals(type)) { + return jp.getCodec().treeToValue(node, SubjectReferenceImage.class); + } else if ("REFERENCE_TYPE_CONTENT".equals(type)) { + return jp.getCodec().treeToValue(node, ContentReferenceImage.class); + } + } + throw new IOException("Unknown or missing referenceType for ReferenceImage"); + } + } + private TestUtils() {} /** Creates a client given the vertexAI and replayId. Can be used in replay tests. */