diff --git a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt index 9f250dc4..21b8bd30 100644 --- a/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt +++ b/generativeai/src/main/java/com/google/ai/client/generativeai/internal/api/server/Types.kt @@ -19,9 +19,11 @@ package com.google.ai.client.generativeai.internal.api.server import com.google.ai.client.generativeai.internal.api.shared.Content import com.google.ai.client.generativeai.internal.api.shared.HarmCategory import com.google.ai.client.generativeai.internal.util.FirstOrdinalSerializer +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonNames internal object BlockReasonSerializer : KSerializer by FirstOrdinalSerializer(BlockReason::class) @@ -54,7 +56,10 @@ internal data class Candidate( val citationMetadata: CitationMetadata? = null ) -@Serializable internal data class CitationMetadata(val citationSources: List) +@Serializable +internal data class CitationMetadata +@OptIn(ExperimentalSerializationApi::class) +constructor(@JsonNames("citations") val citationSources: List) @Serializable internal data class CitationSources( diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt index 7a38a398..4615bcdc 100644 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt +++ b/generativeai/src/test/java/com/google/ai/client/generativeai/StreamingSnapshotTests.kt @@ -142,6 +142,17 @@ internal class StreamingSnapshotTests { } } + @Test + fun `citation returns correctly when using alternative name`() = + goldenStreamingFile("success-citations-altname.txt") { + val responses = model.generateContentStream() + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.any { it.candidates.any { it.citationMetadata.isNotEmpty() } } shouldBe true + } + } + @Test fun `stopped for recitation`() = goldenStreamingFile("failure-recitation-no-content.txt") { diff --git a/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt b/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt index 44c9b425..3c15a9e5 100644 --- a/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt +++ b/generativeai/src/test/java/com/google/ai/client/generativeai/UnarySnapshotTests.kt @@ -124,6 +124,17 @@ internal class UnarySnapshotTests { } } + @Test + fun `citation returns correctly when using alternative name`() = + goldenUnaryFile("success-citations-altname.json") { + withTimeout(testTimeout) { + val response = model.generateContent() + + response.candidates.isEmpty() shouldBe false + response.candidates.first().citationMetadata.isNotEmpty() shouldBe true + } + } + @Test fun `invalid response`() = goldenUnaryFile("failure-invalid-response.json") { diff --git a/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt b/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt new file mode 100644 index 00000000..4c682dc8 --- /dev/null +++ b/generativeai/src/test/resources/golden-files/streaming/success-citations-altname.txt @@ -0,0 +1,12 @@ +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} + +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} + +data: {"candidates": [{"content": {"parts": [{"text": "placeholder"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citations": [{"startIndex": 574,"endIndex": 705,"uri": "https://example.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://example.com","license": ""}]}}]} + diff --git a/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json b/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json new file mode 100644 index 00000000..7adaad5f --- /dev/null +++ b/generativeai/src/test/resources/golden-files/unary/success-citations-altname.json @@ -0,0 +1,70 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "placeholder" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ], + "citationMetadata": { + "citations": [ + { + "startIndex": 574, + "endIndex": 705, + "uri": "https://example.com/", + "license": "" + }, + { + "startIndex": 899, + "endIndex": 1026, + "uri": "https://example.com/", + "license": "" + } + ] + } + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +}