Skip to content

Commit 2db8764

Browse files
andrewheardrlazo
andauthored
[AI] Use common JSON encoder in LiveGenerativeModel (#7880)
Switched from the default `kotlinx.serialization.json.Json` encoder to `com.google.firebase.ai.common.JSON` in `LiveGenerativeModel`. `JSON` sets [`explicitNulls = false`](https://github.com/firebase/firebase-android-sdk/blob/537bb20b6034d52515eb52fb3acb494326902356/ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt#L93), which should prevent `parameters` and `parametersJsonSchema` from both being specified (one as `null`) in the serialized JSON. --------- Co-authored-by: Rodrigo Lazo Paz <rlazo@google.com>
1 parent 171b67c commit 2db8764

6 files changed

Lines changed: 36 additions & 49 deletions

File tree

ai-logic/firebase-ai/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
- [fixed] Fixed an issue causing Live API to fail when using the `GoogleAI` backend (#7880)
34
- [changed] Added the `hybrid` component to request headers coming from `prefer_in_cloud` configurations (#7857)
45

56
# 17.10.0

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.net.NetworkCapabilities
2323
import com.google.firebase.FirebaseApp
2424
import com.google.firebase.ai.common.APIController
2525
import com.google.firebase.ai.common.AppCheckHeaderProvider
26+
import com.google.firebase.ai.common.JSON
2627
import com.google.firebase.ai.generativemodel.CloudGenerativeModelProvider
2728
import com.google.firebase.ai.generativemodel.FallbackGenerativeModelProvider
2829
import com.google.firebase.ai.generativemodel.GenerativeModelProvider
@@ -52,7 +53,6 @@ import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
5253
import com.google.firebase.auth.internal.InternalAuthProvider
5354
import kotlinx.coroutines.flow.Flow
5455
import kotlinx.serialization.InternalSerializationApi
55-
import kotlinx.serialization.json.Json
5656
import kotlinx.serialization.json.JsonObject
5757
import kotlinx.serialization.json.JsonPrimitive
5858
import kotlinx.serialization.json.jsonObject
@@ -272,7 +272,7 @@ internal constructor(
272272
parameter: String
273273
): FunctionResponsePart {
274274
val inputDeserializer = functionDeclaration.inputSchema.getSerializer()
275-
val input = Json.decodeFromString(inputDeserializer, parameter)
275+
val input = JSON.decodeFromString(inputDeserializer, parameter)
276276
val functionReference =
277277
functionDeclaration.functionReference
278278
?: throw RuntimeException("Function reference for ${functionDeclaration.name} is missing")
@@ -281,7 +281,7 @@ internal constructor(
281281
val outputSerializer = functionDeclaration.outputSchema?.getSerializer()
282282
if (outputSerializer != null) {
283283
return FunctionResponsePart.from(
284-
Json.encodeToJsonElement(outputSerializer, output).jsonObject
284+
JSON.encodeToJsonElement(outputSerializer, output).jsonObject
285285
)
286286
.normalizeAgainstCall(functionCall)
287287
}

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import kotlin.coroutines.CoroutineContext
4040
import kotlinx.coroutines.channels.ClosedReceiveChannelException
4141
import kotlinx.serialization.ExperimentalSerializationApi
4242
import kotlinx.serialization.encodeToString
43-
import kotlinx.serialization.json.Json
4443
import kotlinx.serialization.json.JsonObject
4544

4645
/**
@@ -116,7 +115,7 @@ internal constructor(
116115
config?.outputAudioTranscription?.toInternal()
117116
)
118117
.toInternal()
119-
val data: String = Json.encodeToString(clientMessage)
118+
val data: String = JSON.encodeToString(clientMessage)
120119
var webSession: DefaultClientWebSocketSession? = null
121120
try {
122121
webSession = controller.getWebSocketSession(location)

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/TemplateImagenModel.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.google.firebase.ai
1919
import com.google.firebase.FirebaseApp
2020
import com.google.firebase.ai.common.APIController
2121
import com.google.firebase.ai.common.AppCheckHeaderProvider
22+
import com.google.firebase.ai.common.JSON
2223
import com.google.firebase.ai.common.TemplateGenerateImageRequest
2324
import com.google.firebase.ai.type.FirebaseAIException
2425
import com.google.firebase.ai.type.ImagenGenerationResponse
@@ -27,7 +28,6 @@ import com.google.firebase.ai.type.PublicPreviewAPI
2728
import com.google.firebase.ai.type.RequestOptions
2829
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
2930
import com.google.firebase.auth.internal.InternalAuthProvider
30-
import kotlinx.serialization.json.Json
3131
import kotlinx.serialization.json.jsonObject
3232
import org.json.JSONObject
3333

@@ -96,7 +96,7 @@ internal constructor(
9696
inputs: Map<String, Any>
9797
): TemplateGenerateImageRequest {
9898
return TemplateGenerateImageRequest(
99-
Json.parseToJsonElement(JSONObject(inputs).toString()).jsonObject
99+
JSON.parseToJsonElement(JSONObject(inputs).toString()).jsonObject
100100
)
101101
}
102102

ai-logic/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt

Lines changed: 26 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ import kotlinx.coroutines.launch
6363
import kotlinx.serialization.ExperimentalSerializationApi
6464
import kotlinx.serialization.Serializable
6565
import kotlinx.serialization.encodeToString
66-
import kotlinx.serialization.json.Json
6766

6867
/** Represents a live WebSocket session capable of streaming content to and from the server. */
6968
@PublicPreviewAPI
@@ -372,14 +371,10 @@ internal constructor(
372371
* response from the client.
373372
*/
374373
public suspend fun sendFunctionResponse(functionList: List<FunctionResponsePart>) {
375-
FirebaseAIException.catchAsync {
376-
val jsonString =
377-
Json.encodeToString(
378-
BidiGenerateContentToolResponseSetup(functionList.map { it.toInternalFunctionResponse() })
379-
.toInternal()
380-
)
381-
session.send(Frame.Text(jsonString))
382-
}
374+
sendFrame(
375+
BidiGenerateContentToolResponseSetup(functionList.map { it.toInternalFunctionResponse() })
376+
.toInternal()
377+
)
383378
}
384379

385380
/**
@@ -393,11 +388,7 @@ internal constructor(
393388
* results, send 16-bit PCM audio at 24kHz.
394389
*/
395390
public suspend fun sendAudioRealtime(audio: InlineData) {
396-
FirebaseAIException.catchAsync {
397-
val jsonString =
398-
Json.encodeToString(BidiGenerateContentRealtimeInputSetup(audio = audio).toInternal())
399-
session.send(Frame.Text(jsonString))
400-
}
391+
sendFrame(BidiGenerateContentRealtimeInputSetup(audio = audio).toInternal())
401392
}
402393

403394
/**
@@ -415,11 +406,7 @@ internal constructor(
415406
* data (for example, `image/png`, `image/jpeg`, etc.).
416407
*/
417408
public suspend fun sendVideoRealtime(video: InlineData) {
418-
FirebaseAIException.catchAsync {
419-
val jsonString =
420-
Json.encodeToString(BidiGenerateContentRealtimeInputSetup(video = video).toInternal())
421-
session.send(Frame.Text(jsonString))
422-
}
409+
sendFrame(BidiGenerateContentRealtimeInputSetup(video = video).toInternal())
423410
}
424411

425412
/**
@@ -428,11 +415,7 @@ internal constructor(
428415
* @param text Text content to append to the current client's conversation.
429416
*/
430417
public suspend fun sendTextRealtime(text: String) {
431-
FirebaseAIException.catchAsync {
432-
val jsonString =
433-
Json.encodeToString(BidiGenerateContentRealtimeInputSetup(text = text).toInternal())
434-
session.send(Frame.Text(jsonString))
435-
}
418+
sendFrame(BidiGenerateContentRealtimeInputSetup(text = text).toInternal())
436419
}
437420

438421
/**
@@ -446,16 +429,10 @@ internal constructor(
446429
public suspend fun sendMediaStream(
447430
mediaChunks: List<MediaData>,
448431
) {
449-
FirebaseAIException.catchAsync {
450-
val jsonString =
451-
Json.encodeToString(
452-
BidiGenerateContentRealtimeInputSetup(
453-
mediaChunks.map { InlineData(it.data, it.mimeType) }
454-
)
455-
.toInternal()
456-
)
457-
session.send(Frame.Text(jsonString))
458-
}
432+
sendFrame(
433+
BidiGenerateContentRealtimeInputSetup(mediaChunks.map { InlineData(it.data, it.mimeType) })
434+
.toInternal()
435+
)
459436
}
460437

461438
/**
@@ -466,14 +443,24 @@ internal constructor(
466443
* @param content Client [Content] to be sent to the model.
467444
*/
468445
public suspend fun send(content: Content) {
446+
sendFrame(
447+
BidiGenerateContentClientContentSetup(listOf(content.toInternal()), true).toInternal()
448+
)
449+
}
450+
451+
/**
452+
* Encodes the provided data to a JSON string and sends it over the WebSocket session.
453+
*
454+
* If an exception is thrown, it will be handled by [FirebaseAIException.catchAsync]
455+
*
456+
* @param T The type of the data to be sent, which must be serializable.
457+
* @param data The data object to be encoded and sent.
458+
*/
459+
private suspend inline fun <reified T> sendFrame(data: T) =
469460
FirebaseAIException.catchAsync {
470-
val jsonString =
471-
Json.encodeToString(
472-
BidiGenerateContentClientContentSetup(listOf(content.toInternal()), true).toInternal()
473-
)
461+
val jsonString = JSON.encodeToString(data)
474462
session.send(Frame.Text(jsonString))
475463
}
476-
}
477464

478465
/**
479466
* Sends text to the model.

ai-logic/firebase-ai/src/test/java/com/google/firebase/ai/type/ThinkingConfigTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
package com.google.firebase.ai.type
1818

19+
import com.google.firebase.ai.common.JSON
1920
import io.kotest.assertions.json.shouldEqualJson
2021
import io.kotest.matchers.equals.shouldBeEqual
2122
import kotlinx.serialization.encodeToString
22-
import kotlinx.serialization.json.Json
2323
import org.junit.Test
2424

2525
internal class ThinkingConfigTest {
@@ -36,7 +36,7 @@ internal class ThinkingConfigTest {
3636
"""
3737
.trimIndent()
3838

39-
Json.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson)
39+
JSON.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson)
4040
}
4141

4242
@Test
@@ -51,7 +51,7 @@ internal class ThinkingConfigTest {
5151
"""
5252
.trimIndent()
5353

54-
Json.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson)
54+
JSON.encodeToString(thinkingConfig.toInternal()).shouldEqualJson(expectedJson)
5555
}
5656

5757
@Test

0 commit comments

Comments
 (0)