Skip to content

Commit b3f3eb7

Browse files
authored
Merge branch 'main' into feat/pipeline/public-preview
2 parents 5d60214 + 6dab936 commit b3f3eb7

File tree

6 files changed

+167
-9
lines changed

6 files changed

+167
-9
lines changed

firebase-ai/CHANGELOG.md

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

3+
- [feature] Added support for configuring thinking levels with Gemini 3 series
4+
models and onwards. (#7599)
35
- [changed] Added `equals()` function to `GenerativeBackend`.
46

57
# 17.7.0

firebase-ai/api.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,12 +1289,26 @@ package com.google.firebase.ai.type {
12891289
method public com.google.firebase.ai.type.ThinkingConfig build();
12901290
method public com.google.firebase.ai.type.ThinkingConfig.Builder setIncludeThoughts(boolean includeThoughts);
12911291
method public com.google.firebase.ai.type.ThinkingConfig.Builder setThinkingBudget(int thinkingBudget);
1292+
method public com.google.firebase.ai.type.ThinkingConfig.Builder setThinkingLevel(com.google.firebase.ai.type.ThinkingLevel thinkingLevel);
12921293
}
12931294

12941295
public final class ThinkingConfigKt {
12951296
method public static com.google.firebase.ai.type.ThinkingConfig thinkingConfig(kotlin.jvm.functions.Function1<? super com.google.firebase.ai.type.ThinkingConfig.Builder,kotlin.Unit> init);
12961297
}
12971298

1299+
public final class ThinkingLevel {
1300+
method public int getOrdinal();
1301+
property public final int ordinal;
1302+
field public static final com.google.firebase.ai.type.ThinkingLevel.Companion Companion;
1303+
field public static final com.google.firebase.ai.type.ThinkingLevel HIGH;
1304+
field public static final com.google.firebase.ai.type.ThinkingLevel LOW;
1305+
field public static final com.google.firebase.ai.type.ThinkingLevel MEDIUM;
1306+
field public static final com.google.firebase.ai.type.ThinkingLevel MINIMAL;
1307+
}
1308+
1309+
public static final class ThinkingLevel.Companion {
1310+
}
1311+
12981312
public final class Tool {
12991313
method public static com.google.firebase.ai.type.Tool codeExecution();
13001314
method public static com.google.firebase.ai.type.Tool functionDeclarations(java.util.List<com.google.firebase.ai.type.FunctionDeclaration> functionDeclarations);

firebase-ai/gradle.properties

Lines changed: 1 addition & 1 deletion
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=17.7.1
15+
version=17.8.0
1616
latestReleasedVersion=17.7.0

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ package com.google.firebase.ai.type
1919
import kotlinx.serialization.SerialName
2020
import kotlinx.serialization.Serializable
2121

22-
/** Configuration parameters for thinking features. */
22+
/**
23+
* Gemini 2.5 series models and newer utilize a thinking process before generating a response. This
24+
* allows them to reason through complex problems and plan a more coherent and accurate answer. See
25+
* the [thinking documentation](https://firebase.google.com/docs/ai-logic/thinking) for more
26+
* details.
27+
*/
2328
public class ThinkingConfig
2429
private constructor(
2530
internal val thinkingBudget: Int? = null,
26-
internal val includeThoughts: Boolean? = null
31+
internal val includeThoughts: Boolean? = null,
32+
internal val thinkingLevel: ThinkingLevel? = null,
2733
) {
2834

2935
public class Builder() {
@@ -35,14 +41,26 @@ private constructor(
3541
@set:JvmSynthetic // hide void setter from Java
3642
public var includeThoughts: Boolean? = null
3743

44+
@JvmField
45+
@set:JvmSynthetic // hide void setter from Java
46+
public var thinkingLevel: ThinkingLevel? = null
47+
3848
/**
39-
* Indicates the thinking budget in tokens. `0` is disabled. `-1` is dynamic. The default values
40-
* and allowed ranges are model dependent.
49+
* Indicates the thinking budget in tokens.
50+
*
51+
* Use `0` for disabled, and `-1` for dynamic. The range of
52+
* [supported thinking budget values](https://firebase.google.com/docs/ai-logic/thinking#supported-thinking-budget-values)
53+
* depends on the model.
4154
*/
4255
public fun setThinkingBudget(thinkingBudget: Int): Builder = apply {
4356
this.thinkingBudget = thinkingBudget
4457
}
4558

59+
/** Indicates the thinking budget based in Levels. */
60+
public fun setThinkingLevel(thinkingLevel: ThinkingLevel): Builder = apply {
61+
this.thinkingLevel = thinkingLevel
62+
}
63+
4664
/**
4765
* Indicates whether to request the model to include the thoughts parts in the response.
4866
*
@@ -55,16 +73,26 @@ private constructor(
5573
this.includeThoughts = includeThoughts
5674
}
5775

58-
public fun build(): ThinkingConfig =
59-
ThinkingConfig(thinkingBudget = thinkingBudget, includeThoughts = includeThoughts)
76+
public fun build(): ThinkingConfig {
77+
if (thinkingBudget != null && thinkingLevel != null)
78+
throw IllegalArgumentException(
79+
"`thinkingBudget` already set. Cannot set both `thinkingBudget` and `thinkingLevel`"
80+
)
81+
return ThinkingConfig(
82+
thinkingBudget = thinkingBudget,
83+
includeThoughts = includeThoughts,
84+
thinkingLevel = thinkingLevel
85+
)
86+
}
6087
}
6188

62-
internal fun toInternal() = Internal(thinkingBudget, includeThoughts)
89+
internal fun toInternal() = Internal(thinkingBudget, includeThoughts, thinkingLevel?.toInternal())
6390

6491
@Serializable
6592
internal data class Internal(
6693
@SerialName("thinking_budget") val thinkingBudget: Int? = null,
67-
val includeThoughts: Boolean? = null
94+
val includeThoughts: Boolean? = null,
95+
@SerialName("thinking_level") val thinkingLevel: ThinkingLevel.Internal? = null,
6896
)
6997
}
7098

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
17+
package com.google.firebase.ai.type
18+
19+
import kotlinx.serialization.SerialName
20+
import kotlinx.serialization.Serializable
21+
22+
/** Specifies the quality of the thinking response. */
23+
public class ThinkingLevel private constructor(public val ordinal: Int) {
24+
internal fun toInternal() =
25+
when (this) {
26+
MINIMAL -> Internal.MINIMAL
27+
LOW -> Internal.LOW
28+
MEDIUM -> Internal.MEDIUM
29+
HIGH -> Internal.HIGH
30+
else -> throw makeMissingCaseException("ThinkingLevel", ordinal)
31+
}
32+
33+
@Serializable
34+
internal enum class Internal {
35+
@SerialName("THINKING_LEVEL_UNSPECIFIED") UNSPECIFIED,
36+
MINIMAL,
37+
LOW,
38+
MEDIUM,
39+
HIGH,
40+
}
41+
public companion object {
42+
/** A minimal quality thinking response, which provides the lowest latency. */
43+
@JvmField public val MINIMAL: ThinkingLevel = ThinkingLevel(0)
44+
/** A lower quality thinking response, which provides low latency. */
45+
@JvmField public val LOW: ThinkingLevel = ThinkingLevel(1)
46+
47+
/** A medium quality thinking response. */
48+
@JvmField public val MEDIUM: ThinkingLevel = ThinkingLevel(2)
49+
50+
/** A higher quality thinking response, which may increase latency. */
51+
@JvmField public val HIGH: ThinkingLevel = ThinkingLevel(3)
52+
}
53+
}

firebase-ai/src/test/java/com/google/firebase/ai/GenerativeModelTesting.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ import com.google.firebase.ai.type.RequestOptions
3333
import com.google.firebase.ai.type.SafetySetting
3434
import com.google.firebase.ai.type.ServerException
3535
import com.google.firebase.ai.type.TextPart
36+
import com.google.firebase.ai.type.ThinkingLevel
3637
import com.google.firebase.ai.type.content
38+
import com.google.firebase.ai.type.generationConfig
39+
import com.google.firebase.ai.type.thinkingConfig
3740
import io.kotest.assertions.json.shouldContainJsonKey
3841
import io.kotest.assertions.json.shouldContainJsonKeyValue
3942
import io.kotest.assertions.throwables.shouldThrow
@@ -249,4 +252,62 @@ internal class GenerativeModelTesting {
249252
)
250253
)
251254
}
255+
256+
@Test
257+
fun `thinkingLevel and thinkingBudget are mutually exclusive`() = doBlocking {
258+
val exception =
259+
shouldThrow<IllegalArgumentException> {
260+
thinkingConfig {
261+
thinkingLevel = ThinkingLevel.MEDIUM
262+
thinkingBudget = 1
263+
}
264+
}
265+
exception.message shouldContain "Cannot set both"
266+
}
267+
268+
@Test
269+
fun `correctly setting thinkingLevel in request`() = doBlocking {
270+
val mockEngine = MockEngine {
271+
respond(
272+
generateContentResponseAsJsonString("text response"),
273+
HttpStatusCode.OK,
274+
headersOf(HttpHeaders.ContentType, "application/json")
275+
)
276+
}
277+
278+
val apiController =
279+
APIController(
280+
"super_cool_test_key",
281+
"gemini-2.5-flash",
282+
RequestOptions(),
283+
mockEngine,
284+
TEST_CLIENT_ID,
285+
mockFirebaseApp,
286+
TEST_VERSION,
287+
TEST_APP_ID,
288+
null,
289+
)
290+
291+
val generativeModel =
292+
GenerativeModel(
293+
"gemini-2.5-flash",
294+
generationConfig =
295+
generationConfig {
296+
thinkingConfig = thinkingConfig { thinkingLevel = ThinkingLevel.MEDIUM }
297+
},
298+
controller = apiController
299+
)
300+
301+
withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") }
302+
303+
mockEngine.requestHistory.shouldNotBeEmpty()
304+
305+
val request = mockEngine.requestHistory.first().body
306+
request.shouldBeInstanceOf<TextContent>()
307+
308+
request.text.let {
309+
it shouldContainJsonKey "generation_config"
310+
it.shouldContainJsonKeyValue("$.generation_config.thinking_config.thinking_level", "MEDIUM")
311+
}
312+
}
252313
}

0 commit comments

Comments
 (0)