Skip to content

Commit 69bd6a2

Browse files
committed
Merge branch 'main' into dl/grounding
2 parents c1deb3b + 11dfdac commit 69bd6a2

File tree

48 files changed

+1489
-203
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1489
-203
lines changed

.github/workflows/dataconnect.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ env:
2525
FDC_JAVA_VERSION: ${{ inputs.javaVersion || '17' }}
2626
FDC_ANDROID_EMULATOR_API_LEVEL: ${{ inputs.androidEmulatorApiLevel || '34' }}
2727
FDC_NODEJS_VERSION: ${{ inputs.nodeJsVersion || '20' }}
28-
FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.2.0' }}
28+
FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.5.1' }}
2929
FDC_FIREBASE_TOOLS_DIR: /tmp/firebase-tools
3030
FDC_FIREBASE_COMMAND: /tmp/firebase-tools/node_modules/.bin/firebase
3131
FDC_PYTHON_VERSION: ${{ inputs.pythonVersion || '3.13' }}

.github/workflows/dataconnect_demo_app.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ on:
1818

1919
env:
2020
FDC_NODE_VERSION: ${{ inputs.nodeVersion || '20' }}
21-
FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.2.0' }}
21+
FDC_FIREBASE_TOOLS_VERSION: ${{ inputs.firebaseToolsVersion || '14.5.1' }}
2222
FDC_JAVA_VERSION: ${{ inputs.javaVersion || '17' }}
2323
FDC_FIREBASE_TOOLS_DIR: ${{ github.workspace }}/firebase-tools
2424
FDC_FIREBASE_COMMAND: ${{ github.workspace }}/firebase-tools/node_modules/.bin/firebase
@@ -109,7 +109,7 @@ jobs:
109109
--no-daemon \
110110
${{ (inputs.gradleInfoLog && '--info') || '' }} \
111111
--profile \
112-
-PdataConnect.minimalApp.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \
112+
-PdataConnect.demo.firebaseCommand=${{ env.FDC_FIREBASE_COMMAND }} \
113113
assemble test
114114
115115
- uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1

.github/workflows/sessions-e2e.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: set up JDK 17
2424
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
2525
with:
26-
java-version: '11'
26+
java-version: '17'
2727
distribution: 'temurin'
2828
cache: gradle
2929

@@ -39,4 +39,4 @@ jobs:
3939
env:
4040
FTL_RESULTS_BUCKET: fireescape
4141
run: |
42-
./gradlew :firebase-sessions:test-app:deviceCheck withErrorProne -PtargetBackend="prod" -PtriggerCrashes
42+
./gradlew :firebase-sessions:test-app:deviceCheck withErrorProne -PtargetBackend="prod"

firebase-ai/CHANGELOG.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
# Unreleased
2-
32
* [feature] Add support for Grounding with Google Search (#7042).
3+
* [changed] Deprecate the `totalBillableCharacters` field (only usable with pre-2.0 models). (#7042)
4+
* [feature] Added support for extra schema properties like `title`, `minItems`, `maxItems`, `minimum`
5+
and `maximum`. As well as support for the `anyOf` schema. (#7013)
6+
7+
# 16.1.0
48
* [fixed] Fixed `FirebaseAI.getInstance` StackOverflowException (#6971)
5-
* [fixed] Fixed an issue that was causing the SDK to send empty `FunctionDeclaration` descriptions to the API.
9+
* [fixed] Fixed an issue that was causing the SDK to send empty `FunctionDeclaration` descriptions to the API.
10+
* [changed] Introduced the `Voice` class, which accepts a voice name, and deprecated the `Voices` class.
11+
* [changed] **Breaking Change**: Updated `SpeechConfig` to take in `Voice` class instead of `Voices` class.
12+
* **Action Required:** Update all references of `SpeechConfig` initialization to use `Voice` class.
13+
* [fixed] Fix incorrect model name in count token requests to the developer API backend
14+
615

716
# 16.0.0
817
* [feature] Initial release of the Firebase AI SDK (`firebase-ai`). This SDK *replaces* the previous
@@ -21,4 +30,3 @@
2130

2231
Note: This feature is in Public Preview, which means that it is not subject to any SLA or
2332
deprecation policy and could change in backwards-incompatible ways.
24-

firebase-ai/api.txt

Lines changed: 72 additions & 14 deletions
Large diffs are not rendered by default.

firebase-ai/firebase-ai.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ firebaseLibrary {
2828
testLab.enabled = false
2929
publishJavadoc = true
3030
releaseNotes {
31-
name.set("{{firebase_ai}}")
31+
name.set("{{firebase_ai_logic}}")
3232
versionName.set("ai")
3333
hasKTX.set(false)
3434
}
@@ -67,7 +67,10 @@ android {
6767
targetSdk = targetSdkVersion
6868
baseline = file("lint-baseline.xml")
6969
}
70-
sourceSets { getByName("test").java.srcDirs("src/testUtil") }
70+
sourceSets {
71+
// getByName("test").java.srcDirs("src/testUtil")
72+
getByName("androidTest") { kotlin.srcDirs("src/testUtil") }
73+
}
7174
}
7275

7376
// Enable Kotlin "Explicit API Mode". This causes the Kotlin compiler to fail if any

firebase-ai/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
version=16.0.1
16-
latestReleasedVersion=16.0.0
15+
version=16.2.0
16+
latestReleasedVersion=16.1.0
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.firebase.ai
17+
18+
import androidx.test.platform.app.InstrumentationRegistry
19+
import com.google.firebase.FirebaseApp
20+
import com.google.firebase.FirebaseOptions
21+
import com.google.firebase.ai.type.GenerativeBackend
22+
23+
class AIModels {
24+
25+
companion object {
26+
private val API_KEY: String = ""
27+
private val APP_ID: String = ""
28+
private val PROJECT_ID: String = "fireescape-integ-tests"
29+
// General purpose models
30+
var app: FirebaseApp? = null
31+
var flash2Model: GenerativeModel? = null
32+
var flash2LiteModel: GenerativeModel? = null
33+
34+
/** Returns a list of general purpose models to test */
35+
fun getModels(): List<GenerativeModel> {
36+
if (flash2Model == null) {
37+
setup()
38+
}
39+
return listOf(flash2Model!!, flash2LiteModel!!)
40+
}
41+
42+
fun app(): FirebaseApp {
43+
if (app == null) {
44+
setup()
45+
}
46+
return app!!
47+
}
48+
49+
fun setup() {
50+
val context = InstrumentationRegistry.getInstrumentation().context
51+
app =
52+
FirebaseApp.initializeApp(
53+
context,
54+
FirebaseOptions.Builder()
55+
.setApiKey(API_KEY)
56+
.setApplicationId(APP_ID)
57+
.setProjectId(PROJECT_ID)
58+
.build()
59+
)
60+
flash2Model =
61+
FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI())
62+
.generativeModel(
63+
modelName = "gemini-2.0-flash",
64+
)
65+
flash2LiteModel =
66+
FirebaseAI.getInstance(app!!, GenerativeBackend.vertexAI())
67+
.generativeModel(
68+
modelName = "gemini-2.0-flash-lite",
69+
)
70+
}
71+
}
72+
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.firebase.ai
17+
18+
import android.graphics.Bitmap
19+
import com.google.firebase.ai.AIModels.Companion.getModels
20+
import com.google.firebase.ai.type.Content
21+
import com.google.firebase.ai.type.ContentModality
22+
import com.google.firebase.ai.type.CountTokensResponse
23+
import java.io.ByteArrayOutputStream
24+
import kotlinx.coroutines.runBlocking
25+
import org.junit.Test
26+
27+
class CountTokensTests {
28+
29+
/** Ensures that the token count is expected for simple words. */
30+
@Test
31+
fun testCountTokensAmount() {
32+
for (model in getModels()) {
33+
runBlocking {
34+
val response = model.countTokens("this is five different words")
35+
assert(response.totalTokens == 5)
36+
assert(response.promptTokensDetails.size == 1)
37+
assert(response.promptTokensDetails[0].modality == ContentModality.TEXT)
38+
assert(response.promptTokensDetails[0].tokenCount == 5)
39+
}
40+
}
41+
}
42+
43+
/** Ensures that the model returns token counts in the correct modality for text. */
44+
@Test
45+
fun testCountTokensTextModality() {
46+
for (model in getModels()) {
47+
runBlocking {
48+
val response = model.countTokens("this is a text prompt")
49+
checkTokenCountsMatch(response)
50+
assert(response.promptTokensDetails.size == 1)
51+
assert(containsModality(response, ContentModality.TEXT))
52+
}
53+
}
54+
}
55+
56+
/** Ensures that the model returns token counts in the correct modality for bitmap images. */
57+
@Test
58+
fun testCountTokensImageModality() {
59+
for (model in getModels()) {
60+
runBlocking {
61+
val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
62+
val response = model.countTokens(bitmap)
63+
checkTokenCountsMatch(response)
64+
assert(response.promptTokensDetails.size == 1)
65+
assert(containsModality(response, ContentModality.IMAGE))
66+
}
67+
}
68+
}
69+
70+
/**
71+
* Ensures the model can count tokens for multiple modalities at once, and return the
72+
* corresponding token modalities correctly.
73+
*/
74+
@Test
75+
fun testCountTokensTextAndImageModality() {
76+
for (model in getModels()) {
77+
runBlocking {
78+
val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
79+
val response =
80+
model.countTokens(
81+
Content.Builder().text("this is text").build(),
82+
Content.Builder().image(bitmap).build()
83+
)
84+
checkTokenCountsMatch(response)
85+
assert(response.promptTokensDetails.size == 2)
86+
assert(containsModality(response, ContentModality.TEXT))
87+
assert(containsModality(response, ContentModality.IMAGE))
88+
}
89+
}
90+
}
91+
92+
/**
93+
* Ensures the model can count the tokens for a sent file. Additionally, ensures that the model
94+
* treats this sent file as the modality of the mime type, in this case, a plaintext file has its
95+
* tokens counted as `ContentModality.TEXT`.
96+
*/
97+
@Test
98+
fun testCountTokensTextFileModality() {
99+
for (model in getModels()) {
100+
runBlocking {
101+
val response =
102+
model.countTokens(
103+
Content.Builder().inlineData("this is text".toByteArray(), "text/plain").build()
104+
)
105+
checkTokenCountsMatch(response)
106+
assert(response.totalTokens == 3)
107+
assert(response.promptTokensDetails.size == 1)
108+
assert(containsModality(response, ContentModality.TEXT))
109+
}
110+
}
111+
}
112+
113+
/**
114+
* Ensures the model can count the tokens for a sent file. Additionally, ensures that the model
115+
* treats this sent file as the modality of the mime type, in this case, a PNG encoded bitmap has
116+
* its tokens counted as `ContentModality.IMAGE`.
117+
*/
118+
@Test
119+
fun testCountTokensImageFileModality() {
120+
for (model in getModels()) {
121+
runBlocking {
122+
val bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)
123+
val stream = ByteArrayOutputStream()
124+
bitmap.compress(Bitmap.CompressFormat.PNG, 1, stream)
125+
val array = stream.toByteArray()
126+
val response = model.countTokens(Content.Builder().inlineData(array, "image/png").build())
127+
checkTokenCountsMatch(response)
128+
assert(response.promptTokensDetails.size == 1)
129+
assert(containsModality(response, ContentModality.IMAGE))
130+
}
131+
}
132+
}
133+
134+
/**
135+
* Ensures that nothing is free, that is, empty content contains no tokens. For some reason, this
136+
* is treated as `ContentModality.TEXT`.
137+
*/
138+
@Test
139+
fun testCountTokensNothingIsFree() {
140+
for (model in getModels()) {
141+
runBlocking {
142+
val response = model.countTokens(Content.Builder().build())
143+
checkTokenCountsMatch(response)
144+
assert(response.totalTokens == 0)
145+
assert(response.promptTokensDetails.size == 1)
146+
assert(containsModality(response, ContentModality.TEXT))
147+
}
148+
}
149+
}
150+
151+
/**
152+
* Checks if the model can count the tokens for a sent file. Additionally, ensures that the model
153+
* treats this sent file as the modality of the mime type, in this case, a JSON file is not
154+
* recognized, and no tokens are counted. This ensures if/when the model can handle JSON, our
155+
* testing makes us aware.
156+
*/
157+
@Test
158+
fun testCountTokensJsonFileModality() {
159+
for (model in getModels()) {
160+
runBlocking {
161+
val json =
162+
"""
163+
{
164+
"foo": "bar",
165+
"baz": 3,
166+
"qux": [
167+
{
168+
"quux": [
169+
1,
170+
2
171+
]
172+
}
173+
]
174+
}
175+
"""
176+
.trimIndent()
177+
val response =
178+
model.countTokens(
179+
Content.Builder().inlineData(json.toByteArray(), "application/json").build()
180+
)
181+
checkTokenCountsMatch(response)
182+
assert(response.promptTokensDetails.isEmpty())
183+
assert(response.totalTokens == 0)
184+
}
185+
}
186+
}
187+
188+
fun checkTokenCountsMatch(response: CountTokensResponse) {
189+
assert(sumTokenCount(response) == response.totalTokens)
190+
}
191+
192+
fun sumTokenCount(response: CountTokensResponse): Int {
193+
return response.promptTokensDetails.sumOf { it.tokenCount }
194+
}
195+
196+
fun containsModality(response: CountTokensResponse, modality: ContentModality): Boolean {
197+
for (token in response.promptTokensDetails) {
198+
if (token.modality == modality) {
199+
return true
200+
}
201+
}
202+
return false
203+
}
204+
}

0 commit comments

Comments
 (0)