Skip to content

Commit 7d1a8b1

Browse files
feat(firebaseai): ImageConfig and FinishReasons (#18180)
* [AI] Add ImageConfig and expand FinishReasons * style: Fix formatting in api_test.dart * fix(ai): Fix syntax error in api.dart * chore(ai): address code review comments in Flutter SDK * fix(ai): fix typo in documentation in Flutter SDK * Better code organization and public API * fixes * review * fix * review * Add example test case and minor year fix * remove the export from api --------- Co-authored-by: Cynthia J <cynthiajoan@gmail.com>
1 parent 2771f55 commit 7d1a8b1

5 files changed

Lines changed: 349 additions & 24 deletions

File tree

packages/firebase_ai/firebase_ai/example/lib/pages/image_generation_page.dart

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
3939
final FocusNode _textFieldFocus = FocusNode();
4040
final List<MessageData> _messages = <MessageData>[];
4141
bool _loading = false;
42+
ImageAspectRatio? _selectedAspectRatio;
43+
ImageSize? _selectedImageSize;
4244

4345
@override
4446
void initState() {
@@ -79,7 +81,19 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
7981
_scrollDown();
8082

8183
try {
82-
final response = await _model.generateContent([Content.text(prompt)]);
84+
final response = await _model.generateContent(
85+
[Content.text(prompt)],
86+
generationConfig: GenerationConfig(
87+
responseModalities: [
88+
ResponseModalities.text,
89+
ResponseModalities.image,
90+
],
91+
imageConfig: ImageConfig(
92+
aspectRatio: _selectedAspectRatio,
93+
imageSize: _selectedImageSize,
94+
),
95+
),
96+
);
8397

8498
String? textResponse = response.text;
8599
Uint8List? imageBytes;
@@ -161,31 +175,100 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
161175
),
162176
),
163177
Padding(
164-
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
165-
child: Row(
178+
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 15),
179+
child: Column(
180+
mainAxisSize: MainAxisSize.min,
166181
children: [
167-
Expanded(
168-
child: TextField(
169-
autofocus: true,
170-
focusNode: _textFieldFocus,
171-
controller: _textController,
172-
decoration: const InputDecoration(
173-
hintText: 'Enter image prompt...',
182+
Row(
183+
children: [
184+
Expanded(
185+
child: DropdownButtonFormField<ImageAspectRatio?>(
186+
initialValue: _selectedAspectRatio,
187+
decoration: const InputDecoration(
188+
labelText: 'Aspect Ratio',
189+
border: OutlineInputBorder(),
190+
contentPadding: EdgeInsets.symmetric(
191+
horizontal: 10,
192+
vertical: 10,
193+
),
194+
),
195+
items: [
196+
const DropdownMenuItem(
197+
child: Text('Default'),
198+
),
199+
...ImageAspectRatio.values.map(
200+
(e) => DropdownMenuItem(
201+
value: e,
202+
child: Text('${e.name} (${e.toJson()})'),
203+
),
204+
),
205+
],
206+
onChanged: (value) {
207+
setState(() {
208+
_selectedAspectRatio = value;
209+
});
210+
},
211+
),
174212
),
175-
onSubmitted: _generateImage,
176-
),
213+
const SizedBox(width: 15),
214+
Expanded(
215+
child: DropdownButtonFormField<ImageSize?>(
216+
initialValue: _selectedImageSize,
217+
decoration: const InputDecoration(
218+
labelText: 'Image Size',
219+
border: OutlineInputBorder(),
220+
contentPadding: EdgeInsets.symmetric(
221+
horizontal: 10,
222+
vertical: 10,
223+
),
224+
),
225+
items: [
226+
const DropdownMenuItem(
227+
child: Text('Default'),
228+
),
229+
...ImageSize.values.map(
230+
(e) => DropdownMenuItem(
231+
value: e,
232+
child: Text('${e.name} (${e.toJson()})'),
233+
),
234+
),
235+
],
236+
onChanged: (value) {
237+
setState(() {
238+
_selectedImageSize = value;
239+
});
240+
},
241+
),
242+
),
243+
],
177244
),
178-
const SizedBox(width: 15),
179-
if (!_loading)
180-
IconButton(
181-
onPressed: () => _generateImage(_textController.text),
182-
icon: Icon(
183-
Icons.send,
184-
color: Theme.of(context).colorScheme.primary,
245+
const SizedBox(height: 15),
246+
Row(
247+
children: [
248+
Expanded(
249+
child: TextField(
250+
autofocus: true,
251+
focusNode: _textFieldFocus,
252+
controller: _textController,
253+
decoration: const InputDecoration(
254+
hintText: 'Enter image prompt...',
255+
),
256+
onSubmitted: _generateImage,
257+
),
185258
),
186-
)
187-
else
188-
const CircularProgressIndicator(),
259+
const SizedBox(width: 15),
260+
if (!_loading)
261+
IconButton(
262+
onPressed: () => _generateImage(_textController.text),
263+
icon: Icon(
264+
Icons.send,
265+
color: Theme.of(context).colorScheme.primary,
266+
),
267+
)
268+
else
269+
const CircularProgressIndicator(),
270+
],
271+
),
189272
],
190273
),
191274
),

packages/firebase_ai/firebase_ai/lib/firebase_ai.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export 'src/error.dart'
6666
QuotaExceeded,
6767
UnsupportedUserLocation;
6868
export 'src/firebase_ai.dart' show FirebaseAI;
69+
export 'src/image_config.dart' show ImageConfig, ImageAspectRatio, ImageSize;
6970
export 'src/imagen/imagen_api.dart'
7071
show
7172
ImagenSafetySettings,

packages/firebase_ai/firebase_ai/lib/src/api.dart

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import 'content.dart';
1616
import 'error.dart';
17+
import 'image_config.dart';
1718
import 'schema.dart';
1819
import 'tool.dart' show Tool, ToolConfig;
1920

@@ -808,9 +809,45 @@ enum FinishReason {
808809
/// The candidate content was flagged for malformed function call reasons.
809810
malformedFunctionCall('MALFORMED_FUNCTION_CALL'),
810811

811-
/// The model produced an unexpected tool call.
812+
/// Token generation was stopped because the response contained forbidden terms.
813+
blocklist('BLOCKLIST'),
814+
815+
/// Token generation was stopped because the response contained potentially prohibited content.
816+
prohibitedContent('PROHIBITED_CONTENT'),
817+
818+
/// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII).
819+
spii('SPII'),
820+
821+
/// Token generation stopped because generated images contain safety violations.
822+
imageSafety('IMAGE_SAFETY'),
823+
824+
/// Image generation stopped because generated images have other prohibited content.
825+
imageProhibitedContent('IMAGE_PROHIBITED_CONTENT'),
826+
827+
/// Image generation stopped because of other miscellaneous issues.
828+
imageOther('IMAGE_OTHER'),
829+
830+
/// The model was expected to generate an image, but none was generated.
831+
noImage('NO_IMAGE'),
832+
833+
/// Image generation stopped due to recitation.
834+
imageRecitation('IMAGE_RECITATION'),
835+
836+
/// The response candidate content was flagged for using an unsupported language.
837+
language('LANGUAGE'),
838+
839+
/// Model generated a tool call but no tools were enabled in the request.
812840
unexpectedToolCall('UNEXPECTED_TOOL_CALL'),
813841

842+
/// Model called too many tools consecutively, thus the system exited execution.
843+
tooManyToolCalls('TOO_MANY_TOOL_CALLS'),
844+
845+
/// Request has at least one thought signature missing.
846+
missingThoughtSignature('MISSING_THOUGHT_SIGNATURE'),
847+
848+
/// Finished due to malformed response.
849+
malformedResponse('MALFORMED_RESPONSE'),
850+
814851
/// Unknown reason.
815852
other('OTHER');
816853

@@ -831,8 +868,21 @@ enum FinishReason {
831868
'RECITATION' => FinishReason.recitation,
832869
'OTHER' => FinishReason.other,
833870
'MALFORMED_FUNCTION_CALL' => FinishReason.malformedFunctionCall,
871+
'BLOCKLIST' => FinishReason.blocklist,
872+
'PROHIBITED_CONTENT' => FinishReason.prohibitedContent,
873+
'SPII' => FinishReason.spii,
874+
'IMAGE_SAFETY' => FinishReason.imageSafety,
875+
'IMAGE_PROHIBITED_CONTENT' => FinishReason.imageProhibitedContent,
876+
'IMAGE_OTHER' => FinishReason.imageOther,
877+
'NO_IMAGE' => FinishReason.noImage,
878+
'IMAGE_RECITATION' => FinishReason.imageRecitation,
879+
'LANGUAGE' => FinishReason.language,
834880
'UNEXPECTED_TOOL_CALL' => FinishReason.unexpectedToolCall,
835-
_ => throw FormatException('Unhandled FinishReason format', jsonObject),
881+
'TOO_MANY_TOOL_CALLS' => FinishReason.tooManyToolCalls,
882+
'MISSING_THOUGHT_SIGNATURE' => FinishReason.missingThoughtSignature,
883+
'MALFORMED_RESPONSE' => FinishReason.malformedResponse,
884+
'UNKNOWN' => FinishReason.unknown,
885+
_ => FinishReason.unknown,
836886
};
837887
}
838888

@@ -1238,6 +1288,7 @@ final class GenerationConfig extends BaseGenerationConfig {
12381288
this.responseSchema,
12391289
this.responseJsonSchema,
12401290
this.thinkingConfig,
1291+
this.imageConfig,
12411292
}) : assert(responseSchema == null || responseJsonSchema == null,
12421293
'responseSchema and responseJsonSchema cannot both be set.');
12431294

@@ -1286,6 +1337,9 @@ final class GenerationConfig extends BaseGenerationConfig {
12861337
/// support thinking.
12871338
final ThinkingConfig? thinkingConfig;
12881339

1340+
/// Configuration options for generating images with Gemini models.
1341+
final ImageConfig? imageConfig;
1342+
12891343
@override
12901344
Map<String, Object?> toJson() => {
12911345
...super.toJson(),
@@ -1300,6 +1354,8 @@ final class GenerationConfig extends BaseGenerationConfig {
13001354
'responseJsonSchema': responseJsonSchema,
13011355
if (thinkingConfig case final thinkingConfig?)
13021356
'thinkingConfig': thinkingConfig.toJson(),
1357+
if (imageConfig case final imageConfig?)
1358+
'imageConfig': imageConfig.toJson(),
13031359
};
13041360
}
13051361

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/// Configuration options for generating images with Gemini models.
16+
final class ImageConfig {
17+
/// Initializes configuration options for generating images with Gemini.
18+
const ImageConfig({this.aspectRatio, this.imageSize});
19+
20+
/// The aspect ratio of generated images.
21+
final ImageAspectRatio? aspectRatio;
22+
23+
/// The size of the generated images.
24+
final ImageSize? imageSize;
25+
26+
/// Convert to json format.
27+
Map<String, Object?> toJson() => {
28+
if (aspectRatio case final aspectRatio?)
29+
'aspectRatio': aspectRatio.toJson(),
30+
if (imageSize case final imageSize?) 'imageSize': imageSize.toJson(),
31+
};
32+
}
33+
34+
/// An aspect ratio for generated images.
35+
enum ImageAspectRatio {
36+
/// Square (1:1) aspect ratio.
37+
square1x1('1:1'),
38+
39+
/// Portrait widescreen (9:16) aspect ratio.
40+
portrait9x16('9:16'),
41+
42+
/// Widescreen (16:9) aspect ratio.
43+
landscape16x9('16:9'),
44+
45+
/// Portrait full screen (3:4) aspect ratio.
46+
portrait3x4('3:4'),
47+
48+
/// Fullscreen (4:3) aspect ratio.
49+
landscape4x3('4:3'),
50+
51+
/// Portrait (2:3) aspect ratio.
52+
portrait2x3('2:3'),
53+
54+
/// Landscape (3:2) aspect ratio.
55+
landscape3x2('3:2'),
56+
57+
/// Portrait (4:5) aspect ratio.
58+
portrait4x5('4:5'),
59+
60+
/// Landscape (5:4) aspect ratio.
61+
landscape5x4('5:4'),
62+
63+
/// Portrait (1:4) aspect ratio.
64+
portrait1x4('1:4'),
65+
66+
/// Landscape (4:1) aspect ratio.
67+
landscape4x1('4:1'),
68+
69+
/// Portrait (1:8) aspect ratio.
70+
portrait1x8('1:8'),
71+
72+
/// Landscape (8:1) aspect ratio.
73+
landscape8x1('8:1'),
74+
75+
/// Ultrawide (21:9) aspect ratio.
76+
ultrawide21x9('21:9');
77+
78+
const ImageAspectRatio(this._jsonString);
79+
final String _jsonString;
80+
81+
/// Convert to json format.
82+
String toJson() => _jsonString;
83+
84+
@override
85+
String toString() => name;
86+
}
87+
88+
/// The size of images to generate.
89+
enum ImageSize {
90+
/// 512px (0.5K) image size.
91+
size512('512'),
92+
93+
/// 1K image size.
94+
size1K('1K'),
95+
96+
/// 2K image size.
97+
size2K('2K'),
98+
99+
/// 4K image size.
100+
size4K('4K');
101+
102+
const ImageSize(this._jsonString);
103+
final String _jsonString;
104+
105+
/// Convert to json format.
106+
String toJson() => _jsonString;
107+
108+
@override
109+
String toString() => name;
110+
}

0 commit comments

Comments
 (0)