Skip to content
149 changes: 149 additions & 0 deletions packages/firebase_ai/firebase_ai/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
paulb777 marked this conversation as resolved.
Outdated
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');

Expand All @@ -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),
Comment thread
paulb777 marked this conversation as resolved.
Outdated
};
}
Expand Down Expand Up @@ -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});
Comment thread
paulb777 marked this conversation as resolved.
Outdated

/// The aspect ratio of generated images.
final ImageAspectRatio? aspectRatio;

/// The size of the generated images.
final ImageSize? imageSize;

/// Convert to json format.
Map<String, Object?> 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
Expand Down Expand Up @@ -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.');

Expand Down Expand Up @@ -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<String, Object?> toJson() => {
...super.toJson(),
Expand All @@ -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(),
};
}

Expand Down
75 changes: 75 additions & 0 deletions packages/firebase_ai/firebase_ai/test/api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -439,10 +485,34 @@ void main() {
});
});

group('ImageConfig', () {
test('toJson with all fields', () {
final config = ImageConfig(
Comment thread
paulb777 marked this conversation as resolved.
Outdated
aspectRatio: ImageAspectRatio.portrait9x16,
imageSize: ImageSize.size2K,
);
expect(config.toJson(), {
'aspectRatio': '9:16',
'imageSize': '2K',
});
});

test('toJson with some fields null', () {
final config = ImageConfig(
Comment thread
paulb777 marked this conversation as resolved.
Outdated
aspectRatio: ImageAspectRatio.landscape16x9,
);
expect(config.toJson(), {
'aspectRatio': '16:9',
});
});
});
Comment on lines +489 to +509
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

The repository style guide (line 69) recommends using package:checks for assertions instead of package:test's expect. While the existing tests use expect, new tests should ideally follow the updated guidelines.

References
  1. Use package:checks for assertions. (link)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping expect to be consistent with rest of file.


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);
Comment thread
paulb777 marked this conversation as resolved.
Outdated
final config = GenerationConfig(
candidateCount: 1,
stopSequences: ['\n', 'stop'],
Expand All @@ -455,6 +525,7 @@ void main() {
responseMimeType: 'application/json',
responseSchema: schema,
thinkingConfig: thinkingConfig,
imageConfig: imageConfig,
);
expect(config.toJson(), {
'candidateCount': 1,
Expand All @@ -468,6 +539,10 @@ void main() {
'responseMimeType': 'application/json',
'responseSchema': schema.toJson(),
'thinkingConfig': {'thinkingBudget': 100},
'imageConfig': {
'aspectRatio': '1:1',
'imageSize': '1K',
},
});
});

Expand Down
Loading