Skip to content

[AI Logic] Add Imagen support #1279

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 7, 2025
Merged
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
2 changes: 2 additions & 0 deletions docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ Release Notes
- Analytics: Removed deprecated `FirebaseAnalytics.ParameterGroupId`
and `Parameter.Dispose` methods.
- Auth: Removed deprecated `FirebaseUser.UpdateEmailAsync`.
- Firebase AI: Add support for image generation via Imagen. For more info, see
https://firebase.google.com/docs/ai-logic/generate-images-imagen
- Firebase AI: Deprecated `CountTokensResponse.TotalBillableCharacters`, use
`CountTokensResponse.TotalTokens` instead.
- Messaging: Removed deprecated `FirebaseMessage.Dispose`,
Expand Down
22 changes: 22 additions & 0 deletions firebaseai/src/FirebaseAI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,28 @@ public LiveGenerativeModel GetLiveModel(
liveGenerationConfig, tools,
systemInstruction, requestOptions);
}

/// <summary>
/// Initializes an `ImagenModel` with the given parameters.
///
/// - Important: Only Imagen 3 models (named `imagen-3.0-*`) are supported.
/// </summary>
/// <param name="modelName">The name of the Imagen 3 model to use, for example `"imagen-3.0-generate-002"`;
/// see [model versions](https://firebase.google.com/docs/vertex-ai/models) for a list of
/// supported Imagen 3 models.</param>
/// <param name="generationConfig">Configuration options for generating images with Imagen.</param>
/// <param name="safetySettings">Settings describing what types of potentially harmful content your model
/// should allow.</param>
/// <param name="requestOptions">Configuration parameters for sending requests to the backend.</param>
/// <returns>The initialized `ImagenModel` instance.</returns>
public ImagenModel GetImagenModel(
string modelName,
ImagenGenerationConfig? generationConfig = null,
ImagenSafetySettings? safetySettings = null,
RequestOptions? requestOptions = null) {
return new ImagenModel(_firebaseApp, _backend, modelName,
generationConfig, safetySettings, requestOptions);
}
}

}
75 changes: 12 additions & 63 deletions firebaseai/src/GenerativeModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,10 +200,11 @@ public Chat StartChat(IEnumerable<ModelContent> history) {
private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
IEnumerable<ModelContent> content,
CancellationToken cancellationToken) {
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":generateContent");
HttpRequestMessage request = new(HttpMethod.Post,
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":generateContent");

// Set the request headers
await SetRequestHeaders(request);
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);

// Set the content
string bodyJson = MakeGenerateContentRequest(content);
Expand All @@ -214,7 +215,7 @@ private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
#endif

var response = await _httpClient.SendAsync(request, cancellationToken);
await ValidateHttpResponse(response);
await HttpHelpers.ValidateHttpResponse(response);

string result = await response.Content.ReadAsStringAsync();

Expand All @@ -225,40 +226,14 @@ private async Task<GenerateContentResponse> GenerateContentAsyncInternal(
return GenerateContentResponse.FromJson(result, _backend.Provider);
}

// Helper function to throw an exception if the Http Response indicates failure.
// Useful as EnsureSuccessStatusCode can leave out relevant information.
private async Task ValidateHttpResponse(HttpResponseMessage response) {
if (response.IsSuccessStatusCode) {
return;
}

// Status code indicates failure, try to read the content for more details
string errorContent = "No error content available.";
if (response.Content != null) {
try {
errorContent = await response.Content.ReadAsStringAsync();
} catch (Exception readEx) {
// Handle being unable to read the content
errorContent = $"Failed to read error content: {readEx.Message}";
}
}

// Construct the exception with as much information as possible.
var ex = new HttpRequestException(
$"HTTP request failed with status code: {(int)response.StatusCode} ({response.ReasonPhrase}).\n" +
$"Error Content: {errorContent}"
);

throw ex;
}

private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsyncInternal(
IEnumerable<ModelContent> content,
[EnumeratorCancellation] CancellationToken cancellationToken) {
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":streamGenerateContent?alt=sse");
HttpRequestMessage request = new(HttpMethod.Post,
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":streamGenerateContent?alt=sse");

// Set the request headers
await SetRequestHeaders(request);
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);

// Set the content
string bodyJson = MakeGenerateContentRequest(content);
Expand All @@ -269,7 +244,7 @@ private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsy
#endif

var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
await ValidateHttpResponse(response);
await HttpHelpers.ValidateHttpResponse(response);

// We are expecting a Stream as the response, so handle that.
using var stream = await response.Content.ReadAsStreamAsync();
Expand All @@ -291,10 +266,11 @@ private async IAsyncEnumerable<GenerateContentResponse> GenerateContentStreamAsy
private async Task<CountTokensResponse> CountTokensAsyncInternal(
IEnumerable<ModelContent> content,
CancellationToken cancellationToken) {
HttpRequestMessage request = new(HttpMethod.Post, GetURL() + ":countTokens");
HttpRequestMessage request = new(HttpMethod.Post,
HttpHelpers.GetURL(_firebaseApp, _backend, _modelName) + ":countTokens");

// Set the request headers
await SetRequestHeaders(request);
await HttpHelpers.SetRequestHeaders(request, _firebaseApp);

// Set the content
string bodyJson = MakeCountTokensRequest(content);
Expand All @@ -305,7 +281,7 @@ private async Task<CountTokensResponse> CountTokensAsyncInternal(
#endif

var response = await _httpClient.SendAsync(request, cancellationToken);
await ValidateHttpResponse(response);
await HttpHelpers.ValidateHttpResponse(response);

string result = await response.Content.ReadAsStringAsync();

Expand All @@ -316,33 +292,6 @@ private async Task<CountTokensResponse> CountTokensAsyncInternal(
return CountTokensResponse.FromJson(result);
}

private string GetURL() {
if (_backend.Provider == FirebaseAI.Backend.InternalProvider.VertexAI) {
return "https://firebasevertexai.googleapis.com/v1beta" +
"/projects/" + _firebaseApp.Options.ProjectId +
"/locations/" + _backend.Location +
"/publishers/google/models/" + _modelName;
} else if (_backend.Provider == FirebaseAI.Backend.InternalProvider.GoogleAI) {
return "https://firebasevertexai.googleapis.com/v1beta" +
"/projects/" + _firebaseApp.Options.ProjectId +
"/models/" + _modelName;
} else {
throw new NotSupportedException($"Missing support for backend: {_backend.Provider}");
}
}

private async Task SetRequestHeaders(HttpRequestMessage request) {
request.Headers.Add("x-goog-api-key", _firebaseApp.Options.ApiKey);
string version = FirebaseInterops.GetVersionInfoSdkVersion();
request.Headers.Add("x-goog-api-client", $"gl-csharp/8.0 fire/{version}");
if (FirebaseInterops.GetIsDataCollectionDefaultEnabled(_firebaseApp)) {
request.Headers.Add("X-Firebase-AppId", _firebaseApp.Options.AppId);
request.Headers.Add("X-Firebase-AppVersion", UnityEngine.Application.version);
}
// Add additional Firebase tokens to the header.
await FirebaseInterops.AddFirebaseTokensAsync(request, _firebaseApp);
}

private string MakeGenerateContentRequest(IEnumerable<ModelContent> contents) {
Dictionary<string, object> jsonDict = MakeGenerateContentRequestAsDictionary(contents);
return Json.Serialize(jsonDict);
Expand Down
8 changes: 8 additions & 0 deletions firebaseai/src/Imagen.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

206 changes: 206 additions & 0 deletions firebaseai/src/Imagen/ImagenConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/*
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Collections.Generic;

namespace Firebase.AI {
/// <summary>
/// An aspect ratio for images generated by Imagen.
///
/// To specify an aspect ratio for generated images, set `AspectRatio` in
/// your `ImagenGenerationConfig`. See the [Cloud
/// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/image/generate-images#aspect-ratio)
/// for more details and examples of the supported aspect ratios.
/// </summary>
public enum ImagenAspectRatio {
/// <summary>
/// Square (1:1) aspect ratio.
///
/// Common uses for this aspect ratio include social media posts.
/// </summary>
Square1x1,
/// <summary>
/// Portrait widescreen (9:16) aspect ratio.
///
/// This is the `Landscape16x9` aspect ratio rotated 90 degrees. This a relatively new aspect
/// ratio that has been popularized by short form video apps (for example, YouTube shorts). Use
/// this for tall objects with strong vertical orientations such as buildings, trees, waterfalls,
/// or other similar objects.
/// </summary>
Portrait9x16,
/// <summary>
/// Widescreen (16:9) aspect ratio.
///
/// This ratio has replaced `Landscape4x3` as the most common aspect ratio for TVs, monitors,
/// and mobile phone screens (landscape). Use this aspect ratio when you want to capture more of
/// the background (for example, scenic landscapes).
/// </summary>
Landscape16x9,
/// <summary>
/// Portrait full screen (3:4) aspect ratio.
///
/// This is the `Landscape4x3` aspect ratio rotated 90 degrees. This lets to capture more of
/// the scene vertically compared to the `Square1x1` aspect ratio.
/// </summary>
Portrait3x4,
/// <summary>
/// Fullscreen (4:3) aspect ratio.
///
/// This aspect ratio is commonly used in media or film. It is also the dimensions of most old
/// (non-widescreen) TVs and medium format cameras. It captures more of the scene horizontally
/// (compared to `Square1x1`), making it a preferred aspect ratio for photography.
/// </summary>
Landscape4x3
}

/// <summary>
/// An image format for images generated by Imagen.
///
/// To specify an image format for generated images, set `ImageFormat` in
/// your `ImagenGenerationConfig`. See the [Cloud
/// documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/imagen-api#output-options)
/// for more details.
/// </summary>
public readonly struct ImagenImageFormat {
#if !DOXYGEN
public string MimeType { get; }
public int? CompressionQuality { get; }
#endif

private ImagenImageFormat(string mimeType, int? compressionQuality = null) {
MimeType = mimeType;
CompressionQuality = compressionQuality;
}

/// <summary>
/// PNG image format.
///
/// Portable Network Graphic (PNG) is a lossless image format, meaning no image data is lost
/// during compression. Images in PNG format are *typically* larger than JPEG images, though this
/// depends on the image content and JPEG compression quality.
/// </summary>
public static ImagenImageFormat Png() {
return new ImagenImageFormat("image/png");
}

/// <summary>
/// JPEG image format.
///
/// Joint Photographic Experts Group (JPEG) is a lossy compression format, meaning some image data
/// is discarded during compression. Images in JPEG format are *typically* larger than PNG images,
/// though this depends on the image content and JPEG compression quality.
/// </summary>
/// <param name="compressionQuality">The JPEG quality setting from 0 to 100, where `0` is highest level of
/// compression (lowest image quality, smallest file size) and `100` is the lowest level of
/// compression (highest image quality, largest file size); defaults to `75`.</param>
public static ImagenImageFormat Jpeg(int? compressionQuality = null) {
return new ImagenImageFormat("image/jpeg", compressionQuality);
}

/// <summary>
/// Intended for internal use only.
/// This method is used for serializing the object to JSON for the API request.
/// </summary>
internal Dictionary<string, object> ToJson() {
Dictionary<string, object> jsonDict = new() {
["mimeType"] = MimeType
};
if (CompressionQuality != null) {
jsonDict["compressionQuality"] = CompressionQuality.Value;
}
return jsonDict;
}
}

/// <summary>
/// Configuration options for generating images with Imagen.
///
/// See [Parameters for Imagen
/// models](https://firebase.google.com/docs/vertex-ai/model-parameters?platform=unity#imagen) to
/// learn about parameters available for use with Imagen models, including how to configure them.
/// </summary>
public readonly struct ImagenGenerationConfig {
#if !DOXYGEN
public string NegativePrompt { get; }
public int? NumberOfImages { get; }
public ImagenAspectRatio? AspectRatio { get; }
public ImagenImageFormat? ImageFormat { get; }
public bool? AddWatermark { get; }
#endif

/// <summary>
/// Initializes configuration options for generating images with Imagen.
/// </summary>
/// <param name="negativePrompt">Specifies elements to exclude from the generated image;
/// disabled if not specified.</param>
/// <param name="numberOfImages">The number of image samples to generate;
/// defaults to 1 if not specified.</param>
/// <param name="aspectRatio">The aspect ratio of generated images;
/// defaults to to square, 1:1.</param>
/// <param name="imageFormat">The image format of generated images;
/// defaults to PNG.</param>
/// <param name="addWatermark">Whether to add an invisible watermark to generated images;
/// the default value depends on the model.</param>
public ImagenGenerationConfig(
string negativePrompt = null,
int? numberOfImages = null,
ImagenAspectRatio? aspectRatio = null,
ImagenImageFormat? imageFormat = null,
bool? addWatermark = null) {
NegativePrompt = negativePrompt;
NumberOfImages = numberOfImages;
AspectRatio = aspectRatio;
ImageFormat = imageFormat;
AddWatermark = addWatermark;
}

private static string ConvertAspectRatio(ImagenAspectRatio aspectRatio) {
return aspectRatio switch {
ImagenAspectRatio.Square1x1 => "1:1",
ImagenAspectRatio.Portrait9x16 => "9:16",
ImagenAspectRatio.Landscape16x9 => "16:9",
ImagenAspectRatio.Portrait3x4 => "3:4",
ImagenAspectRatio.Landscape4x3 => "4:3",
_ => aspectRatio.ToString(), // Fallback
};
}

/// <summary>
/// Intended for internal use only.
/// This method is used for serializing the object to JSON for the API request.
/// </summary>
internal Dictionary<string, object> ToJson() {
Dictionary<string, object> jsonDict = new() {
["sampleCount"] = NumberOfImages ?? 1
};
if (!string.IsNullOrEmpty(NegativePrompt)) {
jsonDict["negativePrompt"] = NegativePrompt;
}
if (AspectRatio != null) {
jsonDict["aspectRatio"] = ConvertAspectRatio(AspectRatio.Value);
}
if (ImageFormat != null) {
jsonDict["outputOptions"] = ImageFormat?.ToJson();
}
if (AddWatermark != null) {
jsonDict["addWatermark"] = AddWatermark;
}

return jsonDict;
}
}

}
11 changes: 11 additions & 0 deletions firebaseai/src/Imagen/ImagenConfig.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading