Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/main/java/com/google/genai/ReplayApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
45 changes: 34 additions & 11 deletions src/test/java/com/google/genai/TableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,6 @@ private static Collection<DynamicTest> 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")) {
Expand All @@ -224,7 +214,8 @@ private static Collection<DynamicTest> 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();
}
Expand Down Expand Up @@ -337,6 +328,38 @@ private static Map<String, Object> 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;
}

Expand Down
80 changes: 80 additions & 0 deletions src/test/java/com/google/genai/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReferenceImage> {
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. */
Expand Down
Loading