Skip to content
2 changes: 2 additions & 0 deletions firebase-ai/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

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

# 17.7.0
Expand Down
14 changes: 14 additions & 0 deletions firebase-ai/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1289,12 +1289,26 @@ package com.google.firebase.ai.type {
method public com.google.firebase.ai.type.ThinkingConfig build();
method public com.google.firebase.ai.type.ThinkingConfig.Builder setIncludeThoughts(boolean includeThoughts);
method public com.google.firebase.ai.type.ThinkingConfig.Builder setThinkingBudget(int thinkingBudget);
method public com.google.firebase.ai.type.ThinkingConfig.Builder setThinkingLevel(com.google.firebase.ai.type.ThinkingLevel thinkingLevel);
}

public final class ThinkingConfigKt {
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);
}

public final class ThinkingLevel {
method public int getOrdinal();
property public final int ordinal;
field public static final com.google.firebase.ai.type.ThinkingLevel.Companion Companion;
field public static final com.google.firebase.ai.type.ThinkingLevel HIGH;
field public static final com.google.firebase.ai.type.ThinkingLevel LOW;
field public static final com.google.firebase.ai.type.ThinkingLevel MEDIUM;
field public static final com.google.firebase.ai.type.ThinkingLevel MINIMAL;
}

public static final class ThinkingLevel.Companion {
}

public final class Tool {
method public static com.google.firebase.ai.type.Tool codeExecution();
method public static com.google.firebase.ai.type.Tool functionDeclarations(java.util.List<com.google.firebase.ai.type.FunctionDeclaration> functionDeclarations);
Expand Down
2 changes: 1 addition & 1 deletion firebase-ai/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
# See the License for the specific language governing permissions and
# limitations under the License.

version=17.7.1
version=17.8.0
latestReleasedVersion=17.7.0
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ package com.google.firebase.ai.type
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

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

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

@JvmField
@set:JvmSynthetic // hide void setter from Java
public var thinkingLevel: ThinkingLevel? = null

/**
* Indicates the thinking budget in tokens. `0` is disabled. `-1` is dynamic. The default values
* and allowed ranges are model dependent.
* Indicates the thinking budget in tokens.
*
* Use `0` for disabled, and `-1` for dynamic. The range of
* [supported thinking budget values](https://firebase.google.com/docs/ai-logic/thinking#supported-thinking-budget-values)
* depends on the model.
*/
public fun setThinkingBudget(thinkingBudget: Int): Builder = apply {
this.thinkingBudget = thinkingBudget
}

/** Indicates the thinking budget based in Levels. */
public fun setThinkingLevel(thinkingLevel: ThinkingLevel): Builder = apply {
this.thinkingLevel = thinkingLevel
}

/**
* Indicates whether to request the model to include the thoughts parts in the response.
*
Expand All @@ -55,16 +73,26 @@ private constructor(
this.includeThoughts = includeThoughts
}

public fun build(): ThinkingConfig =
ThinkingConfig(thinkingBudget = thinkingBudget, includeThoughts = includeThoughts)
public fun build(): ThinkingConfig {
if (thinkingBudget != null && thinkingLevel != null)
throw IllegalArgumentException(
"`thinkingBudget` already set. Cannot set both `thinkingBudget` and `thinkingLevel`"
)
return ThinkingConfig(
thinkingBudget = thinkingBudget,
includeThoughts = includeThoughts,
thinkingLevel = thinkingLevel
)
}
}

internal fun toInternal() = Internal(thinkingBudget, includeThoughts)
internal fun toInternal() = Internal(thinkingBudget, includeThoughts, thinkingLevel?.toInternal())

@Serializable
internal data class Internal(
@SerialName("thinking_budget") val thinkingBudget: Int? = null,
val includeThoughts: Boolean? = null
val includeThoughts: Boolean? = null,
@SerialName("thinking_level") val thinkingLevel: ThinkingLevel.Internal? = null,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2025 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.type

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/** Specifies the quality of the thinking response. */
public class ThinkingLevel private constructor(public val ordinal: Int) {
internal fun toInternal() =
when (this) {
MINIMAL -> Internal.MINIMAL
LOW -> Internal.LOW
MEDIUM -> Internal.MEDIUM
HIGH -> Internal.HIGH
else -> throw makeMissingCaseException("ThinkingLevel", ordinal)
}

@Serializable
internal enum class Internal {
@SerialName("THINKING_LEVEL_UNSPECIFIED") UNSPECIFIED,
MINIMAL,
LOW,
MEDIUM,
HIGH,
}
public companion object {
/** A minimal quality thinking response, which provides the lowest latency. */
@JvmField public val MINIMAL: ThinkingLevel = ThinkingLevel(0)
/** A lower quality thinking response, which provides low latency. */
@JvmField public val LOW: ThinkingLevel = ThinkingLevel(1)

/** A medium quality thinking response. */
@JvmField public val MEDIUM: ThinkingLevel = ThinkingLevel(2)

/** A higher quality thinking response, which may increase latency. */
@JvmField public val HIGH: ThinkingLevel = ThinkingLevel(3)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import com.google.firebase.ai.type.RequestOptions
import com.google.firebase.ai.type.SafetySetting
import com.google.firebase.ai.type.ServerException
import com.google.firebase.ai.type.TextPart
import com.google.firebase.ai.type.ThinkingLevel
import com.google.firebase.ai.type.content
import com.google.firebase.ai.type.generationConfig
import com.google.firebase.ai.type.thinkingConfig
import io.kotest.assertions.json.shouldContainJsonKey
import io.kotest.assertions.json.shouldContainJsonKeyValue
import io.kotest.assertions.throwables.shouldThrow
Expand Down Expand Up @@ -249,4 +252,62 @@ internal class GenerativeModelTesting {
)
)
}

@Test
fun `thinkingLevel and thinkingBudget are mutually exclusive`() = doBlocking {
val exception =
shouldThrow<IllegalArgumentException> {
thinkingConfig {
thinkingLevel = ThinkingLevel.MEDIUM
thinkingBudget = 1
}
}
exception.message shouldContain "Cannot set both"
}

@Test
fun `correctly setting thinkingLevel in request`() = doBlocking {
val mockEngine = MockEngine {
respond(
generateContentResponseAsJsonString("text response"),
HttpStatusCode.OK,
headersOf(HttpHeaders.ContentType, "application/json")
)
}

val apiController =
APIController(
"super_cool_test_key",
"gemini-2.5-flash",
RequestOptions(),
mockEngine,
TEST_CLIENT_ID,
mockFirebaseApp,
TEST_VERSION,
TEST_APP_ID,
null,
)

val generativeModel =
GenerativeModel(
"gemini-2.5-flash",
generationConfig =
generationConfig {
thinkingConfig = thinkingConfig { thinkingLevel = ThinkingLevel.MEDIUM }
},
controller = apiController
)

withTimeout(5.seconds) { generativeModel.generateContent("my test prompt") }

mockEngine.requestHistory.shouldNotBeEmpty()

val request = mockEngine.requestHistory.first().body
request.shouldBeInstanceOf<TextContent>()

request.text.let {
it shouldContainJsonKey "generation_config"
it.shouldContainJsonKeyValue("$.generation_config.thinking_config.thinking_level", "MEDIUM")
}
}
}
Loading