Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Vertex AI] Add error message for Firebase ML API not enabled #13007

Merged
merged 2 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/vertexai.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ jobs:
- os: macos-14
xcode: Xcode_15.2
runs-on: ${{ matrix.os }}
env:
FIREBASECI_USE_LATEST_GOOGLEAPPMEASUREMENT: 1
steps:
- uses: actions/checkout@v4
- name: Xcode
Expand Down
22 changes: 22 additions & 0 deletions FirebaseVertexAI/Sources/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ struct RPCError: Error {
self.status = status
self.details = details
}

func isFirebaseMLServiceDisabledError() -> Bool {
return details.contains { $0.isFirebaseMLServiceDisabledErrorDetails() }
}
}

extension RPCError: Decodable {
Expand Down Expand Up @@ -76,17 +80,35 @@ struct ErrorDetails {
let type: String
let reason: String?
let domain: String?
let metadata: [String: String]?

func isErrorInfo() -> Bool {
return type == ErrorDetails.errorInfoType
}

func isFirebaseMLServiceDisabledErrorDetails() -> Bool {
guard isErrorInfo() else {
return false
}
guard reason == "SERVICE_DISABLED" else {
return false
}
guard domain == "googleapis.com" else {
return false
}
guard let metadata, metadata["service"] == "firebaseml.googleapis.com" else {
return false
}
return true
}
}

extension ErrorDetails: Decodable, Equatable {
enum CodingKeys: String, CodingKey {
case type = "@type"
case reason
case domain
case metadata
}
}

Expand Down
25 changes: 23 additions & 2 deletions FirebaseVertexAI/Sources/GenerativeAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ struct GenerativeAIService {
/// The Firebase SDK version in the format `fire/<version>`.
static let firebaseVersionTag = "fire/\(FirebaseVersion())"

private let projectID: String

/// Gives permission to talk to the backend.
private let apiKey: String

Expand All @@ -34,7 +36,9 @@ struct GenerativeAIService {

private let urlSession: URLSession

init(apiKey: String, appCheck: AppCheckInterop?, auth: AuthInterop?, urlSession: URLSession) {
init(projectID: String, apiKey: String, appCheck: AppCheckInterop?, auth: AuthInterop?,
urlSession: URLSession) {
self.projectID = projectID
self.apiKey = apiKey
self.appCheck = appCheck
self.auth = auth
Expand Down Expand Up @@ -236,13 +240,30 @@ struct GenerativeAIService {

private func parseError(responseData: Data) -> Error {
do {
return try JSONDecoder().decode(RPCError.self, from: responseData)
let rpcError = try JSONDecoder().decode(RPCError.self, from: responseData)
logRPCError(rpcError)
return rpcError
} catch {
// TODO: Return an error about an unrecognized error payload with the response body
return error
}
}

// 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) {
if error.isFirebaseMLServiceDisabledError() {
Logging.default.error("""
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 \
API is enabled in the Google Cloud Console \
(https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview?project=\
\(projectID)).
""")
}
}

private func parseResponse<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
do {
return try JSONDecoder().decode(type, from: data)
Expand Down
3 changes: 3 additions & 0 deletions FirebaseVertexAI/Sources/GenerativeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public final class GenerativeModel {
///
/// - Parameters:
/// - name: The name of the model to use, for example `"gemini-1.0-pro"`.
/// - projectID: The project ID from the Firebase console.
/// - apiKey: The API key for your project.
/// - generationConfig: The content generation parameters your model should use.
/// - safetySettings: A value describing what types of harmful content your model should allow.
Expand All @@ -61,6 +62,7 @@ public final class GenerativeModel {
/// - requestOptions: Configuration parameters for sending requests to the backend.
/// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`.
init(name: String,
projectID: String,
apiKey: String,
generationConfig: GenerationConfig? = nil,
safetySettings: [SafetySetting]? = nil,
Expand All @@ -73,6 +75,7 @@ public final class GenerativeModel {
urlSession: URLSession = .shared) {
modelResourceName = GenerativeModel.modelResourceName(name: name)
generativeAIService = GenerativeAIService(
projectID: projectID,
apiKey: apiKey,
appCheck: appCheck,
auth: auth,
Expand Down
16 changes: 11 additions & 5 deletions FirebaseVertexAI/Sources/VertexAI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,23 @@ public class VertexAI: NSObject {
systemInstruction: ModelContent? = nil,
requestOptions: RequestOptions = RequestOptions())
-> GenerativeModel {
let modelResourceName = modelResourceName(modelName: modelName, location: location)
guard let projectID = app.options.projectID else {
fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.")
}

let modelResourceName = modelResourceName(
modelName: modelName,
projectID: projectID,
location: location
)

guard let apiKey = app.options.apiKey else {
fatalError("The Firebase app named \"\(app.name)\" has no API key in its configuration.")
}

return GenerativeModel(
name: modelResourceName,
projectID: projectID,
apiKey: apiKey,
generationConfig: generationConfig,
safetySettings: safetySettings,
Expand Down Expand Up @@ -121,10 +130,7 @@ public class VertexAI: NSObject {
auth = ComponentType<AuthInterop>.instance(for: AuthInterop.self, in: app.container)
}

private func modelResourceName(modelName: String, location: String) -> String {
guard let projectID = app.options.projectID else {
fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.")
}
private func modelResourceName(modelName: String, projectID: String, location: String) -> String {
guard !modelName.isEmpty && modelName
.allSatisfy({ !$0.isWhitespace && !$0.isNewline && $0 != "/" }) else {
fatalError("""
Expand Down
1 change: 1 addition & 0 deletions FirebaseVertexAI/Tests/Unit/ChatTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ final class ChatTests: XCTestCase {

let model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"error": {
"code": 403,
"message": "Firebase ML API has not been used in project 1234567890 before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview?project=1234567890 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.",
"status": "PERMISSION_DENIED",
"details": [
{
"@type": "type.googleapis.com/google.rpc.Help",
"links": [
{
"description": "Google developers console API activation",
"url": "https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview?project=1234567890"
}
]
},
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "SERVICE_DISABLED",
"domain": "googleapis.com",
"metadata": {
"service": "firebaseml.googleapis.com",
"consumer": "projects/1234567890"
}
}
]
}
}
63 changes: 63 additions & 0 deletions FirebaseVertexAI/Tests/Unit/GenerativeModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ final class GenerativeModelTests: XCTestCase {
urlSession = try XCTUnwrap(URLSession(configuration: configuration))
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down Expand Up @@ -180,6 +181,7 @@ final class GenerativeModelTests: XCTestCase {
let model = GenerativeModel(
// Model name is prefixed with "models/".
name: "models/test-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down Expand Up @@ -299,6 +301,7 @@ final class GenerativeModelTests: XCTestCase {
let appCheckToken = "test-valid-token"
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -319,6 +322,7 @@ final class GenerativeModelTests: XCTestCase {
func testGenerateContent_appCheck_tokenRefreshError() async throws {
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -340,6 +344,7 @@ final class GenerativeModelTests: XCTestCase {
let authToken = "test-valid-token"
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -360,6 +365,7 @@ final class GenerativeModelTests: XCTestCase {
func testGenerateContent_auth_nilAuthToken() async throws {
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -380,6 +386,7 @@ final class GenerativeModelTests: XCTestCase {
func testGenerateContent_auth_authTokenRefreshError() async throws {
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down Expand Up @@ -441,6 +448,29 @@ final class GenerativeModelTests: XCTestCase {
}
}

func testGenerateContent_failure_firebaseMLAPINotEnabled() async throws {
let expectedStatusCode = 403
MockURLProtocol
.requestHandler = try httpRequestHandler(
forResource: "unary-failure-firebaseml-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: "Firebase ML API has not been used in project"))
XCTAssertTrue(error.isFirebaseMLServiceDisabledError())
return
} catch {
XCTFail("Should throw GenerateContentError.internalError(RPCError); error thrown: \(error)")
}
}

func testGenerateContent_failure_emptyContent() async throws {
MockURLProtocol
.requestHandler = try httpRequestHandler(
Expand Down Expand Up @@ -701,6 +731,7 @@ final class GenerativeModelTests: XCTestCase {
let requestOptions = RequestOptions(timeout: expectedTimeout)
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: requestOptions,
Expand Down Expand Up @@ -738,6 +769,31 @@ final class GenerativeModelTests: XCTestCase {
XCTFail("Should have caught an error.")
}

func testGenerateContentStream_failure_firebaseMLAPINotEnabled() async throws {
let expectedStatusCode = 403
MockURLProtocol
.requestHandler = try httpRequestHandler(
forResource: "unary-failure-firebaseml-api-not-enabled",
withExtension: "json",
statusCode: expectedStatusCode
)

do {
let stream = 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: "Firebase ML API has not been used in project"))
XCTAssertTrue(error.isFirebaseMLServiceDisabledError())
return
}

XCTFail("Should have caught an error.")
}

func testGenerateContentStream_failureEmptyContent() async throws {
MockURLProtocol
.requestHandler = try httpRequestHandler(
Expand Down Expand Up @@ -912,6 +968,7 @@ final class GenerativeModelTests: XCTestCase {
let appCheckToken = "test-valid-token"
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -933,6 +990,7 @@ final class GenerativeModelTests: XCTestCase {
func testGenerateContentStream_appCheck_tokenRefreshError() async throws {
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down Expand Up @@ -1078,6 +1136,7 @@ final class GenerativeModelTests: XCTestCase {
let requestOptions = RequestOptions(timeout: expectedTimeout)
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: requestOptions,
Expand Down Expand Up @@ -1155,6 +1214,7 @@ final class GenerativeModelTests: XCTestCase {
let requestOptions = RequestOptions(timeout: expectedTimeout)
model = GenerativeModel(
name: "my-model",
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: requestOptions,
Expand All @@ -1176,6 +1236,7 @@ final class GenerativeModelTests: XCTestCase {

model = GenerativeModel(
name: modelName,
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -1191,6 +1252,7 @@ final class GenerativeModelTests: XCTestCase {

model = GenerativeModel(
name: modelResourceName,
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand All @@ -1206,6 +1268,7 @@ final class GenerativeModelTests: XCTestCase {

model = GenerativeModel(
name: tunedModelResourceName,
projectID: "my-project-id",
apiKey: "API_KEY",
tools: nil,
requestOptions: RequestOptions(),
Expand Down
Loading