From a4aa415a26a430fed15b7bf32b848fedcbc75b5c Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 2 Mar 2026 10:37:28 -0500 Subject: [PATCH 1/5] [AI] Use `hybrid` tag for `prefer_in_cloud` instances. To correctly tag hybrid request, not only `prefer_on_device` must be tagged, but also `prefer_in_cloud`, since it's not the default configuration and shows an intent to use the feature. Internal b/489062704 --- .../com/google/firebase/ai/GenerativeModel.kt | 10 +- .../ai/GenerativeModelBuilderTests.kt | 125 ++++++++++++++++++ 2 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt index 1620ac86700..cc437f3cb90 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/GenerativeModel.kt @@ -315,10 +315,10 @@ internal constructor( /** * Returns a [GenerativeModelProvider] that uses the cloud backend. * - * @param isFallback Whether this provider is being used as a fallback for another provider. + * @param isHybrid Whether this provider is being used in a hybrid configuration. * @return A [GenerativeModelProvider] that uses the cloud backend. */ - internal fun buildCloudModelProvider(isFallback: Boolean = false): GenerativeModelProvider { + internal fun buildCloudModelProvider(isHybrid: Boolean = false): GenerativeModelProvider { return CloudGenerativeModelProvider( modelName = modelName, generationConfig = generationConfig, @@ -332,7 +332,7 @@ internal constructor( apiKey, modelName, requestOptions, - if (isFallback) "${apiClient} hybrid" else apiClient, + if (isHybrid) "${apiClient} hybrid" else apiClient, firebaseApp, AppCheckHeaderProvider( TAG, @@ -359,13 +359,13 @@ internal constructor( InferenceMode.PREFER_ON_DEVICE -> { FallbackGenerativeModelProvider( defaultModel = buildOnDeviceModelProvider(), - fallbackModel = buildCloudModelProvider(isFallback = true), + fallbackModel = buildCloudModelProvider(isHybrid = true), shouldFallbackInException = true ) } InferenceMode.PREFER_IN_CLOUD -> FallbackGenerativeModelProvider( - defaultModel = buildCloudModelProvider(), + defaultModel = buildCloudModelProvider(isHybrid = true), fallbackModel = buildOnDeviceModelProvider(), precondition = NetworkStatusChecker( diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt new file mode 100644 index 00000000000..a70839426f8 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt @@ -0,0 +1,125 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.ai + +import android.content.Context +import android.net.ConnectivityManager +import com.google.firebase.FirebaseApp +import com.google.firebase.ai.generativemodel.CloudGenerativeModelProvider +import com.google.firebase.ai.generativemodel.FallbackGenerativeModelProvider +import com.google.firebase.ai.generativemodel.GenerativeModelProvider +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.PublicPreviewAPI +import io.kotest.matchers.string.shouldContain +import io.kotest.matchers.string.shouldNotContain +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk +import org.junit.Test + +@OptIn(PublicPreviewAPI::class) +internal class GenerativeModelBuilderTests { + + @Test + fun `getModelProvider uses hybrid suffix in PREFER_ON_DEVICE mode`() { + val firebaseApp = mockk(relaxed = true) + every { firebaseApp.options.applicationId } returns "1:12345:android:67890" + + val builder = GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ).apply { + onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_ON_DEVICE) + } + + val provider = builder.getModelProvider() + + provider.shouldBeInstanceOf() + val fallbackModel = provider.getPrivateField("fallbackModel") + + fallbackModel.shouldBeInstanceOf() + val controller = fallbackModel.getPrivateField("controller") + + val apiClient = controller.getPrivateField("apiClient") + apiClient shouldContain " hybrid" + } + + @Test + fun `getModelProvider uses hybrid suffix in PREFER_IN_CLOUD mode`() { + val firebaseApp = mockk(relaxed = true) + every { firebaseApp.options.applicationId } returns "1:12345:android:67890" + val context = mockk(relaxed = true) + val connectivityManager = mockk(relaxed = true) + every { firebaseApp.applicationContext } returns context + every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + + val builder = GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ).apply { + onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_IN_CLOUD) + } + + val provider = builder.getModelProvider() + + provider.shouldBeInstanceOf() + val defaultModel = provider.getPrivateField("defaultModel") + + defaultModel.shouldBeInstanceOf() + val controller = defaultModel.getPrivateField("controller") + + val apiClient = controller.getPrivateField("apiClient") + apiClient shouldContain " hybrid" + } + + @Test + fun `getModelProvider does NOT use hybrid suffix in ONLY_IN_CLOUD mode`() { + val firebaseApp = mockk(relaxed = true) + every { firebaseApp.options.applicationId } returns "1:12345:android:67890" + + val builder = GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ).apply { + onDeviceConfig = OnDeviceConfig.IN_CLOUD + } + + val provider = builder.getModelProvider() + + provider.shouldBeInstanceOf() + val controller = provider.getPrivateField("controller") + + val apiClient = controller.getPrivateField("apiClient") + apiClient shouldNotContain " hybrid" + } +} + +private fun Any.getPrivateField(name: String): T { + val field = this.javaClass.getDeclaredField(name) + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + return field.get(this) as T +} From ff6a8637b4b22168514061da9adfe5b646601ffd Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 2 Mar 2026 10:52:03 -0500 Subject: [PATCH 2/5] Address comment --- .../ai/GenerativeModelBuilderTests.kt | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt index a70839426f8..ca1e6192b6d 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt @@ -29,16 +29,25 @@ import io.kotest.matchers.string.shouldNotContain import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.every import io.mockk.mockk +import org.junit.Before import org.junit.Test @OptIn(PublicPreviewAPI::class) internal class GenerativeModelBuilderTests { - @Test - fun `getModelProvider uses hybrid suffix in PREFER_ON_DEVICE mode`() { - val firebaseApp = mockk(relaxed = true) + val firebaseApp = mockk(relaxed = true) + + @Before + fun setUp() { every { firebaseApp.options.applicationId } returns "1:12345:android:67890" + val context = mockk(relaxed = true) + val connectivityManager = mockk(relaxed = true) + every { firebaseApp.applicationContext } returns context + every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + } + @Test + fun `getModelProvider uses hybrid suffix in PREFER_ON_DEVICE mode`() { val builder = GenerativeModel.Builder( modelName = "gemini-1.5-flash", apiKey = "apiKey", @@ -63,13 +72,6 @@ internal class GenerativeModelBuilderTests { @Test fun `getModelProvider uses hybrid suffix in PREFER_IN_CLOUD mode`() { - val firebaseApp = mockk(relaxed = true) - every { firebaseApp.options.applicationId } returns "1:12345:android:67890" - val context = mockk(relaxed = true) - val connectivityManager = mockk(relaxed = true) - every { firebaseApp.applicationContext } returns context - every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager - val builder = GenerativeModel.Builder( modelName = "gemini-1.5-flash", apiKey = "apiKey", @@ -94,9 +96,6 @@ internal class GenerativeModelBuilderTests { @Test fun `getModelProvider does NOT use hybrid suffix in ONLY_IN_CLOUD mode`() { - val firebaseApp = mockk(relaxed = true) - every { firebaseApp.options.applicationId } returns "1:12345:android:67890" - val builder = GenerativeModel.Builder( modelName = "gemini-1.5-flash", apiKey = "apiKey", From e9dad357bc4dc0c4ad07576571177bac7e13155d Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 2 Mar 2026 10:52:32 -0500 Subject: [PATCH 3/5] Fix format --- .../ai/GenerativeModelBuilderTests.kt | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt index ca1e6192b6d..e3be2c87651 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt @@ -48,15 +48,15 @@ internal class GenerativeModelBuilderTests { @Test fun `getModelProvider uses hybrid suffix in PREFER_ON_DEVICE mode`() { - val builder = GenerativeModel.Builder( - modelName = "gemini-1.5-flash", - apiKey = "apiKey", - firebaseApp = firebaseApp, - useLimitedUseAppCheckTokens = false, - generativeBackend = GenerativeBackend.googleAI() - ).apply { - onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_ON_DEVICE) - } + val builder = + GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ) + .apply { onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_ON_DEVICE) } val provider = builder.getModelProvider() @@ -64,7 +64,8 @@ internal class GenerativeModelBuilderTests { val fallbackModel = provider.getPrivateField("fallbackModel") fallbackModel.shouldBeInstanceOf() - val controller = fallbackModel.getPrivateField("controller") + val controller = + fallbackModel.getPrivateField("controller") val apiClient = controller.getPrivateField("apiClient") apiClient shouldContain " hybrid" @@ -72,15 +73,15 @@ internal class GenerativeModelBuilderTests { @Test fun `getModelProvider uses hybrid suffix in PREFER_IN_CLOUD mode`() { - val builder = GenerativeModel.Builder( - modelName = "gemini-1.5-flash", - apiKey = "apiKey", - firebaseApp = firebaseApp, - useLimitedUseAppCheckTokens = false, - generativeBackend = GenerativeBackend.googleAI() - ).apply { - onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_IN_CLOUD) - } + val builder = + GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ) + .apply { onDeviceConfig = OnDeviceConfig(InferenceMode.PREFER_IN_CLOUD) } val provider = builder.getModelProvider() @@ -88,7 +89,8 @@ internal class GenerativeModelBuilderTests { val defaultModel = provider.getPrivateField("defaultModel") defaultModel.shouldBeInstanceOf() - val controller = defaultModel.getPrivateField("controller") + val controller = + defaultModel.getPrivateField("controller") val apiClient = controller.getPrivateField("apiClient") apiClient shouldContain " hybrid" @@ -96,20 +98,21 @@ internal class GenerativeModelBuilderTests { @Test fun `getModelProvider does NOT use hybrid suffix in ONLY_IN_CLOUD mode`() { - val builder = GenerativeModel.Builder( - modelName = "gemini-1.5-flash", - apiKey = "apiKey", - firebaseApp = firebaseApp, - useLimitedUseAppCheckTokens = false, - generativeBackend = GenerativeBackend.googleAI() - ).apply { - onDeviceConfig = OnDeviceConfig.IN_CLOUD - } + val builder = + GenerativeModel.Builder( + modelName = "gemini-1.5-flash", + apiKey = "apiKey", + firebaseApp = firebaseApp, + useLimitedUseAppCheckTokens = false, + generativeBackend = GenerativeBackend.googleAI() + ) + .apply { onDeviceConfig = OnDeviceConfig.IN_CLOUD } val provider = builder.getModelProvider() provider.shouldBeInstanceOf() - val controller = provider.getPrivateField("controller") + val controller = + provider.getPrivateField("controller") val apiClient = controller.getPrivateField("apiClient") apiClient shouldNotContain " hybrid" @@ -119,6 +122,5 @@ internal class GenerativeModelBuilderTests { private fun Any.getPrivateField(name: String): T { val field = this.javaClass.getDeclaredField(name) field.isAccessible = true - @Suppress("UNCHECKED_CAST") - return field.get(this) as T + @Suppress("UNCHECKED_CAST") return field.get(this) as T } From 04e779e7fce32908839e81d178854fb21d90c940 Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 2 Mar 2026 10:55:35 -0500 Subject: [PATCH 4/5] Add changelog entry --- firebase-ai/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 935e8c94ea8..56dbda19d36 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,7 +1,9 @@ # Unreleased +- [changed] Added the `hybrid` component to request headers coming from `prefer_in_cloud` configurations (#7857) - [feature] Added experimental support for on-device inference (#7739) - [feature] Added automatic function calling support with `AutoFunctionDeclaration`. + - [feature] Added no-argument overloads for `Tool.urlContext()` and `Tool.googleSearch()`. # 17.9.0 From 592a86395fb079ebe58946ed47e24aa49930a6bd Mon Sep 17 00:00:00 2001 From: Rodrigo Lazo Paz Date: Mon, 2 Mar 2026 12:33:06 -0500 Subject: [PATCH 5/5] Format & address comment --- firebase-ai/CHANGELOG.md | 1 - .../firebase/ai/common/APIController.kt | 2 +- .../CloudGenerativeModelProvider.kt | 2 +- .../FallbackGenerativeModelProvider.kt | 4 +-- .../ai/GenerativeModelBuilderTests.kt | 26 ++++++------------- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 56dbda19d36..f35e564a0fc 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -3,7 +3,6 @@ - [changed] Added the `hybrid` component to request headers coming from `prefer_in_cloud` configurations (#7857) - [feature] Added experimental support for on-device inference (#7739) - [feature] Added automatic function calling support with `AutoFunctionDeclaration`. - - [feature] Added no-argument overloads for `Tool.urlContext()` and `Tool.googleSearch()`. # 17.9.0 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 118cca166f7..3fa4e46acc4 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 @@ -113,7 +113,7 @@ internal constructor( model: String, private val requestOptions: RequestOptions, httpEngine: HttpClientEngine, - private val apiClient: String, + internal val apiClient: String, private val firebaseApp: FirebaseApp, private val appVersion: Int = 0, private val googleAppId: String, diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/CloudGenerativeModelProvider.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/CloudGenerativeModelProvider.kt index 3e7735f3a62..f12d5d4dff6 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/CloudGenerativeModelProvider.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/CloudGenerativeModelProvider.kt @@ -48,7 +48,7 @@ internal class CloudGenerativeModelProvider( private val toolConfig: ToolConfig? = null, private val systemInstruction: Content? = null, private val generativeBackend: GenerativeBackend = GenerativeBackend.googleAI(), - private val controller: APIController, + internal val controller: APIController, ) : GenerativeModelProvider { override suspend fun generateContent(prompt: List): GenerateContentResponse = diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/FallbackGenerativeModelProvider.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/FallbackGenerativeModelProvider.kt index 2ebc9857b9e..b9c9e36fa62 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/FallbackGenerativeModelProvider.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/generativemodel/FallbackGenerativeModelProvider.kt @@ -47,8 +47,8 @@ import kotlinx.coroutines.flow.onEach * Defaults to `true`. */ internal class FallbackGenerativeModelProvider( - private val defaultModel: GenerativeModelProvider, - private val fallbackModel: GenerativeModelProvider, + internal val defaultModel: GenerativeModelProvider, + internal val fallbackModel: GenerativeModelProvider, private val precondition: () -> Boolean = { true }, private val shouldFallbackInException: Boolean = true ) : GenerativeModelProvider { diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt index e3be2c87651..656a7038d69 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelBuilderTests.kt @@ -21,7 +21,6 @@ import android.net.ConnectivityManager import com.google.firebase.FirebaseApp import com.google.firebase.ai.generativemodel.CloudGenerativeModelProvider import com.google.firebase.ai.generativemodel.FallbackGenerativeModelProvider -import com.google.firebase.ai.generativemodel.GenerativeModelProvider import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.PublicPreviewAPI import io.kotest.matchers.string.shouldContain @@ -61,13 +60,12 @@ internal class GenerativeModelBuilderTests { val provider = builder.getModelProvider() provider.shouldBeInstanceOf() - val fallbackModel = provider.getPrivateField("fallbackModel") + val fallbackModel = provider.fallbackModel fallbackModel.shouldBeInstanceOf() - val controller = - fallbackModel.getPrivateField("controller") + val controller = fallbackModel.controller - val apiClient = controller.getPrivateField("apiClient") + val apiClient = controller.apiClient apiClient shouldContain " hybrid" } @@ -86,13 +84,12 @@ internal class GenerativeModelBuilderTests { val provider = builder.getModelProvider() provider.shouldBeInstanceOf() - val defaultModel = provider.getPrivateField("defaultModel") + val defaultModel = provider.defaultModel defaultModel.shouldBeInstanceOf() - val controller = - defaultModel.getPrivateField("controller") + val controller = defaultModel.controller - val apiClient = controller.getPrivateField("apiClient") + val apiClient = controller.apiClient apiClient shouldContain " hybrid" } @@ -111,16 +108,9 @@ internal class GenerativeModelBuilderTests { val provider = builder.getModelProvider() provider.shouldBeInstanceOf() - val controller = - provider.getPrivateField("controller") + val controller = provider.controller - val apiClient = controller.getPrivateField("apiClient") + val apiClient = controller.apiClient apiClient shouldNotContain " hybrid" } } - -private fun Any.getPrivateField(name: String): T { - val field = this.javaClass.getDeclaredField(name) - field.isAccessible = true - @Suppress("UNCHECKED_CAST") return field.get(this) as T -}