From 332475f403e5b2b6db0c16bd1834704874a1d864 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 7 Apr 2026 16:04:55 -0700 Subject: [PATCH 1/5] [AI] Add ImageConfig and expand FinishReasons --- .../firebase_ai/firebase_ai/lib/src/api.dart | 150 ++++++++++++++++++ .../firebase_ai/test/api_test.dart | 64 ++++++++ 2 files changed, 214 insertions(+) diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index 653ef2097013..aee18a835f7d 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -770,6 +770,45 @@ enum FinishReason { /// The candidate content was flagged for malformed function call reasons. malformedFunctionCall('MALFORMED_FUNCTION_CALL'), + /// Token generation was stopped because the response contained forbidden terms. + blocklist('BLOCKLIST'), + + /// Token generation was stopped because the response contained potentially prohibited content. + prohibitedContent('PROHIBITED_CONTENT'), + + /// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII). + spii('SPII'), + + /// Token generation stopped because generated images contain safety violations. + imageSafety('IMAGE_SAFETY'), + + /// Image generation stopped because generated images have other prohibited content. + imageProhibitedContent('IMAGE_PROHIBITED_CONTENT'), + + /// Image generation stopped because of other miscellaneous issue. + imageOther('IMAGE_OTHER'), + + /// The model was expected to generate an image, but none was generated. + noImage('NO_IMAGE'), + + /// Image generation stopped due to recitation. + imageRecitation('IMAGE_RECITATION'), + + /// The response candidate content was flagged for using an unsupported language. + language('LANGUAGE'), + + /// Model generated a tool call but no tools were enabled in the request. + unexpectedToolCall('UNEXPECTED_TOOL_CALL'), + + /// Model called too many tools consecutively, thus the system exited execution. + tooManyToolCalls('TOO_MANY_TOOL_CALLS'), + + /// Request has at least one thought signature missing. + missingThoughtSignature('MISSING_THOUGHT_SIGNATURE'), + + /// Finished due to malformed response. + malformedResponse('MALFORMED_RESPONSE'), + /// Unknown reason. other('OTHER'); @@ -790,6 +829,19 @@ enum FinishReason { 'RECITATION' => FinishReason.recitation, 'OTHER' => FinishReason.other, 'MALFORMED_FUNCTION_CALL' => FinishReason.malformedFunctionCall, + 'BLOCKLIST' => FinishReason.blocklist, + 'PROHIBITED_CONTENT' => FinishReason.prohibitedContent, + 'SPII' => FinishReason.spii, + 'IMAGE_SAFETY' => FinishReason.imageSafety, + 'IMAGE_PROHIBITED_CONTENT' => FinishReason.imageProhibitedContent, + 'IMAGE_OTHER' => FinishReason.imageOther, + 'NO_IMAGE' => FinishReason.noImage, + 'IMAGE_RECITATION' => FinishReason.imageRecitation, + 'LANGUAGE' => FinishReason.language, + 'UNEXPECTED_TOOL_CALL' => FinishReason.unexpectedToolCall, + 'TOO_MANY_TOOL_CALLS' => FinishReason.tooManyToolCalls, + 'MISSING_THOUGHT_SIGNATURE' => FinishReason.missingThoughtSignature, + 'MALFORMED_RESPONSE' => FinishReason.malformedResponse, _ => throw FormatException('Unhandled FinishReason format', jsonObject), }; } @@ -1069,6 +1121,97 @@ class ThinkingConfig { }; } +/// Configuration options for generating images with Gemini models. +final class ImageConfig { + /// Initializes configuration options for generating images with Gemini. + ImageConfig({this.aspectRatio, this.imageSize}); + + /// The aspect ratio of generated images. + final ImageAspectRatio? aspectRatio; + + /// The size of the generated images. + final ImageSize? imageSize; + + /// Convert to json format. + Map toJson() => { + if (aspectRatio case final aspectRatio?) + 'aspectRatio': aspectRatio.toJson(), + if (imageSize case final imageSize?) 'imageSize': imageSize.toJson(), + }; +} + +/// An aspect ratio for generated images. +enum ImageAspectRatio { + /// Square (1:1) aspect ratio. + square1x1('1:1'), + + /// Portrait widescreen (9:16) aspect ratio. + portrait9x16('9:16'), + + /// Widescreen (16:9) aspect ratio. + landscape16x9('16:9'), + + /// Portrait full screen (3:4) aspect ratio. + portrait3x4('3:4'), + + /// Fullscreen (4:3) aspect ratio. + landscape4x3('4:3'), + + /// Portrait (2:3) aspect ratio. + portrait2x3('2:3'), + + /// Landscape (3:2) aspect ratio. + landscape3x2('3:2'), + + /// Portrait (4:5) aspect ratio. + portrait4x5('4:5'), + + /// Landscape (5:4) aspect ratio. + landscape5x4('5:4'), + + /// Portrait (1:4) aspect ratio. + portrait1x4('1:4'), + + /// Landscape (4:1) aspect ratio. + landscape4x1('4:1'), + + /// Portrait (1:8) aspect ratio. + portrait1x8('1:8'), + + /// Landscape (8:1) aspect ratio. + landscape8x1('8:1'), + + /// Ultrawide (21:9) aspect ratio. + ultrawide21x9('21:9'); + + const ImageAspectRatio(this._jsonString); + final String _jsonString; + + /// Convert to json format. + String toJson() => _jsonString; +} + +/// The size of images to generate. +enum ImageSize { + /// 512px (0.5K) image size. + size512('512'), + + /// 1K image size. + size1K('1K'), + + /// 2K image size. + size2K('2K'), + + /// 4K image size. + size4K('4K'); + + const ImageSize(this._jsonString); + final String _jsonString; + + /// Convert to json format. + String toJson() => _jsonString; +} + /// Configuration options for model generation and outputs. abstract class BaseGenerationConfig { // ignore: public_member_api_docs @@ -1196,6 +1339,7 @@ final class GenerationConfig extends BaseGenerationConfig { this.responseSchema, this.responseJsonSchema, this.thinkingConfig, + this.imageConfig, }) : assert(responseSchema == null || responseJsonSchema == null, 'responseSchema and responseJsonSchema cannot both be set.'); @@ -1244,6 +1388,9 @@ final class GenerationConfig extends BaseGenerationConfig { /// support thinking. final ThinkingConfig? thinkingConfig; + /// Configuration options for generating images with Gemini models. + final ImageConfig? imageConfig; + @override Map toJson() => { ...super.toJson(), @@ -1258,8 +1405,11 @@ final class GenerationConfig extends BaseGenerationConfig { 'responseJsonSchema': responseJsonSchema, if (thinkingConfig case final thinkingConfig?) 'thinkingConfig': thinkingConfig.toJson(), + if (imageConfig case final imageConfig?) + 'imageConfig': imageConfig.toJson(), }; } +} /// Type of task for which the embedding will be used. enum TaskType { diff --git a/packages/firebase_ai/firebase_ai/test/api_test.dart b/packages/firebase_ai/firebase_ai/test/api_test.dart index 156e22dfe154..3bb70045f13a 100644 --- a/packages/firebase_ai/firebase_ai/test/api_test.dart +++ b/packages/firebase_ai/firebase_ai/test/api_test.dart @@ -315,9 +315,45 @@ void main() { expect(FinishReason.recitation.toJson(), 'RECITATION'); expect(FinishReason.malformedFunctionCall.toJson(), 'MALFORMED_FUNCTION_CALL'); + expect(FinishReason.blocklist.toJson(), 'BLOCKLIST'); + expect(FinishReason.prohibitedContent.toJson(), 'PROHIBITED_CONTENT'); + expect(FinishReason.spii.toJson(), 'SPII'); + expect(FinishReason.imageSafety.toJson(), 'IMAGE_SAFETY'); + expect(FinishReason.imageProhibitedContent.toJson(), 'IMAGE_PROHIBITED_CONTENT'); + expect(FinishReason.imageOther.toJson(), 'IMAGE_OTHER'); + expect(FinishReason.noImage.toJson(), 'NO_IMAGE'); + expect(FinishReason.imageRecitation.toJson(), 'IMAGE_RECITATION'); + expect(FinishReason.language.toJson(), 'LANGUAGE'); + expect(FinishReason.unexpectedToolCall.toJson(), 'UNEXPECTED_TOOL_CALL'); + expect(FinishReason.tooManyToolCalls.toJson(), 'TOO_MANY_TOOL_CALLS'); + expect(FinishReason.missingThoughtSignature.toJson(), 'MISSING_THOUGHT_SIGNATURE'); + expect(FinishReason.malformedResponse.toJson(), 'MALFORMED_RESPONSE'); expect(FinishReason.other.toJson(), 'OTHER'); }); + test('FinishReason parseValue', () { + expect(FinishReason.parseValue('STOP'), FinishReason.stop); + expect(FinishReason.parseValue('MAX_TOKENS'), FinishReason.maxTokens); + expect(FinishReason.parseValue('SAFETY'), FinishReason.safety); + expect(FinishReason.parseValue('RECITATION'), FinishReason.recitation); + expect(FinishReason.parseValue('MALFORMED_FUNCTION_CALL'), FinishReason.malformedFunctionCall); + expect(FinishReason.parseValue('BLOCKLIST'), FinishReason.blocklist); + expect(FinishReason.parseValue('PROHIBITED_CONTENT'), FinishReason.prohibitedContent); + expect(FinishReason.parseValue('SPII'), FinishReason.spii); + expect(FinishReason.parseValue('IMAGE_SAFETY'), FinishReason.imageSafety); + expect(FinishReason.parseValue('IMAGE_PROHIBITED_CONTENT'), FinishReason.imageProhibitedContent); + expect(FinishReason.parseValue('IMAGE_OTHER'), FinishReason.imageOther); + expect(FinishReason.parseValue('NO_IMAGE'), FinishReason.noImage); + expect(FinishReason.parseValue('IMAGE_RECITATION'), FinishReason.imageRecitation); + expect(FinishReason.parseValue('LANGUAGE'), FinishReason.language); + expect(FinishReason.parseValue('UNEXPECTED_TOOL_CALL'), FinishReason.unexpectedToolCall); + expect(FinishReason.parseValue('TOO_MANY_TOOL_CALLS'), FinishReason.tooManyToolCalls); + expect(FinishReason.parseValue('MISSING_THOUGHT_SIGNATURE'), FinishReason.missingThoughtSignature); + expect(FinishReason.parseValue('MALFORMED_RESPONSE'), FinishReason.malformedResponse); + expect(FinishReason.parseValue('OTHER'), FinishReason.other); + expect(FinishReason.parseValue('UNSPECIFIED'), FinishReason.unknown); + }); + test('ContentModality toJson and toString', () { expect(ContentModality.unspecified.toJson(), 'MODALITY_UNSPECIFIED'); expect(ContentModality.text.toJson(), 'TEXT'); @@ -439,10 +475,33 @@ void main() { }); }); + group('ImageConfig', () { + test('toJson with all fields', () { + final config = ImageConfig( + aspectRatio: ImageAspectRatio.portrait9x16, + imageSize: ImageSize.size2K, + ); + expect(config.toJson(), { + 'aspectRatio': '9:16', + 'imageSize': '2K', + }); + }); + + test('toJson with some fields null', () { + final config = ImageConfig( + aspectRatio: ImageAspectRatio.landscape16x9, + ); + expect(config.toJson(), { + 'aspectRatio': '16:9', + }); + }); + }); + group('GenerationConfig & BaseGenerationConfig', () { test('GenerationConfig toJson with all fields', () { final schema = Schema.object(properties: {}); final thinkingConfig = ThinkingConfig(thinkingBudget: 100); + final imageConfig = ImageConfig(aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K); final config = GenerationConfig( candidateCount: 1, stopSequences: ['\n', 'stop'], @@ -455,6 +514,7 @@ void main() { responseMimeType: 'application/json', responseSchema: schema, thinkingConfig: thinkingConfig, + imageConfig: imageConfig, ); expect(config.toJson(), { 'candidateCount': 1, @@ -468,6 +528,10 @@ void main() { 'responseMimeType': 'application/json', 'responseSchema': schema.toJson(), 'thinkingConfig': {'thinkingBudget': 100}, + 'imageConfig': { + 'aspectRatio': '1:1', + 'imageSize': '1K', + }, }); }); From ed5379bc7d0699f48b962a752afdf0c8829d803b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 7 Apr 2026 16:10:41 -0700 Subject: [PATCH 2/5] style: Fix formatting in api_test.dart --- .../firebase_ai/test/api_test.dart | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/test/api_test.dart b/packages/firebase_ai/firebase_ai/test/api_test.dart index 3bb70045f13a..a5a50d47352c 100644 --- a/packages/firebase_ai/firebase_ai/test/api_test.dart +++ b/packages/firebase_ai/firebase_ai/test/api_test.dart @@ -319,14 +319,16 @@ void main() { expect(FinishReason.prohibitedContent.toJson(), 'PROHIBITED_CONTENT'); expect(FinishReason.spii.toJson(), 'SPII'); expect(FinishReason.imageSafety.toJson(), 'IMAGE_SAFETY'); - expect(FinishReason.imageProhibitedContent.toJson(), 'IMAGE_PROHIBITED_CONTENT'); + expect(FinishReason.imageProhibitedContent.toJson(), + 'IMAGE_PROHIBITED_CONTENT'); expect(FinishReason.imageOther.toJson(), 'IMAGE_OTHER'); expect(FinishReason.noImage.toJson(), 'NO_IMAGE'); expect(FinishReason.imageRecitation.toJson(), 'IMAGE_RECITATION'); expect(FinishReason.language.toJson(), 'LANGUAGE'); expect(FinishReason.unexpectedToolCall.toJson(), 'UNEXPECTED_TOOL_CALL'); expect(FinishReason.tooManyToolCalls.toJson(), 'TOO_MANY_TOOL_CALLS'); - expect(FinishReason.missingThoughtSignature.toJson(), 'MISSING_THOUGHT_SIGNATURE'); + expect(FinishReason.missingThoughtSignature.toJson(), + 'MISSING_THOUGHT_SIGNATURE'); expect(FinishReason.malformedResponse.toJson(), 'MALFORMED_RESPONSE'); expect(FinishReason.other.toJson(), 'OTHER'); }); @@ -336,20 +338,28 @@ void main() { expect(FinishReason.parseValue('MAX_TOKENS'), FinishReason.maxTokens); expect(FinishReason.parseValue('SAFETY'), FinishReason.safety); expect(FinishReason.parseValue('RECITATION'), FinishReason.recitation); - expect(FinishReason.parseValue('MALFORMED_FUNCTION_CALL'), FinishReason.malformedFunctionCall); + expect(FinishReason.parseValue('MALFORMED_FUNCTION_CALL'), + FinishReason.malformedFunctionCall); expect(FinishReason.parseValue('BLOCKLIST'), FinishReason.blocklist); - expect(FinishReason.parseValue('PROHIBITED_CONTENT'), FinishReason.prohibitedContent); + expect(FinishReason.parseValue('PROHIBITED_CONTENT'), + FinishReason.prohibitedContent); expect(FinishReason.parseValue('SPII'), FinishReason.spii); expect(FinishReason.parseValue('IMAGE_SAFETY'), FinishReason.imageSafety); - expect(FinishReason.parseValue('IMAGE_PROHIBITED_CONTENT'), FinishReason.imageProhibitedContent); + expect(FinishReason.parseValue('IMAGE_PROHIBITED_CONTENT'), + FinishReason.imageProhibitedContent); expect(FinishReason.parseValue('IMAGE_OTHER'), FinishReason.imageOther); expect(FinishReason.parseValue('NO_IMAGE'), FinishReason.noImage); - expect(FinishReason.parseValue('IMAGE_RECITATION'), FinishReason.imageRecitation); + expect(FinishReason.parseValue('IMAGE_RECITATION'), + FinishReason.imageRecitation); expect(FinishReason.parseValue('LANGUAGE'), FinishReason.language); - expect(FinishReason.parseValue('UNEXPECTED_TOOL_CALL'), FinishReason.unexpectedToolCall); - expect(FinishReason.parseValue('TOO_MANY_TOOL_CALLS'), FinishReason.tooManyToolCalls); - expect(FinishReason.parseValue('MISSING_THOUGHT_SIGNATURE'), FinishReason.missingThoughtSignature); - expect(FinishReason.parseValue('MALFORMED_RESPONSE'), FinishReason.malformedResponse); + expect(FinishReason.parseValue('UNEXPECTED_TOOL_CALL'), + FinishReason.unexpectedToolCall); + expect(FinishReason.parseValue('TOO_MANY_TOOL_CALLS'), + FinishReason.tooManyToolCalls); + expect(FinishReason.parseValue('MISSING_THOUGHT_SIGNATURE'), + FinishReason.missingThoughtSignature); + expect(FinishReason.parseValue('MALFORMED_RESPONSE'), + FinishReason.malformedResponse); expect(FinishReason.parseValue('OTHER'), FinishReason.other); expect(FinishReason.parseValue('UNSPECIFIED'), FinishReason.unknown); }); @@ -501,7 +511,8 @@ void main() { test('GenerationConfig toJson with all fields', () { final schema = Schema.object(properties: {}); final thinkingConfig = ThinkingConfig(thinkingBudget: 100); - final imageConfig = ImageConfig(aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K); + final imageConfig = ImageConfig( + aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K); final config = GenerationConfig( candidateCount: 1, stopSequences: ['\n', 'stop'], From 63c7faacc4e0d8a33d962e1ec43c0435cc1cde51 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 7 Apr 2026 16:21:08 -0700 Subject: [PATCH 3/5] fix(ai): Fix syntax error in api.dart --- packages/firebase_ai/firebase_ai/lib/src/api.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index aee18a835f7d..73aacfeadf1f 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -1409,7 +1409,6 @@ final class GenerationConfig extends BaseGenerationConfig { 'imageConfig': imageConfig.toJson(), }; } -} /// Type of task for which the embedding will be used. enum TaskType { From e1000111e9c7dcc7f83b60899641faec1f7d7c65 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 7 Apr 2026 20:15:01 -0700 Subject: [PATCH 4/5] chore(ai): address code review comments in Flutter SDK --- packages/firebase_ai/firebase_ai/lib/src/api.dart | 2 +- packages/firebase_ai/firebase_ai/test/api_test.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index 73aacfeadf1f..c69fdbbe551f 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -1124,7 +1124,7 @@ class ThinkingConfig { /// Configuration options for generating images with Gemini models. final class ImageConfig { /// Initializes configuration options for generating images with Gemini. - ImageConfig({this.aspectRatio, this.imageSize}); + const ImageConfig({this.aspectRatio, this.imageSize}); /// The aspect ratio of generated images. final ImageAspectRatio? aspectRatio; diff --git a/packages/firebase_ai/firebase_ai/test/api_test.dart b/packages/firebase_ai/firebase_ai/test/api_test.dart index a5a50d47352c..c251479a1123 100644 --- a/packages/firebase_ai/firebase_ai/test/api_test.dart +++ b/packages/firebase_ai/firebase_ai/test/api_test.dart @@ -487,7 +487,7 @@ void main() { group('ImageConfig', () { test('toJson with all fields', () { - final config = ImageConfig( + const config = ImageConfig( aspectRatio: ImageAspectRatio.portrait9x16, imageSize: ImageSize.size2K, ); @@ -498,7 +498,7 @@ void main() { }); test('toJson with some fields null', () { - final config = ImageConfig( + const config = ImageConfig( aspectRatio: ImageAspectRatio.landscape16x9, ); expect(config.toJson(), { @@ -511,7 +511,7 @@ void main() { test('GenerationConfig toJson with all fields', () { final schema = Schema.object(properties: {}); final thinkingConfig = ThinkingConfig(thinkingBudget: 100); - final imageConfig = ImageConfig( + const imageConfig = ImageConfig( aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K); final config = GenerationConfig( candidateCount: 1, From 00e35fa224dcfa1ba9e22f82829eb4018232259b Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 7 Apr 2026 20:32:18 -0700 Subject: [PATCH 5/5] fix(ai): fix typo in documentation in Flutter SDK --- packages/firebase_ai/firebase_ai/lib/src/api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index c69fdbbe551f..706739ab1071 100644 --- a/packages/firebase_ai/firebase_ai/lib/src/api.dart +++ b/packages/firebase_ai/firebase_ai/lib/src/api.dart @@ -785,7 +785,7 @@ enum FinishReason { /// Image generation stopped because generated images have other prohibited content. imageProhibitedContent('IMAGE_PROHIBITED_CONTENT'), - /// Image generation stopped because of other miscellaneous issue. + /// Image generation stopped because of other miscellaneous issues. imageOther('IMAGE_OTHER'), /// The model was expected to generate an image, but none was generated.