diff --git a/FirebaseVertexAI/Sources/Constants.swift b/FirebaseVertexAI/Sources/Constants.swift index 8f1768d7082..e3f2d45d6df 100644 --- a/FirebaseVertexAI/Sources/Constants.swift +++ b/FirebaseVertexAI/Sources/Constants.swift @@ -21,5 +21,8 @@ import Foundation /// Constants associated with the Vertex AI for Firebase SDK. enum Constants { /// The Vertex AI backend endpoint URL. + /// + /// TODO(andrewheard): Update to "https://firebasevertexai.googleapis.com" after the Vertex AI in + /// Firebase API launch. static let baseURL = "https://firebaseml.googleapis.com" } diff --git a/FirebaseVertexAI/Sources/Errors.swift b/FirebaseVertexAI/Sources/Errors.swift index 17a4dd9e201..57fbe826580 100644 --- a/FirebaseVertexAI/Sources/Errors.swift +++ b/FirebaseVertexAI/Sources/Errors.swift @@ -31,9 +31,14 @@ struct RPCError: Error { self.details = details } + // TODO(andrewheard): Remove this method after the Vertex AI in Firebase API launch. func isFirebaseMLServiceDisabledError() -> Bool { return details.contains { $0.isFirebaseMLServiceDisabledErrorDetails() } } + + func isVertexAIInFirebaseServiceDisabledError() -> Bool { + return details.contains { $0.isVertexAIInFirebaseServiceDisabledErrorDetails() } + } } extension RPCError: Decodable { @@ -86,17 +91,26 @@ struct ErrorDetails { return type == ErrorDetails.errorInfoType } + func isServiceDisabledError() -> Bool { + return isErrorInfo() && reason == "SERVICE_DISABLED" && domain == "googleapis.com" + } + + // TODO(andrewheard): Remove this method after the Vertex AI in Firebase API launch. func isFirebaseMLServiceDisabledErrorDetails() -> Bool { - guard isErrorInfo() else { + guard isServiceDisabledError() else { return false } - guard reason == "SERVICE_DISABLED" else { + guard let metadata, metadata["service"] == "firebaseml.googleapis.com" else { return false } - guard domain == "googleapis.com" else { + return true + } + + func isVertexAIInFirebaseServiceDisabledErrorDetails() -> Bool { + guard isServiceDisabledError() else { return false } - guard let metadata, metadata["service"] == "firebaseml.googleapis.com" else { + guard let metadata, metadata["service"] == "firebasevertexai.googleapis.com" else { return false } return true diff --git a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift index 08b178d5808..327b7d4f1b4 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIRequest.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIRequest.swift @@ -31,6 +31,8 @@ public struct RequestOptions { let timeout: TimeInterval? /// The API version to use in requests to the backend. + /// + /// TODO(andrewheard): Update to "v1beta" after the Vertex AI in Firebase API launch. let apiVersion = "v2beta" /// Initializes a request options object. diff --git a/FirebaseVertexAI/Sources/GenerativeAIService.swift b/FirebaseVertexAI/Sources/GenerativeAIService.swift index e8f7525d3fd..02995926abb 100644 --- a/FirebaseVertexAI/Sources/GenerativeAIService.swift +++ b/FirebaseVertexAI/Sources/GenerativeAIService.swift @@ -266,8 +266,9 @@ struct GenerativeAIService { // Log specific RPC errors that cannot be mitigated or handled by user code. // These errors do not produce specific GenerateContentError or CountTokensError cases. private func logRPCError(_ error: RPCError) { + // TODO(andrewheard): Remove this check after the Vertex AI in Firebase API launch. if error.isFirebaseMLServiceDisabledError() { - VertexLog.error(code: .firebaseMLAPIDisabled, """ + VertexLog.error(code: .vertexAIInFirebaseAPIDisabled, """ The Vertex AI for Firebase SDK requires the Firebase ML API `firebaseml.googleapis.com` to \ be enabled for your project. Get started in the Firebase Console \ (https://console.firebase.google.com/project/\(projectID)/genai/vertex) or verify that the \ @@ -276,6 +277,15 @@ struct GenerativeAIService { \(projectID)). """) } + + if error.isVertexAIInFirebaseServiceDisabledError() { + VertexLog.error(code: .vertexAIInFirebaseAPIDisabled, """ + The Vertex AI for Firebase SDK requires the Firebase Vertex AI API \ + `firebasevertexai.googleapis.com` to be enabled for your project. Get started by visiting \ + the Firebase Console at: \ + https://console.firebase.google.com/project/\(projectID)/genai/vertex + """) + } } private func parseResponse(_ type: T.Type, from data: Data) throws -> T { diff --git a/FirebaseVertexAI/Sources/VertexLog.swift b/FirebaseVertexAI/Sources/VertexLog.swift index c69d019975f..bd400c200c2 100644 --- a/FirebaseVertexAI/Sources/VertexLog.swift +++ b/FirebaseVertexAI/Sources/VertexLog.swift @@ -29,7 +29,7 @@ enum VertexLog { case verboseLoggingEnabled = 101 // API Enablement Errors - case firebaseMLAPIDisabled = 200 + case vertexAIInFirebaseAPIDisabled = 200 // Model Configuration case generativeModelInitialized = 1000 diff --git a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift index c7cfe36df70..e7bd91dcff1 100644 --- a/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift +++ b/FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift @@ -453,6 +453,7 @@ final class GenerativeModelTests: XCTestCase { } } + // TODO(andrewheard): Remove this test case after the Vertex AI in Firebase API launch. func testGenerateContent_failure_firebaseMLAPINotEnabled() async throws { let expectedStatusCode = 403 MockURLProtocol @@ -476,6 +477,30 @@ final class GenerativeModelTests: XCTestCase { } } + func testGenerateContent_failure_firebaseVertexAIAPINotEnabled() async throws { + let expectedStatusCode = 403 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-firebasevertexai-api-not-enabled", + withExtension: "json", + statusCode: expectedStatusCode + ) + + do { + _ = try await model.generateContent(testPrompt) + XCTFail("Should throw GenerateContentError.internalError; no error thrown.") + } catch let GenerateContentError.internalError(error as RPCError) { + XCTAssertEqual(error.httpResponseCode, expectedStatusCode) + XCTAssertEqual(error.status, .permissionDenied) + XCTAssertTrue(error.message + .starts(with: "Vertex AI in Firebase API has not been used in project")) + XCTAssertTrue(error.isVertexAIInFirebaseServiceDisabledError()) + return + } catch { + XCTFail("Should throw GenerateContentError.internalError(RPCError); error thrown: \(error)") + } + } + func testGenerateContent_failure_emptyContent() async throws { MockURLProtocol .requestHandler = try httpRequestHandler( @@ -774,6 +799,7 @@ final class GenerativeModelTests: XCTestCase { XCTFail("Should have caught an error.") } + // TODO(andrewheard): Remove this test case after the Vertex AI in Firebase API launch. func testGenerateContentStream_failure_firebaseMLAPINotEnabled() async throws { let expectedStatusCode = 403 MockURLProtocol @@ -799,6 +825,32 @@ final class GenerativeModelTests: XCTestCase { XCTFail("Should have caught an error.") } + func testGenerateContentStream_failure_vertexAIInFirebaseAPINotEnabled() async throws { + let expectedStatusCode = 403 + MockURLProtocol + .requestHandler = try httpRequestHandler( + forResource: "unary-failure-firebasevertexai-api-not-enabled", + withExtension: "json", + statusCode: expectedStatusCode + ) + + do { + let stream = try model.generateContentStream(testPrompt) + for try await _ in stream { + XCTFail("No content is there, this shouldn't happen.") + } + } catch let GenerateContentError.internalError(error as RPCError) { + XCTAssertEqual(error.httpResponseCode, expectedStatusCode) + XCTAssertEqual(error.status, .permissionDenied) + XCTAssertTrue(error.message + .starts(with: "Vertex AI in Firebase API has not been used in project")) + XCTAssertTrue(error.isVertexAIInFirebaseServiceDisabledError()) + return + } + + XCTFail("Should have caught an error.") + } + func testGenerateContentStream_failureEmptyContent() async throws { MockURLProtocol .requestHandler = try httpRequestHandler(