From 5364515f282f39193521a07e3ec3a28d87923018 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Tue, 27 Jan 2026 11:38:05 -0500 Subject: [PATCH 1/6] [AI] Include the "X-Android-Package" and "X-Android-Cert" headers These headers are necessary to support [API Key restrictions](https://docs.cloud.google.com/docs/authentication/api-keys#adding-application-restrictions). This feature enable you to limit which apps (by matching package name and cert) are allowed to make request. **Important**: We still *strongly* recommend the use of Firebase AppCheck instead of, or in addition to, API key restrictions. --- .../firebase/ai/common/APIController.kt | 56 +++++++++++++++++++ .../firebase/ai/DevAPIUnarySnapshotTests.kt | 3 + .../firebase/ai/GenerativeModelTesting.kt | 7 +++ .../firebase/ai/common/APIControllerTests.kt | 15 ++++- .../google/firebase/ai/common/util/tests.kt | 4 ++ .../java/com/google/firebase/ai/util/tests.kt | 4 ++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index e992f92e674..f080ddfc990 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -16,6 +16,9 @@ package com.google.firebase.ai.common +import android.content.pm.PackageManager +import android.content.pm.Signature +import android.os.Build import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp @@ -65,6 +68,9 @@ import io.ktor.http.contentType import io.ktor.http.withCharset import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.charsets.Charset +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import kotlin.collections.isEmpty import kotlin.math.max import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -268,6 +274,8 @@ internal constructor( contentType(ContentType.Application.Json) header("x-goog-api-key", key) header("x-goog-api-client", apiClient) + header("X-Android-Package", firebaseApp.applicationContext.packageName) + header("X-Android-Cert", getSigningCertFingerprint() ?: "") if (firebaseApp.isDataCollectionDefaultEnabled) { header("X-Firebase-AppId", googleAppId) header("X-Firebase-AppVersion", appVersion) @@ -345,6 +353,54 @@ internal constructor( } } + @OptIn(ExperimentalStdlibApi::class) + private fun getSigningCertFingerprint(): String? { + val signature = getCurrentSignature() ?: return null + try { + val messageDigest = MessageDigest.getInstance("SHA-1") + val digest = messageDigest.digest(signature.toByteArray()) + return digest.toHexString(HexFormat.UpperCase) + } catch (e: NoSuchAlgorithmException) { + Log.w(TAG, "No support for SHA-1 algorithm found.", e) + return null + } + } + + @Suppress("DEPRECATION") + private fun getCurrentSignature(): Signature? { + val packageName = firebaseApp.applicationContext.packageName + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + val packageInfo = + firebaseApp.applicationContext.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNATURES + ) + val signatures = packageInfo?.signatures ?: return null + if (signatures.size > 1) { + Log.d( + TAG, + "Multiple certificates found. On Android < P, certificate order is non-deterministic; an rotated/old cert may be used." + ) + } + return signatures.firstOrNull() + } + val packageInfo = + firebaseApp.applicationContext.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES + ) + val signingInfo = packageInfo?.signingInfo ?: return null + if (signingInfo.hasMultipleSigners()) { + Log.d(TAG, "App has been signed with multiple certificates. Defaulting to the first one") + return signingInfo.apkContentsSigners.first() + } else { + // The `signingCertificateHistory` contains a sorted list of certificates used to sign this + // artifact, with the original one first, and once it's rotated, the current one is added at + // the end of the list. See the method's refdocs for more info. + return signingInfo.signingCertificateHistory.lastOrNull() + } + } + companion object { private val TAG = APIController::class.java.simpleName diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt index e0ba386091b..fbcde7d9a15 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt @@ -16,6 +16,7 @@ package com.google.firebase.ai +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.ai.type.FinishReason import com.google.firebase.ai.type.InvalidAPIKeyException import com.google.firebase.ai.type.PublicPreviewAPI @@ -36,7 +37,9 @@ import io.ktor.http.HttpStatusCode import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.withTimeout import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) internal class DevAPIUnarySnapshotTests { private val testTimeout = 5.seconds diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt index b84f89fd223..9b6c5f50a34 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt @@ -16,6 +16,9 @@ package com.google.firebase.ai +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.JSON @@ -54,8 +57,10 @@ import kotlinx.coroutines.withTimeout import kotlinx.serialization.encodeToString import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito +@RunWith(AndroidJUnit4::class) internal class GenerativeModelTesting { private val TEST_CLIENT_ID = "test" private val TEST_APP_ID = "1:android:12345" @@ -65,7 +70,9 @@ internal class GenerativeModelTesting { @Before fun setup() { + val context = ApplicationProvider.getApplicationContext() Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + Mockito.`when`(mockFirebaseApp.applicationContext).thenReturn(context) } @Test diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt index f3e818085f7..e333569824f 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/APIControllerTests.kt @@ -16,6 +16,9 @@ package com.google.firebase.ai.common +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp import com.google.firebase.ai.BuildConfig import com.google.firebase.ai.common.util.commonTest @@ -56,8 +59,8 @@ import kotlinx.serialization.json.JsonObject import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.Parameterized import org.mockito.Mockito +import org.robolectric.ParameterizedRobolectricTestRunner private val TEST_CLIENT_ID = "genai-android/test" @@ -65,6 +68,7 @@ private val TEST_APP_ID = "1:android:12345" private val TEST_VERSION = 1 +@RunWith(AndroidJUnit4::class) internal class APIControllerTests { private val testTimeout = 5.seconds @@ -96,13 +100,16 @@ internal class APIControllerTests { } @OptIn(ExperimentalSerializationApi::class) +@RunWith(AndroidJUnit4::class) internal class RequestFormatTests { private val mockFirebaseApp = Mockito.mock() @Before fun setup() { + val context = ApplicationProvider.getApplicationContext() Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + Mockito.`when`(mockFirebaseApp.applicationContext).thenReturn(context) } @Test @@ -454,13 +461,15 @@ internal class RequestFormatTests { } } -@RunWith(Parameterized::class) +@RunWith(ParameterizedRobolectricTestRunner::class) internal class ModelNamingTests(private val modelName: String, private val actualName: String) { private val mockFirebaseApp = Mockito.mock() @Before fun setup() { + val context = ApplicationProvider.getApplicationContext() Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + Mockito.`when`(mockFirebaseApp.applicationContext).thenReturn(context) } @Test @@ -495,7 +504,7 @@ internal class ModelNamingTests(private val modelName: String, private val actua companion object { @JvmStatic - @Parameterized.Parameters + @ParameterizedRobolectricTestRunner.Parameters fun data() = listOf( arrayOf("gemini-pro", "models/gemini-pro"), diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt index d9081cae371..93053d004ba 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/tests.kt @@ -18,6 +18,8 @@ package com.google.firebase.ai.common.util +import android.content.Context +import androidx.test.core.app.ApplicationProvider import com.google.firebase.FirebaseApp import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.JSON @@ -95,7 +97,9 @@ internal fun commonTest( block: CommonTest, ) = doBlocking { val mockFirebaseApp = Mockito.mock() + val context = ApplicationProvider.getApplicationContext() Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + Mockito.`when`(mockFirebaseApp.applicationContext).thenReturn(context) val channel = ByteChannel(autoFlush = true) val apiController = diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt index 1394bb07e86..fec9a7c8844 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt @@ -18,6 +18,8 @@ package com.google.firebase.ai.util +import android.content.Context +import androidx.test.core.app.ApplicationProvider import com.google.firebase.FirebaseApp import com.google.firebase.ai.GenerativeModel import com.google.firebase.ai.ImagenModel @@ -107,9 +109,11 @@ internal fun commonTest( block: CommonTest, ) = doBlocking { val channel = ByteChannel(autoFlush = true) + val context = ApplicationProvider.getApplicationContext() val mockFirebaseApp = Mockito.mock() Mockito.`when`(mockFirebaseApp.isDataCollectionDefaultEnabled).thenReturn(false) + Mockito.`when`(mockFirebaseApp.applicationContext).thenReturn(context) val apiController = APIController( "super_cool_test_key", From ff2f859ad97c67ff93f7200518ff1bd8ba627f30 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Tue, 27 Jan 2026 12:01:18 -0500 Subject: [PATCH 2/6] Add Changelog entry --- firebase-ai/CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index def6d24ce6c..eed3bb71303 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -2,7 +2,9 @@ - [feature] Added support for configuring thinking levels with Gemini 3 series models and onwards. (#7599) -- [changed] Added `equals()` function to `GenerativeBackend`. +- [feature] Added support for [API Key + restrictions](https://docs.cloud.google.com/docs/authentication/api-keys#adding-application-restrictions) (#7679) +- [changed] Added `equals()` function to `GenerativeBackend`. (#7597) # 17.7.0 From 0b497fc448ed346552c37c27503d502a3b153112 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Tue, 27 Jan 2026 12:29:14 -0500 Subject: [PATCH 3/6] Update firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../main/kotlin/com/google/firebase/ai/common/APIController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index f080ddfc990..7afd723ccbf 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -70,7 +70,7 @@ import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.charsets.Charset import java.security.MessageDigest import java.security.NoSuchAlgorithmException -import kotlin.collections.isEmpty +import kotlin.math.max import kotlin.math.max import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds From 65e855e6300b655d9d67f0e5d90fc0c16e9b6d6d Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Date: Tue, 27 Jan 2026 12:30:09 -0500 Subject: [PATCH 4/6] Remove redudant math.max introduced by gemini --- .../main/kotlin/com/google/firebase/ai/common/APIController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index 7afd723ccbf..6ef90fb95a4 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -71,7 +71,6 @@ import io.ktor.utils.io.charsets.Charset import java.security.MessageDigest import java.security.NoSuchAlgorithmException import kotlin.math.max -import kotlin.math.max import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineName From c01e01fbacf7c7f42c1add2585be38acb02cd000 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Wed, 28 Jan 2026 05:54:25 -0500 Subject: [PATCH 5/6] Don't compute the fingerprint on each request --- .../firebase/ai/common/APIController.kt | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt index 6ef90fb95a4..118cca166f7 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/APIController.kt @@ -143,6 +143,8 @@ internal constructor( ) private val model = fullModelName(model) + private val appPackageName by lazy { firebaseApp.applicationContext.packageName } + private val appSigningCertFingerprint by lazy { getSigningCertFingerprint() } private val client = HttpClient(httpEngine) { @@ -273,8 +275,8 @@ internal constructor( contentType(ContentType.Application.Json) header("x-goog-api-key", key) header("x-goog-api-client", apiClient) - header("X-Android-Package", firebaseApp.applicationContext.packageName) - header("X-Android-Cert", getSigningCertFingerprint() ?: "") + header("X-Android-Package", appPackageName) + header("X-Android-Cert", appSigningCertFingerprint ?: "") if (firebaseApp.isDataCollectionDefaultEnabled) { header("X-Firebase-AppId", googleAppId) header("X-Firebase-AppVersion", appVersion) @@ -370,10 +372,15 @@ internal constructor( val packageName = firebaseApp.applicationContext.packageName if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { val packageInfo = - firebaseApp.applicationContext.packageManager.getPackageInfo( - packageName, - PackageManager.GET_SIGNATURES - ) + try { + firebaseApp.applicationContext.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNATURES + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + return null + } val signatures = packageInfo?.signatures ?: return null if (signatures.size > 1) { Log.d( @@ -384,10 +391,15 @@ internal constructor( return signatures.firstOrNull() } val packageInfo = - firebaseApp.applicationContext.packageManager.getPackageInfo( - packageName, - PackageManager.GET_SIGNING_CERTIFICATES - ) + try { + firebaseApp.applicationContext.packageManager.getPackageInfo( + packageName, + PackageManager.GET_SIGNING_CERTIFICATES + ) + } catch (e: PackageManager.NameNotFoundException) { + Log.d(TAG, "PackageManager couldn't find the package \"$packageName\"") + return null + } val signingInfo = packageInfo?.signingInfo ?: return null if (signingInfo.hasMultipleSigners()) { Log.d(TAG, "App has been signed with multiple certificates. Defaulting to the first one") From f63390ac5ac3582b24fa954580c6f7879d61eddf Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Wed, 28 Jan 2026 06:57:04 -0500 Subject: [PATCH 6/6] Add tests --- .../firebase/ai/GenerativeModelTesting.kt | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt index 9b6c5f50a34..7bb2de04318 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt @@ -17,6 +17,7 @@ package com.google.firebase.ai import android.content.Context +import android.content.pm.PackageManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.firebase.FirebaseApp @@ -25,6 +26,7 @@ import com.google.firebase.ai.common.JSON import com.google.firebase.ai.common.util.doBlocking import com.google.firebase.ai.type.Candidate import com.google.firebase.ai.type.Content +import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.GenerateContentResponse import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.HarmBlockMethod @@ -44,6 +46,7 @@ import io.kotest.assertions.json.shouldContainJsonKey import io.kotest.assertions.json.shouldContainJsonKeyValue import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain import io.kotest.matchers.types.shouldBeInstanceOf import io.ktor.client.engine.mock.MockEngine @@ -53,6 +56,7 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.content.TextContent import io.ktor.http.headersOf import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.withTimeout import kotlinx.serialization.encodeToString import org.junit.Before @@ -119,6 +123,104 @@ internal class GenerativeModelTesting { } } + @Test + fun `security headers are included in request`() = doBlocking { + val mockEngine = MockEngine { + respond( + generateContentResponseAsJsonString("text response"), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + val generativeModel = generativeModelWithMockEngine(mockEngine) + + withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") } + + val headers = mockEngine.requestHistory.first().headers + headers["X-Android-Package"] shouldBe "com.google.firebase.ai.test" + // X-Android-Cert will be empty because Robolectric doesn't provide signatures by default + headers["X-Android-Cert"] shouldBe "" + } + + @Test + fun `security headers are included in streaming request`() = doBlocking { + val mockEngine = MockEngine { + respond( + generateContentResponseAsJsonString("text response"), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + val generativeModel = generativeModelWithMockEngine(mockEngine) + + withTimeout(5.seconds) { generativeModel.generateContentStream("my test prompt").collect() } + + val headers = mockEngine.requestHistory.first().headers + headers["X-Android-Package"] shouldBe "com.google.firebase.ai.test" + // X-Android-Cert will be empty because Robolectric doesn't provide signatures by default + headers["X-Android-Cert"] shouldBe "" + } + + @Test + fun `security headers are included in countTokens request`() = doBlocking { + val mockEngine = MockEngine { + respond( + JSON.encodeToString(CountTokensResponse.Internal(totalTokens = 10)), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + val generativeModel = generativeModelWithMockEngine(mockEngine) + + withTimeout(5.seconds) { generativeModel.countTokens("my test prompt") } + + val headers = mockEngine.requestHistory.first().headers + headers["X-Android-Package"] shouldBe "com.google.firebase.ai.test" + // X-Android-Cert will be empty because Robolectric doesn't provide signatures by default + headers["X-Android-Cert"] shouldBe "" + } + + @Test + fun `X-Android-Cert is empty when signatures are missing`() = doBlocking { + val mockEngine = MockEngine { + respond( + generateContentResponseAsJsonString("text response"), + HttpStatusCode.OK, + headersOf(HttpHeaders.ContentType, "application/json") + ) + } + + val mockPackageManager = Mockito.mock(PackageManager::class.java) + val mockContext = Mockito.mock(Context::class.java) + Mockito.`when`(mockContext.packageName).thenReturn("com.test.app") + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + + val mockApp = Mockito.mock(FirebaseApp::class.java) + Mockito.`when`(mockApp.applicationContext).thenReturn(mockContext) + + val apiController = + APIController( + "super_cool_test_key", + "gemini-2.5-flash", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + val generativeModel = GenerativeModel("gemini-2.5-flash", controller = apiController) + + withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") } + + val headers = mockEngine.requestHistory.first().headers + headers["X-Android-Package"] shouldBe "com.test.app" + // X-Android-Cert will be empty because Robolectric doesn't provide signatures by default + headers["X-Android-Cert"] shouldBe "" + } + @Test fun `exception thrown when using invalid location`() = doBlocking { val mockEngine = MockEngine { @@ -317,4 +419,21 @@ internal class GenerativeModelTesting { it.shouldContainJsonKeyValue("$.generation_config.thinking_config.thinking_level", "MEDIUM") } } + + private fun generativeModelWithMockEngine(mockEngine: MockEngine): GenerativeModel { + val apiController = + APIController( + "super_cool_test_key", + "gemini-2.5-flash", + RequestOptions(), + mockEngine, + TEST_CLIENT_ID, + mockFirebaseApp, + TEST_VERSION, + TEST_APP_ID, + null, + ) + + return GenerativeModel("gemini-2.5-flash", controller = apiController) + } }