diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 3a1e67a3b2f..def6d24ce6c 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -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 diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index 357fb1c2a8b..c77d286500d 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -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 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 functionDeclarations); diff --git a/firebase-ai/gradle.properties b/firebase-ai/gradle.properties index 215d4a50f32..2181d491391 100644 --- a/firebase-ai/gradle.properties +++ b/firebase-ai/gradle.properties @@ -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 diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt index b220de57e86..0355ec80952 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingConfig.kt @@ -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() { @@ -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. * @@ -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, ) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingLevel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingLevel.kt new file mode 100644 index 00000000000..b124ca57296 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ThinkingLevel.kt @@ -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) + } +} 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 582b722376a..b84f89fd223 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 @@ -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 @@ -249,4 +252,62 @@ internal class GenerativeModelTesting { ) ) } + + @Test + fun `thinkingLevel and thinkingBudget are mutually exclusive`() = doBlocking { + val exception = + shouldThrow { + 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() + + request.text.let { + it shouldContainJsonKey "generation_config" + it.shouldContainJsonKeyValue("$.generation_config.thinking_config.thinking_level", "MEDIUM") + } + } }