From a2fd7442f454a471f29e2201a08d48120e1a762f Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 25 Apr 2024 11:56:18 -0400 Subject: [PATCH 001/103] Disable failing ObjC Performance quickstart on CI (#12853) --- .github/workflows/performance.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 4e5ba16eb98..ce0a4ac8790 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -79,7 +79,7 @@ jobs: env: plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - runs-on: macos-12 # TODO: the legacy ObjC quickstarts don't run with Xcode 15. + runs-on: macos-14 steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 @@ -90,8 +90,9 @@ jobs: quickstart-ios/performance/GoogleService-Info.plist "$plist_secret" - name: Test swift quickstart run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true swift) - - name: Test objc quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true) + # TODO: The legacy ObjC quickstarts don't run with Xcode 15, re-able if we get these working. + # - name: Test objc quickstart + # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart.sh Performance true) quickstart-ftl-cron-only: if: github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule' From b5c4044d38a3a0e65c7419790d1698b2aec758f9 Mon Sep 17 00:00:00 2001 From: themiswang Date: Mon, 29 Apr 2024 18:18:52 -0400 Subject: [PATCH 002/103] Gracefully fail rollout write (#12863) --- .../Controllers/FIRCLSRolloutsPersistenceManager.m | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m index 8bb4c5cabfa..defd9d68a52 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSRolloutsPersistenceManager.m @@ -58,10 +58,15 @@ - (void)updateRolloutsStateToPersistenceWithRollouts:(NSData *_Nonnull)rollouts NSFileHandle *rolloutsFile = [NSFileHandle fileHandleForUpdatingAtPath:rolloutsPath]; dispatch_sync(FIRCLSGetLoggingQueue(), ^{ - [rolloutsFile seekToEndOfFile]; - [rolloutsFile writeData:rollouts]; - NSData *newLineData = [@"\n" dataUsingEncoding:NSUTF8StringEncoding]; - [rolloutsFile writeData:newLineData]; + @try { + [rolloutsFile seekToEndOfFile]; + NSMutableData *rolloutsWithNewLineData = [rollouts mutableCopy]; + [rolloutsWithNewLineData appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [rolloutsFile writeData:rolloutsWithNewLineData]; + } @catch (NSException *exception) { + FIRCLSDebugLog(@"Failed to write new rollouts. Exception name: %s - message: %s", + exception.name, exception.reason); + } }); } From 912df58c205fc8a75c6a1a3e03df846b1e415e0d Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 30 Apr 2024 15:34:12 -0400 Subject: [PATCH 003/103] [Release] Carthage updates for 10.25.0 (#12871) --- ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json | 1 + .../CarthageJSON/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 + ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json | 1 + ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json | 1 + 19 files changed, 19 insertions(+) diff --git a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json index c6144a69ec9..6ce3d42323c 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseABTestingBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseABTesting-2823ac22562f1fbe.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseABTesting-999c4183ee13b8d3.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseABTesting-f0810693c46fdadc.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseABTesting-7c24443801777936.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseABTesting-e87c686cee02758a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseABTesting-6a65ab8b888172af.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseABTesting-197f0cb4125363b6.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json index 5330930f9da..6c257b6edbc 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAdMobBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/Google-Mobile-Ads-SDK-bf8077d30296e04a.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/Google-Mobile-Ads-SDK-ea1e4524d0df93f6.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/Google-Mobile-Ads-SDK-28d9adec807e6c50.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/Google-Mobile-Ads-SDK-4088408e21a81c67.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/Google-Mobile-Ads-SDK-8b0d1ce3d1162b67.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/Google-Mobile-Ads-SDK-046511c3fd0189eb.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/Google-Mobile-Ads-SDK-50008c143ad8f268.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json index ced65ee4f5e..78710f2e78f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAnalytics-a121058bc5824bfa.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseAnalytics-ed7624b45fb9f7f1.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseAnalytics-e3194dd8c803ccd4.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseAnalytics-8af4daf086589ec7.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalytics-95669fcf109f74a2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalytics-c0db6cb0e858e397.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalytics-e8ebe991b5743f71.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json index bf2109b85c3..9d844f1e5eb 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAnalyticsOnDeviceConversionBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAnalyticsOnDeviceConversion-4b5874979659af63.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseAnalyticsOnDeviceConversion-8e03f5b073a147b0.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseAnalyticsOnDeviceConversion-8849e6e43cbbd3c0.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseAnalyticsOnDeviceConversion-3ab4488b5238043b.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAnalyticsOnDeviceConversion-091f5252d693a9f9.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAnalyticsOnDeviceConversion-7bbb73d46383a042.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAnalyticsOnDeviceConversion-eca2f83d40e0278d.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json index 70694d49ab5..b75acaa55d4 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppCheckBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAppCheck-2b52807979acf863.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseAppCheck-24146838e19ccc61.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseAppCheck-e2e664c6ec135fc4.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseAppCheck-81762cfe63fc1817.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppCheck-d19e46a728b1ac4f.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppCheck-8339fde989fe8f24.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppCheck-3ce0f074bfcd2596.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json index f4b43433400..d9037d92218 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAppDistributionBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAppDistribution-139211bb5dd3dbc3.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseAppDistribution-fc3d9f20f693b734.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseAppDistribution-e38dad9115dff3af.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseAppDistribution-cd27707b993aae9d.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAppDistribution-cefc3327ddfceda6.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAppDistribution-7931e42d39575534.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAppDistribution-79dc2b1348d9aee9.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json index 0e775d605a7..5f44d8e69cf 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseAuthBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseAuth-529e82147fbbd402.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseAuth-2bbb81f2b4cbac77.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseAuth-466e0e14d0c342e8.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseAuth-a223b8ebda8fd2c2.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseAuth-e43e66353617f093.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseAuth-8a9591e6daa7e207.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseAuth-7e18a510d0a5b02e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json index ec52855c4a6..e72e5751024 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseCrashlyticsBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseCrashlytics-47c05619edb8ae9b.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseCrashlytics-913794b28b7424dc.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseCrashlytics-0805720ebb051475.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseCrashlytics-4035e9332410161e.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseCrashlytics-d29d3285a7d9fa1d.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseCrashlytics-165beb64483b4278.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseCrashlytics-53604573442e756b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json index 6e2091eb480..a8a3944d148 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDatabaseBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseDatabase-f5156c8169b6358f.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseDatabase-61799b2e188bed5b.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseDatabase-a27e18a8a37d7d0a.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseDatabase-0ac7f999ddc3f338.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDatabase-5b22f689cb66d83a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDatabase-e1a9d1f0c4222cf7.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDatabase-aea9249d81841ee1.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json index 79bd327feb5..a912b76f1de 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseDynamicLinksBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseDynamicLinks-c17c59949b7cc573.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseDynamicLinks-ffffc66283665cc3.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseDynamicLinks-6aa708ba01e222f5.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseDynamicLinks-1c3a48e8f12fc824.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseDynamicLinks-7cf4ae5e96882ca8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseDynamicLinks-c3bdeb37651a5d5d.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseDynamicLinks-bcb5df6ec32f6684.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json index f74d200181f..cc338c3e3b8 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFirestoreBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseFirestore-e4570e4863fe2044.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseFirestore-7d1481e62eb86231.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseFirestore-aebec35b37a4feed.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseFirestore-c5fda3ae8ab62345.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFirestore-73ba0700b1aa6d6a.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFirestore-02eb8da05f81fca5.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFirestore-46fa68ddf287f76e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json index 1f939073b96..7f17ba4aa5f 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseFunctionsBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseFunctions-d98d21836c2f2130.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseFunctions-30a434d6b70589d3.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseFunctions-56fc811f8293f2ec.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseFunctions-c143cedcb64ae0d6.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseFunctions-47189f2c99cdf806.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseFunctions-17c4b760141e38ad.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseFunctions-688a38b567392fcf.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json index 89279121852..5094644f88a 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseGoogleSignInBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/GoogleSignIn-a16b78c06ef8f77c.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/GoogleSignIn-fa5daf30aae63bc6.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/GoogleSignIn-03314eec1dbb9708.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/GoogleSignIn-fb2542a2c86f843a.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/GoogleSignIn-a5b49807be66100b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/GoogleSignIn-0d2e746eb3ff9f92.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/GoogleSignIn-5cb2a2f1f74efd5e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json index d5eba54e216..e2622da1868 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseInAppMessagingBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseInAppMessaging-fbb53083384bea1e.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseInAppMessaging-0a6028f71dc4dad9.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseInAppMessaging-29ae2009ef1950b2.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseInAppMessaging-b90e8ce0168460cd.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseInAppMessaging-91e5426eade46bca.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseInAppMessaging-10801bd111df59de.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseInAppMessaging-91d4dd9878a06b7e.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json index aaa8a8a44a3..5af9887ea07 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMLModelDownloaderBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseMLModelDownloader-b3bffe302a074d0e.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseMLModelDownloader-acc3b6f7b4beb3d8.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseMLModelDownloader-7d8218cf3d3ef34e.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseMLModelDownloader-0373c9fd0cc9bd65.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMLModelDownloader-559cb113c0cfd8f2.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMLModelDownloader-9c909894999c92e4.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMLModelDownloader-9abf9b0e24bfb921.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json index e4156444a48..a17296fa641 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseMessagingBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseMessaging-812bc4f1c2d27e93.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseMessaging-3289b9f5d636eda0.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseMessaging-d1e21fc776fa88ed.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseMessaging-e6c8a212895199fa.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseMessaging-59ef1cc63c660712.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseMessaging-76c02a69e3fe1008.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseMessaging-439a17dcc8b8172b.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json index 33d4139e0ce..56af49deb45 100644 --- a/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebasePerformanceBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebasePerformance-2a39f03d02fcbc5f.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebasePerformance-daf49762760bcd28.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebasePerformance-04ba5863cb67f726.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebasePerformance-bc7b306425147b0a.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebasePerformance-36ac6dfb99caa11b.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebasePerformance-f9f5be8ffad5cbb0.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebasePerformance-0ffe559f7554d8a5.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json index f145a0bfc7e..7819482c9e0 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseRemoteConfigBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseRemoteConfig-be4764f1b3e07c4f.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseRemoteConfig-33cb2acece724af5.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseRemoteConfig-8ebff21169280a12.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseRemoteConfig-93bad5de6b479bb5.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseRemoteConfig-edd1b427b8bbe782.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseRemoteConfig-10b62ee5663aaab3.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseRemoteConfig-2237eb5fcd4a4525.zip", diff --git a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json index e94f059864a..a62ac3a11be 100644 --- a/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json +++ b/ReleaseTooling/CarthageJSON/FirebaseStorageBinary.json @@ -17,6 +17,7 @@ "10.22.0": "https://dl.google.com/dl/firebase/ios/carthage/10.22.0/FirebaseStorage-e3b2849afc9f0f95.zip", "10.23.0": "https://dl.google.com/dl/firebase/ios/carthage/10.23.0/FirebaseStorage-251d7827e3fc52e4.zip", "10.24.0": "https://dl.google.com/dl/firebase/ios/carthage/10.24.0/FirebaseStorage-f4d55d7d55242a20.zip", + "10.25.0": "https://dl.google.com/dl/firebase/ios/carthage/10.25.0/FirebaseStorage-472cc5950e16fed9.zip", "10.3.0": "https://dl.google.com/dl/firebase/ios/carthage/10.3.0/FirebaseStorage-ac463d14593d10a8.zip", "10.4.0": "https://dl.google.com/dl/firebase/ios/carthage/10.4.0/FirebaseStorage-fdf8479115660ce6.zip", "10.5.0": "https://dl.google.com/dl/firebase/ios/carthage/10.5.0/FirebaseStorage-04f255ea8c3a7420.zip", From b36f8dae2debe59e6e7385473903c9cb8abc9bee Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 30 Apr 2024 20:22:15 -0400 Subject: [PATCH 004/103] Update versions for Release 10.27.0 (#12875) --- Firebase.podspec | 48 +++++++++---------- FirebaseABTesting.podspec | 2 +- FirebaseAnalytics.podspec | 6 +-- FirebaseAnalyticsOnDeviceConversion.podspec | 4 +- FirebaseAppCheck.podspec | 2 +- FirebaseAppCheckInterop.podspec | 2 +- FirebaseAppDistribution.podspec | 2 +- FirebaseAuth.podspec | 2 +- FirebaseAuthInterop.podspec | 2 +- FirebaseCore.podspec | 2 +- FirebaseCoreExtension.podspec | 2 +- FirebaseCoreInternal.podspec | 2 +- FirebaseCrashlytics.podspec | 2 +- FirebaseDatabase.podspec | 2 +- FirebaseDynamicLinks.podspec | 2 +- FirebaseFirestore.podspec | 4 +- FirebaseFirestoreInternal.podspec | 2 +- FirebaseFunctions.podspec | 2 +- FirebaseInAppMessaging.podspec | 2 +- FirebaseInstallations.podspec | 2 +- FirebaseMLModelDownloader.podspec | 2 +- FirebaseMessaging.podspec | 2 +- FirebaseMessagingInterop.podspec | 2 +- FirebasePerformance.podspec | 2 +- FirebaseRemoteConfig.podspec | 2 +- FirebaseRemoteConfigInterop.podspec | 2 +- FirebaseSessions.podspec | 2 +- FirebaseSharedSwift.podspec | 2 +- FirebaseStorage.podspec | 2 +- GoogleAppMeasurement.podspec | 4 +- ...leAppMeasurementOnDeviceConversion.podspec | 2 +- Package.swift | 2 +- .../FirebaseManifest/FirebaseManifest.swift | 2 +- 33 files changed, 61 insertions(+), 61 deletions(-) diff --git a/Firebase.podspec b/Firebase.podspec index 18a33a78108..dfbed0c2b8d 100644 --- a/Firebase.podspec +++ b/Firebase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'Firebase' - s.version = '10.25.0' + s.version = '10.27.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 = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.ios.dependency 'FirebaseAnalytics', '~> 10.25.0' - ss.osx.dependency 'FirebaseAnalytics', '~> 10.25.0' - ss.tvos.dependency 'FirebaseAnalytics', '~> 10.25.0' + ss.ios.dependency 'FirebaseAnalytics', '~> 10.27.0' + ss.osx.dependency 'FirebaseAnalytics', '~> 10.27.0' + ss.tvos.dependency 'FirebaseAnalytics', '~> 10.27.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'CoreOnly' do |ss| - ss.dependency 'FirebaseCore', '10.25.0' + ss.dependency 'FirebaseCore', '10.27.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 = '10.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.0' - ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.25.0' + ss.dependency 'FirebaseAnalytics/WithoutAdIdSupport', '~> 10.27.0' ss.dependency 'Firebase/CoreOnly' end s.subspec 'ABTesting' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseABTesting', '~> 10.25.0' + ss.dependency 'FirebaseABTesting', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0-beta' + ss.ios.dependency 'FirebaseAppDistribution', '~> 10.27.0-beta' ss.ios.deployment_target = '11.0' end s.subspec 'AppCheck' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseAppCheck', '~> 10.25.0' + ss.dependency 'FirebaseAppCheck', '~> 10.27.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.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', '~> 10.25.0' + ss.dependency 'FirebaseAuth', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0' + ss.dependency 'FirebaseCrashlytics', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0' + ss.dependency 'FirebaseDatabase', '~> 10.27.0' # Standard platforms PLUS watchOS 7. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0' + ss.ios.dependency 'FirebaseDynamicLinks', '~> 10.27.0' ss.ios.deployment_target = '11.0' end s.subspec 'Firestore' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseFirestore', '~> 10.25.0' + ss.dependency 'FirebaseFirestore', '~> 10.27.0' ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' ss.tvos.deployment_target = '12.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', '~> 10.25.0' + ss.dependency 'FirebaseFunctions', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0-beta' - ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.25.0-beta' + ss.ios.dependency 'FirebaseInAppMessaging', '~> 10.27.0-beta' + ss.tvos.dependency 'FirebaseInAppMessaging', '~> 10.27.0-beta' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'Installations' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseInstallations', '~> 10.25.0' + ss.dependency 'FirebaseInstallations', '~> 10.27.0' end s.subspec 'Messaging' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseMessaging', '~> 10.25.0' + ss.dependency 'FirebaseMessaging', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0-beta' + ss.dependency 'FirebaseMLModelDownloader', '~> 10.27.0-beta' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0' - ss.tvos.dependency 'FirebasePerformance', '~> 10.25.0' + ss.ios.dependency 'FirebasePerformance', '~> 10.27.0' + ss.tvos.dependency 'FirebasePerformance', '~> 10.27.0' ss.ios.deployment_target = '11.0' ss.tvos.deployment_target = '12.0' end s.subspec 'RemoteConfig' do |ss| ss.dependency 'Firebase/CoreOnly' - ss.dependency 'FirebaseRemoteConfig', '~> 10.25.0' + ss.dependency 'FirebaseRemoteConfig', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' @@ -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', '~> 10.25.0' + ss.dependency 'FirebaseStorage', '~> 10.27.0' # Standard platforms PLUS watchOS. ss.ios.deployment_target = '11.0' ss.osx.deployment_target = '10.13' diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index 15aa0f94025..46ae80fe177 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase ABTesting' s.description = <<-DESC diff --git a/FirebaseAnalytics.podspec b/FirebaseAnalytics.podspec index ff954c0ad5b..1d9c0b5bfb7 100644 --- a/FirebaseAnalytics.podspec +++ b/FirebaseAnalytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalytics' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Analytics for iOS' s.description = <<-DESC @@ -37,12 +37,12 @@ Pod::Spec.new do |s| s.default_subspecs = 'AdIdSupport' s.subspec 'AdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement', '10.25.0' + ss.dependency 'GoogleAppMeasurement', '10.27.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end s.subspec 'WithoutAdIdSupport' do |ss| - ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.25.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.27.0' ss.vendored_frameworks = 'Frameworks/FirebaseAnalytics.xcframework' end diff --git a/FirebaseAnalyticsOnDeviceConversion.podspec b/FirebaseAnalyticsOnDeviceConversion.podspec index 1ad84ef275c..03dd329904b 100644 --- a/FirebaseAnalyticsOnDeviceConversion.podspec +++ b/FirebaseAnalyticsOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAnalyticsOnDeviceConversion' - s.version = '10.25.0' + s.version = '10.27.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', '10.25.0' + s.dependency 'GoogleAppMeasurementOnDeviceConversion', '10.27.0' s.static_framework = true diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 678f2823dcf..00168979f17 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheck' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase App Check SDK.' s.description = <<-DESC diff --git a/FirebaseAppCheckInterop.podspec b/FirebaseAppCheckInterop.podspec index ea4bf003d1b..a957d166efb 100644 --- a/FirebaseAppCheckInterop.podspec +++ b/FirebaseAppCheckInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppCheckInterop' - s.version = '10.25.0' + s.version = '10.27.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 ae79c61df45..2fad9cead8d 100644 --- a/FirebaseAppDistribution.podspec +++ b/FirebaseAppDistribution.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAppDistribution' - s.version = '10.25.0-beta' + s.version = '10.27.0-beta' s.summary = 'App Distribution for Firebase iOS SDK.' s.description = <<-DESC diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index f25237ecc9c..c6446e59e82 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Apple platform client for Firebase Authentication' s.description = <<-DESC diff --git a/FirebaseAuthInterop.podspec b/FirebaseAuthInterop.podspec index 89409aaec80..c1c6b0b2d66 100644 --- a/FirebaseAuthInterop.podspec +++ b/FirebaseAuthInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuthInterop' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Interfaces that allow other Firebase SDKs to use Auth functionality.' s.description = <<-DESC diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index bb2cb898364..517ba503d23 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Core' s.description = <<-DESC diff --git a/FirebaseCoreExtension.podspec b/FirebaseCoreExtension.podspec index 4e7da0eb53c..0b1480dd9c9 100644 --- a/FirebaseCoreExtension.podspec +++ b/FirebaseCoreExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreExtension' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Extended FirebaseCore APIs for Firebase product SDKs' s.description = <<-DESC diff --git a/FirebaseCoreInternal.podspec b/FirebaseCoreInternal.podspec index 2cb42104375..10ff13f66db 100644 --- a/FirebaseCoreInternal.podspec +++ b/FirebaseCoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreInternal' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'APIs for internal FirebaseCore usage.' s.description = <<-DESC diff --git a/FirebaseCrashlytics.podspec b/FirebaseCrashlytics.podspec index 1ca34f21665..38d5a760312 100644 --- a/FirebaseCrashlytics.podspec +++ b/FirebaseCrashlytics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCrashlytics' - s.version = '10.25.0' + s.version = '10.27.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/' diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index e6e0f7a20fa..12af9ce7905 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Realtime Database' s.description = <<-DESC diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index 38ba6239639..aecb9a93884 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Dynamic Links' s.description = <<-DESC diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 20385fbf851..edeca1ced9a 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '10.25.0' + s.version = '10.27.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. @@ -37,7 +37,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.dependency 'FirebaseCore', '~> 10.0' s.dependency 'FirebaseCoreExtension', '~> 10.0' - s.dependency 'FirebaseFirestoreInternal', '10.25.0' + s.dependency 'FirebaseFirestoreInternal', '10.27.0' s.dependency 'FirebaseSharedSwift', '~> 10.0' end diff --git a/FirebaseFirestoreInternal.podspec b/FirebaseFirestoreInternal.podspec index b6a5f955b6d..0ab90c8b970 100644 --- a/FirebaseFirestoreInternal.podspec +++ b/FirebaseFirestoreInternal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreInternal' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Google Cloud Firestore' s.description = <<-DESC diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index 8e7a05bbf5b..95e1c491303 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFunctions' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Cloud Functions for Firebase' s.description = <<-DESC diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 4b16c2a94a1..4a0b346600c 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '10.25.0-beta' + s.version = '10.27.0-beta' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index 1b13896d9c2..2d136bace4b 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstallations' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Installations' s.description = <<-DESC diff --git a/FirebaseMLModelDownloader.podspec b/FirebaseMLModelDownloader.podspec index 16724bb2972..94a17627f44 100644 --- a/FirebaseMLModelDownloader.podspec +++ b/FirebaseMLModelDownloader.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMLModelDownloader' - s.version = '10.25.0-beta' + s.version = '10.27.0-beta' s.summary = 'Firebase ML Model Downloader' s.description = <<-DESC diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 8791a5509ee..a65bafc51db 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Messaging' s.description = <<-DESC diff --git a/FirebaseMessagingInterop.podspec b/FirebaseMessagingInterop.podspec index f8bbcca64f4..ce7c6ae1d21 100644 --- a/FirebaseMessagingInterop.podspec +++ b/FirebaseMessagingInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessagingInterop' - s.version = '10.25.0' + s.version = '10.27.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 7600d067e8e..8fa8c6314ba 100644 --- a/FirebasePerformance.podspec +++ b/FirebasePerformance.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebasePerformance' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Performance' s.description = <<-DESC diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 2852224a16b..c3a54d7560a 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfig' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Remote Config' s.description = <<-DESC diff --git a/FirebaseRemoteConfigInterop.podspec b/FirebaseRemoteConfigInterop.podspec index 9ddb0db4b28..b1f6059e549 100644 --- a/FirebaseRemoteConfigInterop.podspec +++ b/FirebaseRemoteConfigInterop.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseRemoteConfigInterop' - s.version = '10.25.0' + s.version = '10.27.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 9f71521aae6..623c449d14d 100644 --- a/FirebaseSessions.podspec +++ b/FirebaseSessions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSessions' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Sessions' s.description = <<-DESC diff --git a/FirebaseSharedSwift.podspec b/FirebaseSharedSwift.podspec index 37443892fce..1e0c02b9e86 100644 --- a/FirebaseSharedSwift.podspec +++ b/FirebaseSharedSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseSharedSwift' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Shared Swift Extensions for Firebase' s.description = <<-DESC diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index af5df133bb7..2bd9d3eab79 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '10.25.0' + s.version = '10.27.0' s.summary = 'Firebase Storage' s.description = <<-DESC diff --git a/GoogleAppMeasurement.podspec b/GoogleAppMeasurement.podspec index abdad07e04f..86fb40d7dbf 100644 --- a/GoogleAppMeasurement.podspec +++ b/GoogleAppMeasurement.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurement' - s.version = '10.25.0' + s.version = '10.27.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', '10.25.0' + ss.dependency 'GoogleAppMeasurement/WithoutAdIdSupport', '10.27.0' ss.vendored_frameworks = 'Frameworks/GoogleAppMeasurementIdentitySupport.xcframework' end diff --git a/GoogleAppMeasurementOnDeviceConversion.podspec b/GoogleAppMeasurementOnDeviceConversion.podspec index a834ca7b79f..5af422da6ba 100644 --- a/GoogleAppMeasurementOnDeviceConversion.podspec +++ b/GoogleAppMeasurementOnDeviceConversion.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleAppMeasurementOnDeviceConversion' - s.version = '10.25.0' + s.version = '10.27.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 f7a7b2174c7..7423e659f66 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ import class Foundation.ProcessInfo import PackageDescription -let firebaseVersion = "10.25.0" +let firebaseVersion = "10.27.0" let package = Package( name: "Firebase", diff --git a/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift b/ReleaseTooling/Sources/FirebaseManifest/FirebaseManifest.swift index 1a3d6a454a8..881d312b883 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: "10.25.0", + version: "10.27.0", pods: [ Pod("FirebaseSharedSwift"), Pod("FirebaseCoreInternal"), From 7a0145207ec66f24e4bd1ceccd4f861a3e22535b Mon Sep 17 00:00:00 2001 From: Alexander Cohen Date: Wed, 1 May 2024 17:31:54 -0400 Subject: [PATCH 005/103] Add SIGTERM support (#12881) --- .../Controllers/FIRCLSMetricKitManager.m | 2 ++ .../Crashlytics/Handlers/FIRCLSMachException.c | 2 ++ Crashlytics/Crashlytics/Handlers/FIRCLSSignal.c | 16 ++++++++++++++-- Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h | 3 ++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.m b/Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.m index 0c081df9d08..40cbcedb907 100644 --- a/Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.m +++ b/Crashlytics/Crashlytics/Controllers/FIRCLSMetricKitManager.m @@ -438,6 +438,8 @@ - (NSString *)getSignalName:(NSNumber *)signalCode { return @"SIGSYS"; case SIGTRAP: return @"SIGTRAP"; + case SIGTERM: + return @"SIGTERM"; default: return @"UNKNOWN"; } diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSMachException.c b/Crashlytics/Crashlytics/Handlers/FIRCLSMachException.c index 92bc830edae..0b71808e7c6 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSMachException.c +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSMachException.c @@ -150,6 +150,8 @@ exception_mask_t FIRCLSMachExceptionMaskForSignal(int signal) { return EXC_MASK_CRASH; case SIGFPE: return EXC_MASK_ARITHMETIC; + case SIGTERM: + return EXC_MASK_CRASH; } return 0; diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.c b/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.c index 1fcdbf22aab..0e745932034 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.c +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.c @@ -21,8 +21,17 @@ #include #if CLS_SIGNAL_SUPPORTED -static const int FIRCLSFatalSignals[FIRCLSSignalCount] = {SIGABRT, SIGBUS, SIGFPE, SIGILL, - SIGSEGV, SIGSYS, SIGTRAP}; +static const int FIRCLSFatalSignals[FIRCLSSignalCount] = { + SIGABRT, SIGBUS, SIGFPE, SIGILL, + SIGSEGV, SIGSYS, SIGTRAP, + // SIGTERM can be caught and is usually sent by iOS and variants + // when Apple wants to try and gracefully shutdown the app + // before sending a SIGKILL (which can't be caught). + // Some areas I've seen this happen are: + // - When the OS updates an app. + // - In some circumstances for Watchdog Events. + // - Resource overuse (CPU, Disk, ...). + SIGTERM}; #if CLS_USE_SIGALTSTACK static void FIRCLSSignalInstallAltStack(FIRCLSSignalReadContext *roContext); @@ -237,6 +246,9 @@ void FIRCLSSignalNameLookup(int number, int code, const char **name, const char case SIGTRAP: *name = "SIGTRAP"; break; + case SIGTERM: + *name = "SIGTERM"; + break; default: *name = "UNKNOWN"; break; diff --git a/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h b/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h index 17d14810080..e9e0478c259 100644 --- a/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h +++ b/Crashlytics/Crashlytics/Handlers/FIRCLSSignal.h @@ -30,7 +30,8 @@ #endif #if CLS_SIGNAL_SUPPORTED -#define FIRCLSSignalCount (7) +// keep in sync with the list in _FIRCLSFatalSignals_. +#define FIRCLSSignalCount (8) typedef struct { const char* path; From 477bb2a6961b18dc6210a0dafc90a975e6a448c6 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 2 May 2024 10:31:16 -0400 Subject: [PATCH 006/103] [Release] Update CHANGELOG.md for #12881 (#12885) --- Crashlytics/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Crashlytics/CHANGELOG.md b/Crashlytics/CHANGELOG.md index e12b9b00dcf..f65bacfc228 100644 --- a/Crashlytics/CHANGELOG.md +++ b/Crashlytics/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [added] Added support for catching the SIGTERM signal (#12881). + # 10.25.0 - [changed] Removed usages of user defaults API from internal Firebase Sessions dependency to eliminate required reason impact. From e8a12e04c2fa704b158905292b2ef3703d4f9ab7 Mon Sep 17 00:00:00 2001 From: Sheikh Bayazid <44797746+sheikhbayazid@users.noreply.github.com> Date: Fri, 3 May 2024 17:50:20 +0200 Subject: [PATCH 007/103] Improve/use shorthand syntax for unwrapping optionals and fix typos (#12369) --- .../tvOSSample/AuthViewController.swift | 2 +- .../NotificationService.swift | 4 +- FirebaseAnalyticsSwift/README.md | 6 +-- .../FIRAppCheckTestApp/AppDelegate.swift | 16 +++--- .../Tests/Unit/Swift/AppCheckAPITests.swift | 2 +- .../SwiftApiTests/AccountInfoTests.swift | 2 +- .../SwiftApiTests/EmailPasswordTests.swift | 2 +- .../Sample/SwiftApiTests/FacebookTests.swift | 8 +-- .../Sample/SwiftApiTests/GoogleTests.swift | 4 +- .../Sample/SwiftApiTests/TestsBase.swift | 4 +- FirebaseCombineSwift/DECISIONS.md | 6 +-- FirebaseCombineSwift/README.md | 2 +- .../Sources/Auth/Auth+Combine.swift | 52 +++++++++---------- .../Auth/GameCenterAuthProvider+Combine.swift | 4 +- .../Sources/Auth/MultiFactor+Combine.swift | 10 ++-- .../Auth/MultiFactorResolver+Combine.swift | 4 +- .../Sources/Auth/OAuthProvider+Combine.swift | 4 +- .../Auth/PhoneAuthProvider+Combine.swift | 12 ++--- .../Sources/Auth/User+Combine.swift | 16 +++--- .../CollectionReference+Combine.swift | 8 +-- .../Firestore/DocumentReference+Combine.swift | 24 ++++----- .../Sources/Firestore/Query+Combine.swift | 8 +-- .../Firestore/Transaction+Combine.swift | 2 +- .../Firestore/WriteBatch+Combine.swift | 2 +- .../Functions/HTTPSCallable+Combine.swift | 8 +-- .../Storage/StorageReference+Combine.swift | 2 +- .../Storage/StorageIntegration.swift | 2 +- .../AuthStateDidChangePublisherTests.swift | 8 +-- .../Auth/IDTokenDidChangePublisherTests.swift | 8 +-- .../Unit/Auth/SignInWithGameCenterTests.swift | 2 +- .../Tests/Unit/AuthBackend+Combine.swift | 4 +- .../HeartbeatLogging/HeartbeatStorage.swift | 2 +- .../Sources/HeartbeatLogging/Storage.swift | 4 +- .../Tests/Unit/HeartbeatStorageTests.swift | 4 +- .../DatabaseReference+WriteEncodable.swift | 2 +- .../Sources/Codable/ServerTimestamp.swift | 2 +- .../Sources/Callable+Codable.swift | 4 +- FirebaseFunctions/Sources/Functions.swift | 10 ++-- .../Sources/FunctionsError.swift | 4 +- FirebaseFunctions/Sources/HTTPSCallable.swift | 2 +- .../Sources/Internal/FunctionsContext.swift | 4 +- .../Internal/FunctionsSerializer.swift | 2 +- .../Tests/Unit/FunctionsAPITests.swift | 6 +-- .../Swift/Source/SwiftUIPreviewHelpers.swift | 2 +- .../Unit/Swift/InstallationsAPITests.swift | 4 +- .../Sources/ModelDownloader.swift | 2 +- .../Sources/ModelFileManager.swift | 2 +- .../Sources/ModelInfoRetriever.swift | 6 +-- .../Sources/TelemetryLogger.swift | 12 ++--- .../NotificationService.swift | 4 +- .../UnitTestsSwift/FIRMessagingAPITest.swift | 2 +- .../Tests/FakeConsole/FakeConsoleTests.swift | 6 +-- .../Tests/SwiftAPI/APITests.swift | 12 ++--- .../Tests/SwiftAPI/RemoteConfigConsole.swift | 6 +-- ...ransport+GoogleDataTransportProtocol.swift | 2 +- .../Installations+InstallationsProtocol.swift | 10 ++-- .../Settings/SettingsDownloadClient.swift | 4 +- .../Sources/Internal/StorageDeleteTask.swift | 2 +- .../Internal/StorageGetDownloadURLTask.swift | 4 +- .../Internal/StorageGetMetadataTask.swift | 4 +- .../Sources/Internal/StorageListTask.swift | 8 +-- .../Sources/Internal/StoragePath.swift | 2 +- .../Internal/StorageTokenAuthorizer.swift | 6 +-- .../Internal/StorageUpdateMetadataTask.swift | 4 +- .../Sources/Internal/StorageUtils.swift | 2 +- FirebaseStorage/Sources/Result.swift | 4 +- .../Sources/StorageDownloadTask.swift | 8 +-- FirebaseStorage/Sources/StorageError.swift | 2 +- .../Sources/StorageReference.swift | 8 +-- .../Sources/StorageUploadTask.swift | 8 +-- .../Tests/Integration/StorageAsyncAwait.swift | 2 +- .../Integration/StorageIntegration.swift | 2 +- .../StorageIntegrationCommon.swift | 2 +- .../Tests/Unit/StorageTestHelpers.swift | 8 +-- .../CollectionReference+AsyncAwait.swift | 4 +- .../AsyncAwait/Firestore+AsyncAwait.swift | 12 ++--- .../Swift/Source/Codable/DocumentID.swift | 4 +- .../Codable/DocumentReference+Codable.swift | 2 +- .../Swift/Source/Codable/EncoderDecoder.swift | 2 +- .../Swift/Source/Codable/ExplicitNull.swift | 2 +- .../Source/Codable/ServerTimestamp.swift | 2 +- .../PropertyWrapper/FirestoreQuery.swift | 4 +- .../FirestoreQueryObservable.swift | 4 +- .../Swift/Tests/API/BasicCompileTests.swift | 46 ++++++++-------- .../Tests/Codable/FirestoreEncoderTests.swift | 4 +- .../Integration/CodableIntegrationTests.swift | 12 ++--- 86 files changed, 266 insertions(+), 266 deletions(-) diff --git a/Example/tvOSSample/tvOSSample/AuthViewController.swift b/Example/tvOSSample/tvOSSample/AuthViewController.swift index 7809d2664b1..afcc7aae2cc 100644 --- a/Example/tvOSSample/tvOSSample/AuthViewController.swift +++ b/Example/tvOSSample/tvOSSample/AuthViewController.swift @@ -55,7 +55,7 @@ class AuthViewController: UIViewController { // MARK: - Internal Helpers private func setUserSignedIn(_ user: User?) { - if let user = user { + if let user { providers.isHidden = true signedIn.isHidden = false diff --git a/Example/watchOSSample/ServiceExtension/NotificationService.swift b/Example/watchOSSample/ServiceExtension/NotificationService.swift index 890c58e486d..95391b5b526 100644 --- a/Example/watchOSSample/ServiceExtension/NotificationService.swift +++ b/Example/watchOSSample/ServiceExtension/NotificationService.swift @@ -25,7 +25,7 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - if let bestAttemptContent = bestAttemptContent { + if let bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) 👩‍💻" @@ -39,7 +39,7 @@ class NotificationService: UNNotificationServiceExtension { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the // original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + if let contentHandler, let bestAttemptContent { contentHandler(bestAttemptContent) } } diff --git a/FirebaseAnalyticsSwift/README.md b/FirebaseAnalyticsSwift/README.md index fff3f702ad9..5c605d01774 100644 --- a/FirebaseAnalyticsSwift/README.md +++ b/FirebaseAnalyticsSwift/README.md @@ -90,11 +90,11 @@ extension View { func analyticsScreen(name: String, class screenClass: String? = nil, extraParameters: [String: Any]? = nil) -> some View { onAppear { var params: [String: Any] = [AnalyticsParameterScreenName: name] - if let screenClass = screenClass { + if let screenClass { params[AnalyticsParameterScreenClass] = screenClass } - if let extraParams = extraParameters { - params.merge(extraParams) { _, new in new } + if let extraParameters { + params.merge(extraParameters) { _, new in new } } Analytics.logEvent(AnalyticsEventScreenView, parameters: params) } diff --git a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift index 1f36beb957d..661fd9fbf0b 100644 --- a/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift +++ b/FirebaseAppCheck/Apps/FIRAppCheckTestApp/FIRAppCheckTestApp/AppDelegate.swift @@ -72,11 +72,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } DeviceCheckProvider(app: firebaseApp)?.getToken { token, error in - if let token = token { + if let token { print("DeviceCheck token: \(token.token), expiration date: \(token.expirationDate)") } - if let error = error { + if let error { print("DeviceCheck error: \((error as NSError).userInfo)") } } @@ -91,11 +91,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("Debug token: \(debugProvider.currentDebugToken())") debugProvider.getToken { token, error in - if let token = token { + if let token { print("Debug FAC token: \(token.token), expiration date: \(token.expirationDate)") } - if let error = error { + if let error { print("Debug error: \(error)") } } @@ -106,11 +106,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func requestLimitedUseToken() { AppCheck.appCheck().limitedUseToken { result, error in - if let result = result { + if let result { print("FAC limited-use token: \(result.token), expiration date: \(result.expirationDate)") } - if let error = error { + if let error { print("Error: \(String(describing: error))") } } @@ -128,11 +128,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } appAttestProvider.getToken { token, error in - if let token = token { + if let token { print("App Attest FAC token: \(token.token), expiration date: \(token.expirationDate)") } - if let error = error { + if let error { print("App Attest error: \(error)") } } diff --git a/FirebaseAppCheck/Tests/Unit/Swift/AppCheckAPITests.swift b/FirebaseAppCheck/Tests/Unit/Swift/AppCheckAPITests.swift index 4e1894501d2..053a12156fa 100644 --- a/FirebaseAppCheck/Tests/Unit/Swift/AppCheckAPITests.swift +++ b/FirebaseAppCheck/Tests/Unit/Swift/AppCheckAPITests.swift @@ -119,7 +119,7 @@ final class AppCheckAPITests { // MARK: - AppCheckErrors AppCheck.appCheck().token(forcingRefresh: false) { _, error in - if let error = error { + if let error { switch error { case AppCheckErrorCode.unknown: break diff --git a/FirebaseAuth/Tests/Sample/SwiftApiTests/AccountInfoTests.swift b/FirebaseAuth/Tests/Sample/SwiftApiTests/AccountInfoTests.swift index 882c9fb85b5..c6bcbda56c7 100644 --- a/FirebaseAuth/Tests/Sample/SwiftApiTests/AccountInfoTests.swift +++ b/FirebaseAuth/Tests/Sample/SwiftApiTests/AccountInfoTests.swift @@ -43,7 +43,7 @@ class AccountInfoTests: TestsBase { let auth = Auth.auth() let expectation1 = expectation(description: "Created account with email and password.") auth.createUser(withEmail: kOldUserEmail, password: "password") { user, error in - if let error = error { + if let error { XCTAssertEqual((error as NSError).code, AuthErrorCode.emailAlreadyInUse.rawValue, "Created a user despite it already exiting.") diff --git a/FirebaseAuth/Tests/Sample/SwiftApiTests/EmailPasswordTests.swift b/FirebaseAuth/Tests/Sample/SwiftApiTests/EmailPasswordTests.swift index 5fa78856d0f..ee34320dd9c 100644 --- a/FirebaseAuth/Tests/Sample/SwiftApiTests/EmailPasswordTests.swift +++ b/FirebaseAuth/Tests/Sample/SwiftApiTests/EmailPasswordTests.swift @@ -29,7 +29,7 @@ class EmailPasswordTests: TestsBase { let auth = Auth.auth() let expectation = self.expectation(description: "Created account with email and password.") auth.createUser(withEmail: kNewEmailToCreateUser, password: "password") { result, error in - if let error = error { + if let error { print("createUserWithEmail has error: \(error)") } expectation.fulfill() diff --git a/FirebaseAuth/Tests/Sample/SwiftApiTests/FacebookTests.swift b/FirebaseAuth/Tests/Sample/SwiftApiTests/FacebookTests.swift index f19471a4bc4..d5dd3bb6f71 100644 --- a/FirebaseAuth/Tests/Sample/SwiftApiTests/FacebookTests.swift +++ b/FirebaseAuth/Tests/Sample/SwiftApiTests/FacebookTests.swift @@ -30,7 +30,7 @@ import XCTest // let credential = FacebookAuthProvider.credential(withAccessToken: facebookAccessToken) // let expectation = self.expectation(description: "Signing in with Facebook finished.") // auth.signIn(with: credential) { result, error in -// if let error = error { +// if let error { // XCTFail("Signing in with Facebook had error: \(error)") // } else { // XCTAssertEqual(auth.currentUser?.displayName, Credentials.kFacebookUserName) @@ -69,7 +69,7 @@ import XCTest // let credential = FacebookAuthProvider.credential(withAccessToken: facebookAccessToken) // let expectation = self.expectation(description: "Facebook linking finished.") // auth.currentUser?.link(with: credential, completion: { result, error in -// if let error = error { +// if let error { // XCTFail("Link to Firebase error: \(error)") // } else { // guard let providers = (auth.currentUser?.providerData) else { @@ -124,7 +124,7 @@ import XCTest // fetcher.setRequestValue("text/plain", forHTTPHeaderField: "Content-Type") // let expectation = self.expectation(description: "Creating Facebook account finished.") // fetcher.beginFetch { data, error in -// if let error = error { +// if let error { // let error = error as NSError // if let message = String(data: error.userInfo["data"] as! Data, encoding: .utf8) { // // May get transient errors here for too many api calls when tests run frequently. @@ -181,7 +181,7 @@ import XCTest // fetcher.setRequestValue("text/plain", forHTTPHeaderField: "Content-Type") // let expectation = self.expectation(description: "Deleting Facebook account finished.") // fetcher.beginFetch { data, error in -// if let error = error { +// if let error { // XCTFail("Deleting Facebook account failed with error: \(error)") // } // expectation.fulfill() diff --git a/FirebaseAuth/Tests/Sample/SwiftApiTests/GoogleTests.swift b/FirebaseAuth/Tests/Sample/SwiftApiTests/GoogleTests.swift index 3da662b0f29..9a2925914b4 100644 --- a/FirebaseAuth/Tests/Sample/SwiftApiTests/GoogleTests.swift +++ b/FirebaseAuth/Tests/Sample/SwiftApiTests/GoogleTests.swift @@ -29,7 +29,7 @@ class GoogleTests: TestsBase { accessToken: googleAccessToken) let expectation = self.expectation(description: "Signing in with Google finished.") auth.signIn(with: credential) { result, error in - if let error = error { + if let error { print("Signing in with Google had error: \(error)") } expectation.fulfill() @@ -65,7 +65,7 @@ class GoogleTests: TestsBase { fetcher.setRequestValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let expectation = self.expectation(description: "Exchanging Google account tokens finished.") fetcher.beginFetch { data, error in - if let error = error { + if let error { XCTFail("Exchanging Google account tokens finished with error: \(error)") } else { do { diff --git a/FirebaseAuth/Tests/Sample/SwiftApiTests/TestsBase.swift b/FirebaseAuth/Tests/Sample/SwiftApiTests/TestsBase.swift index e1f0721b82a..9440f4be640 100644 --- a/FirebaseAuth/Tests/Sample/SwiftApiTests/TestsBase.swift +++ b/FirebaseAuth/Tests/Sample/SwiftApiTests/TestsBase.swift @@ -38,7 +38,7 @@ class TestsBase: XCTestCase { let expectation = self.expectation(description: "Anonymous sign-in finished.") auth.signInAnonymously { result, error in - if let error = error { + if let error { print("Anonymous sign in error: \(error)") } expectation.fulfill() @@ -59,7 +59,7 @@ class TestsBase: XCTestCase { let auth = Auth.auth() let expectation = self.expectation(description: "Delete current user finished.") auth.currentUser?.delete { error in - if let error = error { + if let error { print("Anonymous sign in error: \(error)") } expectation.fulfill() diff --git a/FirebaseCombineSwift/DECISIONS.md b/FirebaseCombineSwift/DECISIONS.md index b8932edfcc6..8d4b642b448 100644 --- a/FirebaseCombineSwift/DECISIONS.md +++ b/FirebaseCombineSwift/DECISIONS.md @@ -28,9 +28,9 @@ extension Auth { password: String) -> Future { Future { /* [weak self] <-- not required */ promise in self?.createUser(withEmail: email, password: password) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -59,4 +59,4 @@ Using the same method and parameter names for one-shot asynchronous methods resu ![image](https://user-images.githubusercontent.com/232107/99672274-76f05680-2a73-11eb-880a-3563f293de7d.png) -To achieve the same for methods that return a stream of events, we'd have to name those `addXzyListener`. This would be in contrast to Apple's naming scheme (e.g. `dataTask(with:completionHandler)` -> `dataTaskPublisher(for:)` \ No newline at end of file +To achieve the same for methods that return a stream of events, we'd have to name those `addXzyListener`. This would be in contrast to Apple's naming scheme (e.g. `dataTask(with:completionHandler)` -> `dataTaskPublisher(for:)` diff --git a/FirebaseCombineSwift/README.md b/FirebaseCombineSwift/README.md index 88038441c28..bd98eb35d81 100644 --- a/FirebaseCombineSwift/README.md +++ b/FirebaseCombineSwift/README.md @@ -78,7 +78,7 @@ In the `sign(_:didSignInFor:withError:)` method, get a Google ID token and Googl ```swift func sign(_ signIn: GIDSignIn!, didSignInFor user: GIDGoogleUser!, withError error: Error?) { // ... - if let error = error { + if let error { // ... return } diff --git a/FirebaseCombineSwift/Sources/Auth/Auth+Combine.swift b/FirebaseCombineSwift/Sources/Auth/Auth+Combine.swift index a146fd26b4e..288d1a0490b 100644 --- a/FirebaseCombineSwift/Sources/Auth/Auth+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/Auth+Combine.swift @@ -86,7 +86,7 @@ public extension Auth { func updateCurrentUser(_ user: User) -> Future { Future { promise in self.updateCurrentUser(user) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -117,9 +117,9 @@ public extension Auth { func signInAnonymously() -> Future { Future { promise in self.signInAnonymously { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -155,9 +155,9 @@ public extension Auth { password: String) -> Future { Future { promise in self.createUser(withEmail: email, password: password) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -189,9 +189,9 @@ public extension Auth { password: String) -> Future { Future { promise in self.signIn(withEmail: email, password: password) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -224,9 +224,9 @@ public extension Auth { link: String) -> Future { Future { promise in self.signIn(withEmail: email, link: link) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -249,7 +249,7 @@ public extension Auth { actionCodeSettings: ActionCodeSettings) -> Future { Future { promise in self.sendSignInLink(toEmail: email, actionCodeSettings: actionCodeSettings) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -274,9 +274,9 @@ public extension Auth { func fetchSignInMethods(forEmail email: String) -> Future<[String], Error> { Future<[String], Error> { promise in self.fetchSignInMethods(forEmail: email) { signInMethods, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let signInMethods = signInMethods { + } else if let signInMethods { promise(.success(signInMethods)) } } @@ -309,7 +309,7 @@ public extension Auth { newPassword: String) -> Future { Future { promise in self.confirmPasswordReset(withCode: code, newPassword: newPassword) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -332,9 +332,9 @@ public extension Auth { func verifyPasswordResetCode(_ code: String) -> Future { Future { promise in self.verifyPasswordResetCode(code) { email, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let email = email { + } else if let email { promise(.success(email)) } } @@ -353,9 +353,9 @@ public extension Auth { func checkActionCode(code: String) -> Future { Future { promise in self.checkActionCode(code) { actionCodeInfo, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let actionCodeInfo = actionCodeInfo { + } else if let actionCodeInfo { promise(.success(actionCodeInfo)) } } @@ -376,7 +376,7 @@ public extension Auth { func applyActionCode(code: String) -> Future { Future { promise in self.applyActionCode(code) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -405,7 +405,7 @@ public extension Auth { func sendPasswordReset(withEmail email: String) -> Future { Future { promise in self.sendPasswordReset(withEmail: email) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -448,7 +448,7 @@ public extension Auth { actionCodeSettings: ActionCodeSettings) -> Future { Future { promise in self.sendPasswordReset(withEmail: email, actionCodeSettings: actionCodeSettings) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -506,9 +506,9 @@ public extension Auth { uiDelegate: AuthUIDelegate?) -> Future { Future { promise in self.signIn(with: provider, uiDelegate: uiDelegate) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -534,9 +534,9 @@ public extension Auth { func signIn(withCustomToken token: String) -> Future { Future { promise in self.signIn(withCustomToken: token) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -583,9 +583,9 @@ public extension Auth { func signIn(with credential: AuthCredential) -> Future { Future { promise in self.signIn(with: credential) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } diff --git a/FirebaseCombineSwift/Sources/Auth/GameCenterAuthProvider+Combine.swift b/FirebaseCombineSwift/Sources/Auth/GameCenterAuthProvider+Combine.swift index 8cf2c7acbf5..95491fbc5df 100644 --- a/FirebaseCombineSwift/Sources/Auth/GameCenterAuthProvider+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/GameCenterAuthProvider+Combine.swift @@ -30,9 +30,9 @@ class func getCredential() -> Future { Future { promise in self.getCredential { authCredential, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authCredential = authCredential { + } else if let authCredential { promise(.success(authCredential)) } } diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift index 870e67c9025..4fd850da1e6 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift @@ -34,9 +34,9 @@ func getSession() -> Future { Future { promise in self.getSessionWithCompletion { session, error in - if let session = session { + if let session { promise(.success(session)) - } else if let error = error { + } else if let error { promise(.failure(error)) } } @@ -59,7 +59,7 @@ displayName: String?) -> Future { Future { promise in self.enroll(with: assertion, displayName: displayName) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -81,7 +81,7 @@ func unenroll(with factorInfo: MultiFactorInfo) -> Future { Future { promise in self.unenroll(with: factorInfo) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -101,7 +101,7 @@ func unenroll(withFactorUID factorUID: String) -> Future { Future { promise in self.unenroll(withFactorUID: factorUID) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) diff --git a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift index 4d27a90cd3d..b2ada743c66 100644 --- a/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift @@ -33,9 +33,9 @@ -> Future { Future { promise in self.resolveSignIn(with: assertion) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } diff --git a/FirebaseCombineSwift/Sources/Auth/OAuthProvider+Combine.swift b/FirebaseCombineSwift/Sources/Auth/OAuthProvider+Combine.swift index 6d14564a05e..05faa7bbd77 100644 --- a/FirebaseCombineSwift/Sources/Auth/OAuthProvider+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/OAuthProvider+Combine.swift @@ -33,9 +33,9 @@ -> Future { Future { promise in self.getCredentialWith(uiDelegate) { authCredential, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authCredential = authCredential { + } else if let authCredential { promise(.success(authCredential)) } } diff --git a/FirebaseCombineSwift/Sources/Auth/PhoneAuthProvider+Combine.swift b/FirebaseCombineSwift/Sources/Auth/PhoneAuthProvider+Combine.swift index 130734c1c63..93af631104d 100644 --- a/FirebaseCombineSwift/Sources/Auth/PhoneAuthProvider+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/PhoneAuthProvider+Combine.swift @@ -54,9 +54,9 @@ -> Future { Future { promise in self.verifyPhoneNumber(phoneNumber, uiDelegate: uiDelegate) { verificationID, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let verificationID = verificationID { + } else if let verificationID { promise(.success(verificationID)) } } @@ -99,9 +99,9 @@ uiDelegate: uiDelegate, multiFactorSession: multiFactorSession ) { verificationID, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let verificationID = verificationID { + } else if let verificationID { promise(.success(verificationID)) } } @@ -131,9 +131,9 @@ self.verifyPhoneNumber(with: phoneMultiFactorInfo, uiDelegate: uiDelegate, multiFactorSession: multiFactorSession) { verificationID, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let verificationID = verificationID { + } else if let verificationID { promise(.success(verificationID)) } } diff --git a/FirebaseCombineSwift/Sources/Auth/User+Combine.swift b/FirebaseCombineSwift/Sources/Auth/User+Combine.swift index 4dfb4fb7d53..114f0bd7130 100644 --- a/FirebaseCombineSwift/Sources/Auth/User+Combine.swift +++ b/FirebaseCombineSwift/Sources/Auth/User+Combine.swift @@ -41,9 +41,9 @@ func link(with credential: AuthCredential) -> Future { Future { promise in self.link(with: credential) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -89,9 +89,9 @@ func reauthenticate(with credential: AuthCredential) -> Future { Future { promise in self.reauthenticate(with: credential) { authDataResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let authDataResult = authDataResult { + } else if let authDataResult { promise(.success(authDataResult)) } } @@ -119,9 +119,9 @@ func unlink(fromProvider provider: String) -> Future { Future { promise in self.unlink(fromProvider: provider) { user, error in - if let user = user { + if let user { promise(.success(user)) - } else if let error = error { + } else if let error { promise(.failure(error)) } } @@ -149,7 +149,7 @@ func sendEmailVerification() -> Future { Future { promise in self.sendEmailVerification { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -188,7 +188,7 @@ -> Future { Future { promise in self.sendEmailVerification(with: actionCodeSettings) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) diff --git a/FirebaseCombineSwift/Sources/Firestore/CollectionReference+Combine.swift b/FirebaseCombineSwift/Sources/Firestore/CollectionReference+Combine.swift index 32dc0004dc7..7c6c482cb4f 100644 --- a/FirebaseCombineSwift/Sources/Firestore/CollectionReference+Combine.swift +++ b/FirebaseCombineSwift/Sources/Firestore/CollectionReference+Combine.swift @@ -40,9 +40,9 @@ var reference: DocumentReference? return Future { promise in reference = self.addDocument(data: data) { error in - if let error = error { + if let error { promise(.failure(error)) - } else if let reference = reference { + } else if let reference { promise(.success(reference)) } } @@ -70,9 +70,9 @@ return Future { promise in do { try reference = self.addDocument(from: value, encoder: encoder) { error in - if let error = error { + if let error { promise(.failure(error)) - } else if let reference = reference { + } else if let reference { promise(.success(reference)) } } diff --git a/FirebaseCombineSwift/Sources/Firestore/DocumentReference+Combine.swift b/FirebaseCombineSwift/Sources/Firestore/DocumentReference+Combine.swift index 5d853fde1c1..b9dd5568d77 100644 --- a/FirebaseCombineSwift/Sources/Firestore/DocumentReference+Combine.swift +++ b/FirebaseCombineSwift/Sources/Firestore/DocumentReference+Combine.swift @@ -39,7 +39,7 @@ func setData(_ documentData: [String: Any]) -> Future { Future { promise in self.setData(documentData) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -62,7 +62,7 @@ func setData(_ documentData: [String: Any], merge: Bool) -> Future { Future { promise in self.setData(documentData, merge: merge) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -88,7 +88,7 @@ func setData(_ documentData: [String: Any], mergeFields: [Any]) -> Future { Future { promise in self.setData(documentData, mergeFields: mergeFields) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -117,7 +117,7 @@ Future { promise in do { try self.setData(from: value, encoder: encoder) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -151,7 +151,7 @@ Future { promise in do { try self.setData(from: value, merge: merge, encoder: encoder) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -186,7 +186,7 @@ Future { promise in do { try self.setData(from: value, mergeFields: mergeFields, encoder: encoder) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -214,7 +214,7 @@ func updateData(_ documentData: [String: Any]) -> Future { Future { promise in self.updateData(documentData) { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -233,7 +233,7 @@ func delete() -> Future { Future { promise in self.delete { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) @@ -254,9 +254,9 @@ -> Future { Future { promise in self.getDocument(source: source) { snapshot, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let snapshot = snapshot { + } else if let snapshot { promise(.success(snapshot)) } } @@ -275,9 +275,9 @@ let subject = PassthroughSubject() let listenerHandle = addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in - if let error = error { + if let error { subject.send(completion: .failure(error)) - } else if let snapshot = snapshot { + } else if let snapshot { subject.send(snapshot) } } diff --git a/FirebaseCombineSwift/Sources/Firestore/Query+Combine.swift b/FirebaseCombineSwift/Sources/Firestore/Query+Combine.swift index 5ad9b8587fc..5d6477b7c45 100644 --- a/FirebaseCombineSwift/Sources/Firestore/Query+Combine.swift +++ b/FirebaseCombineSwift/Sources/Firestore/Query+Combine.swift @@ -32,9 +32,9 @@ func getDocuments(source: FirestoreSource = .default) -> Future { Future { promise in self.getDocuments(source: source) { snapshot, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let snapshot = snapshot { + } else if let snapshot { promise(.success(snapshot)) } } @@ -53,9 +53,9 @@ let subject = PassthroughSubject() let listenerHandle = addSnapshotListener(includeMetadataChanges: includeMetadataChanges) { snapshot, error in - if let error = error { + if let error { subject.send(completion: .failure(error)) - } else if let snapshot = snapshot { + } else if let snapshot { subject.send(snapshot) } } diff --git a/FirebaseCombineSwift/Sources/Firestore/Transaction+Combine.swift b/FirebaseCombineSwift/Sources/Firestore/Transaction+Combine.swift index 523f582cb71..02b584c0ee0 100644 --- a/FirebaseCombineSwift/Sources/Firestore/Transaction+Combine.swift +++ b/FirebaseCombineSwift/Sources/Firestore/Transaction+Combine.swift @@ -64,7 +64,7 @@ return nil } }) { value, error in - if let error = error { + if let error { promise(.failure(error)) } else if let value = value as? T { promise(.success(value)) diff --git a/FirebaseCombineSwift/Sources/Firestore/WriteBatch+Combine.swift b/FirebaseCombineSwift/Sources/Firestore/WriteBatch+Combine.swift index c9740fac853..87ed5ce9ec5 100644 --- a/FirebaseCombineSwift/Sources/Firestore/WriteBatch+Combine.swift +++ b/FirebaseCombineSwift/Sources/Firestore/WriteBatch+Combine.swift @@ -30,7 +30,7 @@ func commit() -> Future { Future { promise in self.commit { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(())) diff --git a/FirebaseCombineSwift/Sources/Functions/HTTPSCallable+Combine.swift b/FirebaseCombineSwift/Sources/Functions/HTTPSCallable+Combine.swift index 7841583a3a9..f30e9083f76 100644 --- a/FirebaseCombineSwift/Sources/Functions/HTTPSCallable+Combine.swift +++ b/FirebaseCombineSwift/Sources/Functions/HTTPSCallable+Combine.swift @@ -42,9 +42,9 @@ func call() -> Future { Future { promise in self.call { callableResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let callableResult = callableResult { + } else if let callableResult { promise(.success(callableResult)) } } @@ -78,9 +78,9 @@ func call(_ data: Any?) -> Future { Future { promise in self.call(data) { callableResult, error in - if let error = error { + if let error { promise(.failure(error)) - } else if let callableResult = callableResult { + } else if let callableResult { promise(.success(callableResult)) } } diff --git a/FirebaseCombineSwift/Sources/Storage/StorageReference+Combine.swift b/FirebaseCombineSwift/Sources/Storage/StorageReference+Combine.swift index 8a96e355602..9a96ae02b43 100644 --- a/FirebaseCombineSwift/Sources/Storage/StorageReference+Combine.swift +++ b/FirebaseCombineSwift/Sources/Storage/StorageReference+Combine.swift @@ -276,7 +276,7 @@ func delete() -> Future { Future { promise in self.delete { error in - if let error = error { + if let error { promise(.failure(error)) } else { promise(.success(true)) diff --git a/FirebaseCombineSwift/Tests/Integration/Storage/StorageIntegration.swift b/FirebaseCombineSwift/Tests/Integration/Storage/StorageIntegration.swift index be8482d3564..d469a9e08a5 100644 --- a/FirebaseCombineSwift/Tests/Integration/Storage/StorageIntegration.swift +++ b/FirebaseCombineSwift/Tests/Integration/Storage/StorageIntegration.swift @@ -633,7 +633,7 @@ class StorageIntegration: XCTestCase { let kFIRStorageIntegrationTestTimeout = 30.0 waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseCombineSwift/Tests/Unit/Auth/AuthStateDidChangePublisherTests.swift b/FirebaseCombineSwift/Tests/Unit/Auth/AuthStateDidChangePublisherTests.swift index d25e97be74d..ec887693697 100644 --- a/FirebaseCombineSwift/Tests/Unit/Auth/AuthStateDidChangePublisherTests.swift +++ b/FirebaseCombineSwift/Tests/Unit/Auth/AuthStateDidChangePublisherTests.swift @@ -171,7 +171,7 @@ class AuthStateDidChangePublisherTests: XCTestCase { .sink { user in print("Running on the main thread? \(Thread.isMainThread)") - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { signedInExpectation.fulfill() } } @@ -191,7 +191,7 @@ class AuthStateDidChangePublisherTests: XCTestCase { let cancellable = Auth.auth().authStateDidChangePublisher() .sink { user in - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } @@ -225,7 +225,7 @@ class AuthStateDidChangePublisherTests: XCTestCase { expect.fulfill() } } else { - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } @@ -261,7 +261,7 @@ class AuthStateDidChangePublisherTests: XCTestCase { expect.fulfill() } } else { - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } diff --git a/FirebaseCombineSwift/Tests/Unit/Auth/IDTokenDidChangePublisherTests.swift b/FirebaseCombineSwift/Tests/Unit/Auth/IDTokenDidChangePublisherTests.swift index cc532f88d90..1e96ad59592 100644 --- a/FirebaseCombineSwift/Tests/Unit/Auth/IDTokenDidChangePublisherTests.swift +++ b/FirebaseCombineSwift/Tests/Unit/Auth/IDTokenDidChangePublisherTests.swift @@ -113,7 +113,7 @@ class IDTokenDidChangePublisherTests: XCTestCase { cancellable = Auth.auth() .idTokenDidChangePublisher() .sink { user in - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } @@ -129,7 +129,7 @@ class IDTokenDidChangePublisherTests: XCTestCase { cancellable = Auth.auth() .idTokenDidChangePublisher() .sink { user in - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { print(#function) expect.fulfill() } @@ -161,7 +161,7 @@ class IDTokenDidChangePublisherTests: XCTestCase { expect.fulfill() } } else { - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } @@ -195,7 +195,7 @@ class IDTokenDidChangePublisherTests: XCTestCase { expect.fulfill() } } else { - if let user = user, user.isAnonymous { + if let user, user.isAnonymous { expect.fulfill() } } diff --git a/FirebaseCombineSwift/Tests/Unit/Auth/SignInWithGameCenterTests.swift b/FirebaseCombineSwift/Tests/Unit/Auth/SignInWithGameCenterTests.swift index a54b932eaa7..ba45270d44d 100644 --- a/FirebaseCombineSwift/Tests/Unit/Auth/SignInWithGameCenterTests.swift +++ b/FirebaseCombineSwift/Tests/Unit/Auth/SignInWithGameCenterTests.swift @@ -73,7 +73,7 @@ class SignInWithGameCenterTests: XCTestCase { body: Data?, contentType: String, completionHandler handler: @escaping FIRAuthBackendRPCIssuerCompletionHandler) { requestURL = URL - if let body = body { + if let body { requestData = body let json = try! JSONSerialization .jsonObject(with: body, options: []) as! [String: Any] diff --git a/FirebaseCombineSwift/Tests/Unit/AuthBackend+Combine.swift b/FirebaseCombineSwift/Tests/Unit/AuthBackend+Combine.swift index 51be478373f..a0bba79d644 100644 --- a/FirebaseCombineSwift/Tests/Unit/AuthBackend+Combine.swift +++ b/FirebaseCombineSwift/Tests/Unit/AuthBackend+Combine.swift @@ -22,9 +22,9 @@ extension FIRAuthBackend { -> Future { Future { promise in self.signIn(withGameCenter: request) { response, error in - if let response = response { + if let response { promise(.success(response)) - } else if let error = error { + } else if let error { promise(.failure(error)) } } diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift index 41572a0dc44..d400944d055 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift @@ -153,7 +153,7 @@ final class HeartbeatStorage: HeartbeatStorageProtocol { /// - heartbeatsBundle: The heartbeats bundle to encode and save. /// - storage: The storage container to write to. private func save(_ heartbeatsBundle: HeartbeatsBundle?, to storage: Storage) throws { - if let heartbeatsBundle = heartbeatsBundle { + if let heartbeatsBundle { let data = try heartbeatsBundle.encoded(using: encoder) try storage.write(data) } else { diff --git a/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift b/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift index 79cf1274b43..a4cac33e27a 100644 --- a/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift +++ b/FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift @@ -71,7 +71,7 @@ final class FileStorage: Storage { func write(_ data: Data?) throws { do { try createDirectories(in: url.deletingLastPathComponent()) - if let data = data { + if let data { try data.write(to: url, options: .atomic) } else { let emptyData = Data() @@ -136,7 +136,7 @@ final class UserDefaultsStorage: Storage { /// /// - Parameter data: The `Data?` to write to this object's associated defaults. func write(_ data: Data?) throws { - if let data = data { + if let data { defaults.set(data, forKey: key) } else { defaults.removeObject(forKey: key) diff --git a/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift b/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift index 51d97672e70..6cff37c608c 100644 --- a/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift +++ b/FirebaseCore/Internal/Tests/Unit/HeartbeatStorageTests.swift @@ -283,7 +283,7 @@ private class StorageFake: Storage { var onWrite: ((Data?) throws -> Void)? func read() throws -> Data { - if let onRead = onRead { + if let onRead { return try onRead() } else if let data = fakeFile { return data @@ -293,7 +293,7 @@ private class StorageFake: Storage { } func write(_ data: Data?) throws { - if let onWrite = onWrite { + if let onWrite { return try onWrite(data) } else { fakeFile = data diff --git a/FirebaseDatabase/Swift/Sources/Codable/DatabaseReference+WriteEncodable.swift b/FirebaseDatabase/Swift/Sources/Codable/DatabaseReference+WriteEncodable.swift index 529fc2e6015..094cb3e2f7c 100644 --- a/FirebaseDatabase/Swift/Sources/Codable/DatabaseReference+WriteEncodable.swift +++ b/FirebaseDatabase/Swift/Sources/Codable/DatabaseReference+WriteEncodable.swift @@ -39,7 +39,7 @@ public extension DatabaseReference { completion: ((Error?) -> Void)? = nil) throws { let encoded = try encoder.encode(value) - if let completion = completion { + if let completion { setValue(encoded, withCompletionBlock: { error, _ in completion(error) }) } else { setValue(encoded) diff --git a/FirebaseDatabase/Swift/Sources/Codable/ServerTimestamp.swift b/FirebaseDatabase/Swift/Sources/Codable/ServerTimestamp.swift index ecfd148879c..a99ba0c637e 100644 --- a/FirebaseDatabase/Swift/Sources/Codable/ServerTimestamp.swift +++ b/FirebaseDatabase/Swift/Sources/Codable/ServerTimestamp.swift @@ -59,7 +59,7 @@ public struct ServerTimestamp: Codable, Equatable, Hashable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if let value = value { + if let value { let interval = value.timeIntervalSince1970 try container.encode(Int(interval * 1000)) } else { diff --git a/FirebaseFunctions/Sources/Callable+Codable.swift b/FirebaseFunctions/Sources/Callable+Codable.swift index 5a8e508a9f0..08753e75f36 100644 --- a/FirebaseFunctions/Sources/Callable+Codable.swift +++ b/FirebaseFunctions/Sources/Callable+Codable.swift @@ -64,10 +64,10 @@ public struct Callable { callable.call(encoded) { result, error in do { - if let result = result { + if let result { let decoded = try decoder.decode(Response.self, from: result.data) completion(.success(decoded)) - } else if let error = error { + } else if let error { completion(.failure(error)) } else { completion(.failure(CallableError.internalError)) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 14eeb4e4f1b..9eb7a5d6a1e 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -347,12 +347,12 @@ enum FunctionsConstants { assert(!name.isEmpty, "Name cannot be empty") // Check if we're using the emulator - if let emulatorOrigin = emulatorOrigin { + if let emulatorOrigin { return "\(emulatorOrigin)/\(projectID)/\(region)/\(name)" } // Check the custom domain. - if let customDomain = customDomain { + if let customDomain { return "\(customDomain)/\(name)" } @@ -368,7 +368,7 @@ enum FunctionsConstants { contextProvider.getContext(options: options) { context, error in // Note: context is always non-nil since some checks could succeed, we're only failing if // there's an error. - if let error = error { + if let error { completion(.failure(error)) } else { let url = self.urlWithName(name) @@ -391,7 +391,7 @@ enum FunctionsConstants { contextProvider.getContext(options: options) { context, error in // Note: context is always non-nil since some checks could succeed, we're only failing if // there's an error. - if let error = error { + if let error { completion(.failure(error)) } else { self.callFunction(url: url, @@ -480,7 +480,7 @@ enum FunctionsConstants { localError = FunctionsErrorCode.deadlineExceeded.generatedError(userInfo: nil) } // If there was an error, report it to the user and stop. - if let localError = localError { + if let localError { completion(.failure(localError)) } else { completion(.failure(error)) diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index 8af6df9e077..9da297a9d13 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -219,7 +219,7 @@ func FunctionsErrorForResponse(status: NSInteger, var details: AnyObject? // Then look through the body for explicit details. - if let body = body, + if let body, let json = try? JSONSerialization.jsonObject(with: body) as? NSDictionary, let errorDetails = json["error"] as? NSDictionary { if let status = errorDetails["status"] as? String { @@ -252,7 +252,7 @@ func FunctionsErrorForResponse(status: NSInteger, var userInfo = [String: Any]() userInfo[NSLocalizedDescriptionKey] = description - if let details = details { + if let details { userInfo[FunctionsErrorDetailsKey] = details } return code.generatedError(userInfo: userInfo) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 8391d153236..629f3949527 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -151,7 +151,7 @@ open class HTTPSCallable: NSObject { return try await withCheckedThrowingContinuation { continuation in // TODO(bonus): Use task to handle and cancellation. self.call(data) { callableResult, error in - if let callableResult = callableResult { + if let callableResult { continuation.resume(returning: callableResult) } else { continuation.resume(throwing: error!) diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 15591f83329..e4902fbee3b 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -60,7 +60,7 @@ class FunctionsContextProvider: NSObject { var error: Error? var limitedUseAppCheckToken: String? - if let auth = auth { + if let auth { dispatchGroup.enter() auth.getToken(forcingRefresh: false) { token, authError in @@ -70,7 +70,7 @@ class FunctionsContextProvider: NSObject { } } - if let appCheck = appCheck { + if let appCheck { dispatchGroup.enter() if options?.requireLimitedUseAppCheckTokens == true { diff --git a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift index 290d9ef38ee..a883ed79f01 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsSerializer.swift @@ -99,7 +99,7 @@ class FUNSerializer: NSObject { } // Throw the internal error that popped up, if it did. - if let decodeError = decodeError { + if let decodeError { throw decodeError } return decoded diff --git a/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift b/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift index e419ae5edf9..cde13386136 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsAPITests.swift @@ -78,7 +78,7 @@ final class FunctionsAPITests: XCTestCase { let data: Any? = nil callableRef.call(data) { result, error in - if let result = result { + if let result { _ = result.data } else if let _ /* error */ = error { // ... @@ -98,7 +98,7 @@ final class FunctionsAPITests: XCTestCase { } callableRef.call { result, error in - if let result = result { + if let result { _ = result.data } else if let _ /* error */ = error { // ... @@ -120,7 +120,7 @@ final class FunctionsAPITests: XCTestCase { // MARK: - FunctionsErrorCode callableRef.call { _, error in - if let error = error { + if let error { switch (error as NSError).code { case FunctionsErrorCode.OK.rawValue: break diff --git a/FirebaseInAppMessaging/Swift/Source/SwiftUIPreviewHelpers.swift b/FirebaseInAppMessaging/Swift/Source/SwiftUIPreviewHelpers.swift index 3752429b6b0..9b64490fb47 100644 --- a/FirebaseInAppMessaging/Swift/Source/SwiftUIPreviewHelpers.swift +++ b/FirebaseInAppMessaging/Swift/Source/SwiftUIPreviewHelpers.swift @@ -96,7 +96,7 @@ public enum InAppMessagingPreviewHelpers { } var actionButton: InAppMessagingActionButton? - if let buttonText = buttonText, + if let buttonText, let buttonTextColor = buttonTextColor, let buttonBackgroundColor = buttonBackgroundColor { actionButton = InAppMessagingActionButton(buttonText: buttonText, diff --git a/FirebaseInstallations/Source/Tests/Unit/Swift/InstallationsAPITests.swift b/FirebaseInstallations/Source/Tests/Unit/Swift/InstallationsAPITests.swift index 51d1ae8f713..01ace9a2afb 100644 --- a/FirebaseInstallations/Source/Tests/Unit/Swift/InstallationsAPITests.swift +++ b/FirebaseInstallations/Source/Tests/Unit/Swift/InstallationsAPITests.swift @@ -131,7 +131,7 @@ final class InstallationsAPITests { // MARK: - InstallationsAuthTokenResult Installations.installations().authToken { result, _ in - if let result = result { + if let result { _ = result.expirationDate _ = result.authToken } @@ -140,7 +140,7 @@ final class InstallationsAPITests { // MARK: - InstallationsErrorCode Installations.installations().authToken { _, error in - if let error = error { + if let error { // Old error handling. switch (error as NSError).code { case Int(InstallationsErrorCode.unknown.rawValue): diff --git a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift b/FirebaseMLModelDownloader/Sources/ModelDownloader.swift index ba5e047757b..fb99d831209 100644 --- a/FirebaseMLModelDownloader/Sources/ModelDownloader.swift +++ b/FirebaseMLModelDownloader/Sources/ModelDownloader.swift @@ -438,7 +438,7 @@ extension ModelDownloader { case let .modelInfo(remoteModelInfo): // Progress handler for model file download. let taskProgressHandler: ModelDownloadTask.ProgressHandler = { progress in - if let progressHandler = progressHandler { + if let progressHandler { self.asyncOnMainQueue(progressHandler(progress)) } } diff --git a/FirebaseMLModelDownloader/Sources/ModelFileManager.swift b/FirebaseMLModelDownloader/Sources/ModelFileManager.swift index e1f24e47db6..8e9acdef84a 100644 --- a/FirebaseMLModelDownloader/Sources/ModelFileManager.swift +++ b/FirebaseMLModelDownloader/Sources/ModelFileManager.swift @@ -195,7 +195,7 @@ extension ModelFileManager { } static let availableStorage = { (error: String?) -> String in - if let error = error { + if let error { return "Failed to check storage capacity on device: \(error)" } else { return "Failed to check storage capacity on device." diff --git a/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift b/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift index 20a8c7ef7dd..afaa58e7a7e 100644 --- a/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift +++ b/FirebaseMLModelDownloader/Sources/ModelInfoRetriever.swift @@ -500,9 +500,9 @@ extension ModelInfoRetriever { request.setValue(bundleID, forHTTPHeaderField: ModelInfoRetriever.bundleIDHTTPHeader) request.setValue(token, forHTTPHeaderField: ModelInfoRetriever.fisTokenHTTPHeader) // Get model hash if local model info is available on device. - if let modelInfo = localModelInfo { + if let localModelInfo { request.setValue( - modelInfo.modelHash, + localModelInfo.modelHash, forHTTPHeaderField: ModelInfoRetriever.hashMatchHTTPHeader ) } @@ -511,7 +511,7 @@ extension ModelInfoRetriever { /// Parse error message from server response. private func getErrorFromResponse(_ data: Data?) -> String? { - if let data = data, + if let data, let responseJSON = try? JSONSerialization .jsonObject(with: data, options: []) as? [String: Any], let error = responseJSON["error"] as? [String: Any], diff --git a/FirebaseMLModelDownloader/Sources/TelemetryLogger.swift b/FirebaseMLModelDownloader/Sources/TelemetryLogger.swift index 23386d06bb1..79b91c8e2d2 100644 --- a/FirebaseMLModelDownloader/Sources/TelemetryLogger.swift +++ b/FirebaseMLModelDownloader/Sources/TelemetryLogger.swift @@ -63,14 +63,14 @@ extension ModelDownloadLogEvent { downloadFailureStatus: Int64? = 0, modelOptions: ModelOptions) { downloadStatus = status self.errorCode = errorCode - if let roughDuration = roughDownloadDuration { - roughDownloadDurationMs = roughDuration + if let roughDownloadDuration { + roughDownloadDurationMs = roughDownloadDuration } - if let exactDuration = exactDownloadDuration { - exactDownloadDurationMs = exactDuration + if let exactDownloadDuration { + exactDownloadDurationMs = exactDownloadDuration } - if let failureStatus = downloadFailureStatus { - self.downloadFailureStatus = failureStatus + if let downloadFailureStatus { + self.downloadFailureStatus = downloadFailureStatus } options = modelOptions } diff --git a/FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift b/FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift index bf48feb3b29..a6bc72e483b 100644 --- a/FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift +++ b/FirebaseMessaging/Apps/AdvancedSample/NotificationServiceExtension/NotificationService.swift @@ -25,7 +25,7 @@ class NotificationService: UNNotificationServiceExtension { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - if let bestAttemptContent = bestAttemptContent { + if let bestAttemptContent { // Modify the notification content here... bestAttemptContent.title = "\(bestAttemptContent.title) 👩🏻‍💻" @@ -43,7 +43,7 @@ class NotificationService: UNNotificationServiceExtension { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the // original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + if let contentHandler, let bestAttemptContent { contentHandler(bestAttemptContent) } } diff --git a/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift b/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift index f48e663bd7f..66441a37ae9 100644 --- a/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift +++ b/FirebaseMessaging/Tests/UnitTestsSwift/FIRMessagingAPITest.swift @@ -94,7 +94,7 @@ func apis() { messaging.subscribe(toTopic: topic) messaging.unsubscribe(fromTopic: topic) messaging.unsubscribe(fromTopic: topic, completion: { error in - if let error = error { + if let error { switch error { // Handle errors in the new format. case MessagingError.timeout: diff --git a/FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift b/FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift index 73944fb9e9b..9f0bf1c188b 100644 --- a/FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift +++ b/FirebaseRemoteConfigSwift/Tests/FakeConsole/FakeConsoleTests.swift @@ -28,7 +28,7 @@ class FakeConsoleTests: APITestBase { func testChangedActivateWillNotFlag() { let expectation = self.expectation(description: #function) config.fetch { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -46,7 +46,7 @@ class FakeConsoleTests: APITestBase { let expectation2 = self.expectation(description: #function + "2") config.fetch { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -64,7 +64,7 @@ class FakeConsoleTests: APITestBase { let kFIRStorageIntegrationTestTimeout = 10.0 waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift index d1b0b4a0489..7bf81c97a37 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/APITests.swift @@ -21,7 +21,7 @@ class APITests: APITestBase { func testFetchThenActivate() { let expectation = self.expectation(description: #function) config.fetch { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -37,7 +37,7 @@ class APITests: APITestBase { func testFetchWithExpirationThenActivate() { let expectation = self.expectation(description: #function) config.fetch(withExpirationDuration: 0) { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -54,7 +54,7 @@ class APITests: APITestBase { let expectation = self.expectation(description: #function) config.fetchAndActivate { status, error in XCTAssertEqual(status, .successFetchedFromRemote) - if let error = error { + if let error { XCTFail("Fetch and Activate Error \(error)") } XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) @@ -68,7 +68,7 @@ class APITests: APITestBase { func testUnchangedActivateWillFlag() { let expectation = self.expectation(description: #function) config.fetch { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -82,7 +82,7 @@ class APITests: APITestBase { waitForExpectations() let expectation2 = self.expectation(description: #function + "2") config.fetch { status, error in - if let error = error { + if let error { XCTFail("Fetch Error \(error)") } XCTAssertEqual(status, RemoteConfigFetchStatus.success) @@ -289,7 +289,7 @@ class APITests: APITestBase { let kTestTimeout = 10.0 waitForExpectations(timeout: kTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift index b4be53d8340..0e4df6e6e05 100644 --- a/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift +++ b/FirebaseRemoteConfigSwift/Tests/SwiftAPI/RemoteConfigConsole.swift @@ -46,7 +46,7 @@ class RemoteConfigConsole { perform(configRequest: .get) { latestConfigJSON in config = latestConfigJSON } - if let config = config { + if let config { saveConfig(config) } return config @@ -210,8 +210,8 @@ class RemoteConfigConsole { /// Perform a synchronous sync with remote config console. private func syncWithConsole() { - if let consoleConfig = activeRemoteConfig { - latestConfig = consoleConfig + if let activeRemoteConfig { + latestConfig = activeRemoteConfig } else { fatalError("Could not sync with console.") } diff --git a/FirebaseSessions/Sources/GoogleDataTransport+GoogleDataTransportProtocol.swift b/FirebaseSessions/Sources/GoogleDataTransport+GoogleDataTransportProtocol.swift index ebc15e9f423..9c04f9c9ba2 100644 --- a/FirebaseSessions/Sources/GoogleDataTransport+GoogleDataTransportProtocol.swift +++ b/FirebaseSessions/Sources/GoogleDataTransport+GoogleDataTransportProtocol.swift @@ -29,7 +29,7 @@ protocol GoogleDataTransportProtocol { extension GDTCORTransport: GoogleDataTransportProtocol { func logGDTEvent(event: GDTCOREvent, completion: @escaping (Result) -> Void) { sendDataEvent(event) { wasWritten, error in - if let error = error { + if let error { completion(.failure(error)) } else if !wasWritten { completion(.failure(GoogleDataTransportProtocolErrors.writeFailure)) diff --git a/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift b/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift index fcbf1231d6c..38ca1eafa9b 100644 --- a/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift +++ b/FirebaseSessions/Sources/Installations+InstallationsProtocol.swift @@ -50,7 +50,7 @@ extension InstallationsProtocol { workingGroup.enter() installationID { (installationID: String?, error: Error?) in - if let installationID = installationID { + if let installationID { intallationComplete = installationID } else if let error = error { errorComplete = error @@ -67,10 +67,10 @@ extension InstallationsProtocol { completion(.failure(FirebaseSessionsError.SessionInstallationsTimeOutError)) return default: - if let installationID = intallationComplete { - completion(.success((installationID, authTokenComplete))) - } else if let error = errorComplete { - completion(.failure(error)) + if let intallationComplete { + completion(.success((intallationComplete, authTokenComplete))) + } else if let errorComplete { + completion(.failure(errorComplete)) } } } diff --git a/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift b/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift index aaea90941dc..1d2a95a3cc5 100644 --- a/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift +++ b/FirebaseSessions/Sources/Settings/SettingsDownloadClient.swift @@ -56,7 +56,7 @@ class SettingsDownloader: SettingsDownloadClient { case let .success(installationsInfo): let request = self.buildRequest(url: validURL, fiid: installationsInfo.0) let task = URLSession.shared.dataTask(with: request) { data, response, error in - if let data = data { + if let data { if let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { completion(.success(dict)) } else { @@ -64,7 +64,7 @@ class SettingsDownloader: SettingsDownloadClient { .JSONParseError("Failed to parse JSON to dictionary") )) } - } else if let error = error { + } else if let error { completion(.failure(.URLSessionError(error.localizedDescription))) } } diff --git a/FirebaseStorage/Sources/Internal/StorageDeleteTask.swift b/FirebaseStorage/Sources/Internal/StorageDeleteTask.swift index 086e8facf14..04891ef5909 100644 --- a/FirebaseStorage/Sources/Internal/StorageDeleteTask.swift +++ b/FirebaseStorage/Sources/Internal/StorageDeleteTask.swift @@ -63,7 +63,7 @@ class StorageDeleteTask: StorageTask, StorageTaskManagement { self.fetcherCompletion = { [weak self] (data: Data?, error: NSError?) in guard let self = self else { return } - if let error = error, self.error == nil { + if let error, self.error == nil { self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) } self.taskCompletion?(self.error) diff --git a/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift b/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift index 72b50a11220..2cca8acdca2 100644 --- a/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift +++ b/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift @@ -64,12 +64,12 @@ class StorageGetDownloadURLTask: StorageTask, StorageTaskManagement { self.fetcherCompletion = { [weak self] (data: Data?, error: NSError?) in guard let self = self else { return } var downloadURL: URL? - if let error = error { + if let error { if self.error == nil { self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) } } else { - if let data = data, + if let data, let responseDictionary = try? JSONSerialization .jsonObject(with: data) as? [String: Any] { downloadURL = self.downloadURLFromMetadataDictionary(responseDictionary) diff --git a/FirebaseStorage/Sources/Internal/StorageGetMetadataTask.swift b/FirebaseStorage/Sources/Internal/StorageGetMetadataTask.swift index 40f65a7b5d8..5202394631a 100644 --- a/FirebaseStorage/Sources/Internal/StorageGetMetadataTask.swift +++ b/FirebaseStorage/Sources/Internal/StorageGetMetadataTask.swift @@ -65,12 +65,12 @@ class StorageGetMetadataTask: StorageTask, StorageTaskManagement { self.fetcherCompletion = { [weak self] (data: Data?, error: NSError?) in guard let self = self else { return } var metadata: StorageMetadata? - if let error = error { + if let error { if self.error == nil { self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) } } else { - if let data = data, + if let data, let responseDictionary = try? JSONSerialization .jsonObject(with: data) as? [String: AnyHashable] { metadata = StorageMetadata(dictionary: responseDictionary) diff --git a/FirebaseStorage/Sources/Internal/StorageListTask.swift b/FirebaseStorage/Sources/Internal/StorageListTask.swift index 94c872aae28..10b22d95b96 100644 --- a/FirebaseStorage/Sources/Internal/StorageListTask.swift +++ b/FirebaseStorage/Sources/Internal/StorageListTask.swift @@ -92,11 +92,11 @@ class StorageListTask: StorageTask, StorageTaskManagement { // items // to return per page. This removes the need to backfill results if Firebase Storage filters // objects that are considered invalid (such as items with two consecutive slashes). - if let pageSize = self.pageSize { + if let pageSize { queryParams["maxResults"] = "\(pageSize)" } - if let previousPageToken = self.previousPageToken { + if let previousPageToken { queryParams["pageToken"] = previousPageToken } @@ -116,10 +116,10 @@ class StorageListTask: StorageTask, StorageTaskManagement { self.fetcherCompletion = { [weak self] (data: Data?, error: NSError?) in guard let self = self else { return } var listResult: StorageListResult? - if let error = error, self.error == nil { + if let error, self.error == nil { self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) } else { - if let data = data, + if let data, let responseDictionary = try? JSONSerialization .jsonObject(with: data) as? [String: Any] { listResult = StorageListResult(with: responseDictionary, reference: self.reference) diff --git a/FirebaseStorage/Sources/Internal/StoragePath.swift b/FirebaseStorage/Sources/Internal/StoragePath.swift index d094771eca3..d27149af40e 100644 --- a/FirebaseStorage/Sources/Internal/StoragePath.swift +++ b/FirebaseStorage/Sources/Internal/StoragePath.swift @@ -135,7 +135,7 @@ class StoragePath: NSCopying, Equatable { init(with bucket: String, object: String? = nil) { self.bucket = bucket - if let object = object { + if let object { self.object = StoragePath.standardizedPathForString(object) } else { self.object = nil diff --git a/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift b/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift index f44ae26e793..217f8775116 100644 --- a/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift +++ b/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift @@ -38,7 +38,7 @@ class StorageTokenAuthorizer: NSObject, GTMSessionFetcherAuthorizer { var tokenError: NSError? let callbackQueue = fetcherService.callbackQueue ?? DispatchQueue.main let fetchTokenGroup = DispatchGroup() - if let auth = auth { + if let auth { fetchTokenGroup.enter() auth.getToken(forcingRefresh: false) { token, error in if let error = error as? NSError { @@ -51,14 +51,14 @@ class StorageTokenAuthorizer: NSObject, GTMSessionFetcherAuthorizer { tokenError = NSError(domain: "FIRStorageErrorDomain", code: StorageErrorCode.unauthenticated.rawValue, userInfo: errorDictionary) - } else if let token = token { + } else if let token { let firebaseToken = "Firebase \(token)" request?.setValue(firebaseToken, forHTTPHeaderField: "Authorization") } fetchTokenGroup.leave() } } - if let appCheck = appCheck { + if let appCheck { fetchTokenGroup.enter() appCheck.getToken(forcingRefresh: false) { tokenResult in request?.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") diff --git a/FirebaseStorage/Sources/Internal/StorageUpdateMetadataTask.swift b/FirebaseStorage/Sources/Internal/StorageUpdateMetadataTask.swift index bc75c48e65c..dad1dcb4886 100644 --- a/FirebaseStorage/Sources/Internal/StorageUpdateMetadataTask.swift +++ b/FirebaseStorage/Sources/Internal/StorageUpdateMetadataTask.swift @@ -73,12 +73,12 @@ class StorageUpdateMetadataTask: StorageTask, StorageTaskManagement { self.fetcherCompletion = { [weak self] (data: Data?, error: NSError?) in guard let self = self else { return } var metadata: StorageMetadata? - if let error = error { + if let error { if self.error == nil { self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) } } else { - if let data = data, + if let data, let responseDictionary = try? JSONSerialization .jsonObject(with: data) as? [String: AnyHashable] { metadata = StorageMetadata(dictionary: responseDictionary) diff --git a/FirebaseStorage/Sources/Internal/StorageUtils.swift b/FirebaseStorage/Sources/Internal/StorageUtils.swift index 457a70aa4ec..1c0ea561eb0 100644 --- a/FirebaseStorage/Sources/Internal/StorageUtils.swift +++ b/FirebaseStorage/Sources/Internal/StorageUtils.swift @@ -38,7 +38,7 @@ class StorageUtils { components.host = reference.storage.host components.port = reference.storage.port - if let queryParams = queryParams { + if let queryParams { var queryItems = [URLQueryItem]() for (key, value) in queryParams { queryItems.append(URLQueryItem(name: key, value: value)) diff --git a/FirebaseStorage/Sources/Result.swift b/FirebaseStorage/Sources/Result.swift index d1260fe57f9..931794cbfb1 100644 --- a/FirebaseStorage/Sources/Result.swift +++ b/FirebaseStorage/Sources/Result.swift @@ -26,9 +26,9 @@ private func getResultCallback(completion: @escaping (Result) -> Vo _: Error?) -> Void { return { (value: T?, error: Error?) in - if let value = value { + if let value { completion(.success(value)) - } else if let error = error { + } else if let error { completion(.failure(StorageError.swiftConvert(objcError: error as NSError))) } else { completion(.failure(StorageError.internalError("Internal failure in getResultCallback"))) diff --git a/FirebaseStorage/Sources/StorageDownloadTask.swift b/FirebaseStorage/Sources/StorageDownloadTask.swift index 62217182c0e..8f9f0332393 100644 --- a/FirebaseStorage/Sources/StorageDownloadTask.swift +++ b/FirebaseStorage/Sources/StorageDownloadTask.swift @@ -116,7 +116,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement { request.url = components?.url var fetcher: GTMSessionFetcher - if let resumeData = resumeData { + if let resumeData { fetcher = GTMSessionFetcher(downloadResumeData: resumeData) fetcher.comment = "Resuming DownloadTask" } else { @@ -125,7 +125,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement { } fetcher.maxRetryInterval = self.reference.storage.maxDownloadRetryInterval - if let fileURL = self.fileURL { + if let fileURL { // Handle file downloads fetcher.destinationFileURL = fileURL fetcher.downloadProgressBlock = { [weak self] (bytesWritten: Int64, @@ -163,7 +163,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement { self.fire(for: .progress, snapshot: self.snapshot) // Handle potential issues with download - if let error = error { + if let error { self.state = .failed self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) self.fire(for: .failure, snapshot: self.snapshot) @@ -171,7 +171,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement { } // Download completed successfully, fire completion callbacks self.state = .success - if let data = data { + if let data { self.downloadData = data } self.fire(for: .success, snapshot: self.snapshot) diff --git a/FirebaseStorage/Sources/StorageError.swift b/FirebaseStorage/Sources/StorageError.swift index 866e07a32cf..68543f50aba 100644 --- a/FirebaseStorage/Sources/StorageError.swift +++ b/FirebaseStorage/Sources/StorageError.swift @@ -76,7 +76,7 @@ public let StorageErrorDomain: String = "FIRStorageErrorDomain" */ static func error(withInvalidRequest request: Data?) -> NSError { var requestString: String - if let request = request { + if let request { requestString = String(data: request, encoding: .utf8) ?? "" } else { requestString = "" diff --git a/FirebaseStorage/Sources/StorageReference.swift b/FirebaseStorage/Sources/StorageReference.swift index d3d45d6e2df..92a547b3f14 100644 --- a/FirebaseStorage/Sources/StorageReference.swift +++ b/FirebaseStorage/Sources/StorageReference.swift @@ -286,7 +286,7 @@ import Foundation queue: storage.dispatchQueue, file: fileURL) - if let completion = completion { + if let completion { task.completionURL = completion let callbackQueue = fetcherService.callbackQueue ?? DispatchQueue.main @@ -330,7 +330,7 @@ import Foundation var paginatedCompletion: ((_: StorageListResult?, _: Error?) -> Void)? paginatedCompletion = { (_ listResult: StorageListResult?, _ error: Error?) in - if let error = error { + if let error { completion(nil, error) return } @@ -531,7 +531,7 @@ import Foundation open func delete() async throws { return try await withCheckedThrowingContinuation { continuation in self.delete { error in - if let error = error { + if let error { continuation.resume(throwing: error) } else { continuation.resume() @@ -595,7 +595,7 @@ import Foundation private func startAndObserveUploadTask(task: StorageUploadTask, completion: ((_: StorageMetadata?, _: Error?) -> Void)?) { - if let completion = completion { + if let completion { task.completionMetadata = completion let callbackQueue = storage.fetcherServiceForApp.callbackQueue ?? DispatchQueue.main diff --git a/FirebaseStorage/Sources/StorageUploadTask.swift b/FirebaseStorage/Sources/StorageUploadTask.swift index 0d1e904661f..8c99e8c4a28 100644 --- a/FirebaseStorage/Sources/StorageUploadTask.swift +++ b/FirebaseStorage/Sources/StorageUploadTask.swift @@ -87,10 +87,10 @@ import Foundation chunkSize: self.reference.storage.uploadChunkSizeBytes, fetcherService: self.fetcherService ) - if let data = self.uploadData { - uploadFetcher.uploadData = data + if let uploadData { + uploadFetcher.uploadData = uploadData uploadFetcher.comment = "Data UploadTask" - } else if let fileURL = self.fileURL { + } else if let fileURL { uploadFetcher.uploadFileURL = fileURL uploadFetcher.comment = "File UploadTask" @@ -120,7 +120,7 @@ import Foundation self.fire(for: .progress, snapshot: self.snapshot) // Handle potential issues with upload - if let error = error { + if let error { self.state = .failed self.error = StorageErrorCode.error(withServerError: error, ref: self.reference) self.metadata = self.uploadMetadata diff --git a/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift b/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift index fafdb21906c..f8729a5b675 100644 --- a/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift +++ b/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift @@ -413,7 +413,7 @@ class StorageAsyncAwait: StorageIntegrationCommon { let kTestTimeout = 60.0 waitForExpectations(timeout: kTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseStorage/Tests/Integration/StorageIntegration.swift b/FirebaseStorage/Tests/Integration/StorageIntegration.swift index 980ab243108..2f407dc7427 100644 --- a/FirebaseStorage/Tests/Integration/StorageIntegration.swift +++ b/FirebaseStorage/Tests/Integration/StorageIntegration.swift @@ -847,7 +847,7 @@ class StorageResultTests: StorageIntegrationCommon { let kFIRStorageIntegrationTestTimeout = 100.0 waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseStorage/Tests/Integration/StorageIntegrationCommon.swift b/FirebaseStorage/Tests/Integration/StorageIntegrationCommon.swift index 47c4fb6f37c..2cc09981167 100644 --- a/FirebaseStorage/Tests/Integration/StorageIntegrationCommon.swift +++ b/FirebaseStorage/Tests/Integration/StorageIntegrationCommon.swift @@ -101,7 +101,7 @@ class StorageIntegrationCommon: XCTestCase { let kTestTimeout = 60.0 waitForExpectations(timeout: kTestTimeout, handler: { error in - if let error = error { + if let error { print(error) } }) diff --git a/FirebaseStorage/Tests/Unit/StorageTestHelpers.swift b/FirebaseStorage/Tests/Unit/StorageTestHelpers.swift index 737be5e6acf..9d6b906d0f1 100644 --- a/FirebaseStorage/Tests/Unit/StorageTestHelpers.swift +++ b/FirebaseStorage/Tests/Unit/StorageTestHelpers.swift @@ -45,7 +45,7 @@ class StorageTestHelpers: XCTestCase { func waitForExpectation(test: XCTest) { waitForExpectations(timeout: 10) { error in - if let error = error { + if let error { print("Error \(error)") } } @@ -54,7 +54,7 @@ class StorageTestHelpers: XCTestCase { func successBlock(withMetadata metadata: StorageMetadata? = nil) -> GTMSessionFetcherTestBlock { var data: Data? - if let metadata = metadata { + if let metadata { data = try? JSONSerialization.data(withJSONObject: metadata.dictionaryRepresentation()) } return block(forData: data, url: nil, statusCode: 200) @@ -99,7 +99,7 @@ class StorageTestHelpers: XCTestCase { statusCode code: Int) -> GTMSessionFetcherTestBlock { let block = { (fetcher: GTMSessionFetcher, response: GTMSessionFetcherTestResponse) in let fetcherURL = fetcher.request?.url! - if let url = url { + if let url { XCTAssertEqual(url, fetcherURL) } let httpResponse = HTTPURLResponse( @@ -111,7 +111,7 @@ class StorageTestHelpers: XCTestCase { var error: NSError? if code >= 400 { var userInfo: [String: Any]? - if let data = data { + if let data { userInfo = ["data": data] } error = NSError(domain: "com.google.HTTPStatus", code: code, userInfo: userInfo) diff --git a/Firestore/Swift/Source/AsyncAwait/CollectionReference+AsyncAwait.swift b/Firestore/Swift/Source/AsyncAwait/CollectionReference+AsyncAwait.swift index 1ea3bc3b467..7fb8e307e03 100644 --- a/Firestore/Swift/Source/AsyncAwait/CollectionReference+AsyncAwait.swift +++ b/Firestore/Swift/Source/AsyncAwait/CollectionReference+AsyncAwait.swift @@ -33,8 +33,8 @@ public extension CollectionReference { return try await withCheckedThrowingContinuation { continuation in var document: DocumentReference? document = self.addDocument(data: data) { error in - if let err = error { - continuation.resume(throwing: err) + if let error { + continuation.resume(throwing: error) } else { // Our callbacks guarantee that we either return an error or a document. continuation.resume(returning: document!) diff --git a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift index eaf29e42937..e85ca9a9791 100644 --- a/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift +++ b/Firestore/Swift/Source/AsyncAwait/Firestore+AsyncAwait.swift @@ -31,8 +31,8 @@ public extension Firestore { func loadBundle(_ bundleData: Data) async throws -> LoadBundleTaskProgress { return try await withCheckedThrowingContinuation { continuation in self.loadBundle(bundleData) { progress, error in - if let err = error { - continuation.resume(throwing: err) + if let error { + continuation.resume(throwing: error) } else { // Our callbacks guarantee that we either return an error or a progress event. continuation.resume(returning: progress!) @@ -49,8 +49,8 @@ public extension Firestore { func loadBundle(_ bundleStream: InputStream) async throws -> LoadBundleTaskProgress { return try await withCheckedThrowingContinuation { continuation in self.loadBundle(bundleStream) { progress, error in - if let err = error { - continuation.resume(throwing: err) + if let error { + continuation.resume(throwing: error) } else { // Our callbacks guarantee that we either return an error or a progress event. continuation.resume(returning: progress!) @@ -107,8 +107,8 @@ public extension Firestore { // See https://github.com/firebase/firebase-ios-sdk/issues/9426 for more details. return try await withCheckedThrowingContinuation { continuation in self.runTransaction(updateBlock) { anyValue, error in - if let err = error { - continuation.resume(throwing: err) + if let error { + continuation.resume(throwing: error) } else { continuation.resume(returning: anyValue) } diff --git a/Firestore/Swift/Source/Codable/DocumentID.swift b/Firestore/Swift/Source/Codable/DocumentID.swift index 1196b0a91bf..9ec6dbb4a9d 100644 --- a/Firestore/Swift/Source/Codable/DocumentID.swift +++ b/Firestore/Swift/Source/Codable/DocumentID.swift @@ -113,7 +113,7 @@ public struct DocumentID: private var value: Value? = nil public init(wrappedValue value: Value?) { - if let value = value { + if let value { logIgnoredValueWarning(value: value) } self.value = value @@ -141,7 +141,7 @@ public struct DocumentID: extension DocumentID: DocumentIDProtocol { init(from documentReference: DocumentReference?) throws { - if let documentReference = documentReference { + if let documentReference { value = try Value.wrap(documentReference) } else { value = nil diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index 59563cf11a9..574b5417dab 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -33,7 +33,7 @@ private protocol CodableDocumentReference: Codable {} /** - * DocumentReference's codable implmentation will just throw for most + * DocumentReference's codable implementation will just throw for most * encoder/decoder however. It is only meant to be encoded by Firestore.Encoder/Firestore.Decoder. */ extension CodableDocumentReference { diff --git a/Firestore/Swift/Source/Codable/EncoderDecoder.swift b/Firestore/Swift/Source/Codable/EncoderDecoder.swift index 43c3b71f378..29b0e57caab 100644 --- a/Firestore/Swift/Source/Codable/EncoderDecoder.swift +++ b/Firestore/Swift/Source/Codable/EncoderDecoder.swift @@ -93,7 +93,7 @@ public extension Firestore { public func decode(_ t: T.Type, from data: Any, in reference: DocumentReference?) throws -> T { - if let reference = reference { + if let reference { userInfo[CodingUserInfoKey.documentRefUserInfoKey] = reference } return try decode(T.self, from: data) diff --git a/Firestore/Swift/Source/Codable/ExplicitNull.swift b/Firestore/Swift/Source/Codable/ExplicitNull.swift index a8590d34f05..615fab9698a 100644 --- a/Firestore/Swift/Source/Codable/ExplicitNull.swift +++ b/Firestore/Swift/Source/Codable/ExplicitNull.swift @@ -47,7 +47,7 @@ extension ExplicitNull: Hashable where Value: Hashable {} extension ExplicitNull: Encodable where Value: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if let value = value { + if let value { try container.encode(value) } else { try container.encodeNil() diff --git a/Firestore/Swift/Source/Codable/ServerTimestamp.swift b/Firestore/Swift/Source/Codable/ServerTimestamp.swift index 94857811a00..ed95fef97b2 100644 --- a/Firestore/Swift/Source/Codable/ServerTimestamp.swift +++ b/Firestore/Swift/Source/Codable/ServerTimestamp.swift @@ -98,7 +98,7 @@ public struct ServerTimestamp: Codable public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if let value = value { + if let value { try container.encode(Value.unwrap(value)) } else { try container.encode(FieldValue.serverTimestamp()) diff --git a/Firestore/Swift/Source/PropertyWrapper/FirestoreQuery.swift b/Firestore/Swift/Source/PropertyWrapper/FirestoreQuery.swift index da0f0a842d7..de3716856bc 100644 --- a/Firestore/Swift/Source/PropertyWrapper/FirestoreQuery.swift +++ b/Firestore/Swift/Source/PropertyWrapper/FirestoreQuery.swift @@ -55,7 +55,7 @@ public enum DecodingFailureStrategy { /// /// `FirestoreQuery` also supports returning a `Result` type. The `.success` case /// returns an array of elements, whereas the `.failure` case returns an error -/// in case mapping the Firestore docments wasn't successful: +/// in case mapping the Firestore documents wasn't successful: /// /// struct ContentView: View { /// @FirestoreQuery( @@ -135,7 +135,7 @@ public struct FirestoreQuery: DynamicProperty { /// If any errors occurred, they will be exposed here as well. public var error: Error? - /// The type of animation to apply when updating the view. If this is ommitted then no + /// The type of animation to apply when updating the view. If this is omitted then no /// animations are fired. public var animation: Animation? } diff --git a/Firestore/Swift/Source/PropertyWrapper/FirestoreQueryObservable.swift b/Firestore/Swift/Source/PropertyWrapper/FirestoreQueryObservable.swift index 159e379eb54..8e4c9280e87 100644 --- a/Firestore/Swift/Source/PropertyWrapper/FirestoreQueryObservable.swift +++ b/Firestore/Swift/Source/PropertyWrapper/FirestoreQueryObservable.swift @@ -47,7 +47,7 @@ class FirestoreQueryObservable: ObservableObject { self.configuration = configuration setupListener = createListener { [weak self] querySnapshot, error in guard let self = self else { return } - if let error = error { + if let error { self.animated { self.items = [] self.projectError(error) @@ -104,7 +104,7 @@ class FirestoreQueryObservable: ObservableObject { self.configuration = configuration setupListener = createListener { [weak self] querySnapshot, error in guard let self = self else { return } - if let error = error { + if let error { self.animated { self.items = .failure(error) self.projectError(error) diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift index 42895d121e5..5723a63ea08 100644 --- a/Firestore/Swift/Tests/API/BasicCompileTests.swift +++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift @@ -143,7 +143,7 @@ func writeDocument(at docRef: DocumentReference) { // Completion callback (via trailing closure syntax). docRef.setData(setData) { error in - if let error = error { + if let error { print("Uh oh! \(error)") return } @@ -154,7 +154,7 @@ func writeDocument(at docRef: DocumentReference) { // merge docRef.setData(setData, merge: true) docRef.setData(setData, merge: true) { error in - if let error = error { + if let error { print("Uh oh! \(error)") return } @@ -166,7 +166,7 @@ func writeDocument(at docRef: DocumentReference) { docRef.delete() docRef.delete { error in - if let error = error { + if let error { print("Uh oh! \(error)") return } @@ -178,15 +178,15 @@ func writeDocument(at docRef: DocumentReference) { func enableDisableNetwork(database db: Firestore) { // closure syntax db.disableNetwork(completion: { error in - if let e = error { - print("Uh oh! \(e)") + if let error { + print("Uh oh! \(error)") return } }) // trailing block syntax db.enableNetwork { error in - if let e = error { - print("Uh oh! \(e)") + if let error { + print("Uh oh! \(error)") return } } @@ -194,8 +194,8 @@ func enableDisableNetwork(database db: Firestore) { func clearPersistence(database db: Firestore) { db.clearPersistence { error in - if let e = error { - print("Uh oh! \(e)") + if let error { + print("Uh oh! \(error)") return } } @@ -217,7 +217,7 @@ func writeDocuments(at docRef: DocumentReference, database db: Firestore) { batch.setData(["c": "d"], forDocument: docRef) // commit with completion callback via trailing closure syntax. batch.commit { error in - if let error = error { + if let error { print("Uh oh! \(error)") return } @@ -235,7 +235,7 @@ func addDocument(to collectionRef: CollectionReference) { func readDocument(at docRef: DocumentReference) { // Trailing closure syntax. docRef.getDocument { document, error in - if let document = document { + if let document { // Note that both document and document.data() is nullable. if let data = document.data() { print("Read document: \(data)") @@ -253,7 +253,7 @@ func readDocument(at docRef: DocumentReference) { if let foo = document["foo"] { print("Field: \(foo)") } - } else if let error = error { + } else if let error { // New way to handle errors. switch error { case FirestoreErrorCode.unavailable: @@ -322,12 +322,12 @@ func readDocumentsWithSource(matching query: Query) { func listenToDocument(at docRef: DocumentReference) { let listener = docRef.addSnapshotListener { document, error in - if let error = error { + if let error { print("Uh oh! Listen canceled: \(error)") return } - if let document = document { + if let document { // Note that document.data() is nullable. if let data: [String: Any] = document.data() { print("Current document: \(data)") @@ -346,7 +346,7 @@ func listenToDocument(at docRef: DocumentReference) { func listenToDocumentWithMetadataChanges(at docRef: DocumentReference) { let listener = docRef.addSnapshotListener(includeMetadataChanges: true) { document, _ in - if let document = document { + if let document { if document.metadata.hasPendingWrites { print("Has pending writes") } @@ -359,12 +359,12 @@ func listenToDocumentWithMetadataChanges(at docRef: DocumentReference) { func listenToDocuments(matching query: Query) { let listener = query.addSnapshotListener { snap, error in - if let error = error { + if let error { print("Uh oh! Listen canceled: \(error)") return } - if let snap = snap { + if let snap { print("NEW SNAPSHOT (empty=\(snap.isEmpty) count=\(snap.count)") // TODO(mikelehen): Figure out how to make "for..in" syntax work @@ -383,7 +383,7 @@ func listenToDocuments(matching query: Query) { func listenToQueryDiffs(onQuery query: Query) { let listener = query.addSnapshotListener { snap, _ in - if let snap = snap { + if let snap { for change in snap.documentChanges { switch change.type { case .added: @@ -403,7 +403,7 @@ func listenToQueryDiffs(onQuery query: Query) { func listenToQueryDiffsWithMetadata(onQuery query: Query) { let listener = query.addSnapshotListener(includeMetadataChanges: true) { snap, _ in - if let snap = snap { + if let snap { for change in snap.documentChanges(includeMetadataChanges: true) { switch change.type { case .added: @@ -470,8 +470,8 @@ func types() { func waitForPendingWrites(database db: Firestore) { db.waitForPendingWrites { error in - if let e = error { - print("Uh oh! \(e)") + if let error { + print("Uh oh! \(error)") return } } @@ -486,8 +486,8 @@ func addSnapshotsInSyncListener(database db: Firestore) { func terminateDb(database db: Firestore) { db.terminate { error in - if let e = error { - print("Uh oh! \(e)") + if let error { + print("Uh oh! \(error)") return } } diff --git a/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift b/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift index 601b2bab46b..7fd5031384c 100644 --- a/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift +++ b/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift @@ -670,7 +670,7 @@ private class DictionarySubject { init(_ subject: [String: Any], in documentName: String? = nil, file: StaticString, line: UInt) { self.subject = subject - if let documentName = documentName { + if let documentName { document = FSTTestDocRef(documentName) } self.file = file @@ -717,7 +717,7 @@ extension String: ServerTimestampWrappable { public static func unwrap(_ value: Self) throws -> Timestamp { let date = formatter.date(from: value) - if let date = date { + if let date { return Timestamp(date: date) } else { throw DateError.invalidDate(value) diff --git a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift index c6d01a63e98..7f91110ea17 100644 --- a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift @@ -36,18 +36,18 @@ class CodableIntegrationTests: FSTIntegrationTestCase { switch flavor { case .docRef: - if let merge = merge { + if let merge { try doc.setData(from: value, merge: merge, completion: completion) - } else if let mergeFields = mergeFields { + } else if let mergeFields { try doc.setData(from: value, mergeFields: mergeFields, completion: completion) } else { try doc.setData(from: value, completion: completion) } case .writeBatch: - if let merge = merge { + if let merge { try doc.firestore.batch().setData(from: value, forDocument: doc, merge: merge) .commit(completion: completion) - } else if let mergeFields = mergeFields { + } else if let mergeFields { try doc.firestore.batch().setData(from: value, forDocument: doc, mergeFields: mergeFields) .commit(completion: completion) } else { @@ -57,9 +57,9 @@ class CodableIntegrationTests: FSTIntegrationTestCase { case .transaction: doc.firestore.runTransaction({ transaction, errorPointer -> Any? in do { - if let merge = merge { + if let merge { try transaction.setData(from: value, forDocument: doc, merge: merge) - } else if let mergeFields = mergeFields { + } else if let mergeFields { try transaction.setData(from: value, forDocument: doc, mergeFields: mergeFields) } else { try transaction.setData(from: value, forDocument: doc) From e1e1b9349e70853d8316b6967067fbcd237b0eb8 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 4 May 2024 07:13:28 -0700 Subject: [PATCH 008/103] Stop CI on Swift podspecs (#12895) --- .github/workflows/analytics.yml | 2 -- .github/workflows/database.yml | 2 +- .github/workflows/firestore.yml | 2 -- .github/workflows/inappmessaging.yml | 2 +- .github/workflows/remoteconfig.yml | 2 +- scripts/health_metrics/file_patterns.json | 10 +++++----- 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/workflows/analytics.yml b/.github/workflows/analytics.yml index 0c6c12f98c0..90ef6121902 100644 --- a/.github/workflows/analytics.yml +++ b/.github/workflows/analytics.yml @@ -42,8 +42,6 @@ jobs: run: scripts/third_party/travis/retry.sh pod spec lint GoogleAppMeasurement.podspec --platforms=${{ matrix.target }} --sources=https://github.com/firebase/SpecsDev.git,https://github.com/firebase/SpecsStaging.git,https://cdn.cocoapods.org/ - name: FirebaseAnalytics run: scripts/third_party/travis/retry.sh pod spec lint FirebaseAnalytics.podspec --platforms=${{ matrix.target }} --sources=https://github.com/firebase/SpecsDev.git,https://github.com/firebase/SpecsStaging.git,https://cdn.cocoapods.org/ - - name: FirebaseAnalyticsSwift - run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseAnalyticsSwift.podspec --allow-warnings --platforms=${{ matrix.target }} # The following steps are only run on `ios` due to product availability. - name: GoogleAppMeasurementOnDeviceConversion run: scripts/third_party/travis/retry.sh pod spec lint GoogleAppMeasurementOnDeviceConversion.podspec --platforms=ios --sources=https://github.com/firebase/SpecsDev.git,https://github.com/firebase/SpecsStaging.git,https://cdn.cocoapods.org/ diff --git a/.github/workflows/database.yml b/.github/workflows/database.yml index 309c80ffbc0..698f52ea6b4 100644 --- a/.github/workflows/database.yml +++ b/.github/workflows/database.yml @@ -135,7 +135,7 @@ jobs: runs-on: macos-14 strategy: matrix: - podspec: [FirebaseDatabase.podspec, FirebaseDatabaseSwift.podspec --allow-warnings] + podspec: [FirebaseDatabase.podspec] target: [ios, tvos, macos] flags: [ '--skip-tests --use-static-frameworks' diff --git a/.github/workflows/firestore.yml b/.github/workflows/firestore.yml index ccf74fc36cb..d74f6585955 100644 --- a/.github/workflows/firestore.yml +++ b/.github/workflows/firestore.yml @@ -362,7 +362,6 @@ jobs: podspec: [ 'FirebaseFirestoreInternal.podspec', 'FirebaseFirestore.podspec', - 'FirebaseFirestoreSwift.podspec', ] steps: @@ -391,7 +390,6 @@ jobs: podspec: [ 'FirebaseFirestoreInternal.podspec', 'FirebaseFirestore.podspec', - 'FirebaseFirestoreSwift.podspec', ] platforms: [ 'macos', diff --git a/.github/workflows/inappmessaging.yml b/.github/workflows/inappmessaging.yml index 918563e9213..efec2295431 100644 --- a/.github/workflows/inappmessaging.yml +++ b/.github/workflows/inappmessaging.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: - podspec: [FirebaseInAppMessaging.podspec, FirebaseInAppMessagingSwift.podspec --allow-warnings] + podspec: [FirebaseInAppMessaging.podspec] os: [macos-14, macos-13] include: - os: macos-14 diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index 9bb193a529d..9976accbb47 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -60,7 +60,7 @@ jobs: matrix: # TODO: macos tests are blocked by https://github.com/erikdoe/ocmock/pull/532 target: [ios, tvos, macos --skip-tests, watchos] - podspec: [FirebaseRemoteConfig.podspec, FirebaseRemoteConfigSwift.podspec --allow-warnings --skip-tests] + podspec: [FirebaseRemoteConfig.podspec] os: [macos-14, macos-13] include: - os: macos-14 diff --git a/scripts/health_metrics/file_patterns.json b/scripts/health_metrics/file_patterns.json index cb56eb0707f..cfda46156e5 100644 --- a/scripts/health_metrics/file_patterns.json +++ b/scripts/health_metrics/file_patterns.json @@ -10,7 +10,7 @@ }, { "sdk": "analytics", - "podspecs": ["FirebaseAnalytics.podspec", "FirebaseAnalyticsSwift.podspec", "GoogleAppMeasurement.podspec"], + "podspecs": ["FirebaseAnalytics.podspec", "GoogleAppMeasurement.podspec"], "filePatterns": [ "^FirebaseAnalytics.*", "^GoogleAppMeasurement.*" @@ -58,7 +58,7 @@ }, { "sdk": "database", - "podspecs": ["FirebaseDatabase.podspec", "FirebaseDatabaseSwift.podspec"], + "podspecs": ["FirebaseDatabase.podspec"], "filePatterns": [ "^FirebaseDatabase.*", "\\.github/workflows/database\\.yml", @@ -85,7 +85,7 @@ }, { "sdk": "firestore", - "podspecs": ["FirebaseFirestore.podspec", "FirebaseFirestoreSwift.podspec"], + "podspecs": ["FirebaseFirestore.podspec"], "filePatterns": [ "^Firestore/.*", "FirebaseAppCheck/Interop/[^/]+\\.h", @@ -117,7 +117,7 @@ }, { "sdk": "inappmessaging", - "podspecs": ["FirebaseInAppMessaging.podspec", "FirebaseInAppMessagingSwift.podspec"], + "podspecs": ["FirebaseInAppMessaging.podspec"], "filePatterns": [ "^FirebaseInAppMessaging.*", "Interop/Analytics/Public/[^/]+\\.h", @@ -160,7 +160,7 @@ }, { "sdk": "remoteconfig", - "podspecs": ["FirebaseRemoteConfig.podspec", "FirebaseRemoteConfigSwift.podspec"], + "podspecs": ["FirebaseRemoteConfig.podspec"], "filePatterns": [ "^FirebaseRemoteConfig.*", "Interop/Analytics/Public/[^/]+\\.h", From b414bfc0d231212e49b1061f042c14cb7d443fa1 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 6 May 2024 20:22:23 -0400 Subject: [PATCH 009/103] [Release Tooling] Add functionality to build dynamic frameworks (#12890) --- .github/workflows/zip.yml | 42 ++++++++++----- .../Sources/ZipBuilder/FrameworkBuilder.swift | 46 ++-------------- .../Sources/ZipBuilder/ZipBuilder.swift | 52 ++++++++++++++++++- scripts/build_zip.sh | 38 ++++++++++---- 4 files changed, 110 insertions(+), 68 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index ef41e52a963..606a1acdc39 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -43,7 +43,9 @@ jobs: run: | mkdir -p release_zip_dir sh -x scripts/build_zip.sh release_zip_dir \ - "${{ github.event.inputs.custom_spec_repos || 'https://github.com/firebase/SpecsStaging.git' }}" + "${{ github.event.inputs.custom_spec_repos || 'https://github.com/firebase/SpecsStaging.git' }}" \ + build-release \ + static - uses: actions/upload-artifact@v4 with: name: Firebase-release-zip-zip @@ -68,6 +70,9 @@ jobs: # Don't run on private repo. if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' needs: build + strategy: + matrix: + linking_type: [static, dynamic] runs-on: macos-14 steps: - uses: actions/checkout@v4 @@ -84,10 +89,11 @@ jobs: mkdir -p zip_output_dir sh -x scripts/build_zip.sh \ zip_output_dir "${{ github.event.inputs.custom_spec_repos || 'https://github.com/firebase/SpecsStaging.git,https://github.com/firebase/SpecsDev.git' }}" \ - build-head + build-head \ + ${{ matrix.linking_type }} - uses: actions/upload-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.linking_type == 'static' && 'Firebase-actions-dir' || 'Firebase-actions-dir-dynamic' }} # Zip the entire output directory since the builder adds subdirectories we don't know the # name of. path: zip_output_dir @@ -103,6 +109,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -114,7 +121,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -164,6 +171,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -175,7 +183,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -217,6 +225,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -228,7 +237,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -268,6 +277,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -279,7 +289,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -343,6 +353,7 @@ jobs: matrix: os: [macos-13] xcode: [Xcode_15.2] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. # - os: macos-13 # xcode: Xcode_15.2 @@ -352,7 +363,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -396,6 +407,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -407,7 +419,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -456,6 +468,7 @@ jobs: # matrix: # # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. # os: [macos-13] + # artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] # xcode: [Xcode_15.2] # runs-on: ${{ matrix.os }} # steps: @@ -463,7 +476,7 @@ jobs: # - name: Get framework dir # uses: actions/download-artifact@v4 # with: - # name: Firebase-actions-dir + # name: ${{ matrix.artifact }} # - uses: ruby/setup-ruby@v1 # - name: Setup Bundler # run: ./scripts/setup_bundler.sh @@ -536,6 +549,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -547,7 +561,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -592,6 +606,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -603,7 +618,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh @@ -647,6 +662,7 @@ jobs: strategy: matrix: os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] include: - os: macos-13 xcode: Xcode_15.2 @@ -658,7 +674,7 @@ jobs: - name: Get framework dir uses: actions/download-artifact@v4 with: - name: Firebase-actions-dir + name: ${{ matrix.artifact }} - uses: ruby/setup-ruby@v1 - name: Setup Bundler run: ./scripts/setup_bundler.sh diff --git a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift index 144459bf90e..cd87fd3272e 100755 --- a/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/FrameworkBuilder.swift @@ -306,18 +306,14 @@ struct FrameworkBuilder { logsDir: URL) -> [URL] { // xcframework doesn't lipo things together but accepts fat frameworks for one target. // We group architectures here to deal with this fact. - var thinFrameworks = [URL]() - for targetPlatform in TargetPlatform.allCases { - let buildDir = projectDir.appendingPathComponent(targetPlatform.buildName) - let slicedFramework = buildSlicedFramework( - withName: FrameworkBuilder.frameworkBuildName(framework), + return targetPlatforms.map { targetPlatform in + buildSlicedFramework( + withName: framework, targetPlatform: targetPlatform, - buildDir: buildDir, + buildDir: projectDir.appendingPathComponent(targetPlatform.buildName), logRoot: logsDir ) - thinFrameworks.append(slicedFramework) } - return thinFrameworks } /// Compiles the specified framework in a temporary directory and writes the build logs to file. @@ -565,17 +561,6 @@ struct FrameworkBuilder { "\(framework): \(error)") } - // CocoaPods creates a `_CodeSignature` directory. Delete it. - // Note that the build only produces a `_CodeSignature` directory for - // macOS and macCatalyst, but we try to delete it for other platforms - // just in case it were to appear. - let codeSignatureDir = platformFrameworkDir - .appendingPathComponent( - platform == .catalyst || platform == .macOS ? "Versions/A/" : "" - ) - .appendingPathComponent("_CodeSignature") - try? fileManager.removeItem(at: codeSignatureDir) - // The minimum OS version is set to 100.0 to work around b/327020913. // TODO(ncooke3): Revert this logic once b/327020913 is fixed. // TODO(ncooke3): Does this need to happen on macOS? @@ -604,23 +589,6 @@ struct FrameworkBuilder { ) } - // The macOS slice's `PrivateHeaders` directory may have a - // `PrivateHeaders` file in it that symbolically links to nowhere. Delete - // it here to avoid putting it in the zip or crashing the Carthage hash - // generation. Because this will throw an error for cases where the file - // does not exist, the error is ignored. - let privateHeadersDir = platformFrameworkDir.appendingPathComponent("PrivateHeaders") - if fileManager.directoryExists(at: privateHeadersDir.resolvingSymlinksInPath()) { - try? fileManager - .removeItem(at: privateHeadersDir.resolvingSymlinksInPath() - .appendingPathComponent("PrivateHeaders")) - } else { - try? fileManager.removeItem(at: privateHeadersDir) - } - let headersDir = platformFrameworkDir.appendingPathComponent("Headers") - .resolvingSymlinksInPath() - try? fileManager.removeItem(at: headersDir.appendingPathComponent("Headers")) - // Move privacy manifest containing resource bundles into the framework. let resourceDir = platformFrameworkDir .appendingPathComponent( @@ -638,12 +606,6 @@ struct FrameworkBuilder { // Bundles are moved rather than copied to prevent them from being // packaged in a `Resources` directory at the root of the xcframework. .forEach { - // Delete `gRPCCertificates-Cpp.bundle` since it is not needed (#9184). - guard $0.lastPathComponent != "gRPCCertificates-Cpp.bundle" else { - try fileManager.removeItem(at: $0) - return - } - try fileManager.moveItem( at: $0, to: resourceDir.appendingPathComponent($0.lastPathComponent) diff --git a/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift b/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift index 2b0b1de7720..992ed2e9d89 100644 --- a/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift +++ b/ReleaseTooling/Sources/ZipBuilder/ZipBuilder.swift @@ -277,7 +277,8 @@ struct ZipBuilder { for groupedFramework in groupedFrameworks { let name = groupedFramework.key let xcframework = FrameworkBuilder.makeXCFramework(withName: name, - frameworks: groupedFramework.value, + frameworks: postProcessFrameworks(groupedFramework + .value), xcframeworksDir: xcframeworksDir, resourceContents: resources[name]) xcframeworks[name] = [xcframework] @@ -299,13 +300,60 @@ struct ZipBuilder { let carthageGoogleUtilitiesXcframework = FrameworkBuilder.makeXCFramework( withName: "GoogleUtilities", - frameworks: carthageGoogleUtilitiesFrameworks, + frameworks: postProcessFrameworks(carthageGoogleUtilitiesFrameworks), xcframeworksDir: xcframeworksCarthageDir, resourceContents: nil ) return (podsBuilt, xcframeworks, carthageGoogleUtilitiesXcframework) } + func postProcessFrameworks(_ frameworks: [URL]) -> [URL] { + for framework in frameworks { + // CocoaPods creates a `_CodeSignature` directory. Delete it. + // Note that the build only produces a `_CodeSignature` directory for + // macOS and macCatalyst (`Versions/A/`), but we try to delete it for + // other platforms just in case it were to appear. + for path in ["", "Versions/A/"] { + let codeSignatureDir = framework + .appendingPathComponent(path) + .appendingPathComponent("_CodeSignature") + .resolvingSymlinksInPath() + try? FileManager.default.removeItem(at: codeSignatureDir) + } + + // Delete `gRPCCertificates-Cpp.bundle` since it is not needed (#9184). + // Depending on the platform, it may be at the root of the framework or + // in a symlinked `Resources` directory (for macOS, macCatalyst). Attempt + // to delete at either patch for each framework. + for path in ["", "Resources"] { + let grpcCertsBundle = framework + .appendingPathComponent(path) + .appendingPathComponent("gRPCCertificates-Cpp.bundle") + .resolvingSymlinksInPath() + try? FileManager.default.removeItem(at: grpcCertsBundle) + } + + // The macOS slice's `PrivateHeaders` directory may have a + // `PrivateHeaders` file in it that symbolically links to nowhere. Delete + // it here to avoid putting it in the zip or crashing the Carthage hash + // generation. Because this will throw an error for cases where the file + // does not exist, the error is ignored. + let privateHeadersDir = framework.appendingPathComponent("PrivateHeaders") + if !FileManager.default.directoryExists(at: privateHeadersDir.resolvingSymlinksInPath()) { + try? FileManager.default.removeItem(at: privateHeadersDir) + } + + // The `Headers` and `PrivateHeaders` directories may contain a symlink + // of the same name. Delete it here to avoid putting it in the zip or + // crashing the Carthage hash generation. + for path in ["Headers", "PrivateHeaders"] { + let headersDir = framework.appendingPathComponent(path).resolvingSymlinksInPath() + try? FileManager.default.removeItem(at: headersDir.appendingPathComponent(path)) + } + } + return frameworks + } + /// Try to build and package the contents of the Zip file. This will throw an error as soon as it /// encounters an error, or will quit due to a fatal error with the appropriate log. /// diff --git a/scripts/build_zip.sh b/scripts/build_zip.sh index c71579ca6d5..ee7e9600f62 100755 --- a/scripts/build_zip.sh +++ b/scripts/build_zip.sh @@ -14,10 +14,12 @@ set -x REPO=`pwd` -if [[ $# -lt 2 ]]; then +if [[ $# -ne 4 ]]; then cat 2>&2 < Date: Tue, 7 May 2024 16:27:06 -0400 Subject: [PATCH 010/103] [InAppMessaging] Fix #12882 (#12910) --- FirebaseInAppMessaging/CHANGELOG.md | 4 ++++ .../Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m | 2 ++ 2 files changed, 6 insertions(+) diff --git a/FirebaseInAppMessaging/CHANGELOG.md b/FirebaseInAppMessaging/CHANGELOG.md index 6ed3ff1c7ca..9f3a86b111c 100644 --- a/FirebaseInAppMessaging/CHANGELOG.md +++ b/FirebaseInAppMessaging/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [fixed] Fixed crash at app start that affected SwiftPM users and CocoaPods + users using static frameworks (#12882). + # 10.25.0 - [changed] Removed usages of user defaults API to eliminate required reason impact. diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m index ed724bfcfe3..c84da5e8758 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m @@ -72,10 +72,12 @@ + (NSBundle *)getViewResourceBundle { [NSBundle mainBundle], // Dynamically linked. [NSBundle bundleForClass:myClass], +#if FIREBASE_BUILD_ZIP_FILE // Embedded static framework (zip distribution). [NSBundle bundleWithURL:[NSBundle.mainBundle.bundleURL URLByAppendingPathComponent: @"Frameworks/FirebaseInAppMessaging.framework"]] +#endif // FIREBASE_BUILD_ZIP_FILE ]) { bundleURL = [containingBundle URLForResource:bundledResource withExtension:@"bundle"]; if (bundleURL != nil) break; From 96b68914c2667e94a5632d9f4081374f676ae9ae Mon Sep 17 00:00:00 2001 From: Dogan Altinbas Date: Tue, 7 May 2024 23:04:20 +0200 Subject: [PATCH 011/103] Improve Usage of SQLITE_OPEN_FILEPROTECTION_NONE in FIRMessagingRmqManager.m (#12909) --- .../Sources/FIRMessagingRmqManager.m | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/FirebaseMessaging/Sources/FIRMessagingRmqManager.m b/FirebaseMessaging/Sources/FIRMessagingRmqManager.m index ad66dcea2ce..fbc9dd04dc4 100644 --- a/FirebaseMessaging/Sources/FIRMessagingRmqManager.m +++ b/FirebaseMessaging/Sources/FIRMessagingRmqManager.m @@ -490,9 +490,11 @@ - (void)openDatabase { BOOL didOpenDatabase = YES; if (![fileManager fileExistsAtPath:path]) { // We've to separate between different versions here because of backwards compatbility issues. - int result = sqlite3_open_v2( - [path UTF8String], &self -> _database, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE, NULL); + int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; +#ifdef SQLITE_OPEN_FILEPROTECTION_NONE + flags |= SQLITE_OPEN_FILEPROTECTION_NONE; +#endif + int result = sqlite3_open_v2([path UTF8String], &self -> _database, flags, NULL); if (result != SQLITE_OK) { NSString *errorString = FIRMessagingStringFromSQLiteResult(result); NSString *errorMessage = [NSString @@ -509,9 +511,11 @@ - (void)openDatabase { [self createTableWithName:kTableS2DRmqIds command:kCreateTableS2DRmqIds]; } else { // Calling sqlite3_open should create the database, since the file doesn't exist. - int result = sqlite3_open_v2( - [path UTF8String], &self -> _database, - SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_NONE, NULL); + int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; +#ifdef SQLITE_OPEN_FILEPROTECTION_NONE + flags |= SQLITE_OPEN_FILEPROTECTION_NONE; +#endif + int result = sqlite3_open_v2([path UTF8String], &self -> _database, flags, NULL); if (result != SQLITE_OK) { NSString *errorString = FIRMessagingStringFromSQLiteResult(result); NSString *errorMessage = From dda0e185c5c78db6695cf760442068d92bb951c8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 8 May 2024 10:21:16 -0400 Subject: [PATCH 012/103] [Release] Update changelog to reflect #12900 (#12915) --- FirebaseMessaging/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FirebaseMessaging/CHANGELOG.md b/FirebaseMessaging/CHANGELOG.md index 8191c09c6b3..27b82cc217e 100644 --- a/FirebaseMessaging/CHANGELOG.md +++ b/FirebaseMessaging/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [fixed] Fixed bug preventing Messaging from working with a custom sqlite3 + dependency (#12900). + # 10.23.0 - [fixed] [CocoaPods] Fix "no rule" warning when running `pod install`. (#12511) From 1c605b2d24fa03a6c2f084b69bbd268b1511d45a Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 8 May 2024 08:10:09 -0700 Subject: [PATCH 013/103] [RTDB] Use NSURLSessionWebSocket instead of SocketRocket where possible (#12894) Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> --- FirebaseDatabase/CHANGELOG.md | 4 + .../Sources/Realtime/FWebSocketConnection.h | 3 +- .../Sources/Realtime/FWebSocketConnection.m | 214 +++++++++++------- FirebaseDatabase/Tests/Integration/FData.m | 4 +- .../Tests/Integration/FIRDatabaseQueryTests.m | 4 +- 5 files changed, 139 insertions(+), 90 deletions(-) diff --git a/FirebaseDatabase/CHANGELOG.md b/FirebaseDatabase/CHANGELOG.md index 61df131aa31..9a24f6b1306 100644 --- a/FirebaseDatabase/CHANGELOG.md +++ b/FirebaseDatabase/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [changed] Update internal socket implementation to use `NSURLSessionWebSocket` where + available. (#12883) + # 10.25.0 - [changed] Removed usages of user defaults API to eliminate required reason impact. diff --git a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h index 676971864cb..14912ff861f 100644 --- a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h +++ b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h @@ -23,7 +23,8 @@ @protocol FWebSocketDelegate; #if !TARGET_OS_WATCH -@interface FWebSocketConnection : NSObject +@interface FWebSocketConnection + : NSObject #else @interface FWebSocketConnection : NSObject #endif // else !TARGET_OS_WATCH diff --git a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m index 5ac1bfa2f85..7edd0abb3e6 100644 --- a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m +++ b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m @@ -26,16 +26,17 @@ #import "FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h" #import "FirebaseDatabase/Sources/Utilities/FStringUtilities.h" -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && - // TARGET_OS_VISION) -#if TARGET_OS_WATCH -#import +#elif TARGET_OS_WATCH #import -#endif // TARGET_OS_WATCH + +#elif TARGET_OS_OSX +#import +#endif + +#import static NSString *const kAppCheckTokenHeader = @"X-Firebase-AppCheck"; static NSString *const kUserAgentHeader = @"User-Agent"; @@ -52,9 +53,10 @@ - (void)shutdown; - (void)onClosed; - (void)closeIfNeverConnected; -#if TARGET_OS_WATCH -@property(nonatomic, strong) NSURLSessionWebSocketTask *webSocketTask; -#else +@property(nonatomic, strong) + NSURLSessionWebSocketTask *webSocketTask API_AVAILABLE( + macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)); +#if !TARGET_OS_WATCH @property(nonatomic, strong) FSRWebSocket *webSocket; #endif // TARGET_OS_WATCH @property(nonatomic, strong) NSNumber *connectionId; @@ -100,40 +102,56 @@ - (instancetype)initWith:(FRepoInfo *)repoInfo userAgent:userAgent googleAppID:googleAppID appCheckToken:appCheckToken]; -#if TARGET_OS_WATCH - // Regular NSURLSession websocket. - NSOperationQueue *opQueue = [[NSOperationQueue alloc] init]; - opQueue.underlyingQueue = queue; - NSURLSession *session = [NSURLSession - sessionWithConfiguration:[NSURLSessionConfiguration - defaultSessionConfiguration] - delegate:self - delegateQueue:opQueue]; - NSURLSessionWebSocketTask *task = - [session webSocketTaskWithRequest:req]; - self.webSocketTask = task; - - if (@available(watchOS 7.0, *)) { - [[NSNotificationCenter defaultCenter] - addObserverForName:WKApplicationWillResignActiveNotification - object:nil - queue:opQueue - usingBlock:^(NSNotification *_Nonnull note) { - FFLog(@"I-RDB083015", - @"Received watchOS background notification, " - @"closing web socket."); - [self onClosed]; - }]; + + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + // Regular NSURLSession websocket. + NSOperationQueue *opQueue = [[NSOperationQueue alloc] init]; + opQueue.underlyingQueue = queue; + NSURLSession *session = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration + defaultSessionConfiguration] + delegate:self + delegateQueue:opQueue]; + NSURLSessionWebSocketTask *task = + [session webSocketTaskWithRequest:req]; + self.webSocketTask = task; + +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION || TARGET_OS_MACCATALYST + NSString *resignName = UIApplicationWillResignActiveNotification; +#elif TARGET_OS_OSX + NSString *resignName = NSApplicationWillResignActiveNotification; +#elif TARGET_OS_WATCH + NSString *resignName = WKApplicationWillResignActiveNotification; +#elif +#error("missing platform") +#endif + if (@available(watchOS 7.0, *)) { + [[NSNotificationCenter defaultCenter] + addObserverForName:resignName + object:nil + queue:opQueue + usingBlock:^(NSNotification *_Nonnull note) { + FFLog(@"I-RDB083015", + @"Received notification that application " + @"will resign, " + @"closing web socket."); + [self onClosed]; + }]; + } + } +#if !TARGET_OS_WATCH + else { + // TODO(mmaksym): Remove googleAppID and userAgent from FSRWebSocket + // as they are passed via NSURLRequest. + self.webSocket = + [[FSRWebSocket alloc] initWithURLRequest:req + queue:queue + googleAppID:googleAppID + andUserAgent:userAgent]; + [self.webSocket setDelegateDispatchQueue:queue]; + self.webSocket.delegate = self; } -#else - // TODO(mmaksym): Remove googleAppID and userAgent from FSRWebSocket as - // they are passed via NSURLRequest. - self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req - queue:queue - googleAppID:googleAppID - andUserAgent:userAgent]; - [self.webSocket setDelegateDispatchQueue:queue]; - self.webSocket.delegate = self; #endif // TARGET_OS_WATCH } return self; @@ -195,13 +213,17 @@ - (void)open { assert(delegate); everConnected = NO; // TODO Assert url -#if TARGET_OS_WATCH - [self.webSocketTask resume]; - // We need to request data from the web socket in order for it to start - // sending data. - [self receiveWebSocketData]; -#else - [self.webSocket open]; + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + [self.webSocketTask resume]; + // We need to request data from the web socket in order for it to start + // sending data. + [self receiveWebSocketData]; + } +#if !TARGET_OS_WATCH + else { + [self.webSocket open]; + } #endif // TARGET_OS_WATCH dispatch_time_t when = dispatch_time( DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC); @@ -214,12 +236,16 @@ - (void)close { FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.", self.connectionId); isClosed = YES; -#if TARGET_OS_WATCH - [self.webSocketTask - cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure - reason:nil]; -#else - [self.webSocket close]; + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + [self.webSocketTask + cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure + reason:nil]; + } +#if !TARGET_OS_WATCH + else { + [self.webSocket close]; + } #endif // TARGET_OS_WATCH } @@ -322,25 +348,27 @@ - (void)handleIncomingFrame:(NSString *)message { } #pragma mark - -#pragma mark URLSessionWebSocketDelegate watchOS implementation -#if TARGET_OS_WATCH +#pragma mark URLSessionWebSocketDelegate implementation - (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask - didOpenWithProtocol:(NSString *)protocol { + didOpenWithProtocol:(NSString *)protocol + API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) { [self webSocketDidOpen]; } - (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode - reason:(NSData *)reason { + reason:(NSData *)reason + API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) { FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@", self.connectionId, (long)closeCode, reason); [self onClosed]; } -- (void)receiveWebSocketData { +- (void)receiveWebSocketData API_AVAILABLE(macos(10.15), ios(13.0), + watchos(6.0), tvos(13.0)) { __weak __auto_type weakSelf = self; [self.webSocketTask receiveMessageWithCompletionHandler:^( NSURLSessionWebSocketMessage *_Nullable message, @@ -364,7 +392,7 @@ - (void)receiveWebSocketData { }]; } -#else +#if !TARGET_OS_WATCH #pragma mark SRWebSocketDelegate implementation @@ -387,7 +415,7 @@ - (void)webSocket:(FSRWebSocket *)webSocket [self onClosed]; } -#endif // TARGET_OS_WATCH +#endif // !TARGET_OS_WATCH // Common to both SRWebSocketDelegate and URLSessionWebSocketDelegate. @@ -413,21 +441,26 @@ - (void)webSocketDidOpen { /** Sends a string through the open web socket. */ - (void)sendStringToWebSocket:(NSString *)string { -#if TARGET_OS_WATCH - // Use built-in URLSessionWebSocket functionality. - [self.webSocketTask sendMessage:[[NSURLSessionWebSocketMessage alloc] - initWithString:string] - completionHandler:^(NSError *_Nullable error) { - if (error) { - FFWarn(@"I-RDB083016", - @"Error sending web socket data: %@.", error); - return; - } - }]; -#else - // Use existing SocketRocket implementation. - [self.webSocket send:string]; -#endif // TARGET_OS_WATCH + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + // Use built-in URLSessionWebSocket functionality. + [self.webSocketTask + sendMessage:[[NSURLSessionWebSocketMessage alloc] + initWithString:string] + completionHandler:^(NSError *_Nullable error) { + if (error) { + FFWarn(@"I-RDB083016", @"Error sending web socket data: %@.", + error); + return; + } + }]; + } +#if !TARGET_OS_WATCH + else { + // Use existing SocketRocket implementation. + [self.webSocket send:string]; + } +#endif // !TARGET_OS_WATCH } /** @@ -446,12 +479,17 @@ - (void)closeIfNeverConnected { if (!everConnected) { FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect", self.connectionId); -#if TARGET_OS_WATCH - [self.webSocketTask - cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNoStatusReceived - reason:nil]; -#else - [self.webSocket close]; + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + [self.webSocketTask + cancelWithCloseCode: + NSURLSessionWebSocketCloseCodeNoStatusReceived + reason:nil]; + } +#if !TARGET_OS_WATCH + else { + [self.webSocket close]; + } #endif // TARGET_OS_WATCH } } @@ -468,9 +506,11 @@ - (void)onClosed { FFLog(@"I-RDB083013", @"Websocket is closing itself"); [self shutdown]; } -#if TARGET_OS_WATCH - self.webSocketTask = nil; -#else + if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0, + watchOS 6.0, *)) { + self.webSocketTask = nil; + } +#if !TARGET_OS_WATCH self.webSocket = nil; #endif // TARGET_OS_WATCH if (keepAlive.isValid) { diff --git a/FirebaseDatabase/Tests/Integration/FData.m b/FirebaseDatabase/Tests/Integration/FData.m index 2fa04319f9f..9570cc1c5aa 100644 --- a/FirebaseDatabase/Tests/Integration/FData.m +++ b/FirebaseDatabase/Tests/Integration/FData.m @@ -2197,7 +2197,9 @@ - (void)testUpdateDoesntAffectPriorityRemotely { }]; } -- (void)testUpdateReplacesChildrenAndIsNotRecursive { +// TODO: On arm hardware Macs, the following test hangs with the emulator, but passes with a real +// project. +- (void)SKIPtestUpdateReplacesChildrenAndIsNotRecursive { FTupleFirebase *refs = [FTestHelpers getRandomNodePair]; FIRDatabaseReference *reader = refs.one; FIRDatabaseReference *writer = refs.two; diff --git a/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m b/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m index 4163cd15ae6..947d5445dd8 100644 --- a/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m +++ b/FirebaseDatabase/Tests/Integration/FIRDatabaseQueryTests.m @@ -4419,7 +4419,9 @@ - (void)testGetUpdatesPersistenceCacheWhenEnabled { } } -- (void)testGetSkipsPersistenceCacheWhenOnline { +// TODO: On arm hardware Macs, the following test hangs with the emulator, but passes with a real +// project. +- (void)SKIPtestGetSkipsPersistenceCacheWhenOnline { FIRDatabase* db = [self databaseForURL:self.databaseURL name:[[NSUUID UUID] UUIDString]]; FIRDatabase* db2 = [self databaseForURL:self.databaseURL name:[[NSUUID UUID] UUIDString]]; From 2bac0ddfe61a6c1b44745bd637c5b54c353c79d8 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 8 May 2024 14:57:19 -0400 Subject: [PATCH 014/103] [InAppMessaging] Follow-up to #12910 (#12917) --- .../DefaultUI/FIRIAMDefaultDisplayImpl.m | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m index c84da5e8758..f23eab96eb6 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m @@ -64,21 +64,35 @@ + (NSBundle *)getViewResourceBundle { bundledResource = @"InAppMessagingDisplayResources"; #endif // SWIFT_PACKAGE + NSMutableArray *bundles = [NSMutableArray array]; + + // Resources may be in main bundle when statically linked. + NSBundle *mainBundle = [NSBundle mainBundle]; + if (mainBundle) { + [bundles addObject:mainBundle]; + } + // Resources may be in the bundle associated with this class when + // dynamically linked. + NSBundle *bundleForClass = [NSBundle bundleForClass:myClass]; + if (bundleForClass) { + [bundles addObject:bundleForClass]; + } + // When embedding static frameworks from the zip distribution, the Xcode + // will copy the resources into the framework's directory. + // TODO(Firebase 11): Remove when Firebase.zip is composed of dynamic + // frameworks. + NSBundle *frameworkBundle = [NSBundle + bundleWithURL: + [NSBundle.mainBundle.bundleURL + URLByAppendingPathComponent:@"Frameworks/FirebaseInAppMessaging.framework"]]; + if (frameworkBundle) { + [bundles addObject:frameworkBundle]; + } + NSBundle *containingBundle; NSURL *bundleURL; // The containing bundle is different whether FIAM is statically or dynamically linked. - for (containingBundle in @[ - // Statically linked. - [NSBundle mainBundle], - // Dynamically linked. - [NSBundle bundleForClass:myClass], -#if FIREBASE_BUILD_ZIP_FILE - // Embedded static framework (zip distribution). - [NSBundle bundleWithURL:[NSBundle.mainBundle.bundleURL - URLByAppendingPathComponent: - @"Frameworks/FirebaseInAppMessaging.framework"]] -#endif // FIREBASE_BUILD_ZIP_FILE - ]) { + for (containingBundle in bundles) { bundleURL = [containingBundle URLForResource:bundledResource withExtension:@"bundle"]; if (bundleURL != nil) break; } From 317a553ba1f0beb47394d73c4bcbf0a30c4b5683 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Wed, 8 May 2024 15:33:18 -0400 Subject: [PATCH 015/103] [Release] Update FIAM changelog to reflect 10.26.0 (#12919) --- FirebaseInAppMessaging/CHANGELOG.md | 7 +++++-- .../Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/FirebaseInAppMessaging/CHANGELOG.md b/FirebaseInAppMessaging/CHANGELOG.md index 9f3a86b111c..9c5a31b6fe5 100644 --- a/FirebaseInAppMessaging/CHANGELOG.md +++ b/FirebaseInAppMessaging/CHANGELOG.md @@ -1,6 +1,9 @@ # Unreleased -- [fixed] Fixed crash at app start that affected SwiftPM users and CocoaPods - users using static frameworks (#12882). +- [fixed] Fixed crash at app start that affected CocoaPods users using static + frameworks (#12882). + +# 10.26.0 +- [fixed] Fixed crash at app start that affected SwiftPM users (#12882). # 10.25.0 - [changed] Removed usages of user defaults API to eliminate required reason diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m index f23eab96eb6..5673e179fb8 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMDefaultDisplayImpl.m @@ -79,8 +79,6 @@ + (NSBundle *)getViewResourceBundle { } // When embedding static frameworks from the zip distribution, the Xcode // will copy the resources into the framework's directory. - // TODO(Firebase 11): Remove when Firebase.zip is composed of dynamic - // frameworks. NSBundle *frameworkBundle = [NSBundle bundleWithURL: [NSBundle.mainBundle.bundleURL From d9f93829b91914d89aa12bc9504ae24472842191 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 9 May 2024 07:38:01 -0700 Subject: [PATCH 016/103] Update FirebaseAppCheck's FirebaseCore dependency (#12925) --- FirebaseAppCheck.podspec | 2 +- FirebaseAppCheck/CHANGELOG.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/FirebaseAppCheck.podspec b/FirebaseAppCheck.podspec index 00168979f17..c8d439f4327 100644 --- a/FirebaseAppCheck.podspec +++ b/FirebaseAppCheck.podspec @@ -46,7 +46,7 @@ Pod::Spec.new do |s| s.dependency 'AppCheckCore', '~> 10.19' s.dependency 'FirebaseAppCheckInterop', '~> 10.17' - s.dependency 'FirebaseCore', '~> 10.0' + s.dependency 'FirebaseCore', '~> 10.18' s.dependency 'PromisesObjC', '~> 2.1' s.dependency 'GoogleUtilities/Environment', '~> 7.13' s.dependency 'GoogleUtilities/UserDefaults', '~> 7.13' diff --git a/FirebaseAppCheck/CHANGELOG.md b/FirebaseAppCheck/CHANGELOG.md index 6876bfbaf4d..59d3d92c921 100644 --- a/FirebaseAppCheck/CHANGELOG.md +++ b/FirebaseAppCheck/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- [fixed] [CocoaPods] missing symbol error for FIRGetLoggerLevel. (#12899) + # 10.25.0 - [changed] Removed usages of user defaults API to eliminate required reason impact. From 8e2c7b7d0973445b405521b1f249234d4784aab0 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 13:26:20 -0400 Subject: [PATCH 017/103] [Firestore] Cleanup compilation guards for building on older SDKs (#12936) --- Firestore/core/test/unit/FSTGoogleTestTests.mm | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/Firestore/core/test/unit/FSTGoogleTestTests.mm b/Firestore/core/test/unit/FSTGoogleTestTests.mm index f0120857d76..2b4c51389a9 100644 --- a/Firestore/core/test/unit/FSTGoogleTestTests.mm +++ b/Firestore/core/test/unit/FSTGoogleTestTests.mm @@ -235,12 +235,8 @@ void XCTestMethod(XCTestCase* self, SEL _cmd) { XCTAssertTrue(true); return; } else if (result->Skipped()) { -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130400 || \ - __TV_OS_VERSION_MAX_ALLOWED >= 130400 || \ - __MAC_OS_X_VERSION_MAX_ALLOWED >= 101504 // Let Xcode know that the test was skipped. XCTSkip(); -#endif } // Test failed :-(. Record the failure such that Xcode will navigate directly @@ -251,9 +247,6 @@ void XCTestMethod(XCTestCase* self, SEL _cmd) { const char* path = part.file_name() ? part.file_name() : ""; int line = part.line_number() > 0 ? part.line_number() : 0; -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 140000 || \ - __MAC_OS_X_VERSION_MAX_ALLOWED >= 101500 - // Xcode 12 auto* location = [[XCTSourceCodeLocation alloc] initWithFilePath:@(path) lineNumber:line]; auto* context = [[XCTSourceCodeContext alloc] initWithLocation:location]; @@ -264,15 +257,6 @@ void XCTestMethod(XCTestCase* self, SEL _cmd) { associatedError:nil attachments:@[]]; [self recordIssue:issue]; - -#else - // Xcode 11 and prior. recordFailureWithDescription:inFile:atLine:expected: - // is deprecated in Xcode 12. - [self recordFailureWithDescription:@(part.message()) - inFile:@(path) - atLine:line - expected:true]; -#endif } } From a2d205c8d90dae090745d036c15e85a6bfbfaed4 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 13:27:54 -0400 Subject: [PATCH 018/103] [Messaging] Cleanup compilation guards for building on older SDKs (#12935) --- FirebaseMessaging/Sources/FIRMessaging.m | 5 ----- .../Sources/FIRMessagingContextManagerService.m | 5 +---- .../UnitTests/FIRMessagingContextManagerServiceTest.m | 7 ++----- .../UnitTests/FIRMessagingRemoteNotificationsProxyTest.m | 9 +-------- 4 files changed, 4 insertions(+), 22 deletions(-) diff --git a/FirebaseMessaging/Sources/FIRMessaging.m b/FirebaseMessaging/Sources/FIRMessaging.m index d28db581f2d..98f5e7dcb68 100644 --- a/FirebaseMessaging/Sources/FIRMessaging.m +++ b/FirebaseMessaging/Sources/FIRMessaging.m @@ -49,13 +49,8 @@ static NSString *const kFIRMessagingMessageViaAPNSRootKey = @"aps"; static NSString *const kFIRMessagingReachabilityHostname = @"www.google.com"; -#if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 const NSNotificationName FIRMessagingRegistrationTokenRefreshedNotification = @"com.firebase.messaging.notif.fcm-token-refreshed"; -#else -NSString *const FIRMessagingRegistrationTokenRefreshedNotification = - @"com.firebase.messaging.notif.fcm-token-refreshed"; -#endif // defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled = @"com.firebase.messaging.auto-init.enabled"; // Auto Init Enabled key stored in NSUserDefaults diff --git a/FirebaseMessaging/Sources/FIRMessagingContextManagerService.m b/FirebaseMessaging/Sources/FIRMessagingContextManagerService.m index 741a62e65a9..b86e84cb3e6 100644 --- a/FirebaseMessaging/Sources/FIRMessagingContextManagerService.m +++ b/FirebaseMessaging/Sources/FIRMessagingContextManagerService.m @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \ - __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \ - __WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST + #import -#endif #import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h" diff --git a/FirebaseMessaging/Tests/UnitTests/FIRMessagingContextManagerServiceTest.m b/FirebaseMessaging/Tests/UnitTests/FIRMessagingContextManagerServiceTest.m index dd03e1bd4e4..1339a690465 100644 --- a/FirebaseMessaging/Tests/UnitTests/FIRMessagingContextManagerServiceTest.m +++ b/FirebaseMessaging/Tests/UnitTests/FIRMessagingContextManagerServiceTest.m @@ -13,12 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \ - __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 || __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || \ - __WATCH_OS_VERSION_MAX_ALLOWED >= __WATCHOS_3_0 || TARGET_OS_MACCATALYST -#import -#endif + #import +#import #import #import "FirebaseMessaging/Sources/FIRMessagingContextManagerService.h" diff --git a/FirebaseMessaging/Tests/UnitTests/FIRMessagingRemoteNotificationsProxyTest.m b/FirebaseMessaging/Tests/UnitTests/FIRMessagingRemoteNotificationsProxyTest.m index 9fadff6f46b..d3bbdc3ead3 100644 --- a/FirebaseMessaging/Tests/UnitTests/FIRMessagingRemoteNotificationsProxyTest.m +++ b/FirebaseMessaging/Tests/UnitTests/FIRMessagingRemoteNotificationsProxyTest.m @@ -14,11 +14,8 @@ * limitations under the License. */ -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \ - __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 -#import -#endif #import +#import #import #import @@ -95,8 +92,6 @@ - (void)application:(GULApplication *)application @end -#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 || \ - __TV_OS_VERSION_MAX_ALLOWED >= __TV_10_0 || __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_10_14 #pragma mark - Incompete UNUserNotificationCenterDelegate @interface IncompleteUserNotificationCenterDelegate : NSObject @end @@ -127,8 +122,6 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center #endif @end -#endif - @interface GULAppDelegateSwizzler (FIRMessagingRemoteNotificationsProxyTest) + (void)resetProxyOriginalDelegateOnceToken; @end From 07c7c340341dd7af49d9a1234fc156f6dd3da46b Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 14:07:09 -0400 Subject: [PATCH 019/103] [Infra] Attempt to fix #12769 (#12938) --- .github/workflows/firestore.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/firestore.yml b/.github/workflows/firestore.yml index d74f6585955..a752bc57f5c 100644 --- a/.github/workflows/firestore.yml +++ b/.github/workflows/firestore.yml @@ -105,8 +105,7 @@ jobs: (github.event_name == 'pull_request' && needs.changes.outputs.changed == 'true') strategy: matrix: - # TODO(#12769): Update to macos-14 which doesn't include Python 3.7 - os: [macos-12, ubuntu-latest] + os: [macos-14, ubuntu-latest] env: MINT_PATH: ${{ github.workspace }}/mint @@ -132,7 +131,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.10' - name: Setup build run: scripts/install_prereqs.sh Firestore ${{ runner.os }} cmake @@ -153,8 +152,7 @@ jobs: strategy: matrix: - # TODO(#12769): Update to macos-14 which doesn't include Python 3.7 - os: [macos-12] + os: [macos-14] databaseId: [(default), test-db] env: @@ -183,7 +181,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.10' - name: Install Secret GoogleService-Info.plist run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/firestore.plist.gpg \ @@ -241,8 +239,7 @@ jobs: strategy: matrix: - # TODO(#12769): Update to macos-14 which doesn't include Python 3.7 - os: [macos-12] + os: [macos-14] sanitizer: [asan, tsan] runs-on: ${{ matrix.os }} @@ -263,7 +260,7 @@ jobs: - uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.10' - name: Setup build run: scripts/install_prereqs.sh Firestore ${{ runner.os }} cmake From b37e061e6b899858b08bfb8ade94996e7046c20b Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 14:29:51 -0400 Subject: [PATCH 020/103] [NFC] Remove outdated comment (#12940) --- .github/workflows/zip.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 606a1acdc39..00d2dbbe93a 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -354,9 +354,6 @@ jobs: os: [macos-13] xcode: [Xcode_15.2] artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] - # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. - # - os: macos-13 - # xcode: Xcode_15.2 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From 9c4f485d42f7c60836c93b205fce709efcef0a03 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 15:04:18 -0400 Subject: [PATCH 021/103] [Infra] Re-enable 'zip / quickstart_framework_firestore' job --- .github/workflows/zip.yml | 99 +++++++++++++++++++-------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 00d2dbbe93a..b95f2367fb4 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -453,56 +453,55 @@ jobs: name: quickstart_artifacts_dynamiclinks path: quickstart-ios/ - # quickstart_framework_firestore: - # # Don't run on private repo. - # if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - # needs: package-head - # env: - # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # SDK: "Firestore" - # strategy: - # matrix: - # # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. - # os: [macos-13] - # artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] - # xcode: [Xcode_15.2] - # runs-on: ${{ matrix.os }} - # steps: - # - uses: actions/checkout@v4 - # - name: Get framework dir - # uses: actions/download-artifact@v4 - # with: - # name: ${{ matrix.artifact }} - # - uses: ruby/setup-ruby@v1 - # - name: Setup Bundler - # run: ./scripts/setup_bundler.sh - # - name: Move frameworks - # run: | - # mkdir -p "${HOME}"/ios_frameworks/ - # find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - # - uses: actions/checkout@v4 - # - name: Setup quickstart - # run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ - # "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - # - name: Xcode - # run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - # - name: Install Secret GoogleService-Info.plist - # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - # quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - # - name: Test Quickstart - # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") - # - name: Remove data before upload - # if: ${{ failure() }} - # run: scripts/remove_data.sh firestore - # - uses: actions/upload-artifact@v4 - # if: ${{ failure() }} - # with: - # name: quickstart_artifacts_firestore - # path: quickstart-ios/ + quickstart_framework_firestore: + # Don't run on private repo. + if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + needs: package-head + env: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + SDK: "Firestore" + strategy: + matrix: + os: [macos-13] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] + xcode: [Xcode_15.2] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Get framework dir + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.artifact }} + - uses: ruby/setup-ruby@v1 + - name: Setup Bundler + run: ./scripts/setup_bundler.sh + - name: Move frameworks + run: | + mkdir -p "${HOME}"/ios_frameworks/ + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + - uses: actions/checkout@v4 + - name: Setup quickstart + run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ + "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install Secret GoogleService-Info.plist + run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ + quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" + - name: Test Quickstart + run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") + - name: Remove data before upload + if: ${{ failure() }} + run: scripts/remove_data.sh firestore + - uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: quickstart_artifacts_firestore + path: quickstart-ios/ check_framework_firestore_symbols: # Don't run on private repo. From 49858cf2d50704c23f4a13bfcb2bc28976ecd095 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 10 May 2024 15:06:08 -0400 Subject: [PATCH 022/103] Revert "[Infra] Re-enable 'zip / quickstart_framework_firestore' job" This reverts commit 9c4f485d42f7c60836c93b205fce709efcef0a03. --- .github/workflows/zip.yml | 99 ++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index b95f2367fb4..00d2dbbe93a 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -453,55 +453,56 @@ jobs: name: quickstart_artifacts_dynamiclinks path: quickstart-ios/ - quickstart_framework_firestore: - # Don't run on private repo. - if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - needs: package-head - env: - plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - SDK: "Firestore" - strategy: - matrix: - os: [macos-13] - artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] - xcode: [Xcode_15.2] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - name: Get framework dir - uses: actions/download-artifact@v4 - with: - name: ${{ matrix.artifact }} - - uses: ruby/setup-ruby@v1 - - name: Setup Bundler - run: ./scripts/setup_bundler.sh - - name: Move frameworks - run: | - mkdir -p "${HOME}"/ios_frameworks/ - find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - - uses: actions/checkout@v4 - - name: Setup quickstart - run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ - "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ - "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ - "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ - "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - - name: Xcode - run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - - name: Install Secret GoogleService-Info.plist - run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - - name: Test Quickstart - run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") - - name: Remove data before upload - if: ${{ failure() }} - run: scripts/remove_data.sh firestore - - uses: actions/upload-artifact@v4 - if: ${{ failure() }} - with: - name: quickstart_artifacts_firestore - path: quickstart-ios/ + # quickstart_framework_firestore: + # # Don't run on private repo. + # if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + # needs: package-head + # env: + # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + # SDK: "Firestore" + # strategy: + # matrix: + # # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. + # os: [macos-13] + # artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] + # xcode: [Xcode_15.2] + # runs-on: ${{ matrix.os }} + # steps: + # - uses: actions/checkout@v4 + # - name: Get framework dir + # uses: actions/download-artifact@v4 + # with: + # name: ${{ matrix.artifact }} + # - uses: ruby/setup-ruby@v1 + # - name: Setup Bundler + # run: ./scripts/setup_bundler.sh + # - name: Move frameworks + # run: | + # mkdir -p "${HOME}"/ios_frameworks/ + # find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + # - uses: actions/checkout@v4 + # - name: Setup quickstart + # run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ + # "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ + # "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ + # "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ + # "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* + # - name: Xcode + # run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + # - name: Install Secret GoogleService-Info.plist + # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ + # quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" + # - name: Test Quickstart + # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") + # - name: Remove data before upload + # if: ${{ failure() }} + # run: scripts/remove_data.sh firestore + # - uses: actions/upload-artifact@v4 + # if: ${{ failure() }} + # with: + # name: quickstart_artifacts_firestore + # path: quickstart-ios/ check_framework_firestore_symbols: # Don't run on private repo. From 38aabb146f07b96e419e55adc1a159f23fe10548 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 15:16:21 -0400 Subject: [PATCH 023/103] [FIAM] Cleanup compilation guards for building on older SDKs (#12937) --- .../Sources/Analytics/FIRIAMClearcutLogStorage.m | 2 -- .../Sources/Analytics/FIRIAMClearcutUploader.m | 2 -- .../DefaultUI/Banner/FIRIAMBannerViewController.m | 4 ---- .../DefaultUI/FIRIAMBaseRenderingViewController.m | 2 -- .../Sources/DefaultUI/FIRIAMRenderingWindowHelper.m | 10 ---------- .../ImageOnly/FIRIAMImageOnlyViewController.m | 6 +----- .../DefaultUI/Modal/FIRIAMModalViewController.m | 6 +----- .../Sources/Flows/FIRIAMActivityLogger.m | 2 -- .../Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m | 2 -- .../Sources/Flows/FIRIAMDisplayExecutor.m | 6 ++---- .../Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m | 2 -- 11 files changed, 4 insertions(+), 40 deletions(-) diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m index 1d94a2de83b..d526f4ea427 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m @@ -95,14 +95,12 @@ - (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds selector:@selector(appWillBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillBecomeInactive:) name:UISceneWillDeactivateNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 @try { [self loadFromCachePath:cachePath]; diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m index c520f21616a..0ca896f7772 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m @@ -102,14 +102,12 @@ - (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)request selector:@selector(scheduleNextSendFromForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scheduleNextSendFromForeground:) name:UISceneWillEnterForegroundNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 _userDefaults = userDefaults ? userDefaults : [GULUserDefaults standardUserDefaults]; // it would be 0 if it does not exist, which is equvilent to saying that // you can send now diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/Banner/FIRIAMBannerViewController.m b/FirebaseInAppMessaging/Sources/DefaultUI/Banner/FIRIAMBannerViewController.m index 08952e2321a..ad7a95252aa 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/Banner/FIRIAMBannerViewController.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/Banner/FIRIAMBannerViewController.m @@ -161,18 +161,14 @@ - (void)viewDidLoad { // Calculate status bar height. CGFloat statusBarHeight = 0; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { UIStatusBarManager *manager = [UIApplication sharedApplication].keyWindow.windowScene.statusBarManager; statusBarHeight = manager.statusBarFrame.size.height; } else { -#endif statusBarHeight = [[UIApplication sharedApplication] statusBarFrame].size.height; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 } -#endif // Pin title label below status bar with cushion. [[self.titleLabel.topAnchor constraintEqualToAnchor:self.view.topAnchor diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMBaseRenderingViewController.m b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMBaseRenderingViewController.m index f05fe050e3d..460fe9f60a2 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMBaseRenderingViewController.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMBaseRenderingViewController.m @@ -55,7 +55,6 @@ - (void)viewDidLoad { selector:@selector(appDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillBecomeInactive:) @@ -67,7 +66,6 @@ - (void)viewDidLoad { name:UISceneDidActivateNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 self.aggregateImpressionTimeInSeconds = 0; } diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMRenderingWindowHelper.m b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMRenderingWindowHelper.m index e920a32e63c..2d42847474b 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMRenderingWindowHelper.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/FIRIAMRenderingWindowHelper.m @@ -28,15 +28,11 @@ + (UIWindow *)windowForBlockingView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { UIWindowForModal = [[self class] iOS13PlusWindow]; } else { -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 UIWindowForModal = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 UIWindowForModal.windowLevel = UIWindowLevelNormal; }); return UIWindowForModal; @@ -47,23 +43,18 @@ + (UIWindow *)windowForNonBlockingView { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { UIWindowForBanner = [[self class] iOS13PlusBannerWindow]; } else { -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 UIWindowForBanner = [[FIRIAMBannerViewUIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 } -#endif UIWindowForBanner.windowLevel = UIWindowLevelNormal; }); return UIWindowForBanner; } -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + (UIWindow *)iOS13PlusWindow API_AVAILABLE(ios(13.0)) { UIWindowScene *foregroundedScene = [[UIApplication sharedApplication] fir_foregroundWindowScene]; if (foregroundedScene.delegate) { @@ -82,7 +73,6 @@ + (FIRIAMBannerViewUIWindow *)iOS13PlusBannerWindow API_AVAILABLE(ios(13.0)) { } } -#endif @end #endif // TARGET_OS_IOS diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/ImageOnly/FIRIAMImageOnlyViewController.m b/FirebaseInAppMessaging/Sources/DefaultUI/ImageOnly/FIRIAMImageOnlyViewController.m index ffc37a408ec..6f57f6e0d0a 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/ImageOnly/FIRIAMImageOnlyViewController.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/ImageOnly/FIRIAMImageOnlyViewController.m @@ -124,11 +124,7 @@ - (void)viewDidLayoutSubviews { CGFloat maxImageViewHeight = self.view.window.frame.size.height - minimalMargine * 2; // Factor in space for the top notch on iPhone X*. -#if defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if (@available(iOS 11.0, *)) { - maxImageViewHeight -= self.view.safeAreaInsets.top; - } -#endif // defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + maxImageViewHeight -= self.view.safeAreaInsets.top; CGFloat adjustedImageViewHeight = self.imageOriginalSize.height; CGFloat adjustedImageViewWidth = self.imageOriginalSize.width; diff --git a/FirebaseInAppMessaging/Sources/DefaultUI/Modal/FIRIAMModalViewController.m b/FirebaseInAppMessaging/Sources/DefaultUI/Modal/FIRIAMModalViewController.m index e605e239cc6..efad4f62396 100644 --- a/FirebaseInAppMessaging/Sources/DefaultUI/Modal/FIRIAMModalViewController.m +++ b/FirebaseInAppMessaging/Sources/DefaultUI/Modal/FIRIAMModalViewController.m @@ -288,11 +288,7 @@ - (void)layoutFineTuneInPortraitMode { TopBottomPaddingAroundMsgCard * 2; // Factor in space for the top notch on iPhone X*. -#if defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 - if (@available(iOS 11.0, *)) { - heightCalcReference -= self.view.safeAreaInsets.top; - } -#endif // defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 + heightCalcReference -= self.view.safeAreaInsets.top; } FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004", diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m index 20862570659..2f6bef92af7 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m @@ -113,14 +113,12 @@ - (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce selector:@selector(appWillBecomeInactive:) name:UIApplicationWillResignActiveNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillBecomeInactive:) name:UISceneWillDeactivateNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (loadFromCache) { @try { [self loadFromCachePath:nil]; diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m index 078b7d39706..1bf6abb5298 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m @@ -33,7 +33,6 @@ - (void)start { selector:@selector(checkAndDisplayNextAppForegroundMessageFromForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self @@ -41,7 +40,6 @@ - (void)start { name:UISceneWillEnterForegroundNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 } - (void)checkAndDisplayNextAppForegroundMessageFromForeground:(NSNotification *)notification { diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m index c4ec0def848..b78bdd3eb01 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m @@ -355,7 +355,6 @@ - (void)displayMessageLoadError:(NSError *)error { [alert addAction:defaultAction]; dispatch_async(dispatch_get_main_queue(), ^{ -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { UIWindowScene *foregroundedScene = [[UIApplication sharedApplication] fir_foregroundWindowScene]; @@ -364,10 +363,9 @@ - (void)displayMessageLoadError:(NSError *)error { return; } self.alertWindow = [[UIWindow alloc] initWithWindowScene:foregroundedScene]; + } else { + self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; } -#else // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 - self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; -#endif UIViewController *alertViewController = [[UIViewController alloc] init]; self.alertWindow.rootViewController = alertViewController; self.alertWindow.hidden = NO; diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m index 0a217b1faa6..f2bd8731f2a 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m @@ -29,14 +29,12 @@ - (void)start { selector:@selector(appWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; -#if defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 if (@available(iOS 13.0, tvOS 13.0, *)) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(appWillEnterForeground:) name:UISceneWillEnterForegroundNotification object:nil]; } -#endif // defined(__IPHONE_13_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 } - (void)appWillEnterForeground:(NSNotification *)notification { From 554255aa0964b66daab4e77e47bee2a80a8f9d09 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 16:18:00 -0400 Subject: [PATCH 024/103] [Infra] Clean up visionOS conditionals (#12934) --- FirebaseCore/Sources/FIRApp.m | 2 +- FirebaseCore/Tests/Unit/FIRAppTest.m | 2 +- .../Sources/Core/FPersistentConnection.m | 3 +-- FirebaseDatabase/Sources/Core/FRepo.m | 6 ++---- .../Sources/Persistence/FLevelDBStorageEngine.m | 3 +-- .../Sources/Realtime/FWebSocketConnection.m | 3 +-- .../Analytics/FIRIAMAnalyticsEventLoggerImpl.m | 4 ++-- .../Analytics/FIRIAMClearcutHttpRequestSender.m | 4 ++-- .../Analytics/FIRIAMClearcutLogStorage.m | 4 ++-- .../Sources/Analytics/FIRIAMClearcutLogger.m | 4 ++-- .../Sources/Analytics/FIRIAMClearcutUploader.m | 4 ++-- .../Sources/Data/FIRIAMFetchResponseParser.m | 4 ++-- .../Data/FIRIAMMessageContentDataWithImageURL.m | 4 ++-- .../Sources/Data/FIRIAMMessageDefinition.m | 4 ++-- .../Sources/Data/FIRIAMRenderingEffectSetting.m | 4 ++-- .../FIRIAMDisplayTriggerDefinition.m | 4 ++-- .../Sources/FIRCore+InAppMessaging.m | 4 ++-- .../Sources/FIRInAppMessaging.m | 4 ++-- .../Sources/Flows/FIRIAMActivityLogger.m | 4 ++-- .../Sources/Flows/FIRIAMBookKeeper.m | 4 ++-- .../Sources/Flows/FIRIAMClientInfoFetcher.m | 4 ++-- .../FIRIAMDisplayCheckOnAnalyticEventsFlow.m | 4 ++-- .../FIRIAMDisplayCheckOnAppForegroundFlow.m | 4 ++-- ...IAMDisplayCheckOnFetchDoneNotificationFlow.m | 4 ++-- .../Flows/FIRIAMDisplayCheckTriggerFlow.m | 4 ++-- .../Sources/Flows/FIRIAMDisplayExecutor.m | 4 ++-- .../Sources/Flows/FIRIAMFetchFlow.m | 4 ++-- .../Flows/FIRIAMFetchOnAppForegroundFlow.m | 4 ++-- .../Sources/Flows/FIRIAMMessageClientCache.m | 4 ++-- .../Flows/FIRIAMMsgFetcherUsingRestful.m | 4 ++-- .../Sources/Flows/FIRIAMServerMsgFetchStorage.m | 4 ++-- .../FIRInAppMessagingRenderingDataClasses.m | 4 ++-- .../Sources/Runtime/FIRIAMActionURLFollower.m | 4 ++-- .../Sources/Runtime/FIRIAMRuntimeManager.m | 4 ++-- .../Sources/Runtime/FIRIAMSDKModeManager.m | 4 ++-- .../Sources/Runtime/FIRIAMSDKSettings.m | 4 ++-- .../Runtime/FIRInAppMessaging+Bootstrap.m | 4 ++-- .../Sources/Util/FIRIAMElapsedTimeTracker.m | 4 ++-- .../Sources/Util/FIRIAMTimeFetcher.m | 4 ++-- .../Sources/Util/NSString+FIRInterlaceStrings.m | 4 ++-- .../UIApplication+FIRForegroundWindowScene.m | 4 ++-- .../Sources/Util/UIColor+FIRIAMHexString.m | 4 ++-- .../Sources/FIRMessagingUtilities.m | 3 +-- .../Sources/Token/FIRMessagingAuthKeychain.m | 2 +- .../src/remote/connectivity_monitor_apple.mm | 17 ++++++----------- Firestore/core/src/util/filesystem_apple.mm | 5 ++--- .../FirebaseFirestoreWrap/dummy.m | 2 +- .../FirebaseInAppMessagingWrap/dummy.m | 2 +- 48 files changed, 91 insertions(+), 103 deletions(-) diff --git a/FirebaseCore/Sources/FIRApp.m b/FirebaseCore/Sources/FIRApp.m index 8f4aefe4bc7..196cba65f8f 100644 --- a/FirebaseCore/Sources/FIRApp.m +++ b/FirebaseCore/Sources/FIRApp.m @@ -855,7 +855,7 @@ + (void)registerSwiftComponents { #pragma mark - App Life Cycle - (void)subscribeForAppDidBecomeActiveNotifications { -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION NSNotificationName notificationName = UIApplicationDidBecomeActiveNotification; #elif TARGET_OS_OSX NSNotificationName notificationName = NSApplicationDidBecomeActiveNotification; diff --git a/FirebaseCore/Tests/Unit/FIRAppTest.m b/FirebaseCore/Tests/Unit/FIRAppTest.m index 285c76500c3..b231e608b92 100644 --- a/FirebaseCore/Tests/Unit/FIRAppTest.m +++ b/FirebaseCore/Tests/Unit/FIRAppTest.m @@ -862,7 +862,7 @@ - (XCTestExpectation *)expectNotificationNamed:(NSNotificationName)name } - (NSNotificationName)appDidBecomeActiveNotificationName { -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION return UIApplicationDidBecomeActiveNotification; #elif TARGET_OS_OSX return NSApplicationDidBecomeActiveNotification; diff --git a/FirebaseDatabase/Sources/Core/FPersistentConnection.m b/FirebaseDatabase/Sources/Core/FPersistentConnection.m index 3d1e8af9053..da2820d5deb 100644 --- a/FirebaseDatabase/Sources/Core/FPersistentConnection.m +++ b/FirebaseDatabase/Sources/Core/FPersistentConnection.m @@ -1256,8 +1256,7 @@ - (void)sendStats:(NSDictionary *)stats { - (void)sendConnectStats { NSMutableDictionary *stats = [NSMutableDictionary dictionary]; -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION if (self.config.persistenceEnabled) { stats[@"persistence.ios.enabled"] = @1; } diff --git a/FirebaseDatabase/Sources/Core/FRepo.m b/FirebaseDatabase/Sources/Core/FRepo.m index 00b0850cd17..628de6a719a 100644 --- a/FirebaseDatabase/Sources/Core/FRepo.m +++ b/FirebaseDatabase/Sources/Core/FRepo.m @@ -52,8 +52,7 @@ #import "FirebaseDatabase/Sources/Utilities/Tuples/FTupleTransaction.h" #import -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #endif @@ -816,8 +815,7 @@ - (void)didEnterBackground { // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual // release build. -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION // The idea is to wait until any outstanding sets get written to disk. Since // the sets might still be in our dispatch queue, we wait for the dispatch // queue to catch up and for persistence to catch up. This may be diff --git a/FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.m b/FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.m index 6516baa43c7..4dd808da3a1 100644 --- a/FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.m +++ b/FirebaseDatabase/Sources/Persistence/FLevelDBStorageEngine.m @@ -275,8 +275,7 @@ - (void)close { } + (NSString *)firebaseDir { -#if TARGET_OS_IOS || TARGET_OS_WATCH || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_VISION NSArray *dirPaths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDir = [dirPaths objectAtIndex:0]; diff --git a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m index 7edd0abb3e6..171b75156a9 100644 --- a/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m +++ b/FirebaseDatabase/Sources/Realtime/FWebSocketConnection.m @@ -164,8 +164,7 @@ - (NSString *)userAgent { // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual // release build. -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION Class uiDeviceClass = NSClassFromString(@"UIDevice"); if (uiDeviceClass) { systemVersion = [uiDeviceClass currentDevice].systemVersion; diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m index 8176cb83c68..f4f8a8bc577 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMAnalyticsEventLoggerImpl.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -176,4 +176,4 @@ - (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.m index 881621eb258..955aab345d4 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutHttpRequestSender.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -204,4 +204,4 @@ - (void)sendClearcutHttpRequestForLogs:(NSArray *)log } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m index d526f4ea427..c0610fc5032 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogStorage.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -207,4 +207,4 @@ - (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogger.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogger.m index ea00175ebd0..be16272c896 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogger.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutLogger.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -214,4 +214,4 @@ - (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m index 0ca896f7772..f424f51f878 100644 --- a/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m +++ b/FirebaseInAppMessaging/Sources/Analytics/FIRIAMClearcutUploader.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #import @@ -244,4 +244,4 @@ - (void)scheduleNextSend { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m b/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m index 8a85321de6e..7becace2949 100644 --- a/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m +++ b/FirebaseInAppMessaging/Sources/Data/FIRIAMFetchResponseParser.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -399,4 +399,4 @@ - (NSString *)sanitizedURLStringFromString:(NSString *)string { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageContentDataWithImageURL.m b/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageContentDataWithImageURL.m index 60b0b78bbcc..abe867bf6d6 100644 --- a/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageContentDataWithImageURL.m +++ b/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageContentDataWithImageURL.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -206,4 +206,4 @@ - (void)fetchImageFromURL:(NSURL *)imageURL @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageDefinition.m b/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageDefinition.m index fb564630bca..4d72b82ac20 100644 --- a/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageDefinition.m +++ b/FirebaseInAppMessaging/Sources/Data/FIRIAMMessageDefinition.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h" @@ -109,4 +109,4 @@ - (BOOL)messageHasStarted { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Data/FIRIAMRenderingEffectSetting.m b/FirebaseInAppMessaging/Sources/Data/FIRIAMRenderingEffectSetting.m index eeefee2d24e..1530049c6ba 100644 --- a/FirebaseInAppMessaging/Sources/Data/FIRIAMRenderingEffectSetting.m +++ b/FirebaseInAppMessaging/Sources/Data/FIRIAMRenderingEffectSetting.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMRenderingEffectSetting.h" @@ -34,4 +34,4 @@ + (instancetype)getDefaultRenderingEffectSetting { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m b/FirebaseInAppMessaging/Sources/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m index 766ec4cc4cf..9de6b8a804b 100644 --- a/FirebaseInAppMessaging/Sources/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m +++ b/FirebaseInAppMessaging/Sources/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h" @@ -43,4 +43,4 @@ - (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.m b/FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.m index 58c3a741cc3..db5ae0816e0 100644 --- a/FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.m +++ b/FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h" @@ -23,4 +23,4 @@ NSString *const kFirebaseInAppMessagingErrorDomain = @"com.firebase.inappmessaging"; FIRLoggerService kFIRLoggerInAppMessaging = @"[FirebaseInAppMessaging]"; -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/FIRInAppMessaging.m b/FirebaseInAppMessaging/Sources/FIRInAppMessaging.m index 3f9595582ac..8b65d0a4efa 100644 --- a/FirebaseInAppMessaging/Sources/FIRInAppMessaging.m +++ b/FirebaseInAppMessaging/Sources/FIRInAppMessaging.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Public/FirebaseInAppMessaging/FIRInAppMessaging.h" @@ -145,4 +145,4 @@ - (void)triggerEvent:(NSString *)eventName { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m index 2f6bef92af7..dac6a28f829 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMActivityLogger.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -243,4 +243,4 @@ - (void)addLogRecord:(FIRIAMActivityRecord *)newRecord { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMBookKeeper.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMBookKeeper.m index db85d8723c5..5e17566b4cc 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMBookKeeper.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMBookKeeper.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -264,4 +264,4 @@ - (void)cleanupFetchRecords { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMClientInfoFetcher.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMClientInfoFetcher.m index 7d32c188a02..f9363e438ca 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMClientInfoFetcher.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMClientInfoFetcher.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h" @@ -133,4 +133,4 @@ - (NSString *)getIAMSDKVersion { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m index 63e7651dc96..4e3a1491919 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" #import "Interop/Analytics/Public/FIRAnalyticsInterop.h" @@ -68,4 +68,4 @@ - (void)stop { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m index 1bf6abb5298..27fc9492ba1 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -65,4 +65,4 @@ - (void)dealloc { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m index 45d4b39f563..6983910c051 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -64,4 +64,4 @@ - (void)dealloc { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckTriggerFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckTriggerFlow.m index b7f3137ad18..6ab0c90c41f 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckTriggerFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayCheckTriggerFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMDisplayCheckTriggerFlow.h" @@ -34,4 +34,4 @@ - (void)stop { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m index b78bdd3eb01..61d756d7b35 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMDisplayExecutor.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -776,4 +776,4 @@ - (void)checkAndDisplayNextAppForegroundMessage { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchFlow.m index 38f8c181ffe..890fece4d57 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -267,4 +267,4 @@ - (void)checkForAppLaunchMessage { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m index f2bd8731f2a..946a9ec56b2 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMFetchOnAppForegroundFlow.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -59,4 +59,4 @@ - (void)dealloc { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m index 37f24531edd..844272ceaa0 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMessageClientCache.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -238,4 +238,4 @@ - (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fet } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMsgFetcherUsingRestful.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMsgFetcherUsingRestful.m index 772148b6018..77053791922 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMMsgFetcherUsingRestful.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMMsgFetcherUsingRestful.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -277,4 +277,4 @@ - (void)fetchMessagesWithImpressionList:(NSArray *)imp } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Flows/FIRIAMServerMsgFetchStorage.m b/FirebaseInAppMessaging/Sources/Flows/FIRIAMServerMsgFetchStorage.m index 713550f8685..dfe42933697 100644 --- a/FirebaseInAppMessaging/Sources/Flows/FIRIAMServerMsgFetchStorage.m +++ b/FirebaseInAppMessaging/Sources/Flows/FIRIAMServerMsgFetchStorage.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseCore/Extension/FirebaseCoreInternal.h" @@ -66,4 +66,4 @@ - (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))c } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m b/FirebaseInAppMessaging/Sources/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m index 36d81939500..663c72b8438 100644 --- a/FirebaseInAppMessaging/Sources/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m +++ b/FirebaseInAppMessaging/Sources/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -371,4 +371,4 @@ - (instancetype)initWithActionText:(nullable NSString *)actionText @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m index cbcae06e2cb..f16936f9423 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMActionURLFollower.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #import @@ -227,4 +227,4 @@ + (BOOL)isHttpOrHttpsScheme:(NSURL *)url { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMRuntimeManager.m b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMRuntimeManager.m index 499b6fddc9b..b3fb094c67b 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMRuntimeManager.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMRuntimeManager.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -446,4 +446,4 @@ - (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKModeManager.m b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKModeManager.m index 6aa77c29f8d..39d51af13ca 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKModeManager.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKModeManager.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import @@ -117,4 +117,4 @@ - (FIRIAMSDKMode)currentMode { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKSettings.m b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKSettings.m index eccb73beaea..d2d05f6267b 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKSettings.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRIAMSDKSettings.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMSDKSettings.h" @@ -37,4 +37,4 @@ - (NSString *)description { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Runtime/FIRInAppMessaging+Bootstrap.m b/FirebaseInAppMessaging/Sources/Runtime/FIRInAppMessaging+Bootstrap.m index 1d1c773bf54..800c70787f6 100644 --- a/FirebaseInAppMessaging/Sources/Runtime/FIRInAppMessaging+Bootstrap.m +++ b/FirebaseInAppMessaging/Sources/Runtime/FIRInAppMessaging+Bootstrap.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRInAppMessaging+Bootstrap.h" @@ -136,4 +136,4 @@ + (void)exitAppWithFatalError:(NSError *)error { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Util/FIRIAMElapsedTimeTracker.m b/FirebaseInAppMessaging/Sources/Util/FIRIAMElapsedTimeTracker.m index b299f17b3c7..d60a8e48c58 100644 --- a/FirebaseInAppMessaging/Sources/Util/FIRIAMElapsedTimeTracker.m +++ b/FirebaseInAppMessaging/Sources/Util/FIRIAMElapsedTimeTracker.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Util/FIRIAMElapsedTimeTracker.h" @interface FIRIAMElapsedTimeTracker () @@ -58,4 +58,4 @@ - (instancetype)initWithTimeFetcher:(id)timeFetcher { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Util/FIRIAMTimeFetcher.m b/FirebaseInAppMessaging/Sources/Util/FIRIAMTimeFetcher.m index c94798d87dd..1c74a24e264 100644 --- a/FirebaseInAppMessaging/Sources/Util/FIRIAMTimeFetcher.m +++ b/FirebaseInAppMessaging/Sources/Util/FIRIAMTimeFetcher.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h" @@ -25,4 +25,4 @@ - (NSTimeInterval)currentTimestampInSeconds { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Util/NSString+FIRInterlaceStrings.m b/FirebaseInAppMessaging/Sources/Util/NSString+FIRInterlaceStrings.m index ed2671daa9a..787da932356 100644 --- a/FirebaseInAppMessaging/Sources/Util/NSString+FIRInterlaceStrings.m +++ b/FirebaseInAppMessaging/Sources/Util/NSString+FIRInterlaceStrings.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Util/NSString+FIRInterlaceStrings.h" @@ -44,4 +44,4 @@ + (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)s @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Util/UIApplication+FIRForegroundWindowScene.m b/FirebaseInAppMessaging/Sources/Util/UIApplication+FIRForegroundWindowScene.m index ad0a5ec22c4..1658ad41c74 100644 --- a/FirebaseInAppMessaging/Sources/Util/UIApplication+FIRForegroundWindowScene.m +++ b/FirebaseInAppMessaging/Sources/Util/UIApplication+FIRForegroundWindowScene.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Private/Util/UIApplication+FIRForegroundWindowScene.h" @@ -37,4 +37,4 @@ - (nullable UIWindowScene *)fir_foregroundWindowScene { @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.m b/FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.m index 3374623070a..34c9e220e0e 100644 --- a/FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.m +++ b/FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.m @@ -15,7 +15,7 @@ */ #import -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import "FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.h" @@ -41,4 +41,4 @@ + (UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString { } @end -#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION diff --git a/FirebaseMessaging/Sources/FIRMessagingUtilities.m b/FirebaseMessaging/Sources/FIRMessagingUtilities.m index 713febdb833..2d9bddb5187 100644 --- a/FirebaseMessaging/Sources/FIRMessagingUtilities.m +++ b/FirebaseMessaging/Sources/FIRMessagingUtilities.m @@ -311,8 +311,7 @@ BOOL FIRMessagingIsProductionApp(void) { #if TARGET_OS_OSX || TARGET_OS_MACCATALYST NSString *path = [[[[NSBundle mainBundle] resourcePath] stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"embedded.provisionprofile"]; -#elif TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#elif TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH || TARGET_OS_VISION NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"embedded.mobileprovision"]; #endif diff --git a/FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.m b/FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.m index 51e2f955334..544d1718d80 100644 --- a/FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.m +++ b/FirebaseMessaging/Sources/Token/FIRMessagingAuthKeychain.m @@ -92,7 +92,7 @@ - (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NS NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account]; NSMutableArray *results; keychainQuery[(__bridge id)kSecReturnData] = (__bridge id)kCFBooleanTrue; -#if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION keychainQuery[(__bridge id)kSecReturnAttributes] = (__bridge id)kCFBooleanTrue; keychainQuery[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll; // FIRMessagingKeychain should only take a query and return a result, will handle the query here. diff --git a/Firestore/core/src/remote/connectivity_monitor_apple.mm b/Firestore/core/src/remote/connectivity_monitor_apple.mm index 3fccfc6ef16..282733cd405 100644 --- a/Firestore/core/src/remote/connectivity_monitor_apple.mm +++ b/Firestore/core/src/remote/connectivity_monitor_apple.mm @@ -18,8 +18,7 @@ #if defined(__APPLE__) -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION #import #endif @@ -50,7 +49,7 @@ NetworkStatus ToNetworkStatus(SCNetworkReachabilityFlags flags) { return NetworkStatus::Unavailable; } -#if TARGET_OS_IPHONE || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IPHONE || TARGET_OS_VISION if (flags & kSCNetworkReachabilityFlagsIsWWAN) { return NetworkStatus::AvailableViaCellular; } @@ -113,8 +112,7 @@ explicit ConnectivityMonitorApple( return; } -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION this->observer_ = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil @@ -126,8 +124,7 @@ explicit ConnectivityMonitorApple( } ~ConnectivityMonitorApple() { -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION [[NSNotificationCenter defaultCenter] removeObserver:this->observer_]; #endif @@ -142,8 +139,7 @@ explicit ConnectivityMonitorApple( } } -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION void OnEnteredForeground() { SCNetworkReachabilityFlags flags{}; if (!SCNetworkReachabilityGetFlags(reachability_, &flags)) return; @@ -171,8 +167,7 @@ void OnReachabilityChanged(SCNetworkReachabilityFlags flags) { private: SCNetworkReachabilityRef reachability_ = nil; -#if TARGET_OS_IOS || TARGET_OS_TV || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION id observer_ = nil; #endif }; diff --git a/Firestore/core/src/util/filesystem_apple.mm b/Firestore/core/src/util/filesystem_apple.mm index 9802017a6f8..5a553476e8a 100644 --- a/Firestore/core/src/util/filesystem_apple.mm +++ b/Firestore/core/src/util/filesystem_apple.mm @@ -45,8 +45,7 @@ } StatusOr Filesystem::AppDataDir(absl::string_view app_name) { -#if TARGET_OS_IOS || TARGET_OS_OSX || \ - (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_OSX || TARGET_OS_VISION NSArray* directories = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSUserDomainMask, YES); return Path::FromNSString(directories[0]).AppendUtf8(app_name); @@ -62,7 +61,7 @@ } StatusOr Filesystem::LegacyDocumentsDir(absl::string_view app_name) { -#if TARGET_OS_IOS || (defined(TARGET_OS_VISION) && TARGET_OS_VISION) +#if TARGET_OS_IOS || TARGET_OS_VISION NSArray* directories = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES); return Path::FromNSString(directories[0]).AppendUtf8(app_name); diff --git a/SwiftPM-PlatformExclude/FirebaseFirestoreWrap/dummy.m b/SwiftPM-PlatformExclude/FirebaseFirestoreWrap/dummy.m index dfc56395493..4b3c19101cf 100644 --- a/SwiftPM-PlatformExclude/FirebaseFirestoreWrap/dummy.m +++ b/SwiftPM-PlatformExclude/FirebaseFirestoreWrap/dummy.m @@ -17,7 +17,7 @@ #warning "Firebase Firestore does not support watchOS" #endif -#if (defined(TARGET_OS_VISION) && TARGET_OS_VISION) && FIREBASE_BINARY_FIRESTORE +#if TARGET_OS_VISION && FIREBASE_BINARY_FIRESTORE #error "Firebase Firestore's binary SPM distribution does not support \ visionOS. To enable the source distribution, quit Xcode and open the desired \ project from the command line with the FIREBASE_SOURCE_FIRESTORE environment \ diff --git a/SwiftPM-PlatformExclude/FirebaseInAppMessagingWrap/dummy.m b/SwiftPM-PlatformExclude/FirebaseInAppMessagingWrap/dummy.m index df4b2c913bd..d58ce4ca51c 100644 --- a/SwiftPM-PlatformExclude/FirebaseInAppMessagingWrap/dummy.m +++ b/SwiftPM-PlatformExclude/FirebaseInAppMessagingWrap/dummy.m @@ -13,6 +13,6 @@ // limitations under the License. #import -#if !(TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)) +#if !(TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION) #warning "Firebase In App Messaging only supports the iOS, tvOS and visionOS platforms." #endif From 520fb3e597bc7fdb4150dfecb7b78dafc5b91564 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Fri, 10 May 2024 22:37:12 -0400 Subject: [PATCH 025/103] [Infra] Re-enable 'zip / quickstart_framework_firestore' job (#12943) --- .github/workflows/zip.yml | 103 ++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/.github/workflows/zip.yml b/.github/workflows/zip.yml index 00d2dbbe93a..fb4d056edfe 100644 --- a/.github/workflows/zip.yml +++ b/.github/workflows/zip.yml @@ -453,56 +453,59 @@ jobs: name: quickstart_artifacts_dynamiclinks path: quickstart-ios/ - # quickstart_framework_firestore: - # # Don't run on private repo. - # if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' - # needs: package-head - # env: - # plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} - # SDK: "Firestore" - # strategy: - # matrix: - # # TODO: Building FirebaseUI fails on Xcode 15 because it needs to sign the resources. - # os: [macos-13] - # artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] - # xcode: [Xcode_15.2] - # runs-on: ${{ matrix.os }} - # steps: - # - uses: actions/checkout@v4 - # - name: Get framework dir - # uses: actions/download-artifact@v4 - # with: - # name: ${{ matrix.artifact }} - # - uses: ruby/setup-ruby@v1 - # - name: Setup Bundler - # run: ./scripts/setup_bundler.sh - # - name: Move frameworks - # run: | - # mkdir -p "${HOME}"/ios_frameworks/ - # find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + - # - uses: actions/checkout@v4 - # - name: Setup quickstart - # run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ - # "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ - # "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* - # - name: Xcode - # run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - # - name: Install Secret GoogleService-Info.plist - # run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ - # quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" - # - name: Test Quickstart - # run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") - # - name: Remove data before upload - # if: ${{ failure() }} - # run: scripts/remove_data.sh firestore - # - uses: actions/upload-artifact@v4 - # if: ${{ failure() }} - # with: - # name: quickstart_artifacts_firestore - # path: quickstart-ios/ + quickstart_framework_firestore: + # Don't run on private repo. + if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + needs: package-head + env: + plist_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + signin_secret: ${{ secrets.GHASecretsGPGPassphrase1 }} + SDK: "Firestore" + strategy: + matrix: + os: [macos-13, macos-14] + artifact: [Firebase-actions-dir, Firebase-actions-dir-dynamic] + include: + - os: macos-13 + xcode: Xcode_15.2 + - os: macos-14 + xcode: Xcode_15.3 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Get framework dir + uses: actions/download-artifact@v4 + with: + name: ${{ matrix.artifact }} + - uses: ruby/setup-ruby@v1 + - name: Setup Bundler + run: ./scripts/setup_bundler.sh + - name: Move frameworks + run: | + mkdir -p "${HOME}"/ios_frameworks/ + find "${GITHUB_WORKSPACE}" -name "Firebase*latest.zip" -exec unzip -d "${HOME}"/ios_frameworks/ {} + + - uses: actions/checkout@v4 + - name: Setup quickstart + run: SAMPLE="$SDK" TARGET="${SDK}Example" NON_FIREBASE_SDKS="SDWebImage FirebaseAuthUI FirebaseEmailAuthUI" scripts/setup_quickstart_framework.sh \ + "${HOME}"/ios_frameworks/Firebase/NonFirebaseSDKs/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseFirestore/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseAuth/* \ + "${HOME}"/ios_frameworks/Firebase/FirebaseAnalytics/* + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Install Secret GoogleService-Info.plist + run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/qs-firestore.plist.gpg \ + quickstart-ios/firestore/GoogleService-Info.plist "$plist_secret" + - name: Test Quickstart + run: ([ -z $plist_secret ] || scripts/third_party/travis/retry.sh scripts/test_quickstart_framework.sh "${SDK}") + - name: Remove data before upload + if: ${{ failure() }} + run: scripts/remove_data.sh firestore + - uses: actions/upload-artifact@v4 + if: ${{ failure() }} + with: + name: quickstart_artifacts_firestore + path: quickstart-ios/ check_framework_firestore_symbols: # Don't run on private repo. From 50d0d29cbb89fc98220db3ad15390fb6bc98eee5 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 18 Mar 2024 19:27:53 +0000 Subject: [PATCH 026/103] Fork `google-generative-ai` for Firebase (#12564) Co-authored-by: Morgan Chen Co-authored-by: Paul Beusterien Co-authored-by: Peter Friese Co-authored-by: Ryan Wilson --- FirebaseVertexAI/Sources/Chat.swift | 184 +++++++++++ .../Sources/CountTokensRequest.swift | 45 +++ FirebaseVertexAI/Sources/Errors.swift | 189 +++++++++++ .../Sources/GenerateContentError.swift | 44 +++ .../Sources/GenerateContentRequest.swift | 49 +++ .../Sources/GenerateContentResponse.swift | 286 +++++++++++++++++ .../Sources/GenerationConfig.swift | 86 +++++ .../Sources/GenerativeAIRequest.swift | 46 +++ .../Sources/GenerativeAIService.swift | 257 +++++++++++++++ .../Sources/GenerativeAISwift.swift | 26 ++ .../Sources/GenerativeModel.swift | 298 ++++++++++++++++++ FirebaseVertexAI/Sources/Logging.swift | 56 ++++ FirebaseVertexAI/Sources/ModelContent.swift | 141 +++++++++ .../Sources/PartsRepresentable+Image.swift | 107 +++++++ .../Sources/PartsRepresentable.swift | 66 ++++ FirebaseVertexAI/Sources/Safety.swift | 182 +++++++++++ Package.swift | 6 +- 17 files changed, 2067 insertions(+), 1 deletion(-) create mode 100644 FirebaseVertexAI/Sources/Chat.swift create mode 100644 FirebaseVertexAI/Sources/CountTokensRequest.swift create mode 100644 FirebaseVertexAI/Sources/Errors.swift create mode 100644 FirebaseVertexAI/Sources/GenerateContentError.swift create mode 100644 FirebaseVertexAI/Sources/GenerateContentRequest.swift create mode 100644 FirebaseVertexAI/Sources/GenerateContentResponse.swift create mode 100644 FirebaseVertexAI/Sources/GenerationConfig.swift create mode 100644 FirebaseVertexAI/Sources/GenerativeAIRequest.swift create mode 100644 FirebaseVertexAI/Sources/GenerativeAIService.swift create mode 100644 FirebaseVertexAI/Sources/GenerativeAISwift.swift create mode 100644 FirebaseVertexAI/Sources/GenerativeModel.swift create mode 100644 FirebaseVertexAI/Sources/Logging.swift create mode 100644 FirebaseVertexAI/Sources/ModelContent.swift create mode 100644 FirebaseVertexAI/Sources/PartsRepresentable+Image.swift create mode 100644 FirebaseVertexAI/Sources/PartsRepresentable.swift create mode 100644 FirebaseVertexAI/Sources/Safety.swift diff --git a/FirebaseVertexAI/Sources/Chat.swift b/FirebaseVertexAI/Sources/Chat.swift new file mode 100644 index 00000000000..c7cfb859fa8 --- /dev/null +++ b/FirebaseVertexAI/Sources/Chat.swift @@ -0,0 +1,184 @@ +// 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 + +/// 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 11.0, macCatalyst 15.0, *) +public class Chat { + private let model: GenerativeModel + + /// Initializes a new chat representing a 1:1 conversation between model and user. + init(model: GenerativeModel, history: [ModelContent]) { + self.model = model + self.history = history + } + + /// 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] + + /// See ``sendMessage(_:)-3ify5``. + public func sendMessage(_ parts: any ThrowingPartsRepresentable...) async throws + -> GenerateContentResponse { + return try await sendMessage([ModelContent(parts: parts)]) + } + + /// 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. + /// - Parameter content: The new content to send as a single chat message. + /// - Returns: The model's response if no error occurred. + /// - Throws: A ``GenerateContentError`` if an error occurred. + public func sendMessage(_ content: @autoclosure () throws -> [ModelContent]) async throws + -> GenerateContentResponse { + // Ensure that the new content has the role set. + let newContent: [ModelContent] + do { + newContent = try content().map(populateContentRole(_:)) + } catch let underlying { + if let contentError = underlying as? ImageConversionError { + throw GenerateContentError.promptImageContentError(underlying: contentError) + } else { + throw GenerateContentError.internalError(underlying: underlying) + } + } + + // Send the history alongside the new message as context. + let request = history + newContent + let result = try await model.generateContent(request) + guard let reply = result.candidates.first?.content else { + let error = NSError(domain: "com.google.generative-ai", + code: -1, + userInfo: [ + NSLocalizedDescriptionKey: "No candidates with content available.", + ]) + throw GenerateContentError.internalError(underlying: error) + } + + // Make sure we inject the role into the content received. + let toAdd = ModelContent(role: "model", parts: reply.parts) + + // Append the request and successful result to history, then return the value. + history.append(contentsOf: newContent) + history.append(toAdd) + return result + } + + /// See ``sendMessageStream(_:)-4abs3``. + @available(macOS 12.0, *) + public func sendMessageStream(_ parts: any ThrowingPartsRepresentable...) + -> AsyncThrowingStream { + return try sendMessageStream([ModelContent(parts: parts)]) + } + + /// 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. + /// - Parameter content: The new content to send as a single chat message. + /// - Returns: A stream containing the model's response or an error if an error occurred. + @available(macOS 12.0, *) + public func sendMessageStream(_ content: @autoclosure () throws -> [ModelContent]) + -> AsyncThrowingStream { + let resolvedContent: [ModelContent] + do { + resolvedContent = try content() + } catch let underlying { + return AsyncThrowingStream { continuation in + let error: Error + if let contentError = underlying as? ImageConversionError { + error = GenerateContentError.promptImageContentError(underlying: contentError) + } else { + error = GenerateContentError.internalError(underlying: underlying) + } + continuation.finish(throwing: error) + } + } + + return AsyncThrowingStream { continuation in + Task { + var aggregatedContent: [ModelContent] = [] + + // Ensure that the new content has the role set. + let newContent: [ModelContent] = resolvedContent.map(populateContentRole(_:)) + + // Send the history alongside the new message as context. + let request = history + newContent + let stream = 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. + if let chunkContent = chunk.candidates.first?.content { + aggregatedContent.append(chunkContent) + } + + // Pass along the chunk. + continuation.yield(chunk) + } + } catch { + // Rethrow the error that the underlying stream threw. Don't add anything to history. + continuation.finish(throwing: error) + return + } + + // Save the request. + history.append(contentsOf: newContent) + + // Aggregate the content to add it to the history before we finish. + let aggregated = aggregatedChunks(aggregatedContent) + history.append(aggregated) + + continuation.finish() + } + } + } + + private func aggregatedChunks(_ chunks: [ModelContent]) -> ModelContent { + var parts: [ModelContent.Part] = [] + var combinedText = "" + for aggregate in chunks { + // Loop through all the parts, aggregating the text and adding the images. + for part in aggregate.parts { + switch part { + case let .text(str): + combinedText += str + + case .data(mimetype: _, _): + // Don't combine it, just add to the content. If there's any text pending, add that as + // a part. + if !combinedText.isEmpty { + parts.append(.text(combinedText)) + combinedText = "" + } + + parts.append(part) + } + } + } + + if !combinedText.isEmpty { + parts.append(.text(combinedText)) + } + + return ModelContent(role: "model", parts: parts) + } + + /// Populates the `role` field with `user` if it doesn't exist. Required in chat sessions. + private func populateContentRole(_ content: ModelContent) -> ModelContent { + if content.role != nil { + return content + } else { + return ModelContent(role: "user", parts: content.parts) + } + } +} diff --git a/FirebaseVertexAI/Sources/CountTokensRequest.swift b/FirebaseVertexAI/Sources/CountTokensRequest.swift new file mode 100644 index 00000000000..0a58d40acc0 --- /dev/null +++ b/FirebaseVertexAI/Sources/CountTokensRequest.swift @@ -0,0 +1,45 @@ +// 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 + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +struct CountTokensRequest { + let model: String + let contents: [ModelContent] + let options: RequestOptions +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CountTokensRequest: Encodable { + enum CodingKeys: CodingKey { + case contents + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CountTokensRequest: GenerativeAIRequest { + typealias Response = CountTokensResponse + + var url: URL { + URL(string: "\(GenerativeAISwift.baseURL)/\(options.apiVersion)/\(model):countTokens")! + } +} + +/// The model's response to a count tokens request. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct CountTokensResponse: Decodable { + /// The total number of tokens in the input given to the model as a prompt. + public let totalTokens: Int +} diff --git a/FirebaseVertexAI/Sources/Errors.swift b/FirebaseVertexAI/Sources/Errors.swift new file mode 100644 index 00000000000..35e7e6b813b --- /dev/null +++ b/FirebaseVertexAI/Sources/Errors.swift @@ -0,0 +1,189 @@ +// 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 + +struct RPCError: Error { + let httpResponseCode: Int + let message: String + let status: RPCStatus + let details: [ErrorDetails] + + private var errorInfo: ErrorDetails? { + return details.first { $0.isErrorInfo() } + } + + init(httpResponseCode: Int, message: String, status: RPCStatus, details: [ErrorDetails]) { + self.httpResponseCode = httpResponseCode + self.message = message + self.status = status + self.details = details + } + + func isInvalidAPIKeyError() -> Bool { + return errorInfo?.reason == "API_KEY_INVALID" + } + + func isUnsupportedUserLocationError() -> Bool { + return message == RPCErrorMessage.unsupportedUserLocation.rawValue + } +} + +extension RPCError: Decodable { + enum CodingKeys: CodingKey { + case error + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let status = try container.decode(ErrorStatus.self, forKey: .error) + + if let code = status.code { + httpResponseCode = code + } else { + httpResponseCode = -1 + } + + if let message = status.message { + self.message = message + } else { + message = "Unknown error." + } + + if let rpcStatus = status.status { + self.status = rpcStatus + } else { + self.status = .unknown + } + + details = status.details + } +} + +struct ErrorStatus { + let code: Int? + let message: String? + let status: RPCStatus? + let details: [ErrorDetails] +} + +struct ErrorDetails { + static let errorInfoType = "type.googleapis.com/google.rpc.ErrorInfo" + + let type: String + let reason: String? + let domain: String? + + func isErrorInfo() -> Bool { + return type == ErrorDetails.errorInfoType + } +} + +extension ErrorDetails: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case type = "@type" + case reason + case domain + } +} + +extension ErrorStatus: Decodable { + enum CodingKeys: CodingKey { + case code + case message + case status + case details + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + code = try container.decodeIfPresent(Int.self, forKey: .code) + message = try container.decodeIfPresent(String.self, forKey: .message) + do { + status = try container.decodeIfPresent(RPCStatus.self, forKey: .status) + } catch { + status = .unknown + } + if container.contains(.details) { + details = try container.decode([ErrorDetails].self, forKey: .details) + } else { + details = [] + } + } +} + +enum RPCStatus: String, Decodable { + // Not an error; returned on success. + case ok = "OK" + + // The operation was cancelled, typically by the caller. + case cancelled = "CANCELLED" + + // Unknown error. + case unknown = "UNKNOWN" + + // The client specified an invalid argument. + case invalidArgument = "INVALID_ARGUMENT" + + // The deadline expired before the operation could complete. + case deadlineExceeded = "DEADLINE_EXCEEDED" + + // Some requested entity (e.g., file or directory) was not found. + case notFound = "NOT_FOUND" + + // The entity that a client attempted to create (e.g., file or directory) already exists. + case alreadyExists = "ALREADY_EXISTS" + + // The caller does not have permission to execute the specified operation. + case permissionDenied = "PERMISSION_DENIED" + + // The request does not have valid authentication credentials for the operation. + case unauthenticated = "UNAUTHENTICATED" + + // Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + // is out of space. + case resourceExhausted = "RESOURCE_EXHAUSTED" + + // The operation was rejected because the system is not in a state required for the operation's + // execution. + case failedPrecondition = "FAILED_PRECONDITION" + + // The operation was aborted, typically due to a concurrency issue such as a sequencer check + // failure or transaction abort. + case aborted = "ABORTED" + + // The operation was attempted past the valid range. + case outOfRange = "OUT_OF_RANGE" + + // The operation is not implemented or is not supported/enabled in this service. + case unimplemented = "UNIMPLEMENTED" + + // Internal errors. + case internalError = "INTERNAL" + + // The service is currently unavailable. + case unavailable = "UNAVAILABLE" + + // Unrecoverable data loss or corruption. + case dataLoss = "DATA_LOSS" +} + +enum RPCErrorMessage: String { + case unsupportedUserLocation = "User location is not supported for the API use." +} + +enum InvalidCandidateError: Error { + case emptyContent(underlyingError: Error) + case malformedContent(underlyingError: Error) +} diff --git a/FirebaseVertexAI/Sources/GenerateContentError.swift b/FirebaseVertexAI/Sources/GenerateContentError.swift new file mode 100644 index 00000000000..055928e5f1b --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerateContentError.swift @@ -0,0 +1,44 @@ +// 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 + +/// Errors that occur when generating content from a model. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public enum GenerateContentError: Error { + /// An error occurred when constructing the prompt. Examine the related error for details. + case promptImageContentError(underlying: ImageConversionError) + + /// An internal error occurred. See the underlying error for more context. + case internalError(underlying: Error) + + /// A prompt was blocked. See the response's `promptFeedback.blockReason` for more information. + case promptBlocked(response: GenerateContentResponse) + + /// A response didn't fully complete. See the `FinishReason` for more information. + case responseStoppedEarly(reason: FinishReason, response: GenerateContentResponse) + + /// The provided API key is invalid. + case invalidAPIKey(message: String) + + /// The user's location (region) is not supported by the API. + /// + /// See the Google documentation for a + /// [list of regions](https://ai.google.dev/available_regions#available_regions) + /// (countries and territories) where the API is available. + /// + /// - Important: The API is only available in + /// [specific regions](https://ai.google.dev/available_regions#available_regions). + case unsupportedUserLocation +} diff --git a/FirebaseVertexAI/Sources/GenerateContentRequest.swift b/FirebaseVertexAI/Sources/GenerateContentRequest.swift new file mode 100644 index 00000000000..417260bb700 --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerateContentRequest.swift @@ -0,0 +1,49 @@ +// 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 + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +struct GenerateContentRequest { + /// Model name. + let model: String + let contents: [ModelContent] + let generationConfig: GenerationConfig? + let safetySettings: [SafetySetting]? + let isStreaming: Bool + let options: RequestOptions +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension GenerateContentRequest: Encodable { + enum CodingKeys: String, CodingKey { + case contents + case generationConfig + case safetySettings + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension GenerateContentRequest: GenerativeAIRequest { + typealias Response = GenerateContentResponse + + var url: URL { + let modelURL = "\(GenerativeAISwift.baseURL)/\(options.apiVersion)/\(model)" + if isStreaming { + return URL(string: "\(modelURL):streamGenerateContent?alt=sse")! + } else { + return URL(string: "\(modelURL):generateContent")! + } + } +} diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift new file mode 100644 index 00000000000..03153da9ae7 --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -0,0 +1,286 @@ +// 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 + +/// The model's response to a generate content request. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct GenerateContentResponse { + /// A list of candidate response content, ordered from best to worst. + public let candidates: [CandidateResponse] + + /// A value containing the safety ratings for the response, or, if the request was blocked, a + /// reason for blocking the request. + public let promptFeedback: PromptFeedback? + + /// The response's content as text, if it exists. + public var text: String? { + guard let candidate = candidates.first else { + Logging.default.error("Could not get text from a response that had no candidates.") + return nil + } + guard let text = candidate.content.parts.first?.text else { + Logging.default.error("Could not get a text part from the first candidate.") + return nil + } + return text + } + + /// Initializer for SwiftUI previews or tests. + public init(candidates: [CandidateResponse], promptFeedback: PromptFeedback?) { + self.candidates = candidates + self.promptFeedback = promptFeedback + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension GenerateContentResponse: Decodable { + enum CodingKeys: CodingKey { + case candidates + case promptFeedback + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + guard container.contains(CodingKeys.candidates) || container + .contains(CodingKeys.promptFeedback) else { + let context = DecodingError.Context( + codingPath: [], + debugDescription: "Failed to decode GenerateContentResponse;" + + " missing keys 'candidates' and 'promptFeedback'." + ) + throw DecodingError.dataCorrupted(context) + } + + if let candidates = try container.decodeIfPresent( + [CandidateResponse].self, + forKey: .candidates + ) { + self.candidates = candidates + } else { + candidates = [] + } + promptFeedback = try container.decodeIfPresent(PromptFeedback.self, forKey: .promptFeedback) + } +} + +/// A struct representing a possible reply to a content generation prompt. Each content generation +/// prompt may produce multiple candidate responses. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct CandidateResponse { + /// The response's content. + public let content: ModelContent + + /// The safety rating of the response content. + public let safetyRatings: [SafetyRating] + + /// The reason the model stopped generating content, if it exists; for example, if the model + /// generated a predefined stop sequence. + public let finishReason: FinishReason? + + /// Cited works in the model's response content, if it exists. + public let citationMetadata: CitationMetadata? + + /// Initializer for SwiftUI previews or tests. + public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?, + citationMetadata: CitationMetadata?) { + self.content = content + self.safetyRatings = safetyRatings + self.finishReason = finishReason + self.citationMetadata = citationMetadata + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CandidateResponse: Decodable { + enum CodingKeys: CodingKey { + case content + case safetyRatings + case finishReason + case finishMessage + case citationMetadata + } + + /// Initializes a response from a decoder. Used for decoding server responses; not for public + /// use. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + do { + if let content = try container.decodeIfPresent(ModelContent.self, forKey: .content) { + self.content = content + } else { + content = ModelContent(parts: []) + } + } catch { + // Check if `content` can be decoded as an empty dictionary to detect the `"content": {}` bug. + if let content = try? container.decode([String: String].self, forKey: .content), + content.isEmpty { + throw InvalidCandidateError.emptyContent(underlyingError: error) + } else { + throw InvalidCandidateError.malformedContent(underlyingError: error) + } + } + + if let safetyRatings = try container.decodeIfPresent( + [SafetyRating].self, + forKey: .safetyRatings + ) { + self.safetyRatings = safetyRatings + } else { + safetyRatings = [] + } + + finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason) + + citationMetadata = try container.decodeIfPresent( + CitationMetadata.self, + forKey: .citationMetadata + ) + } +} + +/// A collection of source attributions for a piece of content. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct CitationMetadata: Decodable { + /// A list of individual cited sources and the parts of the content to which they apply. + public let citationSources: [Citation] +} + +/// A struct describing a source attribution. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct Citation: Decodable { + /// The inclusive beginning of a sequence in a model response that derives from a cited source. + public let startIndex: Int + + /// The exclusive end of a sequence in a model response that derives from a cited source. + public let endIndex: Int + + /// A link to the cited source. + public let uri: String + + /// The license the cited source work is distributed under. + public let license: String +} + +/// A value enumerating possible reasons for a model to terminate a content generation request. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public enum FinishReason: String { + case unknown = "FINISH_REASON_UNKNOWN" + + case unspecified = "FINISH_REASON_UNSPECIFIED" + + /// Natural stop point of the model or provided stop sequence. + case stop = "STOP" + + /// The maximum number of tokens as specified in the request was reached. + case maxTokens = "MAX_TOKENS" + + /// The token generation was stopped because the response was flagged for safety reasons. + /// NOTE: When streaming, the Candidate.content will be empty if content filters blocked the + /// output. + case safety = "SAFETY" + + /// The token generation was stopped because the response was flagged for unauthorized citations. + case recitation = "RECITATION" + + /// All other reasons that stopped token generation. + case other = "OTHER" +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension FinishReason: Decodable { + /// Do not explicitly use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedFinishReason = FinishReason(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized FinishReason with value \"\(value)\".") + self = .unknown + return + } + + self = decodedFinishReason + } +} + +/// A metadata struct containing any feedback the model had on the prompt it was provided. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct PromptFeedback { + /// A type describing possible reasons to block a prompt. + public enum BlockReason: String, Decodable { + /// The block reason is unknown. + case unknown = "UNKNOWN" + + /// The block reason was not specified in the server response. + case unspecified = "BLOCK_REASON_UNSPECIFIED" + + /// The prompt was blocked because it was deemed unsafe. + case safety = "SAFETY" + + /// All other block reasons. + case other = "OTHER" + + /// Do not explicitly use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedBlockReason = BlockReason(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized BlockReason with value \"\(value)\".") + self = .unknown + return + } + + self = decodedBlockReason + } + } + + /// The reason a prompt was blocked, if it was blocked. + public let blockReason: BlockReason? + + /// The safety ratings of the prompt. + public let safetyRatings: [SafetyRating] + + /// Initializer for SwiftUI previews or tests. + public init(blockReason: BlockReason?, safetyRatings: [SafetyRating]) { + self.blockReason = blockReason + self.safetyRatings = safetyRatings + } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension PromptFeedback: Decodable { + enum CodingKeys: CodingKey { + case blockReason + case safetyRatings + } + + /// Do not explicitly use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + blockReason = try container.decodeIfPresent( + PromptFeedback.BlockReason.self, + forKey: .blockReason + ) + if let safetyRatings = try container.decodeIfPresent( + [SafetyRating].self, + forKey: .safetyRatings + ) { + self.safetyRatings = safetyRatings + } else { + safetyRatings = [] + } + } +} diff --git a/FirebaseVertexAI/Sources/GenerationConfig.swift b/FirebaseVertexAI/Sources/GenerationConfig.swift new file mode 100644 index 00000000000..2d1016c965c --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerationConfig.swift @@ -0,0 +1,86 @@ +// 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 + +/// A struct defining model parameters to be used when sending generative AI +/// requests to the backend model. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct GenerationConfig: Encodable { + /// A parameter controlling the degree of randomness in token selection. A + /// temperature of zero is deterministic, always choosing the + /// highest-probability response. Typical values are between 0 and 1 + /// inclusive. Defaults to 0 if unspecified. + public let temperature: Float? + + /// The `topP` parameter changes how the model selects tokens for output. + /// Tokens are selected from the most to least probable until the sum of + /// their probabilities equals the `topP` value. For example, if tokens A, B, + /// and C have probabilities of 0.3, 0.2, and 0.1 respectively and the topP + /// value is 0.5, then the model will select either A or B as the next token + /// by using the `temperature` and exclude C as a candidate. + /// Defaults to 0.95 if unset. + public let topP: Float? + + /// The `topK` parameter changes how the model selects tokens for output. A + /// `topK` of 1 means the selected token is the most probable among all the + /// tokens in the model's vocabulary, while a `topK` of 3 means that the next + /// token is selected from among the 3 most probable using the `temperature`. + /// For each token selection step, the `topK` tokens with the highest + /// probabilities are sampled. Tokens are then further filtered based on + /// `topP` with the final token selected using `temperature` sampling. + /// Defaults to 40 if unspecified. + public let topK: Int? + + /// The maximum number of generated response messages to return. This value + /// must be between [1, 8], inclusive. If unset, this will default to 1. + /// + /// - Note: Only unique candidates are returned. Higher temperatures are more + /// likely to produce unique candidates. Setting `temperature` to 0 will + /// always produce exactly one candidate regardless of the + /// `candidateCount`. + public let candidateCount: Int? + + /// Specifies the maximum number of tokens that can be generated in the + /// response. The number of tokens per word varies depending on the + /// language outputted. The maximum value is capped at 1024. Defaults to 0 + /// (unbounded). + public let maxOutputTokens: Int? + + /// A set of up to 5 `String`s that will stop output generation. If + /// specified, the API will stop at the first appearance of a stop sequence. + /// The stop sequence will not be included as part of the response. + public let stopSequences: [String]? + + /// Creates a new `GenerationConfig` value. + /// + /// - Parameter temperature: See ``temperature`` + /// - Parameter topP: See ``topP`` + /// - Parameter topK: See ``topK`` + /// - Parameter candidateCount: See ``candidateCount`` + /// - Parameter maxOutputTokens: See ``maxOutputTokens`` + /// - Parameter stopSequences: See ``stopSequences`` + public init(temperature: Float? = nil, topP: Float? = nil, topK: Int? = nil, + candidateCount: Int? = nil, maxOutputTokens: Int? = nil, + stopSequences: [String]? = nil) { + // Explicit init because otherwise if we re-arrange the above variables it changes the API + // surface. + self.temperature = temperature + self.topP = topP + self.topK = topK + self.candidateCount = candidateCount + self.maxOutputTokens = maxOutputTokens + self.stopSequences = stopSequences + } +} diff --git a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift new file mode 100644 index 00000000000..2bf94503b1a --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift @@ -0,0 +1,46 @@ +// 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 + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +protocol GenerativeAIRequest: Encodable { + associatedtype Response: Decodable + + var url: URL { get } + + var options: RequestOptions { get } +} + +/// Configuration parameters for sending requests to the backend. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct RequestOptions { + /// 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; if not specified uses the default value + /// for a `URLRequest`. + /// - apiVersion The API version to use in requests to the backend; defaults to "v1". + public init(timeout: TimeInterval? = nil, apiVersion: String = "v1") { + self.timeout = timeout + self.apiVersion = apiVersion + } +} diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift new file mode 100644 index 00000000000..8d904738927 --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -0,0 +1,257 @@ +// 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 + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +struct GenerativeAIService { + /// Gives permission to talk to the backend. + private let apiKey: String + + private let urlSession: URLSession + + init(apiKey: String, urlSession: URLSession) { + self.apiKey = apiKey + self.urlSession = urlSession + } + + func loadRequest(request: T) async throws -> T.Response { + let urlRequest = try urlRequest(request: request) + + #if DEBUG + printCURLCommand(from: urlRequest) + #endif + + let data: Data + let rawResponse: URLResponse + (data, rawResponse) = try await urlSession.data(for: urlRequest) + + let response = try httpResponse(urlResponse: rawResponse) + + // Verify the status code is 200 + guard response.statusCode == 200 else { + Logging.default.error("[GoogleGenerativeAI] The server responded with an error: \(response)") + if let responseString = String(data: data, encoding: .utf8) { + Logging.network.error("[GoogleGenerativeAI] Response payload: \(responseString)") + } + + throw parseError(responseData: data) + } + + return try parseResponse(T.Response.self, from: data) + } + + @available(macOS 12.0, *) + func loadRequestStream(request: T) + -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + Task { + let urlRequest: URLRequest + do { + urlRequest = try self.urlRequest(request: request) + } catch { + continuation.finish(throwing: error) + return + } + + #if DEBUG + printCURLCommand(from: urlRequest) + #endif + + let stream: URLSession.AsyncBytes + let rawResponse: URLResponse + do { + (stream, rawResponse) = try await urlSession.bytes(for: urlRequest) + } catch { + continuation.finish(throwing: error) + return + } + + // Verify the status code is 200 + let response: HTTPURLResponse + do { + response = try httpResponse(urlResponse: rawResponse) + } catch { + continuation.finish(throwing: error) + return + } + + // Verify the status code is 200 + guard response.statusCode == 200 else { + Logging.default + .error("[GoogleGenerativeAI] The server responded with an error: \(response)") + var responseBody = "" + for try await line in stream.lines { + responseBody += line + "\n" + } + + Logging.network.error("[GoogleGenerativeAI] Response payload: \(responseBody)") + continuation.finish(throwing: parseError(responseBody: responseBody)) + + return + } + + // Received lines that are not server-sent events (SSE); these are not prefixed with "data:" + var extraLines: String = "" + + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + for try await line in stream.lines { + Logging.network.debug("[GoogleGenerativeAI] Stream response: \(line)") + + if line.hasPrefix("data:") { + // We can assume 5 characters since it's utf-8 encoded, removing `data:`. + let jsonText = String(line.dropFirst(5)) + let data: Data + do { + data = try jsonData(jsonText: jsonText) + } catch { + continuation.finish(throwing: error) + return + } + + // Handle the content. + do { + let content = try parseResponse(T.Response.self, from: data) + continuation.yield(content) + } catch { + continuation.finish(throwing: error) + return + } + } else { + extraLines += line + } + } + + if extraLines.count > 0 { + continuation.finish(throwing: parseError(responseBody: extraLines)) + return + } + + continuation.finish(throwing: nil) + } + } + } + + // MARK: - Private Helpers + + private func urlRequest(request: T) throws -> URLRequest { + var urlRequest = URLRequest(url: request.url) + urlRequest.httpMethod = "POST" + urlRequest.setValue(apiKey, forHTTPHeaderField: "x-goog-api-key") + urlRequest.setValue("genai-swift/\(GenerativeAISwift.version)", + forHTTPHeaderField: "x-goog-api-client") + urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + urlRequest.httpBody = try encoder.encode(request) + + if let timeoutInterval = request.options.timeout { + urlRequest.timeoutInterval = timeoutInterval + } + + return urlRequest + } + + private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse { + // Verify the status code is 200 + guard let response = urlResponse as? HTTPURLResponse else { + Logging.default + .error( + "[GoogleGenerativeAI] Response wasn't an HTTP response, internal error \(urlResponse)" + ) + throw NSError( + domain: "com.google.generative-ai", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Response was not an HTTP response."] + ) + } + + return response + } + + private func jsonData(jsonText: String) throws -> Data { + guard let data = jsonText.data(using: .utf8) else { + let error = NSError( + domain: "com.google.generative-ai", + code: -1, + userInfo: [NSLocalizedDescriptionKey: "Could not parse response as UTF8."] + ) + throw error + } + + return data + } + + private func parseError(responseBody: String) -> Error { + do { + let data = try jsonData(jsonText: responseBody) + return parseError(responseData: data) + } catch { + return error + } + } + + private func parseError(responseData: Data) -> Error { + do { + return try JSONDecoder().decode(RPCError.self, from: responseData) + } catch { + // TODO: Return an error about an unrecognized error payload with the response body + return error + } + } + + private func parseResponse(_ type: T.Type, from data: Data) throws -> T { + do { + return try JSONDecoder().decode(type, from: data) + } catch { + if let json = String(data: data, encoding: .utf8) { + Logging.network.error("[GoogleGenerativeAI] JSON response: \(json)") + } + Logging.default.error("[GoogleGenerativeAI] Error decoding server JSON: \(error)") + throw error + } + } + + #if DEBUG + private func cURLCommand(from request: URLRequest) -> String { + var returnValue = "curl " + if let allHeaders = request.allHTTPHeaderFields { + for (key, value) in allHeaders { + returnValue += "-H '\(key): \(value)' " + } + } + + guard let url = request.url else { return "" } + returnValue += "'\(url.absoluteString)' " + + guard let body = request.httpBody, + let jsonStr = String(bytes: body, encoding: .utf8) else { return "" } + let escapedJSON = jsonStr.replacingOccurrences(of: "'", with: "'\\''") + returnValue += "-d '\(escapedJSON)'" + + return returnValue + } + + private func printCURLCommand(from request: URLRequest) { + let command = cURLCommand(from: request) + Logging.verbose.debug(""" + [GoogleGenerativeAI] Creating request with the equivalent cURL command: + ----- cURL command ----- + \(command, privacy: .private) + ------------------------ + """) + } + #endif // DEBUG +} diff --git a/FirebaseVertexAI/Sources/GenerativeAISwift.swift b/FirebaseVertexAI/Sources/GenerativeAISwift.swift new file mode 100644 index 00000000000..4146b60d989 --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerativeAISwift.swift @@ -0,0 +1,26 @@ +// 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 + +#if !os(macOS) && !os(iOS) + #warning("Only iOS, macOS, and Catalyst targets are currently fully supported.") +#endif + +/// Constants associated with the GenerativeAISwift SDK +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public enum GenerativeAISwift { + /// String value of the SDK version + public static let version = "0.4.8" + static let baseURL = "https://generativelanguage.googleapis.com" +} diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift new file mode 100644 index 00000000000..3617cd3a251 --- /dev/null +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -0,0 +1,298 @@ +// 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 + +/// 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 11.0, macCatalyst 15.0, *) +public final class GenerativeModel { + // The prefix for a model resource in the Gemini API. + private static let modelResourcePrefix = "models/" + + /// The resource name of the model in the backend; has the format "models/model-name". + let modelResourceName: String + + /// The backing service responsible for sending and receiving model requests to the backend. + let generativeAIService: GenerativeAIService + + /// Configuration parameters used for the MultiModalModel. + let generationConfig: GenerationConfig? + + /// The safety settings to be used for prompts. + let safetySettings: [SafetySetting]? + + /// Configuration parameters for sending requests to the backend. + let requestOptions: RequestOptions + + /// Initializes a new remote model with the given parameters. + /// + /// - Parameters: + /// - name: The name of the model to use, e.g., `"gemini-1.0-pro"`; see + /// [Gemini models](https://ai.google.dev/models/gemini) for a list of supported model names. + /// - apiKey: The API key for your project. + /// - generationConfig: The content generation parameters your model should use. + /// - safetySettings: A value describing what types of harmful content your model should allow. + /// - requestOptions Configuration parameters for sending requests to the backend. + public convenience init(name: String, + apiKey: String, + generationConfig: GenerationConfig? = nil, + safetySettings: [SafetySetting]? = nil, + requestOptions: RequestOptions = RequestOptions()) { + self.init( + name: name, + apiKey: apiKey, + generationConfig: generationConfig, + safetySettings: safetySettings, + requestOptions: requestOptions, + urlSession: .shared + ) + } + + /// The designated initializer for this class. + init(name: String, + apiKey: String, + generationConfig: GenerationConfig? = nil, + safetySettings: [SafetySetting]? = nil, + requestOptions: RequestOptions = RequestOptions(), + urlSession: URLSession) { + modelResourceName = GenerativeModel.modelResourceName(name: name) + generativeAIService = GenerativeAIService(apiKey: apiKey, urlSession: urlSession) + self.generationConfig = generationConfig + self.safetySettings = safetySettings + self.requestOptions = requestOptions + + Logging.default.info(""" + [GoogleGenerativeAI] Model \( + name, + privacy: .public + ) initialized. To enable additional logging, add \ + `\(Logging.enableArgumentKey, privacy: .public)` as a launch argument in Xcode. + """) + Logging.verbose.debug("[GoogleGenerativeAI] Verbose logging enabled.") + } + + /// Generates content from String and/or image inputs, given to the model as a prompt, that are + /// representable as one or more ``ModelContent/Part``s. + /// + /// Since ``ModelContent/Part``s do not specify a role, this method is intended for generating + /// content from + /// [zero-shot](https://developers.google.com/machine-learning/glossary/generative#zero-shot-prompting) + /// or "direct" prompts. For + /// [few-shot](https://developers.google.com/machine-learning/glossary/generative#few-shot-prompting) + /// prompts, see ``generateContent(_:)-58rm0``. + /// + /// - Parameter content: The input(s) given to the model as a prompt (see + /// ``ThrowingPartsRepresentable`` + /// for conforming types). + /// - Returns: The content generated by the model. + /// - Throws: A ``GenerateContentError`` if the request failed. + public func generateContent(_ parts: any ThrowingPartsRepresentable...) + async throws -> GenerateContentResponse { + return try await generateContent([ModelContent(parts: parts)]) + } + + /// Generates new content from input content given to the model as a prompt. + /// + /// - Parameter content: The input(s) given to the model as a prompt. + /// - Returns: The generated content response from the model. + /// - Throws: A ``GenerateContentError`` if the request failed. + public func generateContent(_ content: @autoclosure () throws -> [ModelContent]) async throws + -> GenerateContentResponse { + let response: GenerateContentResponse + do { + let generateContentRequest = try GenerateContentRequest(model: modelResourceName, + contents: content(), + generationConfig: generationConfig, + safetySettings: safetySettings, + isStreaming: false, + options: requestOptions) + response = try await generativeAIService.loadRequest(request: generateContentRequest) + } catch { + if let imageError = error as? ImageConversionError { + throw GenerateContentError.promptImageContentError(underlying: imageError) + } + throw GenerativeModel.generateContentError(from: error) + } + + // Check the prompt feedback to see if the prompt was blocked. + if response.promptFeedback?.blockReason != nil { + throw GenerateContentError.promptBlocked(response: response) + } + + // Check to see if an error should be thrown for stop reason. + if let reason = response.candidates.first?.finishReason, reason != .stop { + throw GenerateContentError.responseStoppedEarly(reason: reason, response: response) + } + + return response + } + + /// Generates content from String and/or image inputs, given to the model as a prompt, that are + /// representable as one or more ``ModelContent/Part``s. + /// + /// Since ``ModelContent/Part``s do not specify a role, this method is intended for generating + /// content from + /// [zero-shot](https://developers.google.com/machine-learning/glossary/generative#zero-shot-prompting) + /// or "direct" prompts. For + /// [few-shot](https://developers.google.com/machine-learning/glossary/generative#few-shot-prompting) + /// prompts, see ``generateContent(_:)-58rm0``. + /// + /// - Parameter content: The input(s) given to the model as a prompt (see + /// ``ThrowingPartsRepresentable`` + /// for conforming types). + /// - Returns: A stream wrapping content generated by the model or a ``GenerateContentError`` + /// error if an error occurred. + @available(macOS 12.0, *) + public func generateContentStream(_ parts: any ThrowingPartsRepresentable...) + -> AsyncThrowingStream { + return try generateContentStream([ModelContent(parts: parts)]) + } + + /// Generates new content from input content given to the model as a prompt. + /// + /// - Parameter content: The input(s) given to the model as a prompt. + /// - Returns: A stream wrapping content generated by the model or a ``GenerateContentError`` + /// error if an error occurred. + @available(macOS 12.0, *) + public func generateContentStream(_ content: @autoclosure () throws -> [ModelContent]) + -> AsyncThrowingStream { + let evaluatedContent: [ModelContent] + do { + evaluatedContent = try content() + } catch let underlying { + return AsyncThrowingStream { continuation in + let error: Error + if let contentError = underlying as? ImageConversionError { + error = GenerateContentError.promptImageContentError(underlying: contentError) + } else { + error = GenerateContentError.internalError(underlying: underlying) + } + continuation.finish(throwing: error) + } + } + + let generateContentRequest = GenerateContentRequest(model: modelResourceName, + contents: evaluatedContent, + generationConfig: generationConfig, + safetySettings: safetySettings, + 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 + } + + // 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) + } else { + // Response was valid content, pass it along and continue. + return response + } + } + } + + /// Creates a new chat conversation using this model with the provided history. + public func startChat(history: [ModelContent] = []) -> Chat { + return Chat(model: self, history: history) + } + + /// Runs the model's tokenizer on String and/or image inputs that are representable as one or more + /// ``ModelContent/Part``s. + /// + /// Since ``ModelContent/Part``s do not specify a role, this method is intended for tokenizing + /// [zero-shot](https://developers.google.com/machine-learning/glossary/generative#zero-shot-prompting) + /// or "direct" prompts. For + /// [few-shot](https://developers.google.com/machine-learning/glossary/generative#few-shot-prompting) + /// input, see ``countTokens(_:)-9spwl``. + /// + /// - Parameter content: The input(s) given to the model as a prompt (see + /// ``ThrowingPartsRepresentable`` + /// for conforming types). + /// - Returns: The results of running the model's tokenizer on the input; contains + /// ``CountTokensResponse/totalTokens``. + /// - Throws: A ``CountTokensError`` if the tokenization request failed. + public func countTokens(_ parts: any ThrowingPartsRepresentable...) async throws + -> CountTokensResponse { + return try await countTokens([ModelContent(parts: parts)]) + } + + /// Runs the model's tokenizer on the input content and returns the token count. + /// + /// - Parameter content: The input given to the model as a prompt. + /// - Returns: The results of running the model's tokenizer on the input; contains + /// ``CountTokensResponse/totalTokens``. + /// - Throws: A ``CountTokensError`` if the tokenization request failed or the input content was + /// invalid. + public func countTokens(_ content: @autoclosure () throws -> [ModelContent]) async throws + -> CountTokensResponse { + do { + let countTokensRequest = try CountTokensRequest( + model: modelResourceName, + contents: content(), + options: requestOptions + ) + return try await generativeAIService.loadRequest(request: countTokensRequest) + } catch { + throw CountTokensError.internalError(underlying: error) + } + } + + /// Returns a model resource name of the form "models/model-name" based on `name`. + private static func modelResourceName(name: String) -> String { + if name.contains("/") { + return name + } else { + return modelResourcePrefix + name + } + } + + /// Returns a `GenerateContentError` (for public consumption) from an internal error. + /// + /// If `error` is already a `GenerateContentError` the error is returned unchanged. + private static func generateContentError(from error: Error) -> GenerateContentError { + if let error = error as? GenerateContentError { + return error + } else if let error = error as? RPCError, error.isInvalidAPIKeyError() { + return GenerateContentError.invalidAPIKey(message: error.message) + } else if let error = error as? RPCError, error.isUnsupportedUserLocationError() { + return GenerateContentError.unsupportedUserLocation + } + return GenerateContentError.internalError(underlying: error) + } +} + +/// See ``GenerativeModel/countTokens(_:)-9spwl``. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public enum CountTokensError: Error { + case internalError(underlying: Error) +} diff --git a/FirebaseVertexAI/Sources/Logging.swift b/FirebaseVertexAI/Sources/Logging.swift new file mode 100644 index 00000000000..458c34ed18d --- /dev/null +++ b/FirebaseVertexAI/Sources/Logging.swift @@ -0,0 +1,56 @@ +// 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 +import OSLog + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +struct Logging { + /// Subsystem that should be used for all Loggers. + static let subsystem = "com.google.generative-ai" + + /// Default category used for most loggers, unless specialized. + static let defaultCategory = "" + + /// The argument required to enable additional logging. + static let enableArgumentKey = "-GoogleGenerativeAIDebugLogEnabled" + + // No initializer available. + @available(*, unavailable) + private init() {} + + /// The default logger that is visible for all users. Note: we shouldn't be using anything lower + /// than `.notice`. + static var `default` = Logger(subsystem: subsystem, category: defaultCategory) + + /// A non default + static var network: Logger = { + if ProcessInfo.processInfo.arguments.contains(enableArgumentKey) { + return Logger(subsystem: subsystem, category: "NetworkResponse") + } else { + // Return a valid logger that's using `OSLog.disabled` as the logger, hiding everything. + return Logger(.disabled) + } + }() + + /// + static var verbose: Logger = { + if ProcessInfo.processInfo.arguments.contains(enableArgumentKey) { + return Logger(subsystem: subsystem, category: defaultCategory) + } else { + // Return a valid logger that's using `OSLog.disabled` as the logger, hiding everything. + return Logger(.disabled) + } + }() +} diff --git a/FirebaseVertexAI/Sources/ModelContent.swift b/FirebaseVertexAI/Sources/ModelContent.swift new file mode 100644 index 00000000000..44648c57852 --- /dev/null +++ b/FirebaseVertexAI/Sources/ModelContent.swift @@ -0,0 +1,141 @@ +// 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 + +/// A type describing data in media formats interpretable by an AI model. Each generative AI +/// request or response contains an `Array` of ``ModelContent``s, and each ``ModelContent`` value +/// may comprise multiple heterogeneous ``ModelContent/Part``s. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct ModelContent: Codable, Equatable { + /// A discrete piece of data in a media format intepretable by an AI model. Within a single value + /// of ``Part``, different data types may not mix. + public enum Part: Codable, Equatable { + enum CodingKeys: String, CodingKey { + case text + case inlineData + } + + enum InlineDataKeys: String, CodingKey { + case mimeType = "mime_type" + case bytes = "data" + } + + /// Text value. + case text(String) + + /// Data with a specified media type. Not all media types may be supported by the AI model. + case data(mimetype: String, Data) + + // MARK: Convenience Initializers + + /// Convenience function for populating a Part with JPEG data. + public static func jpeg(_ data: Data) -> Self { + return .data(mimetype: "image/jpeg", data) + } + + /// Convenience function for populating a Part with PNG data. + public static func png(_ data: Data) -> Self { + return .data(mimetype: "image/png", data) + } + + // MARK: Codable Conformance + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: ModelContent.Part.CodingKeys.self) + switch self { + case let .text(a0): + try container.encode(a0, forKey: .text) + case let .data(mimetype, bytes): + var inlineDataContainer = container.nestedContainer( + keyedBy: InlineDataKeys.self, + forKey: .inlineData + ) + try inlineDataContainer.encode(mimetype, forKey: .mimeType) + try inlineDataContainer.encode(bytes, forKey: .bytes) + } + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if values.contains(.text) { + self = try .text(values.decode(String.self, forKey: .text)) + } else if values.contains(.inlineData) { + let dataContainer = try values.nestedContainer( + keyedBy: InlineDataKeys.self, + forKey: .inlineData + ) + let mimetype = try dataContainer.decode(String.self, forKey: .mimeType) + let bytes = try dataContainer.decode(Data.self, forKey: .bytes) + self = .data(mimetype: mimetype, bytes) + } else { + throw DecodingError.dataCorrupted(.init( + codingPath: [CodingKeys.text, CodingKeys.inlineData], + debugDescription: "Neither text or inline data was found." + )) + } + } + + /// Returns the text contents of this ``Part``, if it contains text. + public var text: String? { + switch self { + case let .text(contents): return contents + default: return nil + } + } + } + + /// The role of the entity creating the ``ModelContent``. For user-generated client requests, + /// for example, the role is `user`. + public let role: String? + + /// The data parts comprising this ``ModelContent`` value. + public let parts: [Part] + + /// Creates a new value from any data or `Array` of data interpretable as a + /// ``Part``. See ``ThrowingPartsRepresentable`` for types that can be interpreted as `Part`s. + public init(role: String? = "user", parts: some ThrowingPartsRepresentable) throws { + self.role = role + try self.parts = parts.tryPartsValue() + } + + /// Creates a new value from any data or `Array` of data interpretable as a + /// ``Part``. See ``ThrowingPartsRepresentable`` for types that can be interpreted as `Part`s. + public init(role: String? = "user", parts: some PartsRepresentable) { + self.role = role + self.parts = parts.partsValue + } + + /// Creates a new value from a list of ``Part``s. + public init(role: String? = "user", parts: [Part]) { + self.role = role + self.parts = parts + } + + /// Creates a new value from any data interpretable as a ``Part``. See + /// ``ThrowingPartsRepresentable`` + /// for types that can be interpreted as `Part`s. + public init(role: String? = "user", _ parts: any ThrowingPartsRepresentable...) throws { + let content = try parts.flatMap { try $0.tryPartsValue() } + self.init(role: role, parts: content) + } + + /// Creates a new value from any data interpretable as a ``Part``. See + /// ``ThrowingPartsRepresentable`` + /// for types that can be interpreted as `Part`s. + public init(role: String? = "user", _ parts: [PartsRepresentable]) { + let content = parts.flatMap { $0.partsValue } + self.init(role: role, parts: content) + } +} diff --git a/FirebaseVertexAI/Sources/PartsRepresentable+Image.swift b/FirebaseVertexAI/Sources/PartsRepresentable+Image.swift new file mode 100644 index 00000000000..052a004e52f --- /dev/null +++ b/FirebaseVertexAI/Sources/PartsRepresentable+Image.swift @@ -0,0 +1,107 @@ +// Copyright 2024 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 UniformTypeIdentifiers +#if canImport(UIKit) + import UIKit // For UIImage extensions. +#elseif canImport(AppKit) + import AppKit // For NSImage extensions. +#endif + +private let imageCompressionQuality: CGFloat = 0.8 + +/// An enum describing failures that can occur when converting image types to model content data. +/// For some image types like `CIImage`, creating valid model content requires creating a JPEG +/// representation of the image that may not yet exist, which may be computationally expensive. +public enum ImageConversionError: Error { + /// The image (the receiver of the call `toModelContentParts()`) was invalid. + case invalidUnderlyingImage + + /// A valid image destination could not be allocated. + case couldNotAllocateDestination + + /// JPEG image data conversion failed, accompanied by the original image, which may be an + /// instance of `NSImageRep`, `UIImage`, `CGImage`, or `CIImage`. + case couldNotConvertToJPEG(Any) +} + +#if canImport(UIKit) + /// Enables images to be representable as ``ThrowingPartsRepresentable``. + @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) + extension UIImage: ThrowingPartsRepresentable { + public func tryPartsValue() throws -> [ModelContent.Part] { + guard let data = jpegData(compressionQuality: imageCompressionQuality) else { + throw ImageConversionError.couldNotConvertToJPEG(self) + } + return [ModelContent.Part.data(mimetype: "image/jpeg", data)] + } + } + +#elseif canImport(AppKit) + /// Enables images to be representable as ``ThrowingPartsRepresentable``. + @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) + extension NSImage: ThrowingPartsRepresentable { + public func tryPartsValue() throws -> [ModelContent.Part] { + guard let cgImage = cgImage(forProposedRect: nil, context: nil, hints: nil) else { + throw ImageConversionError.invalidUnderlyingImage + } + let bmp = NSBitmapImageRep(cgImage: cgImage) + guard let data = bmp.representation(using: .jpeg, properties: [.compressionFactor: 0.8]) + else { + throw ImageConversionError.couldNotConvertToJPEG(bmp) + } + return [ModelContent.Part.data(mimetype: "image/jpeg", data)] + } + } +#endif + +/// Enables `CGImages` to be representable as model content. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CGImage: ThrowingPartsRepresentable { + public func tryPartsValue() throws -> [ModelContent.Part] { + let output = NSMutableData() + guard let imageDestination = CGImageDestinationCreateWithData( + output, UTType.jpeg.identifier as CFString, 1, nil + ) else { + throw ImageConversionError.couldNotAllocateDestination + } + CGImageDestinationAddImage(imageDestination, self, nil) + CGImageDestinationSetProperties(imageDestination, [ + kCGImageDestinationLossyCompressionQuality: imageCompressionQuality, + ] as CFDictionary) + if CGImageDestinationFinalize(imageDestination) { + return [.data(mimetype: "image/jpeg", output as Data)] + } + throw ImageConversionError.couldNotConvertToJPEG(self) + } +} + +/// Enables `CIImages` to be representable as model content. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension CIImage: ThrowingPartsRepresentable { + public func tryPartsValue() throws -> [ModelContent.Part] { + let context = CIContext() + let jpegData = (colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)) + .flatMap { + // The docs specify kCGImageDestinationLossyCompressionQuality as a supported option, but + // Swift's type system does not allow this. + // [kCGImageDestinationLossyCompressionQuality: imageCompressionQuality] + context.jpegRepresentation(of: self, colorSpace: $0, options: [:]) + } + if let jpegData = jpegData { + return [.data(mimetype: "image/jpeg", jpegData)] + } + throw ImageConversionError.couldNotConvertToJPEG(self) + } +} diff --git a/FirebaseVertexAI/Sources/PartsRepresentable.swift b/FirebaseVertexAI/Sources/PartsRepresentable.swift new file mode 100644 index 00000000000..05ba0d9dabc --- /dev/null +++ b/FirebaseVertexAI/Sources/PartsRepresentable.swift @@ -0,0 +1,66 @@ +// 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 + +/// A protocol describing any data that could be serialized to model-interpretable input data, +/// where the serialization process might fail with an error. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public protocol ThrowingPartsRepresentable { + func tryPartsValue() throws -> [ModelContent.Part] +} + +/// A protocol describing any data that could be serialized to model-interpretable input data, +/// where the serialization process cannot fail with an error. For a failable conversion, see +/// ``ThrowingPartsRepresentable`` +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public protocol PartsRepresentable: ThrowingPartsRepresentable { + var partsValue: [ModelContent.Part] { get } +} + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public extension PartsRepresentable { + func tryPartsValue() throws -> [ModelContent.Part] { + return partsValue + } +} + +/// Enables a ``ModelContent.Part`` to be passed in as ``ThrowingPartsRepresentable``. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension ModelContent.Part: ThrowingPartsRepresentable { + public typealias ErrorType = Never + public func tryPartsValue() throws -> [ModelContent.Part] { + return [self] + } +} + +/// Enable an `Array` of ``ThrowingPartsRepresentable`` values to be passed in as a single +/// ``ThrowingPartsRepresentable``. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension [ThrowingPartsRepresentable]: ThrowingPartsRepresentable { + public func tryPartsValue() throws -> [ModelContent.Part] { + return try compactMap { element in + try element.tryPartsValue() + } + .flatMap { $0 } + } +} + +/// Enables a `String` to be passed in as ``ThrowingPartsRepresentable``. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +extension String: PartsRepresentable { + public var partsValue: [ModelContent.Part] { + return [.text(self)] + } +} diff --git a/FirebaseVertexAI/Sources/Safety.swift b/FirebaseVertexAI/Sources/Safety.swift new file mode 100644 index 00000000000..d59511f2476 --- /dev/null +++ b/FirebaseVertexAI/Sources/Safety.swift @@ -0,0 +1,182 @@ +// 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 + +/// A type defining potentially harmful media categories and their model-assigned ratings. A value +/// of this type may be assigned to a category for every model-generated response, not just +/// responses that exceed a certain threshold. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct SafetyRating: Decodable, Equatable, Hashable { + /// The category describing the potential harm a piece of content may pose. See + /// ``SafetySetting/HarmCategory`` for a list of possible values. + public let category: SafetySetting.HarmCategory + + /// The model-generated probability that a given piece of content falls under the harm category + /// described in ``category``. This does not + /// indiciate the severity of harm for a piece of content. See ``HarmProbability`` for a list of + /// possible values. + public let probability: HarmProbability + + /// Initializes a new `SafetyRating` instance with the given category and probability. + /// Use this initializer for SwiftUI previews or tests. + public init(category: SafetySetting.HarmCategory, probability: HarmProbability) { + self.category = category + self.probability = probability + } + + /// The probability that a given model output falls under a harmful content category. This does + /// not indicate the severity of harm for a piece of content. + public enum HarmProbability: String, Codable { + /// Unknown. A new server value that isn't recognized by the SDK. + case unknown = "UNKNOWN" + + /// The probability was not specified in the server response. + case unspecified = "HARM_PROBABILITY_UNSPECIFIED" + + /// The probability is zero or close to zero. For benign content, the probability across all + /// categories will be this value. + case negligible = "NEGLIGIBLE" + + /// The probability is small but non-zero. + case low = "LOW" + + /// The probability is moderate. + case medium = "MEDIUM" + + /// The probability is high. The content described is very likely harmful. + case high = "HIGH" + + /// Initializes a new `SafetyRating` from a decoder. + /// Not for external use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedProbability = HarmProbability(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized HarmProbability with value \"\(value)\".") + self = .unknown + return + } + + self = decodedProbability + } + } +} + +/// Safety feedback for an entire request. +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +public struct SafetyFeedback: Decodable { + /// Safety rating evaluated from content. + public let rating: SafetyRating + + /// Safety settings applied to the request. + public let setting: SafetySetting + + /// Internal initializer. + init(rating: SafetyRating, setting: SafetySetting) { + self.rating = rating + self.setting = setting + } +} + +/// 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 11.0, macCatalyst 15.0, *) +public struct SafetySetting: Codable { + /// A type describing safety attributes, which include harmful categories and topics that can + /// be considered sensitive. + public enum HarmCategory: String, Codable { + /// Unknown. A new server value that isn't recognized by the SDK. + case unknown = "HARM_CATEGORY_UNKNOWN" + + /// Unspecified by the server. + case unspecified = "HARM_CATEGORY_UNSPECIFIED" + + /// Harassment content. + case harassment = "HARM_CATEGORY_HARASSMENT" + + /// Negative or harmful comments targeting identity and/or protected attributes. + case hateSpeech = "HARM_CATEGORY_HATE_SPEECH" + + /// Contains references to sexual acts or other lewd content. + case sexuallyExplicit = "HARM_CATEGORY_SEXUALLY_EXPLICIT" + + /// Promotes or enables access to harmful goods, services, or activities. + case dangerousContent = "HARM_CATEGORY_DANGEROUS_CONTENT" + + /// Do not explicitly use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedCategory = HarmCategory(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized HarmCategory with value \"\(value)\".") + self = .unknown + return + } + + self = decodedCategory + } + } + + /// Block at and beyond a specified ``SafetyRating/HarmProbability``. + public enum BlockThreshold: String, Codable { + /// Unknown. A new server value that isn't recognized by the SDK. + case unknown = "UNKNOWN" + + /// Threshold is unspecified. + case unspecified = "HARM_BLOCK_THRESHOLD_UNSPECIFIED" + + // Content with `.negligible` will be allowed. + case blockLowAndAbove = "BLOCK_LOW_AND_ABOVE" + + /// Content with `.negligible` and `.low` will be allowed. + case blockMediumAndAbove = "BLOCK_MEDIUM_AND_ABOVE" + + /// Content with `.negligible`, `.low`, and `.medium` will be allowed. + case blockOnlyHigh = "BLOCK_ONLY_HIGH" + + /// All content will be allowed. + case blockNone = "BLOCK_NONE" + + /// Do not explicitly use. Initializer required for Decodable conformance. + public init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer().decode(String.self) + guard let decodedThreshold = BlockThreshold(rawValue: value) else { + Logging.default + .error("[GoogleGenerativeAI] Unrecognized BlockThreshold with value \"\(value)\".") + self = .unknown + return + } + + self = decodedThreshold + } + } + + enum CodingKeys: String, CodingKey { + case harmCategory = "category" + case threshold + } + + /// The category this safety setting should be applied to. + public let harmCategory: HarmCategory + + /// The threshold describing what content should be blocked. + public let threshold: BlockThreshold + + /// Initializes a new safety setting with the given category and threshold. + public init(harmCategory: HarmCategory, threshold: BlockThreshold) { + self.harmCategory = harmCategory + self.threshold = threshold + } +} diff --git a/Package.swift b/Package.swift index 49f72ff8f6b..ce0feb8850c 100644 --- a/Package.swift +++ b/Package.swift @@ -1376,7 +1376,11 @@ let package = Package( "FirebaseCoreExtension", .product(name: "GoogleGenerativeAI", package: "generative-ai-swift"), ], - path: "FirebaseVertexAI/Sources" + path: "FirebaseVertexAI/Sources", + sources: [ + "VertexAI.swift", + "VertexAIComponent.swift", + ] ), ] + firestoreTargets(), cLanguageStandard: .c99, From 92f6a1d93259d709dd4b2a8ea29d35da9ba95354 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 18 Mar 2024 21:16:22 +0000 Subject: [PATCH 027/103] Use forked `google-generative-ai` in Vertex AI samples (#12574) --- .../ChatSample/Views/ErrorDetailsView.swift | 24 ----------- FirebaseVertexAI/Sources/Errors.swift | 12 ------ .../Sources/GenerateContentError.swift | 13 ------ .../Sources/GenerativeAIRequest.swift | 4 +- .../Sources/GenerativeAIService.swift | 28 ++++++++++--- .../Sources/GenerativeAISwift.swift | 5 +-- .../Sources/GenerativeModel.swift | 35 +++++----------- FirebaseVertexAI/Sources/VertexAI.swift | 41 +++++-------------- Package.swift | 11 +---- 9 files changed, 48 insertions(+), 125 deletions(-) diff --git a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift b/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift index 399777ad1e8..7b3b78753db 100644 --- a/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift +++ b/FirebaseVertexAI/Sample/ChatSample/Views/ErrorDetailsView.swift @@ -142,22 +142,6 @@ struct ErrorDetailsView: View { SafetyRatingsSection(ratings: ratings) } - case GenerateContentError.invalidAPIKey: - Section("Error Type") { - Text("Invalid API Key") - } - - Section("Details") { - SubtitleFormRow(title: "Error description", value: error.localizedDescription) - SubtitleMarkdownFormRow( - title: "Help", - value: """ - The `API_KEY` provided in the `GoogleService-Info.plist` file is invalid. Download a - new copy of the file from the [Firebase Console](https://console.firebase.google.com). - """ - ) - } - default: Section("Error Type") { Text("Some other error") @@ -222,11 +206,3 @@ struct ErrorDetailsView: View { return ErrorDetailsView(error: error) } - -#Preview("Invalid API Key") { - ErrorDetailsView(error: GenerateContentError.invalidAPIKey) -} - -#Preview("Unsupported User Location") { - ErrorDetailsView(error: GenerateContentError.unsupportedUserLocation) -} diff --git a/FirebaseVertexAI/Sources/Errors.swift b/FirebaseVertexAI/Sources/Errors.swift index 35e7e6b813b..0b1b8848411 100644 --- a/FirebaseVertexAI/Sources/Errors.swift +++ b/FirebaseVertexAI/Sources/Errors.swift @@ -30,14 +30,6 @@ struct RPCError: Error { self.status = status self.details = details } - - func isInvalidAPIKeyError() -> Bool { - return errorInfo?.reason == "API_KEY_INVALID" - } - - func isUnsupportedUserLocationError() -> Bool { - return message == RPCErrorMessage.unsupportedUserLocation.rawValue - } } extension RPCError: Decodable { @@ -179,10 +171,6 @@ enum RPCStatus: String, Decodable { case dataLoss = "DATA_LOSS" } -enum RPCErrorMessage: String { - case unsupportedUserLocation = "User location is not supported for the API use." -} - enum InvalidCandidateError: Error { case emptyContent(underlyingError: Error) case malformedContent(underlyingError: Error) diff --git a/FirebaseVertexAI/Sources/GenerateContentError.swift b/FirebaseVertexAI/Sources/GenerateContentError.swift index 055928e5f1b..31501a7744c 100644 --- a/FirebaseVertexAI/Sources/GenerateContentError.swift +++ b/FirebaseVertexAI/Sources/GenerateContentError.swift @@ -28,17 +28,4 @@ public enum GenerateContentError: Error { /// A response didn't fully complete. See the `FinishReason` for more information. case responseStoppedEarly(reason: FinishReason, response: GenerateContentResponse) - - /// The provided API key is invalid. - case invalidAPIKey(message: String) - - /// The user's location (region) is not supported by the API. - /// - /// See the Google documentation for a - /// [list of regions](https://ai.google.dev/available_regions#available_regions) - /// (countries and territories) where the API is available. - /// - /// - Important: The API is only available in - /// [specific regions](https://ai.google.dev/available_regions#available_regions). - case unsupportedUserLocation } diff --git a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift index 2bf94503b1a..21f35b1b728 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift @@ -38,8 +38,8 @@ public struct RequestOptions { /// - Parameters: /// - timeout The request’s timeout interval in seconds; if not specified uses the default value /// for a `URLRequest`. - /// - apiVersion The API version to use in requests to the backend; defaults to "v1". - public init(timeout: TimeInterval? = nil, apiVersion: String = "v1") { + /// - apiVersion The API version to use in requests to the backend; defaults to "v2beta". + public init(timeout: TimeInterval? = nil, apiVersion: String = "v2beta") { self.timeout = timeout self.apiVersion = apiVersion } diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index 8d904738927..e1df1010c06 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import FirebaseAppCheckInterop +import FirebaseCore import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -19,15 +21,18 @@ struct GenerativeAIService { /// Gives permission to talk to the backend. private let apiKey: String + private let appCheck: AppCheckInterop? + private let urlSession: URLSession - init(apiKey: String, urlSession: URLSession) { + init(apiKey: String, appCheck: AppCheckInterop?, urlSession: URLSession) { self.apiKey = apiKey + self.appCheck = appCheck self.urlSession = urlSession } func loadRequest(request: T) async throws -> T.Response { - let urlRequest = try urlRequest(request: request) + let urlRequest = try await urlRequest(request: request) #if DEBUG printCURLCommand(from: urlRequest) @@ -59,7 +64,7 @@ struct GenerativeAIService { Task { let urlRequest: URLRequest do { - urlRequest = try self.urlRequest(request: request) + urlRequest = try await self.urlRequest(request: request) } catch { continuation.finish(throwing: error) return @@ -146,13 +151,24 @@ struct GenerativeAIService { // MARK: - Private Helpers - private func urlRequest(request: T) throws -> URLRequest { + 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("genai-swift/\(GenerativeAISwift.version)", - forHTTPHeaderField: "x-goog-api-client") + // TODO: Determine the right client header to use. + // urlRequest.setValue("genai-swift/\(GenerativeAISwift.version))", + // forHTTPHeaderField: "x-goog-api-client") urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let appCheck { + let tokenResult = await appCheck.getToken(forcingRefresh: false) + urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") + if let error = tokenResult.error { + Logging.default + .debug("[GoogleGenerativeAI] Failed to fetch AppCheck token. Error: \(error)") + } + } + let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase urlRequest.httpBody = try encoder.encode(request) diff --git a/FirebaseVertexAI/Sources/GenerativeAISwift.swift b/FirebaseVertexAI/Sources/GenerativeAISwift.swift index 4146b60d989..0f0af2bc3e2 100644 --- a/FirebaseVertexAI/Sources/GenerativeAISwift.swift +++ b/FirebaseVertexAI/Sources/GenerativeAISwift.swift @@ -11,6 +11,7 @@ // 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 #if !os(macOS) && !os(iOS) @@ -20,7 +21,5 @@ import Foundation /// Constants associated with the GenerativeAISwift SDK @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public enum GenerativeAISwift { - /// String value of the SDK version - public static let version = "0.4.8" - static let baseURL = "https://generativelanguage.googleapis.com" + static let baseURL = "https://staging-firebaseml.sandbox.googleapis.com" } diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index 3617cd3a251..aad2b3e99c0 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import FirebaseAppCheckInterop import Foundation /// A type that represents a remote multimodal model (like Gemini), with the ability to generate @@ -44,31 +45,21 @@ public final class GenerativeModel { /// - apiKey: The API key for your project. /// - generationConfig: The content generation parameters your model should use. /// - safetySettings: A value describing what types of harmful content your model should allow. - /// - requestOptions Configuration parameters for sending requests to the backend. - public convenience init(name: String, - apiKey: String, - generationConfig: GenerationConfig? = nil, - safetySettings: [SafetySetting]? = nil, - requestOptions: RequestOptions = RequestOptions()) { - self.init( - name: name, - apiKey: apiKey, - generationConfig: generationConfig, - safetySettings: safetySettings, - requestOptions: requestOptions, - urlSession: .shared - ) - } - - /// The designated initializer for this class. + /// - requestOptions: Configuration parameters for sending requests to the backend. + /// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`. init(name: String, apiKey: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, - requestOptions: RequestOptions = RequestOptions(), - urlSession: URLSession) { + requestOptions: RequestOptions, + appCheck: AppCheckInterop?, + urlSession: URLSession = .shared) { modelResourceName = GenerativeModel.modelResourceName(name: name) - generativeAIService = GenerativeAIService(apiKey: apiKey, urlSession: urlSession) + generativeAIService = GenerativeAIService( + apiKey: apiKey, + appCheck: appCheck, + urlSession: urlSession + ) self.generationConfig = generationConfig self.safetySettings = safetySettings self.requestOptions = requestOptions @@ -282,10 +273,6 @@ public final class GenerativeModel { private static func generateContentError(from error: Error) -> GenerateContentError { if let error = error as? GenerateContentError { return error - } else if let error = error as? RPCError, error.isInvalidAPIKeyError() { - return GenerateContentError.invalidAPIKey(message: error.message) - } else if let error = error as? RPCError, error.isUnsupportedUserLocationError() { - return GenerateContentError.unsupportedUserLocation } return GenerateContentError.internalError(underlying: error) } diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index 8fb0f9e8c68..0f79e1be99c 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -12,13 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation - import FirebaseAppCheckInterop import FirebaseCore - -// Exports the GoogleGenerativeAI module to users of the SDK. -@_exported import GoogleGenerativeAI +import Foundation // Avoids exposing internal FirebaseCore APIs to Swift users. @_implementationOnly import FirebaseCoreExtension @@ -31,8 +27,9 @@ open class VertexAI: NSObject { /// Returns an instance of `GoogleGenerativeAI.GenerativeModel` that uses the Vertex AI API. /// /// This instance is configured with the default `FirebaseApp`. - public static func generativeModel(modelName: String, location: String) -> GoogleGenerativeAI - .GenerativeModel { + /// + /// TODO: Add RequestOptions to public API. + public static func generativeModel(modelName: String, location: String) -> GenerativeModel { guard let app = FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } @@ -40,8 +37,10 @@ open class VertexAI: NSObject { } /// Returns an instance of `GoogleGenerativeAI.GenerativeModel` that uses the Vertex AI API. + /// + /// TODO: Add RequestOptions to public API. public static func generativeModel(app: FirebaseApp, modelName: String, - location: String) -> GoogleGenerativeAI.GenerativeModel { + location: String) -> GenerativeModel { guard let provider = ComponentType.instance(for: VertexAIProvider.self, in: app.container) else { fatalError("No \(VertexAIProvider.self) instance found for Firebase app: \(app.name)") @@ -64,18 +63,15 @@ open class VertexAI: NSObject { private let modelResouceName: String lazy var model: GenerativeModel = { - let options = RequestOptions( - apiVersion: "v2beta", - endpoint: "staging-firebaseml.sandbox.googleapis.com", - hooks: [addAppCheckHeader] - ) guard let apiKey = app.options.apiKey else { fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.") } return GenerativeModel( name: modelResouceName, apiKey: apiKey, - requestOptions: options + // TODO: Add RequestOptions to public API. + requestOptions: RequestOptions(), + appCheck: appCheck ) }() @@ -104,21 +100,4 @@ open class VertexAI: NSObject { return "projects/\(projectID)/locations/\(location)/publishers/google/models/\(modelName)" } - - // MARK: Request Hooks - - /// Adds an App Check token to the provided request if App Check is included in the app. - /// - /// This demonstrates how an App Check token can be added to requests; it is currently ignored by - /// the backend. - /// - /// - Parameter request: The `URLRequest` to modify by adding an App Check token header. - func addAppCheckHeader(request: inout URLRequest) async { - guard let appCheck else { - return - } - - let tokenResult = await appCheck.getToken(forcingRefresh: false) - request.addValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck") - } } diff --git a/Package.swift b/Package.swift index ce0feb8850c..f88daf4f620 100644 --- a/Package.swift +++ b/Package.swift @@ -192,10 +192,6 @@ let package = Package( "100.0.0" ..< "101.0.0" ), .package(url: "https://github.com/google/app-check.git", "10.19.0" ..< "11.0.0"), - .package( - url: "https://github.com/google/generative-ai-swift.git", - revision: "c9f2c4913bc65aa267815962c7e91358c2d8463f" - ), ], targets: [ .target( @@ -1374,13 +1370,8 @@ let package = Package( "FirebaseAppCheckInterop", "FirebaseCore", "FirebaseCoreExtension", - .product(name: "GoogleGenerativeAI", package: "generative-ai-swift"), ], - path: "FirebaseVertexAI/Sources", - sources: [ - "VertexAI.swift", - "VertexAIComponent.swift", - ] + path: "FirebaseVertexAI/Sources" ), ] + firestoreTargets(), cLanguageStandard: .c99, From 8af373417ee5514c6782a3b827f59abdd5a39ef8 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 18 Mar 2024 22:30:24 +0000 Subject: [PATCH 028/103] Add CI workflow for Vertex AI (#12577) --- .github/workflows/vertexai.yml | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/vertexai.yml diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml new file mode 100644 index 00000000000..63cfd774e43 --- /dev/null +++ b/.github/workflows/vertexai.yml @@ -0,0 +1,37 @@ +name: vertexai + +on: + pull_request: + paths: + - 'FirebaseVertexAI**' + - '.github/workflows/vertexai.yml' + - 'Gemfile*' + schedule: + # Run every day at 11pm (PST) - cron uses UTC times + - cron: '0 7 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +jobs: + spm: + strategy: + matrix: + target: [iOS, macOS, catalyst] + os: [macos-13] + include: + - os: macos-13 + xcode: Xcode_15.2 + runs-on: ${{ matrix.os }} + env: + FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1 + steps: + - uses: actions/checkout@v4 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Initialize xcodebuild + run: xcodebuild -list + # TODO: Add unit tests and switch from `spmbuildonly` to `spm`. + - name: Build + run: scripts/third_party/travis/retry.sh scripts/build.sh FirebaseVertexAI ${{ matrix.target }} spmbuildonly From f0d31ea40fe0ecd3d3f9967b57f6617fb93da01e Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 19 Mar 2024 15:44:55 +0000 Subject: [PATCH 029/103] Fork `generative-ai-swift` tests for Vertex AI (#12584) Co-authored-by: Morgan Chen Co-authored-by: Paul Beusterien Co-authored-by: Peter Friese Co-authored-by: Ryan Wilson --- FirebaseVertexAI/Tests/Unit/ChatTests.swift | 67 ++ .../failure-model-not-found.json | 13 + .../success-total-tokens.json | 3 + .../streaming-failure-empty-content.txt | 1 + .../streaming-failure-error-mid-stream.txt | 17 + ...streaming-failure-finish-reason-safety.txt | 2 + .../streaming-failure-invalid-json.txt | 1 + .../streaming-failure-malformed-content.txt | 1 + ...treaming-failure-prompt-blocked-safety.txt | 2 + ...treaming-failure-recitation-no-content.txt | 6 + .../streaming-failure-unknown-finish-enum.txt | 11 + .../streaming-success-basic-reply-long.txt | 19 + .../streaming-success-basic-reply-short.txt | 2 + .../streaming-success-citations.txt | 13 + .../streaming-success-citations2.txt | 5 + .../streaming-success-unknown-safety-enum.txt | 2 + .../unary-failure-api-key.json | 21 + .../unary-failure-empty-content.json | 28 + ...e-finish-reason-recitation-no-content.json | 46 + ...ilure-finish-reason-safety-no-content.json | 46 + .../unary-failure-finish-reason-safety.json | 53 + .../unary-failure-image-rejected.json | 13 + .../unary-failure-invalid-response.json | 14 + .../unary-failure-malformed-content.json | 30 + .../unary-failure-prompt-blocked-safety.json | 23 + ...ry-failure-unknown-enum-finish-reason.json | 53 + ...y-failure-unknown-enum-prompt-blocked.json | 23 + .../unary-failure-unknown-model.json | 13 + ...ary-failure-unsupported-user-location.json | 13 + .../unary-success-basic-reply-long.json | 54 + .../unary-success-basic-reply-short.json | 54 + .../unary-success-citations.json | 64 ++ .../unary-success-missing-safety-ratings.json | 16 + .../unary-success-quote-reply.json | 54 + ...y-success-unknown-enum-safety-ratings.json | 46 + .../Tests/Unit/GenerativeModelTests.swift | 930 ++++++++++++++++++ .../Tests/Unit/GoogleAITests.swift | 175 ++++ .../Tests/Unit/MockURLProtocol.swift | 62 ++ .../Tests/Unit/PartsRepresentableTests.swift | 138 +++ 39 files changed, 2134 insertions(+) create mode 100644 FirebaseVertexAI/Tests/Unit/ChatTests.swift create mode 100644 FirebaseVertexAI/Tests/Unit/CountTokenResponses/failure-model-not-found.json create mode 100644 FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-empty-content.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-error-mid-stream.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-finish-reason-safety.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-invalid-json.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-malformed-content.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-prompt-blocked-safety.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-recitation-no-content.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-unknown-finish-enum.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-unknown-safety-enum.txt create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-empty-content.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety-no-content.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-image-rejected.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-invalid-response.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-malformed-content.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-prompt-blocked-safety.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-finish-reason.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-prompt-blocked.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-model.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-missing-safety-ratings.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-quote-reply.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-unknown-enum-safety-ratings.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift create mode 100644 FirebaseVertexAI/Tests/Unit/GoogleAITests.swift create mode 100644 FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift create mode 100644 FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift new file mode 100644 index 00000000000..668e3704122 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -0,0 +1,67 @@ +// 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 +@testable import GoogleGenerativeAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) +final class ChatTests: XCTestCase { + var urlSession: URLSession! + + override func setUp() { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + urlSession = URLSession(configuration: configuration) + } + + override func tearDown() { + MockURLProtocol.requestHandler = nil + } + + func testMergingText() async throws { + let fileURL = try XCTUnwrap(Bundle.module.url( + forResource: "streaming-success-basic-reply-long", + withExtension: "txt" + )) + + MockURLProtocol.requestHandler = { request in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + return (response, fileURL.lines) + } + + let model = GenerativeModel(name: "my-model", apiKey: "API_KEY", urlSession: urlSession) + let chat = Chat(model: model, history: []) + let input = "Test input" + let stream = chat.sendMessageStream(input) + + // Ensure the values are parsed correctly + for try await value in stream { + XCTAssertNotNil(value.text) + } + + XCTAssertEqual(chat.history.count, 2) + XCTAssertEqual(chat.history[0].parts[0].text, input) + + let finalText = "1 2 3 4 5 6 7 8 9 10" + let assembledExpectation = ModelContent(role: "model", parts: finalText) + XCTAssertEqual(chat.history[0].parts[0].text, input) + XCTAssertEqual(chat.history[1], assembledExpectation) + } +} diff --git a/FirebaseVertexAI/Tests/Unit/CountTokenResponses/failure-model-not-found.json b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/failure-model-not-found.json new file mode 100644 index 00000000000..50fcb725667 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/failure-model-not-found.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 404, + "message": "models/test-model-name is not found for API version v1beta, or is not supported for countTokens. Call ListModels to see the list of available models and their supported methods.", + "status": "NOT_FOUND", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "[ORIGINAL ERROR] generic::not_found: models/test-model-name is not found for API version v1beta, or is not supported for countTokens. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/test-model-name is not found for API version v1beta, or is not supported for countTokens. Call ListModels to see the list of available models and their supported methods.\" }" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json new file mode 100644 index 00000000000..0bccd9e7c3d --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json @@ -0,0 +1,3 @@ +{ + "totalTokens": 6 +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-empty-content.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-empty-content.txt new file mode 100644 index 00000000000..5762b515325 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-empty-content.txt @@ -0,0 +1 @@ +data: {"candidates": [{"content": {},"index": 0}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-error-mid-stream.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-error-mid-stream.txt new file mode 100644 index 00000000000..aeb4eb09389 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-error-mid-stream.txt @@ -0,0 +1,17 @@ +data: {"candidates": [{"content": {"parts": [{"text": "First "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": "Second "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +{ + "error": { + "code": 499, + "message": "The operation was cancelled.", + "status": "CANCELLED", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "[ORIGINAL ERROR] generic::cancelled: " + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-finish-reason-safety.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-finish-reason-safety.txt new file mode 100644 index 00000000000..b73c75cf505 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-finish-reason-safety.txt @@ -0,0 +1,2 @@ +data: {"candidates": [{"content": {"parts": [{"text": "No"}]},"finishReason": "SAFETY","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-invalid-json.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-invalid-json.txt new file mode 100644 index 00000000000..3aea78474cb --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-invalid-json.txt @@ -0,0 +1 @@ +data: {"this": [{"is": {"not": [{"a": "valid"}]}, "response": {}}]} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-malformed-content.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-malformed-content.txt new file mode 100644 index 00000000000..273b6cdc5ca --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-malformed-content.txt @@ -0,0 +1 @@ +data: {"candidates": [{"content": {"missing-parts": true},"index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_TOXICITY","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_VIOLENCE","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-prompt-blocked-safety.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-prompt-blocked-safety.txt new file mode 100644 index 00000000000..58c914af08e --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-prompt-blocked-safety.txt @@ -0,0 +1,2 @@ +data: {"promptFeedback": {"blockReason": "SAFETY","safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "HIGH"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-recitation-no-content.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-recitation-no-content.txt new file mode 100644 index 00000000000..60ec91d6dfc --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-recitation-no-content.txt @@ -0,0 +1,6 @@ +data: {"candidates": [{"content": {"parts": [{"text": "Some information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": "Some information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "LOW"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 30,"endIndex": 179,"uri": "https://www.example.com/some-citation","license": ""}]}}]} + +data: {"candidates": [{"finishReason": "RECITATION","index": 0}]} + diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-unknown-finish-enum.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-unknown-finish-enum.txt new file mode 100644 index 00000000000..6194abd8957 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-failure-unknown-finish-enum.txt @@ -0,0 +1,11 @@ +data: {"candidates": [{"content": {"parts": [{"text": "**Cats:**\n\n- **Physical Characteristics:**\n - Size: Cats come"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": " in a wide range of sizes, from small breeds like the Singapura to large breeds like the Maine Coon.\n - Fur: Cats have soft, furry coats"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": " that can vary in length and texture depending on the breed.\n - Eyes: Cats have large, expressive eyes that can be various colors, including green, blue, yellow, and hazel.\n - Ears: Cats have pointed, erect ears that are sensitive to sound.\n - Tail: Cats have long"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": ", flexible tails that they use for balance and communication.\n\n- **Behavior and Personality:**\n - Independent: Cats are often described as independent animals that enjoy spending time alone.\n - Affectionate: Despite their independent nature, cats can be very affectionate and form strong bonds with their owners.\n - Playful: Cats are naturally playful and enjoy engaging in activities such as chasing toys, climbing, and pouncing.\n - Curious: Cats are curious creatures that love to explore their surroundings.\n - Vocal: Cats communicate through a variety of vocalizations, including meows, purrs, hisses, and grow"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "ls.\n\n- **Health and Care:**\n - Diet: Cats are obligate carnivores, meaning they require animal-based protein for optimal health.\n - Grooming: Cats spend a significant amount of time grooming themselves to keep their fur clean and free of mats.\n - Exercise: Cats need regular exercise to stay healthy and active. This can be achieved through play sessions or access to outdoor space.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a cat's health and detecting any potential health issues early on.\n\n**Dogs:**\n\n- **Physical Characteristics:**\n - Size: Dogs come in a wide range of sizes, from small breeds like the Chihuahua to giant breeds like the Great Dane.\n - Fur: Dogs have fur coats that can vary in length, texture, and color depending on the breed.\n - Eyes: Dogs have expressive eyes that can be various colors, including brown, blue, green, and hazel.\n - Ears: Dogs have floppy or erect ears that are sensitive to sound.\n - Tail: Dogs have long, wagging tails that they use for communication and expressing emotions.\n\n- **Behavior and Personality:**\n - Loyal: Dogs are known for their loyalty and"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": " devotion to their owners.\n - Friendly: Dogs are generally friendly and outgoing animals that enjoy interacting with people and other animals.\n - Playful: Dogs are playful and energetic creatures that love to engage in activities such as fetching, running, and playing with toys.\n - Trainable: Dogs are highly trainable and can learn a variety of commands and tricks.\n - Vocal: Dogs communicate through a variety of vocalizations, including barking, howling, whining, and growling.\n\n- **Health and Care:**\n - Diet: Dogs are omnivores and can eat a variety of foods, including meat, vegetables, and grains.\n - Grooming: Dogs require regular grooming to keep their fur clean and free of mats. The frequency of grooming depends on the breed and coat type.\n - Exercise: Dogs need regular exercise to stay healthy and active. The amount of exercise required varies depending on the breed and age of the dog.\n - Veterinary Care: Regular veterinary checkups are essential for maintaining a dog's health and detecting any potential health issues early on."}]},"finishReason": "FAKE_ENUM","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT_NEW_ENUM","probability": "NEGLIGIBLE_UNKNOWN_ENUM"}]}]} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt new file mode 100644 index 00000000000..bca95140490 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt @@ -0,0 +1,19 @@ +data: {"candidates": [{"content": {"parts": [{"text": "1 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": "2 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "3 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "4 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "5 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "6 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "7 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "8 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "9 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": "10"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt new file mode 100644 index 00000000000..a7f5476954e --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt @@ -0,0 +1,2 @@ +data: {"candidates": [{"content": {"parts": [{"text": "Cheyenne"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt new file mode 100644 index 00000000000..4f50be7d32f --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt @@ -0,0 +1,13 @@ +data: {"candidates": [{"content": {"parts": [{"text": "Some information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + +data: {"candidates": [{"content": {"parts": [{"text": " More information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": ", Even more information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} + +data: {"candidates": [{"content": {"parts": [{"text": " Some information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-1","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-2","license": ""}]}}]} + +data: {"candidates": [{"content": {"parts": [{"text": "More information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-3","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-4","license": ""}]}}]} + +data: {"candidates": [{"content": {"parts": [{"text": "Even more information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-5","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-6","license": ""}]}}]} + +data: {"candidates": [{"content": {"parts": [{"text": "Physics (YouTube Channel)"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.google.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.google.com","license": ""}]}}]} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt new file mode 100644 index 00000000000..665e993ad75 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt @@ -0,0 +1,5 @@ +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "In the context of the science fiction comedy novel \"The Hitchhiker's Guide to the Galaxy\" by Douglas Adams, the answer to the \"Ultimate Question of"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.055720285,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.062674366},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.03904829,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03339982},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12220858,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.0540987},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.05781161,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03149938}]}]} + +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " Life, the Universe, and Everything\" is given as 42. However, in real life, there is no universally accepted meaning of life. The meaning"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.05910154,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.038321976},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.049589027,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.019271139},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12787028,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03986249},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.098946586,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04177388}],"citationMetadata": {"citations": [{"startIndex": 52,"endIndex": 181,"uri": "https://imcsteakhouse.com.au/faqs"}]}}]} + +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " of life is a philosophical question that has been pondered by humans for centuries, and there is no single answer that is widely agreed upon."}]},"finishReason": "STOP","safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.044764314,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.028870905},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.04240383,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.012576347},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.10650458,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.034880884},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.11085559,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03191915}]}],"usageMetadata": {"promptTokenCount": 9,"candidatesTokenCount": 91,"totalTokenCount": 100}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-unknown-safety-enum.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-unknown-safety-enum.txt new file mode 100644 index 00000000000..8f7a8ff3c1c --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-unknown-safety-enum.txt @@ -0,0 +1,2 @@ +data: {"candidates": [{"content": {"parts": [{"text": "Cheyenne"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_SOMETHING_NEW","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} + diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json new file mode 100644 index 00000000000..ecf6f6b53fa --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json @@ -0,0 +1,21 @@ +{ + "error": { + "code": 400, + "message": "API key not valid. Please pass a valid API key.", + "status": "INVALID_ARGUMENT", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.ErrorInfo", + "reason": "API_KEY_INVALID", + "domain": "googleapis.com", + "metadata": { + "service": "generativelanguage.googleapis.com" + } + }, + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "Invalid API key: AIzv00G7VmUCUeC-5OglO3hcXM" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-empty-content.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-empty-content.json new file mode 100644 index 00000000000..4e1889660f2 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-empty-content.json @@ -0,0 +1,28 @@ +{ + "candidates": [ + { + "content": {}, + "index": 0 + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json new file mode 100644 index 00000000000..0d99cd71d51 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json @@ -0,0 +1,46 @@ +{ + "candidates": [ + { + "finishReason": "RECITATION", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "HIGH" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety-no-content.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety-no-content.json new file mode 100644 index 00000000000..03958d4afc1 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety-no-content.json @@ -0,0 +1,46 @@ +{ + "candidates": [ + { + "finishReason": "SAFETY", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "HIGH" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety.json new file mode 100644 index 00000000000..3249c739cf8 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-safety.json @@ -0,0 +1,53 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "No" + } + ] + }, + "finishReason": "SAFETY", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "HIGH" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-image-rejected.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-image-rejected.json new file mode 100644 index 00000000000..9dacdc71e7a --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-image-rejected.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 400, + "message": "Request contains an invalid argument.", + "status": "INVALID_ARGUMENT", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-invalid-response.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-invalid-response.json new file mode 100644 index 00000000000..49d05e1840b --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-invalid-response.json @@ -0,0 +1,14 @@ +{ + "this": [ + { + "is": { + "not": [ + { + "a": "valid" + } + ] + }, + "response": {} + } + ] +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-malformed-content.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-malformed-content.json new file mode 100644 index 00000000000..737f2e08548 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-malformed-content.json @@ -0,0 +1,30 @@ +{ + "candidates": [ + { + "content": { + "invalid-field": true + }, + "index": 0 + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-prompt-blocked-safety.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-prompt-blocked-safety.json new file mode 100644 index 00000000000..9d2abbb23d6 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-prompt-blocked-safety.json @@ -0,0 +1,23 @@ +{ + "promptFeedback": { + "blockReason": "SAFETY", + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "HIGH" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-finish-reason.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-finish-reason.json new file mode 100644 index 00000000000..1adbc70cae4 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-finish-reason.json @@ -0,0 +1,53 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Some text" + } + ] + }, + "finishReason": "FAKE_NEW_FINISH_REASON", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-prompt-blocked.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-prompt-blocked.json new file mode 100644 index 00000000000..a00cbb72cfc --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-enum-prompt-blocked.json @@ -0,0 +1,23 @@ +{ + "promptFeedback": { + "blockReason": "FAKE_NEW_BLOCK_REASON", + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-model.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-model.json new file mode 100644 index 00000000000..60b3f55c978 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unknown-model.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 404, + "message": "models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.", + "status": "NOT_FOUND", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "[ORIGINAL ERROR] generic::not_found: models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods. [google.rpc.error_details_ext] { message: \"models/unknown is not found for API version v1, or is not supported for GenerateContent. Call ListModels to see the list of available models and their supported methods.\" }" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json new file mode 100644 index 00000000000..c4c2ace4e20 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json @@ -0,0 +1,13 @@ +{ + "error": { + "code": 400, + "message": "User location is not supported for the API use.", + "status": "FAILED_PRECONDITION", + "details": [ + { + "@type": "type.googleapis.com/google.rpc.DebugInfo", + "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json new file mode 100644 index 00000000000..59b84de92fe --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "You can ask me a wide range of questions on various topics. Here are some examples:\n\n1. **General Knowledge:**\n - What is the capital of France?\n - Who painted the Mona Lisa?\n - What is the largest ocean in the world?\n\n2. **Science and Technology:**\n - How does a computer work?\n - What is the difference between a virus and a bacteria?\n - What are the latest advancements in artificial intelligence?\n\n3. **History and Culture:**\n - Who was the first president of the United States?\n - What is the significance of the Great Wall of China?\n - What are some of the most famous works of Shakespeare?\n\n4. **Current Events:**\n - What is the latest news about the COVID-19 pandemic?\n - Who is the current president of Ukraine?\n - What are the major issues being discussed in the upcoming election?\n\n5. **Personal Questions:**\n - What are your hobbies?\n - What is your favorite book or movie?\n - What are your thoughts on the future of technology?\n\n6. **Fun and Games:**\n - Can you tell me a joke?\n - What is the answer to this riddle: \"I have keys but no locks. I have space but no room. You can enter, but can't go outside.\" (Answer: a keyboard)\n - Let's play a game of 20 questions.\n\n7. **Hypothetical Questions:**\n - What would you do if you won the lottery?\n - What would happen if time travel were possible?\n - What is the meaning of life?\n\n8. **Philosophical Questions:**\n - What is the nature of reality?\n - Does free will exist?\n - What is the difference between right and wrong?\n\n9. **Creative Questions:**\n - Write a poem about a sunset.\n - Design a logo for a new company.\n - Compose a song about your favorite season.\n\n10. **Technical Questions:**\n - How can I improve the performance of my computer?\n - What is the best way to troubleshoot a network issue?\n - How do I create a website using HTML and CSS?\n\nRemember, I am still under development and may not be able to answer all questions perfectly. However, I will do my best to provide you with accurate and informative responses." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json new file mode 100644 index 00000000000..40a9a6da58e --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Mountain View, California, United States" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json new file mode 100644 index 00000000000..ac99fcf0944 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json @@ -0,0 +1,64 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Some information cited from an external source" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ], + "citationMetadata": { + "citationSources": [ + { + "startIndex": 574, + "endIndex": 705, + "uri": "https://www.example.com/some-citation", + "license": "" + } + ] + } + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-missing-safety-ratings.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-missing-safety-ratings.json new file mode 100644 index 00000000000..645d3e6425e --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-missing-safety-ratings.json @@ -0,0 +1,16 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "This is the generated content." + } + ] + }, + "index": 0 + } + ], + "promptFeedback": { + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-quote-reply.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-quote-reply.json new file mode 100644 index 00000000000..e5bf4f0b497 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-quote-reply.json @@ -0,0 +1,54 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Google's mission is to \"organize the world's information and make it universally accessible and useful.\"" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-unknown-enum-safety-ratings.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-unknown-enum-safety-ratings.json new file mode 100644 index 00000000000..3c47a21322c --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-unknown-enum-safety-ratings.json @@ -0,0 +1,46 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Some text" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "MEDIUM" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "FAKE_NEW_HARM_PROBABILITY" + }, + { + "category": "FAKE_NEW_HARM_CATEGORY", + "probability": "HIGH" + } + ] + } + ], + "promptFeedback": { + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "MEDIUM" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "FAKE_NEW_HARM_PROBABILITY" + }, + { + "category": "FAKE_NEW_HARM_CATEGORY", + "probability": "HIGH" + } + ] + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift new file mode 100644 index 00000000000..b835a9ea05f --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -0,0 +1,930 @@ +// 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. + +@testable import GoogleGenerativeAI +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) +final class GenerativeModelTests: XCTestCase { + let testPrompt = "What sorts of questions can I ask you?" + let safetyRatingsNegligible: [SafetyRating] = [ + .init(category: .sexuallyExplicit, probability: .negligible), + .init(category: .hateSpeech, probability: .negligible), + .init(category: .harassment, probability: .negligible), + .init(category: .dangerousContent, probability: .negligible), + ] + + var urlSession: URLSession! + var model: GenerativeModel! + + override func setUp() async throws { + let configuration = URLSessionConfiguration.default + configuration.protocolClasses = [MockURLProtocol.self] + urlSession = try XCTUnwrap(URLSession(configuration: configuration)) + model = GenerativeModel(name: "my-model", apiKey: "API_KEY", urlSession: urlSession) + } + + override func tearDown() { + MockURLProtocol.requestHandler = nil + } + + // MARK: - Generate Content + + func testGenerateContent_success_basicReplyLong() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-long", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + let finishReason = try XCTUnwrap(candidate.finishReason) + XCTAssertEqual(finishReason, .stop) + XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + let partText = try XCTUnwrap(part.text) + XCTAssertTrue(partText.hasPrefix("You can ask me a wide range of questions")) + XCTAssertEqual(response.text, partText) + let promptFeedback = try XCTUnwrap(response.promptFeedback) + XCTAssertNil(promptFeedback.blockReason) + XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) + } + + func testGenerateContent_success_basicReplyShort() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + let finishReason = try XCTUnwrap(candidate.finishReason) + XCTAssertEqual(finishReason, .stop) + XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + XCTAssertEqual(part.text, "Mountain View, California, United States") + XCTAssertEqual(response.text, part.text) + let promptFeedback = try XCTUnwrap(response.promptFeedback) + XCTAssertNil(promptFeedback.blockReason) + XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) + } + + func testGenerateContent_success_citations() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-citations", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + XCTAssertEqual(response.text, "Some information cited from an external source") + let citationMetadata = try XCTUnwrap(candidate.citationMetadata) + XCTAssertEqual(citationMetadata.citationSources.count, 1) + let citationSource = try XCTUnwrap(citationMetadata.citationSources.first) + XCTAssertEqual(citationSource.uri, "https://www.example.com/some-citation") + XCTAssertEqual(citationSource.startIndex, 574) + XCTAssertEqual(citationSource.endIndex, 705) + XCTAssertEqual(citationSource.license, "") + } + + func testGenerateContent_success_quoteReply() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-quote-reply", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + let finishReason = try XCTUnwrap(candidate.finishReason) + XCTAssertEqual(finishReason, .stop) + XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + let partText = try XCTUnwrap(part.text) + XCTAssertTrue(partText.hasPrefix("Google")) + XCTAssertEqual(response.text, part.text) + let promptFeedback = try XCTUnwrap(response.promptFeedback) + XCTAssertNil(promptFeedback.blockReason) + XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) + } + + func testGenerateContent_success_unknownEnum_safetyRatings() async throws { + let expectedSafetyRatings = [ + SafetyRating(category: .harassment, probability: .medium), + SafetyRating(category: .dangerousContent, probability: .unknown), + SafetyRating(category: .unknown, probability: .high), + ] + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-unknown-enum-safety-ratings", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.text, "Some text") + XCTAssertEqual(response.candidates.first?.safetyRatings, expectedSafetyRatings) + XCTAssertEqual(response.promptFeedback?.safetyRatings, expectedSafetyRatings) + } + + func testGenerateContent_success_prefixedModelName() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json" + ) + let model = GenerativeModel( + // Model name is prefixed with "models/". + name: "models/test-model", + apiKey: "API_KEY", + urlSession: urlSession + ) + + _ = try await model.generateContent(testPrompt) + } + + func testGenerateContent_failure_invalidAPIKey() async throws { + let expectedStatusCode = 400 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-api-key", + withExtension: "json", + statusCode: expectedStatusCode + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.internalError; no error thrown.") + } catch let GenerateContentError.invalidAPIKey(message) { + XCTAssertEqual(message, "API key not valid. Please pass a valid API key.") + } catch { + XCTFail("Should throw GenerateContentError.invalidAPIKey; error thrown: \(error)") + } + } + + func testGenerateContent_failure_emptyContent() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-empty-content", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.internalError; no error thrown.") + } catch let GenerateContentError + .internalError(underlying: invalidCandidateError as InvalidCandidateError) { + guard case let .emptyContent(decodingError) = invalidCandidateError else { + XCTFail("Not an InvalidCandidateError.emptyContent error: \(invalidCandidateError)") + return + } + _ = try XCTUnwrap(decodingError as? DecodingError, + "Not a DecodingError: \(decodingError)") + } catch { + XCTFail("Should throw GenerateContentError.internalError; error thrown: \(error)") + } + } + + func testGenerateContent_failure_finishReasonSafety() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-finish-reason-safety", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw") + } catch let GenerateContentError.responseStoppedEarly(reason, response) { + XCTAssertEqual(reason, .safety) + XCTAssertEqual(response.text, "No") + } catch { + XCTFail("Should throw a responseStoppedEarly") + } + } + + func testGenerateContent_failure_finishReasonSafety_noContent() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-finish-reason-safety-no-content", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw") + } catch let GenerateContentError.responseStoppedEarly(reason, response) { + XCTAssertEqual(reason, .safety) + XCTAssertNil(response.text) + } catch { + XCTFail("Should throw a responseStoppedEarly") + } + } + + func testGenerateContent_failure_imageRejected() async throws { + let expectedStatusCode = 400 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-image-rejected", + withExtension: "json", + statusCode: 400 + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.internalError; no error thrown.") + } catch let GenerateContentError.internalError(underlying: rpcError as RPCError) { + XCTAssertEqual(rpcError.status, .invalidArgument) + XCTAssertEqual(rpcError.httpResponseCode, expectedStatusCode) + XCTAssertEqual(rpcError.message, "Request contains an invalid argument.") + } catch { + XCTFail("Should throw GenerateContentError.internalError; error thrown: \(error)") + } + } + + func testGenerateContent_failure_promptBlockedSafety() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-prompt-blocked-safety", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw") + } catch let GenerateContentError.promptBlocked(response) { + XCTAssertNil(response.text) + } catch { + XCTFail("Should throw a promptBlocked") + } + } + + func testGenerateContent_failure_unknownEnum_finishReason() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-unknown-enum-finish-reason", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw") + } catch let GenerateContentError.responseStoppedEarly(reason, response) { + XCTAssertEqual(reason, .unknown) + XCTAssertEqual(response.text, "Some text") + } catch { + XCTFail("Should throw a responseStoppedEarly") + } + } + + func testGenerateContent_failure_unknownEnum_promptBlocked() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-unknown-enum-prompt-blocked", + withExtension: "json" + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw") + } catch let GenerateContentError.promptBlocked(response) { + let promptFeedback = try XCTUnwrap(response.promptFeedback) + XCTAssertEqual(promptFeedback.blockReason, .unknown) + } catch { + XCTFail("Should throw a promptBlocked") + } + } + + func testGenerateContent_failure_unknownModel() async throws { + let expectedStatusCode = 404 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-unknown-model", + withExtension: "json", + statusCode: 404 + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.internalError; no error thrown.") + } catch let GenerateContentError.internalError(underlying: rpcError as RPCError) { + XCTAssertEqual(rpcError.status, .notFound) + XCTAssertEqual(rpcError.httpResponseCode, expectedStatusCode) + XCTAssertTrue(rpcError.message.hasPrefix("models/unknown is not found")) + } catch { + XCTFail("Should throw GenerateContentError.internalError; error thrown: \(error)") + } + } + + func testGenerateContent_failure_unsupportedUserLocation() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-unsupported-user-location", + withExtension: "json", + statusCode: 400 + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.unsupportedUserLocation; no error thrown.") + } catch GenerateContentError.unsupportedUserLocation { + return + } + + XCTFail("Expected an unsupported user location error.") + } + + func testGenerateContent_failure_nonHTTPResponse() async throws { + MockURLProtocol.requestHandler = try nonHTTPRequestHandler() + + var responseError: Error? + var content: GenerateContentResponse? + do { + content = try await model.generateContent(testPrompt) + } catch { + responseError = error + } + + XCTAssertNil(content) + XCTAssertNotNil(responseError) + let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) + guard case let .internalError(underlyingError) = generateContentError else { + XCTFail("Not an internal error: \(generateContentError)") + return + } + XCTAssertEqual(underlyingError.localizedDescription, "Response was not an HTTP response.") + } + + func testGenerateContent_failure_invalidResponse() async throws { + MockURLProtocol.requestHandler = try httpRequestHandler( + forResource: "unary-failure-invalid-response", + withExtension: "json" + ) + + var responseError: Error? + var content: GenerateContentResponse? + do { + content = try await model.generateContent(testPrompt) + } catch { + responseError = error + } + + XCTAssertNil(content) + XCTAssertNotNil(responseError) + let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) + guard case let .internalError(underlyingError) = generateContentError else { + XCTFail("Not an internal error: \(generateContentError)") + return + } + let decodingError = try XCTUnwrap(underlyingError as? DecodingError) + guard case let .dataCorrupted(context) = decodingError else { + XCTFail("Not a data corrupted error: \(decodingError)") + return + } + XCTAssert(context.debugDescription.hasPrefix("Failed to decode GenerateContentResponse")) + } + + func testGenerateContent_failure_malformedContent() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-malformed-content", + withExtension: "json" + ) + + var responseError: Error? + var content: GenerateContentResponse? + do { + content = try await model.generateContent(testPrompt) + } catch { + responseError = error + } + + XCTAssertNil(content) + XCTAssertNotNil(responseError) + let generateContentError = try XCTUnwrap(responseError as? GenerateContentError) + guard case let .internalError(underlyingError) = generateContentError else { + XCTFail("Not an internal error: \(generateContentError)") + return + } + let invalidCandidateError = try XCTUnwrap(underlyingError as? InvalidCandidateError) + guard case let .malformedContent(malformedContentUnderlyingError) = invalidCandidateError else { + XCTFail("Not a malformed content error: \(invalidCandidateError)") + return + } + _ = try XCTUnwrap( + malformedContentUnderlyingError as? DecodingError, + "Not a decoding error: \(malformedContentUnderlyingError)" + ) + } + + func testGenerateContentMissingSafetyRatings() async throws { + MockURLProtocol.requestHandler = try httpRequestHandler( + forResource: "unary-success-missing-safety-ratings", + withExtension: "json" + ) + + let content = try await model.generateContent(testPrompt) + let promptFeedback = try XCTUnwrap(content.promptFeedback) + XCTAssertEqual(promptFeedback.safetyRatings.count, 0) + XCTAssertEqual(content.text, "This is the generated content.") + } + + func testGenerateContent_requestOptions_customTimeout() async throws { + let expectedTimeout = 150.0 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + timeout: expectedTimeout + ) + let requestOptions = RequestOptions(timeout: expectedTimeout) + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: requestOptions, + urlSession: urlSession + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + } + + // MARK: - Generate Content (Streaming) + + func testGenerateContentStream_failureInvalidAPIKey() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-api-key", + withExtension: "json" + ) + + do { + let stream = model.generateContentStream("Hi") + for try await _ in stream { + XCTFail("No content is there, this shouldn't happen.") + } + } catch GenerateContentError.invalidAPIKey { + // invalidAPIKey error is as expected, nothing else to check. + return + } + + XCTFail("Should have caught an error.") + } + + func testGenerateContentStream_failureEmptyContent() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-empty-content", + withExtension: "txt" + ) + + do { + let stream = model.generateContentStream("Hi") + for try await _ in stream { + XCTFail("No content is there, this shouldn't happen.") + } + } catch GenerateContentError.internalError(_ as InvalidCandidateError) { + // Underlying error is as expected, nothing else to check. + return + } + + XCTFail("Should have caught an error.") + } + + func testGenerateContentStream_failureFinishReasonSafety() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-finish-reason-safety", + withExtension: "txt" + ) + + do { + let stream = model.generateContentStream("Hi") + for try await _ in stream { + XCTFail("Content shouldn't be shown, this shouldn't happen.") + } + } catch let GenerateContentError.responseStoppedEarly(reason, _) { + XCTAssertEqual(reason, .safety) + return + } + + XCTFail("Should have caught an error.") + } + + func testGenerateContentStream_failurePromptBlockedSafety() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-prompt-blocked-safety", + withExtension: "txt" + ) + + do { + let stream = model.generateContentStream("Hi") + for try await _ in stream { + XCTFail("Content shouldn't be shown, this shouldn't happen.") + } + } catch let GenerateContentError.promptBlocked(response) { + XCTAssertEqual(response.promptFeedback?.blockReason, .safety) + return + } + + XCTFail("Should have caught an error.") + } + + func testGenerateContentStream_failureUnknownFinishEnum() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-unknown-finish-enum", + withExtension: "txt" + ) + + let stream = model.generateContentStream("Hi") + do { + for try await content in stream { + XCTAssertNotNil(content.text) + } + } catch let GenerateContentError.responseStoppedEarly(reason, _) { + XCTAssertEqual(reason, .unknown) + return + } + + XCTFail("Should have caught an error.") + } + + func testGenerateContentStream_successBasicReplyLong() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-basic-reply-long", + withExtension: "txt" + ) + + var responses = 0 + let stream = model.generateContentStream("Hi") + for try await content in stream { + XCTAssertNotNil(content.text) + responses += 1 + } + + XCTAssertEqual(responses, 10) + } + + func testGenerateContentStream_successBasicReplyShort() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-basic-reply-short", + withExtension: "txt" + ) + + var responses = 0 + let stream = model.generateContentStream("Hi") + for try await content in stream { + XCTAssertNotNil(content.text) + responses += 1 + } + + XCTAssertEqual(responses, 1) + } + + func testGenerateContentStream_successUnknownSafetyEnum() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-unknown-safety-enum", + withExtension: "txt" + ) + + var hadUnknown = false + let stream = model.generateContentStream("Hi") + for try await content in stream { + XCTAssertNotNil(content.text) + if let ratings = content.candidates.first?.safetyRatings, + ratings.contains(where: { $0.category == .unknown }) { + hadUnknown = true + } + } + + XCTAssertTrue(hadUnknown) + } + + func testGenerateContentStream_successWithCitations() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-citations", + withExtension: "txt" + ) + + let stream = model.generateContentStream("Hi") + var citations: [Citation] = [] + for try await content in stream { + XCTAssertNotNil(content.text) + let candidate = try XCTUnwrap(content.candidates.first) + XCTAssertEqual(candidate.finishReason, .stop) + if let sources = candidate.citationMetadata?.citationSources { + citations.append(contentsOf: sources) + } + } + + XCTAssertEqual(citations.count, 8) + XCTAssertTrue(citations + .contains(where: { $0.startIndex == 574 && $0.endIndex == 705 && !$0.uri.isEmpty })) + XCTAssertTrue(citations + .contains(where: { $0.startIndex == 899 && $0.endIndex == 1026 && !$0.uri.isEmpty })) + } + + func testGenerateContentStream_errorMidStream() async throws { + MockURLProtocol.requestHandler = try httpRequestHandler( + forResource: "streaming-failure-error-mid-stream", + withExtension: "txt" + ) + + var responseCount = 0 + do { + let stream = model.generateContentStream("Hi") + for try await content in stream { + XCTAssertNotNil(content.text) + responseCount += 1 + } + } catch let GenerateContentError.internalError(rpcError as RPCError) { + XCTAssertEqual(rpcError.httpResponseCode, 499) + XCTAssertEqual(rpcError.status, .cancelled) + + // Check the content count is correct. + XCTAssertEqual(responseCount, 2) + return + } + + XCTFail("Expected an internalError with an RPCError.") + } + + func testGenerateContentStream_nonHTTPResponse() async throws { + MockURLProtocol.requestHandler = try nonHTTPRequestHandler() + + let stream = model.generateContentStream("Hi") + do { + for try await content in stream { + XCTFail("Unexpected content in stream: \(content)") + } + } catch let GenerateContentError.internalError(underlying) { + XCTAssertEqual(underlying.localizedDescription, "Response was not an HTTP response.") + return + } + + XCTFail("Expected an internal error.") + } + + func testGenerateContentStream_invalidResponse() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-invalid-json", + withExtension: "txt" + ) + + let stream = model.generateContentStream(testPrompt) + do { + for try await content in stream { + XCTFail("Unexpected content in stream: \(content)") + } + } catch let GenerateContentError.internalError(underlying as DecodingError) { + guard case let .dataCorrupted(context) = underlying else { + XCTFail("Not a data corrupted error: \(underlying)") + return + } + XCTAssert(context.debugDescription.hasPrefix("Failed to decode GenerateContentResponse")) + return + } + + XCTFail("Expected an internal error.") + } + + func testGenerateContentStream_malformedContent() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-failure-malformed-content", + withExtension: "txt" + ) + + let stream = model.generateContentStream(testPrompt) + do { + for try await content in stream { + XCTFail("Unexpected content in stream: \(content)") + } + } catch let GenerateContentError.internalError(underlyingError as InvalidCandidateError) { + guard case let .malformedContent(contentError) = underlyingError else { + XCTFail("Not a malformed content error: \(underlyingError)") + return + } + + XCTAssert(contentError is DecodingError) + return + } + + XCTFail("Expected an internal decoding error.") + } + + func testGenerateContentStream_failure_unsupportedUserLocation() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-unsupported-user-location", + withExtension: "json", + statusCode: 400 + ) + + let stream = model.generateContentStream(testPrompt) + do { + for try await content in stream { + XCTFail("Unexpected content in stream: \(content)") + } + } catch GenerateContentError.unsupportedUserLocation { + return + } + + XCTFail("Expected an unsupported user location error.") + } + + func testGenerateContentStream_requestOptions_customTimeout() async throws { + let expectedTimeout = 150.0 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-basic-reply-short", + withExtension: "txt", + timeout: expectedTimeout + ) + let requestOptions = RequestOptions(timeout: expectedTimeout) + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: requestOptions, + urlSession: urlSession + ) + + var responses = 0 + let stream = model.generateContentStream(testPrompt) + for try await content in stream { + XCTAssertNotNil(content.text) + responses += 1 + } + + XCTAssertEqual(responses, 1) + } + + // MARK: - Count Tokens + + func testCountTokens_succeeds() async throws { + MockURLProtocol.requestHandler = try httpRequestHandler( + forResource: "success-total-tokens", + withExtension: "json" + ) + + let response = try await model.countTokens("Why is the sky blue?") + XCTAssertEqual(response.totalTokens, 6) + } + + func testCountTokens_modelNotFound() async throws { + MockURLProtocol.requestHandler = try httpRequestHandler( + forResource: "failure-model-not-found", withExtension: "json", + statusCode: 404 + ) + + do { + _ = try await model.countTokens("Why is the sky blue?") + XCTFail("Request should not have succeeded.") + } catch let CountTokensError.internalError(rpcError as RPCError) { + XCTAssertEqual(rpcError.httpResponseCode, 404) + XCTAssertEqual(rpcError.status, .notFound) + XCTAssert(rpcError.message.hasPrefix("models/test-model-name is not found")) + return + } + + XCTFail("Expected internal RPCError.") + } + + func testCountTokens_requestOptions_customTimeout() async throws { + let expectedTimeout = 150.0 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "success-total-tokens", + withExtension: "json", + timeout: expectedTimeout + ) + let requestOptions = RequestOptions(timeout: expectedTimeout) + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: requestOptions, + urlSession: urlSession + ) + + let response = try await model.countTokens(testPrompt) + + XCTAssertEqual(response.totalTokens, 6) + } + + // MARK: - Model Resource Name + + func testModelResourceName_noPrefix() async throws { + let modelName = "my-model" + let modelResourceName = "models/\(modelName)" + + model = GenerativeModel(name: modelName, apiKey: "API_KEY") + + XCTAssertEqual(model.modelResourceName, modelResourceName) + } + + func testModelResourceName_modelsPrefix() async throws { + let modelResourceName = "models/my-model" + + model = GenerativeModel(name: modelResourceName, apiKey: "API_KEY") + + XCTAssertEqual(model.modelResourceName, modelResourceName) + } + + func testModelResourceName_tunedModelsPrefix() async throws { + let tunedModelResourceName = "tunedModels/my-model" + + model = GenerativeModel(name: tunedModelResourceName, apiKey: "API_KEY") + + XCTAssertEqual(model.modelResourceName, tunedModelResourceName) + } + + // MARK: - Helpers + + private func nonHTTPRequestHandler() throws -> ((URLRequest) -> ( + URLResponse, + AsyncLineSequence? + )) { + return { request in + // This is *not* an HTTPURLResponse + let response = URLResponse( + url: request.url!, + mimeType: nil, + expectedContentLength: 0, + textEncodingName: nil + ) + return (response, nil) + } + } + + private func httpRequestHandler(forResource name: String, + withExtension ext: String, + statusCode: Int = 200, + timeout: TimeInterval = URLRequest + .defaultTimeoutInterval()) throws -> ((URLRequest) throws -> ( + URLResponse, + AsyncLineSequence? + )) { + let fileURL = try XCTUnwrap(Bundle.module.url(forResource: name, withExtension: ext)) + return { request in + let requestURL = try XCTUnwrap(request.url) + XCTAssertEqual(requestURL.path.occurrenceCount(of: "models/"), 1) + XCTAssertEqual(request.timeoutInterval, timeout) + let response = try XCTUnwrap(HTTPURLResponse( + url: requestURL, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + )) + return (response, fileURL.lines) + } + } +} + +private extension String { + /// Returns the number of occurrences of `substring` in the `String`. + func occurrenceCount(of substring: String) -> Int { + return components(separatedBy: substring).count - 1 + } +} + +private extension URLRequest { + /// Returns the default `timeoutInterval` for a `URLRequest`. + static func defaultTimeoutInterval() -> TimeInterval { + let placeholderURL = URL(string: "https://example.com")! + return URLRequest(url: placeholderURL).timeoutInterval + } +} diff --git a/FirebaseVertexAI/Tests/Unit/GoogleAITests.swift b/FirebaseVertexAI/Tests/Unit/GoogleAITests.swift new file mode 100644 index 00000000000..cbc92527a1c --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GoogleAITests.swift @@ -0,0 +1,175 @@ +// 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 GoogleGenerativeAI +import XCTest +#if canImport(AppKit) + import AppKit // For NSImage extensions. +#elseif canImport(UIKit) + import UIKit // For UIImage extensions. +#endif + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +final class GoogleGenerativeAITests: XCTestCase { + func codeSamples() async throws { + let config = GenerationConfig(temperature: 0.2, + topP: 0.1, + topK: 16, + candidateCount: 4, + maxOutputTokens: 256, + stopSequences: ["..."]) + let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)] + + // Permutations without optional arguments. + let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY") + let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY", safetySettings: filters) + let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY", generationConfig: config) + + // All arguments passed. + let genAI = GenerativeModel(name: "gemini-1.0-pro", + apiKey: "API_KEY", + generationConfig: config, // Optional + safetySettings: filters // Optional + ) + // Full Typed Usage + let pngData = Data() // .... + let contents = [ModelContent(role: "user", + parts: [ + .text("Is it a cat?"), + .png(pngData), + ])] + + do { + let response = try await genAI.generateContent(contents) + print(response.text ?? "Couldn't get text... check status") + } catch { + print("Error generating content: \(error)") + } + + // Content input combinations. + let _ = try await genAI.generateContent("Constant String") + let str = "String Variable" + let _ = try await genAI.generateContent(str) + let _ = try await genAI.generateContent([str]) + let _ = try await genAI.generateContent(str, "abc", "def") + #if canImport(UIKit) + _ = try await genAI.generateContent(UIImage()) + _ = try await genAI.generateContent([UIImage()]) + _ = try await genAI + .generateContent([str, UIImage(), ModelContent.Part.text(str)]) + _ = try await genAI.generateContent(str, UIImage(), "def", UIImage()) + _ = try await genAI.generateContent([str, UIImage(), "def", UIImage()]) + _ = try await genAI.generateContent([ModelContent("def", UIImage()), + ModelContent("def", UIImage())]) + #elseif canImport(AppKit) + _ = try await genAI.generateContent(NSImage()) + _ = try await genAI.generateContent([NSImage()]) + _ = try await genAI.generateContent(str, NSImage(), "def", NSImage()) + _ = try await genAI.generateContent([str, NSImage(), "def", NSImage()]) + #endif + + // ThrowingPartsRepresentable combinations. + let _ = ModelContent(parts: [.text(str)]) + let _ = ModelContent(role: "model", parts: [.text(str)]) + let _ = ModelContent(parts: "Constant String") + let _ = ModelContent(parts: str) + // Note: This requires the `try` for some reason. Casting to explicit [PartsRepresentable] also + // doesn't work. + let _ = try ModelContent(parts: [str]) + // Note: without `as [any ThrowingPartsRepresentable]` this will fail to compile with "Cannot + // convert value of type 'String' to expected element type + // 'Array.ArrayLiteralElement'. Not sure if there's a way we can get it to + // work. + let _ = try ModelContent(parts: [str, ModelContent.Part.data( + mimetype: "foo", + Data() + )] as [any ThrowingPartsRepresentable]) + #if canImport(UIKit) + _ = try ModelContent(role: "user", parts: UIImage()) + _ = try ModelContent(role: "user", parts: [UIImage()]) + // Note: without `as [any ThrowingPartsRepresentable]` this will fail to compile with "Cannot + // convert + // value of type `[Any]` to expected type `[any ThrowingPartsRepresentable]`. Not sure if + // there's a + // way we can get it to work. + _ = try ModelContent(parts: [str, UIImage()] as [any ThrowingPartsRepresentable]) + // Alternatively, you can explicitly declare the type in a variable and pass it in. + let representable2: [any ThrowingPartsRepresentable] = [str, UIImage()] + _ = try ModelContent(parts: representable2) + _ = try ModelContent(parts: [str, UIImage(), + ModelContent.Part.text(str)] as [any ThrowingPartsRepresentable]) + #elseif canImport(AppKit) + _ = try ModelContent(role: "user", parts: NSImage()) + _ = try ModelContent(role: "user", parts: [NSImage()]) + // Note: without `as [any ThrowingPartsRepresentable]` this will fail to compile with "Cannot + // convert + // value of type `[Any]` to expected type `[any ThrowingPartsRepresentable]`. Not sure if + // there's a + // way we can get it to work. + _ = try ModelContent(parts: [str, NSImage()] as [any ThrowingPartsRepresentable]) + // Alternatively, you can explicitly declare the type in a variable and pass it in. + let representable2: [any ThrowingPartsRepresentable] = [str, NSImage()] + _ = try ModelContent(parts: representable2) + _ = + try ModelContent(parts: [str, NSImage(), + ModelContent.Part.text(str)] as [any ThrowingPartsRepresentable]) + #endif + + // countTokens API + let _: CountTokensResponse = try await genAI.countTokens("What color is the Sky?") + #if canImport(UIKit) + let _: CountTokensResponse = try await genAI.countTokens("What color is the Sky?", + UIImage()) + let _: CountTokensResponse = try await genAI.countTokens([ + ModelContent("What color is the Sky?", UIImage()), + ModelContent(UIImage(), "What color is the Sky?", UIImage()), + ]) + #endif + + // Chat + _ = genAI.startChat() + _ = genAI.startChat(history: [ModelContent(parts: "abc")]) + } + + // Result builder alternative + + /* + let pngData = Data() // .... + let contents = [GenAIContent(role: "user", + parts: [ + .text("Is it a cat?"), + .png(pngData) + ])] + + // Turns into... + + let contents = GenAIContent { + Role("user") { + Text("Is this a cat?") + Image(png: pngData) + } + } + + GenAIContent { + ForEach(myInput) { input in + Role(input.role) { + input.contents + } + } + } + + // Thoughts: this looks great from a code demo, but since I assume most content will be + // user generated, the result builder may not be the best API. + */ +} diff --git a/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift b/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift new file mode 100644 index 00000000000..30528cdf35a --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/MockURLProtocol.swift @@ -0,0 +1,62 @@ +// 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 +import XCTest + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) +class MockURLProtocol: URLProtocol { + static var requestHandler: ((URLRequest) throws -> ( + URLResponse, + AsyncLineSequence? + ))? + + override class func canInit(with request: URLRequest) -> Bool { return true } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } + + override func startLoading() { + guard let requestHandler = MockURLProtocol.requestHandler else { + fatalError("`requestHandler` is nil.") + } + guard let client = client else { + fatalError("`client` is nil.") + } + + Task { + let (response, stream) = try requestHandler(self.request) + client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + if let stream = stream { + do { + for try await line in stream { + guard let data = line.data(using: .utf8) else { + fatalError("Failed to convert \"\(line)\" to UTF8 data.") + } + client.urlProtocol(self, didLoad: data) + // Add a newline character since AsyncLineSequence strips them when reading line by + // line; + // without the following, the whole file is delivered as a single line. + client.urlProtocol(self, didLoad: "\n".data(using: .utf8)!) + } + } catch { + client.urlProtocol(self, didFailWithError: error) + XCTFail("Unexpected failure reading lines from stream: \(error.localizedDescription)") + } + } + client.urlProtocolDidFinishLoading(self) + } + } + + override func stopLoading() {} +} diff --git a/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift b/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift new file mode 100644 index 00000000000..da2f35f727f --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift @@ -0,0 +1,138 @@ +// Copyright 2024 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 CoreGraphics +import CoreImage +import GoogleGenerativeAI +import XCTest +#if canImport(UIKit) + import UIKit +#else + import AppKit +#endif + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +final class PartsRepresentableTests: XCTestCase { + func testModelContentFromCGImageIsNotEmpty() throws { + // adapted from https://forums.swift.org/t/creating-a-cgimage-from-color-array/18634/2 + var srgbArray = [UInt32](repeating: 0xFFFF_FFFF, count: 8 * 8) + let image = srgbArray.withUnsafeMutableBytes { ptr -> CGImage in + let ctx = CGContext( + data: ptr.baseAddress, + width: 8, + height: 8, + bitsPerComponent: 8, + bytesPerRow: 4 * 8, + space: CGColorSpace(name: CGColorSpace.sRGB)!, + bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue + + CGImageAlphaInfo.premultipliedFirst.rawValue + )! + return ctx.makeImage()! + } + let modelContent = try image.tryPartsValue() + XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)") + } + + func testModelContentFromCIImageIsNotEmpty() throws { + let image = CIImage(color: CIColor.red) + .cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16))) + let modelContent = try image.tryPartsValue() + XCTAssert(modelContent.count > 0, "Expected non-empty model content for CGImage: \(image)") + } + + func testModelContentFromInvalidCIImageThrows() throws { + let image = CIImage.empty() + do { + let _ = try image.tryPartsValue() + } catch { + guard let imageError = (error as? ImageConversionError) else { + XCTFail("Got unexpected error type: \(error)") + return + } + switch imageError { + case let .couldNotConvertToJPEG(source): + // String(describing:) works around a type error. + XCTAssertEqual(String(describing: source), String(describing: image)) + return + case _: + XCTFail("Expected image conversion error, got \(imageError) instead") + return + } + } + XCTFail("Expected model content from invlaid image to error") + } + + #if canImport(UIKit) + func testModelContentFromInvalidUIImageThrows() throws { + let image = UIImage() + do { + _ = try image.tryPartsValue() + } catch { + guard let imageError = (error as? ImageConversionError) else { + XCTFail("Got unexpected error type: \(error)") + return + } + switch imageError { + case let .couldNotConvertToJPEG(source): + // String(describing:) works around a type error. + XCTAssertEqual(String(describing: source), String(describing: image)) + return + case _: + XCTFail("Expected image conversion error, got \(imageError) instead") + return + } + } + XCTFail("Expected model content from invlaid image to error") + } + + func testModelContentFromUIImageIsNotEmpty() throws { + let coreImage = CIImage(color: CIColor.red) + .cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16))) + let image = UIImage(ciImage: coreImage) + let modelContent = try image.tryPartsValue() + XCTAssert(modelContent.count > 0, "Expected non-empty model content for UIImage: \(image)") + } + #else + func testModelContentFromNSImageIsNotEmpty() throws { + let coreImage = CIImage(color: CIColor.red) + .cropped(to: CGRect(origin: CGPointZero, size: CGSize(width: 16, height: 16))) + let rep = NSCIImageRep(ciImage: coreImage) + let image = NSImage(size: rep.size) + image.addRepresentation(rep) + let modelContent = try image.tryPartsValue() + XCTAssert(modelContent.count > 0, "Expected non-empty model content for NSImage: \(image)") + } + + func testModelContentFromInvalidNSImageThrows() throws { + let image = NSImage() + do { + _ = try image.tryPartsValue() + } catch { + guard let imageError = (error as? ImageConversionError) else { + XCTFail("Got unexpected error type: \(error)") + return + } + switch imageError { + case .invalidUnderlyingImage: + // Pass + return + case _: + XCTFail("Expected image conversion error, got \(imageError) instead") + return + } + } + XCTFail("Expected model content from invlaid image to error") + } + #endif +} From f2760d4f6782af19c751d575aea67b7f35099cad Mon Sep 17 00:00:00 2001 From: Morgan Chen Date: Tue, 19 Mar 2024 09:10:15 -0700 Subject: [PATCH 030/103] Add local App Check package to sample targets (#12576) --- .../project.pbxproj | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj index 20f07a5acca..1b4a69cafb0 100644 --- a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj @@ -56,6 +56,10 @@ 88E10F592B11131900C08E95 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F582B11131900C08E95 /* ChatMessage.swift */; }; 88E10F5B2B11133E00C08E95 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5A2B11133E00C08E95 /* MessageView.swift */; }; 88E10F5D2B11135000C08E95 /* BouncingDots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5C2B11135000C08E95 /* BouncingDots.swift */; }; + 8D589E2B2BA8E87A0049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */; }; + 8D589E2D2BA8EEA30049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */; }; + 8D589E2F2BA8EEBD0049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */; }; + 8D589E312BA8EED80049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */; }; CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */; }; /* End PBXBuildFile section */ @@ -97,6 +101,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8D589E312BA8EED80049BFCA /* FirebaseAppCheck in Frameworks */, 886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */, 869200AB2B86BE3100482873 /* FirebaseVertexAI in Frameworks */, 886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */, @@ -108,6 +113,7 @@ buildActionMask = 2147483647; files = ( 88209C242B0FBE1700F64795 /* MarkdownUI in Frameworks */, + 8D589E2F2BA8EEBD0049BFCA /* FirebaseAppCheck in Frameworks */, 869200B12B86BFF500482873 /* FirebaseVertexAI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -116,6 +122,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8D589E2B2BA8E87A0049BFCA /* FirebaseAppCheck in Frameworks */, 88B8A91E2B0FC55100424728 /* MarkdownUI in Frameworks */, 869200AF2B86BFBC00482873 /* FirebaseVertexAI in Frameworks */, 88B8A9372B0FCBE700424728 /* GenerativeAIUIComponents in Frameworks */, @@ -126,6 +133,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8D589E2D2BA8EEA30049BFCA /* FirebaseAppCheck in Frameworks */, 88D9474D2B14F27E008B5580 /* MarkdownUI in Frameworks */, 869200AD2B86BEA100482873 /* FirebaseVertexAI in Frameworks */, CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */, @@ -335,6 +343,7 @@ 886F95D72B17BA420036F07A /* MarkdownUI */, 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */, 869200AA2B86BE3100482873 /* FirebaseVertexAI */, + 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */, ); productName = GenerativeAISample; productReference = 8848C82F2B0D04BC007B434F /* GenerativeAISample.app */; @@ -356,6 +365,7 @@ packageProductDependencies = ( 88209C232B0FBE1700F64795 /* MarkdownUI */, 869200B02B86BFF500482873 /* FirebaseVertexAI */, + 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */, ); productName = GenerativeAITextSample; productReference = 8848C8442B0D051E007B434F /* GenerativeAITextSample.app */; @@ -378,6 +388,7 @@ 88B8A91D2B0FC55100424728 /* MarkdownUI */, 88B8A9362B0FCBE700424728 /* GenerativeAIUIComponents */, 869200AE2B86BFBC00482873 /* FirebaseVertexAI */, + 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */, ); productName = GenerativeAIMultimodalSample; productReference = 8848C8562B0D056C007B434F /* GenerativeAIMultimodalSample.app */; @@ -400,6 +411,7 @@ 88D9474C2B14F27E008B5580 /* MarkdownUI */, CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */, 869200AC2B86BEA100482873 /* FirebaseVertexAI */, + 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */, ); productName = ChatSample; productReference = 88E10F422B110D5300C08E95 /* ChatSample.app */; @@ -1043,6 +1055,22 @@ package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; + 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; + }; + 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; + }; + 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; + }; + 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; + }; CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */ = { isa = XCSwiftPackageProductDependency; productName = GenerativeAIUIComponents; From 23a1febc0f08c2657eb3a6d659c397068f2fbc3e Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 19 Mar 2024 20:59:05 +0000 Subject: [PATCH 031/103] Use `generative-ai-swift` tests in Vertex AI (#12585) --- .github/workflows/vertexai.yml | 7 +- FirebaseVertexAI/Tests/Unit/ChatTests.swift | 11 ++- .../streaming-success-citations2.txt | 5 -- .../unary-failure-api-key.json | 2 +- ...e-finish-reason-recitation-no-content.json | 46 ---------- ...ary-failure-unsupported-user-location.json | 13 --- .../Tests/Unit/GenerativeModelTests.swift | 90 +++++++++---------- .../Tests/Unit/PartsRepresentableTests.swift | 2 +- ...leAITests.swift => VertexAIAPITests.swift} | 46 +++++++--- Package.swift | 9 ++ .../FirebaseVertexAIUnit.xcscheme | 77 ++++++++++++++++ 11 files changed, 178 insertions(+), 130 deletions(-) delete mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt delete mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json delete mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json rename FirebaseVertexAI/Tests/Unit/{GoogleAITests.swift => VertexAIAPITests.swift} (84%) create mode 100644 scripts/spm_test_schemes/FirebaseVertexAIUnit.xcscheme diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index 63cfd774e43..4f0880de320 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -31,7 +31,6 @@ jobs: - name: Xcode run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Initialize xcodebuild - run: xcodebuild -list - # TODO: Add unit tests and switch from `spmbuildonly` to `spm`. - - name: Build - run: scripts/third_party/travis/retry.sh scripts/build.sh FirebaseVertexAI ${{ matrix.target }} spmbuildonly + run: scripts/setup_spm_tests.sh + - name: Build and run tests + run: scripts/third_party/travis/retry.sh scripts/build.sh FirebaseVertexAIUnit ${{ matrix.target }} spm diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift index 668e3704122..046cce0e8bd 100644 --- a/FirebaseVertexAI/Tests/Unit/ChatTests.swift +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -13,9 +13,10 @@ // limitations under the License. import Foundation -@testable import GoogleGenerativeAI import XCTest +@testable import FirebaseVertexAI + @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) final class ChatTests: XCTestCase { var urlSession: URLSession! @@ -46,7 +47,13 @@ final class ChatTests: XCTestCase { return (response, fileURL.lines) } - let model = GenerativeModel(name: "my-model", apiKey: "API_KEY", urlSession: urlSession) + let model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil, + urlSession: urlSession + ) let chat = Chat(model: model, history: []) let input = "Test input" let stream = chat.sendMessageStream(input) diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt deleted file mode 100644 index 665e993ad75..00000000000 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations2.txt +++ /dev/null @@ -1,5 +0,0 @@ -data: {"candidates": [{"content": {"role": "model","parts": [{"text": "In the context of the science fiction comedy novel \"The Hitchhiker's Guide to the Galaxy\" by Douglas Adams, the answer to the \"Ultimate Question of"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.055720285,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.062674366},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.03904829,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03339982},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12220858,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.0540987},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.05781161,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03149938}]}]} - -data: {"candidates": [{"content": {"role": "model","parts": [{"text": " Life, the Universe, and Everything\" is given as 42. However, in real life, there is no universally accepted meaning of life. The meaning"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.05910154,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.038321976},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.049589027,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.019271139},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12787028,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03986249},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.098946586,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04177388}],"citationMetadata": {"citations": [{"startIndex": 52,"endIndex": 181,"uri": "https://imcsteakhouse.com.au/faqs"}]}}]} - -data: {"candidates": [{"content": {"role": "model","parts": [{"text": " of life is a philosophical question that has been pondered by humans for centuries, and there is no single answer that is widely agreed upon."}]},"finishReason": "STOP","safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.044764314,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.028870905},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.04240383,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.012576347},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.10650458,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.034880884},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.11085559,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03191915}]}],"usageMetadata": {"promptTokenCount": 9,"candidatesTokenCount": 91,"totalTokenCount": 100}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json index ecf6f6b53fa..d3b5677366e 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json @@ -9,7 +9,7 @@ "reason": "API_KEY_INVALID", "domain": "googleapis.com", "metadata": { - "service": "generativelanguage.googleapis.com" + "service": "staging-firebaseml.sandbox.googleapis.com" } }, { diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json deleted file mode 100644 index 0d99cd71d51..00000000000 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-finish-reason-recitation-no-content.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "candidates": [ - { - "finishReason": "RECITATION", - "index": 0, - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "HIGH" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } - ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] - } -} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json deleted file mode 100644 index c4c2ace4e20..00000000000 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-unsupported-user-location.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "error": { - "code": 400, - "message": "User location is not supported for the API use.", - "status": "FAILED_PRECONDITION", - "details": [ - { - "@type": "type.googleapis.com/google.rpc.DebugInfo", - "detail": "[ORIGINAL ERROR] generic::failed_precondition: User location is not supported for the API use. [google.rpc.error_details_ext] { message: \"User location is not supported for the API use.\" }" - } - ] - } -} diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index b835a9ea05f..ac1980d93d4 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -12,9 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -@testable import GoogleGenerativeAI import XCTest +@testable import FirebaseVertexAI + @available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) final class GenerativeModelTests: XCTestCase { let testPrompt = "What sorts of questions can I ask you?" @@ -32,7 +33,13 @@ final class GenerativeModelTests: XCTestCase { let configuration = URLSessionConfiguration.default configuration.protocolClasses = [MockURLProtocol.self] urlSession = try XCTUnwrap(URLSession(configuration: configuration)) - model = GenerativeModel(name: "my-model", apiKey: "API_KEY", urlSession: urlSession) + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil, + urlSession: urlSession + ) } override func tearDown() { @@ -163,6 +170,8 @@ final class GenerativeModelTests: XCTestCase { // Model name is prefixed with "models/". name: "models/test-model", apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil, urlSession: urlSession ) @@ -181,10 +190,13 @@ final class GenerativeModelTests: XCTestCase { do { _ = try await model.generateContent(testPrompt) XCTFail("Should throw GenerateContentError.internalError; no error thrown.") - } catch let GenerateContentError.invalidAPIKey(message) { - XCTAssertEqual(message, "API key not valid. Please pass a valid API key.") + } catch let GenerateContentError.internalError(error as RPCError) { + XCTAssertEqual(error.httpResponseCode, 400) + XCTAssertEqual(error.status, .invalidArgument) + XCTAssertEqual(error.message, "API key not valid. Please pass a valid API key.") + return } catch { - XCTFail("Should throw GenerateContentError.invalidAPIKey; error thrown: \(error)") + XCTFail("Should throw GenerateContentError.internalError(RPCError); error thrown: \(error)") } } @@ -342,24 +354,6 @@ final class GenerativeModelTests: XCTestCase { } } - func testGenerateContent_failure_unsupportedUserLocation() async throws { - MockURLProtocol - .requestHandler = try httpRequestHandler( - forResource: "unary-failure-unsupported-user-location", - withExtension: "json", - statusCode: 400 - ) - - do { - _ = try await model.generateContent(testPrompt) - XCTFail("Should throw GenerateContentError.unsupportedUserLocation; no error thrown.") - } catch GenerateContentError.unsupportedUserLocation { - return - } - - XCTFail("Expected an unsupported user location error.") - } - func testGenerateContent_failure_nonHTTPResponse() async throws { MockURLProtocol.requestHandler = try nonHTTPRequestHandler() @@ -468,6 +462,7 @@ final class GenerativeModelTests: XCTestCase { name: "my-model", apiKey: "API_KEY", requestOptions: requestOptions, + appCheck: nil, urlSession: urlSession ) @@ -490,8 +485,10 @@ final class GenerativeModelTests: XCTestCase { for try await _ in stream { XCTFail("No content is there, this shouldn't happen.") } - } catch GenerateContentError.invalidAPIKey { - // invalidAPIKey error is as expected, nothing else to check. + } catch let GenerateContentError.internalError(error as RPCError) { + XCTAssertEqual(error.httpResponseCode, 400) + XCTAssertEqual(error.status, .invalidArgument) + XCTAssertEqual(error.message, "API key not valid. Please pass a valid API key.") return } @@ -747,26 +744,6 @@ final class GenerativeModelTests: XCTestCase { XCTFail("Expected an internal decoding error.") } - func testGenerateContentStream_failure_unsupportedUserLocation() async throws { - MockURLProtocol - .requestHandler = try httpRequestHandler( - forResource: "unary-failure-unsupported-user-location", - withExtension: "json", - statusCode: 400 - ) - - let stream = model.generateContentStream(testPrompt) - do { - for try await content in stream { - XCTFail("Unexpected content in stream: \(content)") - } - } catch GenerateContentError.unsupportedUserLocation { - return - } - - XCTFail("Expected an unsupported user location error.") - } - func testGenerateContentStream_requestOptions_customTimeout() async throws { let expectedTimeout = 150.0 MockURLProtocol @@ -780,6 +757,7 @@ final class GenerativeModelTests: XCTestCase { name: "my-model", apiKey: "API_KEY", requestOptions: requestOptions, + appCheck: nil, urlSession: urlSession ) @@ -837,6 +815,7 @@ final class GenerativeModelTests: XCTestCase { name: "my-model", apiKey: "API_KEY", requestOptions: requestOptions, + appCheck: nil, urlSession: urlSession ) @@ -851,7 +830,12 @@ final class GenerativeModelTests: XCTestCase { let modelName = "my-model" let modelResourceName = "models/\(modelName)" - model = GenerativeModel(name: modelName, apiKey: "API_KEY") + model = GenerativeModel( + name: modelName, + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil + ) XCTAssertEqual(model.modelResourceName, modelResourceName) } @@ -859,7 +843,12 @@ final class GenerativeModelTests: XCTestCase { func testModelResourceName_modelsPrefix() async throws { let modelResourceName = "models/my-model" - model = GenerativeModel(name: modelResourceName, apiKey: "API_KEY") + model = GenerativeModel( + name: modelResourceName, + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil + ) XCTAssertEqual(model.modelResourceName, modelResourceName) } @@ -867,7 +856,12 @@ final class GenerativeModelTests: XCTestCase { func testModelResourceName_tunedModelsPrefix() async throws { let tunedModelResourceName = "tunedModels/my-model" - model = GenerativeModel(name: tunedModelResourceName, apiKey: "API_KEY") + model = GenerativeModel( + name: tunedModelResourceName, + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: nil + ) XCTAssertEqual(model.modelResourceName, tunedModelResourceName) } diff --git a/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift b/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift index da2f35f727f..4243c2441be 100644 --- a/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift +++ b/FirebaseVertexAI/Tests/Unit/PartsRepresentableTests.swift @@ -14,7 +14,7 @@ import CoreGraphics import CoreImage -import GoogleGenerativeAI +import FirebaseVertexAI import XCTest #if canImport(UIKit) import UIKit diff --git a/FirebaseVertexAI/Tests/Unit/GoogleAITests.swift b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift similarity index 84% rename from FirebaseVertexAI/Tests/Unit/GoogleAITests.swift rename to FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift index cbc92527a1c..4676b2e34d9 100644 --- a/FirebaseVertexAI/Tests/Unit/GoogleAITests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -import GoogleGenerativeAI +import FirebaseCore +import FirebaseVertexAI import XCTest #if canImport(AppKit) import AppKit // For NSImage extensions. @@ -21,8 +22,9 @@ import XCTest #endif @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -final class GoogleGenerativeAITests: XCTestCase { +final class VertexAIAPITests: XCTestCase { func codeSamples() async throws { + let app = FirebaseApp.app() let config = GenerationConfig(temperature: 0.2, topP: 0.1, topK: 16, @@ -32,16 +34,40 @@ final class GoogleGenerativeAITests: XCTestCase { let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)] // Permutations without optional arguments. - let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY") - let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY", safetySettings: filters) - let _ = GenerativeModel(name: "gemini-1.0-pro", apiKey: "API_KEY", generationConfig: config) - // All arguments passed. - let genAI = GenerativeModel(name: "gemini-1.0-pro", - apiKey: "API_KEY", - generationConfig: config, // Optional - safetySettings: filters // Optional + // TODO: Change `genAI` to `_` when safetySettings and generationConfig are added to public API. + let genAI = VertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") + let _ = VertexAI.generativeModel( + app: app!, + modelName: "gemini-1.0-pro", + location: "us-central1" ) + + // TODO: Add safetySettings to public API. + // TODO: Add permutation with `app` specified. + // let _ = VertexAI.generativeModel( + // modelName: "gemini-1.0-pro", + // location: "us-central1", + // safetySettings: filters + // ) + // TODO: Add generationConfig to public API. + // TODO: Add permutation with `app` specified. + // let _ = VertexAI.generativeModel( + // modelName: "gemini-1.0-pro", + // location: "us-central1", + // generationConfig: config + // ) + + // All arguments passed. + // TODO: Add safetySettings and generationConfig to public API. + // TODO: Add permutation with `app` specified. + // let genAI = VertexAI.generativeModel( + // modelName: "gemini-1.0-pro", + // location: "us-central1", + // generationConfig: config, // Optional + // safetySettings: filters // Optional + // ) + // Full Typed Usage let pngData = Data() // .... let contents = [ModelContent(role: "user", diff --git a/Package.swift b/Package.swift index f88daf4f620..711ab36cd42 100644 --- a/Package.swift +++ b/Package.swift @@ -1373,6 +1373,15 @@ let package = Package( ], path: "FirebaseVertexAI/Sources" ), + .testTarget( + name: "FirebaseVertexAIUnit", + dependencies: ["FirebaseVertexAI"], + path: "FirebaseVertexAI/Tests/Unit", + resources: [ + .process("CountTokenResponses"), + .process("GenerateContentResponses"), + ] + ), ] + firestoreTargets(), cLanguageStandard: .c99, cxxLanguageStandard: CXXLanguageStandard.gnucxx14 diff --git a/scripts/spm_test_schemes/FirebaseVertexAIUnit.xcscheme b/scripts/spm_test_schemes/FirebaseVertexAIUnit.xcscheme new file mode 100644 index 00000000000..cb4b5adae36 --- /dev/null +++ b/scripts/spm_test_schemes/FirebaseVertexAIUnit.xcscheme @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 4f44ef283bc4530970a68b7d3018014976e8a130 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 20 Mar 2024 14:11:28 +0000 Subject: [PATCH 032/103] Add App Check tests in `GenerativeModelTests` (#12590) --- .../Tests/Unit/GenerativeModelTests.swift | 131 +++++++++++++++++- 1 file changed, 129 insertions(+), 2 deletions(-) diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index ac1980d93d4..13c625036cc 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import FirebaseAppCheckInterop import XCTest @testable import FirebaseVertexAI @@ -178,6 +179,43 @@ final class GenerativeModelTests: XCTestCase { _ = try await model.generateContent(testPrompt) } + func testGenerateContent_appCheck_validToken() async throws { + let appCheckToken = "test-valid-token" + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: AppCheckInteropFake(token: appCheckToken), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + appCheckToken: appCheckToken + ) + + _ = try await model.generateContent(testPrompt) + } + + func testGenerateContent_appCheck_tokenRefreshError() async throws { + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-basic-reply-short", + withExtension: "json", + appCheckToken: AppCheckInteropFake.placeholderTokenValue + ) + + _ = try await model.generateContent(testPrompt) + } + func testGenerateContent_failure_invalidAPIKey() async throws { let expectedStatusCode = 400 MockURLProtocol @@ -654,6 +692,45 @@ final class GenerativeModelTests: XCTestCase { .contains(where: { $0.startIndex == 899 && $0.endIndex == 1026 && !$0.uri.isEmpty })) } + func testGenerateContentStream_appCheck_validToken() async throws { + let appCheckToken = "test-valid-token" + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: AppCheckInteropFake(token: appCheckToken), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-basic-reply-short", + withExtension: "txt", + appCheckToken: appCheckToken + ) + + let stream = model.generateContentStream(testPrompt) + for try await _ in stream {} + } + + func testGenerateContentStream_appCheck_tokenRefreshError() async throws { + model = GenerativeModel( + name: "my-model", + apiKey: "API_KEY", + requestOptions: RequestOptions(), + appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), + urlSession: urlSession + ) + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "streaming-success-basic-reply-short", + withExtension: "txt", + appCheckToken: AppCheckInteropFake.placeholderTokenValue + ) + + let stream = model.generateContentStream(testPrompt) + for try await _ in stream {} + } + func testGenerateContentStream_errorMidStream() async throws { MockURLProtocol.requestHandler = try httpRequestHandler( forResource: "streaming-failure-error-mid-stream", @@ -887,8 +964,8 @@ final class GenerativeModelTests: XCTestCase { private func httpRequestHandler(forResource name: String, withExtension ext: String, statusCode: Int = 200, - timeout: TimeInterval = URLRequest - .defaultTimeoutInterval()) throws -> ((URLRequest) throws -> ( + timeout: TimeInterval = URLRequest.defaultTimeoutInterval(), + appCheckToken: String? = nil) throws -> ((URLRequest) throws -> ( URLResponse, AsyncLineSequence? )) { @@ -897,6 +974,7 @@ final class GenerativeModelTests: XCTestCase { let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(requestURL.path.occurrenceCount(of: "models/"), 1) XCTAssertEqual(request.timeoutInterval, timeout) + XCTAssertEqual(request.value(forHTTPHeaderField: "X-Firebase-AppCheck"), appCheckToken) let response = try XCTUnwrap(HTTPURLResponse( url: requestURL, statusCode: statusCode, @@ -922,3 +1000,52 @@ private extension URLRequest { return URLRequest(url: placeholderURL).timeoutInterval } } + +class AppCheckInteropFake: NSObject, AppCheckInterop { + /// The placeholder token value returned when an error occurs + static let placeholderTokenValue = "placeholder-token" + + var token: String + var error: Error? + + private init(token: String, error: Error?) { + self.token = token + self.error = error + } + + convenience init(token: String) { + self.init(token: token, error: nil) + } + + convenience init(error: Error) { + self.init(token: AppCheckInteropFake.placeholderTokenValue, error: error) + } + + func getToken(forcingRefresh: Bool) async -> any FIRAppCheckTokenResultInterop { + return AppCheckTokenResultInteropFake(token: token, error: error) + } + + func tokenDidChangeNotificationName() -> String { + fatalError("\(#function) not implemented.") + } + + func notificationTokenKey() -> String { + fatalError("\(#function) not implemented.") + } + + func notificationAppNameKey() -> String { + fatalError("\(#function) not implemented.") + } + + private class AppCheckTokenResultInteropFake: NSObject, FIRAppCheckTokenResultInterop { + var token: String + var error: Error? + + init(token: String, error: Error?) { + self.token = token + self.error = error + } + } +} + +struct AppCheckErrorFake: Error {} From af52205fbf2ac90b50ad5795dc95bacb029dae7e Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 21 Mar 2024 20:56:59 +0000 Subject: [PATCH 033/103] Make `generativeModel` an instance method of `VertexAI` (#12599) --- .../ViewModels/ConversationViewModel.swift | 5 +- .../ViewModels/PhotoReasoningViewModel.swift | 5 +- .../ViewModels/SummarizeViewModel.swift | 5 +- FirebaseVertexAI/Sources/VertexAI.swift | 81 +++++++++++-------- .../Sources/VertexAIComponent.swift | 10 +-- .../Tests/Unit/VertexAIAPITests.swift | 49 +++++------ 6 files changed, 85 insertions(+), 70 deletions(-) diff --git a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift index 465789be387..05cbe11250f 100644 --- a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift +++ b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift @@ -36,7 +36,10 @@ class ConversationViewModel: ObservableObject { private var chatTask: Task? init() { - model = VertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") + model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.0-pro", + location: "us-central1" + ) chat = model.startChat() } diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift index dc41e00444a..2f2ed88d4a1 100644 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift @@ -44,7 +44,10 @@ class PhotoReasoningViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.generativeModel(modelName: "gemini-1.0-pro-vision", location: "us-central1") + model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.0-pro-vision", + location: "us-central1" + ) } func reason() async { diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift index fb5b349ac82..0e3073d6da2 100644 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift @@ -32,7 +32,10 @@ class SummarizeViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") + model = VertexAI.vertexAI().generativeModel( + modelName: "gemini-1.0-pro", + location: "us-central1" + ) } func summarize(inputText: String) async { diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index 0f79e1be99c..64e76d3b4f5 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -20,70 +20,81 @@ import Foundation @_implementationOnly import FirebaseCoreExtension @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) -@objc(FIRVertexAI) -open class VertexAI: NSObject { +public class VertexAI: NSObject { // MARK: - Public APIs - /// Returns an instance of `GoogleGenerativeAI.GenerativeModel` that uses the Vertex AI API. + /// The default `VertexAI` instance. /// - /// This instance is configured with the default `FirebaseApp`. - /// - /// TODO: Add RequestOptions to public API. - public static func generativeModel(modelName: String, location: String) -> GenerativeModel { + /// - Returns: An instance of `VertexAI`, configured with the default `FirebaseApp`. + public static func vertexAI() -> VertexAI { guard let app = FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } - return generativeModel(app: app, modelName: modelName, location: location) + + return vertexAI(app: app) } - /// Returns an instance of `GoogleGenerativeAI.GenerativeModel` that uses the Vertex AI API. + /// Creates an instance of `VertexAI` configured with a custom `FirebaseApp`. /// - /// TODO: Add RequestOptions to public API. - public static func generativeModel(app: FirebaseApp, modelName: String, - location: String) -> GenerativeModel { + /// - Parameter app: The custom `FirebaseApp` used for initialization. + /// - Returns: A `VertexAI` instance, configured with the custom `FirebaseApp`. + public static func vertexAI(app: FirebaseApp) -> VertexAI { guard let provider = ComponentType.instance(for: VertexAIProvider.self, in: app.container) else { fatalError("No \(VertexAIProvider.self) instance found for Firebase app: \(app.name)") } - let modelResourceName = modelResourceName(app: app, modelName: modelName, location: location) - let vertexAI = provider.vertexAI(location: location, modelResourceName: modelResourceName) - return vertexAI.model + return provider.vertexAI() } - // MARK: - Private - - /// The `FirebaseApp` associated with this `VertexAI` instance. - private let app: FirebaseApp - - private let appCheck: AppCheckInterop? - - private let location: String - - private let modelResouceName: String + /// Initializes a generative model with the given parameters. + /// + /// - Parameters: + /// - modelName: The name of the model to use, e.g., `"gemini-1.0-pro"`; see + /// [Gemini + /// models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) + /// for a list of supported model names. + /// - location: The location identifier, e.g., `us-central1`; see + /// [Vertex AI + /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) + /// for a list of supported locations. + /// - generationConfig: The content generation parameters your model should use. + /// - safetySettings: A value describing what types of harmful content your model should allow. + /// - requestOptions: Configuration parameters for sending requests to the backend. + public func generativeModel(modelName: String, location: String, + generationConfig: GenerationConfig? = nil, + safetySettings: [SafetySetting]? = nil, + requestOptions: RequestOptions = RequestOptions()) + -> GenerativeModel { + let modelResourceName = modelResourceName(modelName: modelName, location: location) - lazy var model: GenerativeModel = { guard let apiKey = app.options.apiKey else { fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.") } + return GenerativeModel( - name: modelResouceName, + name: modelResourceName, apiKey: apiKey, - // TODO: Add RequestOptions to public API. - requestOptions: RequestOptions(), + generationConfig: generationConfig, + safetySettings: safetySettings, + requestOptions: requestOptions, appCheck: appCheck ) - }() + } + + // MARK: - Private + + /// The `FirebaseApp` associated with this `VertexAI` instance. + private let app: FirebaseApp + + private let appCheck: AppCheckInterop? - init(app: FirebaseApp, location: String, modelResourceName: String) { + init(app: FirebaseApp) { self.app = app appCheck = ComponentType.instance(for: AppCheckInterop.self, in: app.container) - self.location = location - modelResouceName = modelResourceName } - private static func modelResourceName(app: FirebaseApp, modelName: String, - location: String) -> String { + private func modelResourceName(modelName: String, location: String) -> String { if modelName.contains("/") { return modelName } diff --git a/FirebaseVertexAI/Sources/VertexAIComponent.swift b/FirebaseVertexAI/Sources/VertexAIComponent.swift index a8d6c177c74..1378f812626 100644 --- a/FirebaseVertexAI/Sources/VertexAIComponent.swift +++ b/FirebaseVertexAI/Sources/VertexAIComponent.swift @@ -22,7 +22,7 @@ import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @objc(FIRVertexAIProvider) protocol VertexAIProvider { - @objc func vertexAI(location: String, modelResourceName: String) -> VertexAI + @objc func vertexAI() -> VertexAI } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -64,17 +64,17 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { // MARK: - VertexAIProvider conformance - func vertexAI(location: String, modelResourceName: String) -> VertexAI { + func vertexAI() -> VertexAI { os_unfair_lock_lock(&instancesLock) // Unlock before the function returns. defer { os_unfair_lock_unlock(&instancesLock) } - if let instance = instances[modelResourceName] { + if let instance = instances[app.name] { return instance } - let newInstance = VertexAI(app: app, location: location, modelResourceName: modelResourceName) - instances[modelResourceName] = newInstance + let newInstance = VertexAI(app: app) + instances[app.name] = newInstance return newInstance } } diff --git a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift index 4676b2e34d9..a1fd27ab4c5 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift @@ -33,40 +33,35 @@ final class VertexAIAPITests: XCTestCase { stopSequences: ["..."]) let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)] + // Instantiate Vertex AI SDK - Default App + let vertexAI = VertexAI.vertexAI() + + // Instantiate Vertex AI SDK - Custom App + let _ = VertexAI.vertexAI(app: app!) + // Permutations without optional arguments. - // TODO: Change `genAI` to `_` when safetySettings and generationConfig are added to public API. - let genAI = VertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") - let _ = VertexAI.generativeModel( - app: app!, + let _ = vertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") + + let _ = vertexAI.generativeModel( modelName: "gemini-1.0-pro", - location: "us-central1" + location: "us-central1", + safetySettings: filters ) - // TODO: Add safetySettings to public API. - // TODO: Add permutation with `app` specified. - // let _ = VertexAI.generativeModel( - // modelName: "gemini-1.0-pro", - // location: "us-central1", - // safetySettings: filters - // ) - // TODO: Add generationConfig to public API. - // TODO: Add permutation with `app` specified. - // let _ = VertexAI.generativeModel( - // modelName: "gemini-1.0-pro", - // location: "us-central1", - // generationConfig: config - // ) + let _ = vertexAI.generativeModel( + modelName: "gemini-1.0-pro", + location: "us-central1", + generationConfig: config + ) // All arguments passed. - // TODO: Add safetySettings and generationConfig to public API. - // TODO: Add permutation with `app` specified. - // let genAI = VertexAI.generativeModel( - // modelName: "gemini-1.0-pro", - // location: "us-central1", - // generationConfig: config, // Optional - // safetySettings: filters // Optional - // ) + let genAI = vertexAI.generativeModel( + modelName: "gemini-1.0-pro", + location: "us-central1", + generationConfig: config, // Optional + safetySettings: filters // Optional + ) // Full Typed Usage let pngData = Data() // .... From e70550d8646826034bb666969e0e26245e5eaf88 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Fri, 22 Mar 2024 21:06:39 +0000 Subject: [PATCH 034/103] Update test JSON payloads with Vertex AI output (#12615) --- FirebaseVertexAI/Tests/Unit/ChatTests.swift | 2 +- .../streaming-success-basic-reply-long.txt | 20 +++---- .../streaming-success-basic-reply-short.txt | 3 +- .../unary-success-basic-reply-long.json | 56 +++++++++---------- .../unary-success-basic-reply-short.json | 54 +++++++++--------- .../Tests/Unit/GenerativeModelTests.swift | 26 +++++---- 6 files changed, 75 insertions(+), 86 deletions(-) diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift index 046cce0e8bd..5a7868fbe7a 100644 --- a/FirebaseVertexAI/Tests/Unit/ChatTests.swift +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -66,7 +66,7 @@ final class ChatTests: XCTestCase { XCTAssertEqual(chat.history.count, 2) XCTAssertEqual(chat.history[0].parts[0].text, input) - let finalText = "1 2 3 4 5 6 7 8 9 10" + let finalText = "1 2 3 4 5 6 7 8" let assembledExpectation = ModelContent(role: "model", parts: finalText) XCTAssertEqual(chat.history[0].parts[0].text, input) XCTAssertEqual(chat.history[1], assembledExpectation) diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt index bca95140490..218cae0b985 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-long.txt @@ -1,19 +1,15 @@ -data: {"candidates": [{"content": {"parts": [{"text": "1 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "1 "}]}}]} -data: {"candidates": [{"content": {"parts": [{"text": "2 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "2 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.0394904,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04468087},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.034553625,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03890198},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.09401018,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.025809621},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.036562506,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.047691282}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "3 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "3 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.03507868,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.045183755},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.027742893,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.043528143},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.08803312,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.026105914},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.06681233,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.053899158}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "4 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "4 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.037750278,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05089372},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.040087357,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05888469},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.071202725,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.034100424},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.07613248,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.051749535}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "5 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "5 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.04672496,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.059210256},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.04977345,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05623635},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.083890386,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03825006},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.08359067,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05975658}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "6 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "6 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.07779744,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.06052939},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.041930523,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.056756895},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12787028,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05350215},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.09203286,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.048676573}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "7 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "7 "}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.071202725,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.05291181},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.031439852,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04509957},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.11417085,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04922211},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.09451043,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.052716404}]}]} -data: {"candidates": [{"content": {"parts": [{"text": "8 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "9 "}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} - -data: {"candidates": [{"content": {"parts": [{"text": "10"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "8"}]},"finishReason": "STOP","safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.06221698,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.045777276},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.03085051,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04560694},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.0992954,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.040769264},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.100701615,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.061424047}]}],"usageMetadata": {"promptTokenCount": 6,"candidatesTokenCount": 326,"totalTokenCount": 332}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt index a7f5476954e..78f569ddecf 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-basic-reply-short.txt @@ -1,2 +1 @@ -data: {"candidates": [{"content": {"parts": [{"text": "Cheyenne"}]},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} - +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "Mountain View, California"}]},"finishReason": "STOP","safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.02854415,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.052424565},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.24926445,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.0996453},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.087096825,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.043123372},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.14402841,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.086169556}]}],"usageMetadata": {"promptTokenCount": 6,"candidatesTokenCount": 4,"totalTokenCount": 10}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json index 59b84de92fe..325d172b894 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-long.json @@ -2,53 +2,49 @@ "candidates": [ { "content": { + "role": "model", "parts": [ { - "text": "You can ask me a wide range of questions on various topics. Here are some examples:\n\n1. **General Knowledge:**\n - What is the capital of France?\n - Who painted the Mona Lisa?\n - What is the largest ocean in the world?\n\n2. **Science and Technology:**\n - How does a computer work?\n - What is the difference between a virus and a bacteria?\n - What are the latest advancements in artificial intelligence?\n\n3. **History and Culture:**\n - Who was the first president of the United States?\n - What is the significance of the Great Wall of China?\n - What are some of the most famous works of Shakespeare?\n\n4. **Current Events:**\n - What is the latest news about the COVID-19 pandemic?\n - Who is the current president of Ukraine?\n - What are the major issues being discussed in the upcoming election?\n\n5. **Personal Questions:**\n - What are your hobbies?\n - What is your favorite book or movie?\n - What are your thoughts on the future of technology?\n\n6. **Fun and Games:**\n - Can you tell me a joke?\n - What is the answer to this riddle: \"I have keys but no locks. I have space but no room. You can enter, but can't go outside.\" (Answer: a keyboard)\n - Let's play a game of 20 questions.\n\n7. **Hypothetical Questions:**\n - What would you do if you won the lottery?\n - What would happen if time travel were possible?\n - What is the meaning of life?\n\n8. **Philosophical Questions:**\n - What is the nature of reality?\n - Does free will exist?\n - What is the difference between right and wrong?\n\n9. **Creative Questions:**\n - Write a poem about a sunset.\n - Design a logo for a new company.\n - Compose a song about your favorite season.\n\n10. **Technical Questions:**\n - How can I improve the performance of my computer?\n - What is the best way to troubleshoot a network issue?\n - How do I create a website using HTML and CSS?\n\nRemember, I am still under development and may not be able to answer all questions perfectly. However, I will do my best to provide you with accurate and informative responses." + "text": "You can ask me a wide range of questions, including:\n\n* **General knowledge questions:** What is the capital of France? Who painted the Mona Lisa?\n* **Science questions:** What is the chemical formula for water? How does photosynthesis work?\n* **History questions:** When did World War II start? Who was the first president of the United States?\n* **Math questions:** What is the square root of 16? How do you solve for x in the equation x + 5 = 10?\n* **Current events questions:** What is happening in Ukraine? Who is the current president of the United States?\n* **Personal questions:** What are your hobbies? What are your favorite books?\n* **Hypothetical questions:** What would happen if all the ice caps melted? What if humans could live on Mars?\n* **Questions about me:** What is your name? How old are you? What are your capabilities?\n\n**Tips for asking questions:**\n\n* Be specific and clear in your questions.\n* Use correct grammar and spelling.\n* Try to ask open-ended questions that allow for multiple answers.\n* Be patient and wait for my response. I am still under development, so I may take a few seconds to process your question.\n\nPlease note that I am still under development and may not be able to answer all questions accurately. However, I will do my best to provide you with the most relevant information I have available." } - ], - "role": "model" + ] }, "finishReason": "STOP", - "index": 0, "safetyRatings": [ { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.047869004, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.050705366 }, { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.052134257, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.036288295 }, { "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" + "probability": "NEGLIGIBLE", + "probabilityScore": 0.08464396, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.033907957 }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.06290424, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.050611436 } ] } ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 303, + "totalTokenCount": 309 } } diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json index 40a9a6da58e..3e3ddb80fd6 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-basic-reply-short.json @@ -2,53 +2,49 @@ "candidates": [ { "content": { + "role": "model", "parts": [ { "text": "Mountain View, California, United States" } - ], - "role": "model" + ] }, "finishReason": "STOP", - "index": 0, "safetyRatings": [ { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.029035643, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.05613278 }, { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.2641685, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.082253955 }, { "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" + "probability": "NEGLIGIBLE", + "probabilityScore": 0.087252244, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.04509957 }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.1431877, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.11027937 } ] } ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 7, + "totalTokenCount": 13 } } diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 13c625036cc..c249613f9e9 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -25,7 +25,7 @@ final class GenerativeModelTests: XCTestCase { .init(category: .hateSpeech, probability: .negligible), .init(category: .harassment, probability: .negligible), .init(category: .dangerousContent, probability: .negligible), - ] + ].sorted() var urlSession: URLSession! var model: GenerativeModel! @@ -62,15 +62,12 @@ final class GenerativeModelTests: XCTestCase { let candidate = try XCTUnwrap(response.candidates.first) let finishReason = try XCTUnwrap(candidate.finishReason) XCTAssertEqual(finishReason, .stop) - XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.safetyRatings.sorted(), safetyRatingsNegligible) XCTAssertEqual(candidate.content.parts.count, 1) let part = try XCTUnwrap(candidate.content.parts.first) let partText = try XCTUnwrap(part.text) XCTAssertTrue(partText.hasPrefix("You can ask me a wide range of questions")) XCTAssertEqual(response.text, partText) - let promptFeedback = try XCTUnwrap(response.promptFeedback) - XCTAssertNil(promptFeedback.blockReason) - XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) } func testGenerateContent_success_basicReplyShort() async throws { @@ -86,14 +83,11 @@ final class GenerativeModelTests: XCTestCase { let candidate = try XCTUnwrap(response.candidates.first) let finishReason = try XCTUnwrap(candidate.finishReason) XCTAssertEqual(finishReason, .stop) - XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.safetyRatings.sorted(), safetyRatingsNegligible) XCTAssertEqual(candidate.content.parts.count, 1) let part = try XCTUnwrap(candidate.content.parts.first) XCTAssertEqual(part.text, "Mountain View, California, United States") XCTAssertEqual(response.text, part.text) - let promptFeedback = try XCTUnwrap(response.promptFeedback) - XCTAssertNil(promptFeedback.blockReason) - XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) } func testGenerateContent_success_citations() async throws { @@ -131,7 +125,7 @@ final class GenerativeModelTests: XCTestCase { let candidate = try XCTUnwrap(response.candidates.first) let finishReason = try XCTUnwrap(candidate.finishReason) XCTAssertEqual(finishReason, .stop) - XCTAssertEqual(candidate.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(candidate.safetyRatings.sorted(), safetyRatingsNegligible) XCTAssertEqual(candidate.content.parts.count, 1) let part = try XCTUnwrap(candidate.content.parts.first) let partText = try XCTUnwrap(part.text) @@ -139,7 +133,7 @@ final class GenerativeModelTests: XCTestCase { XCTAssertEqual(response.text, part.text) let promptFeedback = try XCTUnwrap(response.promptFeedback) XCTAssertNil(promptFeedback.blockReason) - XCTAssertEqual(promptFeedback.safetyRatings, safetyRatingsNegligible) + XCTAssertEqual(promptFeedback.safetyRatings.sorted(), safetyRatingsNegligible) } func testGenerateContent_success_unknownEnum_safetyRatings() async throws { @@ -627,7 +621,7 @@ final class GenerativeModelTests: XCTestCase { responses += 1 } - XCTAssertEqual(responses, 10) + XCTAssertEqual(responses, 8) } func testGenerateContentStream_successBasicReplyShort() async throws { @@ -1049,3 +1043,11 @@ class AppCheckInteropFake: NSObject, AppCheckInterop { } struct AppCheckErrorFake: Error {} + +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) +extension SafetyRating: Comparable { + public static func < (lhs: FirebaseVertexAI.SafetyRating, + rhs: FirebaseVertexAI.SafetyRating) -> Bool { + return lhs.category.rawValue < rhs.category.rawValue + } +} From 82d73353bd080998e21409b5df30a4a5ebb8393a Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 25 Mar 2024 08:21:58 -0700 Subject: [PATCH 035/103] Build Sample in CI (#12616) --- .github/workflows/vertexai.yml | 42 ++++++++++++++++--- .../Tests/Unit/GenerativeModelTests.swift | 1 + scripts/build.sh | 8 ++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/.github/workflows/vertexai.yml b/.github/workflows/vertexai.yml index 4f0880de320..ebf58529f54 100644 --- a/.github/workflows/vertexai.yml +++ b/.github/workflows/vertexai.yml @@ -3,9 +3,9 @@ name: vertexai on: pull_request: paths: - - 'FirebaseVertexAI**' - - '.github/workflows/vertexai.yml' - - 'Gemfile*' + - 'FirebaseVertexAI**' + - '.github/workflows/vertexai.yml' + - 'Gemfile*' schedule: # Run every day at 11pm (PST) - cron uses UTC times - cron: '0 7 * * *' @@ -32,5 +32,37 @@ jobs: run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer - name: Initialize xcodebuild run: scripts/setup_spm_tests.sh - - name: Build and run tests - run: scripts/third_party/travis/retry.sh scripts/build.sh FirebaseVertexAIUnit ${{ matrix.target }} spm + - uses: nick-fields/retry@v3 + with: + timeout_minutes: 120 + max_attempts: 3 + retry_on: error + retry_wait_seconds: 120 + command: scripts/build.sh FirebaseVertexAIUnit ${{ matrix.target }} spm + + sample: + strategy: + matrix: + # Test build with debug and release configs (whether or not DEBUG is set and optimization level) + build: [build] + include: + - os: macos-13 + xcode: Xcode_15.0.1 + - os: macos-14 + xcode: Xcode_15.2 + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: Xcode + run: sudo xcode-select -s /Applications/${{ matrix.xcode }}.app/Contents/Developer + - name: Initialize xcodebuild + run: xcodebuild -list + - 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/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index c249613f9e9..d2e4f95c0af 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -995,6 +995,7 @@ private extension URLRequest { } } +@available(iOS 15.0, macOS 12.0, macCatalyst 15.0, *) class AppCheckInteropFake: NSObject, AppCheckInterop { /// The placeholder token value returned when an error occurs static let placeholderTokenValue = "placeholder-token" diff --git a/scripts/build.sh b/scripts/build.sh index f44d3d28a21..fcaba2a263d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -490,6 +490,14 @@ case "$product-$platform-$method" in build ;; + VertexSample-*-*) + RunXcodebuild \ + -project 'FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj' \ + -scheme "GenerativeAISample" \ + "${xcb_flags[@]}" \ + build + ;; + Sessions-*-integration) # Perform "pod install" to install the relevant dependencies # ./FirebaseSessions/generate_testapp.sh From fdc2985678e3163881a3e5b8bad9ef04145ac6aa Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Mon, 25 Mar 2024 20:47:37 +0000 Subject: [PATCH 036/103] Fix `CitationMetadata` parsing in Vertex AI (#12626) --- .../Sources/GenerateContentResponse.swift | 6 +- .../streaming-success-citations.txt | 14 ++--- .../unary-success-citations.json | 63 +++++++++---------- .../Tests/Unit/GenerativeModelTests.swift | 25 +++++--- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerateContentResponse.swift b/FirebaseVertexAI/Sources/GenerateContentResponse.swift index 03153da9ae7..4e02abb7f2c 100644 --- a/FirebaseVertexAI/Sources/GenerateContentResponse.swift +++ b/FirebaseVertexAI/Sources/GenerateContentResponse.swift @@ -155,6 +155,10 @@ extension CandidateResponse: Decodable { /// A collection of source attributions for a piece of content. @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public struct CitationMetadata: Decodable { + enum CodingKeys: String, CodingKey { + case citationSources = "citations" + } + /// A list of individual cited sources and the parts of the content to which they apply. public let citationSources: [Citation] } @@ -172,7 +176,7 @@ public struct Citation: Decodable { public let uri: String /// The license the cited source work is distributed under. - public let license: String + public let license: String? } /// A value enumerating possible reasons for a model to terminate a content generation request. diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt index 4f50be7d32f..a530736b308 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/streaming-success-citations.txt @@ -1,13 +1,9 @@ -data: {"candidates": [{"content": {"parts": [{"text": "Some information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}],"promptFeedback": {"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": "Some information"}]}}]} -data: {"candidates": [{"content": {"parts": [{"text": " More information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " More information"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.06632687,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.03825006},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.07477004,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.048767097},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.13695431,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.059866417},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.046119746,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.036425155}]}]} -data: {"candidates": [{"content": {"parts": [{"text": ", Even more information"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}]}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " Some information cited from an external source"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.07850098,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.039416388},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.08035747,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04885778},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.12273335,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.059646938},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.053206205,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.04099903}],"citationMetadata": {"citations": [{"startIndex": 31,"endIndex": 187,"uri": "https://www.example.com/citation-1"}]}}]} -data: {"candidates": [{"content": {"parts": [{"text": " Some information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-1","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-2","license": ""}]}}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " More information cited from an external source"}]},"safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.08803312,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.044183318},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.094176665,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.0575992},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.13660839,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.08035747},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.060197048,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.046464667}],"citationMetadata": {"citations": [{"startIndex": 73,"endIndex": 248,"uri": "https://www.example.com/citation-2"},{"startIndex": 133,"endIndex": 272,"uri": "https://www.example.com/citation-3", "license": "mit"}]}}]} -data: {"candidates": [{"content": {"parts": [{"text": "More information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-3","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-4","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "Even more information cited from an external source"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.example.com/citation-5","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.example.com/citation-6","license": ""}]}}]} - -data: {"candidates": [{"content": {"parts": [{"text": "Physics (YouTube Channel)"}],"role": "model"},"finishReason": "STOP","index": 0,"safetyRatings": [{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE"},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE"}],"citationMetadata": {"citationSources": [{"startIndex": 574,"endIndex": 705,"uri": "https://www.google.com","license": ""},{"startIndex": 899,"endIndex": 1026,"uri": "https://www.google.com","license": ""}]}}]} +data: {"candidates": [{"content": {"role": "model","parts": [{"text": " More information"}]},"finishReason": "STOP","safetyRatings": [{"category": "HARM_CATEGORY_HATE_SPEECH","probability": "NEGLIGIBLE","probabilityScore": 0.12147716,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.0647717},{"category": "HARM_CATEGORY_DANGEROUS_CONTENT","probability": "NEGLIGIBLE","probabilityScore": 0.11858909,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.053899158},{"category": "HARM_CATEGORY_HARASSMENT","probability": "NEGLIGIBLE","probabilityScore": 0.14866412,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.08479541},{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT","probability": "NEGLIGIBLE","probabilityScore": 0.05470151,"severity": "HARM_SEVERITY_NEGLIGIBLE","severityScore": 0.045015533}]}],"usageMetadata": {"promptTokenCount": 9,"candidatesTokenCount": 163,"totalTokenCount": 172}} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json index ac99fcf0944..a0dacc3e883 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-citations.json @@ -2,63 +2,58 @@ "candidates": [ { "content": { + "role": "model", "parts": [ { "text": "Some information cited from an external source" } - ], - "role": "model" + ] }, "finishReason": "STOP", - "index": 0, "safetyRatings": [ { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.16013464, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.074500255 }, { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.09687653, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.049313594 }, { "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" + "probability": "NEGLIGIBLE", + "probabilityScore": 0.16817278, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.09451043 }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE", + "probabilityScore": 0.05023736, + "severity": "HARM_SEVERITY_NEGLIGIBLE", + "severityScore": 0.034553625 } ], "citationMetadata": { - "citationSources": [ + "citations": [ { - "startIndex": 574, - "endIndex": 705, - "uri": "https://www.example.com/some-citation", - "license": "" + "startIndex": 179, + "endIndex": 366, + "uri": "https://www.example.com/some-citation" } ] } } ], - "promptFeedback": { - "safetyRatings": [ - { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HATE_SPEECH", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_HARASSMENT", - "probability": "NEGLIGIBLE" - }, - { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", - "probability": "NEGLIGIBLE" - } - ] + "usageMetadata": { + "promptTokenCount": 11, + "candidatesTokenCount": 135, + "totalTokenCount": 146 } } diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index d2e4f95c0af..41d95110340 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -107,9 +107,9 @@ final class GenerativeModelTests: XCTestCase { XCTAssertEqual(citationMetadata.citationSources.count, 1) let citationSource = try XCTUnwrap(citationMetadata.citationSources.first) XCTAssertEqual(citationSource.uri, "https://www.example.com/some-citation") - XCTAssertEqual(citationSource.startIndex, 574) - XCTAssertEqual(citationSource.endIndex, 705) - XCTAssertEqual(citationSource.license, "") + XCTAssertEqual(citationSource.startIndex, 179) + XCTAssertEqual(citationSource.endIndex, 366) + XCTAssertNil(citationSource.license) } func testGenerateContent_success_quoteReply() async throws { @@ -669,21 +669,30 @@ final class GenerativeModelTests: XCTestCase { ) let stream = model.generateContentStream("Hi") - var citations: [Citation] = [] + var citations = [Citation]() + var responses = [GenerateContentResponse]() for try await content in stream { + responses.append(content) XCTAssertNotNil(content.text) let candidate = try XCTUnwrap(content.candidates.first) - XCTAssertEqual(candidate.finishReason, .stop) if let sources = candidate.citationMetadata?.citationSources { citations.append(contentsOf: sources) } } - XCTAssertEqual(citations.count, 8) + let lastCandidate = try XCTUnwrap(responses.last?.candidates.first) + XCTAssertEqual(lastCandidate.finishReason, .stop) + XCTAssertEqual(citations.count, 3) XCTAssertTrue(citations - .contains(where: { $0.startIndex == 574 && $0.endIndex == 705 && !$0.uri.isEmpty })) + .contains(where: { + $0.startIndex == 31 && $0.endIndex == 187 && $0 + .uri == "https://www.example.com/citation-1" && $0.license == nil + })) XCTAssertTrue(citations - .contains(where: { $0.startIndex == 899 && $0.endIndex == 1026 && !$0.uri.isEmpty })) + .contains(where: { + $0.startIndex == 133 && $0.endIndex == 272 && $0 + .uri == "https://www.example.com/citation-3" && $0.license == "mit" + })) } func testGenerateContentStream_appCheck_validToken() async throws { From 0f1fdc888ca2b72bc33ba2f0bf8faeece17fb6d6 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 26 Mar 2024 13:24:29 -0700 Subject: [PATCH 037/103] Manage location on VertexAI instead of model (#12630) --- .../ViewModels/ConversationViewModel.swift | 5 +-- .../ViewModels/PhotoReasoningViewModel.swift | 5 +-- .../ViewModels/SummarizeViewModel.swift | 5 +-- FirebaseVertexAI/Sources/VertexAI.swift | 40 +++++++++++-------- .../Sources/VertexAIComponent.swift | 6 +-- .../Tests/Unit/VertexAIAPITests.swift | 9 ++--- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift index 05cbe11250f..81667be2514 100644 --- a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift +++ b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift @@ -36,10 +36,7 @@ class ConversationViewModel: ObservableObject { private var chatTask: Task? init() { - model = VertexAI.vertexAI().generativeModel( - modelName: "gemini-1.0-pro", - location: "us-central1" - ) + model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") chat = model.startChat() } diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift index 2f2ed88d4a1..b4880bdc825 100644 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift @@ -44,10 +44,7 @@ class PhotoReasoningViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.vertexAI().generativeModel( - modelName: "gemini-1.0-pro-vision", - location: "us-central1" - ) + model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") } func reason() async { diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift index 0e3073d6da2..e3c78d09060 100644 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift @@ -32,10 +32,7 @@ class SummarizeViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.vertexAI().generativeModel( - modelName: "gemini-1.0-pro", - location: "us-central1" - ) + model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") } func summarize(inputText: String) async { diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index 64e76d3b4f5..2abb4abf7b0 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -25,26 +25,35 @@ public class VertexAI: NSObject { /// The default `VertexAI` instance. /// + /// - Parameter region: The region identifier, e.g., `us-central1`; see + /// [Vertex AI + /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) + /// for a list of supported regions. /// - Returns: An instance of `VertexAI`, configured with the default `FirebaseApp`. - public static func vertexAI() -> VertexAI { + public static func vertexAI(region: String) -> VertexAI { guard let app = FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } - return vertexAI(app: app) + return vertexAI(app: app, region: region) } /// Creates an instance of `VertexAI` configured with a custom `FirebaseApp`. /// - /// - Parameter app: The custom `FirebaseApp` used for initialization. + /// - Parameters: + /// - app: The custom `FirebaseApp` used for initialization. + /// - region: The region identifier, e.g., `us-central1`; see + /// [Vertex AI + /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) + /// for a list of supported regions. /// - Returns: A `VertexAI` instance, configured with the custom `FirebaseApp`. - public static func vertexAI(app: FirebaseApp) -> VertexAI { + public static func vertexAI(app: FirebaseApp, region: String) -> VertexAI { guard let provider = ComponentType.instance(for: VertexAIProvider.self, in: app.container) else { fatalError("No \(VertexAIProvider.self) instance found for Firebase app: \(app.name)") } - return provider.vertexAI() + return provider.vertexAI(region) } /// Initializes a generative model with the given parameters. @@ -54,19 +63,15 @@ public class VertexAI: NSObject { /// [Gemini /// models](https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models#gemini-models) /// for a list of supported model names. - /// - location: The location identifier, e.g., `us-central1`; see - /// [Vertex AI - /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) - /// for a list of supported locations. /// - generationConfig: The content generation parameters your model should use. /// - safetySettings: A value describing what types of harmful content your model should allow. /// - requestOptions: Configuration parameters for sending requests to the backend. - public func generativeModel(modelName: String, location: String, + public func generativeModel(modelName: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, requestOptions: RequestOptions = RequestOptions()) -> GenerativeModel { - let modelResourceName = modelResourceName(modelName: modelName, location: location) + let modelResourceName = modelResourceName(modelName: modelName, region: region) guard let apiKey = app.options.apiKey else { fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.") @@ -89,26 +94,29 @@ public class VertexAI: NSObject { private let appCheck: AppCheckInterop? - init(app: FirebaseApp) { + private let region: String + + init(app: FirebaseApp, region: String) { self.app = app + self.region = region appCheck = ComponentType.instance(for: AppCheckInterop.self, in: app.container) } - private func modelResourceName(modelName: String, location: String) -> String { + private func modelResourceName(modelName: String, region: String) -> String { if modelName.contains("/") { return modelName } guard let projectID = app.options.projectID else { fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.") } - guard !location.isEmpty else { + guard !region.isEmpty else { fatalError(""" - No location specified; see + No region specified; see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions for a list of available regions. """) } - return "projects/\(projectID)/locations/\(location)/publishers/google/models/\(modelName)" + return "projects/\(projectID)/locations/\(region)/publishers/google/models/\(modelName)" } } diff --git a/FirebaseVertexAI/Sources/VertexAIComponent.swift b/FirebaseVertexAI/Sources/VertexAIComponent.swift index 1378f812626..a9b7aa669a6 100644 --- a/FirebaseVertexAI/Sources/VertexAIComponent.swift +++ b/FirebaseVertexAI/Sources/VertexAIComponent.swift @@ -22,7 +22,7 @@ import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @objc(FIRVertexAIProvider) protocol VertexAIProvider { - @objc func vertexAI() -> VertexAI + @objc func vertexAI(_ location: String) -> VertexAI } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -64,7 +64,7 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { // MARK: - VertexAIProvider conformance - func vertexAI() -> VertexAI { + func vertexAI(_ region: String) -> VertexAI { os_unfair_lock_lock(&instancesLock) // Unlock before the function returns. @@ -73,7 +73,7 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { if let instance = instances[app.name] { return instance } - let newInstance = VertexAI(app: app) + let newInstance = VertexAI(app: app, region: region) instances[app.name] = newInstance return newInstance } diff --git a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift index a1fd27ab4c5..a4983eb8834 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift @@ -34,31 +34,28 @@ final class VertexAIAPITests: XCTestCase { let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)] // Instantiate Vertex AI SDK - Default App - let vertexAI = VertexAI.vertexAI() + let vertexAI = VertexAI.vertexAI(region: "my-region") // Instantiate Vertex AI SDK - Custom App - let _ = VertexAI.vertexAI(app: app!) + let _ = VertexAI.vertexAI(app: app!, region: "my-region") // Permutations without optional arguments. - let _ = vertexAI.generativeModel(modelName: "gemini-1.0-pro", location: "us-central1") + let _ = vertexAI.generativeModel(modelName: "gemini-1.0-pro") let _ = vertexAI.generativeModel( modelName: "gemini-1.0-pro", - location: "us-central1", safetySettings: filters ) let _ = vertexAI.generativeModel( modelName: "gemini-1.0-pro", - location: "us-central1", generationConfig: config ) // All arguments passed. let genAI = vertexAI.generativeModel( modelName: "gemini-1.0-pro", - location: "us-central1", generationConfig: config, // Optional safetySettings: filters // Optional ) From 53300e2bf59af0d6c6b5d7e2b236f0ae09928c53 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 26 Mar 2024 17:58:47 -0700 Subject: [PATCH 038/103] VertexAI Component tests; fix instance generation for different regions (#12631) --- FirebaseVertexAI/Sources/VertexAI.swift | 2 +- .../Sources/VertexAIComponent.swift | 8 +- .../Tests/Unit/VertexComponentTests.swift | 115 ++++++++++++++++++ Package.swift | 5 +- 4 files changed, 124 insertions(+), 6 deletions(-) create mode 100644 FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index 2abb4abf7b0..d1682745f98 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -94,7 +94,7 @@ public class VertexAI: NSObject { private let appCheck: AppCheckInterop? - private let region: String + let region: String init(app: FirebaseApp, region: String) { self.app = app diff --git a/FirebaseVertexAI/Sources/VertexAIComponent.swift b/FirebaseVertexAI/Sources/VertexAIComponent.swift index a9b7aa669a6..af8dbb56271 100644 --- a/FirebaseVertexAI/Sources/VertexAIComponent.swift +++ b/FirebaseVertexAI/Sources/VertexAIComponent.swift @@ -22,7 +22,7 @@ import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @objc(FIRVertexAIProvider) protocol VertexAIProvider { - @objc func vertexAI(_ location: String) -> VertexAI + @objc func vertexAI(_ region: String) -> VertexAI } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -32,7 +32,7 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { /// The app associated with all `VertexAI` instances in this container. /// This is `unowned` instead of `weak` so it can be used without unwrapping in `vertexAI(...)` - private unowned let app: FirebaseApp + unowned let app: FirebaseApp /// A map of active `VertexAI` instances for `app`, keyed by model resource names /// (e.g., "projects/my-project-id/locations/us-central1/publishers/google/models/gemini-pro"). @@ -70,11 +70,11 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { // Unlock before the function returns. defer { os_unfair_lock_unlock(&instancesLock) } - if let instance = instances[app.name] { + if let instance = instances[region] { return instance } let newInstance = VertexAI(app: app, region: region) - instances[app.name] = newInstance + instances[region] = newInstance return newInstance } } diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift new file mode 100644 index 00000000000..7c046c5e949 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -0,0 +1,115 @@ +// Copyright 2024 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 FirebaseCore +@testable import FirebaseVertexAI + +import SharedTestUtilities + +import XCTest + +@available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) +class VertexComponentTests: XCTestCase { + static var app: FirebaseApp! + + override class func setUp() { + super.setUp() + if app == nil { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.projectID = "myProjectID" + FirebaseApp.configure(options: options) + app = FirebaseApp(instanceWithName: "test", options: options) + } + } + + // MARK: Interoperability Tests + + /// Tests that the right number of components are being provided for the container. + func testComponentsBeingRegistered() throws { + let components = VertexAIComponent.componentsToRegister() + XCTAssert(components.count == 1) + } + + /// Tests that a vertex instance can be created properly by the VertexAIComponent. + func testVertexInstanceCreation() throws { + let app = try XCTUnwrap(VertexComponentTests.app) + let component = VertexAIComponent(app: app) + let vertex = component.vertexAI("my-region") + XCTAssertNotNil(vertex) + } + + /// Tests that the component container caches instances of VertexAIComponent. + func testMultipleComponentInstancesCreated() throws { + let registrants = NSMutableSet(array: [VertexAIComponent.self]) + let container = FirebaseComponentContainer( + app: VertexComponentTests.app, + registrants: registrants + ) + + let provider1 = ComponentType.instance(for: VertexAIProvider.self, + in: container) + XCTAssertNotNil(provider1) + + let provider2 = ComponentType.instance(for: VertexAIProvider.self, + in: container) + XCTAssertNotNil(provider2) + + // Ensure they're the same instance. + XCTAssert(provider1 === provider2) + } + + /// Tests that instances of vertex created are different. + func testMultipleVertexInstancesCreated() throws { + let app = try XCTUnwrap(VertexComponentTests.app) + let registrants = NSMutableSet(array: [VertexAIComponent.self]) + let container = FirebaseComponentContainer(app: app, registrants: registrants) + + let provider = ComponentType.instance(for: VertexAIProvider.self, + in: container) + XCTAssertNotNil(provider) + + let vertex1 = provider?.vertexAI("randomRegion") + let vertex2 = provider?.vertexAI("randomRegion") + XCTAssertNotNil(vertex1) + + // Ensure they're the same instance. + XCTAssert(vertex1 === vertex2) + + let vertex3 = provider?.vertexAI("differentRegion") + XCTAssertNotNil(vertex3) + + XCTAssert(vertex1 !== vertex3) + } + + /// Test that vertex instances get deallocated. + func testVertexLifecycle() throws { + weak var weakApp: FirebaseApp? + weak var weakVertex: VertexAI? + try autoreleasepool { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.projectID = "myProjectID" + let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) + weakApp = try XCTUnwrap(app1) + let vertex = VertexAI(app: app1, region: "transitory bucket") + weakVertex = vertex + XCTAssertNotNil(weakVertex) + } + XCTAssertNil(weakApp) + XCTAssertNil(weakVertex) + } +} diff --git a/Package.swift b/Package.swift index 711ab36cd42..2db21ca70ab 100644 --- a/Package.swift +++ b/Package.swift @@ -1375,11 +1375,14 @@ let package = Package( ), .testTarget( name: "FirebaseVertexAIUnit", - dependencies: ["FirebaseVertexAI"], + dependencies: ["FirebaseVertexAI", "SharedTestUtilities"], path: "FirebaseVertexAI/Tests/Unit", resources: [ .process("CountTokenResponses"), .process("GenerateContentResponses"), + ], + cSettings: [ + .headerSearchPath("../../../"), ] ), ] + firestoreTargets(), From 120fb62c1eef9a3414f8b7a2095267df72d52048 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 27 Mar 2024 14:34:49 +0000 Subject: [PATCH 039/103] Add function calling support in Vertex AI (#12633) --- FirebaseVertexAI/Sources/Chat.swift | 2 +- .../Sources/FunctionCalling.swift | 235 ++++++++++++++++++ .../Sources/GenerateContentRequest.swift | 2 + .../Sources/GenerativeModel.swift | 8 + FirebaseVertexAI/Sources/JSONValue.swift | 96 +++++++ FirebaseVertexAI/Sources/ModelContent.swift | 16 +- FirebaseVertexAI/Sources/VertexAI.swift | 3 + FirebaseVertexAI/Tests/Unit/ChatTests.swift | 1 + ...success-function-call-empty-arguments.json | 18 ++ ...ry-success-function-call-no-arguments.json | 19 ++ ...-success-function-call-with-arguments.json | 22 ++ .../Tests/Unit/GenerativeModelTests.swift | 79 ++++++ .../Tests/Unit/JSONValueTests.swift | 145 +++++++++++ .../Tests/Unit/VertexComponentTests.swift | 2 +- 14 files changed, 645 insertions(+), 3 deletions(-) create mode 100644 FirebaseVertexAI/Sources/FunctionCalling.swift create mode 100644 FirebaseVertexAI/Sources/JSONValue.swift create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-empty-arguments.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-no-arguments.json create mode 100644 FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-with-arguments.json create mode 100644 FirebaseVertexAI/Tests/Unit/JSONValueTests.swift diff --git a/FirebaseVertexAI/Sources/Chat.swift b/FirebaseVertexAI/Sources/Chat.swift index c7cfb859fa8..e443e18bed8 100644 --- a/FirebaseVertexAI/Sources/Chat.swift +++ b/FirebaseVertexAI/Sources/Chat.swift @@ -153,7 +153,7 @@ public class Chat { case let .text(str): combinedText += str - case .data(mimetype: _, _): + case .data, .functionCall, .functionResponse: // Don't combine it, just add to the content. If there's any text pending, add that as // a part. if !combinedText.isEmpty { diff --git a/FirebaseVertexAI/Sources/FunctionCalling.swift b/FirebaseVertexAI/Sources/FunctionCalling.swift new file mode 100644 index 00000000000..7e4d84d857e --- /dev/null +++ b/FirebaseVertexAI/Sources/FunctionCalling.swift @@ -0,0 +1,235 @@ +// Copyright 2024 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 + +/// A predicted function call returned from the model. +public struct FunctionCall: Equatable, Encodable { + /// The name of the function to call. + public let name: String + + /// The function parameters and values. + public let args: JSONObject +} + +/// A `Schema` object allows the definition of input and output data types. +/// +/// 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). +public class Schema: Encodable { + /// The data type. + let type: DataType + + /// The format of the data. + let format: String? + + /// A brief description of the parameter. + let description: String? + + /// Indicates if the value may be null. + let nullable: Bool? + + /// Possible values of the element of type ``DataType/string`` with "enum" format. + let enumValues: [String]? + + /// Schema of the elements of type ``DataType/array``. + let items: Schema? + + /// Properties of type ``DataType/object``. + let properties: [String: Schema]? + + /// Required properties of type ``DataType/object``. + let requiredProperties: [String]? + + enum CodingKeys: String, CodingKey { + case type + case format + case description + case nullable + case enumValues = "enum" + case items + case properties + case requiredProperties = "required" + } + + /// Constructs a new `Schema`. + /// + /// - Parameters: + /// - type: The data type. + /// - format: The format of the data; used only for primitive datatypes. + /// Supported formats: + /// - ``DataType/integer``: int32, int64 + /// - ``DataType/number``: float, double + /// - ``DataType/string``: enum + /// - description: A brief description of the parameter; may be formatted as Markdown. + /// - nullable: Indicates if the value may be null. + /// - enumValues: Possible values of the element of type ``DataType/string`` with "enum" format. + /// For example, an enum `Direction` may be defined as `["EAST", NORTH", "SOUTH", "WEST"]`. + /// - items: Schema of the elements of type ``DataType/array``. + /// - properties: Properties of type ``DataType/object``. + /// - requiredProperties: Required properties of type ``DataType/object``. + public init(type: DataType, format: String? = nil, description: String? = nil, + nullable: Bool? = nil, + enumValues: [String]? = nil, items: Schema? = nil, + properties: [String: Schema]? = nil, + requiredProperties: [String]? = nil) { + self.type = type + self.format = format + self.description = description + self.nullable = nullable + self.enumValues = enumValues + self.items = items + self.properties = properties + self.requiredProperties = requiredProperties + } +} + +/// A data type. +/// +/// Contains the set of OpenAPI [data types](https://spec.openapis.org/oas/v3.0.3#data-types). +public enum DataType: String, Encodable { + /// A `String` type. + case string = "STRING" + + /// A floating-point number type. + case number = "NUMBER" + + /// An integer type. + case integer = "INTEGER" + + /// A boolean type. + case boolean = "BOOLEAN" + + /// An array type. + case array = "ARRAY" + + /// An object type. + case object = "OBJECT" +} + +/// Structured representation of a function declaration. +/// +/// 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. +public struct FunctionDeclaration { + /// The name of the function. + let name: String + + /// A brief description of the function. + let description: String + + /// Describes the parameters to this function; must be of type ``DataType/object``. + let parameters: Schema? + + /// Constructs a new `FunctionDeclaration`. + /// + /// - Parameters: + /// - name: The name of the function; must be a-z, A-Z, 0-9, or contain underscores and dashes, + /// with a maximum length of 63. + /// - description: A brief description of the function. + /// - parameters: Describes the parameters to this function; the keys are parameter names and + /// the values are ``Schema`` objects describing them. + /// - requiredParameters: A list of required parameters by name. + public init(name: String, description: String, parameters: [String: Schema]?, + requiredParameters: [String]?) { + self.name = name + self.description = description + self.parameters = Schema( + type: .object, + properties: parameters, + requiredProperties: requiredParameters + ) + } +} + +/// Helper tools that the model may use to generate response. +/// +/// 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. +public struct Tool: Encodable { + /// A list of `FunctionDeclarations` available to the model. + let functionDeclarations: [FunctionDeclaration]? + + /// Constructs a new `Tool`. + /// + /// - Parameters: + /// - functionDeclarations: A list of `FunctionDeclarations` available to the model that can be + /// used for function calling. + /// The model or system does not execute the function. Instead the defined function may be + /// returned as a ``FunctionCall`` in ``ModelContent/Part/functionCall(_:)`` with arguments to + /// the client side for execution. The model may decide to call a subset of these functions by + /// populating ``FunctionCall`` in the response. The next conversation turn may contain a + /// ``FunctionResponse`` in ``ModelContent/Part/functionResponse(_:)`` with the + /// ``ModelContent/role`` "function", providing generation context for the next model turn. + public init(functionDeclarations: [FunctionDeclaration]?) { + self.functionDeclarations = functionDeclarations + } +} + +/// Result output from a ``FunctionCall``. +/// +/// Contains a string representing the `FunctionDeclaration.name` and a structured JSON object +/// containing any output from the function is used as context to the model. This should contain the +/// result of a ``FunctionCall`` made based on model prediction. +public struct FunctionResponse: Equatable, Encodable { + /// The name of the function that was called. + let name: String + + /// The function's response. + let response: JSONObject + + /// Constructs a new `FunctionResponse`. + /// + /// - Parameters: + /// - name: The name of the function that was called. + /// - response: The function's response. + public init(name: String, response: JSONObject) { + self.name = name + self.response = response + } +} + +// MARK: - Codable Conformance + +extension FunctionCall: Decodable { + enum CodingKeys: CodingKey { + case name + case args + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + if let args = try container.decodeIfPresent(JSONObject.self, forKey: .args) { + self.args = args + } else { + args = JSONObject() + } + } +} + +extension FunctionDeclaration: Encodable { + enum CodingKeys: String, CodingKey { + case name + case description + case parameters + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(description, forKey: .description) + try container.encode(parameters, forKey: .parameters) + } +} diff --git a/FirebaseVertexAI/Sources/GenerateContentRequest.swift b/FirebaseVertexAI/Sources/GenerateContentRequest.swift index 417260bb700..535ac4da3ae 100644 --- a/FirebaseVertexAI/Sources/GenerateContentRequest.swift +++ b/FirebaseVertexAI/Sources/GenerateContentRequest.swift @@ -21,6 +21,7 @@ struct GenerateContentRequest { let contents: [ModelContent] let generationConfig: GenerationConfig? let safetySettings: [SafetySetting]? + let tools: [Tool]? let isStreaming: Bool let options: RequestOptions } @@ -31,6 +32,7 @@ extension GenerateContentRequest: Encodable { case contents case generationConfig case safetySettings + case tools } } diff --git a/FirebaseVertexAI/Sources/GenerativeModel.swift b/FirebaseVertexAI/Sources/GenerativeModel.swift index aad2b3e99c0..7eab63a1d7a 100644 --- a/FirebaseVertexAI/Sources/GenerativeModel.swift +++ b/FirebaseVertexAI/Sources/GenerativeModel.swift @@ -34,6 +34,9 @@ public final class GenerativeModel { /// The safety settings to be used for prompts. let safetySettings: [SafetySetting]? + /// A list of tools the model may use to generate the next response. + let tools: [Tool]? + /// Configuration parameters for sending requests to the backend. let requestOptions: RequestOptions @@ -45,12 +48,14 @@ public final class GenerativeModel { /// - apiKey: The API key for your project. /// - 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. /// - requestOptions: Configuration parameters for sending requests to the backend. /// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`. init(name: String, apiKey: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, + tools: [Tool]?, requestOptions: RequestOptions, appCheck: AppCheckInterop?, urlSession: URLSession = .shared) { @@ -62,6 +67,7 @@ public final class GenerativeModel { ) self.generationConfig = generationConfig self.safetySettings = safetySettings + self.tools = tools self.requestOptions = requestOptions Logging.default.info(""" @@ -107,6 +113,7 @@ public final class GenerativeModel { contents: content(), generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, isStreaming: false, options: requestOptions) response = try await generativeAIService.loadRequest(request: generateContentRequest) @@ -178,6 +185,7 @@ public final class GenerativeModel { contents: evaluatedContent, generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, isStreaming: true, options: requestOptions) diff --git a/FirebaseVertexAI/Sources/JSONValue.swift b/FirebaseVertexAI/Sources/JSONValue.swift new file mode 100644 index 00000000000..5ce52cd2af9 --- /dev/null +++ b/FirebaseVertexAI/Sources/JSONValue.swift @@ -0,0 +1,96 @@ +// Copyright 2024 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 + +/// A collection of name-value pairs representing a JSON object. +/// +/// This may be decoded from, or encoded to, a +/// [`google.protobuf.Struct`](https://protobuf.dev/reference/protobuf/google.protobuf/#struct). +public typealias JSONObject = [String: JSONValue] + +/// Represents a value in one of JSON's data types. +/// +/// This may be decoded from, or encoded to, a +/// [`google.protobuf.Value`](https://protobuf.dev/reference/protobuf/google.protobuf/#value). +public enum JSONValue { + /// A `null` value. + case null + + /// A numeric value. + case number(Double) + + /// A string value. + case string(String) + + /// A boolean value. + case bool(Bool) + + /// A JSON object. + case object(JSONObject) + + /// An array of `JSONValue`s. + case array([JSONValue]) +} + +extension JSONValue: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if container.decodeNil() { + self = .null + } else if let numberValue = try? container.decode(Double.self) { + self = .number(numberValue) + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let boolValue = try? container.decode(Bool.self) { + self = .bool(boolValue) + } else if let objectValue = try? container.decode(JSONObject.self) { + self = .object(objectValue) + } else if let arrayValue = try? container.decode([JSONValue].self) { + self = .array(arrayValue) + } else { + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Failed to decode JSON value." + ) + } + } +} + +extension JSONValue: Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .null: + try container.encodeNil() + case let .number(numberValue): + // Convert to `Decimal` before encoding for consistent floating-point serialization across + // platforms. E.g., `Double` serializes 3.14159 as 3.1415899999999999 in some cases and + // 3.14159 in others. See + // https://forums.swift.org/t/jsonencoder-encodable-floating-point-rounding-error/41390/4 for + // more details. + try container.encode(Decimal(numberValue)) + case let .string(stringValue): + try container.encode(stringValue) + case let .bool(boolValue): + try container.encode(boolValue) + case let .object(objectValue): + try container.encode(objectValue) + case let .array(arrayValue): + try container.encode(arrayValue) + } + } +} + +extension JSONValue: Equatable {} diff --git a/FirebaseVertexAI/Sources/ModelContent.swift b/FirebaseVertexAI/Sources/ModelContent.swift index 44648c57852..136fc1debe3 100644 --- a/FirebaseVertexAI/Sources/ModelContent.swift +++ b/FirebaseVertexAI/Sources/ModelContent.swift @@ -25,6 +25,8 @@ public struct ModelContent: Codable, Equatable { enum CodingKeys: String, CodingKey { case text case inlineData + case functionCall + case functionResponse } enum InlineDataKeys: String, CodingKey { @@ -38,6 +40,12 @@ public struct ModelContent: Codable, Equatable { /// Data with a specified media type. Not all media types may be supported by the AI model. case data(mimetype: String, Data) + /// A predicted function call returned from the model. + case functionCall(FunctionCall) + + /// A response to a function call. + case functionResponse(FunctionResponse) + // MARK: Convenience Initializers /// Convenience function for populating a Part with JPEG data. @@ -64,6 +72,10 @@ public struct ModelContent: Codable, Equatable { ) try inlineDataContainer.encode(mimetype, forKey: .mimeType) try inlineDataContainer.encode(bytes, forKey: .bytes) + case let .functionCall(functionCall): + try container.encode(functionCall, forKey: .functionCall) + case let .functionResponse(functionResponse): + try container.encode(functionResponse, forKey: .functionResponse) } } @@ -79,10 +91,12 @@ public struct ModelContent: Codable, Equatable { let mimetype = try dataContainer.decode(String.self, forKey: .mimeType) let bytes = try dataContainer.decode(Data.self, forKey: .bytes) self = .data(mimetype: mimetype, bytes) + } else if values.contains(.functionCall) { + self = try .functionCall(values.decode(FunctionCall.self, forKey: .functionCall)) } else { throw DecodingError.dataCorrupted(.init( codingPath: [CodingKeys.text, CodingKeys.inlineData], - debugDescription: "Neither text or inline data was found." + debugDescription: "No text, inline data or function call was found." )) } } diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index d1682745f98..a0df2ab015e 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -65,10 +65,12 @@ public class VertexAI: NSObject { /// for a list of supported model names. /// - 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. /// - requestOptions: Configuration parameters for sending requests to the backend. public func generativeModel(modelName: String, generationConfig: GenerationConfig? = nil, safetySettings: [SafetySetting]? = nil, + tools: [Tool]? = nil, requestOptions: RequestOptions = RequestOptions()) -> GenerativeModel { let modelResourceName = modelResourceName(modelName: modelName, region: region) @@ -82,6 +84,7 @@ public class VertexAI: NSObject { apiKey: apiKey, generationConfig: generationConfig, safetySettings: safetySettings, + tools: tools, requestOptions: requestOptions, appCheck: appCheck ) diff --git a/FirebaseVertexAI/Tests/Unit/ChatTests.swift b/FirebaseVertexAI/Tests/Unit/ChatTests.swift index 5a7868fbe7a..20897a8787b 100644 --- a/FirebaseVertexAI/Tests/Unit/ChatTests.swift +++ b/FirebaseVertexAI/Tests/Unit/ChatTests.swift @@ -50,6 +50,7 @@ final class ChatTests: XCTestCase { let model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil, urlSession: urlSession diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-empty-arguments.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-empty-arguments.json new file mode 100644 index 00000000000..dc0b75ad930 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-empty-arguments.json @@ -0,0 +1,18 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "current_time" + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-no-arguments.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-no-arguments.json new file mode 100644 index 00000000000..05f4f4d58f9 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-no-arguments.json @@ -0,0 +1,19 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "current_time", + "args": {} + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-with-arguments.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-with-arguments.json new file mode 100644 index 00000000000..025735a3a39 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-success-function-call-with-arguments.json @@ -0,0 +1,22 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "sum", + "args": { + "y": 5, + "x": 4 + } + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ] +} diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 41d95110340..9a1210c123c 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -37,6 +37,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil, urlSession: urlSession @@ -165,6 +166,7 @@ final class GenerativeModelTests: XCTestCase { // Model name is prefixed with "models/". name: "models/test-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil, urlSession: urlSession @@ -173,11 +175,79 @@ final class GenerativeModelTests: XCTestCase { _ = try await model.generateContent(testPrompt) } + func testGenerateContent_success_functionCall_emptyArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-empty-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "current_time") + XCTAssertTrue(functionCall.args.isEmpty) + } + + func testGenerateContent_success_functionCall_noArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-no-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "current_time") + XCTAssertTrue(functionCall.args.isEmpty) + } + + func testGenerateContent_success_functionCall_withArguments() async throws { + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-success-function-call-with-arguments", + withExtension: "json" + ) + + let response = try await model.generateContent(testPrompt) + + XCTAssertEqual(response.candidates.count, 1) + let candidate = try XCTUnwrap(response.candidates.first) + XCTAssertEqual(candidate.content.parts.count, 1) + let part = try XCTUnwrap(candidate.content.parts.first) + guard case let .functionCall(functionCall) = part else { + XCTFail("Part is not a FunctionCall.") + return + } + XCTAssertEqual(functionCall.name, "sum") + XCTAssertEqual(functionCall.args.count, 2) + let argX = try XCTUnwrap(functionCall.args["x"]) + XCTAssertEqual(argX, .number(4)) + let argY = try XCTUnwrap(functionCall.args["y"]) + XCTAssertEqual(argY, .number(5)) + } + func testGenerateContent_appCheck_validToken() async throws { let appCheckToken = "test-valid-token" model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: AppCheckInteropFake(token: appCheckToken), urlSession: urlSession @@ -196,6 +266,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), urlSession: urlSession @@ -493,6 +564,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: requestOptions, appCheck: nil, urlSession: urlSession @@ -700,6 +772,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: AppCheckInteropFake(token: appCheckToken), urlSession: urlSession @@ -719,6 +792,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: AppCheckInteropFake(error: AppCheckErrorFake()), urlSession: urlSession @@ -836,6 +910,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: requestOptions, appCheck: nil, urlSession: urlSession @@ -894,6 +969,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: "my-model", apiKey: "API_KEY", + tools: nil, requestOptions: requestOptions, appCheck: nil, urlSession: urlSession @@ -913,6 +989,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: modelName, apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil ) @@ -926,6 +1003,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: modelResourceName, apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil ) @@ -939,6 +1017,7 @@ final class GenerativeModelTests: XCTestCase { model = GenerativeModel( name: tunedModelResourceName, apiKey: "API_KEY", + tools: nil, requestOptions: RequestOptions(), appCheck: nil ) diff --git a/FirebaseVertexAI/Tests/Unit/JSONValueTests.swift b/FirebaseVertexAI/Tests/Unit/JSONValueTests.swift new file mode 100644 index 00000000000..74196230887 --- /dev/null +++ b/FirebaseVertexAI/Tests/Unit/JSONValueTests.swift @@ -0,0 +1,145 @@ +// Copyright 2024 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 + +final class JSONValueTests: XCTestCase { + let decoder = JSONDecoder() + let encoder = JSONEncoder() + + let numberKey = "pi" + let numberValue = 3.14159 + let numberValueEncoded = "3.14159" + let stringKey = "hello" + let stringValue = "Hello, world!" + + override func setUp() { + encoder.outputFormatting = .sortedKeys + } + + func testDecodeNull() throws { + let jsonData = try XCTUnwrap("null".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .null) + } + + func testDecodeNumber() throws { + let jsonData = try XCTUnwrap("\(numberValue)".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .number(numberValue)) + } + + func testDecodeString() throws { + let jsonData = try XCTUnwrap("\"\(stringValue)\"".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .string(stringValue)) + } + + func testDecodeBool() throws { + let expectedBool = true + let jsonData = try XCTUnwrap("\(expectedBool)".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .bool(expectedBool)) + } + + func testDecodeObject() throws { + let expectedObject: JSONObject = [ + numberKey: .number(numberValue), + stringKey: .string(stringValue), + ] + let json = """ + { + "\(numberKey)": \(numberValue), + "\(stringKey)": "\(stringValue)" + } + """ + let jsonData = try XCTUnwrap(json.data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .object(expectedObject)) + } + + func testDecodeArray() throws { + let expectedArray: [JSONValue] = [.null, .number(numberValue)] + let jsonData = try XCTUnwrap("[ null, \(numberValue) ]".data(using: .utf8)) + + let jsonObject = try XCTUnwrap(decoder.decode(JSONValue.self, from: jsonData)) + + XCTAssertEqual(jsonObject, .array(expectedArray)) + } + + func testEncodeNull() throws { + let jsonData = try encoder.encode(JSONValue.null) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "null") + } + + func testEncodeNumber() throws { + let jsonData = try encoder.encode(JSONValue.number(numberValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\(numberValue)") + } + + func testEncodeString() throws { + let jsonData = try encoder.encode(JSONValue.string(stringValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\"\(stringValue)\"") + } + + func testEncodeBool() throws { + let boolValue = true + + let jsonData = try encoder.encode(JSONValue.bool(boolValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "\(boolValue)") + } + + func testEncodeObject() throws { + let objectValue: JSONObject = [ + numberKey: .number(numberValue), + stringKey: .string(stringValue), + ] + + let jsonData = try encoder.encode(JSONValue.object(objectValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual( + json, + "{\"\(stringKey)\":\"\(stringValue)\",\"\(numberKey)\":\(numberValueEncoded)}" + ) + } + + func testEncodeArray() throws { + let arrayValue: [JSONValue] = [.null, .number(numberValue)] + + let jsonData = try encoder.encode(JSONValue.array(arrayValue)) + + let json = try XCTUnwrap(String(data: jsonData, encoding: .utf8)) + XCTAssertEqual(json, "[null,\(numberValueEncoded)]") + } +} diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index 7c046c5e949..e28b747b65c 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -105,7 +105,7 @@ class VertexComponentTests: XCTestCase { options.projectID = "myProjectID" let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) weakApp = try XCTUnwrap(app1) - let vertex = VertexAI(app: app1, region: "transitory bucket") + let vertex = VertexAI(app: app1, region: "transitory region") weakVertex = vertex XCTAssertNotNil(weakVertex) } From dda7acff249fefb1763a9e5bc2b150d5e7a776b3 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 27 Mar 2024 16:35:36 +0000 Subject: [PATCH 040/103] Add `-Preview` suffix to `FirebaseVertexAI` library name (#12641) --- .../project.pbxproj | 100 +++++++++--------- Package.swift | 2 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj index 1b4a69cafb0..9efade7342d 100644 --- a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj @@ -7,10 +7,14 @@ objects = { /* Begin PBXBuildFile section */ - 869200AB2B86BE3100482873 /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 869200AA2B86BE3100482873 /* FirebaseVertexAI */; }; - 869200AD2B86BEA100482873 /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 869200AC2B86BEA100482873 /* FirebaseVertexAI */; }; - 869200AF2B86BFBC00482873 /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 869200AE2B86BFBC00482873 /* FirebaseVertexAI */; }; - 869200B12B86BFF500482873 /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 869200B02B86BFF500482873 /* FirebaseVertexAI */; }; + 868A33662BB476FA00304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33652BB476FA00304BB1 /* FirebaseAppCheck */; }; + 868A33682BB476FA00304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33672BB476FA00304BB1 /* FirebaseVertexAI-Preview */; }; + 868A336A2BB4771900304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33692BB4771900304BB1 /* FirebaseAppCheck */; }; + 868A336C2BB4771900304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */; }; + 868A336E2BB4773600304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336D2BB4773600304BB1 /* FirebaseAppCheck */; }; + 868A33702BB4773600304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */; }; + 868A33722BB4775D00304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33712BB4775D00304BB1 /* FirebaseAppCheck */; }; + 868A33742BB4775D00304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */; }; 869200B32B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; 869200B42B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; 869200B52B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; @@ -56,10 +60,6 @@ 88E10F592B11131900C08E95 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F582B11131900C08E95 /* ChatMessage.swift */; }; 88E10F5B2B11133E00C08E95 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5A2B11133E00C08E95 /* MessageView.swift */; }; 88E10F5D2B11135000C08E95 /* BouncingDots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5C2B11135000C08E95 /* BouncingDots.swift */; }; - 8D589E2B2BA8E87A0049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */; }; - 8D589E2D2BA8EEA30049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */; }; - 8D589E2F2BA8EEBD0049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */; }; - 8D589E312BA8EED80049BFCA /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */; }; CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */; }; /* End PBXBuildFile section */ @@ -101,9 +101,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8D589E312BA8EED80049BFCA /* FirebaseAppCheck in Frameworks */, + 868A33682BB476FA00304BB1 /* FirebaseVertexAI-Preview in Frameworks */, 886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */, - 869200AB2B86BE3100482873 /* FirebaseVertexAI in Frameworks */, + 868A33662BB476FA00304BB1 /* FirebaseAppCheck in Frameworks */, 886F95E32B17D6630036F07A /* GenerativeAIUIComponents in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -112,9 +112,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 868A336C2BB4771900304BB1 /* FirebaseVertexAI-Preview in Frameworks */, + 868A336A2BB4771900304BB1 /* FirebaseAppCheck in Frameworks */, 88209C242B0FBE1700F64795 /* MarkdownUI in Frameworks */, - 8D589E2F2BA8EEBD0049BFCA /* FirebaseAppCheck in Frameworks */, - 869200B12B86BFF500482873 /* FirebaseVertexAI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -122,9 +122,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8D589E2B2BA8E87A0049BFCA /* FirebaseAppCheck in Frameworks */, + 868A33702BB4773600304BB1 /* FirebaseVertexAI-Preview in Frameworks */, 88B8A91E2B0FC55100424728 /* MarkdownUI in Frameworks */, - 869200AF2B86BFBC00482873 /* FirebaseVertexAI in Frameworks */, + 868A336E2BB4773600304BB1 /* FirebaseAppCheck in Frameworks */, 88B8A9372B0FCBE700424728 /* GenerativeAIUIComponents in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -133,9 +133,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 8D589E2D2BA8EEA30049BFCA /* FirebaseAppCheck in Frameworks */, + 868A33742BB4775D00304BB1 /* FirebaseVertexAI-Preview in Frameworks */, 88D9474D2B14F27E008B5580 /* MarkdownUI in Frameworks */, - 869200AD2B86BEA100482873 /* FirebaseVertexAI in Frameworks */, + 868A33722BB4775D00304BB1 /* FirebaseAppCheck in Frameworks */, CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -342,8 +342,8 @@ packageProductDependencies = ( 886F95D72B17BA420036F07A /* MarkdownUI */, 886F95E22B17D6630036F07A /* GenerativeAIUIComponents */, - 869200AA2B86BE3100482873 /* FirebaseVertexAI */, - 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */, + 868A33652BB476FA00304BB1 /* FirebaseAppCheck */, + 868A33672BB476FA00304BB1 /* FirebaseVertexAI-Preview */, ); productName = GenerativeAISample; productReference = 8848C82F2B0D04BC007B434F /* GenerativeAISample.app */; @@ -364,8 +364,8 @@ name = GenerativeAITextSample; packageProductDependencies = ( 88209C232B0FBE1700F64795 /* MarkdownUI */, - 869200B02B86BFF500482873 /* FirebaseVertexAI */, - 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */, + 868A33692BB4771900304BB1 /* FirebaseAppCheck */, + 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */, ); productName = GenerativeAITextSample; productReference = 8848C8442B0D051E007B434F /* GenerativeAITextSample.app */; @@ -387,8 +387,8 @@ packageProductDependencies = ( 88B8A91D2B0FC55100424728 /* MarkdownUI */, 88B8A9362B0FCBE700424728 /* GenerativeAIUIComponents */, - 869200AE2B86BFBC00482873 /* FirebaseVertexAI */, - 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */, + 868A336D2BB4773600304BB1 /* FirebaseAppCheck */, + 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */, ); productName = GenerativeAIMultimodalSample; productReference = 8848C8562B0D056C007B434F /* GenerativeAIMultimodalSample.app */; @@ -410,8 +410,8 @@ packageProductDependencies = ( 88D9474C2B14F27E008B5580 /* MarkdownUI */, CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */, - 869200AC2B86BEA100482873 /* FirebaseVertexAI */, - 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */, + 868A33712BB4775D00304BB1 /* FirebaseAppCheck */, + 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */, ); productName = ChatSample; productReference = 88E10F422B110D5300C08E95 /* ChatSample.app */; @@ -453,7 +453,7 @@ packageReferences = ( 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */, DEA09AC32B1FCE22001962D9 /* XCRemoteSwiftPackageReference "NetworkImage" */, - 869200A92B86BE3100482873 /* XCLocalSwiftPackageReference "../.." */, + 868A33642BB476FA00304BB1 /* XCLocalSwiftPackageReference "../.." */, ); productRefGroup = 8848C8302B0D04BC007B434F /* Products */; projectDirPath = ""; @@ -985,7 +985,7 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 869200A92B86BE3100482873 /* XCLocalSwiftPackageReference "../.." */ = { + 868A33642BB476FA00304BB1 /* XCLocalSwiftPackageReference "../.." */ = { isa = XCLocalSwiftPackageReference; relativePath = ../..; }; @@ -1011,21 +1011,37 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 869200AA2B86BE3100482873 /* FirebaseVertexAI */ = { + 868A33652BB476FA00304BB1 /* FirebaseAppCheck */ = { isa = XCSwiftPackageProductDependency; - productName = FirebaseVertexAI; + productName = FirebaseAppCheck; + }; + 868A33672BB476FA00304BB1 /* FirebaseVertexAI-Preview */ = { + isa = XCSwiftPackageProductDependency; + productName = "FirebaseVertexAI-Preview"; + }; + 868A33692BB4771900304BB1 /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; }; - 869200AC2B86BEA100482873 /* FirebaseVertexAI */ = { + 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */ = { isa = XCSwiftPackageProductDependency; - productName = FirebaseVertexAI; + productName = "FirebaseVertexAI-Preview"; }; - 869200AE2B86BFBC00482873 /* FirebaseVertexAI */ = { + 868A336D2BB4773600304BB1 /* FirebaseAppCheck */ = { isa = XCSwiftPackageProductDependency; - productName = FirebaseVertexAI; + productName = FirebaseAppCheck; + }; + 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */ = { + isa = XCSwiftPackageProductDependency; + productName = "FirebaseVertexAI-Preview"; + }; + 868A33712BB4775D00304BB1 /* FirebaseAppCheck */ = { + isa = XCSwiftPackageProductDependency; + productName = FirebaseAppCheck; }; - 869200B02B86BFF500482873 /* FirebaseVertexAI */ = { + 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */ = { isa = XCSwiftPackageProductDependency; - productName = FirebaseVertexAI; + productName = "FirebaseVertexAI-Preview"; }; 88209C232B0FBE1700F64795 /* MarkdownUI */ = { isa = XCSwiftPackageProductDependency; @@ -1055,22 +1071,6 @@ package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; productName = MarkdownUI; }; - 8D589E2A2BA8E87A0049BFCA /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 8D589E2C2BA8EEA30049BFCA /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 8D589E2E2BA8EEBD0049BFCA /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 8D589E302BA8EED80049BFCA /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */ = { isa = XCSwiftPackageProductDependency; productName = GenerativeAIUIComponents; diff --git a/Package.swift b/Package.swift index 2db21ca70ab..ef7c0246955 100644 --- a/Package.swift +++ b/Package.swift @@ -138,7 +138,7 @@ let package = Package( targets: ["FirebaseStorage"] ), .library( - name: "FirebaseVertexAI", + name: "FirebaseVertexAI-Preview", targets: ["FirebaseVertexAI"] ), ], From 261c2364c71d4e6b0e9d6c386dcf85087951cd53 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 27 Mar 2024 20:33:37 +0000 Subject: [PATCH 041/103] Fix model name in Vertex AI multi-modal sample (#12645) --- .../ViewModels/PhotoReasoningViewModel.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift index b4880bdc825..1fb0097a808 100644 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift @@ -44,7 +44,8 @@ class PhotoReasoningViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") + let vertexAI = VertexAI.vertexAI(region: "us-central1") + model = vertexAI.generativeModel(modelName: "gemini-1.0-pro-vision") } func reason() async { From c9536b825428809ebc33f57ad1a67da7a68567b2 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Wed, 27 Mar 2024 20:37:29 +0000 Subject: [PATCH 042/103] Add `totalBillableCharacters` to `CountTokensResponse` in Vertex AI (#12646) --- FirebaseVertexAI/Sources/CountTokensRequest.swift | 3 +++ .../Tests/Unit/CountTokenResponses/success-total-tokens.json | 3 ++- FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/FirebaseVertexAI/Sources/CountTokensRequest.swift b/FirebaseVertexAI/Sources/CountTokensRequest.swift index 0a58d40acc0..d294212658c 100644 --- a/FirebaseVertexAI/Sources/CountTokensRequest.swift +++ b/FirebaseVertexAI/Sources/CountTokensRequest.swift @@ -42,4 +42,7 @@ extension CountTokensRequest: GenerativeAIRequest { public struct CountTokensResponse: Decodable { /// The total number of tokens in the input given to the model as a prompt. public let totalTokens: Int + + /// The total number of billable characters in the input given to the model as a prompt. + public let totalBillableCharacters: Int } diff --git a/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json index 0bccd9e7c3d..d2ad6e4ff30 100644 --- a/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json +++ b/FirebaseVertexAI/Tests/Unit/CountTokenResponses/success-total-tokens.json @@ -1,3 +1,4 @@ { - "totalTokens": 6 + "totalTokens": 6, + "totalBillableCharacters": 16 } diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 9a1210c123c..123299ac1a7 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -935,7 +935,9 @@ final class GenerativeModelTests: XCTestCase { ) let response = try await model.countTokens("Why is the sky blue?") + XCTAssertEqual(response.totalTokens, 6) + XCTAssertEqual(response.totalBillableCharacters, 16) } func testCountTokens_modelNotFound() async throws { From e63270eafa26cadb4347362ab75c99d4e62762ff Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Thu, 28 Mar 2024 22:17:31 +0000 Subject: [PATCH 043/103] Rename the Vertex AI parameter `region` to `location` (#12656) --- .../ViewModels/ConversationViewModel.swift | 2 +- .../ViewModels/PhotoReasoningViewModel.swift | 2 +- .../ViewModels/SummarizeViewModel.swift | 2 +- FirebaseVertexAI/Sources/VertexAI.swift | 28 +++++++++---------- .../Sources/VertexAIComponent.swift | 10 +++---- .../Tests/Unit/VertexAIAPITests.swift | 4 +-- .../Tests/Unit/VertexComponentTests.swift | 10 +++---- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift index 81667be2514..883cefb359f 100644 --- a/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift +++ b/FirebaseVertexAI/Sample/ChatSample/ViewModels/ConversationViewModel.swift @@ -36,7 +36,7 @@ class ConversationViewModel: ObservableObject { private var chatTask: Task? init() { - model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") + model = VertexAI.vertexAI(location: "us-central1").generativeModel(modelName: "gemini-1.0-pro") chat = model.startChat() } diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift index 1fb0097a808..bfa1cfa9028 100644 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/ViewModels/PhotoReasoningViewModel.swift @@ -44,7 +44,7 @@ class PhotoReasoningViewModel: ObservableObject { private var model: GenerativeModel? init() { - let vertexAI = VertexAI.vertexAI(region: "us-central1") + let vertexAI = VertexAI.vertexAI(location: "us-central1") model = vertexAI.generativeModel(modelName: "gemini-1.0-pro-vision") } diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift index e3c78d09060..a90e1cf15b8 100644 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift +++ b/FirebaseVertexAI/Sample/GenerativeAITextSample/ViewModels/SummarizeViewModel.swift @@ -32,7 +32,7 @@ class SummarizeViewModel: ObservableObject { private var model: GenerativeModel? init() { - model = VertexAI.vertexAI(region: "us-central1").generativeModel(modelName: "gemini-1.0-pro") + model = VertexAI.vertexAI(location: "us-central1").generativeModel(modelName: "gemini-1.0-pro") } func summarize(inputText: String) async { diff --git a/FirebaseVertexAI/Sources/VertexAI.swift b/FirebaseVertexAI/Sources/VertexAI.swift index a0df2ab015e..3e5f5e67050 100644 --- a/FirebaseVertexAI/Sources/VertexAI.swift +++ b/FirebaseVertexAI/Sources/VertexAI.swift @@ -25,35 +25,35 @@ public class VertexAI: NSObject { /// The default `VertexAI` instance. /// - /// - Parameter region: The region identifier, e.g., `us-central1`; see + /// - Parameter location: The region identifier, e.g., `us-central1`; see /// [Vertex AI /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) /// for a list of supported regions. /// - Returns: An instance of `VertexAI`, configured with the default `FirebaseApp`. - public static func vertexAI(region: String) -> VertexAI { + public static func vertexAI(location: String) -> VertexAI { guard let app = FirebaseApp.app() else { fatalError("No instance of the default Firebase app was found.") } - return vertexAI(app: app, region: region) + return vertexAI(app: app, location: location) } /// Creates an instance of `VertexAI` configured with a custom `FirebaseApp`. /// /// - Parameters: /// - app: The custom `FirebaseApp` used for initialization. - /// - region: The region identifier, e.g., `us-central1`; see + /// - location: The region identifier, e.g., `us-central1`; see /// [Vertex AI /// regions](https://cloud.google.com/vertex-ai/docs/general/locations#vertex-ai-regions) /// for a list of supported regions. /// - Returns: A `VertexAI` instance, configured with the custom `FirebaseApp`. - public static func vertexAI(app: FirebaseApp, region: String) -> VertexAI { + public static func vertexAI(app: FirebaseApp, location: String) -> VertexAI { guard let provider = ComponentType.instance(for: VertexAIProvider.self, in: app.container) else { fatalError("No \(VertexAIProvider.self) instance found for Firebase app: \(app.name)") } - return provider.vertexAI(region) + return provider.vertexAI(location) } /// Initializes a generative model with the given parameters. @@ -73,7 +73,7 @@ public class VertexAI: NSObject { tools: [Tool]? = nil, requestOptions: RequestOptions = RequestOptions()) -> GenerativeModel { - let modelResourceName = modelResourceName(modelName: modelName, region: region) + let modelResourceName = modelResourceName(modelName: modelName, location: location) guard let apiKey = app.options.apiKey else { fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.") @@ -97,29 +97,29 @@ public class VertexAI: NSObject { private let appCheck: AppCheckInterop? - let region: String + let location: String - init(app: FirebaseApp, region: String) { + init(app: FirebaseApp, location: String) { self.app = app - self.region = region + self.location = location appCheck = ComponentType.instance(for: AppCheckInterop.self, in: app.container) } - private func modelResourceName(modelName: String, region: String) -> String { + private func modelResourceName(modelName: String, location: String) -> String { if modelName.contains("/") { return modelName } guard let projectID = app.options.projectID else { fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.") } - guard !region.isEmpty else { + guard !location.isEmpty else { fatalError(""" - No region specified; see + No location specified; see https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations#available-regions for a list of available regions. """) } - return "projects/\(projectID)/locations/\(region)/publishers/google/models/\(modelName)" + return "projects/\(projectID)/locations/\(location)/publishers/google/models/\(modelName)" } } diff --git a/FirebaseVertexAI/Sources/VertexAIComponent.swift b/FirebaseVertexAI/Sources/VertexAIComponent.swift index af8dbb56271..a6d487e1208 100644 --- a/FirebaseVertexAI/Sources/VertexAIComponent.swift +++ b/FirebaseVertexAI/Sources/VertexAIComponent.swift @@ -22,7 +22,7 @@ import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @objc(FIRVertexAIProvider) protocol VertexAIProvider { - @objc func vertexAI(_ region: String) -> VertexAI + @objc func vertexAI(_ location: String) -> VertexAI } @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) @@ -64,17 +64,17 @@ class VertexAIComponent: NSObject, Library, VertexAIProvider { // MARK: - VertexAIProvider conformance - func vertexAI(_ region: String) -> VertexAI { + func vertexAI(_ location: String) -> VertexAI { os_unfair_lock_lock(&instancesLock) // Unlock before the function returns. defer { os_unfair_lock_unlock(&instancesLock) } - if let instance = instances[region] { + if let instance = instances[location] { return instance } - let newInstance = VertexAI(app: app, region: region) - instances[region] = newInstance + let newInstance = VertexAI(app: app, location: location) + instances[location] = newInstance return newInstance } } diff --git a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift index a4983eb8834..80236f405de 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexAIAPITests.swift @@ -34,10 +34,10 @@ final class VertexAIAPITests: XCTestCase { let filters = [SafetySetting(harmCategory: .dangerousContent, threshold: .blockOnlyHigh)] // Instantiate Vertex AI SDK - Default App - let vertexAI = VertexAI.vertexAI(region: "my-region") + let vertexAI = VertexAI.vertexAI(location: "my-location") // Instantiate Vertex AI SDK - Custom App - let _ = VertexAI.vertexAI(app: app!, region: "my-region") + let _ = VertexAI.vertexAI(app: app!, location: "my-location") // Permutations without optional arguments. diff --git a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift index e28b747b65c..20f2b15bee8 100644 --- a/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift +++ b/FirebaseVertexAI/Tests/Unit/VertexComponentTests.swift @@ -48,7 +48,7 @@ class VertexComponentTests: XCTestCase { func testVertexInstanceCreation() throws { let app = try XCTUnwrap(VertexComponentTests.app) let component = VertexAIComponent(app: app) - let vertex = component.vertexAI("my-region") + let vertex = component.vertexAI("my-location") XCTAssertNotNil(vertex) } @@ -82,14 +82,14 @@ class VertexComponentTests: XCTestCase { in: container) XCTAssertNotNil(provider) - let vertex1 = provider?.vertexAI("randomRegion") - let vertex2 = provider?.vertexAI("randomRegion") + let vertex1 = provider?.vertexAI("randomLocation") + let vertex2 = provider?.vertexAI("randomLocation") XCTAssertNotNil(vertex1) // Ensure they're the same instance. XCTAssert(vertex1 === vertex2) - let vertex3 = provider?.vertexAI("differentRegion") + let vertex3 = provider?.vertexAI("differentLocation") XCTAssertNotNil(vertex3) XCTAssert(vertex1 !== vertex3) @@ -105,7 +105,7 @@ class VertexComponentTests: XCTestCase { options.projectID = "myProjectID" let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) weakApp = try XCTUnwrap(app1) - let vertex = VertexAI(app: app1, region: "transitory region") + let vertex = VertexAI(app: app1, location: "transitory location") weakVertex = vertex XCTAssertNotNil(weakVertex) } From 942a3cd421a4db39f153591a7034c57092429c7a Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 2 Apr 2024 14:48:35 +0000 Subject: [PATCH 044/103] Update to prod endpoint in Vertex AI (#12657) --- FirebaseVertexAI/Sources/GenerativeAISwift.swift | 2 +- .../Unit/GenerateContentResponses/unary-failure-api-key.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseVertexAI/Sources/GenerativeAISwift.swift b/FirebaseVertexAI/Sources/GenerativeAISwift.swift index 0f0af2bc3e2..933a19c4759 100644 --- a/FirebaseVertexAI/Sources/GenerativeAISwift.swift +++ b/FirebaseVertexAI/Sources/GenerativeAISwift.swift @@ -21,5 +21,5 @@ import Foundation /// Constants associated with the GenerativeAISwift SDK @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) public enum GenerativeAISwift { - static let baseURL = "https://staging-firebaseml.sandbox.googleapis.com" + static let baseURL = "https://firebaseml.googleapis.com" } diff --git a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json index d3b5677366e..e9b472fd031 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json +++ b/FirebaseVertexAI/Tests/Unit/GenerateContentResponses/unary-failure-api-key.json @@ -9,7 +9,7 @@ "reason": "API_KEY_INVALID", "domain": "googleapis.com", "metadata": { - "service": "staging-firebaseml.sandbox.googleapis.com" + "service": "firebaseml.googleapis.com" } }, { From 261fe03e46cf556dbee087ea0dc80725ec2cf35f Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 2 Apr 2024 07:45:50 -0800 Subject: [PATCH 045/103] [VertexAI] Consolidate to single sample app (#12662) --- .../Sample/ChatSample/ChatSampleApp.swift | 36 -- .../GenerativeAIMultimodalSampleApp.swift | 31 -- .../project.pbxproj | 483 ------------------ .../GenerativeAITextSampleApp.swift | 31 -- 4 files changed, 581 deletions(-) delete mode 100644 FirebaseVertexAI/Sample/ChatSample/ChatSampleApp.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/GenerativeAIMultimodalSampleApp.swift delete mode 100644 FirebaseVertexAI/Sample/GenerativeAITextSample/GenerativeAITextSampleApp.swift diff --git a/FirebaseVertexAI/Sample/ChatSample/ChatSampleApp.swift b/FirebaseVertexAI/Sample/ChatSample/ChatSampleApp.swift deleted file mode 100644 index 7d7e02d7e3c..00000000000 --- a/FirebaseVertexAI/Sample/ChatSample/ChatSampleApp.swift +++ /dev/null @@ -1,36 +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 FirebaseCore -import FirebaseVertexAI -import SwiftUI - -@main -struct ChatSampleApp: App { - @StateObject - var viewModel = ConversationViewModel() - - init() { - FirebaseApp.configure() - } - - var body: some Scene { - WindowGroup { - NavigationStack { - ConversationScreen() - .environmentObject(viewModel) - } - } - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/GenerativeAIMultimodalSampleApp.swift b/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/GenerativeAIMultimodalSampleApp.swift deleted file mode 100644 index 49565de52d1..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAIMultimodalSample/GenerativeAIMultimodalSampleApp.swift +++ /dev/null @@ -1,31 +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 FirebaseCore -import SwiftUI - -@main -struct GenerativeAIMultimodalSampleApp: App { - init() { - FirebaseApp.configure() - } - - var body: some Scene { - WindowGroup { - NavigationStack { - PhotoReasoningScreen() - } - } - } -} diff --git a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj index 9efade7342d..ac930a76d95 100644 --- a/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj +++ b/FirebaseVertexAI/Sample/GenerativeAISample.xcodeproj/project.pbxproj @@ -9,34 +9,13 @@ /* Begin PBXBuildFile section */ 868A33662BB476FA00304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33652BB476FA00304BB1 /* FirebaseAppCheck */; }; 868A33682BB476FA00304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33672BB476FA00304BB1 /* FirebaseVertexAI-Preview */; }; - 868A336A2BB4771900304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33692BB4771900304BB1 /* FirebaseAppCheck */; }; - 868A336C2BB4771900304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */; }; - 868A336E2BB4773600304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336D2BB4773600304BB1 /* FirebaseAppCheck */; }; - 868A33702BB4773600304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */; }; - 868A33722BB4775D00304BB1 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33712BB4775D00304BB1 /* FirebaseAppCheck */; }; - 868A33742BB4775D00304BB1 /* FirebaseVertexAI-Preview in Frameworks */ = {isa = PBXBuildFile; productRef = 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */; }; 869200B32B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; - 869200B42B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; - 869200B52B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; - 869200B62B879C4F00482873 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 869200B22B879C4F00482873 /* GoogleService-Info.plist */; }; - 880266762B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8802666F2B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift */; }; - 880266792B0FC39000CF7CB6 /* PhotoReasoningScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880266752B0FC39000CF7CB6 /* PhotoReasoningScreen.swift */; }; - 88209C1F2B0FBDC300F64795 /* SummarizeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1B2B0FBDC300F64795 /* SummarizeScreen.swift */; }; - 88209C202B0FBDC300F64795 /* SummarizeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1D2B0FBDC300F64795 /* SummarizeViewModel.swift */; }; - 88209C242B0FBE1700F64795 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 88209C232B0FBE1700F64795 /* MarkdownUI */; }; - 88263BEF2B239BFE008AB09B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88263BEE2B239BFE008AB09B /* ErrorView.swift */; }; 88263BF02B239C09008AB09B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88263BEE2B239BFE008AB09B /* ErrorView.swift */; }; 88263BF12B239C11008AB09B /* ErrorDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889873842B208563005B4896 /* ErrorDetailsView.swift */; }; 8848C8332B0D04BC007B434F /* GenerativeAISampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8322B0D04BC007B434F /* GenerativeAISampleApp.swift */; }; 8848C8352B0D04BC007B434F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8342B0D04BC007B434F /* ContentView.swift */; }; 8848C8372B0D04BD007B434F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C8362B0D04BD007B434F /* Assets.xcassets */; }; 8848C83A2B0D04BD007B434F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C8392B0D04BD007B434F /* Preview Assets.xcassets */; }; - 8848C8472B0D051E007B434F /* GenerativeAITextSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8462B0D051E007B434F /* GenerativeAITextSampleApp.swift */; }; - 8848C84B2B0D051F007B434F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C84A2B0D051F007B434F /* Assets.xcassets */; }; - 8848C84E2B0D051F007B434F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C84D2B0D051F007B434F /* Preview Assets.xcassets */; }; - 8848C8592B0D056C007B434F /* GenerativeAIMultimodalSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8848C8582B0D056C007B434F /* GenerativeAIMultimodalSampleApp.swift */; }; - 8848C85D2B0D056D007B434F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C85C2B0D056D007B434F /* Assets.xcassets */; }; - 8848C8602B0D056D007B434F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8848C85F2B0D056D007B434F /* Preview Assets.xcassets */; }; 886F95D52B17BA010036F07A /* SummarizeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1B2B0FBDC300F64795 /* SummarizeScreen.swift */; }; 886F95D62B17BA010036F07A /* SummarizeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88209C1D2B0FBDC300F64795 /* SummarizeViewModel.swift */; }; 886F95D82B17BA420036F07A /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 886F95D72B17BA420036F07A /* MarkdownUI */; }; @@ -48,19 +27,6 @@ 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 */; }; - 889873852B208563005B4896 /* ErrorDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 889873842B208563005B4896 /* ErrorDetailsView.swift */; }; - 88B8A91E2B0FC55100424728 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 88B8A91D2B0FC55100424728 /* MarkdownUI */; }; - 88B8A9372B0FCBE700424728 /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 88B8A9362B0FCBE700424728 /* GenerativeAIUIComponents */; }; - 88D9474D2B14F27E008B5580 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = 88D9474C2B14F27E008B5580 /* MarkdownUI */; }; - 88E10F452B110D5300C08E95 /* ChatSampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F442B110D5300C08E95 /* ChatSampleApp.swift */; }; - 88E10F492B110D5400C08E95 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E10F482B110D5400C08E95 /* Assets.xcassets */; }; - 88E10F4C2B110D5400C08E95 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 88E10F4B2B110D5400C08E95 /* Preview Assets.xcassets */; }; - 88E10F552B1112CA00C08E95 /* ConversationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */; }; - 88E10F572B1112F600C08E95 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F562B1112F600C08E95 /* ConversationViewModel.swift */; }; - 88E10F592B11131900C08E95 /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F582B11131900C08E95 /* ChatMessage.swift */; }; - 88E10F5B2B11133E00C08E95 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5A2B11133E00C08E95 /* MessageView.swift */; }; - 88E10F5D2B11135000C08E95 /* BouncingDots.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88E10F5C2B11135000C08E95 /* BouncingDots.swift */; }; - CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */ = {isa = PBXBuildFile; productRef = CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -75,18 +41,12 @@ 8848C8342B0D04BC007B434F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 8848C8362B0D04BD007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8848C8392B0D04BD007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 8848C8442B0D051E007B434F /* GenerativeAITextSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GenerativeAITextSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 8848C8462B0D051E007B434F /* GenerativeAITextSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerativeAITextSampleApp.swift; sourceTree = ""; }; 8848C84A2B0D051F007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8848C84D2B0D051F007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 8848C8562B0D056C007B434F /* GenerativeAIMultimodalSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GenerativeAIMultimodalSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 8848C8582B0D056C007B434F /* GenerativeAIMultimodalSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerativeAIMultimodalSampleApp.swift; sourceTree = ""; }; 8848C85C2B0D056D007B434F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 8848C85F2B0D056D007B434F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 889873842B208563005B4896 /* ErrorDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsView.swift; sourceTree = ""; }; 88B8A9352B0FCBA700424728 /* GenerativeAIUIComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = GenerativeAIUIComponents; sourceTree = ""; }; - 88E10F422B110D5300C08E95 /* ChatSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 88E10F442B110D5300C08E95 /* ChatSampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSampleApp.swift; sourceTree = ""; }; 88E10F482B110D5400C08E95 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 88E10F4B2B110D5400C08E95 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 88E10F542B1112CA00C08E95 /* ConversationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationScreen.swift; sourceTree = ""; }; @@ -108,38 +68,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8848C8412B0D051E007B434F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 868A336C2BB4771900304BB1 /* FirebaseVertexAI-Preview in Frameworks */, - 868A336A2BB4771900304BB1 /* FirebaseAppCheck in Frameworks */, - 88209C242B0FBE1700F64795 /* MarkdownUI in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8848C8532B0D056C007B434F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 868A33702BB4773600304BB1 /* FirebaseVertexAI-Preview in Frameworks */, - 88B8A91E2B0FC55100424728 /* MarkdownUI in Frameworks */, - 868A336E2BB4773600304BB1 /* FirebaseAppCheck in Frameworks */, - 88B8A9372B0FCBE700424728 /* GenerativeAIUIComponents in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88E10F3F2B110D5300C08E95 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 868A33742BB4775D00304BB1 /* FirebaseVertexAI-Preview in Frameworks */, - 88D9474D2B14F27E008B5580 /* MarkdownUI in Frameworks */, - 868A33722BB4775D00304BB1 /* FirebaseAppCheck in Frameworks */, - CB0DFFD52B2B4F08006E109D /* GenerativeAIUIComponents in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -200,9 +128,6 @@ isa = PBXGroup; children = ( 8848C82F2B0D04BC007B434F /* GenerativeAISample.app */, - 8848C8442B0D051E007B434F /* GenerativeAITextSample.app */, - 8848C8562B0D056C007B434F /* GenerativeAIMultimodalSample.app */, - 88E10F422B110D5300C08E95 /* ChatSample.app */, ); name = Products; sourceTree = ""; @@ -231,7 +156,6 @@ children = ( 88209C1C2B0FBDC300F64795 /* ViewModels */, 88209C1A2B0FBDC300F64795 /* Screens */, - 8848C8462B0D051E007B434F /* GenerativeAITextSampleApp.swift */, 8848C84A2B0D051F007B434F /* Assets.xcassets */, 8848C84C2B0D051F007B434F /* Preview Content */, ); @@ -251,7 +175,6 @@ children = ( 8802666E2B0FC39000CF7CB6 /* ViewModels */, 880266742B0FC39000CF7CB6 /* Screens */, - 8848C8582B0D056C007B434F /* GenerativeAIMultimodalSampleApp.swift */, 8848C85C2B0D056D007B434F /* Assets.xcassets */, 8848C85E2B0D056D007B434F /* Preview Content */, ); @@ -273,7 +196,6 @@ 88E10F502B11123600C08E95 /* ViewModels */, 88E10F512B11124100C08E95 /* Views */, 88E10F532B1112B900C08E95 /* Screens */, - 88E10F442B110D5300C08E95 /* ChatSampleApp.swift */, 88E10F482B110D5400C08E95 /* Assets.xcassets */, 88E10F4A2B110D5400C08E95 /* Preview Content */, ); @@ -349,74 +271,6 @@ productReference = 8848C82F2B0D04BC007B434F /* GenerativeAISample.app */; productType = "com.apple.product-type.application"; }; - 8848C8432B0D051E007B434F /* GenerativeAITextSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8848C84F2B0D051F007B434F /* Build configuration list for PBXNativeTarget "GenerativeAITextSample" */; - buildPhases = ( - 8848C8402B0D051E007B434F /* Sources */, - 8848C8412B0D051E007B434F /* Frameworks */, - 8848C8422B0D051E007B434F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GenerativeAITextSample; - packageProductDependencies = ( - 88209C232B0FBE1700F64795 /* MarkdownUI */, - 868A33692BB4771900304BB1 /* FirebaseAppCheck */, - 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */, - ); - productName = GenerativeAITextSample; - productReference = 8848C8442B0D051E007B434F /* GenerativeAITextSample.app */; - productType = "com.apple.product-type.application"; - }; - 8848C8552B0D056C007B434F /* GenerativeAIMultimodalSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 8848C8612B0D056D007B434F /* Build configuration list for PBXNativeTarget "GenerativeAIMultimodalSample" */; - buildPhases = ( - 8848C8522B0D056C007B434F /* Sources */, - 8848C8532B0D056C007B434F /* Frameworks */, - 8848C8542B0D056C007B434F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = GenerativeAIMultimodalSample; - packageProductDependencies = ( - 88B8A91D2B0FC55100424728 /* MarkdownUI */, - 88B8A9362B0FCBE700424728 /* GenerativeAIUIComponents */, - 868A336D2BB4773600304BB1 /* FirebaseAppCheck */, - 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */, - ); - productName = GenerativeAIMultimodalSample; - productReference = 8848C8562B0D056C007B434F /* GenerativeAIMultimodalSample.app */; - productType = "com.apple.product-type.application"; - }; - 88E10F412B110D5300C08E95 /* ChatSample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 88E10F4F2B110D5400C08E95 /* Build configuration list for PBXNativeTarget "ChatSample" */; - buildPhases = ( - 88E10F3E2B110D5300C08E95 /* Sources */, - 88E10F3F2B110D5300C08E95 /* Frameworks */, - 88E10F402B110D5300C08E95 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = ChatSample; - packageProductDependencies = ( - 88D9474C2B14F27E008B5580 /* MarkdownUI */, - CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */, - 868A33712BB4775D00304BB1 /* FirebaseAppCheck */, - 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */, - ); - productName = ChatSample; - productReference = 88E10F422B110D5300C08E95 /* ChatSample.app */; - productType = "com.apple.product-type.application"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -430,15 +284,6 @@ 8848C82E2B0D04BC007B434F = { CreatedOnToolsVersion = 15.1; }; - 8848C8432B0D051E007B434F = { - CreatedOnToolsVersion = 15.1; - }; - 8848C8552B0D056C007B434F = { - CreatedOnToolsVersion = 15.1; - }; - 88E10F412B110D5300C08E95 = { - CreatedOnToolsVersion = 15.1; - }; }; }; buildConfigurationList = 8848C82A2B0D04BC007B434F /* Build configuration list for PBXProject "GenerativeAISample" */; @@ -460,9 +305,6 @@ projectRoot = ""; targets = ( 8848C82E2B0D04BC007B434F /* GenerativeAISample */, - 8848C8432B0D051E007B434F /* GenerativeAITextSample */, - 8848C8552B0D056C007B434F /* GenerativeAIMultimodalSample */, - 88E10F412B110D5300C08E95 /* ChatSample */, ); }; /* End PBXProject section */ @@ -478,36 +320,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8848C8422B0D051E007B434F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8848C84E2B0D051F007B434F /* Preview Assets.xcassets in Resources */, - 8848C84B2B0D051F007B434F /* Assets.xcassets in Resources */, - 869200B42B879C4F00482873 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8848C8542B0D056C007B434F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 8848C8602B0D056D007B434F /* Preview Assets.xcassets in Resources */, - 8848C85D2B0D056D007B434F /* Assets.xcassets in Resources */, - 869200B52B879C4F00482873 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88E10F402B110D5300C08E95 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 88E10F4C2B110D5400C08E95 /* Preview Assets.xcassets in Resources */, - 88E10F492B110D5400C08E95 /* Assets.xcassets in Resources */, - 869200B62B879C4F00482873 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -531,41 +343,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 8848C8402B0D051E007B434F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 88209C1F2B0FBDC300F64795 /* SummarizeScreen.swift in Sources */, - 8848C8472B0D051E007B434F /* GenerativeAITextSampleApp.swift in Sources */, - 88209C202B0FBDC300F64795 /* SummarizeViewModel.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8848C8522B0D056C007B434F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 880266762B0FC39000CF7CB6 /* PhotoReasoningViewModel.swift in Sources */, - 880266792B0FC39000CF7CB6 /* PhotoReasoningScreen.swift in Sources */, - 8848C8592B0D056C007B434F /* GenerativeAIMultimodalSampleApp.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 88E10F3E2B110D5300C08E95 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 88E10F5B2B11133E00C08E95 /* MessageView.swift in Sources */, - 88E10F572B1112F600C08E95 /* ConversationViewModel.swift in Sources */, - 88E10F552B1112CA00C08E95 /* ConversationScreen.swift in Sources */, - 88E10F592B11131900C08E95 /* ChatMessage.swift in Sources */, - 88E10F5D2B11135000C08E95 /* BouncingDots.swift in Sources */, - 88E10F452B110D5300C08E95 /* ChatSampleApp.swift in Sources */, - 88263BEF2B239BFE008AB09B /* ErrorView.swift in Sources */, - 889873852B208563005B4896 /* ErrorDetailsView.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ @@ -748,192 +525,6 @@ }; name = Release; }; - 8848C8502B0D051F007B434F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"GenerativeAITextSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.GenerativeAITextSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 8848C8512B0D051F007B434F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"GenerativeAITextSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.GenerativeAITextSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 8848C8622B0D056D007B434F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"GenerativeAIMultimodalSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.GenerativeAIMultimodalSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 8848C8632B0D056D007B434F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"GenerativeAIMultimodalSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.GenerativeAIMultimodalSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 88E10F4D2B110D5400C08E95 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"ChatSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.ChatSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 88E10F4E2B110D5400C08E95 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"ChatSample/Preview Content\""; - DEVELOPMENT_TEAM = ""; - ENABLE_PREVIEWS = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 16.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.google.generativeai.ChatSample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -955,33 +546,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 8848C84F2B0D051F007B434F /* Build configuration list for PBXNativeTarget "GenerativeAITextSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8848C8502B0D051F007B434F /* Debug */, - 8848C8512B0D051F007B434F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 8848C8612B0D056D007B434F /* Build configuration list for PBXNativeTarget "GenerativeAIMultimodalSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 8848C8622B0D056D007B434F /* Debug */, - 8848C8632B0D056D007B434F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 88E10F4F2B110D5400C08E95 /* Build configuration list for PBXNativeTarget "ChatSample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 88E10F4D2B110D5400C08E95 /* Debug */, - 88E10F4E2B110D5400C08E95 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ @@ -1019,35 +583,6 @@ isa = XCSwiftPackageProductDependency; productName = "FirebaseVertexAI-Preview"; }; - 868A33692BB4771900304BB1 /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 868A336B2BB4771900304BB1 /* FirebaseVertexAI-Preview */ = { - isa = XCSwiftPackageProductDependency; - productName = "FirebaseVertexAI-Preview"; - }; - 868A336D2BB4773600304BB1 /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 868A336F2BB4773600304BB1 /* FirebaseVertexAI-Preview */ = { - isa = XCSwiftPackageProductDependency; - productName = "FirebaseVertexAI-Preview"; - }; - 868A33712BB4775D00304BB1 /* FirebaseAppCheck */ = { - isa = XCSwiftPackageProductDependency; - productName = FirebaseAppCheck; - }; - 868A33732BB4775D00304BB1 /* FirebaseVertexAI-Preview */ = { - isa = XCSwiftPackageProductDependency; - productName = "FirebaseVertexAI-Preview"; - }; - 88209C232B0FBE1700F64795 /* MarkdownUI */ = { - isa = XCSwiftPackageProductDependency; - package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; - productName = MarkdownUI; - }; 886F95D72B17BA420036F07A /* MarkdownUI */ = { isa = XCSwiftPackageProductDependency; package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; @@ -1057,24 +592,6 @@ isa = XCSwiftPackageProductDependency; productName = GenerativeAIUIComponents; }; - 88B8A91D2B0FC55100424728 /* MarkdownUI */ = { - isa = XCSwiftPackageProductDependency; - package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; - productName = MarkdownUI; - }; - 88B8A9362B0FCBE700424728 /* GenerativeAIUIComponents */ = { - isa = XCSwiftPackageProductDependency; - productName = GenerativeAIUIComponents; - }; - 88D9474C2B14F27E008B5580 /* MarkdownUI */ = { - isa = XCSwiftPackageProductDependency; - package = 88209C212B0FBDF700F64795 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; - productName = MarkdownUI; - }; - CB0DFFD42B2B4F08006E109D /* GenerativeAIUIComponents */ = { - isa = XCSwiftPackageProductDependency; - productName = GenerativeAIUIComponents; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 8848C8272B0D04BC007B434F /* Project object */; diff --git a/FirebaseVertexAI/Sample/GenerativeAITextSample/GenerativeAITextSampleApp.swift b/FirebaseVertexAI/Sample/GenerativeAITextSample/GenerativeAITextSampleApp.swift deleted file mode 100644 index c800e1f7be8..00000000000 --- a/FirebaseVertexAI/Sample/GenerativeAITextSample/GenerativeAITextSampleApp.swift +++ /dev/null @@ -1,31 +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 FirebaseCore -import SwiftUI - -@main -struct GenerativeAITextSampleApp: App { - init() { - FirebaseApp.configure() - } - - var body: some Scene { - WindowGroup { - NavigationStack { - SummarizeScreen() - } - } - } -} From aa18e59794571c81f4c259796fb6ce0e04618002 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 2 Apr 2024 17:11:08 +0000 Subject: [PATCH 046/103] Add platform tags in `x-goog-api-client` (#12680) --- FirebaseCore/Sources/FIRApp.m | 2 +- FirebaseVertexAI/Sources/GenerativeAIService.swift | 13 ++++++++++--- .../Tests/Unit/GenerativeModelTests.swift | 5 +++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/FirebaseCore/Sources/FIRApp.m b/FirebaseCore/Sources/FIRApp.m index a8cc38c12de..12afc134321 100644 --- a/FirebaseCore/Sources/FIRApp.m +++ b/FirebaseCore/Sources/FIRApp.m @@ -829,7 +829,7 @@ + (void)registerSwiftComponents { @"FIRSessions" : @"fire-ses", @"FIRFunctionsComponent" : @"fire-fun", @"FIRStorageComponent" : @"fire-str", - @"FIRVertexAIComponent" : @"fire-vtx", + @"FIRVertexAIComponent" : @"fire-vertex", }; for (NSString *className in swiftComponents.allKeys) { Class klass = NSClassFromString(className); diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index e1df1010c06..b3687c3c013 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -18,6 +18,12 @@ import Foundation @available(iOS 15.0, macOS 11.0, macCatalyst 15.0, *) struct GenerativeAIService { + /// The language of the SDK in the format "gl-/" where version may be blank. + static let languageTag = "gl-swift/" + + /// The Firebase SDK version in the format "fire/". + static let firebaseVersionTag = "fire/\(FirebaseVersion())" + /// Gives permission to talk to the backend. private let apiKey: String @@ -155,9 +161,10 @@ struct GenerativeAIService { var urlRequest = URLRequest(url: request.url) urlRequest.httpMethod = "POST" urlRequest.setValue(apiKey, forHTTPHeaderField: "x-goog-api-key") - // TODO: Determine the right client header to use. - // urlRequest.setValue("genai-swift/\(GenerativeAISwift.version))", - // forHTTPHeaderField: "x-goog-api-client") + urlRequest.setValue( + "\(GenerativeAIService.languageTag) \(GenerativeAIService.firebaseVersionTag)", + forHTTPHeaderField: "x-goog-api-client" + ) urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") if let appCheck { diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index 123299ac1a7..8031fb95dcf 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -13,6 +13,7 @@ // limitations under the License. import FirebaseAppCheckInterop +import FirebaseCore import XCTest @testable import FirebaseVertexAI @@ -1058,6 +1059,10 @@ final class GenerativeModelTests: XCTestCase { let requestURL = try XCTUnwrap(request.url) XCTAssertEqual(requestURL.path.occurrenceCount(of: "models/"), 1) XCTAssertEqual(request.timeoutInterval, timeout) + let apiClientTags = try XCTUnwrap(request.value(forHTTPHeaderField: "x-goog-api-client")) + .components(separatedBy: " ") + XCTAssert(apiClientTags.contains(GenerativeAIService.languageTag)) + XCTAssert(apiClientTags.contains(GenerativeAIService.firebaseVersionTag)) XCTAssertEqual(request.value(forHTTPHeaderField: "X-Firebase-AppCheck"), appCheckToken) let response = try XCTUnwrap(HTTPURLResponse( url: requestURL, From 56feccc9932065dacfcc639248b35d83a2da9d02 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 2 Apr 2024 09:52:57 -0800 Subject: [PATCH 047/103] [vertex-ai] Fix for SwiftFormat 0.53.5 (#12683) --- .../Sources/GenerativeAIUIComponents/InputField.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift b/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift index 317baaaf3c1..3f12ea65d8a 100644 --- a/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift +++ b/FirebaseVertexAI/Sample/GenerativeAIUIComponents/Sources/GenerativeAIUIComponents/InputField.swift @@ -13,6 +13,7 @@ // limitations under the License. import SwiftUI + public struct InputField