Skip to content
Draft
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
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 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');

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),
};
}
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.
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<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', () {
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',
});
});
});
Comment on lines +488 to +508
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);
const imageConfig = ImageConfig(
aspectRatio: ImageAspectRatio.square1x1, imageSize: ImageSize.size1K);
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