diff --git a/packages/firebase_ai/firebase_ai/lib/src/api.dart b/packages/firebase_ai/firebase_ai/lib/src/api.dart index 653ef2097013..706739ab1071 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 issues. + 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. + const 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,6 +1405,8 @@ final class GenerationConfig extends BaseGenerationConfig { 'responseJsonSchema': responseJsonSchema, if (thinkingConfig case final thinkingConfig?) 'thinkingConfig': thinkingConfig.toJson(), + if (imageConfig case final imageConfig?) + 'imageConfig': imageConfig.toJson(), }; } diff --git a/packages/firebase_ai/firebase_ai/test/api_test.dart b/packages/firebase_ai/firebase_ai/test/api_test.dart index 156e22dfe154..c251479a1123 100644 --- a/packages/firebase_ai/firebase_ai/test/api_test.dart +++ b/packages/firebase_ai/firebase_ai/test/api_test.dart @@ -315,9 +315,55 @@ 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 +485,34 @@ void main() { }); }); + group('ImageConfig', () { + test('toJson with all fields', () { + const config = ImageConfig( + aspectRatio: ImageAspectRatio.portrait9x16, + imageSize: ImageSize.size2K, + ); + expect(config.toJson(), { + 'aspectRatio': '9:16', + 'imageSize': '2K', + }); + }); + + test('toJson with some fields null', () { + const 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); + const imageConfig = ImageConfig( + aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K); final config = GenerationConfig( candidateCount: 1, stopSequences: ['\n', 'stop'], @@ -455,6 +525,7 @@ void main() { responseMimeType: 'application/json', responseSchema: schema, thinkingConfig: thinkingConfig, + imageConfig: imageConfig, ); expect(config.toJson(), { 'candidateCount': 1, @@ -468,6 +539,10 @@ void main() { 'responseMimeType': 'application/json', 'responseSchema': schema.toJson(), 'thinkingConfig': {'thinkingBudget': 100}, + 'imageConfig': { + 'aspectRatio': '1:1', + 'imageSize': '1K', + }, }); });