Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
26f0784
[AI] Add config for image aspect ratio and size with Nano Banana
andrewheard Mar 10, 2026
9d6081e
Merge remote-tracking branch 'origin/ah/ai-image-config' into pb-imag…
paulb777 Mar 10, 2026
07e45e5
build issues
paulb777 Mar 10, 2026
2005fea
Add `available` annotations
andrewheard Mar 10, 2026
918fcf3
docs, agents, and more tests
paulb777 Mar 10, 2026
879fa39
integration tests
paulb777 Mar 10, 2026
7ac3ae3
Add missing default value `imageSize: ImagenImageSize? = nil`
andrewheard Mar 10, 2026
a1c1006
Fix tests
andrewheard Mar 10, 2026
30405c2
Address TODOs
andrewheard Mar 10, 2026
3e2736a
More build fixes
andrewheard Mar 10, 2026
e88c0de
review
paulb777 Mar 10, 2026
bd09d10
Merge remote-tracking branch 'origin/ah/ai-image-config' into pb-imag…
paulb777 Mar 10, 2026
36068b9
Apply suggestions from code review
paulb777 Mar 10, 2026
eccd2c8
review, style
paulb777 Mar 11, 2026
a663da0
style
paulb777 Mar 11, 2026
5f37627
Replace custom `Encodable` conformance with `EncodableProtoEnum`
andrewheard Mar 11, 2026
36a9828
Check sizes in image generation tests
andrewheard Mar 11, 2026
5ca0094
Add image generation size integration test
andrewheard Mar 11, 2026
bc1d717
Add TODO for Imagen integration test
andrewheard Mar 11, 2026
b8107c3
Add imagen image_size integration test
paulb777 Mar 11, 2026
a433717
Merge remote-tracking branch 'origin/main' into pb-image-config
paulb777 Mar 13, 2026
268800c
post-merge availability cleanup
paulb777 Mar 13, 2026
a01f1c9
remove done todo
paulb777 Mar 13, 2026
5656eca
missed available
paulb777 Mar 13, 2026
2b587fa
Merge branch 'main' into pb-image-config
paulb777 Apr 6, 2026
19b5c28
Revert Imagen API changes
paulb777 Apr 6, 2026
7906f98
remove file
paulb777 Apr 6, 2026
ddd1c6f
Merge branch 'main' into pb-image-config and resolve conflicts in AGE…
paulb777 Apr 13, 2026
ee987f5
Merge branch 'main' into pb-image-config
paulb777 Apr 24, 2026
77cd85d
review
paulb777 Apr 24, 2026
f4c9c70
fix merge botch
paulb777 Apr 24, 2026
ad7d5c7
[AI] Finish Reasons
paulb777 Mar 11, 2026
abefac7
Fix tests
paulb777 Mar 11, 2026
4d8cd07
review and remove unnecessary flaky test
paulb777 Mar 12, 2026
3a69994
review and changelog
paulb777 Mar 12, 2026
7573ed6
finish message
paulb777 Apr 6, 2026
6014d6f
Add back missing changelog entry
daymxn Apr 28, 2026
7343c3f
Merge branch 'main' into pb-finish-reason
daymxn Apr 28, 2026
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
8 changes: 7 additions & 1 deletion 8 FirebaseAI/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
- [fixed] Fixed a `no member 'autoFunctionDeclaration'` compilation error on
unofficially supported Xcode versions older than 26.2. (#16037)
- [fixed] Fixed missing thought summary output in `GenerativeModelSession.streamResponse`. (#16075)
- [fixed] Removed unnecessary log statements related to retrieving text parts during automatic function calling.
Comment thread
daymxn marked this conversation as resolved.
- [fixed] Removed unnecessary log statements related to retrieving text parts during automatic function calling. (#16087, #16122)
- [feature] Adds support for configuring image generation properties,
such as aspect ratio and image size, through the new `ImageConfig` struct
and its integration with `GenerationConfig`. (#15923)
- [feature] Adds support for new `FinishReason` values including
image-related reasons like `imageSafety`, `noImage`, and others for
tool-use, language, and malformed responses. (#15931)

# 12.12.0
- [feature] Added support for automatic function calling in
Expand Down
51 changes: 49 additions & 2 deletions 51 FirebaseAI/Sources/GenerateContentResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ public struct Candidate: Sendable {
/// generated a predefined stop sequence.
public let finishReason: FinishReason?

/// A human-readable description of why the model stopped generating content, if it exists.
public let finishMessage: String?

/// Cited works in the model's response content, if it exists.
public let citationMetadata: CitationMetadata?

Expand All @@ -208,10 +211,11 @@ public struct Candidate: Sendable {
/// Initializer for SwiftUI previews or tests.
public init(content: ModelContent, safetyRatings: [SafetyRating], finishReason: FinishReason?,
citationMetadata: CitationMetadata?, groundingMetadata: GroundingMetadata? = nil,
urlContextMetadata: URLContextMetadata? = nil) {
urlContextMetadata: URLContextMetadata? = nil, finishMessage: String? = nil) {
self.content = content
self.safetyRatings = safetyRatings
self.finishReason = finishReason
self.finishMessage = finishMessage
self.citationMetadata = citationMetadata
self.groundingMetadata = groundingMetadata
self.urlContextMetadata = urlContextMetadata
Expand All @@ -220,7 +224,8 @@ public struct Candidate: Sendable {
// Returns `true` if the candidate contains no information that a developer could use.
var isEmpty: Bool {
content.parts
.isEmpty && finishReason == nil && citationMetadata == nil && groundingMetadata == nil &&
.isEmpty && finishReason == nil && finishMessage == nil && citationMetadata == nil &&
groundingMetadata == nil &&
urlContextMetadata == nil
}
}
Expand Down Expand Up @@ -280,6 +285,16 @@ public struct FinishReason: DecodableProtoEnum, Hashable, Sendable {
case prohibitedContent = "PROHIBITED_CONTENT"
case spii = "SPII"
case malformedFunctionCall = "MALFORMED_FUNCTION_CALL"
case imageSafety = "IMAGE_SAFETY"
case imageProhibitedContent = "IMAGE_PROHIBITED_CONTENT"
case imageOther = "IMAGE_OTHER"
case noImage = "NO_IMAGE"
case imageRecitation = "IMAGE_RECITATION"
case language = "LANGUAGE"
case unexpectedToolCall = "UNEXPECTED_TOOL_CALL"
case tooManyToolCalls = "TOO_MANY_TOOL_CALLS"
case missingThoughtSignature = "MISSING_THOUGHT_SIGNATURE"
case malformedResponse = "MALFORMED_RESPONSE"
}

/// Natural stop point of the model or provided stop sequence.
Expand Down Expand Up @@ -312,6 +327,36 @@ public struct FinishReason: DecodableProtoEnum, Hashable, Sendable {
/// Token generation was stopped because the function call generated by the model was invalid.
public static let malformedFunctionCall = FinishReason(kind: .malformedFunctionCall)

/// Token generation stopped because generated images contain safety violations.
public static let imageSafety = FinishReason(kind: .imageSafety)

/// Image generation stopped because generated images have other prohibited content.
public static let imageProhibitedContent = FinishReason(kind: .imageProhibitedContent)

/// Image generation stopped because of other miscellaneous issue.
public static let imageOther = FinishReason(kind: .imageOther)

/// The model was expected to generate an image, but none was generated.
public static let noImage = FinishReason(kind: .noImage)

/// Image generation stopped due to recitation.
public static let imageRecitation = FinishReason(kind: .imageRecitation)

/// The response candidate content was flagged for using an unsupported language.
public static let language = FinishReason(kind: .language)

/// Model generated a tool call but no tools were enabled in the request.
public static let unexpectedToolCall = FinishReason(kind: .unexpectedToolCall)

/// Model called too many tools consecutively, thus the system exited execution.
public static let tooManyToolCalls = FinishReason(kind: .tooManyToolCalls)

/// Request has at least one thought signature missing.
public static let missingThoughtSignature = FinishReason(kind: .missingThoughtSignature)

/// Finished due to malformed response.
public static let malformedResponse = FinishReason(kind: .malformedResponse)

/// Returns the raw string representation of the `FinishReason` value.
///
/// > Note: This value directly corresponds to the values in the [REST
Expand Down Expand Up @@ -558,6 +603,7 @@ extension Candidate: Decodable {
case content
case safetyRatings
case finishReason
case finishMessage
case citationMetadata
case groundingMetadata
case urlContextMetadata
Expand Down Expand Up @@ -592,6 +638,7 @@ extension Candidate: Decodable {
}

finishReason = try container.decodeIfPresent(FinishReason.self, forKey: .finishReason)
finishMessage = try container.decodeIfPresent(String.self, forKey: .finishMessage)
Comment thread
daymxn marked this conversation as resolved.

citationMetadata = try container.decodeIfPresent(
CitationMetadata.self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,44 @@ struct GenerateContentIntegrationTests {
#endif // canImport(UIKit)
}

@Test(arguments: [
(InstanceConfig.vertexAI_v1beta, ModelNames.gemini2_5_FlashImage),
(InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_FlashImage),
(InstanceConfig.googleAI_v1beta, ModelNames.gemini2_5_FlashImage),
(InstanceConfig.googleAI_v1beta, ModelNames.gemini3_1_FlashImagePreview),
(InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini3_1_FlashImagePreview),
])
func generateContent_finishReason_imageSafety(_ config: InstanceConfig,
modelName: String) async throws {
let generationConfig = GenerationConfig(
responseModalities: [.image]
)
let model = FirebaseAI.componentInstance(config).generativeModel(
modelName: modelName,
generationConfig: generationConfig,
)
let prompt = "A graphic image of violence" // This prompt should trigger safety violation

do {
let response = try await model.generateContent(prompt)

// vertexAI gemini3_1_FlashImagePreview doesn't throw.
let candidate = try #require(response.candidates.first)
#expect(candidate.finishReason == .stop)
} catch {
guard let error = error as? GenerateContentError else {
Issue.record("Expected a \(GenerateContentError.self); got \(error.self).")
throw error
}
guard case let .responseStoppedEarly(reason, response) = error else {
Issue.record("Expected a GenerateContentError.responseStoppedEarly; got \(error.self).")
throw error
}
#expect(reason == .imageSafety || reason == .noImage)
#expect(response.candidates.first?.content.parts.isEmpty == true) // Ensure no content
}
}

@Test(arguments: [
(InstanceConfig.vertexAI_v1beta, ModelNames.gemini2_5_FlashImage),
(InstanceConfig.vertexAI_v1beta_global, ModelNames.gemini2_5_FlashImage),
Expand Down
57 changes: 57 additions & 0 deletions 57 FirebaseAI/Tests/Unit/APITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,61 @@ final class APITests: XCTestCase {
XCTAssertEqual(cacheDetail?.modality, .text)
XCTAssertEqual(cacheDetail?.tokenCount, 50)
}

func testFinishReason_decoding() throws {
let decoder = JSONDecoder()
let testCases: [FinishReason] = [
.language,
.unexpectedToolCall,
.tooManyToolCalls,
.missingThoughtSignature,
.malformedResponse,
.imageSafety,
.imageProhibitedContent,
.imageOther,
.noImage,
.imageRecitation,
]

for expectedReason in testCases {
let reasonString = expectedReason.rawValue
let json = createResponseJSON(finishReason: reasonString)
let response = try decoder.decode(GenerateContentResponse.self, from: json)
XCTAssertEqual(
response.candidates.first?.finishReason,
expectedReason,
"Failed to decode finishReason '\(reasonString)'"
)
}
Comment thread
paulb777 marked this conversation as resolved.
}
Comment thread
paulb777 marked this conversation as resolved.

// Helper function to create JSON for GenerateContentResponse with a specific finish reason
private func createResponseJSON(finishReason: String) -> Data {
return """
{
"candidates": [
{
"content": {
"parts": [
{ "text": "Test reply" }
],
"role": "model"
},
"finishReason": "\(finishReason)",
"index": 0,
"safetyRatings": []
}
],
"usageMetadata": {
"promptTokenCount": 10,
"cachedContentTokenCount": 0,
"candidatesTokenCount": 5,
"totalTokenCount": 15,
"promptTokensDetails": [],
"cacheTokensDetails": [],
"candidatesTokensDetails": []
}
}
""".data(using: .utf8)!
}
}
33 changes: 33 additions & 0 deletions 33 FirebaseAI/Tests/Unit/Types/GenerateContentResponseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,22 @@ final class GenerateContentResponseTests: XCTestCase {
XCTAssertEqual(candidate.finishReason, .stop)
}

func testDecodeCandidate_withFinishMessage() throws {
let json = """
{
"content": { "role": "model", "parts": [ { "text": "Some text." } ] },
"finishReason": "STOP",
"finishMessage": "Stopped due to something."
}
"""
let jsonData = try XCTUnwrap(json.data(using: .utf8))

let candidate = try jsonDecoder.decode(Candidate.self, from: jsonData)

XCTAssertEqual(candidate.finishMessage, "Stopped due to something.")
XCTAssertEqual(candidate.finishReason, .stop)
}

// MARK: - Candidate.isEmpty

func testCandidateIsEmpty_allEmpty_isTrue() throws {
Expand Down Expand Up @@ -287,4 +303,21 @@ final class GenerateContentResponseTests: XCTestCase {
"A candidate with only `urlContextMetadata` should not be empty."
)
}

func testCandidateIsEmpty_withFinishMessage_isFalse() throws {
let candidate = Candidate(
content: ModelContent(parts: []),
safetyRatings: [],
finishReason: nil,
citationMetadata: nil,
groundingMetadata: nil,
urlContextMetadata: nil,
finishMessage: "Stopped"
)

XCTAssertFalse(
candidate.isEmpty,
"A candidate with only `finishMessage` should not be empty."
)
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.