diff --git a/.changes/common/distribution-doll-drop-bridge.json b/.changes/common/distribution-doll-drop-bridge.json new file mode 100644 index 00000000..34b090c9 --- /dev/null +++ b/.changes/common/distribution-doll-drop-bridge.json @@ -0,0 +1 @@ +{"type":"PATCH","changes":["Make JSON decoding lenient. This should fix issues with JSON literals."]} diff --git a/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt b/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt index b23faa76..d815f44a 100644 --- a/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt +++ b/common/src/main/kotlin/com/google/ai/client/generativeai/common/APIController.kt @@ -52,6 +52,7 @@ import kotlinx.serialization.json.Json internal val JSON = Json { ignoreUnknownKeys = true prettyPrint = false + isLenient = true } /** diff --git a/common/src/test/java/com/google/ai/client/generativeai/common/UnarySnapshotTests.kt b/common/src/test/java/com/google/ai/client/generativeai/common/UnarySnapshotTests.kt index 13ec5428..8801a955 100644 --- a/common/src/test/java/com/google/ai/client/generativeai/common/UnarySnapshotTests.kt +++ b/common/src/test/java/com/google/ai/client/generativeai/common/UnarySnapshotTests.kt @@ -24,6 +24,7 @@ import com.google.ai.client.generativeai.common.shared.FunctionCallPart import com.google.ai.client.generativeai.common.shared.HarmCategory import com.google.ai.client.generativeai.common.shared.TextPart import com.google.ai.client.generativeai.common.util.goldenUnaryFile +import com.google.ai.client.generativeai.common.util.shouldNotBeNullOrEmpty import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.collections.shouldNotBeEmpty import io.kotest.matchers.nulls.shouldNotBeNull @@ -313,4 +314,21 @@ internal class UnarySnapshotTests { callPart.functionCall.args["season"] shouldBe null } } + + @Test + fun `function call contains json literal`() = + goldenUnaryFile("success-function-call-json-literal.json") { + withTimeout(testTimeout) { + val response = apiController.generateContent(textGenerateContentRequest("prompt")) + val content = response.candidates.shouldNotBeNullOrEmpty().first().content + val callPart = + content.let { + it.shouldNotBeNull() + it.parts.shouldNotBeEmpty() + it.parts.first().shouldBeInstanceOf() + } + + callPart.functionCall.args["current"] shouldBe "true" + } + } } diff --git a/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt b/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt index 88599c80..fa84d258 100644 --- a/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt +++ b/common/src/test/java/com/google/ai/client/generativeai/common/util/tests.kt @@ -26,6 +26,8 @@ import com.google.ai.client.generativeai.common.RequestOptions import com.google.ai.client.generativeai.common.server.Candidate import com.google.ai.client.generativeai.common.shared.Content import com.google.ai.client.generativeai.common.shared.TextPart +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond import io.ktor.http.HttpHeaders @@ -184,3 +186,14 @@ internal fun loadGoldenFile(path: String): File = loadResourceFile("golden-files /** Loads a file from the test resources directory. */ internal fun loadResourceFile(path: String) = File("src/test/resources/$path") + +/** + * Ensures that a collection is neither null or empty. + * + * Syntax sugar for [shouldNotBeNull] and [shouldNotBeEmpty]. + */ +inline fun Collection?.shouldNotBeNullOrEmpty(): Collection { + shouldNotBeNull() + shouldNotBeEmpty() + return this +} diff --git a/common/src/test/resources/golden-files/unary/success-function-call-json-literal.json b/common/src/test/resources/golden-files/unary/success-function-call-json-literal.json new file mode 100644 index 00000000..fe457188 --- /dev/null +++ b/common/src/test/resources/golden-files/unary/success-function-call-json-literal.json @@ -0,0 +1,45 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "functionName", + "args": { + "original_title": "String", + "current": true + } + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 774, + "candidatesTokenCount": 4176, + "totalTokenCount": 4950 + } +}