diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 49721333ac8..487d0229d3b 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -56,10 +56,14 @@ jobs: distribution: temurin cache: gradle - - name: Clone mock responses + - name: Clone vertexai mock responses if: matrix.module == ':firebase-vertexai' run: firebase-vertexai/update_responses.sh + - name: Clone ai mock responses + if: matrix.module == ':firebase-ai' + run: firebase-ai/update_responses.sh + - name: Add google-services.json env: INTEG_TESTS_GOOGLE_SERVICES: ${{ secrets.INTEG_TESTS_GOOGLE_SERVICES }} diff --git a/firebase-ai/CHANGELOG.md b/firebase-ai/CHANGELOG.md index 0334dc35f6b..0042f361afc 100644 --- a/firebase-ai/CHANGELOG.md +++ b/firebase-ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased * [changed] **Breaking Change**: `LiveModelFutures.connect` now returns `ListenableFuture` instead of `ListenableFuture`. - * **Action Required:** Remove any transformations from LiveSession object to LiveSessionFutures object. + * **Action Required:** Remove any transformations from LiveSession object to LiveSessionFutures object. * **Action Required:** Change type of variable handling `LiveModelFutures.connect` to `ListenableFuture` * [changed] **Breaking Change**: Removed `UNSPECIFIED` value for enum class `ResponseModality` * **Action Required:** Remove all references to `ResponseModality.UNSPECIFIED` @@ -13,88 +13,3 @@ * [fixed] Fixed an issue with `LiveContentResponse` audio data not being present when the model was interrupted or the turn completed. (#6870) * [fixed] Fixed an issue with `LiveSession` not converting exceptions to `FirebaseVertexAIException`. (#6870) - - -# 16.3.0 -* [feature] Emits a warning when attempting to use an incompatible model with - `GenerativeModel` or `ImagenModel`. -* [changed] Added new exception type for quota exceeded scenarios. -* [feature] `CountTokenRequest` now includes `GenerationConfig` from the model. -* [feature] **Public Preview:** Added support for streaming input and output (including audio) using the [Gemini Live API](/docs/vertex-ai/live-api?platform=android) - **Note**: This feature is in Public Preview, which means that it is not subject to any SLA or deprecation policy and could change in backwards-incompatible ways. -* [changed] **Breaking Change**: `ImagenInlineImage.data` now returns the raw - image bytes (in JPEG or PNG format, as specified in - `ImagenInlineImage.mimeType`) instead of Base64-encoded data. (#6800) - * **Action Required:** Remove any Base64 decoding from your - `ImagenInlineImage.data` usage. - * The `asBitmap()` helper method is unaffected and requires no code changes. - -# 16.2.0 -* [fixed] Added support for new values sent by the server for `FinishReason` and `BlockReason`. -* [changed] Added support for modality-based token count. (#6658) -* [feature] Added support for generating images with Imagen models. - -# 16.1.0 -* [changed] Internal improvements to correctly handle empty model responses. - -# 16.0.2 -* [fixed] Improved error message when using an invalid location. (#6428) -* [fixed] Fixed issue where Firebase App Check error tokens were unintentionally missing from the requests. (#6409) -* [fixed] Clarified in the documentation that `Schema.integer` and `Schema.float` only provide hints to the model. (#6420) -* [fixed] Fixed issue were `Schema.double` set the format parameter in `Schema`. (#6432) - -# 16.0.1 -* [fixed] Fixed issue where authorization headers weren't correctly formatted and were ignored by the backend. (#6400) - -# 16.0.0 -* [feature] {{firebase_vertexai}} is now Generally Available (GA) and can be - used in production apps. - - Use the {{firebase_vertexai_sdk}} to call the {{gemini_api_vertexai_long}} - directly from your app. This client SDK is built specifically for use with - Android apps, offering security options against unauthorized clients - as well as integrations with other Firebase services. - - * If you're new to this library, visit the - [getting started guide](/docs/vertex-ai/get-started?platform=android). - - * If you were using the preview version of the library, visit the - [migration guide](/docs/vertex-ai/migrate-to-ga?platform=android) to learn - about some important updates. -* [changed] **Breaking Change**: Changed `functionCallingConfig` parameter type to be nullable in `ToolConfig`. (#6373) -* [changed] **Breaking Change**: Removed `functionResponse` accessor method from `GenerateContentResponse`. (#6373) -* [changed] **Breaking Change**: Migrated `FirebaseVertexAIException` from a sealed class to an abstract class, and marked constructors as internal. (#6368) -* [feature] Added support for `title` and `publicationDate` in citations. (#6309) -* [feature] Added support for `frequencyPenalty`, `presencePenalty`, and `HarmBlockMethod`. (#6309) -* [changed] **Breaking Change**: Introduced `Citations` class. Now `CitationMetadata` wraps that type. (#6276) -* [changed] **Breaking Change**: Reworked `Schema` declaration mechanism. (#6258) -* [changed] **Breaking Change**: Reworked function calling mechanism to use the new `Schema` format. Function calls no longer use native types, nor include references to the actual executable code. (#6258) -* [changed] **Breaking Change**: Made `totalBillableCharacters` field in `CountTokens` nullable and optional. (#6294) -* [changed] **Breaking Change**: Removed `UNKNOWN` option for the `HarmBlockThreshold` enum. (#6294) -* [changed] **Breaking Change**: Removed `UNSPECIFIED` option for the `HarmBlockThreshold`, `HarmProbability`, `HarmSeverity`, and `BlockReason` enums. (#6294) -* [changed] **Breaking Change**: Renamed `BlockThreshold` as `HarmBlockThreshold`. (#6262) -* [changed] **Breaking Change**: Renamed all types and methods starting with `blob` to start with `inlineData`. (#6309) -* [changed] **Breaking Change**: Changed the order of arguments in `InlineDataPart` to match `ImagePart`. (#6340) -* [changed] **Breaking Change**: Changed `RequestOption` to accept only `long` timeout values. (#6289) -* [changed] **Breaking Change**: Moved `requestOptions` to the last positional argument in the `generativeModel` argument list. (#6292) -* [changed] **Breaking Change**: Replaced sealed classes with abstract classes for `StringFormat`. (#6334) -* [changed] **Breaking Change**: Refactored enum classes to be normal classes. (#6340) -* [changed] **Breaking Change**: Marked `GenerativeModel` properties as private. (#6309) -* [changed] **Breaking Change**: Changed `method` parameter type to be nullable in `SafetySettings`. (#6379) - -# 16.0.0-beta05 -* [changed] Merged core networking code into VertexAI from a separate library -* [feature] added support for `responseSchema` in `GenerationConfig`. - -# 16.0.0-beta03 -* [changed] Breaking Change: changed `Schema.int` to return 32 bit integers instead of 64 bit (long). -* [changed] Added `Schema.long` to return 64-bit integer numbers. -* [changed] Added `Schema.double` to handle floating point numbers. -* [changed] Marked `Schema.num` as deprecated, prefer using `Schema.double`. -* [fixed] Fixed an issue with decoding JSON literals (#6028). - -# 16.0.0-beta01 -* [feature] Added support for `responseMimeType` in `GenerationConfig`. -* [changed] Renamed `GoogleGenerativeAIException` to `FirebaseVertexAIException`. -* [changed] Updated the KDocs for various classes and functions. - diff --git a/firebase-ai/api.txt b/firebase-ai/api.txt index ce949f1f01d..69356ef118d 100644 --- a/firebase-ai/api.txt +++ b/firebase-ai/api.txt @@ -13,7 +13,7 @@ package com.google.firebase.ai { property public final java.util.List history; } - public final class FirebaseVertexAI { + public final class FirebaseAI { method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName); method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null); method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null); @@ -21,10 +21,10 @@ package com.google.firebase.ai { method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null); method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null, com.google.firebase.ai.type.Content? systemInstruction = null); method public com.google.firebase.ai.GenerativeModel generativeModel(String modelName, com.google.firebase.ai.type.GenerationConfig? generationConfig = null, java.util.List? safetySettings = null, java.util.List? tools = null, com.google.firebase.ai.type.ToolConfig? toolConfig = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); - method public static com.google.firebase.ai.FirebaseVertexAI getInstance(); - method public static com.google.firebase.ai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app); - method public static com.google.firebase.ai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, String location); - method public static com.google.firebase.ai.FirebaseVertexAI getInstance(String location); + method public static com.google.firebase.ai.FirebaseAI getInstance(); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); + method public static com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.ImagenModel imagenModel(String modelName, com.google.firebase.ai.type.ImagenGenerationConfig? generationConfig = null, com.google.firebase.ai.type.ImagenSafetySettings? safetySettings = null); @@ -34,21 +34,21 @@ package com.google.firebase.ai { method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null); method @com.google.firebase.ai.type.PublicPreviewAPI public com.google.firebase.ai.LiveGenerativeModel liveModel(String modelName, com.google.firebase.ai.type.LiveGenerationConfig? generationConfig = null, java.util.List? tools = null, com.google.firebase.ai.type.Content? systemInstruction = null, com.google.firebase.ai.type.RequestOptions requestOptions = com.google.firebase.ai.type.RequestOptions()); - property public static final com.google.firebase.ai.FirebaseVertexAI instance; - field public static final com.google.firebase.ai.FirebaseVertexAI.Companion Companion; + property public static final com.google.firebase.ai.FirebaseAI instance; + field public static final com.google.firebase.ai.FirebaseAI.Companion Companion; } - public static final class FirebaseVertexAI.Companion { - method public com.google.firebase.ai.FirebaseVertexAI getInstance(); - method public com.google.firebase.ai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app); - method public com.google.firebase.ai.FirebaseVertexAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, String location); - method public com.google.firebase.ai.FirebaseVertexAI getInstance(String location); - property public final com.google.firebase.ai.FirebaseVertexAI instance; + public static final class FirebaseAI.Companion { + method public com.google.firebase.ai.FirebaseAI getInstance(); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.ai.type.GenerativeBackend backend); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app); + method public com.google.firebase.ai.FirebaseAI getInstance(com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend); + property public final com.google.firebase.ai.FirebaseAI instance; } - public final class FirebaseVertexAIKt { - method public static com.google.firebase.ai.FirebaseVertexAI getVertexAI(com.google.firebase.Firebase); - method public static com.google.firebase.ai.FirebaseVertexAI vertexAI(com.google.firebase.Firebase, com.google.firebase.FirebaseApp app = Firebase.app, String location = "us-central1"); + public final class FirebaseAIKt { + method public static com.google.firebase.ai.FirebaseAI ai(com.google.firebase.Firebase, com.google.firebase.FirebaseApp app = Firebase.app, com.google.firebase.ai.type.GenerativeBackend backend = GenerativeBackend.googleAI()); + method public static com.google.firebase.ai.FirebaseAI getAi(com.google.firebase.Firebase); } public final class GenerativeModel { @@ -147,7 +147,7 @@ package com.google.firebase.ai.java { package com.google.firebase.ai.type { - public final class AudioRecordInitializationFailedException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class AudioRecordInitializationFailedException extends com.google.firebase.ai.type.FirebaseAIException { ctor public AudioRecordInitializationFailedException(String message); } @@ -224,7 +224,7 @@ package com.google.firebase.ai.type { property public final String? role; } - public final class ContentBlockedException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class ContentBlockedException extends com.google.firebase.ai.type.FirebaseAIException { } public final class ContentKt { @@ -288,7 +288,7 @@ package com.google.firebase.ai.type { public static final class FinishReason.Companion { } - public abstract class FirebaseVertexAIException extends java.lang.RuntimeException { + public abstract class FirebaseAIException extends java.lang.RuntimeException { } public final class FunctionCallPart implements com.google.firebase.ai.type.Part { @@ -367,6 +367,19 @@ package com.google.firebase.ai.type { method public static com.google.firebase.ai.type.GenerationConfig generationConfig(kotlin.jvm.functions.Function1 init); } + public final class GenerativeBackend { + method public static com.google.firebase.ai.type.GenerativeBackend googleAI(); + method public static com.google.firebase.ai.type.GenerativeBackend vertexAI(); + method public static com.google.firebase.ai.type.GenerativeBackend vertexAI(String location = "us-central1"); + field public static final com.google.firebase.ai.type.GenerativeBackend.Companion Companion; + } + + public static final class GenerativeBackend.Companion { + method public com.google.firebase.ai.type.GenerativeBackend googleAI(); + method public com.google.firebase.ai.type.GenerativeBackend vertexAI(); + method public com.google.firebase.ai.type.GenerativeBackend vertexAI(String location = "us-central1"); + } + public final class HarmBlockMethod { method public int getOrdinal(); property public final int ordinal; @@ -554,13 +567,13 @@ package com.google.firebase.ai.type { property public final String mimeType; } - public final class InvalidAPIKeyException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class InvalidAPIKeyException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class InvalidLocationException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class InvalidLocationException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class InvalidStateException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class InvalidStateException extends com.google.firebase.ai.type.FirebaseAIException { } @com.google.firebase.ai.type.PublicPreviewAPI public final class LiveContentResponse { @@ -661,7 +674,7 @@ package com.google.firebase.ai.type { method public static String? asTextOrNull(com.google.firebase.ai.type.Part); } - public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class PromptBlockedException extends com.google.firebase.ai.type.FirebaseAIException { method public com.google.firebase.ai.type.GenerateContentResponse? getResponse(); property public final com.google.firebase.ai.type.GenerateContentResponse? response; } @@ -679,7 +692,7 @@ package com.google.firebase.ai.type { @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This API is part of an experimental public preview and may change in " + "backwards-incompatible ways without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface PublicPreviewAPI { } - public final class QuotaExceededException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class QuotaExceededException extends com.google.firebase.ai.type.FirebaseAIException { } public final class RequestOptions { @@ -687,10 +700,10 @@ package com.google.firebase.ai.type { ctor public RequestOptions(long timeoutInMillis = 180.seconds.inWholeMilliseconds); } - public final class RequestTimeoutException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class RequestTimeoutException extends com.google.firebase.ai.type.FirebaseAIException { } - @com.google.firebase.ai.type.PublicPreviewAPI public final class ResponseModality { + public final class ResponseModality { method public int getOrdinal(); property public final int ordinal; field public static final com.google.firebase.ai.type.ResponseModality AUDIO; @@ -702,7 +715,7 @@ package com.google.firebase.ai.type { public static final class ResponseModality.Companion { } - public final class ResponseStoppedException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class ResponseStoppedException extends com.google.firebase.ai.type.FirebaseAIException { method public com.google.firebase.ai.type.GenerateContentResponse getResponse(); property public final com.google.firebase.ai.type.GenerateContentResponse response; } @@ -807,20 +820,20 @@ package com.google.firebase.ai.type { method public com.google.firebase.ai.type.Schema str(String? description = null, boolean nullable = false, com.google.firebase.ai.type.StringFormat? format = null); } - public final class SerializationException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class SerializationException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class ServerException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class ServerException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class ServiceConnectionHandshakeFailedException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class ServiceConnectionHandshakeFailedException extends com.google.firebase.ai.type.FirebaseAIException { ctor public ServiceConnectionHandshakeFailedException(String message, Throwable? cause = null); } - public final class ServiceDisabledException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class ServiceDisabledException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class SessionAlreadyReceivingException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class SessionAlreadyReceivingException extends com.google.firebase.ai.type.FirebaseAIException { ctor public SessionAlreadyReceivingException(); } @@ -856,10 +869,10 @@ package com.google.firebase.ai.type { ctor public ToolConfig(com.google.firebase.ai.type.FunctionCallingConfig? functionCallingConfig); } - public final class UnknownException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class UnknownException extends com.google.firebase.ai.type.FirebaseAIException { } - public final class UnsupportedUserLocationException extends com.google.firebase.ai.type.FirebaseVertexAIException { + public final class UnsupportedUserLocationException extends com.google.firebase.ai.type.FirebaseAIException { } public final class UsageMetadata { diff --git a/firebase-ai/consumer-rules.pro b/firebase-ai/consumer-rules.pro index f328794a748..b5225e0c05e 100644 --- a/firebase-ai/consumer-rules.pro +++ b/firebase-ai/consumer-rules.pro @@ -20,5 +20,5 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class com.google.firebase.vertexai.type.** { *; } --keep class com.google.firebase.vertexai.common.** { *; } +-keep class com.google.firebase.ai.type.** { *; } +-keep class com.google.firebase.ai.common.** { *; } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAI.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt similarity index 70% rename from firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAI.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt index 3eb75dd043e..46cdc2dbf3e 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAI.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAI.kt @@ -19,30 +19,32 @@ package com.google.firebase.ai import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Blocking -import com.google.firebase.app -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider -import com.google.firebase.auth.internal.InternalAuthProvider -import com.google.firebase.inject.Provider import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.GenerativeBackendEnum import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenSafetySettings -import com.google.firebase.ai.type.InvalidLocationException +import com.google.firebase.ai.type.InvalidStateException import com.google.firebase.ai.type.LiveGenerationConfig import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.SafetySetting import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.ToolConfig +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.app +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider +import com.google.firebase.inject.Provider import kotlin.coroutines.CoroutineContext -/** Entry point for all _Vertex AI for Firebase_ functionality. */ -public class FirebaseVertexAI +/** Entry point for all _Firebase AI_ functionality. */ +public class FirebaseAI internal constructor( private val firebaseApp: FirebaseApp, + private val backend: GenerativeBackend, @Blocking private val blockingDispatcher: CoroutineContext, - private val location: String, private val appCheckProvider: Provider, private val internalAuthProvider: Provider, ) { @@ -70,20 +72,24 @@ internal constructor( systemInstruction: Content? = null, requestOptions: RequestOptions = RequestOptions(), ): GenerativeModel { - if (location.trim().isEmpty() || location.contains("/")) { - throw InvalidLocationException(location) - } + val modelUri = + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + "projects/${firebaseApp.options.projectId}/models/${modelName}" + } if (!modelName.startsWith(GEMINI_MODEL_NAME_PREFIX)) { Log.w( TAG, """Unsupported Gemini model "${modelName}"; see https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. """ - .trimIndent() + .trimIndent(), ) } return GenerativeModel( - "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", + modelUri, firebaseApp.options.apiKey, firebaseApp, generationConfig, @@ -92,6 +98,7 @@ internal constructor( toolConfig, systemInstruction, requestOptions, + backend, appCheckProvider.get(), internalAuthProvider.get(), ) @@ -123,21 +130,23 @@ internal constructor( """Unsupported Gemini model "$modelName"; see https://firebase.google.com/docs/vertex-ai/models for a list supported Gemini model names. """ - .trimIndent() + .trimIndent(), ) } - if (location.trim().isEmpty() || location.contains("/")) { - throw InvalidLocationException(location) - } return LiveGenerativeModel( - "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + throw InvalidStateException("Live Model is not yet available on the Google AI backend") + }, firebaseApp.options.apiKey, firebaseApp, blockingDispatcher, generationConfig, tools, systemInstruction, - location, + backend.location, requestOptions, appCheckProvider.get(), internalAuthProvider.get(), @@ -161,20 +170,24 @@ internal constructor( safetySettings: ImagenSafetySettings? = null, requestOptions: RequestOptions = RequestOptions(), ): ImagenModel { - if (location.trim().isEmpty() || location.contains("/")) { - throw InvalidLocationException(location) - } + val modelUri = + when (backend.backend) { + GenerativeBackendEnum.VERTEX_AI -> + "projects/${firebaseApp.options.projectId}/locations/${backend.location}/publishers/google/models/${modelName}" + GenerativeBackendEnum.GOOGLE_AI -> + "projects/${firebaseApp.options.projectId}/models/${modelName}" + } if (!modelName.startsWith(IMAGEN_MODEL_NAME_PREFIX)) { Log.w( TAG, """Unsupported Imagen model "${modelName}"; see https://firebase.google.com/docs/vertex-ai/models for a list supported Imagen model names. """ - .trimIndent() + .trimIndent(), ) } return ImagenModel( - "projects/${firebaseApp.options.projectId}/locations/${location}/publishers/google/models/${modelName}", + modelUri, firebaseApp.options.apiKey, firebaseApp, generationConfig, @@ -186,41 +199,47 @@ internal constructor( } public companion object { - /** The [FirebaseVertexAI] instance for the default [FirebaseApp] */ + /** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */ @JvmStatic - public val instance: FirebaseVertexAI - get() = getInstance(location = "us-central1") - - @JvmStatic public fun getInstance(app: FirebaseApp): FirebaseVertexAI = getInstance(app) + public val instance: FirebaseAI + get() = getInstance(backend = GenerativeBackend.googleAI()) /** - * Returns the [FirebaseVertexAI] instance for the provided [FirebaseApp] and [location]. + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. * - * @param location location identifier, defaults to `us-central1`; see available - * [Vertex AI regions](https://firebase.google.com/docs/vertex-ai/locations?platform=android#available-locations) - * . + * @param backend the backend reference to make generative AI requests to. . */ @JvmStatic @JvmOverloads - public fun getInstance(app: FirebaseApp = Firebase.app, location: String): FirebaseVertexAI { - val multiResourceComponent = app[FirebaseVertexAIMultiResourceComponent::class.java] - return multiResourceComponent.get(location) + public fun getInstance( + app: FirebaseApp = Firebase.app, + backend: GenerativeBackend + ): FirebaseAI { + val multiResourceComponent = app[FirebaseAIMultiResourceComponent::class.java] + return multiResourceComponent.get(backend) } + /** The [FirebaseAI] instance for the provided [FirebaseApp] using the Google AI Backend. */ + @JvmStatic public fun getInstance(app: FirebaseApp): FirebaseAI = getInstance(app) + private const val GEMINI_MODEL_NAME_PREFIX = "gemini-" private const val IMAGEN_MODEL_NAME_PREFIX = "imagen-" - private val TAG = FirebaseVertexAI::class.java.simpleName + private val TAG = FirebaseAI::class.java.simpleName } } -/** Returns the [FirebaseVertexAI] instance of the default [FirebaseApp]. */ -public val Firebase.vertexAI: FirebaseVertexAI - get() = FirebaseVertexAI.instance +/** The [FirebaseAI] instance for the default [FirebaseApp] using the Google AI Backend. */ +public val Firebase.ai: FirebaseAI + get() = FirebaseAI.instance -/** Returns the [FirebaseVertexAI] instance of a given [FirebaseApp]. */ -public fun Firebase.vertexAI( +/** + * Returns the [FirebaseAI] instance for the provided [FirebaseApp] and [backend]. + * + * @param backend the backend reference to make generative AI requests to. + */ +public fun Firebase.ai( app: FirebaseApp = Firebase.app, - location: String = "us-central1" -): FirebaseVertexAI = FirebaseVertexAI.getInstance(app, location) + backend: GenerativeBackend = GenerativeBackend.googleAI() +): FirebaseAI = FirebaseAI.getInstance(app, backend) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAIMultiResourceComponent.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt similarity index 80% rename from firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAIMultiResourceComponent.kt rename to firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt index 7d07b9f2586..fd71e1c2b25 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseVertexAIMultiResourceComponent.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIMultiResourceComponent.kt @@ -18,6 +18,7 @@ package com.google.firebase.ai import androidx.annotation.GuardedBy import com.google.firebase.FirebaseApp +import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider import com.google.firebase.auth.internal.InternalAuthProvider @@ -29,25 +30,25 @@ import kotlin.coroutines.CoroutineContext * * @hide */ -internal class FirebaseVertexAIMultiResourceComponent( +internal class FirebaseAIMultiResourceComponent( private val app: FirebaseApp, @Blocking val blockingDispatcher: CoroutineContext, private val appCheckProvider: Provider, private val internalAuthProvider: Provider, ) { - @GuardedBy("this") private val instances: MutableMap = mutableMapOf() + @GuardedBy("this") private val instances: MutableMap = mutableMapOf() - fun get(location: String): FirebaseVertexAI = + fun get(backend: GenerativeBackend): FirebaseAI = synchronized(this) { - instances[location] - ?: FirebaseVertexAI( + instances[backend.location] + ?: FirebaseAI( app, + backend, blockingDispatcher, - location, appCheckProvider, - internalAuthProvider + internalAuthProvider, ) - .also { instances[location] = it } + .also { instances[backend.location] = it } } } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt index a9690efa8d6..a8b6b2cb1a3 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/FirebaseAIRegistrar.kt @@ -30,7 +30,7 @@ import com.google.firebase.platforminfo.LibraryVersionComponent import kotlinx.coroutines.CoroutineDispatcher /** - * [ComponentRegistrar] for setting up [FirebaseVertexAI] and its internal dependencies. + * [ComponentRegistrar] for setting up [FirebaseAI] and its internal dependencies. * * @hide */ @@ -38,14 +38,14 @@ import kotlinx.coroutines.CoroutineDispatcher internal class FirebaseAIRegistrar : ComponentRegistrar { override fun getComponents() = listOf( - Component.builder(FirebaseVertexAIMultiResourceComponent::class.java) + Component.builder(FirebaseAIMultiResourceComponent::class.java) .name(LIBRARY_NAME) .add(Dependency.required(firebaseApp)) .add(Dependency.required(blockingDispatcher)) .add(Dependency.optionalProvider(appCheckInterop)) .add(Dependency.optionalProvider(internalAuthProvider)) .factory { container -> - FirebaseVertexAIMultiResourceComponent( + FirebaseAIMultiResourceComponent( container[firebaseApp], container.get(blockingDispatcher), container.getProvider(appCheckInterop), @@ -57,7 +57,7 @@ internal class FirebaseAIRegistrar : ComponentRegistrar { ) private companion object { - private const val LIBRARY_NAME = "fire-vertex" + private const val LIBRARY_NAME = "fire-ai" private val firebaseApp = unqualified(FirebaseApp::class.java) private val appCheckInterop = unqualified(InteropAppCheckTokenProvider::class.java) 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 08eb30e2ac3..ded387e2458 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 @@ -18,8 +18,6 @@ package com.google.firebase.ai import android.graphics.Bitmap import com.google.firebase.FirebaseApp -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider -import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider import com.google.firebase.ai.common.CountTokensRequest @@ -27,9 +25,12 @@ import com.google.firebase.ai.common.GenerateContentRequest import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.CountTokensResponse import com.google.firebase.ai.type.FinishReason -import com.google.firebase.ai.type.FirebaseVertexAIException +import com.google.firebase.ai.type.FirebaseAIException import com.google.firebase.ai.type.GenerateContentResponse import com.google.firebase.ai.type.GenerationConfig +import com.google.firebase.ai.type.GenerativeBackend +import com.google.firebase.ai.type.GenerativeBackendEnum +import com.google.firebase.ai.type.InvalidStateException import com.google.firebase.ai.type.PromptBlockedException import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.ResponseStoppedException @@ -38,6 +39,8 @@ import com.google.firebase.ai.type.SerializationException import com.google.firebase.ai.type.Tool import com.google.firebase.ai.type.ToolConfig import com.google.firebase.ai.type.content +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -55,6 +58,7 @@ internal constructor( private val tools: List? = null, private val toolConfig: ToolConfig? = null, private val systemInstruction: Content? = null, + private val generativeBackend: GenerativeBackend = GenerativeBackend.googleAI(), private val controller: APIController, ) { internal constructor( @@ -67,6 +71,7 @@ internal constructor( toolConfig: ToolConfig? = null, systemInstruction: Content? = null, requestOptions: RequestOptions = RequestOptions(), + generativeBackend: GenerativeBackend, appCheckTokenProvider: InteropAppCheckTokenProvider? = null, internalAuthProvider: InternalAuthProvider? = null, ) : this( @@ -76,6 +81,7 @@ internal constructor( tools, toolConfig, systemInstruction, + generativeBackend, APIController( apiKey, modelName, @@ -91,14 +97,14 @@ internal constructor( * * @param prompt The input(s) given to the model as a prompt. * @return The content generated by the model. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun generateContent(vararg prompt: Content): GenerateContentResponse = try { controller.generateContent(constructRequest(*prompt)).toPublic().validate() } catch (e: Throwable) { - throw FirebaseVertexAIException.from(e) + throw FirebaseAIException.from(e) } /** @@ -106,13 +112,13 @@ internal constructor( * * @param prompt The input(s) given to the model as a prompt. * @return A [Flow] which will emit responses as they are returned by the model. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public fun generateContentStream(vararg prompt: Content): Flow = controller .generateContentStream(constructRequest(*prompt)) - .catch { throw FirebaseVertexAIException.from(it) } + .catch { throw FirebaseAIException.from(it) } .map { it.toPublic().validate() } /** @@ -120,8 +126,8 @@ internal constructor( * * @param prompt The text to be send to the model as a prompt. * @return The content generated by the model. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun generateContent(prompt: String): GenerateContentResponse = generateContent(content { text(prompt) }) @@ -131,8 +137,8 @@ internal constructor( * * @param prompt The text to be send to the model as a prompt. * @return A [Flow] which will emit responses as they are returned by the model. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public fun generateContentStream(prompt: String): Flow = generateContentStream(content { text(prompt) }) @@ -142,8 +148,8 @@ internal constructor( * * @param prompt The image to be converted into a single piece of [Content] to send to the model. * @return A [GenerateContentResponse] after some delay. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun generateContent(prompt: Bitmap): GenerateContentResponse = generateContent(content { image(prompt) }) @@ -153,8 +159,8 @@ internal constructor( * * @param prompt The image to be converted into a single piece of [Content] to send to the model. * @return A [Flow] which will emit responses as they are returned by the model. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public fun generateContentStream(prompt: Bitmap): Flow = generateContentStream(content { image(prompt) }) @@ -168,14 +174,14 @@ internal constructor( * * @param prompt The input(s) given to the model as a prompt. * @return The [CountTokensResponse] of running the model's tokenizer on the input. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun countTokens(vararg prompt: Content): CountTokensResponse { try { return controller.countTokens(constructCountTokensRequest(*prompt)).toPublic() } catch (e: Throwable) { - throw FirebaseVertexAIException.from(e) + throw FirebaseAIException.from(e) } } @@ -184,8 +190,8 @@ internal constructor( * * @param prompt The text given to the model as a prompt. * @return The [CountTokensResponse] of running the model's tokenizer on the input. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun countTokens(prompt: String): CountTokensResponse { return countTokens(content { text(prompt) }) @@ -196,8 +202,8 @@ internal constructor( * * @param prompt The image given to the model as a prompt. * @return The [CountTokensResponse] of running the model's tokenizer on the input. - * @throws [FirebaseVertexAIException] if the request failed. - * @see [FirebaseVertexAIException] for types of errors. + * @throws [FirebaseAIException] if the request failed. + * @see [FirebaseAIException] for types of errors. */ public suspend fun countTokens(prompt: Bitmap): CountTokensResponse { return countTokens(content { image(prompt) }) @@ -208,7 +214,18 @@ internal constructor( GenerateContentRequest( modelName, prompt.map { it.toInternal() }, - safetySettings?.map { it.toInternal() }, + safetySettings + ?.also { safetySettingList -> + if ( + generativeBackend.backend == GenerativeBackendEnum.GOOGLE_AI && + safetySettingList.any { it.method != null } + ) { + throw InvalidStateException( + "HarmBlockMethod is unsupported by the Google Developer API" + ) + } + } + ?.map { it.toInternal() }, generationConfig?.toInternal(), tools?.map { it.toInternal() }, toolConfig?.toInternal(), @@ -216,7 +233,10 @@ internal constructor( ) private fun constructCountTokensRequest(vararg prompt: Content) = - CountTokensRequest.forVertexAI(constructRequest(*prompt)) + when (generativeBackend.backend) { + GenerativeBackendEnum.GOOGLE_AI -> CountTokensRequest.forGoogleAI(constructRequest(*prompt)) + GenerativeBackendEnum.VERTEX_AI -> CountTokensRequest.forVertexAI(constructRequest(*prompt)) + } private fun GenerateContentResponse.validate() = apply { if (candidates.isEmpty() && promptFeedback == null) { diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt index 95f34d7b51b..7fbd0776a52 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/ImagenModel.kt @@ -17,19 +17,19 @@ package com.google.firebase.ai import com.google.firebase.FirebaseApp -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider -import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider import com.google.firebase.ai.common.ContentBlockedException import com.google.firebase.ai.common.GenerateImageRequest -import com.google.firebase.ai.type.FirebaseVertexAIException +import com.google.firebase.ai.type.FirebaseAIException import com.google.firebase.ai.type.ImagenGenerationConfig import com.google.firebase.ai.type.ImagenGenerationResponse import com.google.firebase.ai.type.ImagenInlineImage import com.google.firebase.ai.type.ImagenSafetySettings import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider /** * Represents a generative model (like Imagen), capable of generating images based on various input @@ -79,7 +79,7 @@ internal constructor( .validate() .toPublicInline() } catch (e: Throwable) { - throw FirebaseVertexAIException.from(e) + throw FirebaseAIException.from(e) } private fun constructRequest( diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt index 9b6c4f64e74..1f2adbde0b6 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/LiveGenerativeModel.kt @@ -17,9 +17,6 @@ package com.google.firebase.ai import com.google.firebase.FirebaseApp -import com.google.firebase.annotations.concurrent.Blocking -import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider -import com.google.firebase.auth.internal.InternalAuthProvider import com.google.firebase.ai.common.APIController import com.google.firebase.ai.common.AppCheckHeaderProvider import com.google.firebase.ai.common.JSON @@ -31,6 +28,9 @@ import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.ServiceConnectionHandshakeFailedException import com.google.firebase.ai.type.Tool +import com.google.firebase.annotations.concurrent.Blocking +import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider +import com.google.firebase.auth.internal.InternalAuthProvider import io.ktor.websocket.Frame import io.ktor.websocket.close import io.ktor.websocket.readBytes 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 8a16c6628e3..a0287c161c2 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 @@ -19,7 +19,6 @@ package com.google.firebase.ai.common import android.util.Log import com.google.firebase.Firebase import com.google.firebase.FirebaseApp -import com.google.firebase.options import com.google.firebase.ai.common.util.decodeToFlow import com.google.firebase.ai.common.util.fullModelName import com.google.firebase.ai.type.CountTokensResponse @@ -30,6 +29,7 @@ import com.google.firebase.ai.type.ImagenGenerationResponse import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import com.google.firebase.ai.type.Response +import com.google.firebase.options import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.engine.HttpClientEngine @@ -165,7 +165,6 @@ internal constructor( suspend fun getWebSocketSession(location: String): ClientWebSocketSession = client.webSocketSession(getBidiEndpoint(location)) - fun generateContentStream( request: GenerateContentRequest ): Flow = @@ -294,7 +293,7 @@ private suspend fun validateResponse(response: HttpResponse) { val htmlContentType = ContentType.Text.Html.withCharset(Charset.forName("utf-8")) if (response.status == HttpStatusCode.NotFound && response.contentType() == htmlContentType) throw ServerException( - """URL not found. Please verify the location used to create the `FirebaseVertexAI` object + """URL not found. Please verify the location used to create the `FirebaseAI` object | See https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions | for the list of available locations. Raw response: ${response.bodyAsText()}""" .trimMargin() @@ -324,7 +323,7 @@ private suspend fun validateResponse(response: HttpResponse) { val errorMessage = if (it.metadata?.get("service") == "firebasevertexai.googleapis.com") { """ - The Vertex AI in Firebase SDK requires the Vertex AI in Firebase API + The Firebase AI SDK requires the Vertex AI in Firebase API (`firebasevertexai.googleapis.com`) to be enabled in your Firebase project. Enable this API by visiting the Firebase Console at https://console.firebase.google.com/project/${Firebase.options.projectId}/genai diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt index 23ee8cff626..7e54ad1f629 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/common/Request.kt @@ -53,6 +53,15 @@ internal data class CountTokensRequest( ) : Request { companion object { + fun forGoogleAI(generateContentRequest: GenerateContentRequest) = + CountTokensRequest( + generateContentRequest = + generateContentRequest.model?.let { + generateContentRequest.copy(model = fullModelName(it)) + } + ?: generateContentRequest + ) + fun forVertexAI(generateContentRequest: GenerateContentRequest) = CountTokensRequest( model = generateContentRequest.model?.let { fullModelName(it) }, diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt index 0668c72a1c7..57a531c1cd8 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/java/GenerativeModelFutures.kt @@ -22,7 +22,7 @@ import com.google.firebase.ai.GenerativeModel import com.google.firebase.ai.java.ChatFutures.Companion.from import com.google.firebase.ai.type.Content import com.google.firebase.ai.type.CountTokensResponse -import com.google.firebase.ai.type.FirebaseVertexAIException +import com.google.firebase.ai.type.FirebaseAIException import com.google.firebase.ai.type.GenerateContentResponse import kotlinx.coroutines.reactive.asPublisher import org.reactivestreams.Publisher @@ -39,7 +39,7 @@ public abstract class GenerativeModelFutures internal constructor() { * * @param prompt The input(s) given to the model as a prompt. * @return The content generated by the model. - * @throws [FirebaseVertexAIException] if the request failed. + * @throws [FirebaseAIException] if the request failed. */ public abstract fun generateContent( vararg prompt: Content @@ -50,7 +50,7 @@ public abstract class GenerativeModelFutures internal constructor() { * * @param prompt The input(s) given to the model as a prompt. * @return A [Publisher] which will emit responses as they are returned by the model. - * @throws [FirebaseVertexAIException] if the request failed. + * @throws [FirebaseAIException] if the request failed. */ public abstract fun generateContentStream( vararg prompt: Content @@ -61,7 +61,7 @@ public abstract class GenerativeModelFutures internal constructor() { * * @param prompt The input(s) given to the model as a prompt. * @return The [CountTokensResponse] of running the model's tokenizer on the input. - * @throws [FirebaseVertexAIException] if the request failed. + * @throws [FirebaseAIException] if the request failed. */ public abstract fun countTokens(vararg prompt: Content): ListenableFuture diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt index c595885f346..57a27f241a0 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Exceptions.kt @@ -16,25 +16,25 @@ package com.google.firebase.ai.type -import com.google.firebase.ai.FirebaseVertexAI +import com.google.firebase.ai.FirebaseAI import com.google.firebase.ai.common.FirebaseCommonAIException import kotlinx.coroutines.TimeoutCancellationException -/** Parent class for any errors that occur from the [FirebaseVertexAI] SDK. */ -public abstract class FirebaseVertexAIException +/** Parent class for any errors that occur from the [FirebaseAI] SDK. */ +public abstract class FirebaseAIException internal constructor(message: String, cause: Throwable? = null) : RuntimeException(message, cause) { internal companion object { /** - * Converts a [Throwable] to a [FirebaseVertexAIException]. + * Converts a [Throwable] to a [FirebaseAIException]. * * Will populate default messages as expected, and propagate the provided [cause] through the * resulting exception. */ - internal fun from(cause: Throwable): FirebaseVertexAIException = + internal fun from(cause: Throwable): FirebaseAIException = when (cause) { - is FirebaseVertexAIException -> cause + is FirebaseAIException -> cause is FirebaseCommonAIException -> when (cause) { is com.google.firebase.ai.common.SerializationException -> @@ -69,8 +69,7 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti } /** - * Catch any exception thrown in the [callback] block and rethrow it as a - * [FirebaseVertexAIException]. + * Catch any exception thrown in the [callback] block and rethrow it as a [FirebaseAIException]. * * Will return whatever the [callback] returns as well. * @@ -85,8 +84,7 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti } /** - * Catch any exception thrown in the [callback] block and rethrow it as a - * [FirebaseVertexAIException]. + * Catch any exception thrown in the [callback] block and rethrow it as a [FirebaseAIException]. * * Will return whatever the [callback] returns as well. * @@ -105,16 +103,16 @@ internal constructor(message: String, cause: Throwable? = null) : RuntimeExcepti /** Something went wrong while trying to deserialize a response from the server. */ public class SerializationException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** The server responded with a non 200 response code. */ public class ServerException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** The provided API Key is not valid. */ public class InvalidAPIKeyException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** * A request was blocked. @@ -129,7 +127,7 @@ internal constructor( cause: Throwable? = null, message: String? = null, ) : - FirebaseVertexAIException( + FirebaseAIException( "Prompt was blocked: ${response?.promptFeedback?.blockReason?.name?: message}", cause, ) { @@ -138,7 +136,7 @@ internal constructor( public class ContentBlockedException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** * The user's location (region) is not supported by the API. @@ -149,7 +147,7 @@ internal constructor(message: String, cause: Throwable? = null) : */ // TODO(rlazo): Add secondary constructor to pass through the message? public class UnsupportedUserLocationException internal constructor(cause: Throwable? = null) : - FirebaseVertexAIException("User location is not supported for the API use.", cause) + FirebaseAIException("User location is not supported for the API use.", cause) /** * Some form of state occurred that shouldn't have. @@ -157,7 +155,7 @@ public class UnsupportedUserLocationException internal constructor(cause: Throwa * Usually indicative of consumer error. */ public class InvalidStateException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** * A request was stopped during generation for some reason. @@ -166,7 +164,7 @@ public class InvalidStateException internal constructor(message: String, cause: */ public class ResponseStoppedException internal constructor(public val response: GenerateContentResponse, cause: Throwable? = null) : - FirebaseVertexAIException( + FirebaseAIException( "Content generation stopped. Reason: ${response.candidates.first().finishReason?.name}", cause, ) @@ -178,7 +176,7 @@ internal constructor(public val response: GenerateContentResponse, cause: Throwa */ public class RequestTimeoutException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** * The specified Vertex AI location is invalid. @@ -188,7 +186,7 @@ internal constructor(message: String, cause: Throwable? = null) : */ public class InvalidLocationException internal constructor(location: String, cause: Throwable? = null) : - FirebaseVertexAIException("Invalid location \"${location}\"", cause) + FirebaseAIException("Invalid location \"${location}\"", cause) /** * The service is not enabled for this Firebase project. Learn how to enable the required services @@ -197,7 +195,7 @@ internal constructor(location: String, cause: Throwable? = null) : */ public class ServiceDisabledException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** * The request has hit a quota limit. Learn more about quotas in the @@ -205,22 +203,22 @@ internal constructor(message: String, cause: Throwable? = null) : */ public class QuotaExceededException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** Streaming session already receiving. */ public class SessionAlreadyReceivingException : - FirebaseVertexAIException( + FirebaseAIException( "This session is already receiving. Please call stopReceiving() before calling this again." ) /** Audio record initialization failures for audio streaming */ public class AudioRecordInitializationFailedException(message: String) : - FirebaseVertexAIException(message) + FirebaseAIException(message) /** Handshake failed with the server */ public class ServiceConnectionHandshakeFailedException(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) /** Catch all case for exceptions not explicitly expected. */ public class UnknownException internal constructor(message: String, cause: Throwable? = null) : - FirebaseVertexAIException(message, cause) + FirebaseAIException(message, cause) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt new file mode 100644 index 00000000000..9598266fa68 --- /dev/null +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/GenerativeBackend.kt @@ -0,0 +1,48 @@ +/* + * 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 + +/** Represents a reference to a backend for generative AI. */ +public class GenerativeBackend +internal constructor(internal val location: String, internal val backend: GenerativeBackendEnum) { + public companion object { + + /** References the Google Developer API backend. */ + @JvmStatic + public fun googleAI(): GenerativeBackend = + GenerativeBackend("", GenerativeBackendEnum.GOOGLE_AI) + + /** + * References the VertexAI Enterprise backend. + * + * @param location passes a valid cloud server location, defaults to "us-central1" + */ + @JvmStatic + @JvmOverloads + public fun vertexAI(location: String = "us-central1"): GenerativeBackend { + if (location.isBlank() || location.contains("/")) { + throw InvalidLocationException(location) + } + return GenerativeBackend(location, GenerativeBackendEnum.VERTEX_AI) + } + } +} + +internal enum class GenerativeBackendEnum { + GOOGLE_AI, + VERTEX_AI, +} diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt index 48b72cbb431..74c8669ad2f 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/LiveSession.kt @@ -21,11 +21,11 @@ import android.media.AudioFormat import android.media.AudioTrack import android.util.Log import androidx.annotation.RequiresPermission -import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.ai.common.JSON import com.google.firebase.ai.common.util.CancelledCoroutineScope import com.google.firebase.ai.common.util.accumulateUntil import com.google.firebase.ai.common.util.childJob +import com.google.firebase.annotations.concurrent.Blocking import io.ktor.client.plugins.websocket.ClientWebSocketSession import io.ktor.websocket.Frame import io.ktor.websocket.close @@ -97,7 +97,7 @@ internal constructor( public suspend fun startAudioConversation( functionCallHandler: ((FunctionCallPart) -> FunctionResponsePart)? = null ) { - FirebaseVertexAIException.catchAsync { + FirebaseAIException.catchAsync { if (scope.isActive) { Log.w( TAG, @@ -124,7 +124,7 @@ internal constructor( * If there is no audio conversation currently active, this function does nothing. */ public fun stopAudioConversation() { - FirebaseVertexAIException.catch { + FirebaseAIException.catch { if (!startedReceiving.getAndSet(false)) return@catch scope.cancel() @@ -146,7 +146,7 @@ internal constructor( * @see stopReceiving */ public fun receive(): Flow { - return FirebaseVertexAIException.catch { + return FirebaseAIException.catch { if (startedReceiving.getAndSet(true)) { throw SessionAlreadyReceivingException() } @@ -164,7 +164,7 @@ internal constructor( } } .onCompletion { stopAudioConversation() } - .catch { throw FirebaseVertexAIException.from(it) } + .catch { throw FirebaseAIException.from(it) } // TODO(b/410059569): Add back when fixed // return session.incoming.receiveAsFlow().transform { frame -> @@ -190,7 +190,7 @@ internal constructor( */ // TODO(b/410059569): Remove when fixed public fun stopReceiving() { - FirebaseVertexAIException.catch { + FirebaseAIException.catch { if (!startedReceiving.getAndSet(false)) return@catch scope.cancel() @@ -211,7 +211,7 @@ internal constructor( * response from the client. */ public suspend fun sendFunctionResponse(functionList: List) { - FirebaseVertexAIException.catchAsync { + FirebaseAIException.catchAsync { val jsonString = Json.encodeToString( BidiGenerateContentToolResponseSetup(functionList.map { it.toInternalFunctionCall() }) @@ -231,7 +231,7 @@ internal constructor( public suspend fun sendMediaStream( mediaChunks: List, ) { - FirebaseVertexAIException.catchAsync { + FirebaseAIException.catchAsync { val jsonString = Json.encodeToString( BidiGenerateContentRealtimeInputSetup(mediaChunks.map { (it.toInternal()) }).toInternal() @@ -248,7 +248,7 @@ internal constructor( * @param content Client [Content] to be sent to the model. */ public suspend fun send(content: Content) { - FirebaseVertexAIException.catchAsync { + FirebaseAIException.catchAsync { val jsonString = Json.encodeToString( BidiGenerateContentClientContentSetup(listOf(content.toInternal()), true).toInternal() @@ -265,7 +265,7 @@ internal constructor( * @param text Text to be sent to the model. */ public suspend fun send(text: String) { - FirebaseVertexAIException.catchAsync { send(Content.Builder().text(text).build()) } + FirebaseAIException.catchAsync { send(Content.Builder().text(text).build()) } } /** @@ -277,7 +277,7 @@ internal constructor( * @see stopReceiving */ public suspend fun close() { - FirebaseVertexAIException.catchAsync { + FirebaseAIException.catchAsync { session.close() stopAudioConversation() } @@ -291,7 +291,7 @@ internal constructor( ?.buffer(UNLIMITED) ?.accumulateUntil(MIN_BUFFER_SIZE) ?.onEach { sendMediaStream(listOf(MediaData(it, "audio/pcm"))) } - ?.catch { throw FirebaseVertexAIException.from(it) } + ?.catch { throw FirebaseAIException.from(it) } ?.launchIn(scope) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt index 58b056c9211..4d1a8297e0c 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/Part.kt @@ -45,7 +45,14 @@ public class TextPart(public val text: String) : Part { * * @param image [Bitmap] to convert into a [Part] */ -public class ImagePart(public val image: Bitmap) : Part +public class ImagePart(public val image: Bitmap) : Part { + + internal fun toInlineDataPart() = + InlineDataPart( + android.util.Base64.decode(encodeBitmapToBase64Jpeg(image), BASE_64_FLAGS), + "image/jpeg" + ) +} /** * Represents binary data with an associated MIME type sent to and received from requests. @@ -164,7 +171,7 @@ internal fun Part.toInternal(): InternalPart { is TextPart -> TextPart.Internal(text) is ImagePart -> InlineDataPart.Internal( - InlineDataPart.Internal.InlineData("image/jpeg", encodeBitmapToBase64Png(image)) + InlineDataPart.Internal.InlineData("image/jpeg", encodeBitmapToBase64Jpeg(image)) ) is InlineDataPart -> InlineDataPart.Internal( @@ -186,7 +193,7 @@ internal fun Part.toInternal(): InternalPart { } } -private fun encodeBitmapToBase64Png(input: Bitmap): String { +private fun encodeBitmapToBase64Jpeg(input: Bitmap): String { ByteArrayOutputStream().let { input.compress(Bitmap.CompressFormat.JPEG, 80, it) return android.util.Base64.encodeToString(it.toByteArray(), BASE_64_FLAGS) diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt index 19429495d3d..dc4211e7222 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/RequestOptions.kt @@ -37,6 +37,8 @@ internal constructor( */ @JvmOverloads public constructor( - timeoutInMillis: Long = 180.seconds.inWholeMilliseconds - ) : this(timeout = timeoutInMillis.toDuration(DurationUnit.MILLISECONDS)) + timeoutInMillis: Long = 180.seconds.inWholeMilliseconds, + ) : this( + timeout = timeoutInMillis.toDuration(DurationUnit.MILLISECONDS), + ) } diff --git a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt index 65a6633afc2..4c1586227a2 100644 --- a/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt +++ b/firebase-ai/src/main/kotlin/com/google/firebase/ai/type/ResponseModality.kt @@ -21,7 +21,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable /** Represents the type of content present in a response (e.g., text, image, audio). */ -@PublicPreviewAPI public class ResponseModality private constructor(public val ordinal: Int) { @Serializable(Internal.Serializer::class) diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt new file mode 100644 index 00000000000..fde573fd634 --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIStreamingSnapshotTests.kt @@ -0,0 +1,109 @@ +/* + * 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 + +import com.google.firebase.ai.type.BlockReason +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.PromptBlockedException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.util.goldenDevAPIStreamingFile +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.ktor.http.HttpStatusCode +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withTimeout +import org.junit.Test + +internal class DevAPIStreamingSnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenDevAPIStreamingFile("streaming-success-basic-reply-short.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.first().candidates.first().finishReason shouldBe FinishReason.STOP + responseList.first().candidates.first().content.parts.isEmpty() shouldBe false + responseList.first().candidates.first().safetyRatings.isEmpty() shouldBe false + } + } + + @Test + fun `long reply`() = + goldenDevAPIStreamingFile("streaming-success-basic-reply-long.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.isEmpty() shouldBe false + responseList.forEach { + it.candidates.first().finishReason shouldBe FinishReason.STOP + it.candidates.first().content.parts.isEmpty() shouldBe false + it.candidates.first().safetyRatings.isEmpty() shouldBe false + } + } + } + + @Test + fun `prompt blocked for safety`() = + goldenDevAPIStreamingFile("streaming-failure-prompt-blocked-safety.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response?.promptFeedback?.blockReason shouldBe BlockReason.SAFETY + } + } + + @Test + fun `citation parsed correctly`() = + goldenDevAPIStreamingFile("streaming-success-citations.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val responseList = responses.toList() + responseList.any { + it.candidates.any { it.citationMetadata?.citations?.isNotEmpty() ?: false } + } shouldBe true + } + } + + @Test + fun `stopped for recitation`() = + goldenDevAPIStreamingFile("streaming-failure-recitation-no-content.txt") { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { + val exception = shouldThrow { responses.collect() } + exception.response.candidates.first().finishReason shouldBe FinishReason.RECITATION + } + } + + @Test + fun `image rejected`() = + goldenDevAPIStreamingFile("streaming-failure-image-rejected.txt", HttpStatusCode.BadRequest) { + val responses = model.generateContentStream("prompt") + + withTimeout(testTimeout) { shouldThrow { responses.collect() } } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt new file mode 100644 index 00000000000..7eb06a702fb --- /dev/null +++ b/firebase-ai/src/test/java/com/google/firebase/ai/DevAPIUnarySnapshotTests.kt @@ -0,0 +1,109 @@ +/* + * 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 + +import com.google.firebase.ai.type.FinishReason +import com.google.firebase.ai.type.InvalidAPIKeyException +import com.google.firebase.ai.type.ResponseStoppedException +import com.google.firebase.ai.type.ServerException +import com.google.firebase.ai.util.goldenDevAPIUnaryFile +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.should +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.ktor.http.HttpStatusCode +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.withTimeout +import org.junit.Test + +internal class DevAPIUnarySnapshotTests { + private val testTimeout = 5.seconds + + @Test + fun `short reply`() = + goldenDevAPIUnaryFile("unary-success-basic-reply-short.txt") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + } + } + + @Test + fun `long reply`() = + goldenDevAPIUnaryFile("unary-success-basic-reply-long.txt") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().finishReason shouldBe FinishReason.STOP + response.candidates.first().content.parts.isEmpty() shouldBe false + } + } + + @Test + fun `prompt blocked for safety`() = + goldenDevAPIUnaryFile("unary-failure-prompt-blocked-safety.txt") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response.candidates[0].finishReason shouldBe FinishReason.MAX_TOKENS + } + } + } + + @Test + fun `response blocked for safety`() = + goldenDevAPIUnaryFile("unary-failure-finish-reason-safety.txt") { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } should + { + it.response.candidates[0].finishReason shouldBe FinishReason.MAX_TOKENS + } + } + } + + @Test + fun `citation returns correctly`() = + goldenDevAPIUnaryFile("unary-success-citations.txt") { + withTimeout(testTimeout) { + val response = model.generateContent("prompt") + + response.candidates.isEmpty() shouldBe false + response.candidates.first().citationMetadata?.citations?.size shouldBe 4 + response.candidates.first().citationMetadata?.citations?.forEach { + it.startIndex shouldNotBe null + it.endIndex shouldNotBe null + } + } + } + + @Test + fun `invalid api key`() = + goldenDevAPIUnaryFile("unary-failure-api-key.txt", HttpStatusCode.BadRequest) { + withTimeout(testTimeout) { + shouldThrow { model.generateContent("prompt") } + } + } + @Test + fun `unknown model`() = + goldenDevAPIUnaryFile("unary-failure-unknown-model.txt", HttpStatusCode.NotFound) { + withTimeout(testTimeout) { shouldThrow { model.generateContent("prompt") } } + } +} diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt index b9933010bd2..797c4587665 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/common/util/descriptorToJson.kt @@ -145,8 +145,7 @@ internal fun typeNameFromKind(kind: SerialKind): String { * want in the json output. There are two class names * * - `com.google.firebase.ai.type.Content.Internal` for regular scenarios - * - `com.google.firebase.ai.type.Content.Internal.SomeClass` for nested classes in the - * serializer. + * - `com.google.firebase.ai.type.Content.Internal.SomeClass` for nested classes in the serializer. * * For the later time we need the second to last component, for the former we need the last * component. diff --git a/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt index 1be49d75389..44b7a1dff21 100644 --- a/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt +++ b/firebase-ai/src/test/java/com/google/firebase/ai/util/tests.kt @@ -22,6 +22,7 @@ import com.google.firebase.FirebaseApp import com.google.firebase.ai.GenerativeModel import com.google.firebase.ai.ImagenModel import com.google.firebase.ai.common.APIController +import com.google.firebase.ai.type.GenerativeBackend import com.google.firebase.ai.type.PublicPreviewAPI import com.google.firebase.ai.type.RequestOptions import io.kotest.matchers.collections.shouldNotBeEmpty @@ -38,9 +39,9 @@ import java.io.File import kotlinx.coroutines.launch import org.mockito.Mockito -internal val TEST_CLIENT_ID = "firebase-vertexai-android/test" -internal val TEST_APP_ID = "1:android:12345" -internal val TEST_VERSION = 1 +private val TEST_CLIENT_ID = "firebase-vertexai-android/test" +private val TEST_APP_ID = "1:android:12345" +private val TEST_VERSION = 1 /** String separator used in SSE communication to signal the end of a message. */ internal const val SSE_SEPARATOR = "\r\n\r\n" @@ -98,10 +99,10 @@ internal typealias CommonTest = suspend CommonTestScope.() -> Unit * @param block The test contents themselves, with the [CommonTestScope] implicitly provided * @see CommonTestScope */ -@OptIn(PublicPreviewAPI::class) internal fun commonTest( status: HttpStatusCode = HttpStatusCode.OK, requestOptions: RequestOptions = RequestOptions(), + backend: GenerativeBackend = GenerativeBackend.vertexAI(), block: CommonTest, ) = doBlocking { val channel = ByteChannel(autoFlush = true) @@ -122,7 +123,8 @@ internal fun commonTest( TEST_APP_ID, null, ) - val model = GenerativeModel("cool-model-name", controller = apiController) + val model = + GenerativeModel("cool-model-name", generativeBackend = backend, controller = apiController) val imagenModel = ImagenModel("cooler-model-name", controller = apiController) CommonTestScope(channel, model, imagenModel).block() } @@ -141,12 +143,13 @@ internal fun commonTest( internal fun goldenStreamingFile( name: String, httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + backend: GenerativeBackend = GenerativeBackend.vertexAI(), block: CommonTest, ) = doBlocking { val goldenFile = loadGoldenFile(name) val messages = goldenFile.readLines().filter { it.isNotBlank() } - commonTest(httpStatusCode) { + commonTest(httpStatusCode, backend = backend) { launch { for (message in messages) { channel.writeFully("$message$SSE_SEPARATOR".toByteArray()) @@ -173,7 +176,24 @@ internal fun goldenVertexStreamingFile( name: String, httpStatusCode: HttpStatusCode = HttpStatusCode.OK, block: CommonTest, -) = goldenStreamingFile("vertexai/$name", httpStatusCode, block) +) = goldenStreamingFile("vertexai/$name", httpStatusCode, block = block) + +/** + * A variant of [goldenStreamingFile] for testing the developer api + * + * Loads the *Golden File* and automatically parses the messages from it; providing it to the + * channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenStreamingFile + */ +internal fun goldenDevAPIStreamingFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenStreamingFile("vertexai/$name", httpStatusCode, GenerativeBackend.googleAI(), block) /** * A variant of [commonTest] for performing snapshot tests. @@ -188,9 +208,10 @@ internal fun goldenVertexStreamingFile( internal fun goldenUnaryFile( name: String, httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + backend: GenerativeBackend = GenerativeBackend.vertexAI(), block: CommonTest, ) = - commonTest(httpStatusCode) { + commonTest(httpStatusCode, backend = backend) { val goldenFile = loadGoldenFile(name) val message = goldenFile.readText() @@ -212,7 +233,22 @@ internal fun goldenVertexUnaryFile( name: String, httpStatusCode: HttpStatusCode = HttpStatusCode.OK, block: CommonTest, -) = goldenUnaryFile("vertexai/$name", httpStatusCode, block) +) = goldenUnaryFile("vertexai/$name", httpStatusCode, block = block) + +/** + * A variant of [goldenUnaryFile] for developer api tests Loads the *Golden File* and automatically + * provides it to the channel. + * + * @param name The name of the *Golden File* to load + * @param httpStatusCode An optional [HttpStatusCode] to return as a response + * @param block The test contents themselves, with a [CommonTestScope] implicitly provided + * @see goldenUnaryFile + */ +internal fun goldenDevAPIUnaryFile( + name: String, + httpStatusCode: HttpStatusCode = HttpStatusCode.OK, + block: CommonTest, +) = goldenUnaryFile("developerapi/$name", httpStatusCode, GenerativeBackend.googleAI(), block) /** * Loads a *Golden File* from the resource directory. diff --git a/firebase-ai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java similarity index 98% rename from firebase-ai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java rename to firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java index cd7960dcb22..5e363ed95b2 100644 --- a/firebase-ai/src/testUtil/java/com/google/firebase/vertexai/JavaCompileTests.java +++ b/firebase-ai/src/testUtil/java/com/google/firebase/ai/JavaCompileTests.java @@ -18,8 +18,7 @@ import android.graphics.Bitmap; import com.google.common.util.concurrent.ListenableFuture; -import com.google.firebase.concurrent.FirebaseExecutors; -import com.google.firebase.ai.FirebaseVertexAI; +import com.google.firebase.ai.FirebaseAI; import com.google.firebase.ai.GenerativeModel; import com.google.firebase.ai.java.ChatFutures; import com.google.firebase.ai.java.GenerativeModelFutures; @@ -45,6 +44,7 @@ import com.google.firebase.ai.type.SafetyRating; import com.google.firebase.ai.type.TextPart; import com.google.firebase.ai.type.UsageMetadata; +import com.google.firebase.concurrent.FirebaseExecutors; import java.util.Calendar; import java.util.List; import java.util.Map; @@ -62,7 +62,7 @@ public class JavaCompileTests { public void initializeJava() throws Exception { - FirebaseVertexAI vertex = FirebaseVertexAI.getInstance(); + FirebaseAI vertex = FirebaseAI.getInstance(); GenerativeModel model = vertex.generativeModel("fake-model-name"); GenerativeModelFutures futures = GenerativeModelFutures.from(model); testFutures(futures); diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index 21a20ef8adf..c902f6b1c9e 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit 21a20ef8adf3949de8dd65758a16f83aab344b3c +Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss index ed31caa60f2..9719c1e1e67 160000 --- a/firebase-crashlytics-ndk/src/third_party/lss +++ b/firebase-crashlytics-ndk/src/third_party/lss @@ -1 +1 @@ -Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0 +Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31 diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index 7477036e238..4332ddb6963 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit 7477036e238e54f220bed206f71036db8064dd34 +Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430