labels) {
+ this.labels = labels;
+ }
+
+ public @Nullable String getImageSize() {
+ return this.imageSize;
+ }
+
+ public void setImageSize(@Nullable String imageSize) {
+ this.imageSize = imageSize;
+ }
+
+ public @Nullable Float getTemperature() {
+ return this.temperature;
+ }
+
+ public void setTemperature(@Nullable Float temperature) {
+ this.temperature = temperature;
+ }
+
+ public @Nullable Float getTopP() {
+ return this.topP;
+ }
+
+ public void setTopP(@Nullable Float topP) {
+ this.topP = topP;
+ }
+
+ public @Nullable Float getTopK() {
+ return this.topK;
+ }
+
+ public void setTopK(@Nullable Float topK) {
+ this.topK = topK;
+ }
+
+ public @Nullable Integer getMaxOutputTokens() {
+ return this.maxOutputTokens;
+ }
+
+ public void setMaxOutputTokens(@Nullable Integer maxOutputTokens) {
+ this.maxOutputTokens = maxOutputTokens;
+ }
+
+ public GoogleGenAiImageOptions toOptions() {
+ return GoogleGenAiImageOptions.builder()
+ .model(this.model)
+ .n(this.n)
+ .aspectRatio(this.aspectRatio)
+ .seed(this.seed)
+ .safetyFilterLevel(this.safetyFilterLevel)
+ .personGeneration(this.personGeneration)
+ .outputMimeType(this.outputMimeType)
+ .outputCompressionQuality(this.outputCompressionQuality)
+ .labels(this.labels)
+ .imageSize(this.imageSize)
+ .temperature(this.temperature)
+ .topP(this.topP)
+ .topK(this.topK)
+ .maxOutputTokens(this.maxOutputTokens)
+ .build();
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/image/package-info.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/image/package-info.java
new file mode 100644
index 0000000000..16bd2962e3
--- /dev/null
+++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/java/org/springframework/ai/model/google/genai/autoconfigure/image/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/**
+ * Auto-configuration for Google GenAI image generation.
+ */
+@org.jspecify.annotations.NullMarked
+package org.springframework.ai.model.google.genai.autoconfigure.image;
diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index d054f706df..16304b49a7 100644
--- a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -16,3 +16,5 @@
org.springframework.ai.model.google.genai.autoconfigure.chat.GoogleGenAiChatAutoConfiguration
org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiEmbeddingConnectionAutoConfiguration
org.springframework.ai.model.google.genai.autoconfigure.embedding.GoogleGenAiTextEmbeddingAutoConfiguration
+org.springframework.ai.model.google.genai.autoconfigure.image.GoogleGenAiImageConnectionAutoConfiguration
+org.springframework.ai.model.google.genai.autoconfigure.image.GoogleGenAiImageAutoConfiguration
diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImageAutoConfigurationIT.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImageAutoConfigurationIT.java
new file mode 100644
index 0000000000..cb617894d1
--- /dev/null
+++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImageAutoConfigurationIT.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.model.google.genai.autoconfigure.image;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.google.genai.image.GoogleGenAiImageModel;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.retry.autoconfigure.SpringAiRetryAutoConfiguration;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link GoogleGenAiImageAutoConfiguration}.
+ *
+ *
+ * Activated via either {@code GOOGLE_API_KEY} (Gemini Developer API) or
+ * {@code GOOGLE_CLOUD_PROJECT} + {@code GOOGLE_CLOUD_LOCATION} (Vertex AI). Tests are
+ * skipped when the corresponding environment variables are absent.
+ *
+ * @author Olivier Le Quellec
+ */
+class GoogleGenAiImageAutoConfigurationIT {
+
+ @Test
+ @EnabledIfEnvironmentVariable(named = "GOOGLE_API_KEY", matches = ".+")
+ void imageWithApiKey() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.google.genai.image.api-key=" + System.getenv("GOOGLE_API_KEY"),
+ "spring.ai.google.genai.image.model=gemini-2.5-flash-image", "spring.ai.google.genai.image.n=1")
+ .withConfiguration(AutoConfigurations.of(GoogleGenAiImageAutoConfiguration.class,
+ SpringAiRetryAutoConfiguration.class));
+
+ contextRunner.run(context -> {
+ GoogleGenAiImageModel imageModel = context.getBean(GoogleGenAiImageModel.class);
+ ImageResponse response = imageModel.call(new ImagePrompt("A simple red apple"));
+ assertThat(response.getResults()).isNotEmpty();
+ assertThat(response.getResults().get(0).getOutput()).isNotNull();
+ });
+ }
+
+ @Test
+ @EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_PROJECT", matches = ".+")
+ @EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_LOCATION", matches = ".+")
+ void imageWithVertexAi() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.google.genai.image.project-id=" + System.getenv("GOOGLE_CLOUD_PROJECT"),
+ "spring.ai.google.genai.image.location=" + System.getenv("GOOGLE_CLOUD_LOCATION"),
+ "spring.ai.google.genai.image.model=gemini-2.5-flash-image", "spring.ai.google.genai.image.n=1")
+ .withConfiguration(AutoConfigurations.of(GoogleGenAiImageAutoConfiguration.class,
+ SpringAiRetryAutoConfiguration.class));
+
+ contextRunner.run(context -> {
+ GoogleGenAiImageModel imageModel = context.getBean(GoogleGenAiImageModel.class);
+ ImageResponse response = imageModel.call(new ImagePrompt("A simple red apple"));
+ assertThat(response.getResults()).isNotEmpty();
+ assertThat(response.getResults().get(0).getOutput()).isNotNull();
+ });
+ }
+
+ @Test
+ @EnabledIfEnvironmentVariable(named = "GOOGLE_API_KEY", matches = ".+")
+ void imageModelActivation() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withPropertyValues("spring.ai.google.genai.image.api-key=test-key");
+
+ // Test that image model is not activated when disabled
+ contextRunner
+ .withConfiguration(AutoConfigurations.of(GoogleGenAiImageAutoConfiguration.class,
+ GoogleGenAiImageConnectionAutoConfiguration.class))
+ .withPropertyValues("spring.ai.model.image=none")
+ .run(context -> {
+ assertThat(context.getBeansOfType(GoogleGenAiImageProperties.class)).isEmpty();
+ assertThat(context.getBeansOfType(GoogleGenAiImageModel.class)).isEmpty();
+ });
+
+ // Test that image model is activated when enabled
+ contextRunner
+ .withConfiguration(AutoConfigurations.of(GoogleGenAiImageAutoConfiguration.class,
+ GoogleGenAiImageConnectionAutoConfiguration.class, SpringAiRetryAutoConfiguration.class))
+ .withPropertyValues("spring.ai.model.image=google-genai")
+ .run(context -> {
+ assertThat(context.getBeansOfType(GoogleGenAiImageProperties.class)).isNotEmpty();
+ assertThat(context.getBeansOfType(GoogleGenAiImageModel.class)).isNotEmpty();
+ });
+ }
+
+}
diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImagePropertiesTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImagePropertiesTests.java
new file mode 100644
index 0000000000..31a3ef60de
--- /dev/null
+++ b/auto-configurations/models/spring-ai-autoconfigure-model-google-genai/src/test/java/org/springframework/ai/model/google/genai/autoconfigure/image/GoogleGenAiImagePropertiesTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.model.google.genai.autoconfigure.image;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.ai.google.genai.image.GoogleGenAiImageOptions;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Configuration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for Google GenAI Image properties binding.
+ *
+ * @author Olivier Le Quellec
+ */
+class GoogleGenAiImagePropertiesTests {
+
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(PropertiesTestConfiguration.class);
+
+ @Test
+ void connectionPropertiesBinding() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.google.genai.image.api-key=test-key",
+ "spring.ai.google.genai.image.project-id=test-project",
+ "spring.ai.google.genai.image.location=us-central1")
+ .run(context -> {
+ GoogleGenAiImageConnectionProperties props = context
+ .getBean(GoogleGenAiImageConnectionProperties.class);
+ assertThat(props.getApiKey()).isEqualTo("test-key");
+ assertThat(props.getProjectId()).isEqualTo("test-project");
+ assertThat(props.getLocation()).isEqualTo("us-central1");
+ });
+ }
+
+ @Test
+ void optionsPropertiesBinding() {
+ this.contextRunner
+ .withPropertyValues("spring.ai.google.genai.image.model=gemini-2.5-flash-image",
+ "spring.ai.google.genai.image.n=2", "spring.ai.google.genai.image.aspect-ratio=16:9",
+ "spring.ai.google.genai.image.seed=42",
+ "spring.ai.google.genai.image.safety-filter-level=BLOCK_ONLY_HIGH",
+ "spring.ai.google.genai.image.person-generation=ALLOW_ADULT",
+ "spring.ai.google.genai.image.output-mime-type=image/png",
+ "spring.ai.google.genai.image.output-compression-quality=80",
+ "spring.ai.google.genai.image.image-size=2K", "spring.ai.google.genai.image.labels.env=test",
+ "spring.ai.google.genai.image.temperature=0.7", "spring.ai.google.genai.image.top-p=0.9",
+ "spring.ai.google.genai.image.top-k=40", "spring.ai.google.genai.image.max-output-tokens=1024")
+ .run(context -> {
+ GoogleGenAiImageProperties props = context.getBean(GoogleGenAiImageProperties.class);
+ GoogleGenAiImageOptions options = props.toOptions();
+ assertThat(options.getModel()).isEqualTo("gemini-2.5-flash-image");
+ assertThat(options.getN()).isEqualTo(2);
+ assertThat(options.getAspectRatio()).isEqualTo("16:9");
+ assertThat(options.getSeed()).isEqualTo(42);
+ assertThat(options.getSafetyFilterLevel())
+ .isEqualTo(GoogleGenAiImageOptions.SafetyFilterLevel.BLOCK_ONLY_HIGH);
+ assertThat(options.getPersonGeneration())
+ .isEqualTo(GoogleGenAiImageOptions.PersonGeneration.ALLOW_ADULT);
+ assertThat(options.getOutputMimeType()).isEqualTo("image/png");
+ assertThat(options.getOutputCompressionQuality()).isEqualTo(80);
+ assertThat(options.getImageSize()).isEqualTo("2K");
+ assertThat(options.getLabels()).containsEntry("env", "test");
+ assertThat(options.getTemperature()).isEqualTo(0.7f);
+ assertThat(options.getTopP()).isEqualTo(0.9f);
+ assertThat(options.getTopK()).isEqualTo(40.0f);
+ assertThat(options.getMaxOutputTokens()).isEqualTo(1024);
+ });
+ }
+
+ @Test
+ void defaultOptionsBinding() {
+ this.contextRunner.run(context -> {
+ GoogleGenAiImageProperties props = context.getBean(GoogleGenAiImageProperties.class);
+ assertThat(props.toOptions().getModel()).isEqualTo(GoogleGenAiImageOptions.DEFAULT_MODEL_NAME);
+ assertThat(props.toOptions().getAspectRatio()).isEqualTo(GoogleGenAiImageOptions.DEFAULT_ASPECT_RATIO);
+ });
+ }
+
+ @Configuration
+ @EnableConfigurationProperties({ GoogleGenAiImageConnectionProperties.class, GoogleGenAiImageProperties.class })
+ static class PropertiesTestConfiguration {
+
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/pom.xml b/models/spring-ai-google-genai-image/pom.xml
new file mode 100644
index 0000000000..e65d42a416
--- /dev/null
+++ b/models/spring-ai-google-genai-image/pom.xml
@@ -0,0 +1,87 @@
+
+
+
+
+ 4.0.0
+
+ org.springframework.ai
+ spring-ai-parent
+ 2.0.0-SNAPSHOT
+ ../../pom.xml
+
+ spring-ai-google-genai-image
+ jar
+ Spring AI Model - Google GenAI Image
+ Google GenAI image generation models support
+ https://github.com/spring-projects/spring-ai
+
+
+
+
+ com.google.genai
+ google-genai
+ ${com.google.genai.version}
+
+
+
+
+ org.springframework.ai
+ spring-ai-model
+ ${project.parent.version}
+
+
+
+ org.springframework.ai
+ spring-ai-retry
+ ${project.parent.version}
+
+
+
+
+ org.springframework
+ spring-context-support
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ io.micrometer
+ micrometer-observation-test
+ test
+
+
+
+
+ org.springframework.ai
+ spring-ai-test
+ ${project.parent.version}
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageConnectionDetails.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageConnectionDetails.java
new file mode 100644
index 0000000000..bf7d9d5fd8
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageConnectionDetails.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import com.google.genai.Client;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * GoogleGenAiImageConnectionDetails represents the details of a connection to the image
+ * service using the new Google Gen AI SDK. It provides methods to create and configure
+ * the GenAI Client instance.
+ *
+ * @author Olivier Le Quellec
+ * @since 1.1.0
+ */
+public final class GoogleGenAiImageConnectionDetails {
+
+ public static final String DEFAULT_LOCATION = "us-central1";
+
+ public static final String DEFAULT_PUBLISHER = "google";
+
+ /**
+ * Your project ID.
+ */
+ private final @Nullable String projectId;
+
+ /**
+ * A location is a region
+ * you can specify in a request to control where data is stored at rest. For a list of
+ * available regions, see Generative
+ * AI on Vertex AI locations.
+ */
+ private final @Nullable String location;
+
+ /**
+ * The API key for using Gemini Developer API. If null, Vertex AI mode will be used.
+ */
+ private final @Nullable String apiKey;
+
+ /**
+ * The GenAI Client instance configured for this connection.
+ */
+ private final Client genAiClient;
+
+ private GoogleGenAiImageConnectionDetails(@Nullable String projectId, @Nullable String location,
+ @Nullable String apiKey, Client genAiClient) {
+ this.projectId = projectId;
+ this.location = location;
+ this.apiKey = apiKey;
+ this.genAiClient = genAiClient;
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public @Nullable String getProjectId() {
+ return this.projectId;
+ }
+
+ public @Nullable String getLocation() {
+ return this.location;
+ }
+
+ public @Nullable String getApiKey() {
+ return this.apiKey;
+ }
+
+ public Client getGenAiClient() {
+ return this.genAiClient;
+ }
+
+ /**
+ * Constructs the model endpoint name in the format expected by the image models.
+ * @param modelName the model name (e.g., "gemini-2.5-flash-image")
+ * @return the full model endpoint name
+ */
+ public String getModelEndpointName(String modelName) {
+ // For the new SDK, we just return the model name as is
+ // The SDK handles the full endpoint construction internally
+ return modelName;
+ }
+
+ public static final class Builder {
+
+ /**
+ * Your project ID.
+ */
+ private @Nullable String projectId;
+
+ /**
+ * A location is a
+ * region you can
+ * specify in a request to control where data is stored at rest. For a list of
+ * available regions, see Generative
+ * AI on Vertex AI locations.
+ */
+ private @Nullable String location;
+
+ /**
+ * The API key for using Gemini Developer API. If null, Vertex AI mode will be
+ * used.
+ */
+ private @Nullable String apiKey;
+
+ /**
+ * Custom GenAI client instance. If provided, other settings will be ignored.
+ */
+ private @Nullable Client genAiClient;
+
+ public Builder projectId(@Nullable String projectId) {
+ this.projectId = projectId;
+ return this;
+ }
+
+ public Builder location(@Nullable String location) {
+ this.location = location;
+ return this;
+ }
+
+ public Builder apiKey(@Nullable String apiKey) {
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ public Builder genAiClient(@Nullable Client genAiClient) {
+ this.genAiClient = genAiClient;
+ return this;
+ }
+
+ public GoogleGenAiImageConnectionDetails build() {
+ // If a custom client is provided, use it directly
+ if (this.genAiClient != null) {
+ return new GoogleGenAiImageConnectionDetails(this.projectId, this.location, this.apiKey,
+ this.genAiClient);
+ }
+
+ // Otherwise, build a new client
+ Client.Builder clientBuilder = Client.builder();
+
+ if (StringUtils.hasText(this.apiKey)) {
+ // Use Gemini Developer API mode
+ clientBuilder.apiKey(this.apiKey);
+ }
+ else {
+ // Use Vertex AI mode
+ Assert.hasText(this.projectId, "Project ID must be provided for Vertex AI mode");
+
+ if (!StringUtils.hasText(this.location)) {
+ this.location = DEFAULT_LOCATION;
+ }
+
+ clientBuilder.project(this.projectId).location(this.location).vertexAI(true);
+ }
+
+ Client builtClient = clientBuilder.build();
+ return new GoogleGenAiImageConnectionDetails(this.projectId, this.location, this.apiKey, builtClient);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageGenerationMetadata.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageGenerationMetadata.java
new file mode 100644
index 0000000000..e70203647e
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageGenerationMetadata.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.ai.image.ImageGenerationMetadata;
+
+/**
+ * Image generation metadata returned by the Google GenAI image API.
+ *
+ * @author Olivier Le Quellec
+ * @since 1.1.0
+ */
+public class GoogleGenAiImageGenerationMetadata implements ImageGenerationMetadata {
+
+ private final @Nullable String enhancedPrompt;
+
+ private final @Nullable String raiFilteredReason;
+
+ private final @Nullable String mimeType;
+
+ private final @Nullable String gcsUri;
+
+ public GoogleGenAiImageGenerationMetadata(@Nullable String enhancedPrompt, @Nullable String raiFilteredReason,
+ @Nullable String mimeType, @Nullable String gcsUri) {
+ this.enhancedPrompt = enhancedPrompt;
+ this.raiFilteredReason = raiFilteredReason;
+ this.mimeType = mimeType;
+ this.gcsUri = gcsUri;
+ }
+
+ public @Nullable String getEnhancedPrompt() {
+ return this.enhancedPrompt;
+ }
+
+ public @Nullable String getRaiFilteredReason() {
+ return this.raiFilteredReason;
+ }
+
+ public @Nullable String getMimeType() {
+ return this.mimeType;
+ }
+
+ public @Nullable String getGcsUri() {
+ return this.gcsUri;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof GoogleGenAiImageGenerationMetadata that)) {
+ return false;
+ }
+ return Objects.equals(this.enhancedPrompt, that.enhancedPrompt)
+ && Objects.equals(this.raiFilteredReason, that.raiFilteredReason)
+ && Objects.equals(this.mimeType, that.mimeType) && Objects.equals(this.gcsUri, that.gcsUri);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.enhancedPrompt, this.raiFilteredReason, this.mimeType, this.gcsUri);
+ }
+
+ @Override
+ public String toString() {
+ return "GoogleGenAiImageGenerationMetadata{" + "enhancedPrompt='" + this.enhancedPrompt + '\''
+ + ", raiFilteredReason='" + this.raiFilteredReason + '\'' + ", mimeType='" + this.mimeType + '\''
+ + ", gcsUri='" + this.gcsUri + '\'' + '}';
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModel.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModel.java
new file mode 100644
index 0000000000..eb9fcc4be1
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModel.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import com.google.genai.Client;
+import com.google.genai.types.Candidate;
+import com.google.genai.types.Content;
+import com.google.genai.types.GenerateContentConfig;
+import com.google.genai.types.GenerateContentResponse;
+import com.google.genai.types.HarmCategory;
+import com.google.genai.types.ImageConfig;
+import com.google.genai.types.Part;
+import com.google.genai.types.SafetySetting;
+import io.micrometer.observation.ObservationRegistry;
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.ai.image.Image;
+import org.springframework.ai.image.ImageGeneration;
+import org.springframework.ai.image.ImageMessage;
+import org.springframework.ai.image.ImageModel;
+import org.springframework.ai.image.ImageOptions;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.image.ImageResponseMetadata;
+import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationContext;
+import org.springframework.ai.image.observation.ImageModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationDocumentation;
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.observation.conventions.AiProvider;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.core.retry.RetryTemplate;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A class representing an Image Model using the new Google Gen AI SDK.
+ *
+ * @author Olivier Le Quellec
+ * @since 1.1.0
+ */
+public class GoogleGenAiImageModel implements ImageModel {
+
+ private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention();
+
+ private final GoogleGenAiImageOptions options;
+
+ private final GoogleGenAiImageConnectionDetails connectionDetails;
+
+ private final RetryTemplate retryTemplate;
+
+ /**
+ * Observation registry used for instrumentation.
+ */
+ private final ObservationRegistry observationRegistry;
+
+ /**
+ * Conventions to use for generating observations.
+ */
+ private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION;
+
+ /**
+ * The GenAI client instance.
+ */
+ private final Client genAiClient;
+
+ public GoogleGenAiImageModel(GoogleGenAiImageConnectionDetails connectionDetails,
+ GoogleGenAiImageOptions defaultImageOptions) {
+ this(connectionDetails, defaultImageOptions, RetryUtils.DEFAULT_RETRY_TEMPLATE);
+ }
+
+ public GoogleGenAiImageModel(GoogleGenAiImageConnectionDetails connectionDetails,
+ GoogleGenAiImageOptions defaultImageOptions, RetryTemplate retryTemplate) {
+ this(connectionDetails, defaultImageOptions, retryTemplate, ObservationRegistry.NOOP);
+ }
+
+ public GoogleGenAiImageModel(GoogleGenAiImageConnectionDetails connectionDetails,
+ GoogleGenAiImageOptions defaultImageOptions, RetryTemplate retryTemplate,
+ ObservationRegistry observationRegistry) {
+ Assert.notNull(connectionDetails, "GoogleGenAiImageConnectionDetails must not be null");
+ Assert.notNull(defaultImageOptions, "GoogleGenAiImageOptions must not be null");
+ Assert.notNull(retryTemplate, "retryTemplate must not be null");
+ Assert.notNull(observationRegistry, "observationRegistry must not be null");
+ this.options = defaultImageOptions;
+ this.connectionDetails = connectionDetails;
+ this.genAiClient = connectionDetails.getGenAiClient();
+ this.retryTemplate = retryTemplate;
+ this.observationRegistry = observationRegistry;
+ }
+
+ @Override
+ public ImageResponse call(ImagePrompt prompt) {
+ ImagePrompt imagePrompt = buildImagePrompt(prompt);
+
+ var observationContext = ImageModelObservationContext.builder()
+ .imagePrompt(imagePrompt)
+ .provider(AiProvider.GOOGLE_GENAI_AI.value())
+ .build();
+
+ return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION
+ .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,
+ this.observationRegistry)
+ .observe(() -> {
+ GoogleGenAiImageOptions options = (GoogleGenAiImageOptions) imagePrompt.getOptions();
+ Assert.notNull(options, "Options must not be null");
+ String model = options.getModel();
+ Assert.notNull(model, "Model must not be null");
+ String modelName = this.connectionDetails.getModelEndpointName(model);
+
+ // Build the GenerateContentConfig
+ GenerateContentConfig.Builder configBuilder = GenerateContentConfig.builder();
+
+ // Request image output from the content generation endpoint.
+ configBuilder.responseModalities("TEXT", "IMAGE");
+
+ if (Objects.nonNull(options.getN())) {
+ configBuilder.candidateCount(options.getN());
+ }
+ if (Objects.nonNull(options.getSeed())) {
+ configBuilder.seed(options.getSeed());
+ }
+ if (Objects.nonNull(options.getTemperature())) {
+ configBuilder.temperature(options.getTemperature());
+ }
+ if (Objects.nonNull(options.getTopP())) {
+ configBuilder.topP(options.getTopP());
+ }
+ if (Objects.nonNull(options.getTopK())) {
+ configBuilder.topK(options.getTopK());
+ }
+ if (Objects.nonNull(options.getMaxOutputTokens())) {
+ configBuilder.maxOutputTokens(options.getMaxOutputTokens());
+ }
+ if (Objects.nonNull(options.getLabels()) && !options.getLabels().isEmpty()) {
+ configBuilder.labels(options.getLabels());
+ }
+ if (Objects.nonNull(options.getSafetyFilterLevel()) && options
+ .getSafetyFilterLevel() != GoogleGenAiImageOptions.SafetyFilterLevel.SAFETY_FILTER_LEVEL_UNSPECIFIED) {
+ configBuilder.safetySettings(buildSafetySettings(options.getSafetyFilterLevel()));
+ }
+
+ // Image specific options are carried by the nested ImageConfig.
+ ImageConfig.Builder imageConfigBuilder = ImageConfig.builder();
+ boolean hasImageConfig = false;
+ if (StringUtils.hasText(options.getAspectRatio())) {
+ imageConfigBuilder.aspectRatio(options.getAspectRatio());
+ hasImageConfig = true;
+ }
+ if (StringUtils.hasText(options.getImageSize())) {
+ imageConfigBuilder.imageSize(options.getImageSize());
+ hasImageConfig = true;
+ }
+ if (Objects.nonNull(options.getPersonGeneration())) {
+ imageConfigBuilder.personGeneration(options.getPersonGeneration().name());
+ hasImageConfig = true;
+ }
+ if (StringUtils.hasText(options.getOutputMimeType())) {
+ imageConfigBuilder.outputMimeType(options.getOutputMimeType());
+ hasImageConfig = true;
+ }
+ if (Objects.nonNull(options.getOutputCompressionQuality())) {
+ imageConfigBuilder.outputCompressionQuality(options.getOutputCompressionQuality());
+ hasImageConfig = true;
+ }
+ if (hasImageConfig) {
+ configBuilder.imageConfig(imageConfigBuilder.build());
+ }
+
+ GenerateContentConfig config = configBuilder.build();
+
+ // Convert instructions to single prompt for image
+
+ final String promptText = prompt.getInstructions()
+ .stream()
+ .map(ImageMessage::getText)
+ .filter(StringUtils::hasText)
+ .reduce((first, second) -> first + "\n" + second)
+ .orElseThrow(() -> new IllegalArgumentException(
+ "ImagePrompt must contain at least one non-empty message"));
+
+ GenerateContentResponse imagesResponse = RetryUtils.execute(this.retryTemplate,
+ () -> this.genAiClient.models.generateContent(modelName, promptText, config));
+
+ // Process the response: each candidate may contain multiple content
+ // parts, so add an ImageGeneration for every part that carries image
+ // data.
+ final List generationList = imagesResponse.candidates()
+ .stream()
+ .flatMap(List::stream)
+ .map(Candidate::content)
+ .flatMap(Optional::stream)
+ .map(Content::parts)
+ .flatMap(Optional::stream)
+ .flatMap(List::stream)
+ .map(Part::inlineData)
+ .flatMap(Optional::stream)
+ .map(blob -> {
+ String b64Json = blob.data()
+ .map(imageBytes -> Base64.getEncoder().encodeToString(imageBytes))
+ .orElse(null);
+
+ Image image = new Image(null, b64Json);
+
+ GoogleGenAiImageGenerationMetadata metadata = new GoogleGenAiImageGenerationMetadata(null, null,
+ blob.mimeType().orElse(null), image.getUrl());
+
+ return new ImageGeneration(image, metadata);
+ })
+ .toList();
+
+ ImageResponse response = new ImageResponse(generationList, new ImageResponseMetadata());
+
+ observationContext.setResponse(response);
+
+ return response;
+ });
+ }
+
+ ImagePrompt buildImagePrompt(ImagePrompt imagePrompt) {
+ @Nullable ImageOptions requestOptions = imagePrompt.getOptions();
+ GoogleGenAiImageOptions mergedOptions = this.options;
+
+ if (Objects.nonNull(requestOptions)) {
+ GoogleGenAiImageOptions.Builder builder = GoogleGenAiImageOptions.builder()
+ .from(this.options)
+ .model(ModelOptionsUtils.mergeOption(requestOptions.getModel(), this.options.getModel()))
+ .n(ModelOptionsUtils.mergeOption(requestOptions.getN(), this.options.getN()))
+ .outputMimeType(ModelOptionsUtils.mergeOption(requestOptions.getResponseFormat(),
+ this.options.getResponseFormat()));
+
+ if (requestOptions instanceof GoogleGenAiImageOptions googleOptions) {
+ builder.from(googleOptions);
+ }
+
+ mergedOptions = builder.build();
+ }
+
+ // Validate request options
+ if (!StringUtils.hasText(mergedOptions.getModel())) {
+ throw new IllegalArgumentException("model cannot be null or empty");
+ }
+
+ return new ImagePrompt(imagePrompt.getInstructions(), mergedOptions);
+ }
+
+ /**
+ * Applies the configured {@link GoogleGenAiImageOptions.SafetyFilterLevel} as a
+ * {@link SafetySetting} threshold across the harm categories supported by the
+ * {@code generateContent} API. The enum names of {@code SafetyFilterLevel} map
+ * directly onto the SDK {@code HarmBlockThreshold} values.
+ * @param safetyFilterLevel the configured safety filter level
+ * @return the list of safety settings to apply to the request
+ */
+ private List buildSafetySettings(GoogleGenAiImageOptions.SafetyFilterLevel safetyFilterLevel) {
+ String threshold = safetyFilterLevel.name();
+ List categories = List.of(HarmCategory.Known.HARM_CATEGORY_HARASSMENT,
+ HarmCategory.Known.HARM_CATEGORY_HATE_SPEECH, HarmCategory.Known.HARM_CATEGORY_SEXUALLY_EXPLICIT,
+ HarmCategory.Known.HARM_CATEGORY_DANGEROUS_CONTENT);
+ List safetySettings = new ArrayList<>();
+ for (HarmCategory.Known category : categories) {
+ safetySettings.add(SafetySetting.builder().category(category).threshold(threshold).build());
+ }
+ return safetySettings;
+ }
+
+ /**
+ * Use the provided convention for reporting observation data
+ * @param observationConvention The provided convention
+ */
+ public void setObservationConvention(@Nullable ImageModelObservationConvention observationConvention) {
+ Assert.notNull(observationConvention, "observationConvention cannot be null");
+ this.observationConvention = observationConvention;
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelName.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelName.java
new file mode 100644
index 0000000000..dfad252507
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelName.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import org.springframework.ai.model.ModelDescription;
+
+/**
+ * Known Google GenAI image generation model names.
+ *
+ * @author Olivier Le Quellec
+ * @since 1.1.0
+ */
+public enum GoogleGenAiImageModelName implements ModelDescription {
+
+ GEMINI_2_5_FLASH_IMAGE("gemini-2.5-flash-image", "Nano Banana"),
+
+ GEMINI_3_PRO_IMAGE("gemini-3-pro-image", "Nano Banana Pro"),
+
+ GEMINI_3_1_FLASH_IMAGE("gemini-3.1-flash-image", "Nano Banana 2");
+
+ private final String modelName;
+
+ private final String description;
+
+ GoogleGenAiImageModelName(String value, String description) {
+ this.modelName = value;
+ this.description = description;
+ }
+
+ @Override
+ public String getName() {
+ return this.modelName;
+ }
+
+ @Override
+ public String getDescription() {
+ return this.description;
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageOptions.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageOptions.java
new file mode 100644
index 0000000000..c20eb944bf
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/GoogleGenAiImageOptions.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.ai.image.ImageOptions;
+import org.springframework.util.StringUtils;
+
+/**
+ * Options for the Image supported by the GenAI SDK
+ *
+ * @author Olivier Le Quellec
+ * @since 1.1.0
+ */
+public class GoogleGenAiImageOptions implements ImageOptions {
+
+ public static final String DEFAULT_MODEL_NAME = GoogleGenAiImageModelName.GEMINI_2_5_FLASH_IMAGE.getName();
+
+ public static final String DEFAULT_ASPECT_RATIO = "1:1";
+
+ // @formatter:off
+
+ /**
+ * The model to use.
+ */
+ private final @Nullable String model;
+
+ /**
+ * Number of images to generate. Must be between 1 and 4.
+ */
+ private final @Nullable Integer n;
+
+ /**
+ * Random seed for image generation.
+ */
+ private final @Nullable Integer seed;
+
+ /**
+ * Aspect ratio of the generated images. Supported values: 1:1, 3:4, 4:3, 9:16, 16:9.
+ */
+ private final @Nullable String aspectRatio;
+
+ /**
+ * Filter level for safety filtering.
+ */
+ private final @Nullable SafetyFilterLevel safetyFilterLevel;
+
+ /**
+ * Allows generation of people by the model.
+ */
+ private final @Nullable PersonGeneration personGeneration;
+
+ /**
+ * MIME type of the generated image (e.g. {@code image/png}, {@code image/jpeg}).
+ */
+ private final @Nullable String outputMimeType;
+
+ /**
+ * Compression quality of the generated image (for {@code image/jpeg} only).
+ */
+ private final @Nullable Integer outputCompressionQuality;
+
+ /**
+ * User specified labels to track billing usage.
+ */
+ private final @Nullable Map labels;
+
+ /**
+ * The size of the largest dimension of the generated image. Supported: {@code 1K},
+ * {@code 2K}.
+ */
+ private final @Nullable String imageSize;
+
+ /**
+ * Controls the degree of randomness in token selection. Lower temperatures are good
+ * for prompts that require a less open-ended or creative response, while higher
+ * temperatures can lead to more diverse or creative results.
+ */
+ private final @Nullable Float temperature;
+
+ /**
+ * Tokens are selected from the most to least probable until the sum of their
+ * probabilities equals this value.
+ */
+ private final @Nullable Float topP;
+
+ /**
+ * For each token selection step, the {@code topK} tokens with the highest
+ * probabilities are sampled.
+ */
+ private final @Nullable Float topK;
+
+ /**
+ * Maximum number of tokens that can be generated in the response.
+ */
+ private final @Nullable Integer maxOutputTokens;
+
+ protected GoogleGenAiImageOptions(
+ @Nullable String model,
+ @Nullable Integer n,
+ @Nullable String aspectRatio,
+ @Nullable Integer seed,
+ @Nullable SafetyFilterLevel safetyFilterLevel,
+ @Nullable PersonGeneration personGeneration,
+ @Nullable String outputMimeType,
+ @Nullable Integer outputCompressionQuality,
+ @Nullable Map labels,
+ @Nullable String imageSize,
+ @Nullable Float temperature,
+ @Nullable Float topP,
+ @Nullable Float topK,
+ @Nullable Integer maxOutputTokens) {
+ this.model = (model != null ? model : DEFAULT_MODEL_NAME);
+ this.n = n;
+ this.aspectRatio = aspectRatio;
+ this.seed = seed;
+ this.safetyFilterLevel = safetyFilterLevel;
+ this.personGeneration = personGeneration;
+ this.outputMimeType = outputMimeType;
+ this.outputCompressionQuality = outputCompressionQuality;
+ this.labels = (labels == null) ? null : new LinkedHashMap<>(labels);
+ this.imageSize = imageSize;
+ this.temperature = temperature;
+ this.topP = topP;
+ this.topK = topK;
+ this.maxOutputTokens = maxOutputTokens;
+ }
+
+ public static GoogleGenAiImageOptions.Builder builder() {
+ return new Builder();
+ }
+
+
+ // @formatter:on
+
+ @Override
+ public @Nullable String getModel() {
+ return this.model;
+ }
+
+ @Override
+ public @Nullable Integer getN() {
+ return this.n;
+ }
+
+ /**
+ * Image width is not directly configurable Use {@link #getAspectRatio()} or
+ * {@link #getImageSize()} instead.
+ * @return always {@code null}
+ */
+ @Override
+ public @Nullable Integer getWidth() {
+ return null;
+ }
+
+ /**
+ * Image height is not directly configurable Use {@link #getAspectRatio()} or
+ * {@link #getImageSize()} instead.
+ * @return always {@code null}
+ */
+ @Override
+ public @Nullable Integer getHeight() {
+ return null;
+ }
+
+ @Override
+ public @Nullable String getResponseFormat() {
+ return this.outputMimeType;
+ }
+
+ @Override
+ public @Nullable String getStyle() {
+ return null;
+ }
+
+ public @Nullable String getAspectRatio() {
+ return this.aspectRatio;
+ }
+
+ public @Nullable Integer getSeed() {
+ return this.seed;
+ }
+
+ public @Nullable SafetyFilterLevel getSafetyFilterLevel() {
+ return this.safetyFilterLevel;
+ }
+
+ public @Nullable PersonGeneration getPersonGeneration() {
+ return this.personGeneration;
+ }
+
+ public @Nullable String getOutputMimeType() {
+ return this.outputMimeType;
+ }
+
+ public @Nullable Integer getOutputCompressionQuality() {
+ return this.outputCompressionQuality;
+ }
+
+ public @Nullable Map getLabels() {
+ return this.labels;
+ }
+
+ public @Nullable String getImageSize() {
+ return this.imageSize;
+ }
+
+ public @Nullable Float getTemperature() {
+ return this.temperature;
+ }
+
+ public @Nullable Float getTopP() {
+ return this.topP;
+ }
+
+ public @Nullable Float getTopK() {
+ return this.topK;
+ }
+
+ public @Nullable Integer getMaxOutputTokens() {
+ return this.maxOutputTokens;
+ }
+
+ /**
+ * Safety filter level for image generation.
+ */
+ public enum SafetyFilterLevel {
+
+ BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH, BLOCK_NONE, SAFETY_FILTER_LEVEL_UNSPECIFIED
+
+ }
+
+ /**
+ * Person generation policy.
+ */
+ public enum PersonGeneration {
+
+ DONT_ALLOW, ALLOW_ADULT, ALLOW_ALL, PERSON_GENERATION_UNSPECIFIED
+
+ }
+
+ public static final class Builder {
+
+ private @Nullable String model;
+
+ private @Nullable Integer n;
+
+ private @Nullable String aspectRatio;
+
+ private @Nullable Integer seed;
+
+ private @Nullable SafetyFilterLevel safetyFilterLevel;
+
+ private @Nullable PersonGeneration personGeneration;
+
+ private @Nullable String outputMimeType;
+
+ private @Nullable Integer outputCompressionQuality;
+
+ private @Nullable Map labels;
+
+ private @Nullable String imageSize;
+
+ private @Nullable Float temperature;
+
+ private @Nullable Float topP;
+
+ private @Nullable Float topK;
+
+ private @Nullable Integer maxOutputTokens;
+
+ public Builder() {
+ }
+
+ public Builder from(GoogleGenAiImageOptions fromOptions) {
+ if (StringUtils.hasText(fromOptions.getModel())) {
+ this.model = fromOptions.getModel();
+ }
+ if (Objects.nonNull(fromOptions.getN())) {
+ this.n = fromOptions.getN();
+ }
+ if (StringUtils.hasText(fromOptions.getAspectRatio())) {
+ this.aspectRatio = fromOptions.getAspectRatio();
+ }
+ if (Objects.nonNull(fromOptions.getSeed())) {
+ this.seed = fromOptions.getSeed();
+ }
+ if (Objects.nonNull(fromOptions.getSafetyFilterLevel())) {
+ this.safetyFilterLevel = fromOptions.getSafetyFilterLevel();
+ }
+ if (Objects.nonNull(fromOptions.getPersonGeneration())) {
+ this.personGeneration = fromOptions.getPersonGeneration();
+ }
+ if (StringUtils.hasText(fromOptions.getOutputMimeType())) {
+ this.outputMimeType = fromOptions.getOutputMimeType();
+ }
+ if (Objects.nonNull(fromOptions.getOutputCompressionQuality())) {
+ this.outputCompressionQuality = fromOptions.getOutputCompressionQuality();
+ }
+ if (Objects.nonNull(fromOptions.getLabels())) {
+ this.labels = fromOptions.getLabels();
+ }
+ if (StringUtils.hasText(fromOptions.getImageSize())) {
+ this.imageSize = fromOptions.getImageSize();
+ }
+
+ if (Objects.nonNull(fromOptions.getTemperature())) {
+ this.temperature = fromOptions.getTemperature();
+ }
+ if (Objects.nonNull(fromOptions.getTopP())) {
+ this.topP = fromOptions.getTopP();
+ }
+ if (Objects.nonNull(fromOptions.getTopK())) {
+ this.topK = fromOptions.getTopK();
+ }
+ if (Objects.nonNull(fromOptions.getMaxOutputTokens())) {
+ this.maxOutputTokens = fromOptions.getMaxOutputTokens();
+ }
+
+ return this;
+ }
+
+ public Builder model(@Nullable String model) {
+ this.model = model;
+ return this;
+ }
+
+ public Builder model(GoogleGenAiImageModelName model) {
+ this.model = model.getName();
+ return this;
+ }
+
+ public Builder n(@Nullable Integer n) {
+ this.n = n;
+ return this;
+ }
+
+ public Builder aspectRatio(@Nullable String aspectRatio) {
+ this.aspectRatio = aspectRatio;
+ return this;
+ }
+
+ public Builder seed(@Nullable Integer seed) {
+ this.seed = seed;
+ return this;
+ }
+
+ public Builder safetyFilterLevel(@Nullable SafetyFilterLevel safetyFilterLevel) {
+ this.safetyFilterLevel = safetyFilterLevel;
+ return this;
+ }
+
+ public Builder personGeneration(@Nullable PersonGeneration personGeneration) {
+ this.personGeneration = personGeneration;
+ return this;
+ }
+
+ public Builder outputMimeType(@Nullable String outputMimeType) {
+ this.outputMimeType = outputMimeType;
+ return this;
+ }
+
+ public Builder outputCompressionQuality(@Nullable Integer outputCompressionQuality) {
+ this.outputCompressionQuality = outputCompressionQuality;
+ return this;
+ }
+
+ public Builder labels(@Nullable Map labels) {
+ this.labels = labels;
+ return this;
+ }
+
+ public Builder imageSize(@Nullable String imageSize) {
+ this.imageSize = imageSize;
+ return this;
+ }
+
+ public Builder temperature(@Nullable Float temperature) {
+ this.temperature = temperature;
+ return this;
+ }
+
+ public Builder topP(@Nullable Float topP) {
+ this.topP = topP;
+ return this;
+ }
+
+ public Builder topK(@Nullable Float topK) {
+ this.topK = topK;
+ return this;
+ }
+
+ public Builder maxOutputTokens(@Nullable Integer maxOutputTokens) {
+ this.maxOutputTokens = maxOutputTokens;
+ return this;
+ }
+
+ public GoogleGenAiImageOptions build() {
+ return new GoogleGenAiImageOptions(this.model, this.n, this.aspectRatio, this.seed, this.safetyFilterLevel,
+ this.personGeneration, this.outputMimeType, this.outputCompressionQuality, this.labels,
+ this.imageSize, this.temperature, this.topP, this.topK, this.maxOutputTokens);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/package-info.java b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/package-info.java
new file mode 100644
index 0000000000..eacb919379
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/main/java/org/springframework/ai/google/genai/image/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/**
+ * Google GenAI image generation support for Spring AI.
+ */
+@org.jspecify.annotations.NullMarked
+package org.springframework.ai.google.genai.image;
diff --git a/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelIT.java b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelIT.java
new file mode 100644
index 0000000000..73b014ba46
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelIT.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import com.google.genai.Client;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import org.springframework.ai.image.ImageGeneration;
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for image models {@link GoogleGenAiImageModel}.
+ *
+ * @author Olivier Le Quellec
+ */
+@SpringBootTest(classes = GoogleGenAiImageModelIT.Config.class)
+@EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_PROJECT", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_LOCATION", matches = ".+")
+class GoogleGenAiImageModelIT {
+
+ @Autowired
+ private GoogleGenAiImageModel imageModel;
+
+ @Autowired
+ private Client genAiClient;
+
+ @ParameterizedTest(name = "{0} : {displayName} ")
+ @ValueSource(strings = { "gemini-2.5-flash-image" })
+ void defaultImage(String modelName) {
+ assertThat(this.imageModel).isNotNull();
+
+ var options = GoogleGenAiImageOptions.builder().model(modelName).n(1).build();
+
+ ImagePrompt imagePrompt = new ImagePrompt("A light cream colored mini golden doodle dog", options);
+
+ ImageResponse imageResponse = this.imageModel.call(imagePrompt);
+
+ assertThat(imageResponse.getResults()).hasSize(1);
+
+ ImageGeneration imageGeneration = imageResponse.getResults().get(0);
+ assertThat(imageGeneration.getOutput()).isNotNull();
+ assertThat(imageGeneration.getOutput().getB64Json()).isNotBlank();
+ }
+
+ @SpringBootConfiguration
+ static class Config {
+
+ @Bean
+ public GoogleGenAiImageConnectionDetails connectionDetails() {
+ return GoogleGenAiImageConnectionDetails.builder()
+ .projectId(System.getenv("GOOGLE_CLOUD_PROJECT"))
+ .location(System.getenv("GOOGLE_CLOUD_LOCATION"))
+ .build();
+ }
+
+ @Bean
+ public Client genAiClient(GoogleGenAiImageConnectionDetails connectionDetails) {
+ return connectionDetails.getGenAiClient();
+ }
+
+ @Bean
+ public GoogleGenAiImageModel googleGenAiImageModel(GoogleGenAiImageConnectionDetails connectionDetails) {
+
+ GoogleGenAiImageOptions options = GoogleGenAiImageOptions.builder()
+ .model(GoogleGenAiImageModelName.GEMINI_2_5_FLASH_IMAGE.getName())
+ .build();
+
+ return new GoogleGenAiImageModel(connectionDetails, options);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelObservationIT.java b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelObservationIT.java
new file mode 100644
index 0000000000..4a39426d65
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageModelObservationIT.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import io.micrometer.observation.ObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistry;
+import io.micrometer.observation.tck.TestObservationRegistryAssert;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.image.observation.DefaultImageModelObservationConvention;
+import org.springframework.ai.image.observation.ImageModelObservationDocumentation.LowCardinalityKeyNames;
+import org.springframework.ai.observation.conventions.AiOperationType;
+import org.springframework.ai.observation.conventions.AiProvider;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for observation instrumentation in {@link GoogleGenAiImageModel}.
+ *
+ * @author Olivier Le Quellec
+ */
+@SpringBootTest(classes = GoogleGenAiImageModelObservationIT.Config.class)
+@EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_PROJECT", matches = ".+")
+@EnabledIfEnvironmentVariable(named = "GOOGLE_CLOUD_LOCATION", matches = ".+")
+public class GoogleGenAiImageModelObservationIT {
+
+ @Autowired
+ TestObservationRegistry observationRegistry;
+
+ @Autowired
+ GoogleGenAiImageModel imageModel;
+
+ @BeforeEach
+ void setUp() {
+ this.observationRegistry.clear();
+ }
+
+ @Test
+ void observationForImageOperation() {
+
+ var options = GoogleGenAiImageOptions.builder()
+ .model(GoogleGenAiImageModelName.GEMINI_2_5_FLASH_IMAGE.getName())
+ .n(1)
+ .build();
+
+ ImagePrompt imagePrompt = new ImagePrompt("A light cream colored mini golden doodle dog", options);
+
+ ImageResponse imageResponse = this.imageModel.call(imagePrompt);
+ assertThat(imageResponse.getResults()).isNotEmpty();
+
+ TestObservationRegistryAssert.assertThat(this.observationRegistry)
+ .doesNotHaveAnyRemainingCurrentObservation()
+ .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME)
+ .that()
+ .hasContextualNameEqualTo("image " + GoogleGenAiImageModelName.GEMINI_2_5_FLASH_IMAGE.getName())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(),
+ AiOperationType.IMAGE.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.AI_PROVIDER.asString(),
+ AiProvider.GOOGLE_GENAI_AI.value())
+ .hasLowCardinalityKeyValue(LowCardinalityKeyNames.REQUEST_MODEL.asString(),
+ GoogleGenAiImageModelName.GEMINI_2_5_FLASH_IMAGE.getName())
+ .hasBeenStarted()
+ .hasBeenStopped();
+ }
+
+ @SpringBootConfiguration
+ static class Config {
+
+ @Bean
+ public TestObservationRegistry observationRegistry() {
+ return TestObservationRegistry.create();
+ }
+
+ @Bean
+ public GoogleGenAiImageConnectionDetails connectionDetails() {
+ return GoogleGenAiImageConnectionDetails.builder()
+ .projectId(System.getenv("GOOGLE_CLOUD_PROJECT"))
+ .location(System.getenv("GOOGLE_CLOUD_LOCATION"))
+ .build();
+ }
+
+ @Bean
+ public GoogleGenAiImageModel googleGenAiImageModel(GoogleGenAiImageConnectionDetails connectionDetails,
+ ObservationRegistry observationRegistry) {
+
+ GoogleGenAiImageOptions options = GoogleGenAiImageOptions.builder()
+ .model(GoogleGenAiImageOptions.DEFAULT_MODEL_NAME)
+ .build();
+
+ return new GoogleGenAiImageModel(connectionDetails, options, RetryUtils.DEFAULT_RETRY_TEMPLATE,
+ observationRegistry);
+ }
+
+ }
+
+}
diff --git a/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageRetryTests.java b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageRetryTests.java
new file mode 100644
index 0000000000..d1c6080bc1
--- /dev/null
+++ b/models/spring-ai-google-genai-image/src/test/java/org/springframework/ai/google/genai/image/GoogleGenAiImageRetryTests.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2023-present the original author or authors.
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+package org.springframework.ai.google.genai.image;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Optional;
+
+import com.google.genai.Client;
+import com.google.genai.Models;
+import com.google.genai.types.Blob;
+import com.google.genai.types.Candidate;
+import com.google.genai.types.Content;
+import com.google.genai.types.GenerateContentConfig;
+import com.google.genai.types.GenerateContentResponse;
+import com.google.genai.types.Part;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import org.springframework.ai.image.ImagePrompt;
+import org.springframework.ai.image.ImageResponse;
+import org.springframework.ai.retry.RetryUtils;
+import org.springframework.ai.retry.TransientAiException;
+import org.springframework.core.retry.RetryListener;
+import org.springframework.core.retry.RetryPolicy;
+import org.springframework.core.retry.RetryTemplate;
+import org.springframework.core.retry.Retryable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+/**
+ * @author Olivier Le Quellec
+ */
+@ExtendWith(MockitoExtension.class)
+public class GoogleGenAiImageRetryTests {
+
+ private TestRetryListener retryListener;
+
+ private RetryTemplate retryTemplate;
+
+ private Client mockGenAiClient;
+
+ @Mock
+ private Models mockModels;
+
+ @Mock
+ private GoogleGenAiImageConnectionDetails mockConnectionDetails;
+
+ private GoogleGenAiImageModel imageModel;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ this.retryTemplate = RetryUtils.SHORT_RETRY_TEMPLATE;
+ this.retryListener = new TestRetryListener();
+ this.retryTemplate.setRetryListener(this.retryListener);
+
+ // Create a mock Client and use reflection to set the models field
+ this.mockGenAiClient = mock(Client.class);
+ Field modelsField = Client.class.getDeclaredField("models");
+ modelsField.setAccessible(true);
+ modelsField.set(this.mockGenAiClient, this.mockModels);
+
+ // Set up the mock connection details to return the mock client
+ given(this.mockConnectionDetails.getGenAiClient()).willReturn(this.mockGenAiClient);
+ given(this.mockConnectionDetails.getModelEndpointName(anyString()))
+ .willAnswer(invocation -> invocation.getArgument(0));
+
+ this.imageModel = new GoogleGenAiImageModel(this.mockConnectionDetails,
+ GoogleGenAiImageOptions.builder().build(), this.retryTemplate);
+ }
+
+ @Test
+ public void googleGenAiImageTransientError() {
+ // Create mock image response: candidate -> content -> part -> inline image data
+ Blob mockBlob = mock(Blob.class);
+ given(mockBlob.data()).willReturn(Optional.of(new byte[] { 1, 2, 3 }));
+ given(mockBlob.mimeType()).willReturn(Optional.of("image/png"));
+
+ Part mockPart = mock(Part.class);
+ given(mockPart.inlineData()).willReturn(Optional.of(mockBlob));
+
+ Content mockContent = mock(Content.class);
+ given(mockContent.parts()).willReturn(Optional.of(List.of(mockPart)));
+
+ Candidate mockCandidate = mock(Candidate.class);
+ given(mockCandidate.content()).willReturn(Optional.of(mockContent));
+
+ GenerateContentResponse mockResponse = mock(GenerateContentResponse.class);
+ given(mockResponse.candidates()).willReturn(Optional.of(List.of(mockCandidate)));
+
+ // Setup the mock client to throw transient errors then succeed
+ given(this.mockModels.generateContent(anyString(), anyString(), any(GenerateContentConfig.class)))
+ .willThrow(new TransientAiException("Transient Error 1"))
+ .willThrow(new TransientAiException("Transient Error 2"))
+ .willReturn(mockResponse);
+
+ var options = GoogleGenAiImageOptions.builder().model("model").build();
+ ImageResponse result = this.imageModel
+ .call(new ImagePrompt("A light cream colored mini golden doodle", options));
+
+ assertThat(result).isNotNull();
+ assertThat(result.getResults()).hasSize(1);
+ assertThat(result.getResults().get(0).getOutput()).isNotNull();
+ assertThat(this.retryListener.onSuccessRetryCount).isEqualTo(1);
+ assertThat(this.retryListener.onErrorRetryCount).isEqualTo(2);
+
+ verify(this.mockModels, times(3)).generateContent(anyString(), anyString(), any(GenerateContentConfig.class));
+ }
+
+ @Test
+ public void googleGenAiImageNonTransientError() {
+ // Setup the mock client to throw a non-transient error
+ given(this.mockModels.generateContent(anyString(), anyString(), any(GenerateContentConfig.class)))
+ .willThrow(new RuntimeException("Non Transient Error"));
+
+ var options = GoogleGenAiImageOptions.builder().model("model").build();
+ // Assert that a RuntimeException is thrown and not retried
+ assertThatThrownBy(
+ () -> this.imageModel.call(new ImagePrompt("A light cream colored mini golden doodle", options)))
+ .isInstanceOf(RuntimeException.class);
+
+ // Verify that generateContent was called only once (no retries for non-transient
+ // errors)
+ verify(this.mockModels, times(1)).generateContent(anyString(), anyString(), any(GenerateContentConfig.class));
+ }
+
+ private static class TestRetryListener implements RetryListener {
+
+ int onErrorRetryCount = 0;
+
+ int onSuccessRetryCount = 0;
+
+ @Override
+ public void beforeRetry(final RetryPolicy retryPolicy, final Retryable> retryable) {
+ // Count each retry attempt
+ this.onErrorRetryCount++;
+ }
+
+ @Override
+ public void onRetrySuccess(final RetryPolicy retryPolicy, final Retryable> retryable, final Object result) {
+ // Count successful retries - we increment when we succeed after a failure
+ this.onSuccessRetryCount++;
+ }
+
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 59776f0b5b..3648972f77 100644
--- a/pom.xml
+++ b/pom.xml
@@ -127,6 +127,7 @@
models/spring-ai-elevenlabs
models/spring-ai-google-genai
models/spring-ai-google-genai-embedding
+ models/spring-ai-google-genai-image
models/spring-ai-mistral-ai
models/spring-ai-ollama
models/spring-ai-openai
@@ -154,6 +155,7 @@
starters/spring-ai-starter-model-elevenlabs
starters/spring-ai-starter-model-google-genai
starters/spring-ai-starter-model-google-genai-embedding
+ starters/spring-ai-starter-model-google-genai-image
starters/spring-ai-starter-model-mistral-ai
starters/spring-ai-starter-model-ollama
starters/spring-ai-starter-model-openai
@@ -264,7 +266,7 @@
2.41.22
24.09
26.83.0
- 1.54.0
+ 1.56.0
0.22.0
3.9.1
0.32.0
diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml
index 98cce8c9af..89b833dd6d 100644
--- a/spring-ai-bom/pom.xml
+++ b/spring-ai-bom/pom.xml
@@ -292,6 +292,12 @@
${project.version}