From aba8378dbf3efdd444f120234c59af5d682f0c58 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 24 Feb 2025 09:06:13 -0800 Subject: [PATCH 01/53] Swift 6 build fixes for Vertex AI (#14480) --- .../Protocols/Internal/CodableProtoEnum.swift | 2 +- .../Public/Imagen/ImagenAspectRatio.swift | 2 +- .../Imagen/ImagenSafetyFilterLevel.swift | 2 +- .../Sources/Types/Public/Schema.swift | 2 +- FirebaseVertexAI/Sources/VertexAI.swift | 21 +++++++++++++------ 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/FirebaseVertexAI/Sources/Protocols/Internal/CodableProtoEnum.swift b/FirebaseVertexAI/Sources/Protocols/Internal/CodableProtoEnum.swift index 4392157a897..6ea434e37bc 100644 --- a/FirebaseVertexAI/Sources/Protocols/Internal/CodableProtoEnum.swift +++ b/FirebaseVertexAI/Sources/Protocols/Internal/CodableProtoEnum.swift @@ -13,7 +13,7 @@ // limitations under the License. /// A type that represents a Protocol Buffer raw enum value. -protocol ProtoEnum { +protocol ProtoEnum: Sendable { /// The type representing the valid values for the protobuf enum. /// /// > Important: This type must conform to `RawRepresentable` with the `RawValue == String`. diff --git a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenAspectRatio.swift b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenAspectRatio.swift index 9c0f51353ad..a7e9fd905f0 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenAspectRatio.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenAspectRatio.swift @@ -21,7 +21,7 @@ import Foundation /// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images#aspect-ratio) /// for more details and examples of the supported aspect ratios. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct ImagenAspectRatio { +public struct ImagenAspectRatio: Sendable { /// Square (1:1) aspect ratio. /// /// Common uses for this aspect ratio include social media posts. diff --git a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenSafetyFilterLevel.swift b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenSafetyFilterLevel.swift index c2ea4110c02..43fe1ad59ad 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenSafetyFilterLevel.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenSafetyFilterLevel.swift @@ -23,7 +23,7 @@ /// guidelines](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen#safety-filters) /// for more details. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct ImagenSafetyFilterLevel: ProtoEnum { +public struct ImagenSafetyFilterLevel: ProtoEnum, Sendable { enum Kind: String { case blockLowAndAbove = "block_low_and_above" case blockMediumAndAbove = "block_medium_and_above" diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index a5fd2cdd0fb..01e1cee135b 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -40,7 +40,7 @@ public class Schema { /// Modifiers describing the expected format of an integer `Schema`. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) - public struct IntegerFormat: EncodableProtoEnum { + public struct IntegerFormat: EncodableProtoEnum, Sendable { enum Kind: String { case int32 case int64 diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index efccd4f430c..bb6415988a5 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -149,12 +149,21 @@ public class VertexAI { private let auth: AuthInterop? - /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in - /// the format `appName:location`. - private static var instances: [String: VertexAI] = [:] - - /// Lock to manage access to the `instances` array to avoid race conditions. - private static var instancesLock: os_unfair_lock = .init() + #if compiler(>=6) + /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in + /// the format `appName:location`. + private nonisolated(unsafe) static var instances: [String: VertexAI] = [:] + + /// Lock to manage access to the `instances` array to avoid race conditions. + private nonisolated(unsafe) static var instancesLock: os_unfair_lock = .init() + #else + /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in + /// the format `appName:location`. + private static var instances: [String: VertexAI] = [:] + + /// Lock to manage access to the `instances` array to avoid race conditions. + private static var instancesLock: os_unfair_lock = .init() + #endif let projectID: String let apiKey: String From b4e58654e629dcbc2a319bac7f0e315f61067217 Mon Sep 17 00:00:00 2001 From: cherylEnkidu <96084918+cherylEnkidu@users.noreply.github.com> Date: Mon, 24 Feb 2025 17:29:46 -0500 Subject: [PATCH 02/53] Specify Swift Version in Firestore test settings (#14488) --- Firestore/Example/Firestore.xcodeproj/project.pbxproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 15abfd2adfb..8deefcabab8 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -5986,6 +5986,7 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ""; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 13.0; }; @@ -6035,6 +6036,7 @@ OTHER_CFLAGS = ""; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; @@ -6058,7 +6060,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.9; WRAPPER_EXTENSION = app; }; name = Debug; @@ -6080,7 +6082,7 @@ MODULE_NAME = ExampleApp; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.9; WRAPPER_EXTENSION = app; }; name = Release; From df200d7a605e2edbb3806bb4fd51bcd242830cda Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 25 Feb 2025 09:03:02 -0800 Subject: [PATCH 03/53] Fix generate notice CI (#14492) --- .github/actions/notices_generation/Gemfile | 5 +- .../actions/notices_generation/Gemfile.lock | 61 ++++++++++++------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/.github/actions/notices_generation/Gemfile b/.github/actions/notices_generation/Gemfile index 24197d9a369..cbaf67c9bc1 100644 --- a/.github/actions/notices_generation/Gemfile +++ b/.github/actions/notices_generation/Gemfile @@ -1,9 +1,6 @@ source "https://rubygems.org" # cocoapods isn't needed for generating the Gemfile.lock, but is needed for the CI job -gem "cocoapods" +gem "cocoapods", ">= 1.16.2" gem "octokit", "~> 4.19" -gem "xcodeproj", "~> 1.21" gem "plist" -# activesupport is locked because of https://github.com/CocoaPods/CocoaPods/issues/12081 -gem 'activesupport', '7.0.8' diff --git a/.github/actions/notices_generation/Gemfile.lock b/.github/actions/notices_generation/Gemfile.lock index 8ea9bd7b820..82e64d458d3 100644 --- a/.github/actions/notices_generation/Gemfile.lock +++ b/.github/actions/notices_generation/Gemfile.lock @@ -1,12 +1,22 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.5) + CFPropertyList (3.0.7) + base64 + nkf rexml - activesupport (7.0.8) + activesupport (7.1.5.1) + base64 + benchmark (>= 0.3) + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) + logger (>= 1.4.2) minitest (>= 5.1) + mutex_m + securerandom (>= 0.3) tzinfo (~> 2.0) addressable (2.8.0) public_suffix (>= 2.0.2, < 5.0) @@ -14,13 +24,16 @@ GEM httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) atomos (0.1.3) + base64 (0.2.0) + benchmark (0.4.0) + bigdecimal (3.1.9) claide (1.1.0) - cocoapods (1.12.1) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.1) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -32,8 +45,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.1) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -44,7 +57,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -53,7 +66,9 @@ GEM netrc (~> 0.11) cocoapods-try (1.2.0) colored2 (3.1.2) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.5) + connection_pool (2.5.0) + drb (2.2.1) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) @@ -64,48 +79,52 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - httpclient (2.8.3) - i18n (1.14.1) + httpclient (2.9.0) + mutex_m + i18n (1.14.7) concurrent-ruby (~> 1.0) - json (2.7.2) - minitest (5.20.0) + json (2.10.1) + logger (1.6.6) + minitest (5.25.4) molinillo (0.8.0) multipart-post (2.1.1) - nanaimo (0.3.0) + mutex_m (0.3.0) + nanaimo (0.4.0) nap (1.1.0) netrc (0.11.0) + nkf (0.2.0) octokit (4.19.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) plist (3.6.0) public_suffix (4.0.6) - rexml (3.3.9) + rexml (3.4.1) ruby-macho (2.5.1) ruby2_keywords (0.0.2) sawyer (0.8.2) addressable (>= 2.3.5) faraday (> 0.8, < 2.0) + securerandom (0.3.2) typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) - xcodeproj (1.25.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) PLATFORMS + ruby x86_64-linux DEPENDENCIES - activesupport (= 7.0.8) - cocoapods + cocoapods (>= 1.16.2) octokit (~> 4.19) plist - xcodeproj (~> 1.21) BUNDLED WITH 2.3.11 From 3c7b987a46f6bb0d329487d70398dd32fe9cae8c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 25 Feb 2025 11:54:15 -0800 Subject: [PATCH 04/53] Add google-app-id to Vertex AI requests (#14479) --- FirebaseVertexAI/Sources/FirebaseInfo.swift | 44 +++++++ .../Sources/GenerativeAIService.swift | 30 +++-- .../Sources/GenerativeModel.swift | 10 +- .../Types/Public/Imagen/ImagenModel.swift | 10 +- FirebaseVertexAI/Sources/VertexAI.swift | 44 +++---- FirebaseVertexAI/Tests/Unit/ChatTests.swift | 14 ++- .../Tests/Unit/GenerativeModelTests.swift | 108 +++++++++--------- .../Tests/Unit/VertexComponentTests.swift | 8 +- 8 files changed, 148 insertions(+), 120 deletions(-) create mode 100644 FirebaseVertexAI/Sources/FirebaseInfo.swift diff --git a/FirebaseVertexAI/Sources/FirebaseInfo.swift b/FirebaseVertexAI/Sources/FirebaseInfo.swift new file mode 100644 index 00000000000..cddebffadde --- /dev/null +++ b/FirebaseVertexAI/Sources/FirebaseInfo.swift @@ -0,0 +1,44 @@ +// 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. + +import Foundation + +import FirebaseAppCheckInterop +import FirebaseAuthInterop +import FirebaseCore + +/// Firebase data used by VertexAI +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +struct FirebaseInfo { + let appCheck: AppCheckInterop? + let auth: AuthInterop? + let projectID: String + let apiKey: String + let googleAppID: String + let app: FirebaseApp + + init(appCheck: AppCheckInterop? = nil, + auth: AuthInterop? = nil, + projectID: String, + apiKey: String, + googleAppID: String, + firebaseApp: FirebaseApp) { + self.appCheck = appCheck + self.auth = auth + self.projectID = projectID + self.apiKey = apiKey + self.googleAppID = googleAppID + app = firebaseApp + } +} diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index fc35c2b258a..b3f150b1acb 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -26,23 +26,12 @@ struct GenerativeAIService { /// The Firebase SDK version in the format `fire/`. static let firebaseVersionTag = "fire/\(FirebaseVersion())" - private let projectID: String - - /// Gives permission to talk to the backend. - private let apiKey: String - - private let appCheck: AppCheckInterop? - - private let auth: AuthInterop? + private let firebaseInfo: FirebaseInfo private let urlSession: URLSession - init(projectID: String, apiKey: String, appCheck: AppCheckInterop?, auth: AuthInterop?, - urlSession: URLSession) { - self.projectID = projectID - self.apiKey = apiKey - self.appCheck = appCheck - self.auth = auth + init(firebaseInfo: FirebaseInfo, urlSession: URLSession) { + self.firebaseInfo = firebaseInfo self.urlSession = urlSession } @@ -180,14 +169,14 @@ struct GenerativeAIService { private func urlRequest(request: T) async throws -> URLRequest { var urlRequest = URLRequest(url: request.url) urlRequest.httpMethod = "POST" - urlRequest.setValue(apiKey, forHTTPHeaderField: "x-goog-api-key") + urlRequest.setValue(firebaseInfo.apiKey, forHTTPHeaderField: "x-goog-api-key") urlRequest.setValue( "\(GenerativeAIService.languageTag) \(GenerativeAIService.firebaseVersionTag)", forHTTPHeaderField: "x-goog-api-client" ) urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let appCheck { + if let appCheck = firebaseInfo.appCheck { let tokenResult = await appCheck.getToken(forcingRefresh: false) urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") if let error = tokenResult.error { @@ -198,10 +187,16 @@ struct GenerativeAIService { } } - if let auth, let authToken = try await auth.getToken(forcingRefresh: false) { + if let auth = firebaseInfo.auth, let authToken = try await auth.getToken( + forcingRefresh: false + ) { urlRequest.setValue("Firebase \(authToken)", forHTTPHeaderField: "Authorization") } + if firebaseInfo.app.isDataCollectionDefaultEnabled { + urlRequest.setValue(firebaseInfo.googleAppID, forHTTPHeaderField: "X-Firebase-AppId") + } + let encoder = JSONEncoder() urlRequest.httpBody = try encoder.encode(request) urlRequest.timeoutInterval = request.options.timeout @@ -260,6 +255,7 @@ struct GenerativeAIService { // Log specific RPC errors that cannot be mitigated or handled by user code. // These errors do not produce specific GenerateContentError or CountTokensError cases. private func logRPCError(_ error: BackendError) { + let projectID = firebaseInfo.projectID if error.isVertexAIInFirebaseServiceDisabledError() { VertexLog.error(code: .vertexAIInFirebaseAPIDisabled, """ The Vertex AI in Firebase SDK requires the Vertex AI in Firebase API \ diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index 0d2ea829f55..ef104cbc8de 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -59,23 +59,17 @@ public final class GenerativeModel { /// - requestOptions: Configuration parameters for sending requests to the backend. /// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`. init(name: String, - projectID: String, - apiKey: String, + firebaseInfo: FirebaseInfo, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, tools: [Tool]?, toolConfig: ToolConfig? = nil, systemInstruction: ModelContent? = nil, requestOptions: RequestOptions, - appCheck: AppCheckInterop?, - auth: AuthInterop?, urlSession: URLSession = .shared) { modelResourceName = name generativeAIService = GenerativeAIService( - projectID: projectID, - apiKey: apiKey, - appCheck: appCheck, - auth: auth, + firebaseInfo: firebaseInfo, urlSession: urlSession ) self.generationConfig = generationConfig diff --git a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift index 2196d4c6040..8f894a52488 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift @@ -41,20 +41,14 @@ public final class ImagenModel { let requestOptions: RequestOptions init(name: String, - projectID: String, - apiKey: String, + firebaseInfo: FirebaseInfo, generationConfig: ImagenGenerationConfig?, safetySettings: ImagenSafetySettings?, requestOptions: RequestOptions, - appCheck: AppCheckInterop?, - auth: AuthInterop?, urlSession: URLSession = .shared) { modelResourceName = name generativeAIService = GenerativeAIService( - projectID: projectID, - apiKey: apiKey, - appCheck: appCheck, - auth: auth, + firebaseInfo: firebaseInfo, urlSession: urlSession ) self.generationConfig = generationConfig diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index bb6415988a5..097f3230ec4 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -91,16 +91,13 @@ public class VertexAI { -> GenerativeModel { return GenerativeModel( name: modelResourceName(modelName: modelName), - projectID: projectID, - apiKey: apiKey, + firebaseInfo: firebaseInfo, generationConfig: generationConfig, safetySettings: safetySettings, tools: tools, toolConfig: toolConfig, systemInstruction: systemInstruction, - requestOptions: requestOptions, - appCheck: appCheck, - auth: auth + requestOptions: requestOptions ) } @@ -126,13 +123,10 @@ public class VertexAI { requestOptions: RequestOptions = RequestOptions()) -> ImagenModel { return ImagenModel( name: modelResourceName(modelName: modelName), - projectID: projectID, - apiKey: apiKey, + firebaseInfo: firebaseInfo, generationConfig: generationConfig, safetySettings: safetySettings, - requestOptions: requestOptions, - appCheck: appCheck, - auth: auth + requestOptions: requestOptions ) } @@ -142,12 +136,8 @@ public class VertexAI { // MARK: - Private - /// The `FirebaseApp` associated with this `VertexAI` instance. - private let app: FirebaseApp - - private let appCheck: AppCheckInterop? - - private let auth: AuthInterop? + /// Firebase data relevant to Vertex AI. + let firebaseInfo: FirebaseInfo #if compiler(>=6) /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in @@ -165,25 +155,26 @@ public class VertexAI { private static var instancesLock: os_unfair_lock = .init() #endif - let projectID: String - let apiKey: String let location: String init(app: FirebaseApp, location: String) { - self.app = app - appCheck = ComponentType.instance(for: AppCheckInterop.self, in: app.container) - auth = ComponentType.instance(for: AuthInterop.self, in: app.container) - guard let projectID = app.options.projectID else { fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.") } - self.projectID = projectID - guard let apiKey = app.options.apiKey else { fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.") } - self.apiKey = apiKey - + firebaseInfo = FirebaseInfo( + appCheck: ComponentType.instance( + for: AppCheckInterop.self, + in: app.container + ), + auth: ComponentType.instance(for: AuthInterop.self, in: app.container), + projectID: projectID, + apiKey: apiKey, + googleAppID: app.options.googleAppID, + firebaseApp: app + ) self.location = location } @@ -205,6 +196,7 @@ public class VertexAI { """) } + let projectID = firebaseInfo.projectID return "projects/\(projectID)/locations/\(location)/publishers/google/models/\(modelName)" } } diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift index 1c4988faf7c..a0525880da1 100644 --- a/FirebaseVertexAI/Tests/Unit/ChatTests.swift +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -15,6 +15,7 @@ import Foundation import XCTest +import FirebaseCore @testable import FirebaseVertexAI @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) @@ -53,14 +54,19 @@ final class ChatTests: XCTestCase { return (response, fileURL.lines) } + let app = FirebaseApp(instanceWithName: "testApp", + options: FirebaseOptions(googleAppID: "ignore", + gcmSenderID: "ignore")) let model = GenerativeModel( name: "my-model", - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: FirebaseInfo( + projectID: "my-project-id", + apiKey: "API_KEY", + googleAppID: "My app ID", + firebaseApp: app + ), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: nil, urlSession: urlSession ) let chat = Chat(model: model, history: []) diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 3ed40ce2530..10a7d41d793 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -68,12 +68,9 @@ final class GenerativeModelTests: XCTestCase { urlSession = try XCTUnwrap(URLSession(configuration: configuration)) model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: nil, urlSession: urlSession ) } @@ -269,12 +266,9 @@ final class GenerativeModelTests: XCTestCase { let model = GenerativeModel( // Model name is prefixed with "models/". name: "models/test-model", - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: nil, urlSession: urlSession ) @@ -389,12 +383,9 @@ final class GenerativeModelTests: XCTestCase { let appCheckToken = "test-valid-token" model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken)), tools: nil, requestOptions: RequestOptions(), - appCheck: AppCheckInteropFake(token: appCheckToken), - auth: nil, urlSession: urlSession ) MockURLProtocol @@ -407,15 +398,33 @@ final class GenerativeModelTests: XCTestCase { _ = try await model.generateContent(testPrompt) } + func testGenerateContent_dataCollectionOff() async throws { + let appCheckToken = "test-valid-token" + model = GenerativeModel( + name: testModelResourceName, + firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken), + privateAppID: true), + tools: nil, + requestOptions: RequestOptions(), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + appCheckToken: appCheckToken, + dataCollection: false + ) + + _ = try await model.generateContent(testPrompt) + } + func testGenerateContent_appCheck_tokenRefreshError() async throws { model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(error: AppCheckErrorFake())), tools: nil, requestOptions: RequestOptions(), - appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), - auth: nil, urlSession: urlSession ) MockURLProtocol @@ -432,12 +441,9 @@ final class GenerativeModelTests: XCTestCase { let authToken = "test-valid-token" model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(token: authToken)), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: AuthInteropFake(token: authToken), urlSession: urlSession ) MockURLProtocol @@ -453,12 +459,9 @@ final class GenerativeModelTests: XCTestCase { func testGenerateContent_auth_nilAuthToken() async throws { model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(token: nil)), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: AuthInteropFake(token: nil), urlSession: urlSession ) MockURLProtocol @@ -474,12 +477,9 @@ final class GenerativeModelTests: XCTestCase { func testGenerateContent_auth_authTokenRefreshError() async throws { model = GenerativeModel( name: "my-model", - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(error: AuthErrorFake())), tools: nil, requestOptions: RequestOptions(), - appCheck: nil, - auth: AuthInteropFake(error: AuthErrorFake()), urlSession: urlSession ) MockURLProtocol @@ -856,12 +856,9 @@ final class GenerativeModelTests: XCTestCase { let requestOptions = RequestOptions(timeout: expectedTimeout) model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), tools: nil, requestOptions: requestOptions, - appCheck: nil, - auth: nil, urlSession: urlSession ) @@ -1151,12 +1148,9 @@ final class GenerativeModelTests: XCTestCase { let appCheckToken = "test-valid-token" model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken)), tools: nil, requestOptions: RequestOptions(), - appCheck: AppCheckInteropFake(token: appCheckToken), - auth: nil, urlSession: urlSession ) MockURLProtocol @@ -1173,12 +1167,9 @@ final class GenerativeModelTests: XCTestCase { func testGenerateContentStream_appCheck_tokenRefreshError() async throws { model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(error: AppCheckErrorFake())), tools: nil, requestOptions: RequestOptions(), - appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), - auth: nil, urlSession: urlSession ) MockURLProtocol @@ -1319,12 +1310,9 @@ final class GenerativeModelTests: XCTestCase { let requestOptions = RequestOptions(timeout: expectedTimeout) model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), tools: nil, requestOptions: requestOptions, - appCheck: nil, - auth: nil, urlSession: urlSession ) @@ -1394,14 +1382,11 @@ final class GenerativeModelTests: XCTestCase { ) model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), generationConfig: generationConfig, tools: [Tool(functionDeclarations: [sumFunction])], systemInstruction: systemInstruction, requestOptions: RequestOptions(), - appCheck: nil, - auth: nil, urlSession: urlSession ) @@ -1453,12 +1438,9 @@ final class GenerativeModelTests: XCTestCase { let requestOptions = RequestOptions(timeout: expectedTimeout) model = GenerativeModel( name: testModelResourceName, - projectID: "my-project-id", - apiKey: "API_KEY", + firebaseInfo: testFirebaseInfo(), tools: nil, requestOptions: requestOptions, - appCheck: nil, - auth: nil, urlSession: urlSession ) @@ -1469,6 +1451,23 @@ final class GenerativeModelTests: XCTestCase { // MARK: - Helpers + private func testFirebaseInfo(appCheck: AppCheckInterop? = nil, + auth: AuthInterop? = nil, + privateAppID: Bool = false) -> FirebaseInfo { + let app = FirebaseApp(instanceWithName: "testApp", + options: FirebaseOptions(googleAppID: "ignore", + gcmSenderID: "ignore")) + app.isDataCollectionDefaultEnabled = !privateAppID + return FirebaseInfo( + appCheck: appCheck, + auth: auth, + projectID: "my-project-id", + apiKey: "API_KEY", + googleAppID: "My app ID", + firebaseApp: app + ) + } + private func nonHTTPRequestHandler() throws -> ((URLRequest) -> ( URLResponse, AsyncLineSequence? @@ -1495,7 +1494,8 @@ final class GenerativeModelTests: XCTestCase { statusCode: Int = 200, timeout: TimeInterval = RequestOptions().timeout, appCheckToken: String? = nil, - authToken: String? = nil) throws -> ((URLRequest) throws -> ( + authToken: String? = nil, + dataCollection: Bool = true) throws -> ((URLRequest) throws -> ( URLResponse, AsyncLineSequence? )) { @@ -1515,6 +1515,8 @@ final class GenerativeModelTests: XCTestCase { XCTAssert(apiClientTags.contains(GenerativeAIService.languageTag)) XCTAssert(apiClientTags.contains(GenerativeAIService.firebaseVersionTag)) XCTAssertEqual(request.value(forHTTPHeaderField: "X-Firebase-AppCheck"), appCheckToken) + let googleAppID = request.value(forHTTPHeaderField: "X-Firebase-AppId") + XCTAssertEqual(googleAppID, dataCollection ? "My app ID" : nil) if let authToken { XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Firebase \(authToken)") } else { diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index 5e685dd98bd..832d56f9cb4 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -50,8 +50,8 @@ class VertexComponentTests: XCTestCase { let vertex = VertexAI.vertexAI(app: VertexComponentTests.app, location: location) XCTAssertNotNil(vertex) - XCTAssertEqual(vertex.projectID, VertexComponentTests.projectID) - XCTAssertEqual(vertex.apiKey, VertexComponentTests.apiKey) + XCTAssertEqual(vertex.firebaseInfo.projectID, VertexComponentTests.projectID) + XCTAssertEqual(vertex.firebaseInfo.apiKey, VertexComponentTests.apiKey) XCTAssertEqual(vertex.location, location) } @@ -121,12 +121,12 @@ class VertexComponentTests: XCTestCase { let app = try XCTUnwrap(VertexComponentTests.app) let vertex = VertexAI.vertexAI(app: app, location: location) let model = "test-model-name" - let modelResourceName = vertex.modelResourceName(modelName: model) + let projectID = vertex.firebaseInfo.projectID XCTAssertEqual( modelResourceName, - "projects/\(vertex.projectID)/locations/\(vertex.location)/publishers/google/models/\(model)" + "projects/\(projectID)/locations/\(vertex.location)/publishers/google/models/\(model)" ) } From f205fa82e24b7af6dc2490b221840fd1784393f6 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 25 Feb 2025 12:19:33 -0800 Subject: [PATCH 05/53] [vertex-ai] Disable google-app-id (#14493) --- FirebaseVertexAI/Sources/GenerativeAIService.swift | 7 ++++--- FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index b3f150b1acb..1dbb5e8715b 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -193,9 +193,10 @@ struct GenerativeAIService { urlRequest.setValue("Firebase \(authToken)", forHTTPHeaderField: "Authorization") } - if firebaseInfo.app.isDataCollectionDefaultEnabled { - urlRequest.setValue(firebaseInfo.googleAppID, forHTTPHeaderField: "X-Firebase-AppId") - } + // TODO: wait for release approval. +// if firebaseInfo.app.isDataCollectionDefaultEnabled { +// urlRequest.setValue(firebaseInfo.googleAppID, forHTTPHeaderField: "X-Firebase-AppId") +// } let encoder = JSONEncoder() urlRequest.httpBody = try encoder.encode(request) diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 10a7d41d793..bfe8905b708 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -1515,8 +1515,9 @@ final class GenerativeModelTests: XCTestCase { XCTAssert(apiClientTags.contains(GenerativeAIService.languageTag)) XCTAssert(apiClientTags.contains(GenerativeAIService.firebaseVersionTag)) XCTAssertEqual(request.value(forHTTPHeaderField: "X-Firebase-AppCheck"), appCheckToken) - let googleAppID = request.value(forHTTPHeaderField: "X-Firebase-AppId") - XCTAssertEqual(googleAppID, dataCollection ? "My app ID" : nil) + // TODO: Wait for release approval + // let googleAppID = request.value(forHTTPHeaderField: "X-Firebase-AppId") + // XCTAssertEqual(googleAppID, dataCollection ? "My app ID" : nil) if let authToken { XCTAssertEqual(request.value(forHTTPHeaderField: "Authorization"), "Firebase \(authToken)") } else { From 2ea5d7b37159aae1fd2822466f2008c6e0b1da3d Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Tue, 25 Feb 2025 16:04:22 -0500 Subject: [PATCH 06/53] [Messaging] Update `FIRMessaging` header doc (#14494) --- .../Sources/Public/FirebaseMessaging/FIRMessaging.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h b/FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h index f8282d30b30..fded673f5fb 100644 --- a/FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h +++ b/FirebaseMessaging/Sources/Public/FirebaseMessaging/FIRMessaging.h @@ -158,7 +158,7 @@ NS_SWIFT_NAME(MessagingDelegate) @end /** - * Firebase Messaging lets you reliably deliver messages at no cost. + * Firebase Messaging lets you reliably deliver messages. * * To send or receive messages, the app must get a * registration token. This token authorizes an From 2236c2e9f767f0f404d0166d939791a28d790e5b Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 25 Feb 2025 16:23:06 -0500 Subject: [PATCH 07/53] [Release] Carthage updates for M160 / 11.9.0 (#14495) --- ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json | 1 + .../FirebaseAnalyticsOnDeviceConversionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json | 1 + .../CarthageJSON/FirebaseMLModelDownloaderBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseVertexAIBinary.json | 3 ++- 20 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json index f5a88327113..9620e442515 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseABTesting-b416850a88a115fd.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseABTesting-76f42e76c5778188.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseABTesting-14bbd5283f79341d.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseABTesting-5436773ba2b9326e.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/ABTesting-d0fdf10c43e985b1.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/ABTesting-d0fdf10c43e985b1.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/ABTesting-a71d17cadc209af9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json index cdf6b70e859..4afe24bc1ec 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/Google-Mobile-Ads-SDK-ae940d9c2e982d0c.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/Google-Mobile-Ads-SDK-b7707cde4da4511a.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/Google-Mobile-Ads-SDK-c532110c75092750.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/Google-Mobile-Ads-SDK-5e6520b0a474def3.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/AdMob-8a654a42c33bbcc8.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/AdMob-63dab3b525b94cd9.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/AdMob-134752c6180a2a41.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json index f1535032ebf..97c0ea030b1 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseAnalytics-2e55879349ec4350.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAnalytics-74a530056782a0ed.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAnalytics-0929c5c36f6a3dd2.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAnalytics-fe649e5740ef72e9.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Analytics-2468c231ebeb7922.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Analytics-bc8101d420b896c5.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Analytics-d2b6a6b0242db786.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json index 02ef1b40b36..c999d590a17 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseAnalyticsOnDeviceConversion-b8b1ff77c9cef93f.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAnalyticsOnDeviceConversion-2cb0a577e8fccf23.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAnalyticsOnDeviceConversion-63230e1864a8ae13.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAnalyticsOnDeviceConversion-9a6c9da05109a7b7.zip", "9.0.0": "https://dl.google.com/dl/firebase/ios/carthage/9.0.0/FirebaseAnalyticsOnDeviceConversion-31aedde70a736b8a.zip", "9.1.0": "https://dl.google.com/dl/firebase/ios/carthage/9.1.0/FirebaseAnalyticsOnDeviceConversion-f13b5a47d1e3978d.zip", "9.2.0": "https://dl.google.com/dl/firebase/ios/carthage/9.2.0/FirebaseAnalyticsOnDeviceConversion-2ebf567c4d97de12.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json index 1e75c518008..df7266d03d3 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseAppCheck-b667b2d77859e6f5.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAppCheck-31041ca049010d8b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAppCheck-68439fef0d9ee01c.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAppCheck-366c926c105319b0.zip", "8.0.0": "https://dl.google.com/dl/firebase/ios/carthage/8.0.0/FirebaseAppCheck-9ef1d217cf057203.zip", "8.1.0": "https://dl.google.com/dl/firebase/ios/carthage/8.1.0/FirebaseAppCheck-fc03215d9fe45d3a.zip", "8.10.0": "https://dl.google.com/dl/firebase/ios/carthage/8.10.0/FirebaseAppCheck-6ebe9e9539f06003.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json index 08b09bd318c..b3fdaacb6f4 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseAppDistribution-fa8b697f72858bab.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAppDistribution-908bdcdc87eee86b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAppDistribution-e558ade73b5891d6.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAppDistribution-32e12df219d91736.zip", "6.31.0": "https://dl.google.com/dl/firebase/ios/carthage/6.31.0/FirebaseAppDistribution-07f6a2cf7f576a8a.zip", "6.32.0": "https://dl.google.com/dl/firebase/ios/carthage/6.32.0/FirebaseAppDistribution-a9c4f5db794508ca.zip", "6.33.0": "https://dl.google.com/dl/firebase/ios/carthage/6.33.0/FirebaseAppDistribution-448a96d2ade54581.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json index 9b92cb94d34..99f4df0ec6e 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseAuth-64df854207604748.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseAuth-24605cbb83eadb6e.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseAuth-f41dc3e6a1a923d7.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseAuth-ab131b2e07abc902.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Auth-0fa76ba0f7956220.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Auth-5ddd2b4351012c7a.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Auth-5e248984d78d7284.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json index cfa6c3fb54c..55afd71ba7f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseCrashlytics-b760ff3d3f7fa48d.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseCrashlytics-066561f91425ee41.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseCrashlytics-36f932dcd3db6874.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseCrashlytics-81aa29d9a106acb0.zip", "6.15.0": "https://dl.google.com/dl/firebase/ios/carthage/6.15.0/FirebaseCrashlytics-1c6d22d5b73c84fd.zip", "6.16.0": "https://dl.google.com/dl/firebase/ios/carthage/6.16.0/FirebaseCrashlytics-938e5fd0e2eab3b3.zip", "6.17.0": "https://dl.google.com/dl/firebase/ios/carthage/6.17.0/FirebaseCrashlytics-fa09f0c8f31ed5d9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json index 2bfec1febcd..fe3f6e035cb 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseDatabase-9d2f09f945978fe0.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseDatabase-665f1d12ee1c5583.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseDatabase-8e544ced90fb6eb2.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseDatabase-8b970d6e0f67a415.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Database-1f7a820452722c7d.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Database-1f7a820452722c7d.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Database-59a12d87456b3e1c.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json index fcbb308a6b2..d076294ace1 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseDynamicLinks-7668e8c844ee2916.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseDynamicLinks-da6308f8d91eddde.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseDynamicLinks-5568c6a1f12f4285.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseDynamicLinks-4cbb097f66378e34.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/DynamicLinks-6a76740211df73f5.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/DynamicLinks-6a76740211df73f5.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/DynamicLinks-6a76740211df73f5.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json index 1c6c7afbf12..c86e2cff01a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseFirestore-01f9c373d4a1aad6.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseFirestore-b234cb861ecaaabe.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseFirestore-792558f0eddb9934.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseFirestore-30a2451150d46015.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Firestore-68fc02c229d0cc69.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Firestore-87a804ab561d91db.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Firestore-ecb3eea7bde7e8e8.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json index 567ea3b83c9..e8ea8afd92c 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseFunctions-6f8db76aa21e01a3.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseFunctions-1b9c374ba8165fcb.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseFunctions-e85bcbe133482bbc.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseFunctions-1681244d37d89040.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Functions-f4c426016dd41e38.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Functions-c6c44427c3034736.zip", "5.0.0": "https://dl.google.com/dl/firebase/ios/carthage/5.0.0/Functions-146f34c401bd459b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json index 5c227e3f08d..d6e22acd771 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/GoogleSignIn-aabe9743448ca661.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/GoogleSignIn-e745ddfd77045287.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/GoogleSignIn-45d907510d5c840b.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/GoogleSignIn-d4359fb699843869.zip", "6.0.0": "https://dl.google.com/dl/firebase/ios/carthage/6.0.0/GoogleSignIn-de9c5d5e8eb6d6ea.zip", "6.1.0": "https://dl.google.com/dl/firebase/ios/carthage/6.1.0/GoogleSignIn-8c82f2870573a793.zip", "6.10.0": "https://dl.google.com/dl/firebase/ios/carthage/6.10.0/GoogleSignIn-ff3aef61c4a55b05.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json index ca5bc50c85d..8cbc22d790a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseInAppMessaging-912c056e69fd1a60.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseInAppMessaging-ee6bedaea672150b.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseInAppMessaging-af2b93ac9f853087.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseInAppMessaging-67bccdf31b1dc458.zip", "5.10.0": "https://dl.google.com/dl/firebase/ios/carthage/5.10.0/InAppMessaging-a7a3f933362f6e95.zip", "5.11.0": "https://dl.google.com/dl/firebase/ios/carthage/5.11.0/InAppMessaging-fa28ce1b88fbca93.zip", "5.12.0": "https://dl.google.com/dl/firebase/ios/carthage/5.12.0/InAppMessaging-fa28ce1b88fbca93.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json index bf3966a176b..2f4ea2b6a81 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseMLModelDownloader-a91e9e37024a3f32.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseMLModelDownloader-6c596e97794f0430.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseMLModelDownloader-373db3aced970d88.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseMLModelDownloader-2d08410294abf160.zip", "8.0.0": "https://dl.google.com/dl/firebase/ios/carthage/8.0.0/FirebaseMLModelDownloader-8f972757fb181320.zip", "8.1.0": "https://dl.google.com/dl/firebase/ios/carthage/8.1.0/FirebaseMLModelDownloader-058ad59fa6dc0111.zip", "8.10.0": "https://dl.google.com/dl/firebase/ios/carthage/8.10.0/FirebaseMLModelDownloader-286479a966d2fb37.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json index b5f4b2e20d2..e06f16827cb 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseMessaging-6b71a5b258ab61db.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseMessaging-9d0027ada8995751.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseMessaging-ffd97136b9f3cde5.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseMessaging-379bf3738f94ef44.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Messaging-a22ef2b5f2f30f82.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Messaging-94fa4e090c7e9185.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Messaging-2a00a1c64a19d176.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json index 351a272bcc1..3130593515c 100644 --- a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebasePerformance-9cbd51ad69fbfb15.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebasePerformance-3b0d7bf17d771ece.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebasePerformance-e7063e87d9b3d1b7.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebasePerformance-9a2f8d3983650cea.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Performance-d8693eb892bfa05b.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Performance-0a400f9460f7a71d.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Performance-f5b4002ab96523e4.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json index e6ad1a56a07..5351c78d59f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseRemoteConfig-d60da8df32c1796b.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseRemoteConfig-68b7ad270036fef7.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseRemoteConfig-6e66634da4590f07.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseRemoteConfig-7455afe6f2231467.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/RemoteConfig-7e9635365ccd4a17.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/RemoteConfig-e7928fcb6311c439.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/RemoteConfig-9ab1ca5f360a1780.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json index 1bb9c52db1b..6c7b3c7d789 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json @@ -37,6 +37,7 @@ "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseStorage-1826dbf39f492220.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseStorage-1fb496e024a9eb57.zip", "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseStorage-f55cadd62f44b14f.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseStorage-5a28ee1b2244be55.zip", "4.11.0": "https://dl.google.com/dl/firebase/ios/carthage/4.11.0/Storage-6b3e77e1a7fdbc61.zip", "4.12.0": "https://dl.google.com/dl/firebase/ios/carthage/4.12.0/Storage-4721c35d2b90a569.zip", "4.9.0": "https://dl.google.com/dl/firebase/ios/carthage/4.9.0/Storage-821299369b9d0fb2.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseVertexAIBinary.json b/ReleaseTooling/CarthageJSON/FirebaseVertexAIBinary.json index 9cbde2926b7..6c00a2b874a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseVertexAIBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseVertexAIBinary.json @@ -2,5 +2,6 @@ "11.5.0": "https://dl.google.com/dl/firebase/ios/carthage/11.5.0/FirebaseVertexAI-d5d0ffd8010245da.zip", "11.6.0": "https://dl.google.com/dl/firebase/ios/carthage/11.6.0/FirebaseVertexAI-6f6520d750ba54c4.zip", "11.7.0": "https://dl.google.com/dl/firebase/ios/carthage/11.7.0/FirebaseVertexAI-bd6d038eb0cf85c6.zip", - "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseVertexAI-7cd0c55f6f7b0d90.zip" + "11.8.0": "https://dl.google.com/dl/firebase/ios/carthage/11.8.0/FirebaseVertexAI-7cd0c55f6f7b0d90.zip", + "11.9.0": "https://dl.google.com/dl/firebase/ios/carthage/11.9.0/FirebaseVertexAI-503a85ac10ba2a66.zip" } From 4c4521317b96879fea9ed2c3a9da3b8445fcf71b Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 25 Feb 2025 20:40:18 -0500 Subject: [PATCH 08/53] [Release] Update versions for Release 11.10.0 (#14498) --- Firebase.podspec | 48 +++++++++---------- FirebaseABTesting.podspec | 4 +- FirebaseAnalytics.podspec | 8 ++-- FirebaseAnalyticsOnDeviceConversion.podspec | 4 +- FirebaseAppCheck.podspec | 4 +- FirebaseAppCheckInterop.podspec | 2 +- FirebaseAppDistribution.podspec | 4 +- FirebaseAuth.podspec | 6 +-- FirebaseAuthInterop.podspec | 2 +- FirebaseCombineSwift.podspec | 4 +- FirebaseCore.podspec | 4 +- FirebaseCoreExtension.podspec | 4 +- FirebaseCoreInternal.podspec | 2 +- FirebaseCrashlytics.podspec | 4 +- FirebaseDatabase.podspec | 4 +- FirebaseDynamicLinks.podspec | 4 +- FirebaseFirestore.podspec | 8 ++-- FirebaseFirestoreInternal.podspec | 4 +- FirebaseFunctions.podspec | 6 +-- FirebaseInAppMessaging.podspec | 4 +- FirebaseInstallations.podspec | 4 +- FirebaseMLModelDownloader.podspec | 6 +-- FirebaseMessaging.podspec | 4 +- FirebaseMessagingInterop.podspec | 2 +- FirebasePerformance.podspec | 4 +- FirebaseRemoteConfig.podspec | 4 +- FirebaseRemoteConfigInterop.podspec | 2 +- FirebaseSessions.podspec | 6 +-- FirebaseSharedSwift.podspec | 2 +- FirebaseStorage.podspec | 6 +-- FirebaseVertexAI.podspec | 6 +-- GoogleAppMeasurement.podspec | 4 +- ...leAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 2 +- .../FirebaseManifest/FirebaseManifest.swift | 2 +- 35 files changed, 93 insertions(+), 93 deletions(-) diff --git a/Firebase.podspec b/Firebase.podspec index d3ceb4e68df..edf2335e02b 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Firebase' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase' s.description = <<-DESC @@ -36,14 +36,14 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '12.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '13.0' - ss.ios.dependency 'FirebaseAnalytics', '~> 11.9.0' - ss.osx.dependency 'FirebaseAnalytics', '~> 11.9.0' - ss.tvos.dependency 'FirebaseAnalytics', '~> 11.9.0' + ss.ios.dependency 'FirebaseAnalytics', '~> 11.10.0' + ss.osx.dependency 'FirebaseAnalytics', '~> 11.10.0' + ss.tvos.dependency 'FirebaseAnalytics', '~> 11.10.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'CoreOnly' do |ss| - ss.dependency 'FirebaseCore', '~> 11.9.0' + ss.dependency 'FirebaseCore', '~> 11.10.0' ss.source_files = 'CoreOnly/Sources/Firebase.h' ss.preserve_paths = 'CoreOnly/Sources/module.modulemap' if ENV['FIREBASE_POD_REPO_FOR_DEV_POD'] then @@ -79,13 +79,13 @@ Simplify your app development, grow your user base, and monetize more effectivel ss.ios.deployment_target = '12.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '13.0' - ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 11.9.0' + ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 11.10.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'ABTesting' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseABTesting', '~> 11.9.0' + ss.dependency 'FirebaseABTesting', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -95,13 +95,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'AppDistribution' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseAppDistribution', '~> 11.9.0-beta' + ss.ios.dependency 'FirebaseAppDistribution', '~> 11.10.0-beta' ss.ios.deployment_target = '13.0' end s.subspec 'AppCheck' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAppCheck', '~> 11.9.0' + ss.dependency 'FirebaseAppCheck', '~> 11.10.0' ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '13.0' @@ -110,7 +110,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Auth' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAuth', '~> 11.9.0' + ss.dependency 'FirebaseAuth', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -120,7 +120,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Crashlytics' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseCrashlytics', '~> 11.9.0' + ss.dependency 'FirebaseCrashlytics', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '12.0' ss.osx.deployment_target = '10.15' @@ -130,7 +130,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Database' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseDatabase', '~> 11.9.0' + ss.dependency 'FirebaseDatabase', '~> 11.10.0' # Standard platforms PLUS watchOS 7. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -140,13 +140,13 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'DynamicLinks' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseDynamicLinks', '~> 11.9.0' + ss.ios.dependency 'FirebaseDynamicLinks', '~> 11.10.0' ss.ios.deployment_target = '13.0' end s.subspec 'Firestore' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFirestore', '~> 11.9.0' + ss.dependency 'FirebaseFirestore', '~> 11.10.0' ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' ss.tvos.deployment_target = '13.0' @@ -154,7 +154,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Functions' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFunctions', '~> 11.9.0' + ss.dependency 'FirebaseFunctions', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -164,20 +164,20 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'InAppMessaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebaseInAppMessaging', '~> 11.9.0-beta' - ss.tvos.dependency 'FirebaseInAppMessaging', '~> 11.9.0-beta' + ss.ios.dependency 'FirebaseInAppMessaging', '~> 11.10.0-beta' + ss.tvos.dependency 'FirebaseInAppMessaging', '~> 11.10.0-beta' ss.ios.deployment_target = '13.0' ss.tvos.deployment_target = '13.0' end s.subspec 'Installations' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseInstallations', '~> 11.9.0' + ss.dependency 'FirebaseInstallations', '~> 11.10.0' end s.subspec 'Messaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMessaging', '~> 11.9.0' + ss.dependency 'FirebaseMessaging', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -187,7 +187,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'MLModelDownloader' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMLModelDownloader', '~> 11.9.0-beta' + ss.dependency 'FirebaseMLModelDownloader', '~> 11.10.0-beta' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -197,15 +197,15 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Performance' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.ios.dependency 'FirebasePerformance', '~> 11.9.0' - ss.tvos.dependency 'FirebasePerformance', '~> 11.9.0' + ss.ios.dependency 'FirebasePerformance', '~> 11.10.0' + ss.tvos.dependency 'FirebasePerformance', '~> 11.10.0' ss.ios.deployment_target = '13.0' ss.tvos.deployment_target = '13.0' end s.subspec 'RemoteConfig' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseRemoteConfig', '~> 11.9.0' + ss.dependency 'FirebaseRemoteConfig', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' @@ -215,7 +215,7 @@ Simplify your app development, grow your user base, and monetize more effectivel s.subspec 'Storage' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseStorage', '~> 11.9.0' + ss.dependency 'FirebaseStorage', '~> 11.10.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '13.0' ss.osx.deployment_target = '10.15' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index c734a386c2c..91f1b67347a 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase ABTesting' s.description = <<-DESC @@ -52,7 +52,7 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' } - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index 77065b91c55..6e8892a60c2 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalytics' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Analytics for iOS' s.description = <<-DESC @@ -26,7 +26,7 @@ Pod::Spec.new do |s| s.libraries = 'c++', 'sqlite3', 'z' s.frameworks = 'StoreKit' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0' s.dependency 'GoogleUtilities/MethodSwizzler', '~> 8.0' @@ -37,12 +37,12 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement', '11.9.0' + ss.dependency 'GoogleAppMeasurement', '11.10.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'WithoutAdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.9.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.10.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index 735152fca77..4b252fcc0bb 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'On device conversion measurement plugin for FirebaseAnalytics. Not intended for direct use.' s.description = <<-DESC @@ -18,7 +18,7 @@ Pod::Spec.new do |s| s.cocoapods_version = '>= 1.12.0' - s.dependency 'GoogleAppMeasurementOnDeviceConversion', '11.9.0' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '11.10.0' s.static_framework = true diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 6a26b59dbf2..3917fd9041f 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheck' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase App Check SDK.' s.description = <<-DESC @@ -46,7 +46,7 @@ Pod::Spec.new do |s| s.dependency 'AppCheckCore', '~> 11.0' s.dependency 'FirebaseAppCheckInterop', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' diff --git a/FirebaseAppCheckInterop.podspec b/FirebaseAppCheckInterop.podspec index ae50c0b7ccc..5c15ded0a94 100644 --- a/FirebaseAppCheckInterop.podspec +++ b/FirebaseAppCheckInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheckInterop' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Interfaces that allow other Firebase SDKs to use AppCheck functionality.' s.description = <<-DESC diff --git a/FirebaseAppDistribution.podspec b/FirebaseAppDistribution.podspec index c51c27b916f..cf39ec63695 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppDistribution' - s.version = '11.9.0-beta' + s.version = '11.10.0-beta' s.summary = 'App Distribution for Firebase iOS SDK.' s.description = <<-DESC @@ -30,7 +30,7 @@ iOS SDK for App Distribution for Firebase. ] s.public_header_files = base_dir + 'Public/FirebaseAppDistribution/*.h' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' s.dependency 'FirebaseInstallations', '~> 11.0' diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index b6e9a1d8fee..970e2475c33 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Apple platform client for Firebase Authentication' s.description = <<-DESC @@ -58,8 +58,8 @@ supports email and password accounts, as well as several 3rd party authenticatio s.ios.framework = 'SafariServices' s.dependency 'FirebaseAuthInterop', '~> 11.0' s.dependency 'FirebaseAppCheckInterop', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 5.0' diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index a0b2c7b0528..391f93aabe6 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthInterop' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Auth functionality.' s.description = <<-DESC diff --git a/FirebaseCombineSwift.podspec b/FirebaseCombineSwift.podspec index a53d06384f8..a69c13b9f48 100644 --- a/FirebaseCombineSwift.podspec +++ b/FirebaseCombineSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCombineSwift' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Swift extensions with Combine support for Firebase' s.description = <<-DESC @@ -51,7 +51,7 @@ for internal testing only. It should not be published. s.osx.framework = 'AppKit' s.tvos.framework = 'UIKit' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseAuth', '~> 11.0' s.dependency 'FirebaseFunctions', '~> 11.0' s.dependency 'FirebaseFirestore', '~> 11.0' diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index 259cb9941ac..5462b954b3a 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Core' s.description = <<-DESC @@ -53,7 +53,7 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration # Remember to also update version in `cmake/external/GoogleUtilities.cmake` s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/Logger', '~> 8.0' - s.dependency 'FirebaseCoreInternal', '~> 11.9.0' + s.dependency 'FirebaseCoreInternal', '~> 11.10.0' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index 955dfc85de4..22885e43bd9 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreExtension' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Extended FirebaseCore APIs for Firebase product SDKs' s.description = <<-DESC @@ -34,5 +34,5 @@ Pod::Spec.new do |s| "#{s.module_name}_Privacy" => 'FirebaseCore/Extension/Resources/PrivacyInfo.xcprivacy' } - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' end diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index f3d23d97c32..033df32b75b 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreInternal' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'APIs for internal FirebaseCore usage.' s.description = <<-DESC diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 01bfd35d710..c6e63214abc 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCrashlytics' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Best and lightest-weight crash reporting for mobile, desktop and tvOS.' s.description = 'Firebase Crashlytics helps you track, prioritize, and fix stability issues that erode app quality.' s.homepage = 'https://firebase.google.com/' @@ -59,7 +59,7 @@ Pod::Spec.new do |s| cp -f ./Crashlytics/CrashlyticsInputFiles.xcfilelist ./CrashlyticsInputFiles.xcfilelist PREPARE_COMMAND_END - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'FirebaseSessions', '~> 11.0' s.dependency 'FirebaseRemoteConfigInterop', '~> 11.0' diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index fcab457bf64..c19fddfd96e 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Realtime Database' s.description = <<-DESC @@ -48,7 +48,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel s.macos.frameworks = 'CFNetwork', 'Security', 'SystemConfiguration' s.watchos.frameworks = 'CFNetwork', 'Security', 'WatchKit' s.dependency 'leveldb-library', '~> 1.22' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseAppCheckInterop', '~> 11.0' s.dependency 'FirebaseSharedSwift', '~> 11.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index 4a6a88df9b8..eb25ddf09b5 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Dynamic Links' s.description = <<-DESC @@ -37,7 +37,7 @@ Firebase Dynamic Links are deep links that enhance user experience and increase } s.frameworks = 'QuartzCore' s.weak_framework = 'WebKit' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 5767123823f..6bdad5c55b3 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC Google Cloud Firestore is a NoSQL document database built for automatic scaling, high performance, and ease of application development. @@ -35,9 +35,9 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, "#{s.module_name}_Privacy" => 'Firestore/Swift/Source/Resources/PrivacyInfo.xcprivacy' } - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' - s.dependency 'FirebaseFirestoreInternal', '11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' + s.dependency 'FirebaseFirestoreInternal', '11.10.0' s.dependency 'FirebaseSharedSwift', '~> 11.0' end diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index 40b4f79601e..ca40cbafa6b 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreInternal' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC @@ -93,7 +93,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, } s.dependency 'FirebaseAppCheckInterop', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' abseil_version = '~> 1.20240722.0' s.dependency 'abseil/algorithm', abseil_version diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index 1ad3cd403e2..c70e03349d8 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Cloud Functions for Firebase' s.description = <<-DESC @@ -35,8 +35,8 @@ Cloud Functions for Firebase. 'FirebaseFunctions/Sources/**/*.swift', ] - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.dependency 'FirebaseAppCheckInterop', '~> 11.0' s.dependency 'FirebaseAuthInterop', '~> 11.0' s.dependency 'FirebaseMessagingInterop', '~> 11.0' diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 2cad4e8360f..b260d106c29 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '11.9.0-beta' + s.version = '11.10.0-beta' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC @@ -80,7 +80,7 @@ See more product details at https://firebase.google.com/products/in-app-messagin s.framework = 'UIKit' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'FirebaseABTesting', '~> 11.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 5ed6bda5eb8..2288f15dbdd 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstallations' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Installations' s.description = <<-DESC @@ -45,7 +45,7 @@ Pod::Spec.new do |s| } s.framework = 'Security' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'PromisesObjC', '~> 2.4' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index 1b5fd7a8eba..e45316db820 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMLModelDownloader' - s.version = '11.9.0-beta' + s.version = '11.10.0-beta' s.summary = 'Firebase ML Model Downloader' s.description = <<-DESC @@ -36,8 +36,8 @@ Pod::Spec.new do |s| ] s.framework = 'Foundation' - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'GoogleDataTransport', '~> 10.0' s.dependency 'GoogleUtilities/UserDefaults', '~> 8.0' diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 9ed8d682690..4e1dcca5821 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Messaging' s.description = <<-DESC @@ -62,7 +62,7 @@ device, and it is completely free. s.osx.framework = 'SystemConfiguration' s.weak_framework = 'UserNotifications' s.dependency 'FirebaseInstallations', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0' s.dependency 'GoogleUtilities/Reachability', '~> 8.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' diff --git a/FirebaseMessagingInterop.podspec b/FirebaseMessagingInterop.podspec index e0f7d6c981f..9d7f3465ba9 100644 --- a/FirebaseMessagingInterop.podspec +++ b/FirebaseMessagingInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessagingInterop' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Messaging functionality.' s.description = <<-DESC diff --git a/FirebasePerformance.podspec b/FirebasePerformance.podspec index 2dcbccbb9aa..4620b245cde 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebasePerformance' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Performance' s.description = <<-DESC @@ -59,7 +59,7 @@ Firebase Performance library to measure performance of Mobile and Web Apps. s.ios.framework = 'CoreTelephony' s.framework = 'QuartzCore' s.framework = 'SystemConfiguration' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'FirebaseRemoteConfig', '~> 11.0' s.dependency 'FirebaseSessions', '~> 11.0' diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index b68200fb7c2..ed3069901c5 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfig' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Remote Config' s.description = <<-DESC @@ -52,7 +52,7 @@ app update. } s.dependency 'FirebaseABTesting', '~> 11.0' s.dependency 'FirebaseSharedSwift', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GoogleUtilities/NSData+zlib', '~> 8.0' diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec index 3d79e275393..6c8fe49d690 100644 --- a/FirebaseRemoteConfigInterop.podspec +++ b/FirebaseRemoteConfigInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfigInterop' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Remote Config functionality.' s.description = <<-DESC diff --git a/FirebaseSessions.podspec b/FirebaseSessions.podspec index 817be12fae8..152e3094eda 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSessions' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Sessions' s.description = <<-DESC @@ -39,8 +39,8 @@ Pod::Spec.new do |s| base_dir + 'SourcesObjC/**/*.{c,h,m,mm}', ] - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.dependency 'FirebaseInstallations', '~> 11.0' s.dependency 'GoogleDataTransport', '~> 10.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index 020f18aa7b4..6e1b3e544a5 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSharedSwift' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Shared Swift Extensions for Firebase' s.description = <<-DESC diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 4f1f3fd0fd3..70b4f66ff28 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Firebase Storage' s.description = <<-DESC @@ -39,8 +39,8 @@ Firebase Storage provides robust, secure file uploads and downloads from Firebas s.dependency 'FirebaseAppCheckInterop', '~> 11.0' s.dependency 'FirebaseAuthInterop', '~> 11.0' - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 5.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' diff --git a/FirebaseVertexAI.podspec b/FirebaseVertexAI.podspec index 92256eba00d..f435ff4b9bf 100644 --- a/FirebaseVertexAI.podspec +++ b/FirebaseVertexAI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseVertexAI' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Vertex AI in Firebase SDK' s.description = <<-DESC @@ -46,8 +46,8 @@ Firebase SDK. s.dependency 'FirebaseAppCheckInterop', '~> 11.4' s.dependency 'FirebaseAuthInterop', '~> 11.4' - s.dependency 'FirebaseCore', '~> 11.9.0' - s.dependency 'FirebaseCoreExtension', '~> 11.9.0' + s.dependency 'FirebaseCore', '~> 11.10.0' + s.dependency 'FirebaseCoreExtension', '~> 11.10.0' s.test_spec 'unit' do |unit_tests| unit_tests_dir = 'FirebaseVertexAI/Tests/Unit/' diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index fae4b8a3475..1783054f9cb 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurement' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = 'Shared measurement methods for Google libraries. Not intended for direct use.' s.description = <<-DESC @@ -37,7 +37,7 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.9.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '11.10.0' ss.vendored_frameworks = 'Frameworks/GoogleAppMeasurementIdentitySupport.xcframework' end diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index 19cf684b051..5c440f20cb6 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '11.9.0' + s.version = '11.10.0' s.summary = <<-SUMMARY On device conversion measurement plugin for Google App Measurement. Not intended for direct use. diff --git a/Package.swift b/Package.swift index 6ccbc529dbe..e89aea07d8d 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ import class Foundation.ProcessInfo import PackageDescription -let firebaseVersion = "11.9.0" +let firebaseVersion = "11.10.0" let package = Package( name: "Firebase", diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 7ae61af6fc5..a7a98fd4436 100755 --- a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift +++ b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift @@ -21,7 +21,7 @@ import Foundation /// The version and releasing fields of the non-Firebase pods should be reviewed every release. /// The array should be ordered so that any pod's dependencies precede it in the list. public let shared = Manifest( - version: "11.9.0", + version: "11.10.0", pods: [ Pod("FirebaseSharedSwift"), Pod("FirebaseCoreInternal"), From d61b46abde3892e6be4064f33aa07876925b8bca Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 26 Feb 2025 11:06:15 -0500 Subject: [PATCH 09/53] [Vertex AI] Add integration test for `generateContentStream` (#14501) --- .../Tests/Integration/IntegrationTests.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift index 75a4a52e9f1..b5bfc94b93b 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift @@ -90,6 +90,45 @@ final class IntegrationTests: XCTestCase { XCTAssertEqual(candidatesTokensDetails.tokenCount, usageMetadata.candidatesTokenCount) } + func testGenerateContentStream() async throws { + let expectedText = """ + 1. Mercury + 2. Venus + 3. Earth + 4. Mars + 5. Jupiter + 6. Saturn + 7. Uranus + 8. Neptune + """ + let prompt = """ + What are the names of the planets in the solar system, ordered from closest to furthest from + the sun? Answer with a Markdown numbered list of the names and no other text. + """ + let chat = model.startChat() + + let stream = try chat.sendMessageStream(prompt) + var textValues = [String]() + for try await value in stream { + try textValues.append(XCTUnwrap(value.text)) + } + + let userHistory = try XCTUnwrap(chat.history.first) + XCTAssertEqual(userHistory.role, "user") + XCTAssertEqual(userHistory.parts.count, 1) + let promptTextPart = try XCTUnwrap(userHistory.parts.first as? TextPart) + XCTAssertEqual(promptTextPart.text, prompt) + let modelHistory = try XCTUnwrap(chat.history.last) + XCTAssertEqual(modelHistory.role, "model") + XCTAssertEqual(modelHistory.parts.count, 1) + let modelTextPart = try XCTUnwrap(modelHistory.parts.first as? TextPart) + let modelText = modelTextPart.text.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(modelText, expectedText) + XCTAssertGreaterThan(textValues.count, 1) + let text = textValues.joined().trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(text, expectedText) + } + func testGenerateContent_appCheckNotConfigured_shouldFail() async throws { let app = try FirebaseApp.defaultNamedCopy(name: TestAppCheckProviderFactory.notConfiguredName) addTeardownBlock { await app.delete() } From 7249d3a9544dec058646ede5d85c9f32d8ccf516 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 26 Feb 2025 10:25:50 -0800 Subject: [PATCH 10/53] [VertexAI] More Swift 6 (#14499) --- .github/workflows/vertexai.yml | 8 +++- FirebaseVertexAI/Sources/Chat.swift | 44 +++++++++++++------ FirebaseVertexAI/Sources/FirebaseInfo.swift | 9 ++-- .../Sources/FunctionCalling.swift | 8 ++-- .../Sources/GenerateContentRequest.swift | 2 +- .../Sources/GenerationConfig.swift | 2 +- .../Sources/GenerativeAIRequest.swift | 4 +- .../Sources/GenerativeAIService.swift | 2 +- .../Sources/GenerativeModel.swift | 2 +- FirebaseVertexAI/Sources/Safety.swift | 2 +- .../Imagen/ImagenGenerationRequest.swift | 2 +- .../Sources/Types/Public/Schema.swift | 2 +- 12 files changed, 55 insertions(+), 32 deletions(-) diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index bc2bb18f361..de1a9387ac2 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -129,10 +129,14 @@ jobs: strategy: matrix: target: [ios] - os: [macos-14] + os: [macos-14, macos-15] include: - os: macos-14 xcode: Xcode_15.2 + warnings: --allow-warnings + - os: macos-15 + xcode: Xcode_16.2 + warnings: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -144,7 +148,7 @@ jobs: - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Build and test - run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseVertexAI.podspec --platforms=${{ matrix.target }} + run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseVertexAI.podspec --platforms=${{ matrix.target }} ${{ matrix.warnings }} sample: strategy: diff --git a/FirebaseVertexAI/Sources/Chat.swift b/FirebaseVertexAI/Sources/Chat.swift index 717bf29aae2..2c3ce2d4c7f 100644 --- a/FirebaseVertexAI/Sources/Chat.swift +++ b/FirebaseVertexAI/Sources/Chat.swift @@ -17,7 +17,7 @@ import Foundation /// An object that represents a back-and-forth chat with a model, capturing the history and saving /// the context in memory between each message sent. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public class Chat { +public final class Chat: Sendable { private let model: GenerativeModel /// Initializes a new chat representing a 1:1 conversation between model and user. @@ -26,9 +26,28 @@ public class Chat { self.history = history } + private let historyLock = NSLock() + #if compiler(>=6) + private nonisolated(unsafe) var _history: [ModelContent] = [] + #else + private var _history: [ModelContent] = [] + #endif /// The previous content from the chat that has been successfully sent and received from the /// model. This will be provided to the model for each message sent as context for the discussion. - public var history: [ModelContent] + public var history: [ModelContent] { + get { + historyLock.withLock { _history } + } + set { + historyLock.withLock { _history = newValue } + } + } + + private func appendHistory(contentsOf: [ModelContent]) { + historyLock.withLock { + _history.append(contentsOf: contentsOf) + } + } /// Sends a message using the existing history of this chat as context. If successful, the message /// and response will be added to the history. If unsuccessful, history will remain unchanged. @@ -66,7 +85,7 @@ public class Chat { let toAdd = ModelContent(role: "model", parts: reply.parts) // Append the request and successful result to history, then return the value. - history.append(contentsOf: newContent) + appendHistory(contentsOf: newContent) history.append(toAdd) return result } @@ -88,16 +107,16 @@ public class Chat { @available(macOS 12.0, *) public func sendMessageStream(_ content: [ModelContent]) throws -> AsyncThrowingStream { + // Ensure that the new content has the role set. + let newContent: [ModelContent] = content.map(populateContentRole(_:)) + + // Send the history alongside the new message as context. + let request = history + newContent + let stream = try model.generateContentStream(request) return AsyncThrowingStream { continuation in Task { var aggregatedContent: [ModelContent] = [] - // Ensure that the new content has the role set. - let newContent: [ModelContent] = content.map(populateContentRole(_:)) - - // Send the history alongside the new message as context. - let request = history + newContent - let stream = try model.generateContentStream(request) do { for try await chunk in stream { // Capture any content that's streaming. This should be populated if there's no error. @@ -115,12 +134,11 @@ public class Chat { } // Save the request. - history.append(contentsOf: newContent) + appendHistory(contentsOf: newContent) // Aggregate the content to add it to the history before we finish. - let aggregated = aggregatedChunks(aggregatedContent) - history.append(aggregated) - + let aggregated = self.aggregatedChunks(aggregatedContent) + self.history.append(aggregated) continuation.finish() } } diff --git a/FirebaseVertexAI/Sources/FirebaseInfo.swift b/FirebaseVertexAI/Sources/FirebaseInfo.swift index cddebffadde..8bd705e4b29 100644 --- a/FirebaseVertexAI/Sources/FirebaseInfo.swift +++ b/FirebaseVertexAI/Sources/FirebaseInfo.swift @@ -14,13 +14,14 @@ import Foundation -import FirebaseAppCheckInterop -import FirebaseAuthInterop -import FirebaseCore +// TODO: Remove `@preconcurrency` when possible. +@preconcurrency import FirebaseAppCheckInterop +@preconcurrency import FirebaseAuthInterop +@preconcurrency import FirebaseCore /// Firebase data used by VertexAI @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -struct FirebaseInfo { +struct FirebaseInfo: Sendable { let appCheck: AppCheckInterop? let auth: AuthInterop? let projectID: String diff --git a/FirebaseVertexAI/Sources/FunctionCalling.swift b/FirebaseVertexAI/Sources/FunctionCalling.swift index 60514a6f5f4..13d498f6635 100644 --- a/FirebaseVertexAI/Sources/FunctionCalling.swift +++ b/FirebaseVertexAI/Sources/FunctionCalling.swift @@ -19,7 +19,7 @@ import Foundation /// This `FunctionDeclaration` is a representation of a block of code that can be used as a ``Tool`` /// by the model and executed by the client. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct FunctionDeclaration { +public struct FunctionDeclaration: Sendable { /// The name of the function. let name: String @@ -55,7 +55,7 @@ public struct FunctionDeclaration { /// A `Tool` is a piece of code that enables the system to interact with external systems to perform /// an action, or set of actions, outside of knowledge and scope of the model. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct Tool { +public struct Tool: Sendable { /// A list of `FunctionDeclarations` available to the model. let functionDeclarations: [FunctionDeclaration]? @@ -89,7 +89,7 @@ public struct Tool { /// Configuration for specifying function calling behavior. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct FunctionCallingConfig { +public struct FunctionCallingConfig: Sendable { /// Defines the execution behavior for function calling by defining the execution mode. enum Mode: String { case auto = "AUTO" @@ -135,7 +135,7 @@ public struct FunctionCallingConfig { /// Tool configuration for any `Tool` specified in the request. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct ToolConfig { +public struct ToolConfig: Sendable { let functionCallingConfig: FunctionCallingConfig? public init(functionCallingConfig: FunctionCallingConfig? = nil) { diff --git a/FirebaseVertexAI/Sources/GenerateContentRequest.swift b/FirebaseVertexAI/Sources/GenerateContentRequest.swift index ffa98fe4159..e37515bee7e 100644 --- a/FirebaseVertexAI/Sources/GenerateContentRequest.swift +++ b/FirebaseVertexAI/Sources/GenerateContentRequest.swift @@ -15,7 +15,7 @@ import Foundation @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -struct GenerateContentRequest { +struct GenerateContentRequest: Sendable { /// Model name. let model: String let contents: [ModelContent] diff --git a/FirebaseVertexAI/Sources/GenerationConfig.swift b/FirebaseVertexAI/Sources/GenerationConfig.swift index 125dba31fd2..e726cc83d4d 100644 --- a/FirebaseVertexAI/Sources/GenerationConfig.swift +++ b/FirebaseVertexAI/Sources/GenerationConfig.swift @@ -17,7 +17,7 @@ import Foundation /// A struct defining model parameters to be used when sending generative AI /// requests to the backend model. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct GenerationConfig { +public struct GenerationConfig: Sendable { /// Controls the degree of randomness in token selection. let temperature: Float? diff --git a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift index 92ae7179cae..5454912daa1 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift @@ -15,7 +15,7 @@ import Foundation @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -protocol GenerativeAIRequest: Encodable { +protocol GenerativeAIRequest: Sendable, Encodable { associatedtype Response: Decodable var url: URL { get } @@ -26,7 +26,7 @@ protocol GenerativeAIRequest: Encodable { /// Configuration parameters for sending requests to the backend. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) // TODO(#14405): Make the `apiVersion` constructor public in Firebase 12 with a default of `.v1`. -public struct RequestOptions { +public struct RequestOptions: Sendable { /// The request’s timeout interval in seconds; if not specified uses the default value for a /// `URLRequest`. let timeout: TimeInterval diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index 1dbb5e8715b..de8a18ee333 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -69,7 +69,7 @@ struct GenerativeAIService { @available(macOS 12.0, *) func loadRequestStream(request: T) - -> AsyncThrowingStream { + -> AsyncThrowingStream where T: Sendable { return AsyncThrowingStream { continuation in Task { let urlRequest: URLRequest diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index ef104cbc8de..9191ae3e6ec 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -19,7 +19,7 @@ import Foundation /// A type that represents a remote multimodal model (like Gemini), with the ability to generate /// content based on various input types. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public final class GenerativeModel { +public final class GenerativeModel: Sendable { /// The resource name of the model in the backend; has the format "models/model-name". let modelResourceName: String diff --git a/FirebaseVertexAI/Sources/Safety.swift b/FirebaseVertexAI/Sources/Safety.swift index decb54ba0e8..b8a28f68f68 100644 --- a/FirebaseVertexAI/Sources/Safety.swift +++ b/FirebaseVertexAI/Sources/Safety.swift @@ -146,7 +146,7 @@ public struct SafetyRating: Equatable, Hashable, Sendable { /// A type used to specify a threshold for harmful content, beyond which the model will return a /// fallback response instead of generated content. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public struct SafetySetting { +public struct SafetySetting: Sendable { /// Block at and beyond a specified ``SafetyRating/HarmProbability``. public struct HarmBlockThreshold: EncodableProtoEnum, Sendable { enum Kind: String { diff --git a/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift b/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift index e938dc36d83..eb354b1bb4f 100644 --- a/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift +++ b/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift @@ -15,7 +15,7 @@ import Foundation @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -struct ImagenGenerationRequest { +struct ImagenGenerationRequest: Sendable { let model: String let options: RequestOptions let instances: [ImageGenerationInstance] diff --git a/FirebaseVertexAI/Sources/Types/Public/Schema.swift b/FirebaseVertexAI/Sources/Types/Public/Schema.swift index 01e1cee135b..912a1d41c0b 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Schema.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Schema.swift @@ -19,7 +19,7 @@ import Foundation /// These types can be objects, but also primitives and arrays. Represents a select subset of an /// [OpenAPI 3.0 schema object](https://spec.openapis.org/oas/v3.0.3#schema). @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -public class Schema { +public final class Schema: Sendable { /// Modifiers describing the expected format of a string `Schema`. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) public struct StringFormat: EncodableProtoEnum { From ba7849ab2e9239dbf542d60b8dc4f305d4408d35 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 26 Feb 2025 16:19:59 -0800 Subject: [PATCH 11/53] [VertexAI] Add Swift 6 testing to CI (#14503) --- .github/workflows/vertexai.yml | 11 +++++++++-- FirebaseVertexAI/CHANGELOG.md | 3 +++ .../Tests/Unit/MockURLProtocol.swift | 18 ++++++++++++------ .../Tests/Unit/VertexComponentTests.swift | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index de1a9387ac2..0b67a8285da 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -128,15 +128,20 @@ jobs: if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' strategy: matrix: - target: [ios] - os: [macos-14, macos-15] include: - os: macos-14 xcode: Xcode_15.2 + swift_version: 5.9 warnings: --allow-warnings - os: macos-15 xcode: Xcode_16.2 + swift_version: 5.9 warnings: + #TODO: Fix remaining warning in GenerativeAIService and remove --allow-warnings. + - os: macos-15 + xcode: Xcode_16.2 + swift_version: 6.0 + warnings: --allow-warnings runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -147,6 +152,8 @@ jobs: run: scripts/setup_bundler.sh - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Set Swift swift_version + run: sed -i "" "s#s.swift_version = '5.9'#s.swift_version = '${{ matrix.swift_version}}'#" FirebaseVertexAI.podspec - name: Build and test run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseVertexAI.podspec --platforms=${{ matrix.target }} ${{ matrix.warnings }} diff --git a/FirebaseVertexAI/CHANGELOG.md b/FirebaseVertexAI/CHANGELOG.md index 383651f4fb8..47396ecb768 100644 --- a/FirebaseVertexAI/CHANGELOG.md +++ b/FirebaseVertexAI/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [feature] The Firebase Vertex AI SDK no longer requires `@preconcurrency` when imported in Swift 6. + # 11.9.0 - [feature] **Public Preview**: Added support for generating images using the Imagen 3 model. diff --git a/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift b/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift index 93d0fffd47e..335af1b5667 100644 --- a/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift +++ b/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift @@ -16,12 +16,18 @@ import Foundation import XCTest @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -class MockURLProtocol: URLProtocol { - static var requestHandler: ((URLRequest) throws -> ( - URLResponse, - AsyncLineSequence? - ))? - +class MockURLProtocol: URLProtocol, @unchecked Sendable { + #if compiler(>=6) + nonisolated(unsafe) static var requestHandler: ((URLRequest) throws -> ( + URLResponse, + AsyncLineSequence? + ))? + #else + static var requestHandler: ((URLRequest) throws -> ( + URLResponse, + AsyncLineSequence? + ))? + #endif override class func canInit(with request: URLRequest) -> Bool { #if os(watchOS) print("MockURLProtocol cannot be used on watchOS.") diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index 832d56f9cb4..427e40bbc6a 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseCore +@preconcurrency import FirebaseCore import Foundation import XCTest From 9dda47ec8535ffceab9206e67ad3e1719559ff2f Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 27 Feb 2025 17:50:19 -0500 Subject: [PATCH 12/53] [CI] Run `buildup_SpecsTesting_repo` job on `macos-15` (#14506) --- .github/workflows/prerelease.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 124a53f907d..4513b92ed9e 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -112,7 +112,7 @@ jobs: needs: [buildup_SpecsTesting_repo_FirebaseCore, specs_checking] # Don't run on private repo unless it is a PR. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'workflow_dispatch' - runs-on: macos-14 + runs-on: macos-15 strategy: fail-fast: false matrix: ${{fromJson(needs.specs_checking.outputs.matrix)}} From fb0bd6d5c1783bf7174693041e278cd97ebdff9f Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Fri, 28 Feb 2025 02:27:27 +0330 Subject: [PATCH 13/53] [Crashlytics] docs: fix a typo in the change log (#14511) --- Crashlytics/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index d7ebe887305..14e65e3dae1 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,5 +1,5 @@ # 11.9.0 -- [fixed] Made on-demand fatal recording thread suspension configurable through setting to imrpove performance and avoid audio glitch on Unity. Change is for framework only. +- [fixed] Made on-demand fatal recording thread suspension configurable through setting to improve performance and avoid audio glitch on Unity. Change is for framework only. # 11.7.0 - [fixed] Updated `upload-symbols` to version 3.20, wait for `debug.dylib` DWARF content getting generated when build with `--build-phase` option. Added `debug.dylib` DWARF content to run script input file list for user who enabled user script sandboxing (#14054). From d610f4d98be552de7b0d944018553dea49ff238f Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Fri, 28 Feb 2025 02:27:57 +0330 Subject: [PATCH 14/53] docs: fix the word `equivalent` (#14509) --- .../Sources/Analytics/FIRIAMClearcutUploader.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m index a1a0646f5f3..6a4fad08089 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m @@ -109,7 +109,7 @@ - (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)request object:nil]; } _userDefaults = userDefaults ? userDefaults : [GULUserDefaults standardUserDefaults]; - // it would be 0 if it does not exist, which is equvilent to saying that + // it would be 0 if it does not exist, which is equivalent to saying that // you can send now _nextValidSendTimeInMills = (int64_t)[_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills]; From 6039e0bf3115e95b6434f896f833827383e82ce7 Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Fri, 28 Feb 2025 03:20:45 +0330 Subject: [PATCH 15/53] docs: fix the word `unavailable` (#14508) --- .../Tests/UnitTestsSwift/FIRMessagingAPITest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift b/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift index 66441a37ae9..8705e09a07a 100644 --- a/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift +++ b/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift @@ -85,7 +85,7 @@ func apis() { @unknown default: () } - // TODO: Mark the initializer as unavialable, as devs shouldn't be able to instantiate this. + // TODO: Mark the initializer as unavailable, as devs shouldn't be able to instantiate this. _ = MessagingMessageInfo().status NotificationCenter.default.post(name: .MessagingRegistrationTokenRefreshed, object: nil) From 1d32f365dea853d543e0f111a67d458e6cc49f35 Mon Sep 17 00:00:00 2001 From: Seyed Mojtaba Hosseini Zeidabadi Date: Fri, 28 Feb 2025 18:42:51 +0330 Subject: [PATCH 16/53] chore: remove unused `user defaults key for remove user property time in seconds` property (#14510) --- .../Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m index f4f8a8bc577..ac7458ad0a5 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m @@ -51,11 +51,6 @@ @interface FIRIAMAnalyticsEventLoggerImpl () static NSString *const kFAUserPropertyForLastNotification = @"_ln"; static NSString *const kFAUserPropertyPrefixForFIAM = @"fiam:"; -// This user defaults key is for the entry to tell when we should remove the private user -// property from a prior action url click to stop conversion attribution for a campaign -static NSString *const kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds = - @"firebase-iam-conversion-tracking-expires-in-seconds"; - @implementation FIRIAMAnalyticsEventLoggerImpl { id _analytics; } From 0ea9a19ca38a104cbaa7e264993875273436f7cb Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Fri, 28 Feb 2025 23:53:36 +0100 Subject: [PATCH 17/53] Refactored `FunctionsSerializer.decode(_:)` (#14514) --- FirebaseFunctions/Sources/Functions.swift | 4 +--- .../Sources/Internal/FunctionsSerializer.swift | 7 ++++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 51e405b2f39..0af64811e35 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -541,10 +541,8 @@ enum FunctionsConstants { private func callableResult(fromResponseData data: Data) throws -> HTTPSCallableResult { let processedData = try processedData(fromResponseData: data) let json = try responseDataJSON(from: processedData) - // TODO: Refactor `decode(_:)` so it either returns a non-optional object or throws let payload = try serializer.decode(json) - // TODO: Remove `as Any` once `decode(_:)` is refactored - return HTTPSCallableResult(data: payload as Any) + return HTTPSCallableResult(data: payload) } private func processedData(fromResponseData data: Data) throws -> Data { diff --git a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift index d7f6ec5ad65..53d865da3c6 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift @@ -58,7 +58,7 @@ final class FunctionsSerializer { } } - func decode(_ object: Any) throws -> AnyObject? { + func decode(_ object: Any) throws -> Any { // Return these types as is. PORTING NOTE: Moved from the bottom of the func for readability. if let dict = object as? NSDictionary { if let requestedType = dict["@type"] as? String { @@ -66,8 +66,9 @@ final class FunctionsSerializer { // Seems like we should throw here - but this maintains compatibility. return dict } - let result = try decodeWrappedType(requestedType, value) - if result != nil { return result } + if let result = try decodeWrappedType(requestedType, value) { + return result + } // Treat unknown types as dictionaries, so we don't crash old clients when we add types. } From 8710c7adb0bdfb1f1a6f76066bdfda2133840d5f Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Mon, 3 Mar 2025 20:53:39 +0400 Subject: [PATCH 18/53] [Functions] Include endpoint and region details in error messages (#14487) --- FirebaseFunctions/Sources/Functions.swift | 28 +++++++++++++------ .../Sources/FunctionsError.swift | 5 +++- .../Tests/Unit/FunctionsErrorTests.swift | 22 ++++++++++++--- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 0af64811e35..ce189579c87 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -401,9 +401,9 @@ enum FunctionsConstants { do { let rawData = try await fetcher.beginFetch() - return try callableResult(fromResponseData: rawData) + return try callableResult(fromResponseData: rawData, endpointURL: url) } catch { - throw processedError(fromResponseError: error) + throw processedError(fromResponseError: error, endpointURL: url) } } @@ -454,10 +454,10 @@ enum FunctionsConstants { fetcher.beginFetch { [self] data, error in let result: Result if let error { - result = .failure(processedError(fromResponseError: error)) + result = .failure(processedError(fromResponseError: error, endpointURL: url)) } else if let data { do { - result = try .success(callableResult(fromResponseData: data)) + result = try .success(callableResult(fromResponseData: data, endpointURL: url)) } catch { result = .failure(error) } @@ -523,11 +523,14 @@ enum FunctionsConstants { return fetcher } - private func processedError(fromResponseError error: any Error) -> any Error { + private func processedError(fromResponseError error: any Error, + endpointURL url: URL) -> any Error { let error = error as NSError let localError: (any Error)? = if error.domain == kGTMSessionFetcherStatusDomain { FunctionsError( httpStatusCode: error.code, + region: region, + url: url, body: error.userInfo["data"] as? Data, serializer: serializer ) @@ -538,16 +541,23 @@ enum FunctionsConstants { return localError ?? error } - private func callableResult(fromResponseData data: Data) throws -> HTTPSCallableResult { - let processedData = try processedData(fromResponseData: data) + private func callableResult(fromResponseData data: Data, + endpointURL url: URL) throws -> HTTPSCallableResult { + let processedData = try processedData(fromResponseData: data, endpointURL: url) let json = try responseDataJSON(from: processedData) let payload = try serializer.decode(json) return HTTPSCallableResult(data: payload) } - private func processedData(fromResponseData data: Data) throws -> Data { + private func processedData(fromResponseData data: Data, endpointURL url: URL) throws -> Data { // `data` might specify a custom error. If so, throw the error. - if let bodyError = FunctionsError(httpStatusCode: 200, body: data, serializer: serializer) { + if let bodyError = FunctionsError( + httpStatusCode: 200, + region: region, + url: url, + body: data, + serializer: serializer + ) { throw bodyError } diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index f8815b3ce60..34e896b63d4 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -180,7 +180,8 @@ struct FunctionsError: CustomNSError { /// } /// ``` /// - serializer: The `FunctionsSerializer` used to decode `details` in the error body. - init?(httpStatusCode: Int, body: Data?, serializer: FunctionsSerializer) { + init?(httpStatusCode: Int, region: String, url: URL, body: Data?, + serializer: FunctionsSerializer) { // Start with reasonable defaults from the status code. var code = FunctionsErrorCode(httpStatusCode: httpStatusCode) var description = Self.errorDescription(from: code) @@ -224,6 +225,8 @@ struct FunctionsError: CustomNSError { var userInfo = [String: Any]() userInfo[NSLocalizedDescriptionKey] = description + userInfo["region"] = region + userInfo["url"] = url if let details { userInfo[FunctionsErrorDetailsKey] = details } diff --git a/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift index 99b4c8334b3..5288097aeaa 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsErrorTests.swift @@ -45,6 +45,8 @@ final class FunctionsErrorTests: XCTestCase { // The error should be `nil`. let error = FunctionsError( httpStatusCode: 200, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: nil, serializer: FunctionsSerializer() ) @@ -56,6 +58,8 @@ final class FunctionsErrorTests: XCTestCase { // The error should be inferred from the HTTP status code. let error = FunctionsError( httpStatusCode: 429, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: nil, serializer: FunctionsSerializer() ) @@ -66,7 +70,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 8) XCTAssertEqual(nsError.localizedDescription, "RESOURCE EXHAUSTED") - XCTAssertEqual(nsError.userInfo.count, 1) + XCTAssertEqual(nsError.userInfo.count, 3) } func testInitWithOKStatusCodeAndIncompleteErrorBody() { @@ -75,6 +79,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 200, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -85,7 +91,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 11) XCTAssertEqual(nsError.localizedDescription, "OUT OF RANGE") - XCTAssertEqual(nsError.userInfo.count, 1) + XCTAssertEqual(nsError.userInfo.count, 3) } func testInitWithErrorStatusCodeAndErrorBody() { @@ -96,6 +102,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 499, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -106,7 +114,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 11) XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage") - XCTAssertEqual(nsError.userInfo.count, 2) + XCTAssertEqual(nsError.userInfo.count, 4) XCTAssertEqual(nsError.userInfo["details"] as? Int, 123) } @@ -119,6 +127,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 401, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -133,6 +143,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 403, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) @@ -143,7 +155,7 @@ final class FunctionsErrorTests: XCTestCase { XCTAssertEqual(nsError.domain, "com.firebase.functions") XCTAssertEqual(nsError.code, 7) // `permissionDenied`, inferred from the HTTP status code XCTAssertEqual(nsError.localizedDescription, "TEST_ErrorMessage") - XCTAssertEqual(nsError.userInfo.count, 2) + XCTAssertEqual(nsError.userInfo.count, 4) XCTAssertEqual(nsError.userInfo["details"] as? NSNull, NSNull()) } @@ -155,6 +167,8 @@ final class FunctionsErrorTests: XCTestCase { let error = FunctionsError( httpStatusCode: 503, + region: "my-region", + url: URL(string: "https://example.com/fake_func")!, body: responseData, serializer: FunctionsSerializer() ) From ea8fb07f37a3dc647bb494a7a6b17b9eb6f78c26 Mon Sep 17 00:00:00 2001 From: Yakov Manshin Date: Tue, 4 Mar 2025 15:47:28 +0100 Subject: [PATCH 19/53] Refactored `FunctionsSerializer.encode(_:)` (#14524) --- .../Internal/FunctionsSerializer.swift | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift index 53d865da3c6..5fa9d4e3fe2 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift @@ -31,28 +31,23 @@ extension FunctionsSerializer { final class FunctionsSerializer { // MARK: - Internal APIs - func encode(_ object: Any) throws -> AnyObject { + func encode(_ object: Any) throws -> Any { if object is NSNull { - return object as AnyObject + return object } else if object is NSNumber { return try encodeNumber(object as! NSNumber) } else if object is NSString { - return object as AnyObject - } else if object is NSDictionary { - let dict = object as! NSDictionary + return object + } else if let dict = object as? NSDictionary { let encoded = NSMutableDictionary() try dict.forEach { key, value in encoded[key] = try encode(value) } return encoded - } else if object is NSArray { - let array = object as! NSArray - let encoded = NSMutableArray() - try array.forEach { element in - try encoded.add(encode(element)) + } else if let array = object as? NSArray { + return try array.map { element in + try encode(element) } - return encoded - } else { throw Error.unsupportedType(typeName: typeName(of: object)) } From c869f574ee3f87a5759ad0facad186cc3b1c9ac6 Mon Sep 17 00:00:00 2001 From: Aashish <112133849+aashishpatil-g@users.noreply.github.com> Date: Tue, 4 Mar 2025 23:13:48 +0000 Subject: [PATCH 20/53] Add DataConnect product definition to build.sh (#14527) --- scripts/build.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scripts/build.sh b/scripts/build.sh index 15019bd184c..696956a1f64 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -700,6 +700,15 @@ case "$product-$platform-$method" in test ;; + FirebaseDataConnect-*-spm) + RunXcodebuild \ + -scheme $product \ + "${xcb_flags[@]}" \ + IPHONEOS_DEPLOYMENT_TARGET=15.0 \ + TVOS_DEPLOYMENT_TARGET=15.0 \ + test + ;; + # Note that the combine tests require setting the minimum iOS and tvOS version to 13.0 *-*-spm) RunXcodebuild \ From 72cf6f7e83f7664e9c38d07cabd3627ce0453373 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 5 Mar 2025 09:10:21 -0500 Subject: [PATCH 21/53] [Vertex AI] Remove unused `finishMessage` coding key (#14528) --- FirebaseVertexAI/Sources/GenerateContentResponse.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index cd9ea9373d9..a8a11a21e1f 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -330,7 +330,6 @@ extension Candidate: Decodable { case content case safetyRatings case finishReason - case finishMessage case citationMetadata } From f2d1b72f0e86dad328757ca963125af2cb13267f Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 5 Mar 2025 10:09:47 -0500 Subject: [PATCH 22/53] [Infra] Update `generate_issues.yml` to give GITHUB_TOKEN write permissions for issues. (#14529) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .github/workflows/generate_issues.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/generate_issues.yml b/.github/workflows/generate_issues.yml index f28a423b1db..a8411561dd8 100644 --- a/.github/workflows/generate_issues.yml +++ b/.github/workflows/generate_issues.yml @@ -8,6 +8,10 @@ on: schedule: # Run every day at 4am (PST) - cron uses UTC times - cron: '0 12 * * *' + +permissions: + issues: write + jobs: generate_an_issue: # Don't run on private repo. From 2ea79ab37566829a216f0d47b94b055c871e8ccb Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 5 Mar 2025 11:08:07 -0500 Subject: [PATCH 23/53] [Infra] Update to clang-format@20 and apply updated styling. (#14531) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- CONTRIBUTING.md | 2 +- scripts/setup_check.sh | 2 +- scripts/style.sh | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5722110bb01..578ffb947fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -132,7 +132,7 @@ To develop Firebase software, **install**: To install [clang-format] and [mint] using [Homebrew]: ```console - brew install clang-format@19 + brew install clang-format@20 brew install mint ``` diff --git a/scripts/setup_check.sh b/scripts/setup_check.sh index a1e6ad64ecc..805596a9ae6 100755 --- a/scripts/setup_check.sh +++ b/scripts/setup_check.sh @@ -35,7 +35,7 @@ fi # install clang-format brew update -brew install clang-format@19 +brew install clang-format@20 # mint installs tools from Mintfile on demand. brew install mint diff --git a/scripts/style.sh b/scripts/style.sh index ea8172060fe..837d553d2ca 100755 --- a/scripts/style.sh +++ b/scripts/style.sh @@ -56,7 +56,7 @@ version="${version/ (*)/}" version="${version/.*/}" case "$version" in - 19) + 20) ;; google3-trunk) echo "Please use a publicly released clang-format; a recent LLVM release" @@ -65,13 +65,12 @@ case "$version" in exit 1 ;; *) - echo "Please upgrade to clang-format version 19." + echo "Please upgrade to clang-format version 20." echo "If it's installed via homebrew you can run:" echo "brew upgrade clang-format" exit 1 ;; esac - # Ensure that tools in `Mintfile` are installed locally to avoid permissions # problems that would otherwise arise from the default of installing in # /usr/local. From 2583fef01fa3a53ea6dc688c676845e078b3820c Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 5 Mar 2025 10:51:44 -0800 Subject: [PATCH 24/53] [Vertex AI] Refactor generateContentStream to fix Swift 6 warning (#14504) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Nick Cooke Co-authored-by: Andrew Heard --- .../Sources/GenerativeModel.swift | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index 9191ae3e6ec..adbb7109351 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -185,33 +185,31 @@ public final class GenerativeModel: Sendable { isStreaming: true, options: requestOptions) - var responseIterator = generativeAIService.loadRequestStream(request: generateContentRequest) - .makeAsyncIterator() - return AsyncThrowingStream { - let response: GenerateContentResponse? - do { - response = try await responseIterator.next() - } catch { - throw GenerativeModel.generateContentError(from: error) - } - - // The responseIterator will return `nil` when it's done. - guard let response = response else { - // This is the end of the stream! Signal it by sending `nil`. - return nil - } + return AsyncThrowingStream { continuation in + let responseStream = generativeAIService.loadRequestStream(request: generateContentRequest) + Task { + do { + for try await response in responseStream { + // Check the prompt feedback to see if the prompt was blocked. + if response.promptFeedback?.blockReason != nil { + throw GenerateContentError.promptBlocked(response: response) + } - // Check the prompt feedback to see if the prompt was blocked. - if response.promptFeedback?.blockReason != nil { - throw GenerateContentError.promptBlocked(response: response) - } + // If the stream ended early unexpectedly, throw an error. + if let finishReason = response.candidates.first?.finishReason, finishReason != .stop { + throw GenerateContentError.responseStoppedEarly( + reason: finishReason, + response: response + ) + } - // If the stream ended early unexpectedly, throw an error. - if let finishReason = response.candidates.first?.finishReason, finishReason != .stop { - throw GenerateContentError.responseStoppedEarly(reason: finishReason, response: response) - } else { - // Response was valid content, pass it along and continue. - return response + continuation.yield(response) + } + continuation.finish() + } catch { + continuation.finish(throwing: GenerativeModel.generateContentError(from: error)) + return + } } } } From 25f1a1722d0bbe0fe461fe2da0aa79329cd228d9 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 5 Mar 2025 16:50:54 -0500 Subject: [PATCH 25/53] [Vertex AI] Add `APIConfig` to support switching to the Developer API (#14521) --- FirebaseVertexAI/Sources/APIVersion.swift | 30 ----- FirebaseVertexAI/Sources/Constants.swift | 3 - .../Sources/CountTokensRequest.swift | 4 +- .../Sources/GenerateContentRequest.swift | 25 ++++- .../Sources/GenerativeAIRequest.swift | 16 +-- .../Sources/GenerativeModel.swift | 52 +++++---- .../Sources/Types/Internal/APIConfig.swift | 92 ++++++++++++++++ .../Imagen/ImagenGenerationRequest.swift | 10 +- .../Types/Public/Imagen/ImagenModel.swift | 6 + FirebaseVertexAI/Sources/VertexAI.swift | 103 +++++++++++++++--- .../Tests/Unit/APIVersionTests.swift | 32 ------ FirebaseVertexAI/Tests/Unit/ChatTests.swift | 1 + .../Tests/Unit/GenerativeModelTests.swift | 15 +++ .../Tests/Unit/RequestOptionsTest.swift | 27 ----- .../Imagen/ImagenGenerationRequestTests.swift | 11 +- .../Unit/Types/Internal/APIConfigTests.swift | 52 +++++++++ .../Tests/Unit/VertexComponentTests.swift | 99 +++++++++++++++-- 17 files changed, 412 insertions(+), 166 deletions(-) delete mode 100644 FirebaseVertexAI/Sources/APIVersion.swift create mode 100644 FirebaseVertexAI/Sources/Types/Internal/APIConfig.swift delete mode 100644 FirebaseVertexAI/Tests/Unit/APIVersionTests.swift create mode 100644 FirebaseVertexAI/Tests/Unit/Types/Internal/APIConfigTests.swift diff --git a/FirebaseVertexAI/Sources/APIVersion.swift b/FirebaseVertexAI/Sources/APIVersion.swift deleted file mode 100644 index 1d2edfbe647..00000000000 --- a/FirebaseVertexAI/Sources/APIVersion.swift +++ /dev/null @@ -1,30 +0,0 @@ -// 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. - -/// Versions of the Vertex AI in Firebase server API. -@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -// TODO(#14405): Make `APIVersion`, `v1` and `v1beta` public in Firebase 12. -struct APIVersion { - /// The stable channel for version 1 of the API. - static let v1 = APIVersion(versionIdentifier: "v1") - - /// The beta channel for version 1 of the API. - static let v1beta = APIVersion(versionIdentifier: "v1beta") - - let versionIdentifier: String - - init(versionIdentifier: String) { - self.versionIdentifier = versionIdentifier - } -} diff --git a/FirebaseVertexAI/Sources/Constants.swift b/FirebaseVertexAI/Sources/Constants.swift index 8f410c8768a..2b7ac34508d 100644 --- a/FirebaseVertexAI/Sources/Constants.swift +++ b/FirebaseVertexAI/Sources/Constants.swift @@ -16,9 +16,6 @@ import Foundation /// Constants associated with the Vertex AI for Firebase SDK. enum Constants { - /// The Vertex AI backend endpoint URL. - static let baseURL = "https://firebasevertexai.googleapis.com" - /// The base reverse-DNS name for `NSError` or `CustomNSError` error domains. /// /// - Important: A suffix must be appended to produce an error domain (e.g., diff --git a/FirebaseVertexAI/Sources/CountTokensRequest.swift b/FirebaseVertexAI/Sources/CountTokensRequest.swift index ef200ea2723..b057b00d6e7 100644 --- a/FirebaseVertexAI/Sources/CountTokensRequest.swift +++ b/FirebaseVertexAI/Sources/CountTokensRequest.swift @@ -23,6 +23,7 @@ struct CountTokensRequest { let tools: [Tool]? let generationConfig: GenerationConfig? + let apiConfig: APIConfig let options: RequestOptions } @@ -31,7 +32,8 @@ extension CountTokensRequest: GenerativeAIRequest { typealias Response = CountTokensResponse var url: URL { - URL(string: "\(Constants.baseURL)/\(options.apiVersion)/\(model):countTokens")! + URL(string: + "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/\(model):countTokens")! } } diff --git a/FirebaseVertexAI/Sources/GenerateContentRequest.swift b/FirebaseVertexAI/Sources/GenerateContentRequest.swift index e37515bee7e..97e7248fc7c 100644 --- a/FirebaseVertexAI/Sources/GenerateContentRequest.swift +++ b/FirebaseVertexAI/Sources/GenerateContentRequest.swift @@ -24,7 +24,8 @@ struct GenerateContentRequest: Sendable { let tools: [Tool]? let toolConfig: ToolConfig? let systemInstruction: ModelContent? - let isStreaming: Bool + let apiConfig: APIConfig + let apiMethod: APIMethod let options: RequestOptions } @@ -40,16 +41,28 @@ extension GenerateContentRequest: Encodable { } } +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +extension GenerateContentRequest { + enum APIMethod: String { + case generateContent + case streamGenerateContent + case countTokens + } +} + @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) extension GenerateContentRequest: GenerativeAIRequest { typealias Response = GenerateContentResponse var url: URL { - let modelURL = "\(Constants.baseURL)/\(options.apiVersion)/\(model)" - if isStreaming { - return URL(string: "\(modelURL):streamGenerateContent?alt=sse")! - } else { - return URL(string: "\(modelURL):generateContent")! + let modelURL = "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/\(model)" + switch apiMethod { + case .generateContent: + return URL(string: "\(modelURL):\(apiMethod.rawValue)")! + case .streamGenerateContent: + return URL(string: "\(modelURL):\(apiMethod.rawValue)?alt=sse")! + case .countTokens: + fatalError("\(Self.self) should be a property of \(CountTokensRequest.self).") } } } diff --git a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift index 5454912daa1..4f3291e1913 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift @@ -25,31 +25,17 @@ protocol GenerativeAIRequest: Sendable, Encodable { /// Configuration parameters for sending requests to the backend. @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -// TODO(#14405): Make the `apiVersion` constructor public in Firebase 12 with a default of `.v1`. public struct RequestOptions: Sendable { /// The request’s timeout interval in seconds; if not specified uses the default value for a /// `URLRequest`. let timeout: TimeInterval - /// The API version to use in requests to the backend. - let apiVersion: String - - /// Initializes a request options object. - /// - /// - Parameters: - /// - timeout: The request’s timeout interval in seconds; defaults to 180 seconds. - /// - apiVersion: The API version to use in requests to the backend. - init(timeout: TimeInterval = 180.0, apiVersion: APIVersion) { - self.timeout = timeout - self.apiVersion = apiVersion.versionIdentifier - } - /// Initializes a request options object. /// /// - Parameters: /// - timeout: The request’s timeout interval in seconds; defaults to 180 seconds. public init(timeout: TimeInterval = 180.0) { - self.init(timeout: timeout, apiVersion: .v1beta) + self.timeout = timeout } } diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index adbb7109351..59a91ac7b92 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -23,6 +23,9 @@ public final class GenerativeModel: Sendable { /// The resource name of the model in the backend; has the format "models/model-name". let modelResourceName: String + /// Configuration for the backend API used by this model. + let apiConfig: APIConfig + /// The backing service responsible for sending and receiving model requests to the backend. let generativeAIService: GenerativeAIService @@ -48,8 +51,8 @@ public final class GenerativeModel: Sendable { /// /// - Parameters: /// - name: The name of the model to use, for example `"gemini-1.0-pro"`. - /// - projectID: The project ID from the Firebase console. - /// - apiKey: The API key for your project. + /// - firebaseInfo: Firebase data used by the SDK, including project ID and API key. + /// - apiConfig: Configuration for the backend API used by this model. /// - generationConfig: The content generation parameters your model should use. /// - safetySettings: A value describing what types of harmful content your model should allow. /// - tools: A list of ``Tool`` objects that the model may use to generate the next response. @@ -60,6 +63,7 @@ public final class GenerativeModel: Sendable { /// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`. init(name: String, firebaseInfo: FirebaseInfo, + apiConfig: APIConfig, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, tools: [Tool]?, @@ -68,6 +72,7 @@ public final class GenerativeModel: Sendable { requestOptions: RequestOptions, urlSession: URLSession = .shared) { modelResourceName = name + self.apiConfig = apiConfig generativeAIService = GenerativeAIService( firebaseInfo: firebaseInfo, urlSession: urlSession @@ -118,15 +123,18 @@ public final class GenerativeModel: Sendable { -> GenerateContentResponse { try content.throwIfError() let response: GenerateContentResponse - let generateContentRequest = GenerateContentRequest(model: modelResourceName, - contents: content, - generationConfig: generationConfig, - safetySettings: safetySettings, - tools: tools, - toolConfig: toolConfig, - systemInstruction: systemInstruction, - isStreaming: false, - options: requestOptions) + let generateContentRequest = GenerateContentRequest( + model: modelResourceName, + contents: content, + generationConfig: generationConfig, + safetySettings: safetySettings, + tools: tools, + toolConfig: toolConfig, + systemInstruction: systemInstruction, + apiConfig: apiConfig, + apiMethod: .generateContent, + options: requestOptions + ) do { response = try await generativeAIService.loadRequest(request: generateContentRequest) } catch { @@ -175,15 +183,18 @@ public final class GenerativeModel: Sendable { public func generateContentStream(_ content: [ModelContent]) throws -> AsyncThrowingStream { try content.throwIfError() - let generateContentRequest = GenerateContentRequest(model: modelResourceName, - contents: content, - generationConfig: generationConfig, - safetySettings: safetySettings, - tools: tools, - toolConfig: toolConfig, - systemInstruction: systemInstruction, - isStreaming: true, - options: requestOptions) + let generateContentRequest = GenerateContentRequest( + model: modelResourceName, + contents: content, + generationConfig: generationConfig, + safetySettings: safetySettings, + tools: tools, + toolConfig: toolConfig, + systemInstruction: systemInstruction, + apiConfig: apiConfig, + apiMethod: .streamGenerateContent, + options: requestOptions + ) return AsyncThrowingStream { continuation in let responseStream = generativeAIService.loadRequestStream(request: generateContentRequest) @@ -249,6 +260,7 @@ public final class GenerativeModel: Sendable { systemInstruction: systemInstruction, tools: tools, generationConfig: generationConfig, + apiConfig: apiConfig, options: requestOptions ) return try await generativeAIService.loadRequest(request: countTokensRequest) diff --git a/FirebaseVertexAI/Sources/Types/Internal/APIConfig.swift b/FirebaseVertexAI/Sources/Types/Internal/APIConfig.swift new file mode 100644 index 00000000000..f0e6e7a90ab --- /dev/null +++ b/FirebaseVertexAI/Sources/Types/Internal/APIConfig.swift @@ -0,0 +1,92 @@ +// 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. + +/// Configuration for the generative AI backend API used by this SDK. +struct APIConfig: Sendable, Hashable { + /// The service to use for generative AI. + /// + /// This controls which backend API is used by the SDK. + let service: Service + + /// The version of the selected API to use, e.g., "v1". + let version: Version + + /// Initializes an API configuration. + /// + /// - Parameters: + /// - service: The API service to use for generative AI. + /// - version: The version of the API to use. + init(service: Service, version: Version) { + self.service = service + self.version = version + } +} + +extension APIConfig { + /// API services providing generative AI functionality. + /// + /// See [Vertex AI and Google AI + /// differences](https://cloud.google.com/vertex-ai/generative-ai/docs/overview#how-gemini-vertex-different-gemini-aistudio) + /// for a comparison of the two [API services](https://google.aip.dev/9#api-service). + enum Service: Hashable { + /// The Gemini Enterprise API provided by Vertex AI. + /// + /// See the [Cloud + /// docs](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference) for + /// more details. + case vertexAI + + /// The Gemini Developer API provided by Google AI. + /// + /// See the [Google AI docs](https://ai.google.dev/gemini-api/docs) for more details. + case developer(endpoint: Endpoint) + + /// The specific network address to use for API requests. + /// + /// This must correspond with the API set in `service`. + var endpoint: Endpoint { + switch self { + case .vertexAI: + return .firebaseVertexAIProd + case let .developer(endpoint: endpoint): + return endpoint + } + } + } +} + +extension APIConfig.Service { + /// Network addresses for generative AI API services. + enum Endpoint: String { + /// The Vertex AI in Firebase production endpoint. + case firebaseVertexAIProd = "https://firebasevertexai.googleapis.com" + + /// The Vertex AI in Firebase staging endpoint; for SDK development and testing only. + case firebaseVertexAIStaging = "https://staging-firebasevertexai.sandbox.googleapis.com" + + /// The Gemini Developer API production endpoint; for SDK development and testing only. + case generativeLanguage = "https://generativelanguage.googleapis.com" + } +} + +extension APIConfig { + /// Versions of the configured API service (`APIConfig.Service`). + enum Version: String { + /// The stable channel for version 1 of the API. + case v1 + + /// The beta channel for version 1 of the API. + case v1beta + } +} diff --git a/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift b/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift index eb354b1bb4f..ffb0e8bcf57 100644 --- a/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift +++ b/FirebaseVertexAI/Sources/Types/Internal/Imagen/ImagenGenerationRequest.swift @@ -17,13 +17,18 @@ import Foundation @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) struct ImagenGenerationRequest: Sendable { let model: String + let apiConfig: APIConfig let options: RequestOptions let instances: [ImageGenerationInstance] let parameters: ImageGenerationParameters - init(model: String, options: RequestOptions, instances: [ImageGenerationInstance], + init(model: String, + apiConfig: APIConfig, + options: RequestOptions, + instances: [ImageGenerationInstance], parameters: ImageGenerationParameters) { self.model = model + self.apiConfig = apiConfig self.options = options self.instances = instances self.parameters = parameters @@ -35,7 +40,8 @@ extension ImagenGenerationRequest: GenerativeAIRequest where ImageType: Decodabl typealias Response = ImagenGenerationResponse var url: URL { - return URL(string: "\(Constants.baseURL)/\(options.apiVersion)/\(model):predict")! + return URL(string: + "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/\(model):predict")! } } diff --git a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift index 8f894a52488..8cf13e818a0 100644 --- a/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift +++ b/FirebaseVertexAI/Sources/Types/Public/Imagen/ImagenModel.swift @@ -30,6 +30,9 @@ public final class ImagenModel { /// The resource name of the model in the backend; has the format "models/model-name". let modelResourceName: String + /// Configuration for the backend API used by this model. + let apiConfig: APIConfig + /// The backing service responsible for sending and receiving model requests to the backend. let generativeAIService: GenerativeAIService @@ -42,11 +45,13 @@ public final class ImagenModel { init(name: String, firebaseInfo: FirebaseInfo, + apiConfig: APIConfig, generationConfig: ImagenGenerationConfig?, safetySettings: ImagenSafetySettings?, requestOptions: RequestOptions, urlSession: URLSession = .shared) { modelResourceName = name + self.apiConfig = apiConfig generativeAIService = GenerativeAIService( firebaseInfo: firebaseInfo, urlSession: urlSession @@ -123,6 +128,7 @@ public final class ImagenModel { -> ImagenGenerationResponse where T: Decodable, T: ImagenImageRepresentable { let request = ImagenGenerationRequest( model: modelResourceName, + apiConfig: apiConfig, options: requestOptions, instances: [ImageGenerationInstance(prompt: prompt)], parameters: parameters diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index 097f3230ec4..b3404f4333a 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -36,8 +36,12 @@ public class VertexAI { guard let app = FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } + let vertexInstance = vertexAI(app: app, location: location) + assert(vertexInstance.apiConfig.service == .vertexAI) + assert(vertexInstance.apiConfig.service.endpoint == .firebaseVertexAIProd) + assert(vertexInstance.apiConfig.version == .v1beta) - return vertexAI(app: app, location: location) + return vertexInstance } /// Creates an instance of `VertexAI` configured with a custom `FirebaseApp`. @@ -50,18 +54,12 @@ public class VertexAI { /// for a list of supported locations. /// - Returns: A `VertexAI` instance, configured with the custom `FirebaseApp`. public static func vertexAI(app: FirebaseApp, location: String = "us-central1") -> VertexAI { - os_unfair_lock_lock(&instancesLock) - - // Unlock before the function returns. - defer { os_unfair_lock_unlock(&instancesLock) } + let vertexInstance = vertexAI(app: app, location: location, apiConfig: defaultVertexAIAPIConfig) + assert(vertexInstance.apiConfig.service == .vertexAI) + assert(vertexInstance.apiConfig.service.endpoint == .firebaseVertexAIProd) + assert(vertexInstance.apiConfig.version == .v1beta) - let instanceKey = "\(app.name):\(location)" - if let instance = instances[instanceKey] { - return instance - } - let newInstance = VertexAI(app: app, location: location) - instances[instanceKey] = newInstance - return newInstance + return vertexInstance } /// Initializes a generative model with the given parameters. @@ -92,6 +90,7 @@ public class VertexAI { return GenerativeModel( name: modelResourceName(modelName: modelName), firebaseInfo: firebaseInfo, + apiConfig: apiConfig, generationConfig: generationConfig, safetySettings: safetySettings, tools: tools, @@ -124,6 +123,7 @@ public class VertexAI { return ImagenModel( name: modelResourceName(modelName: modelName), firebaseInfo: firebaseInfo, + apiConfig: apiConfig, generationConfig: generationConfig, safetySettings: safetySettings, requestOptions: requestOptions @@ -139,25 +139,61 @@ public class VertexAI { /// Firebase data relevant to Vertex AI. let firebaseInfo: FirebaseInfo + let apiConfig: APIConfig + #if compiler(>=6) /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in /// the format `appName:location`. - private nonisolated(unsafe) static var instances: [String: VertexAI] = [:] + private nonisolated(unsafe) static var instances: [InstanceKey: VertexAI] = [:] /// Lock to manage access to the `instances` array to avoid race conditions. private nonisolated(unsafe) static var instancesLock: os_unfair_lock = .init() #else /// A map of active `VertexAI` instances keyed by the `FirebaseApp` name and the `location`, in /// the format `appName:location`. - private static var instances: [String: VertexAI] = [:] + private static var instances: [InstanceKey: VertexAI] = [:] /// Lock to manage access to the `instances` array to avoid race conditions. private static var instancesLock: os_unfair_lock = .init() #endif - let location: String + let location: String? + + static let defaultVertexAIAPIConfig = APIConfig(service: .vertexAI, version: .v1beta) + static let defaultDeveloperAPIConfig = APIConfig( + service: .developer(endpoint: .generativeLanguage), + version: .v1beta + ) + + static func developerAPI(apiConfig: APIConfig = defaultDeveloperAPIConfig) -> VertexAI { + guard let app = FirebaseApp.app() else { + fatalError("No instance of the default Firebase app was found.") + } + + return developerAPI(app: app, apiConfig: apiConfig) + } + + static func developerAPI(app: FirebaseApp, + apiConfig: APIConfig = defaultDeveloperAPIConfig) -> VertexAI { + return vertexAI(app: app, location: nil, apiConfig: apiConfig) + } + + static func vertexAI(app: FirebaseApp, location: String?, apiConfig: APIConfig) -> VertexAI { + os_unfair_lock_lock(&instancesLock) + + // Unlock before the function returns. + defer { os_unfair_lock_unlock(&instancesLock) } - init(app: FirebaseApp, location: String) { + let instanceKey = InstanceKey(appName: app.name, location: location, apiConfig: apiConfig) + if let instance = instances[instanceKey] { + return instance + } + let newInstance = VertexAI(app: app, location: location, apiConfig: apiConfig) + instances[instanceKey] = newInstance + return newInstance + } + + init(app: FirebaseApp, location: String?, apiConfig: APIConfig) { guard let projectID = app.options.projectID else { fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.") } @@ -175,6 +211,7 @@ public class VertexAI { googleAppID: app.options.googleAppID, firebaseApp: app ) + self.apiConfig = apiConfig self.location = location } @@ -187,6 +224,19 @@ public class VertexAI { available models. """) } + + switch apiConfig.service { + case .vertexAI: + return vertexAIModelResourceName(modelName: modelName) + case .developer: + return developerModelResourceName(modelName: modelName) + } + } + + private func vertexAIModelResourceName(modelName: String) -> String { + guard let location else { + fatalError("Location must be specified for the Vertex AI service.") + } guard !location.isEmpty && location .allSatisfy({ !$0.isWhitespace && !$0.isNewline && $0 != "/" }) else { fatalError(""" @@ -199,4 +249,25 @@ public class VertexAI { let projectID = firebaseInfo.projectID return "projects/\(projectID)/locations/\(location)/publishers/google/models/\(modelName)" } + + private func developerModelResourceName(modelName: String) -> String { + switch apiConfig.service.endpoint { + case .firebaseVertexAIStaging: + let projectID = firebaseInfo.projectID + return "projects/\(projectID)/models/\(modelName)" + case .generativeLanguage: + return "models/\(modelName)" + default: + fatalError("The Developer API is not supported on '\(apiConfig.service.endpoint)'.") + } + } + + /// Identifier for a unique instance of ``VertexAI``. + /// + /// This type is `Hashable` so that it can be used as a key in the `instances` dictionary. + private struct InstanceKey: Sendable, Hashable { + let appName: String + let location: String? + let apiConfig: APIConfig + } } diff --git a/FirebaseVertexAI/Tests/Unit/APIVersionTests.swift b/FirebaseVertexAI/Tests/Unit/APIVersionTests.swift deleted file mode 100644 index 0f9743b21a2..00000000000 --- a/FirebaseVertexAI/Tests/Unit/APIVersionTests.swift +++ /dev/null @@ -1,32 +0,0 @@ -// 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. - -import XCTest - -@testable import FirebaseVertexAI - -@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) -final class APIVersionTests: XCTestCase { - func testInitialize_v1() { - let apiVersion: APIVersion = .v1 - - XCTAssertEqual(apiVersion.versionIdentifier, "v1") - } - - func testInitialize_v1beta() { - let apiVersion: APIVersion = .v1beta - - XCTAssertEqual(apiVersion.versionIdentifier, "v1beta") - } -} diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift index a0525880da1..4e8a1ae0f73 100644 --- a/FirebaseVertexAI/Tests/Unit/ChatTests.swift +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -65,6 +65,7 @@ final class ChatTests: XCTestCase { googleAppID: "My app ID", firebaseApp: app ), + apiConfig: APIConfig(service: .vertexAI, version: .v1beta), tools: nil, requestOptions: RequestOptions(), urlSession: urlSession diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index bfe8905b708..891e4bc359e 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -58,6 +58,7 @@ final class GenerativeModelTests: XCTestCase { ].sorted() let testModelResourceName = "projects/test-project-id/locations/test-location/publishers/google/models/test-model" + let apiConfig = APIConfig(service: .vertexAI, version: .v1beta) var urlSession: URLSession! var model: GenerativeModel! @@ -69,6 +70,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -267,6 +269,7 @@ final class GenerativeModelTests: XCTestCase { // Model name is prefixed with "models/". name: "models/test-model", firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -384,6 +387,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken)), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -404,6 +408,7 @@ final class GenerativeModelTests: XCTestCase { name: testModelResourceName, firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken), privateAppID: true), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -423,6 +428,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(error: AppCheckErrorFake())), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -442,6 +448,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(token: authToken)), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -460,6 +467,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(token: nil)), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -478,6 +486,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", firebaseInfo: testFirebaseInfo(auth: AuthInteropFake(error: AuthErrorFake())), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -857,6 +866,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, tools: nil, requestOptions: requestOptions, urlSession: urlSession @@ -1149,6 +1159,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(token: appCheckToken)), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -1168,6 +1179,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(appCheck: AppCheckInteropFake(error: AppCheckErrorFake())), + apiConfig: apiConfig, tools: nil, requestOptions: RequestOptions(), urlSession: urlSession @@ -1311,6 +1323,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, tools: nil, requestOptions: requestOptions, urlSession: urlSession @@ -1383,6 +1396,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, generationConfig: generationConfig, tools: [Tool(functionDeclarations: [sumFunction])], systemInstruction: systemInstruction, @@ -1439,6 +1453,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: testModelResourceName, firebaseInfo: testFirebaseInfo(), + apiConfig: apiConfig, tools: nil, requestOptions: requestOptions, urlSession: urlSession diff --git a/FirebaseVertexAI/Tests/Unit/RequestOptionsTest.swift b/FirebaseVertexAI/Tests/Unit/RequestOptionsTest.swift index adf9441d716..1525393e812 100644 --- a/FirebaseVertexAI/Tests/Unit/RequestOptionsTest.swift +++ b/FirebaseVertexAI/Tests/Unit/RequestOptionsTest.swift @@ -19,13 +19,11 @@ import XCTest @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) final class RequestOptionsTests: XCTestCase { let defaultTimeout: TimeInterval = 180.0 - let defaultAPIVersion = APIVersion.v1beta.versionIdentifier func testInitialize_defaultValues() { let requestOptions = RequestOptions() XCTAssertEqual(requestOptions.timeout, defaultTimeout) - XCTAssertEqual(requestOptions.apiVersion, defaultAPIVersion) } func testInitialize_timeout() { @@ -34,30 +32,5 @@ final class RequestOptionsTests: XCTestCase { let requestOptions = RequestOptions(timeout: expectedTimeout) XCTAssertEqual(requestOptions.timeout, expectedTimeout) - XCTAssertEqual(requestOptions.apiVersion, defaultAPIVersion) - } - - func testInitialize_apiVersion_v1() { - let requestOptions = RequestOptions(apiVersion: .v1) - - XCTAssertEqual(requestOptions.timeout, defaultTimeout) - XCTAssertEqual(requestOptions.apiVersion, APIVersion.v1.versionIdentifier) - } - - func testInitialize_apiVersion_v1beta() { - let requestOptions = RequestOptions(apiVersion: .v1beta) - - XCTAssertEqual(requestOptions.timeout, defaultTimeout) - XCTAssertEqual(requestOptions.apiVersion, APIVersion.v1beta.versionIdentifier) - } - - func testInitialize_allOptions() { - let expectedTimeout = 30.0 - let expectedAPIVersion = APIVersion.v1 - - let requestOptions = RequestOptions(timeout: expectedTimeout, apiVersion: expectedAPIVersion) - - XCTAssertEqual(requestOptions.timeout, expectedTimeout) - XCTAssertEqual(requestOptions.apiVersion, expectedAPIVersion.versionIdentifier) } } diff --git a/FirebaseVertexAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift b/FirebaseVertexAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift index 14c59393ef6..679553a080f 100644 --- a/FirebaseVertexAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift +++ b/FirebaseVertexAI/Tests/Unit/Types/Imagen/ImagenGenerationRequestTests.swift @@ -36,6 +36,7 @@ final class ImagenGenerationRequestTests: XCTestCase { addWatermark: nil, includeResponsibleAIFilterReason: includeResponsibleAIFilterReason ) + let apiConfig = APIConfig(service: .vertexAI, version: .v1beta) let instance = ImageGenerationInstance(prompt: "test-prompt") @@ -46,6 +47,7 @@ final class ImagenGenerationRequestTests: XCTestCase { func testInitializeRequest_inlineDataImage() throws { let request = ImagenGenerationRequest( model: modelName, + apiConfig: apiConfig, options: requestOptions, instances: [instance], parameters: parameters @@ -57,13 +59,15 @@ final class ImagenGenerationRequestTests: XCTestCase { XCTAssertEqual(request.parameters, parameters) XCTAssertEqual( request.url, - URL(string: "\(Constants.baseURL)/\(requestOptions.apiVersion)/\(modelName):predict") + URL(string: + "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/\(modelName):predict") ) } func testInitializeRequest_fileDataImage() throws { let request = ImagenGenerationRequest( model: modelName, + apiConfig: apiConfig, options: requestOptions, instances: [instance], parameters: parameters @@ -75,7 +79,8 @@ final class ImagenGenerationRequestTests: XCTestCase { XCTAssertEqual(request.parameters, parameters) XCTAssertEqual( request.url, - URL(string: "\(Constants.baseURL)/\(requestOptions.apiVersion)/\(modelName):predict") + URL(string: + "\(apiConfig.service.endpoint.rawValue)/\(apiConfig.version.rawValue)/\(modelName):predict") ) } @@ -84,6 +89,7 @@ final class ImagenGenerationRequestTests: XCTestCase { func testEncodeRequest_inlineDataImage() throws { let request = ImagenGenerationRequest( model: modelName, + apiConfig: apiConfig, options: RequestOptions(), instances: [instance], parameters: parameters @@ -112,6 +118,7 @@ final class ImagenGenerationRequestTests: XCTestCase { func testEncodeRequest_fileDataImage() throws { let request = ImagenGenerationRequest( model: modelName, + apiConfig: apiConfig, options: RequestOptions(), instances: [instance], parameters: parameters diff --git a/FirebaseVertexAI/Tests/Unit/Types/Internal/APIConfigTests.swift b/FirebaseVertexAI/Tests/Unit/Types/Internal/APIConfigTests.swift new file mode 100644 index 00000000000..7633cf63960 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/Types/Internal/APIConfigTests.swift @@ -0,0 +1,52 @@ +// 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. + +import XCTest + +@testable import FirebaseVertexAI + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, tvOS 15.0, watchOS 8.0, *) +final class APIConfigTests: XCTestCase { + func testInitialize_vertexAI_prod_v1() { + let apiConfig = APIConfig(service: .vertexAI, version: .v1) + + XCTAssertEqual(apiConfig.service.endpoint.rawValue, "https://firebasevertexai.googleapis.com") + XCTAssertEqual(apiConfig.version.rawValue, "v1") + } + + func testInitialize_vertexAI_prod_v1beta() { + let apiConfig = APIConfig(service: .vertexAI, version: .v1beta) + + XCTAssertEqual(apiConfig.service.endpoint.rawValue, "https://firebasevertexai.googleapis.com") + XCTAssertEqual(apiConfig.version.rawValue, "v1beta") + } + + func testInitialize_developer_staging_v1beta() { + let apiConfig = APIConfig( + service: .developer(endpoint: .firebaseVertexAIStaging), version: .v1beta + ) + + XCTAssertEqual( + apiConfig.service.endpoint.rawValue, "https://staging-firebasevertexai.sandbox.googleapis.com" + ) + XCTAssertEqual(apiConfig.version.rawValue, "v1beta") + } + + func testInitialize_developer_generativeLanguage_v1beta() { + let apiConfig = APIConfig(service: .developer(endpoint: .generativeLanguage), version: .v1beta) + + XCTAssertEqual(apiConfig.service.endpoint.rawValue, "https://generativelanguage.googleapis.com") + XCTAssertEqual(apiConfig.version.rawValue, "v1beta") + } +} diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index 427e40bbc6a..2d7c1ec567f 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -33,26 +33,48 @@ class VertexComponentTests: XCTestCase { return options }() - static let app = { - FirebaseApp.configure(options: options) - return FirebaseApp(instanceWithName: "test", options: options) - }() + static let app = FirebaseApp(instanceWithName: "test", options: options) let location = "test-location" + let modelName = "test-model-name" + let systemInstruction = ModelContent(role: "system", parts: "test-system-instruction-prompt") + + override class func setUp() { + FirebaseApp.configure(options: options) + guard FirebaseApp.app() != nil else { + fatalError("The default app does not exist.") + } + } /// Test that the objc class is available for the component system to update the user agent. func testComponentsBeingRegistered() throws { XCTAssertNotNil(NSClassFromString("FIRVertexAIComponent")) } + /// Tests that a vertex instance can be created properly using the default Firebase pp. + func testVertexInstanceCreation_defaultApp() throws { + let vertex = VertexAI.vertexAI(location: location) + + XCTAssertNotNil(vertex) + XCTAssertEqual(vertex.firebaseInfo.projectID, VertexComponentTests.projectID) + XCTAssertEqual(vertex.firebaseInfo.apiKey, VertexComponentTests.apiKey) + XCTAssertEqual(vertex.location, location) + XCTAssertEqual(vertex.apiConfig.service, .vertexAI) + XCTAssertEqual(vertex.apiConfig.service.endpoint, .firebaseVertexAIProd) + XCTAssertEqual(vertex.apiConfig.version, .v1beta) + } + /// Tests that a vertex instance can be created properly. - func testVertexInstanceCreation() throws { + func testVertexInstanceCreation_customApp() throws { let vertex = VertexAI.vertexAI(app: VertexComponentTests.app, location: location) XCTAssertNotNil(vertex) XCTAssertEqual(vertex.firebaseInfo.projectID, VertexComponentTests.projectID) XCTAssertEqual(vertex.firebaseInfo.apiKey, VertexComponentTests.apiKey) XCTAssertEqual(vertex.location, location) + XCTAssertEqual(vertex.apiConfig.service, .vertexAI) + XCTAssertEqual(vertex.apiConfig.service.endpoint, .firebaseVertexAIProd) + XCTAssertEqual(vertex.apiConfig.version, .v1beta) } /// Tests that Vertex instances are reused properly. @@ -98,6 +120,14 @@ class VertexComponentTests: XCTestCase { XCTAssert(vertex1 !== vertex2) // Ensure they are different instances. } + func testSameAppAndDifferentAPI_newInstanceCreated() throws { + let vertex1 = VertexAI.vertexAI(app: VertexComponentTests.app) + let vertex2 = VertexAI.developerAPI(app: VertexComponentTests.app) + + // Ensure they are different instances. + XCTAssert(vertex1 !== vertex2) + } + /// Test that vertex instances get deallocated. func testVertexLifecycle() throws { weak var weakApp: FirebaseApp? @@ -109,7 +139,11 @@ class VertexComponentTests: XCTestCase { options.apiKey = VertexComponentTests.apiKey let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) weakApp = try XCTUnwrap(app1) - let vertex = VertexAI(app: app1, location: "transitory location") + let vertex = VertexAI( + app: app1, + location: "transitory location", + apiConfig: VertexAI.defaultVertexAIAPIConfig + ) weakVertex = vertex XCTAssertNotNil(weakVertex) } @@ -117,25 +151,50 @@ class VertexComponentTests: XCTestCase { XCTAssertNil(weakVertex) } - func testModelResourceName() throws { + func testModelResourceName_vertexAI() throws { let app = try XCTUnwrap(VertexComponentTests.app) let vertex = VertexAI.vertexAI(app: app, location: location) let model = "test-model-name" - let modelResourceName = vertex.modelResourceName(modelName: model) let projectID = vertex.firebaseInfo.projectID + let modelResourceName = vertex.modelResourceName(modelName: model) + + let location = try XCTUnwrap(vertex.location) XCTAssertEqual( modelResourceName, - "projects/\(projectID)/locations/\(vertex.location)/publishers/google/models/\(model)" + "projects/\(projectID)/locations/\(location)/publishers/google/models/\(model)" ) } - func testGenerativeModel() async throws { + func testModelResourceName_developerAPI_generativeLanguage() throws { + let app = try XCTUnwrap(VertexComponentTests.app) + let vertex = VertexAI.developerAPI(app: app) + let model = "test-model-name" + + let modelResourceName = vertex.modelResourceName(modelName: model) + + XCTAssertEqual(modelResourceName, "models/\(model)") + } + + func testModelResourceName_developerAPI_firebaseVertexAI() throws { + let app = try XCTUnwrap(VertexComponentTests.app) + let apiConfig = APIConfig( + service: .developer(endpoint: .firebaseVertexAIStaging), + version: .v1beta + ) + let vertex = VertexAI.developerAPI(app: app, apiConfig: apiConfig) + let model = "test-model-name" + let projectID = vertex.firebaseInfo.projectID + + let modelResourceName = vertex.modelResourceName(modelName: model) + + XCTAssertEqual(modelResourceName, "projects/\(projectID)/models/\(model)") + } + + func testGenerativeModel_vertexAI() async throws { let app = try XCTUnwrap(VertexComponentTests.app) let vertex = VertexAI.vertexAI(app: app, location: location) - let modelName = "test-model-name" let modelResourceName = vertex.modelResourceName(modelName: modelName) - let systemInstruction = ModelContent(role: "system", parts: "test-system-instruction-prompt") let generativeModel = vertex.generativeModel( modelName: modelName, @@ -144,5 +203,21 @@ class VertexComponentTests: XCTestCase { XCTAssertEqual(generativeModel.modelResourceName, modelResourceName) XCTAssertEqual(generativeModel.systemInstruction, systemInstruction) + XCTAssertEqual(generativeModel.apiConfig, VertexAI.defaultVertexAIAPIConfig) + } + + func testGenerativeModel_developerAPI() async throws { + let app = try XCTUnwrap(VertexComponentTests.app) + let vertex = VertexAI.developerAPI(app: app) + let modelResourceName = vertex.modelResourceName(modelName: modelName) + + let generativeModel = vertex.generativeModel( + modelName: modelName, + systemInstruction: systemInstruction + ) + + XCTAssertEqual(generativeModel.modelResourceName, modelResourceName) + XCTAssertEqual(generativeModel.systemInstruction, systemInstruction) + XCTAssertEqual(generativeModel.apiConfig, VertexAI.defaultDeveloperAPIConfig) } } From 2b802c4a59b5d152376d9b15f6c8847141e27c5d Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 5 Mar 2025 13:51:42 -0800 Subject: [PATCH 26/53] Update README.md for clang-format 20 (#14533) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5a0c418e97..7ab3b4ba615 100644 --- a/README.md +++ b/README.md @@ -153,7 +153,7 @@ GitHub Actions will verify that any code changes are done in a style-compliant way. Install `clang-format` and `mint`: ```console -brew install clang-format@19 +brew install clang-format@20 brew install mint ``` From fe3ee9053f9e64c6f3cfdbfa31ae431cd037896d Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 6 Mar 2025 06:51:53 -0800 Subject: [PATCH 27/53] [vertex-ai] Check for Swift 6 warnings (#14535) --- .github/workflows/vertexai.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index 0b67a8285da..b9f3619274b 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -137,11 +137,10 @@ jobs: xcode: Xcode_16.2 swift_version: 5.9 warnings: - #TODO: Fix remaining warning in GenerativeAIService and remove --allow-warnings. - os: macos-15 xcode: Xcode_16.2 swift_version: 6.0 - warnings: --allow-warnings + warnings: runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From 81ab6f9e20223fca2fb9caf3b565c4a9e9cc8a1a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 6 Mar 2025 10:23:16 -0500 Subject: [PATCH 28/53] [CI] Run `check` workflow on Ubuntu (#14534) --- .github/workflows/check.yml | 12 +++++++++--- scripts/check.sh | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b2a6a56a984..6ddea6fef00 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -15,7 +15,7 @@ jobs: check: # Don't run on private repo. if: github.repository == 'Firebase/firebase-ios-sdk' - runs-on: macos-14 + runs-on: ubuntu-latest env: MINT_PATH: ${{ github.workspace }}/mint steps: @@ -33,7 +33,13 @@ jobs: restore-keys: ${{ runner.os }}-mint- - name: Setup check - run: scripts/setup_check.sh + run: | + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + scripts/setup_check.sh - name: Check - run: scripts/check.sh --test-only + run: | + eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + # Add Homebrew clang-format to first in PATH + export PATH="/home/linuxbrew/.linuxbrew/opt/clang-format/bin:$PATH" + scripts/check.sh --test-only diff --git a/scripts/check.sh b/scripts/check.sh index 59cad32aa93..6e98cd089f0 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -290,7 +290,7 @@ python --version "${top_dir}/scripts/check_filename_spaces.sh" "${top_dir}/scripts/check_copyright.sh" "${top_dir}/scripts/check_test_inclusion.py" -"${top_dir}/scripts/check_imports.swift" +swift "${top_dir}/scripts/check_imports.swift" # Google C++ style lint_cmd=("${top_dir}/scripts/check_lint.py") From 8ee1e96f8901b39fd3355ed27e66566faa9355bd Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 6 Mar 2025 18:31:41 -0500 Subject: [PATCH 29/53] [Config] Add encoder and decoder param to public codable APIs (#14525) --- FirebaseRemoteConfig/CHANGELOG.md | 5 +++ FirebaseRemoteConfig/Swift/Codable.swift | 10 +++--- .../Tests/Swift/SwiftAPI/Codable.swift | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index b90b6cc2871..ff7355e7862 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased +- [fixed] Codable APIs now accept optional `FirebaseDataEncoder` and + `FirebaseDataDecoder` parameters, allowing for customization of + encoding/decoding behavior. (#14368) + # 11.9.0 - [fixed] Mark internal `fetchSession` property as `atomic` to prevent a concurrency related crash. (#14449) diff --git a/FirebaseRemoteConfig/Swift/Codable.swift b/FirebaseRemoteConfig/Swift/Codable.swift index d2df8a90db0..1152890be83 100644 --- a/FirebaseRemoteConfig/Swift/Codable.swift +++ b/FirebaseRemoteConfig/Swift/Codable.swift @@ -47,19 +47,21 @@ public extension RemoteConfig { /// Decodes a struct from the respective Remote Config values. /// /// - Parameter asType: The type to decode to. - func decoded(asType: Value.Type = Value.self) throws -> Value { + func decoded(asType: Value.Type = Value.self, + decoder: FirebaseDataDecoder = .init()) throws -> Value { let keys = allKeys(from: RemoteConfigSource.default) + allKeys(from: RemoteConfigSource.remote) let config = keys.reduce(into: [String: FirebaseRemoteConfigValueDecoderHelper]()) { $0[$1] = FirebaseRemoteConfigValueDecoderHelper(value: configValue(forKey: $1)) } - return try FirebaseDataDecoder().decode(Value.self, from: config) + return try decoder.decode(Value.self, from: config) } /// Sets config defaults from an encodable struct. /// /// - Parameter value: The object to use to set the defaults. - func setDefaults(from value: Value) throws { - guard let encoded = try FirebaseDataEncoder().encode(value) as? [String: NSObject] else { + func setDefaults(from value: Value, + encoder: FirebaseDataEncoder = .init()) throws { + guard let encoded = try encoder.encode(value) as? [String: NSObject] else { throw RemoteConfigCodableError.invalidSetDefaultsInput( "The setDefaults input: \(value), must be a Struct that encodes to a Dictionary" ) diff --git a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/Codable.swift b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/Codable.swift index 3ccacbb75de..ba055d09889 100644 --- a/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/Codable.swift +++ b/FirebaseRemoteConfig/Tests/Swift/SwiftAPI/Codable.swift @@ -13,6 +13,7 @@ // limitations under the License. import FirebaseRemoteConfig +import FirebaseSharedSwift import XCTest @@ -206,4 +207,35 @@ class CodableTests: APITestBase { XCTAssertEqual(readDefaults.arrayValue, ["foo", "bar", "baz"]) XCTAssertEqual(readDefaults.arrayIntValue, [1, 2, 0, 3]) } + + // MARK: - Test using injected encoder/decoder. + + func testDateEncodingAndDecodingWithNonDefaultCoders() throws { + // Given + struct DateDefaults: Codable { + let date: Date + } + + let defaults = DateDefaults(date: Date()) + + // When + let encoder = FirebaseDataEncoder() + encoder.dateEncodingStrategy = .iso8601 + try config.setDefaults(from: defaults, encoder: encoder) + + // - Uses default decoder that won't decode ISO8601 format. + let improperlyDecodedDefaults: DateDefaults = try config.decoded() + XCTAssertNotEqual(improperlyDecodedDefaults.date, defaults.date) + + // Then + let decoder = FirebaseDataDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let decodedDefaults: DateDefaults = try config.decoded(decoder: decoder) + XCTAssert(Calendar.current.isDate( + decodedDefaults.date, + equalTo: defaults.date, + toGranularity: .second + )) + } } From 52bae699f73b9663d142145c43ff3742839cf9d5 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:33:27 -0500 Subject: [PATCH 30/53] [Infra] Install visionOS sims to workflows that couldn't find it (#14543) --- .github/workflows/abtesting.yml | 3 +++ .github/workflows/auth.yml | 3 +++ .github/workflows/core.yml | 3 +++ .github/workflows/crashlytics.yml | 3 +++ .github/workflows/mlmodeldownloader.yml | 3 +++ .github/workflows/remoteconfig.yml | 3 +++ .github/workflows/sessions.yml | 3 +++ .github/workflows/vertexai.yml | 3 +++ 8 files changed, 24 insertions(+) diff --git a/.github/workflows/abtesting.yml b/.github/workflows/abtesting.yml index e2276e99178..ca4c532ca88 100644 --- a/.github/workflows/abtesting.yml +++ b/.github/workflows/abtesting.yml @@ -120,6 +120,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - uses: nick-fields/retry@v3 diff --git a/.github/workflows/auth.yml b/.github/workflows/auth.yml index 2d1e83f3288..260a35be252 100644 --- a/.github/workflows/auth.yml +++ b/.github/workflows/auth.yml @@ -141,6 +141,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS spm' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - uses: nick-fields/retry@v3 diff --git a/.github/workflows/core.yml b/.github/workflows/core.yml index 72206f4ff65..3a345918410 100644 --- a/.github/workflows/core.yml +++ b/.github/workflows/core.yml @@ -101,6 +101,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: Unit Tests diff --git a/.github/workflows/crashlytics.yml b/.github/workflows/crashlytics.yml index 47867fb5fd8..fe5561bf8dc 100644 --- a/.github/workflows/crashlytics.yml +++ b/.github/workflows/crashlytics.yml @@ -116,6 +116,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - uses: nick-fields/retry@v3 diff --git a/.github/workflows/mlmodeldownloader.yml b/.github/workflows/mlmodeldownloader.yml index f36056c50b8..f163a3bf62d 100644 --- a/.github/workflows/mlmodeldownloader.yml +++ b/.github/workflows/mlmodeldownloader.yml @@ -131,6 +131,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: Unit Tests diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index 53457d35e70..b8a02e62b1f 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -155,6 +155,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: Unit Tests diff --git a/.github/workflows/sessions.yml b/.github/workflows/sessions.yml index 2fa03e3af31..e30510d6fdd 100644 --- a/.github/workflows/sessions.yml +++ b/.github/workflows/sessions.yml @@ -111,6 +111,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - uses: nick-fields/retry@v3 diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index b9f3619274b..18afa033320 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -81,6 +81,9 @@ jobs: run: scripts/update_vertexai_responses.sh - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - uses: nick-fields/retry@v3 From 0fcadb959f93d60944eba7482fcc72c1beb5619a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Fri, 7 Mar 2025 10:46:56 -0500 Subject: [PATCH 31/53] [Vertex AI] Parameterize integration tests for Vertex and Dev API (#14540) --- FirebaseVertexAI/Sources/VertexAI.swift | 44 ++----- .../GenerateContentIntegrationTests.swift | 118 ++++++++++++++++++ .../Tests/Integration/IntegrationTests.swift | 21 ---- .../VertexAITestApp.xcodeproj/project.pbxproj | 4 + .../Tests/Unit/VertexComponentTests.swift | 41 ++++-- 5 files changed, 163 insertions(+), 65 deletions(-) create mode 100644 FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index b3404f4333a..16fc8be0561 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -25,35 +25,18 @@ import Foundation public class VertexAI { // MARK: - Public APIs - /// The default `VertexAI` instance. - /// - /// - Parameter location: The region identifier, defaulting to `us-central1`; see [Vertex AI - /// regions - /// ](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions) - /// for a list of supported regions. - /// - Returns: An instance of `VertexAI`, configured with the default `FirebaseApp`. - public static func vertexAI(location: String = "us-central1") -> VertexAI { - guard let app = FirebaseApp.app() else { - fatalError("No instance of the default Firebase app was found.") - } - let vertexInstance = vertexAI(app: app, location: location) - assert(vertexInstance.apiConfig.service == .vertexAI) - assert(vertexInstance.apiConfig.service.endpoint == .firebaseVertexAIProd) - assert(vertexInstance.apiConfig.version == .v1beta) - - return vertexInstance - } - - /// Creates an instance of `VertexAI` configured with a custom `FirebaseApp`. + /// Creates an instance of `VertexAI`. /// /// - Parameters: - /// - app: The custom `FirebaseApp` used for initialization. + /// - app: A custom `FirebaseApp` used for initialization; if not specified, uses the default + /// ``FirebaseApp``. /// - location: The region identifier, defaulting to `us-central1`; see /// [Vertex AI locations] /// (https://firebase.google.com/docs/vertex-ai/locations?platform=ios#available-locations) /// for a list of supported locations. /// - Returns: A `VertexAI` instance, configured with the custom `FirebaseApp`. - public static func vertexAI(app: FirebaseApp, location: String = "us-central1") -> VertexAI { + public static func vertexAI(app: FirebaseApp? = nil, + location: String = "us-central1") -> VertexAI { let vertexInstance = vertexAI(app: app, location: location, apiConfig: defaultVertexAIAPIConfig) assert(vertexInstance.apiConfig.service == .vertexAI) assert(vertexInstance.apiConfig.service.endpoint == .firebaseVertexAIProd) @@ -160,25 +143,12 @@ public class VertexAI { let location: String? static let defaultVertexAIAPIConfig = APIConfig(service: .vertexAI, version: .v1beta) - static let defaultDeveloperAPIConfig = APIConfig( - service: .developer(endpoint: .generativeLanguage), - version: .v1beta - ) - static func developerAPI(apiConfig: APIConfig = defaultDeveloperAPIConfig) -> VertexAI { - guard let app = FirebaseApp.app() else { + static func vertexAI(app: FirebaseApp?, location: String?, apiConfig: APIConfig) -> VertexAI { + guard let app = app ?? FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } - return developerAPI(app: app, apiConfig: apiConfig) - } - - static func developerAPI(app: FirebaseApp, - apiConfig: APIConfig = defaultDeveloperAPIConfig) -> VertexAI { - return vertexAI(app: app, location: nil, apiConfig: apiConfig) - } - - static func vertexAI(app: FirebaseApp, location: String?, apiConfig: APIConfig) -> VertexAI { os_unfair_lock_lock(&instancesLock) // Unlock before the function returns. diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift new file mode 100644 index 00000000000..bef4349fc90 --- /dev/null +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift @@ -0,0 +1,118 @@ +// 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. + +import FirebaseAuth +import FirebaseCore +import FirebaseStorage +import FirebaseVertexAI +import Testing +import VertexAITestApp + +@testable import struct FirebaseVertexAI.APIConfig + +@Suite(.serialized) +struct GenerateContentIntegrationTests { + static let vertexV1Config = APIConfig(service: .vertexAI, version: .v1) + static let vertexV1BetaConfig = APIConfig(service: .vertexAI, version: .v1beta) + static let developerV1BetaConfig = APIConfig( + service: .developer(endpoint: .generativeLanguage), + version: .v1beta + ) + + // Set temperature, topP and topK to lowest allowed values to make responses more deterministic. + static let generationConfig = GenerationConfig( + temperature: 0.0, + topP: 0.0, + topK: 1, + responseMIMEType: "text/plain" + ) + static let systemInstruction = ModelContent( + role: "system", + parts: "You are a friendly and helpful assistant." + ) + static let safetySettings = [ + SafetySetting(harmCategory: .harassment, threshold: .blockLowAndAbove), + SafetySetting(harmCategory: .hateSpeech, threshold: .blockLowAndAbove), + SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockLowAndAbove), + SafetySetting(harmCategory: .dangerousContent, threshold: .blockLowAndAbove), + SafetySetting(harmCategory: .civicIntegrity, threshold: .blockLowAndAbove), + ] + // Candidates and total token counts may differ slightly between runs due to whitespace tokens. + let tokenCountAccuracy = 1 + + let storage: Storage + let userID1: String + + init() async throws { + let authResult = try await Auth.auth().signIn( + withEmail: Credentials.emailAddress1, + password: Credentials.emailPassword1 + ) + userID1 = authResult.user.uid + + storage = Storage.storage() + } + + @Test(arguments: [vertexV1Config, vertexV1BetaConfig, developerV1BetaConfig]) + func generateContent(_ apiConfig: APIConfig) async throws { + let model = GenerateContentIntegrationTests.model(apiConfig: apiConfig) + let prompt = "Where is Google headquarters located? Answer with the city name only." + + let response = try await model.generateContent(prompt) + + let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines) + #expect(text == "Mountain View") + + let usageMetadata = try #require(response.usageMetadata) + #expect(usageMetadata.promptTokenCount == 21) + #expect(usageMetadata.candidatesTokenCount.isEqual(to: 3, accuracy: tokenCountAccuracy)) + #expect(usageMetadata.totalTokenCount.isEqual(to: 24, accuracy: tokenCountAccuracy)) + #expect(usageMetadata.promptTokensDetails.count == 1) + let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first) + #expect(promptTokensDetails.modality == .text) + #expect(promptTokensDetails.tokenCount == usageMetadata.promptTokenCount) + #expect(usageMetadata.candidatesTokensDetails.count == 1) + let candidatesTokensDetails = try #require(usageMetadata.candidatesTokensDetails.first) + #expect(candidatesTokensDetails.modality == .text) + #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount) + } + + static func model(apiConfig: APIConfig) -> GenerativeModel { + return instance(apiConfig: apiConfig).generativeModel( + modelName: "gemini-2.0-flash", + generationConfig: generationConfig, + safetySettings: safetySettings, + tools: [], + toolConfig: .init(functionCallingConfig: .none()), + systemInstruction: systemInstruction + ) + } + + // TODO(andrewheard): Move this helper to a file in the Utilities folder. + static func instance(apiConfig: APIConfig) -> VertexAI { + switch apiConfig.service { + case .vertexAI: + return VertexAI.vertexAI(app: nil, location: "us-central1", apiConfig: apiConfig) + case .developer: + return VertexAI.vertexAI(app: nil, location: nil, apiConfig: apiConfig) + } + } +} + +// TODO(andrewheard): Move this extension to a file in the Utilities folder. +extension Numeric where Self: Strideable, Self.Stride.Magnitude: Comparable { + func isEqual(to other: Self, accuracy: Self.Stride) -> Bool { + return distance(to: other).magnitude < accuracy.magnitude + } +} diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift index b5bfc94b93b..4cd60cf3e76 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift @@ -69,27 +69,6 @@ final class IntegrationTests: XCTestCase { // MARK: - Generate Content - func testGenerateContent() async throws { - let prompt = "Where is Google headquarters located? Answer with the city name only." - - let response = try await model.generateContent(prompt) - - let text = try XCTUnwrap(response.text).trimmingCharacters(in: .whitespacesAndNewlines) - XCTAssertEqual(text, "Mountain View") - let usageMetadata = try XCTUnwrap(response.usageMetadata) - XCTAssertEqual(usageMetadata.promptTokenCount, 21) - XCTAssertEqual(usageMetadata.candidatesTokenCount, 3, accuracy: tokenCountAccuracy) - XCTAssertEqual(usageMetadata.totalTokenCount, 24, accuracy: tokenCountAccuracy) - XCTAssertEqual(usageMetadata.promptTokensDetails.count, 1) - let promptTokensDetails = try XCTUnwrap(usageMetadata.promptTokensDetails.first) - XCTAssertEqual(promptTokensDetails.modality, .text) - XCTAssertEqual(promptTokensDetails.tokenCount, usageMetadata.promptTokenCount) - XCTAssertEqual(usageMetadata.candidatesTokensDetails.count, 1) - let candidatesTokensDetails = try XCTUnwrap(usageMetadata.candidatesTokensDetails.first) - XCTAssertEqual(candidatesTokensDetails.modality, .text) - XCTAssertEqual(candidatesTokensDetails.tokenCount, usageMetadata.candidatesTokenCount) - } - func testGenerateContentStream() async throws { let expectedText = """ 1. Mercury diff --git a/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj b/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj index b2b2b4f643f..ea5c7a45531 100644 --- a/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 8692F29E2CC9477800539E8F /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 8692F29D2CC9477800539E8F /* FirebaseVertexAI */; }; 8698D7462CD3CF3600ABA833 /* FirebaseAppTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D7452CD3CF2F00ABA833 /* FirebaseAppTestUtils.swift */; }; 8698D7482CD4332B00ABA833 /* TestAppCheckProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D7472CD4332B00ABA833 /* TestAppCheckProviderFactory.swift */; }; + 86D77DFC2D7A5340003D155D /* GenerateContentIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -49,6 +50,7 @@ 868A7C552CCC271300E449DD /* TestApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TestApp.entitlements; sourceTree = ""; }; 8698D7452CD3CF2F00ABA833 /* FirebaseAppTestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAppTestUtils.swift; sourceTree = ""; }; 8698D7472CD4332B00ABA833 /* TestAppCheckProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppCheckProviderFactory.swift; sourceTree = ""; }; + 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateContentIntegrationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -126,6 +128,7 @@ children = ( 868A7C4D2CCC1F4700E449DD /* Credentials.swift */, 8661386D2CC943DE00F4B78E /* IntegrationTests.swift */, + 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */, 864F8F702D4980D60002EA7E /* ImagenIntegrationTests.swift */, 862218802D04E08D007ED2D4 /* IntegrationTestUtils.swift */, ); @@ -273,6 +276,7 @@ 868A7C4F2CCC229F00E449DD /* Credentials.swift in Sources */, 864F8F712D4980DD0002EA7E /* ImagenIntegrationTests.swift in Sources */, 862218812D04E098007ED2D4 /* IntegrationTestUtils.swift in Sources */, + 86D77DFC2D7A5340003D155D /* GenerateContentIntegrationTests.swift in Sources */, 8661386E2CC943DE00F4B78E /* IntegrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index 2d7c1ec567f..857b9e024cf 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -51,8 +51,22 @@ class VertexComponentTests: XCTestCase { XCTAssertNotNil(NSClassFromString("FIRVertexAIComponent")) } - /// Tests that a vertex instance can be created properly using the default Firebase pp. + /// Tests that a vertex instance can be created properly using the default Firebase app. func testVertexInstanceCreation_defaultApp() throws { + let vertex = VertexAI.vertexAI() + + XCTAssertNotNil(vertex) + XCTAssertEqual(vertex.firebaseInfo.projectID, VertexComponentTests.projectID) + XCTAssertEqual(vertex.firebaseInfo.apiKey, VertexComponentTests.apiKey) + XCTAssertEqual(vertex.location, "us-central1") + XCTAssertEqual(vertex.apiConfig.service, .vertexAI) + XCTAssertEqual(vertex.apiConfig.service.endpoint, .firebaseVertexAIProd) + XCTAssertEqual(vertex.apiConfig.version, .v1beta) + } + + /// Tests that a vertex instance can be created properly using the default Firebase app and custom + /// location. + func testVertexInstanceCreation_defaultApp_customLocation() throws { let vertex = VertexAI.vertexAI(location: location) XCTAssertNotNil(vertex) @@ -121,8 +135,16 @@ class VertexComponentTests: XCTestCase { } func testSameAppAndDifferentAPI_newInstanceCreated() throws { - let vertex1 = VertexAI.vertexAI(app: VertexComponentTests.app) - let vertex2 = VertexAI.developerAPI(app: VertexComponentTests.app) + let vertex1 = VertexAI.vertexAI( + app: VertexComponentTests.app, + location: location, + apiConfig: APIConfig(service: .vertexAI, version: .v1beta) + ) + let vertex2 = VertexAI.vertexAI( + app: VertexComponentTests.app, + location: location, + apiConfig: APIConfig(service: .vertexAI, version: .v1) + ) // Ensure they are different instances. XCTAssert(vertex1 !== vertex2) @@ -168,7 +190,8 @@ class VertexComponentTests: XCTestCase { func testModelResourceName_developerAPI_generativeLanguage() throws { let app = try XCTUnwrap(VertexComponentTests.app) - let vertex = VertexAI.developerAPI(app: app) + let apiConfig = APIConfig(service: .developer(endpoint: .generativeLanguage), version: .v1beta) + let vertex = VertexAI.vertexAI(app: app, location: nil, apiConfig: apiConfig) let model = "test-model-name" let modelResourceName = vertex.modelResourceName(modelName: model) @@ -182,7 +205,7 @@ class VertexComponentTests: XCTestCase { service: .developer(endpoint: .firebaseVertexAIStaging), version: .v1beta ) - let vertex = VertexAI.developerAPI(app: app, apiConfig: apiConfig) + let vertex = VertexAI.vertexAI(app: app, location: nil, apiConfig: apiConfig) let model = "test-model-name" let projectID = vertex.firebaseInfo.projectID @@ -208,7 +231,11 @@ class VertexComponentTests: XCTestCase { func testGenerativeModel_developerAPI() async throws { let app = try XCTUnwrap(VertexComponentTests.app) - let vertex = VertexAI.developerAPI(app: app) + let apiConfig = APIConfig( + service: .developer(endpoint: .firebaseVertexAIStaging), + version: .v1beta + ) + let vertex = VertexAI.vertexAI(app: app, location: nil, apiConfig: apiConfig) let modelResourceName = vertex.modelResourceName(modelName: modelName) let generativeModel = vertex.generativeModel( @@ -218,6 +245,6 @@ class VertexComponentTests: XCTestCase { XCTAssertEqual(generativeModel.modelResourceName, modelResourceName) XCTAssertEqual(generativeModel.systemInstruction, systemInstruction) - XCTAssertEqual(generativeModel.apiConfig, VertexAI.defaultDeveloperAPIConfig) + XCTAssertEqual(generativeModel.apiConfig, apiConfig) } } From c0501cb67051d54b8d985d57097b659784d82999 Mon Sep 17 00:00:00 2001 From: themiswang Date: Fri, 7 Mar 2025 11:55:22 -0500 Subject: [PATCH 32/53] remove useless method (#14544) --- .../Components/FIRCLSApplication.h | 4 --- .../Components/FIRCLSApplication.m | 26 ------------------- 2 files changed, 30 deletions(-) diff --git a/Crashlytics/Crashlytics/Components/FIRCLSApplication.h b/Crashlytics/Crashlytics/Components/FIRCLSApplication.h index a04c8669969..0661d3128d5 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSApplication.h +++ b/Crashlytics/Crashlytics/Components/FIRCLSApplication.h @@ -83,10 +83,6 @@ UIApplication* FIRCLSApplicationSharedInstance(void); id FIRCLSApplicationSharedInstance(void); #endif -void FIRCLSApplicationOpenURL(NSURL* url, - NSExtensionContext* extensionContext, - void (^completionBlock)(BOOL success)); - id FIRCLSApplicationBeginActivity(NSActivityOptions options, NSString* reason); void FIRCLSApplicationEndActivity(id activity); diff --git a/Crashlytics/Crashlytics/Components/FIRCLSApplication.m b/Crashlytics/Crashlytics/Components/FIRCLSApplication.m index e1a0db632ec..67e452bd84d 100644 --- a/Crashlytics/Crashlytics/Components/FIRCLSApplication.m +++ b/Crashlytics/Crashlytics/Components/FIRCLSApplication.m @@ -155,32 +155,6 @@ id FIRCLSApplicationSharedInstance(void) { } #endif -void FIRCLSApplicationOpenURL(NSURL* url, - NSExtensionContext* extensionContext, - void (^completionBlock)(BOOL success)) { - if (extensionContext) { - [extensionContext openURL:url completionHandler:completionBlock]; - return; - } - - BOOL result = NO; - -#if TARGET_OS_IOS - // What's going on here is the value returned is a scalar, but we really need an object to - // call this dynamically. Hoops must be jumped. - NSInvocationOperation* op = - [[NSInvocationOperation alloc] initWithTarget:FIRCLSApplicationSharedInstance() - selector:@selector(openURL:) - object:url]; - [op start]; - [op.result getValue:&result]; -#elif CLS_TARGET_OS_OSX - result = [[NSClassFromString(@"NSWorkspace") sharedWorkspace] openURL:url]; -#endif - - completionBlock(result); -} - id FIRCLSApplicationBeginActivity(NSActivityOptions options, NSString* reason) { if ([[NSProcessInfo processInfo] respondsToSelector:@selector(beginActivityWithOptions: reason:)]) { From c7ceee47cb981eef83e204b117a71304f8d1ff63 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 7 Mar 2025 14:50:33 -0800 Subject: [PATCH 33/53] Add imagen generation example to VertexAI Sample (#14545) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- FirebaseVertexAI/CHANGELOG.md | 3 +- .../Sample/ImagenScreen/ImagenScreen.swift | 70 ++++++++++++++++ .../Sample/ImagenScreen/ImagenViewModel.swift | 83 +++++++++++++++++++ .../VertexAISample.xcodeproj/project.pbxproj | 16 ++++ .../Sample/VertexAISample/ContentView.swift | 5 ++ 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 FirebaseVertexAI/Sample/ImagenScreen/ImagenScreen.swift create mode 100644 FirebaseVertexAI/Sample/ImagenScreen/ImagenViewModel.swift diff --git a/FirebaseVertexAI/CHANGELOG.md b/FirebaseVertexAI/CHANGELOG.md index 47396ecb768..e7fc4c18d6c 100644 --- a/FirebaseVertexAI/CHANGELOG.md +++ b/FirebaseVertexAI/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased -- [feature] The Firebase Vertex AI SDK no longer requires `@preconcurrency` when imported in Swift 6. +- [feature] The Vertex AI SDK no longer requires `@preconcurrency` when imported in Swift 6. +- [feature] The Vertex AI Sample App now includes an image generation example. # 11.9.0 - [feature] **Public Preview**: Added support for generating images using the diff --git a/FirebaseVertexAI/Sample/ImagenScreen/ImagenScreen.swift b/FirebaseVertexAI/Sample/ImagenScreen/ImagenScreen.swift new file mode 100644 index 00000000000..ada89c834d7 --- /dev/null +++ b/FirebaseVertexAI/Sample/ImagenScreen/ImagenScreen.swift @@ -0,0 +1,70 @@ +// 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. + +import SwiftUI + +struct ImagenScreen: View { + @StateObject var viewModel = ImagenViewModel() + + enum FocusedField: Hashable { + case message + } + + @FocusState + var focusedField: FocusedField? + + var body: some View { + VStack { + TextField("Enter a prompt to generate an image", text: $viewModel.userInput) + .focused($focusedField, equals: .message) + .textFieldStyle(.roundedBorder) + .onSubmit { + onGenerateTapped() + } + .padding() + + Button("Generate") { + onGenerateTapped() + } + .padding() + if viewModel.inProgress { + Text("Waiting for model response ...") + } + ForEach(viewModel.images, id: \.self) { + Image(uiImage: $0) + .resizable() + .scaledToFill() + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .aspectRatio(nil, contentMode: .fit) + .clipped() + } + } + .navigationTitle("Imagen sample") + .onAppear { + focusedField = .message + } + } + + private func onGenerateTapped() { + focusedField = nil + + Task { + await viewModel.generateImage(prompt: viewModel.userInput) + } + } +} + +#Preview { + ImagenScreen() +} diff --git a/FirebaseVertexAI/Sample/ImagenScreen/ImagenViewModel.swift b/FirebaseVertexAI/Sample/ImagenScreen/ImagenViewModel.swift new file mode 100644 index 00000000000..d8c8c7ca31e --- /dev/null +++ b/FirebaseVertexAI/Sample/ImagenScreen/ImagenViewModel.swift @@ -0,0 +1,83 @@ +// 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. + +import FirebaseVertexAI +import Foundation +import OSLog +import SwiftUI + +@MainActor +class ImagenViewModel: ObservableObject { + private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") + + @Published + var userInput: String = "" + + @Published + var images = [UIImage]() + + @Published + var errorMessage: String? + + @Published + var inProgress = false + + private let model: ImagenModel + + // 1. Initialize the Vertex AI service + private let vertexAI = VertexAI.vertexAI() + + init() { + // 2. Configure Imagen settings + let modelName = "imagen-3.0-generate-002" + let safetySettings = ImagenSafetySettings( + safetyFilterLevel: .blockLowAndAbove + ) + var generationConfig = ImagenGenerationConfig() + generationConfig.numberOfImages = 4 + generationConfig.aspectRatio = .landscape4x3 + + // 3. Initialize the Imagen model + model = vertexAI.imagenModel( + modelName: modelName, + generationConfig: generationConfig, + safetySettings: safetySettings + ) + } + + func generateImage(prompt: String) async { + guard !inProgress else { + print("Already generating images...") + return + } + do { + defer { + inProgress = false + } + inProgress = true + // 4. Call generateImages with the text prompt + let response = try await model.generateImages(prompt: prompt) + + // 5. Print the reason images were filtered out, if any. + if let filteredReason = response.filteredReason { + print("Image(s) Blocked: \(filteredReason)") + } + + // 6. Convert the image data to UIImage for display in the UI + images = response.images.compactMap { UIImage(data: $0.data) } + } catch { + logger.error("Error generating images: \(error)") + } + } +} diff --git a/FirebaseVertexAI/Sample/VertexAISample.xcodeproj/project.pbxproj b/FirebaseVertexAI/Sample/VertexAISample.xcodeproj/project.pbxproj index 54c2c620638..294f803319c 100644 --- a/FirebaseVertexAI/Sample/VertexAISample.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Sample/VertexAISample.xcodeproj/project.pbxproj @@ -31,6 +31,8 @@ 886F95E02B17D5010036F07A /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F562B1112F600C08E95 /* ConversationViewModel.swift */; }; 886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */; }; 886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */; }; + DEFECAA92D7B4CCD00EF9621 /* ImagenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */; }; + DEFECAAA2D7B4CCD00EF9621 /* ImagenScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -60,6 +62,8 @@ 88E10F582B11131900C08E95 /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = ""; }; 88E10F5A2B11133E00C08E95 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 88E10F5C2B11135000C08E95 /* BouncingDots.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BouncingDots.swift; sourceTree = ""; }; + DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenScreen.swift; sourceTree = ""; }; + DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagenViewModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -146,6 +150,7 @@ 8848C8262B0D04BC007B434F = { isa = PBXGroup; children = ( + DEFECAA82D7B4CCD00EF9621 /* ImagenScreen */, 88B8A9352B0FCBA700424728 /* GenerativeAIUIComponents */, 869200B22B879C4F00482873 /* GoogleService-Info.plist */, 8848C8312B0D04BC007B434F /* VertexAISample */, @@ -279,6 +284,15 @@ path = Screens; sourceTree = ""; }; + DEFECAA82D7B4CCD00EF9621 /* ImagenScreen */ = { + isa = PBXGroup; + children = ( + DEFECAA62D7B4CCD00EF9621 /* ImagenScreen.swift */, + DEFECAA72D7B4CCD00EF9621 /* ImagenViewModel.swift */, + ); + path = ImagenScreen; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -374,6 +388,8 @@ 886F95E02B17D5010036F07A /* ConversationViewModel.swift in Sources */, 886F95DD2B17D5010036F07A /* MessageView.swift in Sources */, 886F95DC2B17BAEF0036F07A /* PhotoReasoningScreen.swift in Sources */, + DEFECAA92D7B4CCD00EF9621 /* ImagenViewModel.swift in Sources */, + DEFECAAA2D7B4CCD00EF9621 /* ImagenScreen.swift in Sources */, 886F95DB2B17BAEF0036F07A /* PhotoReasoningViewModel.swift in Sources */, 886F95E12B17D5010036F07A /* ConversationScreen.swift in Sources */, 88263BF02B239C09008AB09B /* ErrorView.swift in Sources */, diff --git a/FirebaseVertexAI/Sample/VertexAISample/ContentView.swift b/FirebaseVertexAI/Sample/VertexAISample/ContentView.swift index 2841d634a6c..2c13af833cb 100644 --- a/FirebaseVertexAI/Sample/VertexAISample/ContentView.swift +++ b/FirebaseVertexAI/Sample/VertexAISample/ContentView.swift @@ -45,6 +45,11 @@ struct ContentView: View { } label: { Label("Function Calling", systemImage: "function") } + NavigationLink { + ImagenScreen() + } label: { + Label("Imagen", systemImage: "camera.circle") + } } .navigationTitle("Generative AI Samples") } From ec296952b74773e1098d0df650bce613bb2b40d5 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 10 Mar 2025 13:04:43 -0400 Subject: [PATCH 34/53] [Config] Split Codable APIs up and improve doc comments (#14552) --- FirebaseRemoteConfig/Swift/Codable.swift | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/FirebaseRemoteConfig/Swift/Codable.swift b/FirebaseRemoteConfig/Swift/Codable.swift index 1152890be83..a99aeac5b92 100644 --- a/FirebaseRemoteConfig/Swift/Codable.swift +++ b/FirebaseRemoteConfig/Swift/Codable.swift @@ -44,11 +44,23 @@ public enum RemoteConfigCodableError: Error { } public extension RemoteConfig { - /// Decodes a struct from the respective Remote Config values. + /// Decodes the given type from the respective Remote Config values. /// /// - Parameter asType: The type to decode to. + /// - Throws: An error if the decoding fails. + /// - Returns: The decoded value; otherwise, an error if one occurred. + func decoded(asType: Value.Type = Value.self) throws -> Value { + try decoded(asType: asType, decoder: FirebaseDataDecoder()) + } + + /// Decodes the given type from the respective Remote Config values. + /// - Parameters: + /// - asType: The type to decode to. + /// - decoder: The encoder to use to decode the given type. + /// - Throws: An error if the decoding fails. + /// - Returns: The decoded value; otherwise, an error if one occurred. func decoded(asType: Value.Type = Value.self, - decoder: FirebaseDataDecoder = .init()) throws -> Value { + decoder: FirebaseDataDecoder) throws -> Value { let keys = allKeys(from: RemoteConfigSource.default) + allKeys(from: RemoteConfigSource.remote) let config = keys.reduce(into: [String: FirebaseRemoteConfigValueDecoderHelper]()) { $0[$1] = FirebaseRemoteConfigValueDecoderHelper(value: configValue(forKey: $1)) @@ -59,8 +71,18 @@ public extension RemoteConfig { /// Sets config defaults from an encodable struct. /// /// - Parameter value: The object to use to set the defaults. + /// - Throws: An error if the encoding fails. + func setDefaults(from value: Value) throws { + try setDefaults(from: value, encoder: FirebaseDataEncoder()) + } + + /// Sets config defaults from an encodable struct. + /// - Parameters: + /// - value: The object to use to set the defaults. + /// - encoder: The encoder to use to encode the given object. + /// - Throws: An error if the encoding fails. func setDefaults(from value: Value, - encoder: FirebaseDataEncoder = .init()) throws { + encoder: FirebaseDataEncoder) throws { guard let encoded = try encoder.encode(value) as? [String: NSObject] else { throw RemoteConfigCodableError.invalidSetDefaultsInput( "The setDefaults input: \(value), must be a Struct that encodes to a Dictionary" From 37a1a329953c4418301ef755d4b51e9762382c1f Mon Sep 17 00:00:00 2001 From: Tushar Khandelwal <64364243+tusharkhandelwal8@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:22:43 +0530 Subject: [PATCH 35/53] Fix RCNConfigRealtime crash. (#14518) Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- FirebaseRemoteConfig/CHANGELOG.md | 1 + .../Sources/RCNConfigRealtime.m | 33 +++++++++++-------- .../Tests/Unit/RCNRemoteConfigTest.m | 25 ++++++++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md index ff7355e7862..a714287abd4 100644 --- a/FirebaseRemoteConfig/CHANGELOG.md +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -2,6 +2,7 @@ - [fixed] Codable APIs now accept optional `FirebaseDataEncoder` and `FirebaseDataDecoder` parameters, allowing for customization of encoding/decoding behavior. (#14368) +- [fixed] Fix intermittent `RCNConfigRealtime` crash due to incorrect parsing of fragmented JSON. (#14518) # 11.9.0 - [fixed] Mark internal `fetchSession` property as `atomic` to prevent a concurrency diff --git a/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m b/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m index f7f5d1e44a1..ddc6f21ccec 100644 --- a/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m +++ b/FirebaseRemoteConfig/Sources/RCNConfigRealtime.m @@ -573,20 +573,27 @@ - (void)URLSession:(NSURLSession *)session return; } - NSRange endRange = [strData rangeOfString:@"}"]; NSRange beginRange = [strData rangeOfString:@"{"]; - if (beginRange.location != NSNotFound && endRange.location != NSNotFound) { - FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000015", - @"Received config update message on stream."); - NSRange msgRange = - NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1); - strData = [strData substringWithRange:msgRange]; - data = [strData dataUsingEncoding:NSUTF8StringEncoding]; - NSDictionary *response = [NSJSONSerialization JSONObjectWithData:data - options:NSJSONReadingMutableContainers - error:&dataError]; - - [self evaluateStreamResponse:response error:dataError]; + if (beginRange.location != NSNotFound) { + NSRange endRange = + [strData rangeOfString:@"}" + options:0 + range:NSMakeRange(beginRange.location + 1, + strData.length - beginRange.location - 1)]; + if (endRange.location != NSNotFound) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000015", + @"Received config update message on stream."); + NSRange msgRange = + NSMakeRange(beginRange.location, endRange.location - beginRange.location + 1); + strData = [strData substringWithRange:msgRange]; + data = [strData dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *response = + [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&dataError]; + + [self evaluateStreamResponse:response error:dataError]; + } } } diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m index 6c96c1a7dbe..1021d7014d1 100644 --- a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -1834,6 +1834,31 @@ - (void)testFetchAndActivateRolloutsNotifyInterop { [self waitForExpectations:@[ notificationExpectation ] timeout:_expectationTimeout]; } +- (void)testURLSessionDelegateHandlesChunkedJSON { + NSString *testString = @"} {\"testKey\":\"testValue\"}"; + NSData *testData = [testString dataUsingEncoding:NSUTF8StringEncoding]; + + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString + stringWithFormat:@"Test delegate method handling chunked JSON - instance %d", i]]; + + NSURLSession *networkSession = [_configFetch[i] currentNetworkSession]; + NSURLSessionDataTask *dataTask = [_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any] + fetchTypeHeader:[OCMArg any] + completionHandler:nil]; + + XCTAssertNoThrow([_configRealtime[i] URLSession:networkSession + dataTask:dataTask + didReceiveData:testData]); + [expectations[i] fulfill]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + - (void)testSetCustomSignals { NSMutableArray *expectations = [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; From baf60edf13c8c9af8f50035b4ef03c3fa35cbf91 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:37:52 -0400 Subject: [PATCH 36/53] [Functions] Add support for streamable cloud functions (#14395) Co-authored-by: Eblen Macari Co-authored-by: Eblen M Co-authored-by: Rodrigo Lazo --- .github/workflows/functions.yml | 13 +- FirebaseFunctions/Backend/index.js | 92 +++- FirebaseFunctions/Backend/start.sh | 5 + FirebaseFunctions/CHANGELOG.md | 3 + .../Sources/Callable+Codable.swift | 178 +++++++- FirebaseFunctions/Sources/Functions.swift | 195 ++++++++ FirebaseFunctions/Sources/HTTPSCallable.swift | 5 + .../Tests/Integration/IntegrationTests.swift | 422 ++++++++++++++++++ 8 files changed, 896 insertions(+), 17 deletions(-) diff --git a/.github/workflows/functions.yml b/.github/workflows/functions.yml index e89bccc8ff5..4da67e2c200 100644 --- a/.github/workflows/functions.yml +++ b/.github/workflows/functions.yml @@ -31,8 +31,6 @@ jobs: matrix: target: [ios, tvos, macos, watchos] build-env: - - os: macos-14 - xcode: Xcode_15.2 - os: macos-15 xcode: Xcode_16.2 runs-on: ${{ matrix.build-env.os }} @@ -43,14 +41,12 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.build-env.xcode }}.app/Contents/Developer - name: Setup Bundler run: scripts/setup_bundler.sh - # The integration tests are flaky on Xcode 15 so only run the unit tests. The integration tests still run with SPM. - # - name: Integration Test Server - # run: FirebaseFunctions/Backend/start.sh synchronous + - name: Integration Test Server + run: FirebaseFunctions/Backend/start.sh synchronous - name: Build and test run: | scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseFunctions.podspec \ - --test-specs=unit --platforms=${{ matrix.target }} - + --platforms=${{ matrix.target }} spm-package-resolved: runs-on: macos-14 @@ -145,6 +141,9 @@ jobs: key: ${{needs.spm-package-resolved.outputs.cache_key}} - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install visionOS, if needed. + if: matrix.target == 'visionOS' + run: xcodebuild -downloadPlatform visionOS - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - name: Unit Tests diff --git a/FirebaseFunctions/Backend/index.js b/FirebaseFunctions/Backend/index.js index 3bfd6f31328..bebcc0f421b 100644 --- a/FirebaseFunctions/Backend/index.js +++ b/FirebaseFunctions/Backend/index.js @@ -16,6 +16,14 @@ const assert = require('assert'); const functionsV1 = require('firebase-functions/v1'); const functionsV2 = require('firebase-functions/v2'); +// MARK: - Utilities + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +// MARK: - Callable Functions + exports.dataTest = functionsV1.https.onRequest((request, response) => { assert.deepEqual(request.body, { data: { @@ -121,14 +129,10 @@ exports.timeoutTest = functionsV1.https.onRequest((request, response) => { const streamData = ["hello", "world", "this", "is", "cool"] -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -}; - async function* generateText() { for (const chunk of streamData) { yield chunk; - await sleep(1000); + await sleep(100); } }; @@ -136,7 +140,7 @@ exports.genStream = functionsV2.https.onCall( async (request, response) => { if (request.acceptsStreaming) { for await (const chunk of generateText()) { - response.sendChunk({ chunk }); + response.sendChunk(chunk); } } return streamData.join(" "); @@ -145,11 +149,81 @@ exports.genStream = functionsV2.https.onCall( exports.genStreamError = functionsV2.https.onCall( async (request, response) => { + // Note: The functions backend does not pass the error message to the + // client at this time. + throw Error("BOOM") + } +); + +const weatherForecasts = { + Toronto: { conditions: 'snowy', temperature: 25 }, + London: { conditions: 'rainy', temperature: 50 }, + Dubai: { conditions: 'sunny', temperature: 75 } +}; + +async function* generateForecast(locations) { + for (const location of locations) { + yield { 'location': location, ...weatherForecasts[location.name] }; + await sleep(100); + } +}; + +exports.genStreamWeather = functionsV2.https.onCall( + async (request, response) => { + const forecasts = []; if (request.acceptsStreaming) { - for await (const chunk of generateText()) { - response.write({ chunk }); + for await (const chunk of generateForecast(request.data)) { + forecasts.push(chunk) + response.sendChunk(chunk); + } + } + return { forecasts }; + } +); + +exports.genStreamWeatherError = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + for await (const chunk of generateForecast(request.data)) { + // Remove the location field, since the SDK cannot decode the message + // if it's there. + delete chunk.location; + response.sendChunk(chunk); + } + } + return "Number of forecasts generated: " + request.data.length; + } +); + +exports.genStreamEmpty = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Send no chunks + } + // Implicitly return null. + } +); + +exports.genStreamResultOnly = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + // Do not send any chunks. + } + return "Only a result"; + } +); + +exports.genStreamLargeData = functionsV2.https.onCall( + async (request, response) => { + if (request.acceptsStreaming) { + const largeString = 'A'.repeat(10000); + const chunkSize = 1024; + for (let i = 0; i < largeString.length; i += chunkSize) { + const chunk = largeString.substring(i, i + chunkSize); + response.sendChunk(chunk); + await sleep(100); } - throw Error("BOOM") } + return "Stream Completed"; } ); diff --git a/FirebaseFunctions/Backend/start.sh b/FirebaseFunctions/Backend/start.sh index 1ee3777cdc8..8afecf1c387 100755 --- a/FirebaseFunctions/Backend/start.sh +++ b/FirebaseFunctions/Backend/start.sh @@ -57,6 +57,11 @@ FUNCTIONS_BIN="./node_modules/.bin/functions" "${FUNCTIONS_BIN}" deploy timeoutTest --trigger-http "${FUNCTIONS_BIN}" deploy genStream --trigger-http "${FUNCTIONS_BIN}" deploy genStreamError --trigger-http +"${FUNCTIONS_BIN}" deploy genStreamWeather --trigger-http +"${FUNCTIONS_BIN}" deploy genStreamWeatherError --trigger-http +"${FUNCTIONS_BIN}" deploy genStreamEmpty --trigger-http +"${FUNCTIONS_BIN}" deploy genStreamResultOnly --trigger-http +"${FUNCTIONS_BIN}" deploy genStreamLargeData --trigger-http if [ "$1" != "synchronous" ]; then # Wait for the user to tell us to stop the server. diff --git a/FirebaseFunctions/CHANGELOG.md b/FirebaseFunctions/CHANGELOG.md index 89a663ec718..c0354c5ecea 100644 --- a/FirebaseFunctions/CHANGELOG.md +++ b/FirebaseFunctions/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Streaming callable functions are now supported. + # 11.9.0 - [fixed] Fixed App Check token reporting to enable differentiating outdated (`MISSING`) and inauthentic (`INVALID`) clients; see [Monitor App Check diff --git a/FirebaseFunctions/Sources/Callable+Codable.swift b/FirebaseFunctions/Sources/Callable+Codable.swift index 489433a0a7e..287eff55ebb 100644 --- a/FirebaseFunctions/Sources/Callable+Codable.swift +++ b/FirebaseFunctions/Sources/Callable+Codable.swift @@ -15,7 +15,11 @@ import FirebaseSharedSwift import Foundation -/// A `Callable` is reference to a particular Callable HTTPS trigger in Cloud Functions. +/// A `Callable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. +/// +/// - Note: If the Callable HTTPS trigger accepts no parameters, ``Never`` can be used for +/// iOS 17.0+. Otherwise, a simple encodable placeholder type (e.g., +/// `struct EmptyRequest: Encodable {}`) can be used. public struct Callable { /// The timeout to use when calling the function. Defaults to 70 seconds. public var timeoutInterval: TimeInterval { @@ -160,3 +164,175 @@ public struct Callable { return try await call(data) } } + +/// Used to determine when a `StreamResponse<_, _>` is being decoded. +private protocol StreamResponseProtocol {} + +/// A convenience type used to receive both the streaming callable function's yielded messages and +/// its return value. +/// +/// This can be used as the generic `Response` parameter to ``Callable`` to receive both the +/// yielded messages and final return value of the streaming callable function. +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +public enum StreamResponse: Decodable, + StreamResponseProtocol { + /// The message yielded by the callable function. + case message(Message) + /// The final result returned by the callable function. + case result(Result) + + private enum CodingKeys: String, CodingKey { + case message + case result + } + + public init(from decoder: any Decoder) throws { + do { + let container = try decoder + .container(keyedBy: Self.CodingKeys.self) + guard let onlyKey = container.allKeys.first, container.allKeys.count == 1 else { + throw DecodingError + .typeMismatch( + Self.self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.", + underlyingError: nil + ) + ) + } + + switch onlyKey { + case .message: + self = try Self + .message(container.decode(Message.self, forKey: .message)) + case .result: + self = try Self + .result(container.decode(Result.self, forKey: .result)) + } + } catch { + throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error]) + } + } +} + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +public extension Callable where Request: Sendable, Response: Sendable { + /// Creates a stream that yields responses from the streaming callable function. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a FCM + /// token to identify the app instance. If a user is logged in with Firebase Auth, an auth ID + /// token for the user is included. If App Check is integrated, an app check token is included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Important: The final result returned by the callable function is only accessible when + /// using `StreamResponse` as the `Response` generic type. + /// + /// Example of using `stream` _without_ `StreamResponse`: + /// ```swift + /// let callable: Callable = // ... + /// let request: MyRequest = // ... + /// let stream = try callable.stream(request) + /// for try await response in stream { + /// // Process each `MyResponse` message + /// print(response) + /// } + /// ``` + /// + /// Example of using `stream` _with_ `StreamResponse`: + /// ```swift + /// let callable: Callable> = // ... + /// let request: MyRequest = // ... + /// let stream = try callable.stream(request) + /// for try await response in stream { + /// switch response { + /// case .message(let message): + /// // Process each `MyMessage` + /// print(message) + /// case .result(let result): + /// // Process the final `MyResult` + /// print(result) + /// } + /// } + /// ``` + /// + /// - Parameter data: The `Request` data to pass to the callable function. + /// - Throws: A ``FunctionsError`` if the parameter `data` cannot be encoded. + /// - Returns: A stream wrapping responses yielded by the streaming callable function or + /// a ``FunctionsError`` if an error occurred. + func stream(_ data: Request? = nil) throws -> AsyncThrowingStream { + let encoded: Any + do { + encoded = try encoder.encode(data) + } catch { + throw FunctionsError(.invalidArgument, userInfo: [NSUnderlyingErrorKey: error]) + } + + return AsyncThrowingStream { continuation in + Task { + do { + for try await response in callable.stream(encoded) { + do { + // This response JSON should only be able to be decoded to an `StreamResponse<_, _>` + // instance. If the decoding succeeds and the decoded response conforms to + // `StreamResponseProtocol`, we know the `Response` generic argument + // is `StreamResponse<_, _>`. + let responseJSON = switch response { + case .message(let json), .result(let json): json + } + let response = try decoder.decode(Response.self, from: responseJSON) + if response is StreamResponseProtocol { + continuation.yield(response) + } else { + // `Response` is a custom type that matched the decoding logic as the + // `StreamResponse<_, _>` type. Only the `StreamResponse<_, _>` type should decode + // successfully here to avoid exposing the `result` value in a custom type. + throw FunctionsError(.internal) + } + } catch let error as FunctionsError where error.code == .dataLoss { + // `Response` is of type `StreamResponse<_, _>`, but failed to decode. Rethrow. + throw error + } catch { + // `Response` is *not* of type `StreamResponse<_, _>`, and needs to be unboxed and + // decoded. + guard case let .message(messageJSON) = response else { + // Since `Response` is not a `StreamResponse<_, _>`, only messages should be + // decoded. + continue + } + + do { + let boxedMessage = try decoder.decode( + StreamResponseMessage.self, + from: messageJSON + ) + continuation.yield(boxedMessage.message) + } catch { + throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error]) + } + } + } + } catch { + continuation.finish(throwing: error) + } + continuation.finish() + } + } + } + + /// A container type for the type-safe decoding of the message object from the generic `Response` + /// type. + private struct StreamResponseMessage: Decodable { + let message: Response + } +} + +/// A container type for differentiating between message and result responses. +enum JSONStreamResponse { + case message([String: Any]) + case result([String: Any]) +} diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index ce189579c87..d9e00afb34a 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -471,6 +471,201 @@ enum FunctionsConstants { } } + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func stream(at url: URL, + data: Any?, + options: HTTPSCallableOptions?, + timeout: TimeInterval) + -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + let urlRequest: URLRequest + do { + let context = try await contextProvider.context(options: options) + urlRequest = try makeRequestForStreamableContent( + url: url, + data: data, + options: options, + timeout: timeout, + context: context + ) + } catch { + continuation.finish(throwing: FunctionsError( + .invalidArgument, + userInfo: [NSUnderlyingErrorKey: error] + )) + return + } + + let stream: URLSession.AsyncBytes + let rawResponse: URLResponse + do { + (stream, rawResponse) = try await URLSession.shared.bytes(for: urlRequest) + } catch { + continuation.finish(throwing: FunctionsError( + .unavailable, + userInfo: [NSUnderlyingErrorKey: error] + )) + return + } + + // Verify the status code is an HTTP response. + guard let response = rawResponse as? HTTPURLResponse else { + continuation.finish( + throwing: FunctionsError( + .unavailable, + userInfo: [NSLocalizedDescriptionKey: "Response was not an HTTP response."] + ) + ) + return + } + + // Verify the status code is a 200. + guard response.statusCode == 200 else { + continuation.finish( + throwing: FunctionsError( + httpStatusCode: response.statusCode, + region: region, + url: url, + body: nil, + serializer: serializer + ) + ) + return + } + + do { + for try await line in stream.lines { + guard line.hasPrefix("data:") else { + continuation.finish( + throwing: FunctionsError( + .dataLoss, + userInfo: [NSLocalizedDescriptionKey: "Unexpected format for streamed response."] + ) + ) + return + } + + do { + // We can assume 5 characters since it's utf-8 encoded, removing `data:`. + let jsonText = String(line.dropFirst(5)) + let data = try jsonData(jsonText: jsonText) + // Handle the content and parse it. + let content = try callableStreamResult(fromResponseData: data, endpointURL: url) + continuation.yield(content) + } catch { + continuation.finish(throwing: error) + return + } + } + } catch { + continuation.finish( + throwing: FunctionsError( + .dataLoss, + userInfo: [ + NSLocalizedDescriptionKey: "Unexpected format for streamed response.", + NSUnderlyingErrorKey: error, + ] + ) + ) + return + } + + continuation.finish() + } + } + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + private func callableStreamResult(fromResponseData data: Data, + endpointURL url: URL) throws -> JSONStreamResponse { + let data = try processedData(fromResponseData: data, endpointURL: url) + + let responseJSONObject: Any + do { + responseJSONObject = try JSONSerialization.jsonObject(with: data) + } catch { + throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error]) + } + + guard let responseJSON = responseJSONObject as? [String: Any] else { + let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."] + throw FunctionsError(.dataLoss, userInfo: userInfo) + } + + if let _ = responseJSON["result"] { + return .result(responseJSON) + } else if let _ = responseJSON["message"] { + return .message(responseJSON) + } else { + throw FunctionsError( + .dataLoss, + userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."] + ) + } + } + + private func jsonData(jsonText: String) throws -> Data { + guard let data = jsonText.data(using: .utf8) else { + throw FunctionsError(.dataLoss, userInfo: [ + NSUnderlyingErrorKey: DecodingError.dataCorrupted(DecodingError.Context( + codingPath: [], + debugDescription: "Could not parse response as UTF8." + )), + ]) + } + return data + } + + private func makeRequestForStreamableContent(url: URL, + data: Any?, + options: HTTPSCallableOptions?, + timeout: TimeInterval, + context: FunctionsContext) throws + -> URLRequest { + var urlRequest = URLRequest( + url: url, + cachePolicy: .useProtocolCachePolicy, + timeoutInterval: timeout + ) + + let data = data ?? NSNull() + let encoded = try serializer.encode(data) + let body = ["data": encoded] + let payload = try JSONSerialization.data(withJSONObject: body, options: [.fragmentsAllowed]) + urlRequest.httpBody = payload + + // Set the headers for starting a streaming session. + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + urlRequest.setValue("text/event-stream", forHTTPHeaderField: "Accept") + urlRequest.httpMethod = "POST" + + if let authToken = context.authToken { + let value = "Bearer \(authToken)" + urlRequest.setValue(value, forHTTPHeaderField: "Authorization") + } + + if let fcmToken = context.fcmToken { + urlRequest.setValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader) + } + + if options?.requireLimitedUseAppCheckTokens == true { + if let appCheckToken = context.limitedUseAppCheckToken { + urlRequest.setValue( + appCheckToken, + forHTTPHeaderField: Constants.appCheckTokenHeader + ) + } + } else if let appCheckToken = context.appCheckToken { + urlRequest.setValue( + appCheckToken, + forHTTPHeaderField: Constants.appCheckTokenHeader + ) + } + + return urlRequest + } + private func makeFetcher(url: URL, data: Any?, options: HTTPSCallableOptions?, diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index c2281e54866..b423ac4195a 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -143,4 +143,9 @@ open class HTTPSCallable: NSObject { try await functions .callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval) } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func stream(_ data: Any? = nil) -> AsyncThrowingStream { + functions.stream(at: url, data: data, options: options, timeout: timeoutInterval) + } } diff --git a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift index 5260bd10b2b..878cec1c9a0 100644 --- a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift +++ b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift @@ -65,6 +65,7 @@ struct DataTestResponse: Decodable, Equatable { var code: Int32 } +/// - Important: These tests require the emulator. Run `./FirebaseFunctions/Backend/start.sh` class IntegrationTests: XCTestCase { let functions = Functions(projectID: "functions-integration-test", region: "us-central1", @@ -868,6 +869,427 @@ class IntegrationTests: XCTestCase { } } +// MARK: - Streaming + +/// A convenience type used to represent that a callable function does not +/// accept parameters. +/// +/// This can be used as the generic `Request` parameter to ``Callable`` to +/// indicate the callable function does not accept parameters. +private struct EmptyRequest: Encodable {} + +@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) +extension IntegrationTests { + func testStream_NoArgs() async throws { + // 1. Custom `EmptyRequest` struct is passed as a placeholder generic arg. + let callable: Callable = functions.httpsCallable("genStream") + // 2. No request data is passed when creating stream. + let stream = try callable.stream() + var streamContents: [String] = [] + for try await response in stream { + streamContents.append(response) + } + XCTAssertEqual( + streamContents, + ["hello", "world", "this", "is", "cool"] + ) + } + + @available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *) + func testStream_NoArgs_UeeNever() async throws { + let callable: Callable = functions.httpsCallable("genStream") + let stream = try callable.stream() + var streamContents: [String] = [] + for try await response in stream { + streamContents.append(response) + } + XCTAssertEqual( + streamContents, + ["hello", "world", "this", "is", "cool"] + ) + } + + func testStream_SimpleStreamResponse() async throws { + let callable: Callable> = functions + .httpsCallable("genStream") + let stream = try callable.stream() + var streamContents: [String] = [] + for try await response in stream { + switch response { + case let .message(message): + streamContents.append(message) + case let .result(result): + streamContents.append(result) + } + } + XCTAssertEqual( + streamContents, + ["hello", "world", "this", "is", "cool", "hello world this is cool"] + ) + } + + func testStream_CodableString() async throws { + let byName: Callable = functions.httpsCallable("genStream") + let stream = try byName.stream() + let result: [String] = try await stream.reduce([]) { $0 + [$1] } + XCTAssertEqual(result, ["hello", "world", "this", "is", "cool"]) + } + + private struct Location: Codable, Equatable { + let name: String + } + + private struct WeatherForecast: Decodable, Equatable { + enum Conditions: String, Decodable { + case sunny + case rainy + case snowy + } + + let location: Location + let temperature: Int + let conditions: Conditions + } + + private struct WeatherForecastReport: Decodable, Equatable { + let forecasts: [WeatherForecast] + } + + func testStream_CodableObject() async throws { + let callable: Callable<[Location], WeatherForecast> = functions + .httpsCallable("genStreamWeather") + let stream = try callable.stream([ + Location(name: "Toronto"), + Location(name: "London"), + Location(name: "Dubai"), + ]) + let result: [WeatherForecast] = try await stream.reduce([]) { $0 + [$1] } + XCTAssertEqual( + result, + [ + WeatherForecast(location: Location(name: "Toronto"), temperature: 25, conditions: .snowy), + WeatherForecast(location: Location(name: "London"), temperature: 50, conditions: .rainy), + WeatherForecast(location: Location(name: "Dubai"), temperature: 75, conditions: .sunny), + ] + ) + } + + func testStream_ResponseMessageDecodingFailure() async throws { + let callable: Callable<[Location], StreamResponse> = + functions + .httpsCallable("genStreamWeatherError") + let stream = try callable.stream([Location(name: "Toronto")]) + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .dataLoss { + XCTAssertNotNil(error.errorUserInfo[NSUnderlyingErrorKey] as? DecodingError) + } + } + + func testStream_ResponseResultDecodingFailure() async throws { + let callable: Callable<[Location], StreamResponse> = functions + .httpsCallable("genStreamWeather") + let stream = try callable.stream([Location(name: "Toronto")]) + do { + for try await response in stream { + if case .result = response { + XCTFail("Expected error to be thrown from stream.") + } + } + } catch let error as FunctionsError where error.code == .dataLoss { + XCTAssertNotNil(error.errorUserInfo[NSUnderlyingErrorKey] as? DecodingError) + } + } + + func testStream_ComplexStreamResponse() async throws { + let callable: Callable<[Location], StreamResponse> = + functions + .httpsCallable("genStreamWeather") + let stream = try callable.stream([ + Location(name: "Toronto"), + Location(name: "London"), + Location(name: "Dubai"), + ]) + var streamContents: [WeatherForecast] = [] + var streamResult: WeatherForecastReport? + for try await response in stream { + switch response { + case let .message(message): + streamContents.append(message) + case let .result(result): + streamResult = result + } + } + XCTAssertEqual( + streamContents, + [ + WeatherForecast(location: Location(name: "Toronto"), temperature: 25, conditions: .snowy), + WeatherForecast(location: Location(name: "London"), temperature: 50, conditions: .rainy), + WeatherForecast(location: Location(name: "Dubai"), temperature: 75, conditions: .sunny), + ] + ) + + try XCTAssertEqual( + XCTUnwrap(streamResult), WeatherForecastReport(forecasts: streamContents) + ) + } + + func testStream_ComplexStreamResponse_Functional() async throws { + let callable: Callable<[Location], StreamResponse> = + functions + .httpsCallable("genStreamWeather") + let stream = try callable.stream([ + Location(name: "Toronto"), + Location(name: "London"), + Location(name: "Dubai"), + ]) + let result: (accumulatedMessages: [WeatherForecast], result: WeatherForecastReport?) = + try await stream.reduce(([], nil)) { partialResult, streamResponse in + switch streamResponse { + case let .message(message): + (partialResult.accumulatedMessages + [message], partialResult.result) + case let .result(result): + (partialResult.accumulatedMessages, result) + } + } + XCTAssertEqual( + result.accumulatedMessages, + [ + WeatherForecast(location: Location(name: "Toronto"), temperature: 25, conditions: .snowy), + WeatherForecast(location: Location(name: "London"), temperature: 50, conditions: .rainy), + WeatherForecast(location: Location(name: "Dubai"), temperature: 75, conditions: .sunny), + ] + ) + + try XCTAssertEqual( + XCTUnwrap(result.result), WeatherForecastReport(forecasts: result.accumulatedMessages) + ) + } + + func testStream_Canceled() async throws { + let task = Task.detached { [self] in + let callable: Callable = functions.httpsCallable("genStream") + let stream = try callable.stream() + // Since we cancel the call we are expecting an empty array. + return try await stream.reduce([]) { $0 + [$1] } as [String] + } + // We cancel the task and we expect a null response even if the stream was initiated. + task.cancel() + let respone = try await task.value + XCTAssertEqual(respone, []) + } + + func testStream_NonexistentFunction() async throws { + let callable: Callable = functions.httpsCallable( + "nonexistentFunction" + ) + let stream = try callable.stream() + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .notFound { + XCTAssertEqual(error.localizedDescription, "NOT FOUND") + } + } + + func testStream_StreamError() async throws { + let callable: Callable = functions.httpsCallable("genStreamError") + let stream = try callable.stream() + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .internal { + XCTAssertEqual(error.localizedDescription, "INTERNAL") + } + } + + func testStream_RequestEncodingFailure() async throws { + struct Foo: Encodable { + enum CodingKeys: CodingKey {} + + func encode(to encoder: any Encoder) throws { + throw EncodingError + .invalidValue("", EncodingError.Context(codingPath: [], debugDescription: "")) + } + } + let callable: Callable = functions + .httpsCallable("genStream") + do { + _ = try callable.stream(Foo()) + } catch let error as FunctionsError where error.code == .invalidArgument { + _ = try XCTUnwrap(error.errorUserInfo[NSUnderlyingErrorKey] as? EncodingError) + } + } + + /// This tests an edge case to assert that if a custom `Response` is used + /// that matches the decoding logic of `StreamResponse`, the custom + /// `Response` does not decode successfully. + func testStream_ResultIsOnlyExposedInStreamResponse() async throws { + // The implementation is copied from `StreamResponse`. The only difference is the do-catch is + // removed from the decoding initializer. + enum MyStreamResponse: Decodable { + /// The message yielded by the callable function. + case message(Message) + /// The final result returned by the callable function. + case result(Result) + + private enum CodingKeys: String, CodingKey { + case message + case result + } + + public init(from decoder: any Decoder) throws { + let container = try decoder + .container(keyedBy: Self.CodingKeys.self) + var allKeys = ArraySlice(container.allKeys) + guard let onlyKey = allKeys.popFirst(), allKeys.isEmpty else { + throw DecodingError + .typeMismatch( + Self.self, + DecodingError.Context( + codingPath: container.codingPath, + debugDescription: "Invalid number of keys found, expected one.", + underlyingError: nil + ) + ) + } + + switch onlyKey { + case .message: + self = try Self + .message(container.decode(Message.self, forKey: .message)) + case .result: + self = try Self + .result(container.decode(Result.self, forKey: .result)) + } + } + } + + let callable: Callable<[Location], MyStreamResponse> = + functions + .httpsCallable("genStreamWeather") + let stream = try callable.stream([Location(name: "Toronto")]) + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .dataLoss { + XCTAssertNotNil(error.errorUserInfo[NSUnderlyingErrorKey] as? DecodingError) + } + } + + func testStream_ForNonStreamingCF3() async throws { + let callable: Callable = functions.httpsCallable("scalarTest") + let stream = try callable.stream(17) + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .dataLoss { + XCTAssertEqual(error.localizedDescription, "Unexpected format for streamed response.") + } + } + + func testStream_EmptyStream() async throws { + let callable: Callable = functions.httpsCallable("genStreamEmpty") + var streamContents: [String] = [] + for try await response in try callable.stream() { + streamContents.append(response) + } + XCTAssertEqual(streamContents, []) + } + + func testStream_ResultOnly() async throws { + let callable: Callable = functions.httpsCallable("genStreamResultOnly") + let stream = try callable.stream() + for try await _ in stream { + // The stream should not yield anything, so this should not be reached. + XCTFail("Stream should not yield any messages") + } + // Because StreamResponse was not used, the result is not accessible, + // but the message should not throw. + } + + func testStream_ResultOnly_StreamResponse() async throws { + struct EmptyResponse: Decodable {} + let callable: Callable> = functions + .httpsCallable( + "genStreamResultOnly" + ) + let stream = try callable.stream() + var streamResult = "" + for try await response in stream { + switch response { + case .message: + XCTFail("Stream should not yield any messages") + case let .result(result): + streamResult = result + } + } + // The hardcoded string matches the CF3's return value. + XCTAssertEqual(streamResult, "Only a result") + } + + func testStream_UnexpectedType() async throws { + // This function yields strings, not integers. + let callable: Callable = functions.httpsCallable("genStream") + let stream = try callable.stream() + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .dataLoss { + XCTAssertNotNil(error.errorUserInfo[NSUnderlyingErrorKey] as? DecodingError) + } + } + + func testStream_Timeout() async throws { + var callable: Callable = functions.httpsCallable("timeoutTest") + // Set a short timeout + callable.timeoutInterval = 0.01 // 10 milliseconds + + let stream = try callable.stream() + + do { + for try await _ in stream { + XCTFail("Expected error to be thrown from stream.") + } + } catch let error as FunctionsError where error.code == .unavailable { + // This should be a timeout error. + XCTAssertEqual( + error.localizedDescription, + "The operation couldn’t be completed. (com.firebase.functions error 14.)" + ) + XCTAssertNotNil(error.errorUserInfo[NSUnderlyingErrorKey] as? URLError) + } + } + + func testStream_LargeData() async throws { + func generateLargeString() -> String { + var largeString = "" + for _ in 0 ..< 10000 { + largeString += "A" + } + return largeString + } + let callable: Callable = functions.httpsCallable("genStreamLargeData") + let stream = try callable.stream() + var concatenatedData = "" + for try await response in stream { + concatenatedData += response + } + // Assert that the concatenated data matches the expected large data. + XCTAssertEqual(concatenatedData, generateLargeString()) + } +} + +// MARK: - Helpers + private class AuthTokenProvider: AuthInterop { func getUserID() -> String? { return "fake user" From 94bac424e8e7c2ecfeb88690b224851466b53b75 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 10 Mar 2025 15:17:31 -0400 Subject: [PATCH 37/53] [Vertex AI] Add enum integration test and run on Developer API v1 (#14547) --- .github/workflows/vertexai.yml | 3 + .../Sources/Public/FirebaseCore/FIROptions.h | 4 +- .../Tests/TestApp/Sources/Constants.swift | 25 ++++ .../Tests/TestApp/Sources/TestApp.swift | 12 ++ .../Sources/TestAppCheckProviderFactory.swift | 12 +- .../GenerateContentIntegrationTests.swift | 107 +++++++++++------- .../Tests/Integration/IntegrationTests.swift | 4 +- .../IntegrationTestUtils.swift | 6 + .../Tests/Utilities/VertexAITestUtils.swift | 59 ++++++++++ .../VertexAITestApp.xcodeproj/project.pbxproj | 14 ++- ...TestApp-GoogleService-Info-Spark.plist.gpg | Bin 0 -> 543 bytes 11 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 FirebaseVertexAI/Tests/TestApp/Sources/Constants.swift rename FirebaseVertexAI/Tests/TestApp/Tests/{Integration => Utilities}/IntegrationTestUtils.swift (88%) create mode 100644 FirebaseVertexAI/Tests/TestApp/Tests/Utilities/VertexAITestUtils.swift create mode 100644 scripts/gha-encrypted/VertexAI/TestApp-GoogleService-Info-Spark.plist.gpg diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index 18afa033320..926fad3a4e2 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -118,6 +118,9 @@ jobs: - name: Install Secret GoogleService-Info.plist run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/VertexAI/TestApp-GoogleService-Info.plist.gpg \ FirebaseVertexAI/Tests/TestApp/Resources/GoogleService-Info.plist "$secrets_passphrase" + - name: Install Secret GoogleService-Info-Spark.plist + run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/VertexAI/TestApp-GoogleService-Info-Spark.plist.gpg \ + FirebaseVertexAI/Tests/TestApp/Resources/GoogleService-Info-Spark.plist "$secrets_passphrase" - name: Install Secret Credentials.swift run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/VertexAI/TestApp-Credentials.swift.gpg \ FirebaseVertexAI/Tests/TestApp/Tests/Integration/Credentials.swift "$secrets_passphrase" diff --git a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h index 8f8d945d765..14e60fcde33 100644 --- a/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h +++ b/FirebaseCore/Sources/Public/FirebaseCore/FIROptions.h @@ -102,12 +102,12 @@ NS_SWIFT_NAME(FirebaseOptions) * This will read the file synchronously from disk. * For example: * ```swift - * if let path = Bundle.main.path(forResource:"GoogleServices-Info", ofType:"plist") { + * if let path = Bundle.main.path(forResource:"GoogleService-Info", ofType:"plist") { * let options = FirebaseOptions(contentsOfFile: path) * } * ``` * Note that it is not possible to customize `FirebaseOptions` for Firebase Analytics which expects - * a static file named `GoogleServices-Info.plist` - + * a static file named `GoogleService-Info.plist` - * https://github.com/firebase/firebase-ios-sdk/issues/230. * Returns `nil` if the plist file does not exist or is invalid. */ diff --git a/FirebaseVertexAI/Tests/TestApp/Sources/Constants.swift b/FirebaseVertexAI/Tests/TestApp/Sources/Constants.swift new file mode 100644 index 00000000000..7e2f450597a --- /dev/null +++ b/FirebaseVertexAI/Tests/TestApp/Sources/Constants.swift @@ -0,0 +1,25 @@ +// 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. + +public enum FirebaseAppNames { + /// The name, or a substring of the name, of Firebase apps where App Check is not configured. + public static let appCheckNotConfigured = "app-check-not-configured" + + /// The name of a Firebase app with no billing account (i.e., the "Spark" plan). + public static let spark = "spark" +} + +public enum ModelNames { + public static let gemini2FlashLite = "gemini-2.0-flash-lite-001" +} diff --git a/FirebaseVertexAI/Tests/TestApp/Sources/TestApp.swift b/FirebaseVertexAI/Tests/TestApp/Sources/TestApp.swift index 737072bccb6..b466503417d 100644 --- a/FirebaseVertexAI/Tests/TestApp/Sources/TestApp.swift +++ b/FirebaseVertexAI/Tests/TestApp/Sources/TestApp.swift @@ -20,7 +20,19 @@ import SwiftUI struct TestApp: App { init() { AppCheck.setAppCheckProviderFactory(TestAppCheckProviderFactory()) + + // Configure default Firebase App FirebaseApp.configure() + + // Configure a Firebase App without a billing account (i.e., the "Spark" plan). + guard let plistPath = + Bundle.main.path(forResource: "GoogleService-Info-Spark", ofType: "plist") else { + fatalError("The file 'GoogleService-Info-Spark.plist' was not found.") + } + guard let options = FirebaseOptions(contentsOfFile: plistPath) else { + fatalError("Failed to parse options from 'GoogleService-Info-Spark.plist'.") + } + FirebaseApp.configure(name: FirebaseAppNames.spark, options: options) } var body: some Scene { diff --git a/FirebaseVertexAI/Tests/TestApp/Sources/TestAppCheckProviderFactory.swift b/FirebaseVertexAI/Tests/TestApp/Sources/TestAppCheckProviderFactory.swift index ec7a08ceafc..03212293549 100644 --- a/FirebaseVertexAI/Tests/TestApp/Sources/TestAppCheckProviderFactory.swift +++ b/FirebaseVertexAI/Tests/TestApp/Sources/TestAppCheckProviderFactory.swift @@ -19,15 +19,13 @@ import Foundation /// An `AppCheckProviderFactory` for the Test App. /// /// Defaults to the `AppCheckDebugProvider` unless the `FirebaseApp` `name` contains -/// ``notConfiguredName``, in which case App Check is not configured; this facilitates integration -/// testing of App Check failure cases. +/// ``FirebaseAppNames/appCheckNotConfigured``, in which case App Check is not configured; this +/// facilitates integration testing of App Check failure cases. public class TestAppCheckProviderFactory: NSObject, AppCheckProviderFactory { - /// The name, or a substring of the name, of Firebase apps where App Check is not configured. - public static let notConfiguredName = "app-check-not-configured" - - /// Returns the `AppCheckDebugProvider` unless `app.name` contains ``notConfiguredName``. + /// Returns the `AppCheckDebugProvider` unless `app.name` contains + /// ``FirebaseAppNames/appCheckNotConfigured``. public func createProvider(with app: FirebaseApp) -> (any AppCheckProvider)? { - if app.name.contains(TestAppCheckProviderFactory.notConfiguredName) { + if app.name.contains(FirebaseAppNames.appCheckNotConfigured) { return nil } diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift index bef4349fc90..de0b2e76556 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/GenerateContentIntegrationTests.swift @@ -23,25 +23,28 @@ import VertexAITestApp @Suite(.serialized) struct GenerateContentIntegrationTests { - static let vertexV1Config = APIConfig(service: .vertexAI, version: .v1) - static let vertexV1BetaConfig = APIConfig(service: .vertexAI, version: .v1beta) - static let developerV1BetaConfig = APIConfig( - service: .developer(endpoint: .generativeLanguage), - version: .v1beta + static let vertexV1Config = + InstanceConfig(apiConfig: APIConfig(service: .vertexAI, version: .v1)) + static let vertexV1BetaConfig = + InstanceConfig(apiConfig: APIConfig(service: .vertexAI, version: .v1beta)) + static let developerV1Config = InstanceConfig( + appName: FirebaseAppNames.spark, + apiConfig: APIConfig( + service: .developer(endpoint: .generativeLanguage), version: .v1 + ) + ) + static let developerV1BetaConfig = InstanceConfig( + appName: FirebaseAppNames.spark, + apiConfig: APIConfig( + service: .developer(endpoint: .generativeLanguage), version: .v1beta + ) ) + static let allConfigs = + [vertexV1Config, vertexV1BetaConfig, developerV1Config, developerV1BetaConfig] // Set temperature, topP and topK to lowest allowed values to make responses more deterministic. - static let generationConfig = GenerationConfig( - temperature: 0.0, - topP: 0.0, - topK: 1, - responseMIMEType: "text/plain" - ) - static let systemInstruction = ModelContent( - role: "system", - parts: "You are a friendly and helpful assistant." - ) - static let safetySettings = [ + let generationConfig = GenerationConfig(temperature: 0.0, topP: 0.0, topK: 1) + let safetySettings = [ SafetySetting(harmCategory: .harassment, threshold: .blockLowAndAbove), SafetySetting(harmCategory: .hateSpeech, threshold: .blockLowAndAbove), SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockLowAndAbove), @@ -64,9 +67,13 @@ struct GenerateContentIntegrationTests { storage = Storage.storage() } - @Test(arguments: [vertexV1Config, vertexV1BetaConfig, developerV1BetaConfig]) - func generateContent(_ apiConfig: APIConfig) async throws { - let model = GenerateContentIntegrationTests.model(apiConfig: apiConfig) + @Test(arguments: allConfigs) + func generateContent(_ config: InstanceConfig) async throws { + let model = VertexAI.componentInstance(config).generativeModel( + modelName: ModelNames.gemini2FlashLite, + generationConfig: generationConfig, + safetySettings: safetySettings + ) let prompt = "Where is Google headquarters located? Answer with the city name only." let response = try await model.generateContent(prompt) @@ -75,9 +82,9 @@ struct GenerateContentIntegrationTests { #expect(text == "Mountain View") let usageMetadata = try #require(response.usageMetadata) - #expect(usageMetadata.promptTokenCount == 21) + #expect(usageMetadata.promptTokenCount == 13) #expect(usageMetadata.candidatesTokenCount.isEqual(to: 3, accuracy: tokenCountAccuracy)) - #expect(usageMetadata.totalTokenCount.isEqual(to: 24, accuracy: tokenCountAccuracy)) + #expect(usageMetadata.totalTokenCount.isEqual(to: 16, accuracy: tokenCountAccuracy)) #expect(usageMetadata.promptTokensDetails.count == 1) let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first) #expect(promptTokensDetails.modality == .text) @@ -88,31 +95,45 @@ struct GenerateContentIntegrationTests { #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount) } - static func model(apiConfig: APIConfig) -> GenerativeModel { - return instance(apiConfig: apiConfig).generativeModel( - modelName: "gemini-2.0-flash", - generationConfig: generationConfig, + @Test( + "Generate an enum and provide a system instruction", + arguments: [ + vertexV1Config, + vertexV1BetaConfig, + /* System instructions are not supported on the v1 Developer API. */ + developerV1BetaConfig, + ] + ) + func generateContentEnum(_ config: InstanceConfig) async throws { + let model = VertexAI.componentInstance(config).generativeModel( + modelName: ModelNames.gemini2FlashLite, + generationConfig: GenerationConfig( + responseMIMEType: "text/x.enum", // Not supported on the v1 Developer API + responseSchema: .enumeration(values: ["Red", "Green", "Blue"]) + ), safetySettings: safetySettings, - tools: [], - toolConfig: .init(functionCallingConfig: .none()), - systemInstruction: systemInstruction + tools: [], // Not supported on the v1 Developer API + toolConfig: .init(functionCallingConfig: .none()), // Not supported on the v1 Developer API + systemInstruction: ModelContent(role: "system", parts: "Always pick blue.") ) - } + let prompt = "What is your favourite colour?" - // TODO(andrewheard): Move this helper to a file in the Utilities folder. - static func instance(apiConfig: APIConfig) -> VertexAI { - switch apiConfig.service { - case .vertexAI: - return VertexAI.vertexAI(app: nil, location: "us-central1", apiConfig: apiConfig) - case .developer: - return VertexAI.vertexAI(app: nil, location: nil, apiConfig: apiConfig) - } - } -} + let response = try await model.generateContent(prompt) + + let text = try #require(response.text).trimmingCharacters(in: .whitespacesAndNewlines) + #expect(text == "Blue") -// TODO(andrewheard): Move this extension to a file in the Utilities folder. -extension Numeric where Self: Strideable, Self.Stride.Magnitude: Comparable { - func isEqual(to other: Self, accuracy: Self.Stride) -> Bool { - return distance(to: other).magnitude < accuracy.magnitude + let usageMetadata = try #require(response.usageMetadata) + #expect(usageMetadata.promptTokenCount == 14) + #expect(usageMetadata.candidatesTokenCount.isEqual(to: 1, accuracy: tokenCountAccuracy)) + #expect(usageMetadata.totalTokenCount.isEqual(to: 15, accuracy: tokenCountAccuracy)) + #expect(usageMetadata.promptTokensDetails.count == 1) + let promptTokensDetails = try #require(usageMetadata.promptTokensDetails.first) + #expect(promptTokensDetails.modality == .text) + #expect(promptTokensDetails.tokenCount == usageMetadata.promptTokenCount) + #expect(usageMetadata.candidatesTokensDetails.count == 1) + let candidatesTokensDetails = try #require(usageMetadata.candidatesTokensDetails.first) + #expect(candidatesTokensDetails.modality == .text) + #expect(candidatesTokensDetails.tokenCount == usageMetadata.candidatesTokenCount) } } diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift index 4cd60cf3e76..4bac229088e 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTests.swift @@ -109,7 +109,7 @@ final class IntegrationTests: XCTestCase { } func testGenerateContent_appCheckNotConfigured_shouldFail() async throws { - let app = try FirebaseApp.defaultNamedCopy(name: TestAppCheckProviderFactory.notConfiguredName) + let app = try FirebaseApp.defaultNamedCopy(name: FirebaseAppNames.appCheckNotConfigured) addTeardownBlock { await app.delete() } let vertex = VertexAI.vertexAI(app: app) let model = vertex.generativeModel(modelName: "gemini-2.0-flash") @@ -285,7 +285,7 @@ final class IntegrationTests: XCTestCase { } func testCountTokens_appCheckNotConfigured_shouldFail() async throws { - let app = try FirebaseApp.defaultNamedCopy(name: TestAppCheckProviderFactory.notConfiguredName) + let app = try FirebaseApp.defaultNamedCopy(name: FirebaseAppNames.appCheckNotConfigured) addTeardownBlock { await app.delete() } let vertex = VertexAI.vertexAI(app: app) let model = vertex.generativeModel(modelName: "gemini-2.0-flash") diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTestUtils.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Utilities/IntegrationTestUtils.swift similarity index 88% rename from FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTestUtils.swift rename to FirebaseVertexAI/Tests/TestApp/Tests/Utilities/IntegrationTestUtils.swift index 2d6a447c1d9..cf211339b98 100644 --- a/FirebaseVertexAI/Tests/TestApp/Tests/Integration/IntegrationTestUtils.swift +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Utilities/IntegrationTestUtils.swift @@ -37,3 +37,9 @@ enum IntegrationTestUtils { } } } + +extension Numeric where Self: Strideable, Self.Stride.Magnitude: Comparable { + func isEqual(to other: Self, accuracy: Self.Stride) -> Bool { + return distance(to: other).magnitude < accuracy.magnitude + } +} diff --git a/FirebaseVertexAI/Tests/TestApp/Tests/Utilities/VertexAITestUtils.swift b/FirebaseVertexAI/Tests/TestApp/Tests/Utilities/VertexAITestUtils.swift new file mode 100644 index 00000000000..f76bd8ff148 --- /dev/null +++ b/FirebaseVertexAI/Tests/TestApp/Tests/Utilities/VertexAITestUtils.swift @@ -0,0 +1,59 @@ +// 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. + +import FirebaseCore +import VertexAITestApp + +@testable import struct FirebaseVertexAI.APIConfig +@testable import class FirebaseVertexAI.VertexAI + +struct InstanceConfig { + let appName: String? + let location: String? + let apiConfig: APIConfig + + init(appName: String? = nil, location: String? = nil, apiConfig: APIConfig) { + self.appName = appName + self.location = location + self.apiConfig = apiConfig + } + + var app: FirebaseApp? { + return appName.map { FirebaseApp.app(name: $0) } ?? FirebaseApp.app() + } +} + +extension VertexAI { + static func componentInstance(_ instanceConfig: InstanceConfig) -> VertexAI { + switch instanceConfig.apiConfig.service { + case .vertexAI: + let location = instanceConfig.location ?? "us-central1" + return VertexAI.vertexAI( + app: instanceConfig.app, + location: location, + apiConfig: instanceConfig.apiConfig + ) + case .developer: + assert( + instanceConfig.location == nil, + "The Developer API is global and does not support `location`." + ) + return VertexAI.vertexAI( + app: instanceConfig.app, + location: nil, + apiConfig: instanceConfig.apiConfig + ) + } + } +} diff --git a/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj b/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj index ea5c7a45531..29f333bf127 100644 --- a/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Tests/TestApp/VertexAITestApp.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ 8698D7462CD3CF3600ABA833 /* FirebaseAppTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D7452CD3CF2F00ABA833 /* FirebaseAppTestUtils.swift */; }; 8698D7482CD4332B00ABA833 /* TestAppCheckProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8698D7472CD4332B00ABA833 /* TestAppCheckProviderFactory.swift */; }; 86D77DFC2D7A5340003D155D /* GenerateContentIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */; }; + 86D77DFE2D7B5C86003D155D /* GoogleService-Info-Spark.plist in Resources */ = {isa = PBXBuildFile; fileRef = 86D77DFD2D7B5C86003D155D /* GoogleService-Info-Spark.plist */; }; + 86D77E022D7B63AF003D155D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D77E012D7B63AC003D155D /* Constants.swift */; }; + 86D77E042D7B6C9D003D155D /* VertexAITestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D77E032D7B6C95003D155D /* VertexAITestUtils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -51,6 +54,9 @@ 8698D7452CD3CF2F00ABA833 /* FirebaseAppTestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseAppTestUtils.swift; sourceTree = ""; }; 8698D7472CD4332B00ABA833 /* TestAppCheckProviderFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppCheckProviderFactory.swift; sourceTree = ""; }; 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerateContentIntegrationTests.swift; sourceTree = ""; }; + 86D77DFD2D7B5C86003D155D /* GoogleService-Info-Spark.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info-Spark.plist"; sourceTree = ""; }; + 86D77E012D7B63AC003D155D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 86D77E032D7B6C95003D155D /* VertexAITestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexAITestUtils.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -101,6 +107,7 @@ 868A7C532CCC26B500E449DD /* Assets.xcassets */, 868A7C552CCC271300E449DD /* TestApp.entitlements */, 868A7C462CCA931B00E449DD /* GoogleService-Info.plist */, + 86D77DFD2D7B5C86003D155D /* GoogleService-Info-Spark.plist */, ); path = Resources; sourceTree = ""; @@ -119,6 +126,7 @@ 8661385B2CC943DD00F4B78E /* TestApp.swift */, 8698D7472CD4332B00ABA833 /* TestAppCheckProviderFactory.swift */, 8661385D2CC943DD00F4B78E /* ContentView.swift */, + 86D77E012D7B63AC003D155D /* Constants.swift */, ); path = Sources; sourceTree = ""; @@ -130,7 +138,6 @@ 8661386D2CC943DE00F4B78E /* IntegrationTests.swift */, 86D77DFB2D7A5340003D155D /* GenerateContentIntegrationTests.swift */, 864F8F702D4980D60002EA7E /* ImagenIntegrationTests.swift */, - 862218802D04E08D007ED2D4 /* IntegrationTestUtils.swift */, ); path = Integration; sourceTree = ""; @@ -147,7 +154,9 @@ 8698D7442CD3CEF700ABA833 /* Utilities */ = { isa = PBXGroup; children = ( + 86D77E032D7B6C95003D155D /* VertexAITestUtils.swift */, 8698D7452CD3CF2F00ABA833 /* FirebaseAppTestUtils.swift */, + 862218802D04E08D007ED2D4 /* IntegrationTestUtils.swift */, ); path = Utilities; sourceTree = ""; @@ -243,6 +252,7 @@ buildActionMask = 2147483647; files = ( 868A7C522CCC263300E449DD /* Preview Assets.xcassets in Resources */, + 86D77DFE2D7B5C86003D155D /* GoogleService-Info-Spark.plist in Resources */, 868A7C542CCC26B500E449DD /* Assets.xcassets in Resources */, 868A7C482CCA931B00E449DD /* GoogleService-Info.plist in Resources */, ); @@ -265,6 +275,7 @@ 8661385E2CC943DD00F4B78E /* ContentView.swift in Sources */, 8661385C2CC943DD00F4B78E /* TestApp.swift in Sources */, 8698D7482CD4332B00ABA833 /* TestAppCheckProviderFactory.swift in Sources */, + 86D77E022D7B63AF003D155D /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -272,6 +283,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 86D77E042D7B6C9D003D155D /* VertexAITestUtils.swift in Sources */, 8698D7462CD3CF3600ABA833 /* FirebaseAppTestUtils.swift in Sources */, 868A7C4F2CCC229F00E449DD /* Credentials.swift in Sources */, 864F8F712D4980DD0002EA7E /* ImagenIntegrationTests.swift in Sources */, diff --git a/scripts/gha-encrypted/VertexAI/TestApp-GoogleService-Info-Spark.plist.gpg b/scripts/gha-encrypted/VertexAI/TestApp-GoogleService-Info-Spark.plist.gpg new file mode 100644 index 0000000000000000000000000000000000000000..71463a8043d690dfb788ef6d180dda616cb8dd37 GIT binary patch literal 543 zcmV+)0^t3O4Fm}T2!<8HBeG8e^ZC;00WzVA0Kov%a4&N&YcK=^eM|GuPDYKAwRN}gH*e&iWr$f#$?-lszf4Y~^*5Ci8TE(0Fx#2ub27NiR%jeh(GUtCi5 zL~_dJ<7+aRD+y?biv0sQ9gTXhqEcpBMrbQ z!RtnQBgB`||N8F#avJuS#1)b9YT(1S`&CT{%LG9s@9G^skX$>b&jBah1@w>DQfd$e zEiF8cJYkFzd@orHh3PJ0EKxNlJ*)j~(dfLta!W@Dq94?GYPCJ!rTXB&udFO_h@tJ% zGiuT^JPp60U$JAyd*15)cV8uVbf;2RTmuR$RVshkso6j9jOw9wG3cHtB=+0VrBLQ! zT!>wmD#>tQS`>v~ko!(ZMK!@B`1dISR5on?sc0Yc33q}y_onv@jg=X8GlBX!dtBuw zhsiH8!U-GAm#+a9wm0IYl;J}rY7|SXCaIFb4$TQ)cQwMhfEv{wQq$R)v10h2l;sh& zc)()VKSRr8^xz$?ag&d|NFUHeq{|p`r#J2K@%MItna?qclsQ_`-oL(}Lz)&s{{#cE h(k-{=ZCTK9UOc8I*F+A=4FF0Y>ZnHRh>j12tp>4o0rCI< literal 0 HcmV?d00001 From bec50f946b6b75b0f3b23cfe951603363c637e16 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 10 Mar 2025 14:49:55 -0700 Subject: [PATCH 38/53] Remove VertexAI sample (#14553) --- .github/workflows/vertexai.yml | 32 - FirebaseVertexAI/CHANGELOG.md | 2 + FirebaseVertexAI/README.md | 2 +- .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../ChatSample/Assets.xcassets/Contents.json | 6 - .../ChatSample/Models/ChatMessage.swift | 64 -- .../Preview Assets.xcassets/Contents.json | 6 - .../Screens/ConversationScreen.swift | 129 ---- .../ViewModels/ConversationViewModel.swift | 130 ---- .../ChatSample/Views/BouncingDots.swift | 77 -- .../ChatSample/Views/ErrorDetailsView.swift | 260 ------- .../Sample/ChatSample/Views/ErrorView.swift | 96 --- .../Sample/ChatSample/Views/MessageView.swift | 108 --- .../Screens/FunctionCallingScreen.swift | 131 ---- .../ViewModels/FunctionCallingViewModel.swift | 255 ------- .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../Assets.xcassets/Contents.json | 6 - .../Preview Assets.xcassets/Contents.json | 6 - .../Screens/PhotoReasoningScreen.swift | 78 --- .../ViewModels/PhotoReasoningViewModel.swift | 119 ---- .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../Assets.xcassets/Contents.json | 6 - .../Preview Assets.xcassets/Contents.json | 6 - .../Screens/SummarizeScreen.swift | 80 --- .../ViewModels/SummarizeViewModel.swift | 68 -- .../GenerativeAIUIComponents/Package.swift | 35 - .../GenerativeAIUIComponents/InputField.swift | 83 --- .../MultimodalInputField.swift | 183 ----- .../Sample/GoogleService-Info.plist | 40 -- .../Sample/ImagenScreen/ImagenScreen.swift | 70 -- .../Sample/ImagenScreen/ImagenViewModel.swift | 83 --- FirebaseVertexAI/Sample/README.md | 50 +- .../VertexAISample.xcodeproj/project.pbxproj | 660 ------------------ .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 13 - .../Assets.xcassets/Contents.json | 6 - .../Sample/VertexAISample/ContentView.swift | 61 -- .../Preview Assets.xcassets/Contents.json | 6 - .../VertexAISample/VertexAISampleApp.swift | 67 -- scripts/build.sh | 8 - 43 files changed, 7 insertions(+), 3108 deletions(-) delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Models/ChatMessage.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Screens/ConversationScreen.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Views/BouncingDots.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Views/ErrorView.swift delete mode 100644 FirebaseVertexAI/Sample/ChatSample/Views/MessageView.swift delete mode 100644 FirebaseVertexAI/Sample/FunctionCallingSample/Screens/FunctionCallingScreen.swift delete mode 100644 FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/Screens/SummarizeScreen.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIUIComponents/Package.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/MultimodalInputField.swift delete mode 100644 FirebaseVertexAI/Sample/GoogleService-Info.plist delete mode 100644 FirebaseVertexAI/Sample/ImagenScreen/ImagenScreen.swift delete mode 100644 FirebaseVertexAI/Sample/ImagenScreen/ImagenViewModel.swift delete mode 100644 FirebaseVertexAI/Sample/VertexAISample.xcodeproj/project.pbxproj delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/ContentView.swift delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 FirebaseVertexAI/Sample/VertexAISample/VertexAISampleApp.swift diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index 926fad3a4e2..46965c0b5e8 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -161,35 +161,3 @@ jobs: run: sed -i "" "s#s.swift_version = '5.9'#s.swift_version = '${{ matrix.swift_version}}'#" FirebaseVertexAI.podspec - name: Build and test run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseVertexAI.podspec --platforms=${{ matrix.target }} ${{ matrix.warnings }} - - sample: - strategy: - matrix: - include: - - os: macos-13 - xcode: Xcode_15.2 - - os: macos-14 - xcode: Xcode_15.4 - - os: macos-15 - xcode: Xcode_16.2 - runs-on: ${{ matrix.os }} - needs: spm-package-resolved - env: - FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 - steps: - - uses: actions/checkout@v4 - - uses: actions/cache/restore@v4 - with: - path: .build - key: ${{needs.spm-package-resolved.outputs.cache_key}} - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Placeholder GoogleService-Info.plist for build testing - run: cp FirebaseCore/Tests/Unit/Resources/GoogleService-Info.plist FirebaseVertexAI/Sample/ - - uses: nick-fields/retry@v3 - with: - timeout_minutes: 120 - max_attempts: 3 - retry_on: error - retry_wait_seconds: 120 - command: scripts/build.sh VertexSample iOS diff --git a/FirebaseVertexAI/CHANGELOG.md b/FirebaseVertexAI/CHANGELOG.md index e7fc4c18d6c..1718b883d7d 100644 --- a/FirebaseVertexAI/CHANGELOG.md +++ b/FirebaseVertexAI/CHANGELOG.md @@ -1,6 +1,8 @@ # Unreleased - [feature] The Vertex AI SDK no longer requires `@preconcurrency` when imported in Swift 6. - [feature] The Vertex AI Sample App now includes an image generation example. +- [changed] The Vertex AI Sample App is now part of the + [quickstart-ios repo](https://github.com/firebase/quickstart-ios/tree/main/vertexai). # 11.9.0 - [feature] **Public Preview**: Added support for generating images using the diff --git a/FirebaseVertexAI/README.md b/FirebaseVertexAI/README.md index 84182e15ad9..0392f86e996 100644 --- a/FirebaseVertexAI/README.md +++ b/FirebaseVertexAI/README.md @@ -1,7 +1,7 @@ # Vertex AI for Firebase SDK - For developer documentation, please visit https://firebase.google.com/docs/vertex-ai. -- Try out the [sample app](Sample/README.md) to get started. +- Try out the [sample app](https://github.com/firebase/quickstart-ios/tree/main/vertexai to get started. ## Development diff --git a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json b/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970081..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3ee1a..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Models/ChatMessage.swift b/FirebaseVertexAI/Sample/ChatSample/Models/ChatMessage.swift deleted file mode 100644 index 6f7ab321b12..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Models/ChatMessage.swift +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2023 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. - -import Foundation - -enum Participant { - case system - case user -} - -struct ChatMessage: Identifiable, Equatable { - let id = UUID().uuidString - var message: String - let participant: Participant - var pending = false - - static func pending(participant: Participant) -> ChatMessage { - Self(message: "", participant: participant, pending: true) - } -} - -extension ChatMessage { - static var samples: [ChatMessage] = [ - .init(message: "Hello. What can I do for you today?", participant: .system), - .init(message: "Show me a simple loop in Swift.", participant: .user), - .init(message: """ - Sure, here is a simple loop in Swift: - - # Example 1 - ``` - for i in 1...5 { - print("Hello, world!") - } - ``` - - This loop will print the string "Hello, world!" five times. The for loop iterates over a range of numbers, - in this case the numbers from 1 to 5. The variable i is assigned each number in the range, and the code inside the loop is executed. - - **Here is another example of a simple loop in Swift:** - ```swift - var sum = 0 - for i in 1...100 { - sum += i - } - print("The sum of the numbers from 1 to 100 is \\(sum).") - ``` - - This loop calculates the sum of the numbers from 1 to 100. The variable sum is initialized to 0, and then the for loop iterates over the range of numbers from 1 to 100. The variable i is assigned each number in the range, and the value of i is added to the sum variable. After the loop has finished executing, the value of sum is printed to the console. - """, participant: .system), - ] - - static var sample = samples[0] -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Screens/ConversationScreen.swift b/FirebaseVertexAI/Sample/ChatSample/Screens/ConversationScreen.swift deleted file mode 100644 index 78c903e3412..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Screens/ConversationScreen.swift +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import GenerativeAIUIComponents -import SwiftUI - -struct ConversationScreen: View { - @EnvironmentObject - var viewModel: ConversationViewModel - - @State - private var userPrompt = "" - - enum FocusedField: Hashable { - case message - } - - @FocusState - var focusedField: FocusedField? - - var body: some View { - VStack { - ScrollViewReader { scrollViewProxy in - List { - ForEach(viewModel.messages) { message in - MessageView(message: message) - } - if let error = viewModel.error { - ErrorView(error: error) - .tag("errorView") - } - } - .listStyle(.plain) - .onChange(of: viewModel.messages, perform: { newValue in - if viewModel.hasError { - // wait for a short moment to make sure we can actually scroll to the bottom - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - withAnimation { - scrollViewProxy.scrollTo("errorView", anchor: .bottom) - } - focusedField = .message - } - } else { - guard let lastMessage = viewModel.messages.last else { return } - - // wait for a short moment to make sure we can actually scroll to the bottom - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - withAnimation { - scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom) - } - focusedField = .message - } - } - }) - } - InputField("Message...", text: $userPrompt) { - Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill") - .font(.title) - } - .focused($focusedField, equals: .message) - .onSubmit { sendOrStop() } - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: newChat) { - Image(systemName: "square.and.pencil") - } - } - } - .navigationTitle("Chat sample") - .onAppear { - focusedField = .message - } - } - - private func sendMessage() { - Task { - let prompt = userPrompt - userPrompt = "" - await viewModel.sendMessage(prompt, streaming: true) - } - } - - private func sendOrStop() { - focusedField = nil - - if viewModel.busy { - viewModel.stop() - } else { - sendMessage() - } - } - - private func newChat() { - viewModel.startNewChat() - } -} - -struct ConversationScreen_Previews: PreviewProvider { - struct ContainerView: View { - @StateObject var viewModel = ConversationViewModel() - - var body: some View { - ConversationScreen() - .environmentObject(viewModel) - .onAppear { - viewModel.messages = ChatMessage.samples - } - } - } - - static var previews: some View { - NavigationStack { - ConversationScreen() - } - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift deleted file mode 100644 index a2c8305c8b4..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import Foundation -import UIKit - -@MainActor -class ConversationViewModel: ObservableObject { - /// This array holds both the user's and the system's chat messages - @Published var messages = [ChatMessage]() - - /// Indicates we're waiting for the model to finish - @Published var busy = false - - @Published var error: Error? - var hasError: Bool { - return error != nil - } - - private var model: GenerativeModel - private var chat: Chat - private var stopGenerating = false - - private var chatTask: Task? - - init() { - model = VertexAI.vertexAI().generativeModel(modelName: "gemini-2.0-flash-001") - chat = model.startChat() - } - - func sendMessage(_ text: String, streaming: Bool = true) async { - error = nil - if streaming { - await internalSendMessageStreaming(text) - } else { - await internalSendMessage(text) - } - } - - func startNewChat() { - stop() - error = nil - chat = model.startChat() - messages.removeAll() - } - - func stop() { - chatTask?.cancel() - error = nil - } - - private func internalSendMessageStreaming(_ text: String) async { - chatTask?.cancel() - - chatTask = Task { - busy = true - defer { - busy = false - } - - // first, add the user's message to the chat - let userMessage = ChatMessage(message: text, participant: .user) - messages.append(userMessage) - - // add a pending message while we're waiting for a response from the backend - let systemMessage = ChatMessage.pending(participant: .system) - messages.append(systemMessage) - - do { - let responseStream = try chat.sendMessageStream(text) - for try await chunk in responseStream { - messages[messages.count - 1].pending = false - if let text = chunk.text { - messages[messages.count - 1].message += text - } - } - } catch { - self.error = error - print(error.localizedDescription) - messages.removeLast() - } - } - } - - private func internalSendMessage(_ text: String) async { - chatTask?.cancel() - - chatTask = Task { - busy = true - defer { - busy = false - } - - // first, add the user's message to the chat - let userMessage = ChatMessage(message: text, participant: .user) - messages.append(userMessage) - - // add a pending message while we're waiting for a response from the backend - let systemMessage = ChatMessage.pending(participant: .system) - messages.append(systemMessage) - - do { - var response: GenerateContentResponse? - response = try await chat.sendMessage(text) - - if let responseText = response?.text { - // replace pending message with backend response - messages[messages.count - 1].message = responseText - messages[messages.count - 1].pending = false - } - } catch { - self.error = error - print(error.localizedDescription) - messages.removeLast() - } - } - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Views/BouncingDots.swift b/FirebaseVertexAI/Sample/ChatSample/Views/BouncingDots.swift deleted file mode 100644 index 6895e6723da..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Views/BouncingDots.swift +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2023 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. - -import SwiftUI - -struct BouncingDots: View { - @State - private var dot1YOffset: CGFloat = 0.0 - - @State - private var dot2YOffset: CGFloat = 0.0 - - @State - private var dot3YOffset: CGFloat = 0.0 - - let animation = Animation.easeInOut(duration: 0.8) - .repeatForever(autoreverses: true) - - var body: some View { - HStack(spacing: 8) { - Circle() - .fill(Color.white) - .frame(width: 10, height: 10) - .offset(y: dot1YOffset) - .onAppear { - withAnimation(self.animation.delay(0.0)) { - self.dot1YOffset = -5 - } - } - Circle() - .fill(Color.white) - .frame(width: 10, height: 10) - .offset(y: dot2YOffset) - .onAppear { - withAnimation(self.animation.delay(0.2)) { - self.dot2YOffset = -5 - } - } - Circle() - .fill(Color.white) - .frame(width: 10, height: 10) - .offset(y: dot3YOffset) - .onAppear { - withAnimation(self.animation.delay(0.4)) { - self.dot3YOffset = -5 - } - } - } - .onAppear { - let baseOffset: CGFloat = -2 - - self.dot1YOffset = baseOffset - self.dot2YOffset = baseOffset - self.dot3YOffset = baseOffset - } - } -} - -struct BouncingDots_Previews: PreviewProvider { - static var previews: some View { - BouncingDots() - .frame(width: 200, height: 50) - .background(.blue) - .roundedCorner(10, corners: [.allCorners]) - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift b/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift deleted file mode 100644 index 38c4ed0c410..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import MarkdownUI -import SwiftUI - -private extension HarmCategory { - /// Returns a description of the `HarmCategory` suitable for displaying in the UI. - var displayValue: String { - switch self { - case .dangerousContent: "Dangerous content" - case .harassment: "Harassment" - case .hateSpeech: "Hate speech" - case .sexuallyExplicit: "Sexually explicit" - case .civicIntegrity: "Civic integrity" - default: "Unknown HarmCategory: \(rawValue)" - } - } -} - -private extension SafetyRating.HarmProbability { - /// Returns a description of the `HarmProbability` suitable for displaying in the UI. - var displayValue: String { - switch self { - case .high: "High" - case .low: "Low" - case .medium: "Medium" - case .negligible: "Negligible" - default: "Unknown HarmProbability: \(rawValue)" - } - } -} - -private struct SubtitleFormRow: View { - var title: String - var value: String - - var body: some View { - VStack(alignment: .leading) { - Text(title) - .font(.subheadline) - Text(value) - } - } -} - -private struct SubtitleMarkdownFormRow: View { - var title: String - var value: String - - var body: some View { - VStack(alignment: .leading) { - Text(title) - .font(.subheadline) - Markdown(value) - } - } -} - -private struct SafetyRatingsSection: View { - var ratings: [SafetyRating] - - var body: some View { - Section("Safety ratings") { - List(ratings, id: \.self) { rating in - HStack { - Text(rating.category.displayValue).font(.subheadline) - Spacer() - Text(rating.probability.displayValue) - } - } - } - } -} - -struct ErrorDetailsView: View { - var error: Error - - var body: some View { - NavigationView { - Form { - switch error { - case let GenerateContentError.internalError(underlying: underlyingError): - Section("Error Type") { - Text("Internal error") - } - - Section("Details") { - SubtitleFormRow(title: "Error description", - value: underlyingError.localizedDescription) - } - - case let GenerateContentError.promptBlocked(response: generateContentResponse): - Section("Error Type") { - Text("Your prompt was blocked") - } - - Section("Details") { - if let reason = generateContentResponse.promptFeedback?.blockReason { - SubtitleFormRow(title: "Reason for blocking", value: reason.rawValue) - } - - if let text = generateContentResponse.text { - SubtitleMarkdownFormRow(title: "Last chunk for the response", value: text) - } - } - - if let ratings = generateContentResponse.candidates.first?.safetyRatings { - SafetyRatingsSection(ratings: ratings) - } - - case let GenerateContentError.responseStoppedEarly( - reason: finishReason, - response: generateContentResponse - ): - - Section("Error Type") { - Text("Response stopped early") - } - - Section("Details") { - SubtitleFormRow(title: "Reason for finishing early", value: finishReason.rawValue) - - if let text = generateContentResponse.text { - SubtitleMarkdownFormRow(title: "Last chunk for the response", value: text) - } - } - - if let ratings = generateContentResponse.candidates.first?.safetyRatings { - SafetyRatingsSection(ratings: ratings) - } - - default: - Section("Error Type") { - Text("Some other error") - } - - Section("Details") { - SubtitleFormRow(title: "Error description", value: error.localizedDescription) - } - } - } - .navigationTitle("Error details") - .navigationBarTitleDisplayMode(.inline) - } - } -} - -#Preview("Response Stopped Early") { - let error = GenerateContentError.responseStoppedEarly( - reason: .maxTokens, - response: GenerateContentResponse(candidates: [ - Candidate(content: ModelContent(role: "model", parts: - """ - A _hypothetical_ model response. - Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. - """), - safetyRatings: [ - SafetyRating( - category: .dangerousContent, - probability: .medium, - probabilityScore: 0.8, - severity: .medium, - severityScore: 0.9, - blocked: false - ), - SafetyRating( - category: .harassment, - probability: .low, - probabilityScore: 0.5, - severity: .low, - severityScore: 0.6, - blocked: false - ), - SafetyRating( - category: .hateSpeech, - probability: .low, - probabilityScore: 0.3, - severity: .medium, - severityScore: 0.2, - blocked: false - ), - SafetyRating( - category: .sexuallyExplicit, - probability: .low, - probabilityScore: 0.2, - severity: .negligible, - severityScore: 0.5, - blocked: false - ), - ], - finishReason: FinishReason.maxTokens, - citationMetadata: nil), - ]) - ) - - return ErrorDetailsView(error: error) -} - -#Preview("Prompt Blocked") { - let error = GenerateContentError.promptBlocked( - response: GenerateContentResponse(candidates: [ - Candidate(content: ModelContent(role: "model", parts: - """ - A _hypothetical_ model response. - Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. - """), - safetyRatings: [ - SafetyRating( - category: .dangerousContent, - probability: .low, - probabilityScore: 0.8, - severity: .medium, - severityScore: 0.9, - blocked: false - ), - SafetyRating( - category: .harassment, - probability: .low, - probabilityScore: 0.5, - severity: .low, - severityScore: 0.6, - blocked: false - ), - SafetyRating( - category: .hateSpeech, - probability: .low, - probabilityScore: 0.3, - severity: .medium, - severityScore: 0.2, - blocked: false - ), - SafetyRating( - category: .sexuallyExplicit, - probability: .low, - probabilityScore: 0.2, - severity: .negligible, - severityScore: 0.5, - blocked: false - ), - ], - finishReason: FinishReason.other, - citationMetadata: nil), - ]) - ) - - return ErrorDetailsView(error: error) -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorView.swift b/FirebaseVertexAI/Sample/ChatSample/Views/ErrorView.swift deleted file mode 100644 index a5d43c30b2d..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorView.swift +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import SwiftUI - -struct ErrorView: View { - var error: Error - @State private var isDetailsSheetPresented = false - var body: some View { - HStack { - Text("An error occurred.") - Button(action: { isDetailsSheetPresented.toggle() }) { - Image(systemName: "info.circle") - } - } - .frame(maxWidth: .infinity, alignment: .center) - .listRowSeparator(.hidden) - .sheet(isPresented: $isDetailsSheetPresented) { - ErrorDetailsView(error: error) - } - } -} - -#Preview { - NavigationView { - let errorPromptBlocked = GenerateContentError.promptBlocked( - response: GenerateContentResponse( - candidates: [ - Candidate( - content: ModelContent(role: "model", parts: [ - """ - A _hypothetical_ model response. - Cillum ex aliqua amet aliquip labore amet eiusmod consectetur reprehenderit sit commodo. - """, - ]), - safetyRatings: [ - SafetyRating( - category: .dangerousContent, - probability: .high, - probabilityScore: 0.8, - severity: .medium, - severityScore: 0.9, - blocked: true - ), - SafetyRating( - category: .harassment, - probability: .low, - probabilityScore: 0.5, - severity: .low, - severityScore: 0.6, - blocked: false - ), - SafetyRating( - category: .hateSpeech, - probability: .low, - probabilityScore: 0.3, - severity: .medium, - severityScore: 0.2, - blocked: false - ), - SafetyRating( - category: .sexuallyExplicit, - probability: .low, - probabilityScore: 0.2, - severity: .negligible, - severityScore: 0.5, - blocked: false - ), - ], - finishReason: FinishReason.other, - citationMetadata: nil - ), - ] - ) - ) - List { - MessageView(message: ChatMessage.samples[0]) - MessageView(message: ChatMessage.samples[1]) - ErrorView(error: errorPromptBlocked) - } - .listStyle(.plain) - .navigationTitle("Chat sample") - } -} diff --git a/FirebaseVertexAI/Sample/ChatSample/Views/MessageView.swift b/FirebaseVertexAI/Sample/ChatSample/Views/MessageView.swift deleted file mode 100644 index 79894503ffd..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/Views/MessageView.swift +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2023 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. - -import MarkdownUI -import SwiftUI - -struct RoundedCorner: Shape { - var radius: CGFloat = .infinity - var corners: UIRectCorner = .allCorners - - func path(in rect: CGRect) -> Path { - let path = UIBezierPath( - roundedRect: rect, - byRoundingCorners: corners, - cornerRadii: CGSize(width: radius, height: radius) - ) - return Path(path.cgPath) - } -} - -extension View { - func roundedCorner(_ radius: CGFloat, corners: UIRectCorner) -> some View { - clipShape(RoundedCorner(radius: radius, corners: corners)) - } -} - -struct MessageContentView: View { - var message: ChatMessage - - var body: some View { - if message.pending { - BouncingDots() - } else { - Markdown(message.message) - .markdownTextStyle { - FontFamilyVariant(.normal) - FontSize(.em(0.85)) - ForegroundColor(message.participant == .system ? Color(UIColor.label) : .white) - } - .markdownBlockStyle(\.codeBlock) { configuration in - configuration.label - .relativeLineSpacing(.em(0.25)) - .markdownTextStyle { - FontFamilyVariant(.monospaced) - FontSize(.em(0.85)) - ForegroundColor(Color(.label)) - } - .padding() - .background(Color(.secondarySystemBackground)) - .clipShape(RoundedRectangle(cornerRadius: 8)) - .markdownMargin(top: .zero, bottom: .em(0.8)) - } - } - } -} - -struct MessageView: View { - var message: ChatMessage - - var body: some View { - HStack { - if message.participant == .user { - Spacer() - } - MessageContentView(message: message) - .padding(10) - .background(message.participant == .system - ? Color(UIColor.systemFill) - : Color(UIColor.systemBlue)) - .roundedCorner(10, - corners: [ - .topLeft, - .topRight, - message.participant == .system ? .bottomRight : .bottomLeft, - ]) - if message.participant == .system { - Spacer() - } - } - .listRowSeparator(.hidden) - } -} - -struct MessageView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - List { - MessageView(message: ChatMessage.samples[0]) - MessageView(message: ChatMessage.samples[1]) - MessageView(message: ChatMessage.samples[2]) - MessageView(message: ChatMessage(message: "Hello!", participant: .system, pending: true)) - } - .listStyle(.plain) - .navigationTitle("Chat sample") - } - } -} diff --git a/FirebaseVertexAI/Sample/FunctionCallingSample/Screens/FunctionCallingScreen.swift b/FirebaseVertexAI/Sample/FunctionCallingSample/Screens/FunctionCallingScreen.swift deleted file mode 100644 index f16da39e22f..00000000000 --- a/FirebaseVertexAI/Sample/FunctionCallingSample/Screens/FunctionCallingScreen.swift +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import GenerativeAIUIComponents -import SwiftUI - -struct FunctionCallingScreen: View { - @EnvironmentObject - var viewModel: FunctionCallingViewModel - - @State - private var userPrompt = "What is 100 Euros in U.S. Dollars?" - - enum FocusedField: Hashable { - case message - } - - @FocusState - var focusedField: FocusedField? - - var body: some View { - VStack { - ScrollViewReader { scrollViewProxy in - List { - Text("Interact with a currency conversion API using function calling in Gemini.") - ForEach(viewModel.messages) { message in - MessageView(message: message) - } - if let error = viewModel.error { - ErrorView(error: error) - .tag("errorView") - } - } - .listStyle(.plain) - .onChange(of: viewModel.messages, perform: { newValue in - if viewModel.hasError { - // Wait for a short moment to make sure we can actually scroll to the bottom. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - withAnimation { - scrollViewProxy.scrollTo("errorView", anchor: .bottom) - } - focusedField = .message - } - } else { - guard let lastMessage = viewModel.messages.last else { return } - - // Wait for a short moment to make sure we can actually scroll to the bottom. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { - withAnimation { - scrollViewProxy.scrollTo(lastMessage.id, anchor: .bottom) - } - focusedField = .message - } - } - }) - .onTapGesture { - focusedField = nil - } - } - InputField("Message...", text: $userPrompt) { - Image(systemName: viewModel.busy ? "stop.circle.fill" : "arrow.up.circle.fill") - .font(.title) - } - .focused($focusedField, equals: .message) - .onSubmit { sendOrStop() } - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: newChat) { - Image(systemName: "square.and.pencil") - } - } - } - .navigationTitle("Function Calling") - .onAppear { - focusedField = .message - } - } - - private func sendMessage() { - Task { - let prompt = userPrompt - userPrompt = "" - await viewModel.sendMessage(prompt, streaming: true) - } - } - - private func sendOrStop() { - if viewModel.busy { - viewModel.stop() - } else { - sendMessage() - } - } - - private func newChat() { - viewModel.startNewChat() - } -} - -struct FunctionCallingScreen_Previews: PreviewProvider { - struct ContainerView: View { - @EnvironmentObject - var viewModel: FunctionCallingViewModel - - var body: some View { - FunctionCallingScreen() - .onAppear { - viewModel.messages = ChatMessage.samples - } - } - } - - static var previews: some View { - NavigationStack { - FunctionCallingScreen().environmentObject(FunctionCallingViewModel()) - } - } -} diff --git a/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift b/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift deleted file mode 100644 index 569cf770dd5..00000000000 --- a/FirebaseVertexAI/Sample/FunctionCallingSample/ViewModels/FunctionCallingViewModel.swift +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import Foundation -import UIKit - -@MainActor -class FunctionCallingViewModel: ObservableObject { - /// This array holds both the user's and the system's chat messages - @Published var messages = [ChatMessage]() - - /// Indicates we're waiting for the model to finish - @Published var busy = false - - @Published var error: Error? - var hasError: Bool { - return error != nil - } - - /// Function calls pending processing - private var functionCalls = [FunctionCallPart]() - - private var model: GenerativeModel - private var chat: Chat - - private var chatTask: Task? - - init() { - model = VertexAI.vertexAI().generativeModel( - modelName: "gemini-2.0-flash-001", - tools: [.functionDeclarations([ - FunctionDeclaration( - name: "get_exchange_rate", - description: "Get the exchange rate for currencies between countries", - parameters: [ - "currency_from": .enumeration( - values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], - description: "The currency to convert from in ISO 4217 format" - ), - "currency_to": .enumeration( - values: ["USD", "EUR", "JPY", "GBP", "AUD", "CAD"], - description: "The currency to convert to in ISO 4217 format" - ), - ] - ), - ])] - ) - chat = model.startChat() - } - - func sendMessage(_ text: String, streaming: Bool = true) async { - error = nil - chatTask?.cancel() - - chatTask = Task { - busy = true - defer { - busy = false - } - - // first, add the user's message to the chat - let userMessage = ChatMessage(message: text, participant: .user) - messages.append(userMessage) - - // add a pending message while we're waiting for a response from the backend - let systemMessage = ChatMessage.pending(participant: .system) - messages.append(systemMessage) - - print(messages) - do { - repeat { - if streaming { - try await internalSendMessageStreaming(text) - } else { - try await internalSendMessage(text) - } - } while !functionCalls.isEmpty - } catch { - self.error = error - print(error.localizedDescription) - messages.removeLast() - } - } - } - - func startNewChat() { - stop() - error = nil - chat = model.startChat() - messages.removeAll() - } - - func stop() { - chatTask?.cancel() - error = nil - } - - private func internalSendMessageStreaming(_ text: String) async throws { - let functionResponses = try await processFunctionCalls() - let responseStream: AsyncThrowingStream - if functionResponses.isEmpty { - responseStream = try chat.sendMessageStream(text) - } else { - for functionResponse in functionResponses { - messages.insert(functionResponse.chatMessage(), at: messages.count - 1) - } - responseStream = try chat.sendMessageStream([functionResponses.modelContent()]) - } - for try await chunk in responseStream { - processResponseContent(content: chunk) - } - } - - private func internalSendMessage(_ text: String) async throws { - let functionResponses = try await processFunctionCalls() - let response: GenerateContentResponse - if functionResponses.isEmpty { - response = try await chat.sendMessage(text) - } else { - for functionResponse in functionResponses { - messages.insert(functionResponse.chatMessage(), at: messages.count - 1) - } - response = try await chat.sendMessage([functionResponses.modelContent()]) - } - processResponseContent(content: response) - } - - func processResponseContent(content: GenerateContentResponse) { - guard let candidate = content.candidates.first else { - fatalError("No candidate.") - } - - for part in candidate.content.parts { - switch part { - case let textPart as TextPart: - // replace pending message with backend response - messages[messages.count - 1].message += textPart.text - messages[messages.count - 1].pending = false - case let functionCallPart as FunctionCallPart: - messages.insert(functionCallPart.chatMessage(), at: messages.count - 1) - functionCalls.append(functionCallPart) - default: - fatalError("Unsupported response part: \(part)") - } - } - } - - func processFunctionCalls() async throws -> [FunctionResponsePart] { - var functionResponses = [FunctionResponsePart]() - for functionCall in functionCalls { - switch functionCall.name { - case "get_exchange_rate": - let exchangeRates = getExchangeRate(args: functionCall.args) - functionResponses.append(FunctionResponsePart( - name: "get_exchange_rate", - response: exchangeRates - )) - default: - fatalError("Unknown function named \"\(functionCall.name)\".") - } - } - functionCalls = [] - - return functionResponses - } - - // MARK: - Callable Functions - - func getExchangeRate(args: JSONObject) -> JSONObject { - // 1. Validate and extract the parameters provided by the model (from a `FunctionCall`) - guard case let .string(from) = args["currency_from"] else { - fatalError("Missing `currency_from` parameter.") - } - guard case let .string(to) = args["currency_to"] else { - fatalError("Missing `currency_to` parameter.") - } - - // 2. Get the exchange rate - let allRates: [String: [String: Double]] = [ - "AUD": ["CAD": 0.89265, "EUR": 0.6072, "GBP": 0.51714, "JPY": 97.75, "USD": 0.66379], - "CAD": ["AUD": 1.1203, "EUR": 0.68023, "GBP": 0.57933, "JPY": 109.51, "USD": 0.74362], - "EUR": ["AUD": 1.6469, "CAD": 1.4701, "GBP": 0.85168, "JPY": 160.99, "USD": 1.0932], - "GBP": ["AUD": 1.9337, "CAD": 1.7261, "EUR": 1.1741, "JPY": 189.03, "USD": 1.2836], - "JPY": ["AUD": 0.01023, "CAD": 0.00913, "EUR": 0.00621, "GBP": 0.00529, "USD": 0.00679], - "USD": ["AUD": 1.5065, "CAD": 1.3448, "EUR": 0.91475, "GBP": 0.77907, "JPY": 147.26], - ] - guard let fromRates = allRates[from] else { - return ["error": .string("No data for currency \(from).")] - } - guard let toRate = fromRates[to] else { - return ["error": .string("No data for currency \(to).")] - } - - // 3. Return the exchange rates as a JSON object (returned to the model in a `FunctionResponse`) - return ["rates": .number(toRate)] - } -} - -private extension FunctionCallPart { - func chatMessage() -> ChatMessage { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - let jsonData: Data - do { - jsonData = try encoder.encode(self) - } catch { - fatalError("JSON Encoding Failed: \(error.localizedDescription)") - } - guard let json = String(data: jsonData, encoding: .utf8) else { - fatalError("Failed to convert JSON data to a String.") - } - let messageText = "Function call requested by model:\n```\n\(json)\n```" - - return ChatMessage(message: messageText, participant: .system) - } -} - -private extension FunctionResponsePart { - func chatMessage() -> ChatMessage { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - - let jsonData: Data - do { - jsonData = try encoder.encode(self) - } catch { - fatalError("JSON Encoding Failed: \(error.localizedDescription)") - } - guard let json = String(data: jsonData, encoding: .utf8) else { - fatalError("Failed to convert JSON data to a String.") - } - let messageText = "Function response returned by app:\n```\n\(json)\n```" - - return ChatMessage(message: messageText, participant: .user) - } -} - -private extension [FunctionResponsePart] { - func modelContent() -> ModelContent { - return ModelContent(role: "function", parts: self) - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970081..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3ee1a..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift deleted file mode 100644 index 930214770d8..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/Screens/PhotoReasoningScreen.swift +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2023 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. - -import GenerativeAIUIComponents -import MarkdownUI -import PhotosUI -import SwiftUI - -struct PhotoReasoningScreen: View { - @StateObject var viewModel = PhotoReasoningViewModel() - - enum FocusedField: Hashable { - case message - } - - @FocusState - var focusedField: FocusedField? - - var body: some View { - VStack { - MultimodalInputField(text: $viewModel.userInput, selection: $viewModel.selectedItems) - .focused($focusedField, equals: .message) - .onSubmit { - onSendTapped() - } - - ScrollViewReader { scrollViewProxy in - List { - if let outputText = viewModel.outputText { - HStack(alignment: .top) { - if viewModel.inProgress { - ProgressView() - } else { - Image(systemName: "cloud.circle.fill") - .font(.title2) - } - - Markdown("\(outputText)") - } - .listRowSeparator(.hidden) - } - } - .listStyle(.plain) - } - } - .navigationTitle("Multimodal sample") - .onAppear { - focusedField = .message - } - } - - // MARK: - Actions - - private func onSendTapped() { - focusedField = nil - - Task { - await viewModel.reason() - } - } -} - -#Preview { - NavigationStack { - PhotoReasoningScreen() - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift deleted file mode 100644 index 722e8ede238..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import Foundation -import OSLog -import PhotosUI -import SwiftUI - -@MainActor -class PhotoReasoningViewModel: ObservableObject { - // Maximum value for the larger of the two image dimensions (height and width) in pixels. This is - // being used to reduce the image size in bytes. - private static let largestImageDimension = 768.0 - - private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") - - @Published - var userInput: String = "" - - @Published - var selectedItems = [PhotosPickerItem]() - - @Published - var outputText: String? = nil - - @Published - var errorMessage: String? - - @Published - var inProgress = false - - private var model: GenerativeModel? - - init() { - model = VertexAI.vertexAI().generativeModel(modelName: "gemini-2.0-flash-001") - } - - func reason() async { - defer { - inProgress = false - } - guard let model else { - return - } - - do { - inProgress = true - errorMessage = nil - outputText = "" - - let prompt = "Look at the image(s), and then answer the following question: \(userInput)" - - var images = [any PartsRepresentable]() - for item in selectedItems { - if let data = try? await item.loadTransferable(type: Data.self) { - guard let image = UIImage(data: data) else { - logger.error("Failed to parse data as an image, skipping.") - continue - } - if image.size.fits(largestDimension: PhotoReasoningViewModel.largestImageDimension) { - images.append(image) - } else { - guard let resizedImage = image - .preparingThumbnail(of: image.size - .aspectFit(largestDimension: PhotoReasoningViewModel.largestImageDimension)) else { - logger.error("Failed to resize image: \(image)") - continue - } - - images.append(resizedImage) - } - } - } - - let outputContentStream = try model.generateContentStream(prompt, images) - - // stream response - for try await outputContent in outputContentStream { - guard let line = outputContent.text else { - return - } - - outputText = (outputText ?? "") + line - } - } catch { - logger.error("\(error.localizedDescription)") - errorMessage = error.localizedDescription - } - } -} - -private extension CGSize { - func fits(largestDimension length: CGFloat) -> Bool { - return width <= length && height <= length - } - - func aspectFit(largestDimension length: CGFloat) -> CGSize { - let aspectRatio = width / height - if width > height { - let width = min(self.width, length) - return CGSize(width: width, height: round(width / aspectRatio)) - } else { - let height = min(self.height, length) - return CGSize(width: round(height * aspectRatio), height: height) - } - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json b/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb878970081..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 13613e3ee1a..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json b/FirebaseVertexAI/Sample/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a7f..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/Screens/SummarizeScreen.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/Screens/SummarizeScreen.swift deleted file mode 100644 index 748c1addd5f..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/Screens/SummarizeScreen.swift +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2023 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. - -import MarkdownUI -import SwiftUI - -struct SummarizeScreen: View { - @StateObject var viewModel = SummarizeViewModel() - @State var userInput = "" - - enum FocusedField: Hashable { - case message - } - - @FocusState - var focusedField: FocusedField? - - var body: some View { - VStack { - VStack(alignment: .leading) { - Text("Enter some text, then tap on _Go_ to summarize it.") - .padding(.horizontal, 6) - HStack(alignment: .top) { - TextField("Enter text summarize", text: $userInput, axis: .vertical) - .focused($focusedField, equals: .message) - .textFieldStyle(.roundedBorder) - .onSubmit { - onSummarizeTapped() - } - Button("Go") { - onSummarizeTapped() - } - .padding(.top, 4) - } - } - .padding(.horizontal, 16) - - List { - HStack(alignment: .top) { - if viewModel.inProgress { - ProgressView() - } else { - Image(systemName: "cloud.circle.fill") - .font(.title2) - } - - Markdown("\(viewModel.outputText)") - } - .listRowSeparator(.hidden) - } - .listStyle(.plain) - } - .navigationTitle("Text sample") - } - - private func onSummarizeTapped() { - focusedField = nil - - Task { - await viewModel.summarize(inputText: userInput) - } - } -} - -#Preview { - NavigationStack { - SummarizeScreen() - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift deleted file mode 100644 index 927f124b8e7..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2023 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. - -import FirebaseVertexAI -import Foundation -import OSLog - -@MainActor -class SummarizeViewModel: ObservableObject { - private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "generative-ai") - - @Published - var outputText = "" - - @Published - var errorMessage: String? - - @Published - var inProgress = false - - private var model: GenerativeModel? - - init() { - model = VertexAI.vertexAI().generativeModel(modelName: "gemini-2.0-flash-001") - } - - func summarize(inputText: String) async { - defer { - inProgress = false - } - guard let model else { - return - } - - do { - inProgress = true - errorMessage = nil - outputText = "" - - let prompt = "Summarize the following text for me: \(inputText)" - - let outputContentStream = try model.generateContentStream(prompt) - - // stream response - for try await outputContent in outputContentStream { - guard let line = outputContent.text else { - return - } - - outputText = outputText + line - } - } catch { - logger.error("\(error.localizedDescription)") - errorMessage = error.localizedDescription - } - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Package.swift b/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Package.swift deleted file mode 100644 index 808f5f42a97..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Package.swift +++ /dev/null @@ -1,35 +0,0 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - -// Copyright 2023 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. -import PackageDescription - -let package = Package( - name: "GenerativeAIUIComponents", - platforms: [ - .iOS(.v16), - ], - products: [ - .library( - name: "GenerativeAIUIComponents", - targets: ["GenerativeAIUIComponents"] - ), - ], - targets: [ - .target( - name: "GenerativeAIUIComponents" - ), - ] -) diff --git a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift b/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift deleted file mode 100644 index 67941c370cb..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2023 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. - -import SwiftUI - -public struct InputField