Skip to content
1 change: 1 addition & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Release Notes
### Upcoming
- Changes
- Firebase AI: Add support for Grounding with Google Maps.
- Firebase AI: Improved image configuration when using Nano Banana.
- Storage: Added `ListAsync` API to list items and prefixes under a reference.

### 13.10.0
Expand Down
50 changes: 50 additions & 0 deletions firebaseai/src/Candidate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,46 @@ public enum FinishReason
/// Token generation was stopped because the function call generated by the model was invalid.
/// </summary>
MalformedFunctionCall,
/// <summary>
/// Token generation stopped because generated images contain safety violations.
/// </summary>
ImageSafety,
/// <summary>
/// Image generation stopped because generated images have other prohibited content.
/// </summary>
ImageProhibitedContent,
/// <summary>
/// Image generation stopped because of other miscellaneous issue.
/// </summary>
ImageOther,
/// <summary>
/// The model was expected to generate an image, but none was generated.
/// </summary>
NoImage,
/// <summary>
/// Image generation stopped due to recitation.
/// </summary>
ImageRecitation,
/// <summary>
/// The response candidate content was flagged for using an unsupported language.
/// </summary>
Language,
/// <summary>
/// Model generated a tool call but no tools were enabled in the request.
/// </summary>
UnexpectedToolCall,
/// <summary>
/// Model called too many tools consecutively, thus the system exited execution.
/// </summary>
TooManyToolCalls,
/// <summary>
/// Request has at least one thought signature missing.
/// </summary>
MissingThoughtSignature,
/// <summary>
/// Finished due to malformed response.
/// </summary>
MalformedResponse,
}

/// <summary>
Expand Down Expand Up @@ -137,6 +177,16 @@ private static FinishReason ParseFinishReason(string str)
"PROHIBITED_CONTENT" => Firebase.AI.FinishReason.ProhibitedContent,
"SPII" => Firebase.AI.FinishReason.SPII,
"MALFORMED_FUNCTION_CALL" => Firebase.AI.FinishReason.MalformedFunctionCall,
"IMAGE_SAFETY" => Firebase.AI.FinishReason.ImageSafety,
"IMAGE_PROHIBITED_CONTENT" => Firebase.AI.FinishReason.ImageProhibitedContent,
"IMAGE_OTHER" => Firebase.AI.FinishReason.ImageOther,
"NO_IMAGE" => Firebase.AI.FinishReason.NoImage,
"IMAGE_RECITATION" => Firebase.AI.FinishReason.ImageRecitation,
"LANGUAGE" => Firebase.AI.FinishReason.Language,
"UNEXPECTED_TOOL_CALL" => Firebase.AI.FinishReason.UnexpectedToolCall,
"TOO_MANY_TOOL_CALLS" => Firebase.AI.FinishReason.TooManyToolCalls,
"MISSING_THOUGHT_SIGNATURE" => Firebase.AI.FinishReason.MissingThoughtSignature,
"MALFORMED_RESPONSE" => Firebase.AI.FinishReason.MalformedResponse,
_ => Firebase.AI.FinishReason.Unknown,
};
}
Expand Down
87 changes: 85 additions & 2 deletions firebaseai/src/GenerationConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public readonly struct GenerationConfig
private readonly JsonSchema _responseJsonSchema;
private readonly List<ResponseModality> _responseModalities;
private readonly ThinkingConfig? _thinkingConfig;
private readonly ImageConfig? _imageConfig;

/// <summary>
/// Creates a new `GenerationConfig` value.
Expand Down Expand Up @@ -168,6 +169,9 @@ public readonly struct GenerationConfig
/// An error will be returned if this field is set for models that don't
/// support thinking.
/// </param>
/// <param name="imageConfig">
/// Configuration for the aspect ratio and size of generated images.
/// </param>
public GenerationConfig(
float? temperature = null,
float? topP = null,
Expand All @@ -181,7 +185,8 @@ public GenerationConfig(
Schema responseSchema = null,
JsonSchema responseJsonSchema = null,
IEnumerable<ResponseModality> responseModalities = null,
ThinkingConfig? thinkingConfig = null)
ThinkingConfig? thinkingConfig = null,
ImageConfig? imageConfig = null)
{
_temperature = temperature;
_topP = topP;
Expand All @@ -197,6 +202,7 @@ public GenerationConfig(
_responseModalities = responseModalities != null ?
new List<ResponseModality>(responseModalities) : null;
_thinkingConfig = thinkingConfig;
_imageConfig = imageConfig;
}

/// <summary>
Expand All @@ -222,7 +228,8 @@ internal Dictionary<string, object> ToJson()
jsonDict["responseModalities"] =
_responseModalities.Select(EnumConverters.ResponseModalityToString).ToList();
}
if (_thinkingConfig != null) jsonDict["thinkingConfig"] = _thinkingConfig?.ToJson();
if (_thinkingConfig != null) jsonDict["thinkingConfig"] = _thinkingConfig.Value.ToJson();
if (_imageConfig != null) jsonDict["imageConfig"] = _imageConfig.Value.ToJson();
Comment thread
a-maurice marked this conversation as resolved.

return jsonDict;
}
Expand Down Expand Up @@ -321,5 +328,81 @@ internal Dictionary<string, object> ToJson()
}
}

/// <summary>
/// Configuration options for generating images with Gemini models.
/// </summary>
public readonly struct ImageConfig
{
/// <summary>
/// The aspect ratio of generated images.
/// </summary>
public readonly struct AspectRatio
{
public string Value { get; }

/// <summary>
/// Constructs a custom AspectRatio, instead of one of the presets.
/// Note that the backend model needs to support the requested ratio.
/// </summary>
public AspectRatio(string value) { Value = value; }

public static readonly AspectRatio Square1x1 = new("1:1");
public static readonly AspectRatio Portrait9x16 = new("9:16");
public static readonly AspectRatio Landscape16x9 = new("16:9");
public static readonly AspectRatio Portrait3x4 = new("3:4");
public static readonly AspectRatio Landscape4x3 = new("4:3");
public static readonly AspectRatio Portrait2x3 = new("2:3");
public static readonly AspectRatio Landscape3x2 = new("3:2");
public static readonly AspectRatio Portrait4x5 = new("4:5");
public static readonly AspectRatio Landscape5x4 = new("5:4");
public static readonly AspectRatio Portrait1x4 = new("1:4");
public static readonly AspectRatio Landscape4x1 = new("4:1");
public static readonly AspectRatio Portrait1x8 = new("1:8");
public static readonly AspectRatio Landscape8x1 = new("8:1");
public static readonly AspectRatio Ultrawide21x9 = new("21:9");

public override string ToString() => Value;
}

/// <summary>
/// The size of images to generate.
/// </summary>
public readonly struct ImageSize
{
public string Value { get; }

/// <summary>
/// Constructs a custom ImageSize, instead of one of the presets.
/// Note that the backend model needs to support the requested size.
/// </summary>
public ImageSize(string value) { Value = value; }

public static readonly ImageSize Size512 = new("512");
public static readonly ImageSize Size1K = new("1K");
public static readonly ImageSize Size2K = new("2K");
public static readonly ImageSize Size4K = new("4K");

public override string ToString() => Value;
}

public AspectRatio? Ratio { get; }
public ImageSize? Size { get; }

/// <summary>
/// Creates a new `ImageConfig` with the given settings.
/// </summary>
public ImageConfig(AspectRatio? aspectRatio = null, ImageSize? imageSize = null)
{
Ratio = aspectRatio;
Size = imageSize;
}

internal Dictionary<string, object> ToJson()
{
Dictionary<string, object> jsonDict = new();
if (Ratio?.Value is string aspectRatio) jsonDict["aspectRatio"] = aspectRatio;
if (Size?.Value is string imageSize) jsonDict["imageSize"] = imageSize;
return jsonDict;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ protected override void Start()
TestReadSecureFile,
// Internal tests for Json parsing, requires using a source library.
InternalTestBasicReplyShort,
InternalTestFinishReasonExpanded,
InternalTestImageConfigSerialization,
InternalTestCitations,
InternalTestBlockedSafetyWithMessage,
InternalTestFinishReasonSafetyNoContent,
Expand Down Expand Up @@ -878,7 +880,9 @@ async Task TestGenerateImage(Backend backend)
{
var model = GetFirebaseAI(backend).GetGenerativeModel("gemini-2.5-flash-image",
generationConfig: new GenerationConfig(
responseModalities: new[] { ResponseModality.Text, ResponseModality.Image })
responseModalities: new[] { ResponseModality.Text, ResponseModality.Image },
imageConfig: new ImageConfig(
aspectRatio: ImageConfig.AspectRatio.Square1x1))
);

GenerateContentResponse response = await model.GenerateContentAsync(
Expand All @@ -890,6 +894,7 @@ async Task TestGenerateImage(Backend backend)
// We don't care much about the response, just that there is an image, and text.
bool foundText = false;
bool foundImage = false;
Texture2D image = new(1, 2);
var candidate = response.Candidates.First();
foreach (var part in candidate.Content.Parts)
{
Expand All @@ -902,10 +907,13 @@ async Task TestGenerateImage(Backend backend)
if (dataPart.MimeType.Contains("image"))
{
foundImage = true;
image.LoadImage(dataPart.Data.ToArray());
}
}
}
Assert($"Missing expected modalities. Text: {foundText}, Image: {foundImage}", foundText && foundImage);
// The height and width should match, since we requested a 1x1 aspect ratio.
AssertEq("Image dimensions should match.", image.height, image.width);
}

// Test generating an image via Imagen.
Expand Down Expand Up @@ -1541,6 +1549,61 @@ async Task InternalTestBasicReplyShort()
ValidateUsageMetadata(response.UsageMetadata, 6, 7, 0, 0, 13);
}

// Test that parsing a response with expanded FinishReason works.
Task InternalTestFinishReasonExpanded()
{
string jsonStr = @"{
""candidates"": [{
""content"": {
""parts"": [{""text"": ""Hello""}],
""role"": ""model""
},
""finishReason"": ""IMAGE_SAFETY""
}]
}";
Dictionary<string, object> json = (Dictionary<string, object>)Json.Deserialize(jsonStr);
GenerateContentResponse response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.VertexAI);

Assert("Response missing candidates.", response.Candidates.Any());
Candidate candidate = response.Candidates.First();
AssertEq("FinishReason", candidate.FinishReason, FinishReason.ImageSafety);

// Test another one
jsonStr = @"{
""candidates"": [{
""content"": {""parts"": []},
""finishReason"": ""MALFORMED_RESPONSE""
}]
}";
json = (Dictionary<string, object>)Json.Deserialize(jsonStr);
response = GenerateContentResponse.FromJson(json, FirebaseAI.Backend.InternalProvider.VertexAI);
candidate = response.Candidates.First();
AssertEq("FinishReason", candidate.FinishReason, FinishReason.MalformedResponse);

return Task.CompletedTask;
}

// Test that ImageConfig serialization works as expected.
Task InternalTestImageConfigSerialization()
{
var imageConfig = new ImageConfig(ImageConfig.AspectRatio.Landscape16x9, ImageConfig.ImageSize.Size1K);
var json = imageConfig.ToJson();

AssertEq("ImageConfig.aspectRatio", json["aspectRatio"], "16:9");
AssertEq("ImageConfig.imageSize", json["imageSize"], "1K");

var genConfig = new GenerationConfig(imageConfig: imageConfig);
var genJson = genConfig.ToJson();

Assert("GenerationConfig missing imageConfig", genJson.ContainsKey("imageConfig"));
var imageConfigJson = genJson["imageConfig"] as Dictionary<string, object>;
Assert("imageConfig is not a dictionary", imageConfigJson != null);
AssertEq("imageConfigJson.aspectRatio", imageConfigJson["aspectRatio"], "16:9");
AssertEq("imageConfigJson.imageSize", imageConfigJson["imageSize"], "1K");

return Task.CompletedTask;
}

// Test that parsing a response including Citations works.
// https://github.com/FirebaseExtended/vertexai-sdk-test-data/blob/main/mock-responses/unary-success-citations.json
async Task InternalTestCitations()
Expand Down
Loading