Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

extension Constants {
enum Imagen {}
}

extension Constants.Imagen {
static let errorDomain = "\(Constants.baseErrorDomain).Imagen"

enum ErrorCode: Int {
case imagesBlocked = 1000
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ extension ImagenGenerationResponse: Decodable where T: Decodable {
images.append(image)
} else if let filteredReason = try? predictionsContainer.decode(RAIFilteredReason.self) {
filteredReasons.append(filteredReason.raiFilteredReason)
} else if let _ = try? predictionsContainer.decode(JSONObject.self) {
// TODO(#14221): Log unsupported prediction type message with the decoded `JSONObject`.
} else if let unsupportedPrediction = try? predictionsContainer.decode(JSONObject.self) {
VertexLog.warning(
code: .decodedUnsupportedImagenPredictionType,
"Ignoring unsupported Imagen prediction: \(unsupportedPrediction)"
)
} else {
// This should never be thrown since JSONObject accepts any valid JSON.
throw DecodingError.dataCorruptedError(
Expand All @@ -73,11 +76,17 @@ extension ImagenGenerationResponse: Decodable where T: Decodable {

self.images = images
let filteredReason = filteredReasons.joined(separator: "\n")
if filteredReason.isEmpty {
self.filteredReason = nil
} else {
self.filteredReason = filteredReason
self.filteredReason = filteredReason.isEmpty ? nil : filteredReason

guard !images.isEmpty || !filteredReasons.isEmpty else {
throw DecodingError.dataCorruptedError(
forKey: .predictions,
in: container,
debugDescription: "No images or filtered reasons in response."
)
}
guard !images.isEmpty else {
throw ImagenImagesBlockedError(message: filteredReason)
}
// TODO(#14221): Throw `ImagenImagesBlockedError` with `filteredReason` if `images` is empty.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation

/// An error that occurs when image generation fails due to all generated images being blocked.
///
/// The images may have been blocked due to the specified ``ImagenSafetyFilterLevel``, the
/// ``ImagenPersonFilterLevel``, or filtering included in the model. These filter levels may be
/// adjusted in your ``ImagenSafetySettings``. See the [Responsible AI and usage guidelines for
/// Imagen](https://cloud.google.com/vertex-ai/generative-ai/docs/image/responsible-ai-imagen)
/// for more details.
public struct ImagenImagesBlockedError: Error {
/// The reason that all generated images were blocked (filtered out).
let message: String
}

// MARK: - CustomNSError Conformance

extension ImagenImagesBlockedError: CustomNSError {
public static var errorDomain: String {
return Constants.Imagen.errorDomain
}

public var errorCode: Int {
return Constants.Imagen.ErrorCode.imagesBlocked.rawValue
}

public var errorUserInfo: [String: Any] {
return [NSLocalizedDescriptionKey: message]
}
}
1 change: 1 addition & 0 deletions FirebaseVertexAI/Sources/VertexLog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ enum VertexLog {
case decodedInvalidProtoDateDay = 3010
case decodedInvalidCitationPublicationDate = 3011
case generateContentResponseUnrecognizedContentModality = 3012
case decodedUnsupportedImagenPredictionType = 3013

// SDK State Errors
case generateContentResponseNoCandidates = 4000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,17 @@ struct ImagenIntegrationTests {
)
let imagePrompt = "A woman, 35mm portrait, in front of a mountain range"

let response = try await model.generateImages(prompt: imagePrompt)

#expect(response.images.isEmpty)
let filteredReason = try #require(response.filteredReason)
// 39322892: Detects a person or face when it isn't allowed due to the request safety settings.
#expect(filteredReason.contains("39322892"))
// TODO(#14221): Update implementation and test to throw an exception when all filtered out.
await #expect {
try await model.generateImages(prompt: imagePrompt)
} throws: {
let error = try #require($0 as? ImagenImagesBlockedError)
#expect(error.errorCode == 1000) // Constants.Imagen.ErrorCode.imagesBlocked
// 39322892: Detected a person or face when it isn't allowed due to request safety settings.
return error.localizedDescription.contains("39322892")
}
}

// TODO(#14221): Add an integration test for the prompt being blocked.

// TODO(#14221): Add integration tests for validating that Storage Rules are enforced.
// TODO(#14452): Add integration tests for validating that Storage Rules are enforced.
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,13 +168,17 @@ final class ImagenGenerationResponseTests: XCTestCase {
"""
let jsonData = try XCTUnwrap(json.data(using: .utf8))

let response = try decoder.decode(
ImagenGenerationResponse<ImagenInlineImage>.self,
from: jsonData
)

XCTAssertEqual(response.images, [])
XCTAssertEqual(response.filteredReason, raiFilteredReason)
do {
let response = try decoder.decode(
ImagenGenerationResponse<ImagenGCSImage>.self,
from: jsonData
)
XCTFail("Expected a ImagenImagesBlockedError, got response: \(response)")
} catch let error as ImagenImagesBlockedError {
XCTAssertEqual(error.message, raiFilteredReason)
} catch {
XCTFail("Expected an ImagenImagesBlockedError, got error: \(error)")
}
}

func testDecodeResponse_noImagesAnd_noFilteredReason() throws {
Expand Down Expand Up @@ -211,13 +215,17 @@ final class ImagenGenerationResponseTests: XCTestCase {
"""
let jsonData = try XCTUnwrap(json.data(using: .utf8))

let response = try decoder.decode(
ImagenGenerationResponse<ImagenGCSImage>.self,
from: jsonData
)

XCTAssertEqual(response.images, [])
XCTAssertEqual(response.filteredReason, "\(raiFilteredReason1)\n\(raiFilteredReason2)")
do {
let response = try decoder.decode(
ImagenGenerationResponse<ImagenGCSImage>.self,
from: jsonData
)
XCTFail("Expected an ImagenImagesBlockedError, got response: \(response)")
} catch let error as ImagenImagesBlockedError {
XCTAssertEqual(error.message, "\(raiFilteredReason1)\n\(raiFilteredReason2)")
} catch {
XCTFail("Expected an ImagenImagesBlockedError, got error: \(error)")
}
}

func testDecodeResponse_unknownPrediction() throws {
Expand All @@ -232,12 +240,16 @@ final class ImagenGenerationResponseTests: XCTestCase {
"""
let jsonData = try XCTUnwrap(json.data(using: .utf8))

let response = try decoder.decode(
ImagenGenerationResponse<ImagenInlineImage>.self,
from: jsonData
)

XCTAssertEqual(response.images, [])
XCTAssertNil(response.filteredReason)
do {
let response = try decoder.decode(
ImagenGenerationResponse<ImagenGCSImage>.self,
from: jsonData
)
XCTFail("Expected a DecodingError.dataCorrupted, got response: \(response)")
} catch let DecodingError.dataCorrupted(context) {
XCTAssertEqual(context.debugDescription, "No images or filtered reasons in response.")
} catch {
XCTFail("Expected a DecodingError.dataCorrupted, got error: \(error)")
}
}
}
Loading