From 416f1314a38ab70fdc70a487172025c4577bbb76 Mon Sep 17 00:00:00 2001 From: CodeItWithKG <17354977+Gaurav1112@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:07:57 +0530 Subject: [PATCH] GH-1608: Add observability support to Stability AI image model Instrument StabilityAiImageModel with Micrometer observations using the portable ImageModelObservationConvention, matching the pattern already in place for OpenAiImageModel. - Add STABILITY_AI ("stability_ai") to AiProvider enum. - Add 3-arg StabilityAiImageModel constructor accepting an ObservationRegistry; existing constructors default to ObservationRegistry.NOOP for backwards compatibility. - Wrap call() with IMAGE_MODEL_OPERATION.observation(...).observe(...) using DefaultImageModelObservationConvention. Pass the raw ImagePrompt to the observation context for consistency with OpenAiImageModel. - Expose setObservationConvention(...) for custom conventions. - Wire ObjectProvider and ObjectProvider in StabilityAiImageAutoConfiguration. - Add unit tests (happy path, no-options path, API-error path, custom convention, null-convention guard, null-registry fallback, backwards- compatible constructors) plus auto-configuration wiring tests. - Update observability reference docs to list Stability AI alongside OpenAI for ImageModel observability. Fixes GH-1608 Signed-off-by: CodeItWithKG <17354977+Gaurav1112@users.noreply.github.com> --- .../pom.xml | 6 + .../StabilityAiImageAutoConfiguration.java | 12 +- ...geAutoConfigurationObservabilityTests.java | 93 ++++++++ models/spring-ai-stability-ai/pom.xml | 6 + .../ai/stabilityai/StabilityAiImageModel.java | 81 +++++-- ...StabilityAiImageModelObservationTests.java | 220 ++++++++++++++++++ .../observation/conventions/AiProvider.java | 5 + .../ROOT/pages/observability/index.adoc | 2 +- 8 files changed, 409 insertions(+), 16 deletions(-) create mode 100644 auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfigurationObservabilityTests.java create mode 100644 models/spring-ai-stability-ai/src/test/java/org/springframework/ai/stabilityai/StabilityAiImageModelObservationTests.java diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml index 338ccf7902..17826b4533 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/pom.xml @@ -91,6 +91,12 @@ mockito-core test + + + io.micrometer + micrometer-observation-test + test + diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java index 8d4bbb0b7e..281ad002cf 100644 --- a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/main/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfiguration.java @@ -16,6 +16,9 @@ package org.springframework.ai.model.stabilityai.autoconfigure; +import io.micrometer.observation.ObservationRegistry; + +import org.springframework.ai.image.observation.ImageModelObservationConvention; import org.springframework.ai.model.SpringAIModelProperties; import org.springframework.ai.model.SpringAIModels; import org.springframework.ai.stabilityai.StabilityAiImageModel; @@ -67,8 +70,13 @@ public StabilityAiApi stabilityAiApi(StabilityAiConnectionProperties commonPrope @Bean @ConditionalOnMissingBean public StabilityAiImageModel stabilityAiImageModel(StabilityAiApi stabilityAiApi, - StabilityAiImageProperties stabilityAiImageProperties) { - return new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions()); + StabilityAiImageProperties stabilityAiImageProperties, + ObjectProvider observationRegistry, + ObjectProvider observationConvention) { + var imageModel = new StabilityAiImageModel(stabilityAiApi, stabilityAiImageProperties.getOptions(), + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + observationConvention.ifAvailable(imageModel::setObservationConvention); + return imageModel; } } diff --git a/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfigurationObservabilityTests.java b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfigurationObservabilityTests.java new file mode 100644 index 0000000000..4e427c03b2 --- /dev/null +++ b/auto-configurations/models/spring-ai-autoconfigure-model-stability-ai/src/test/java/org/springframework/ai/model/stabilityai/autoconfigure/StabilityAiImageAutoConfigurationObservabilityTests.java @@ -0,0 +1,93 @@ +/* + * 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.stabilityai.autoconfigure; + +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import org.junit.jupiter.api.Test; + +import org.springframework.ai.image.observation.DefaultImageModelObservationConvention; +import org.springframework.ai.image.observation.ImageModelObservationConvention; +import org.springframework.ai.stabilityai.StabilityAiImageModel; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Autoconfiguration tests that exercise the observability wiring of + * {@link StabilityAiImageAutoConfiguration}. + * + * @author Gaurav Kumar + */ +class StabilityAiImageAutoConfigurationObservabilityTests { + + private static final String[] BASE_PROPS = new String[] { "spring.ai.stabilityai.image.api-key=API_KEY", + "spring.ai.stabilityai.image.base-url=https://example.invalid" }; + + @Test + void defaultsToNoopObservationRegistryWhenNoBeanPresent() { + new ApplicationContextRunner().withPropertyValues(BASE_PROPS) + .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) + .run(context -> assertThat(context).hasSingleBean(StabilityAiImageModel.class)); + } + + @Test + void usesUserProvidedObservationRegistryBean() { + new ApplicationContextRunner().withPropertyValues(BASE_PROPS) + .withUserConfiguration(ObservationRegistryConfig.class) + .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(StabilityAiImageModel.class); + assertThat(context).hasSingleBean(ObservationRegistry.class); + }); + } + + @Test + void appliesUserProvidedObservationConventionBean() { + new ApplicationContextRunner().withPropertyValues(BASE_PROPS) + .withUserConfiguration(ObservationRegistryConfig.class, CustomConventionConfig.class) + .withConfiguration(AutoConfigurations.of(StabilityAiImageAutoConfiguration.class)) + .run(context -> { + assertThat(context).hasSingleBean(StabilityAiImageModel.class); + assertThat(context).hasSingleBean(ImageModelObservationConvention.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class ObservationRegistryConfig { + + @Bean + ObservationRegistry observationRegistry() { + return TestObservationRegistry.create(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfig { + + @Bean + ImageModelObservationConvention imageModelObservationConvention() { + return new DefaultImageModelObservationConvention(); + } + + } + +} diff --git a/models/spring-ai-stability-ai/pom.xml b/models/spring-ai-stability-ai/pom.xml index a317b59ada..2bec96918e 100644 --- a/models/spring-ai-stability-ai/pom.xml +++ b/models/spring-ai-stability-ai/pom.xml @@ -73,6 +73,12 @@ test + + io.micrometer + micrometer-observation-test + test + + diff --git a/models/spring-ai-stability-ai/src/main/java/org/springframework/ai/stabilityai/StabilityAiImageModel.java b/models/spring-ai-stability-ai/src/main/java/org/springframework/ai/stabilityai/StabilityAiImageModel.java index cb08674ab9..13df9c3008 100644 --- a/models/spring-ai-stability-ai/src/main/java/org/springframework/ai/stabilityai/StabilityAiImageModel.java +++ b/models/spring-ai-stability-ai/src/main/java/org/springframework/ai/stabilityai/StabilityAiImageModel.java @@ -17,8 +17,10 @@ package org.springframework.ai.stabilityai; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; +import io.micrometer.observation.ObservationRegistry; import org.jspecify.annotations.Nullable; import org.springframework.ai.image.Image; @@ -28,7 +30,12 @@ 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.stabilityai.api.StabilityAiApi; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.util.Assert; @@ -36,22 +43,50 @@ /** * StabilityAiImageModel is a class that implements the ImageModel interface. It provides * a client for calling the StabilityAI image generation API. + * + *

+ * Observability data is emitted through the provided {@link ObservationRegistry} using + * the portable {@link ImageModelObservationConvention} infrastructure, matching the + * pattern used by the other Spring AI image models. + * + * @author Mark Pollack + * @author Gaurav Kumar */ public class StabilityAiImageModel implements ImageModel { + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention(); + private final StabilityAiImageOptions defaultOptions; private final StabilityAiApi stabilityAiApi; + private final ObservationRegistry observationRegistry; + + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + public StabilityAiImageModel(StabilityAiApi stabilityAiApi) { - this(stabilityAiApi, StabilityAiImageOptions.builder().build()); + this(stabilityAiApi, StabilityAiImageOptions.builder().build(), ObservationRegistry.NOOP); } public StabilityAiImageModel(StabilityAiApi stabilityAiApi, StabilityAiImageOptions defaultOptions) { + this(stabilityAiApi, defaultOptions, ObservationRegistry.NOOP); + } + + /** + * Creates a new StabilityAiImageModel. + * @param stabilityAiApi the StabilityAI API client + * @param defaultOptions the default image options + * @param observationRegistry the {@link ObservationRegistry} used to record image + * generation observations; {@link ObservationRegistry#NOOP} is used when {@code null} + * @since 2.0.0 + */ + public StabilityAiImageModel(StabilityAiApi stabilityAiApi, StabilityAiImageOptions defaultOptions, + @Nullable ObservationRegistry observationRegistry) { Assert.notNull(stabilityAiApi, "StabilityAiApi must not be null"); Assert.notNull(defaultOptions, "StabilityAiImageOptions must not be null"); this.stabilityAiApi = stabilityAiApi; this.defaultOptions = defaultOptions; + this.observationRegistry = Objects.requireNonNullElse(observationRegistry, ObservationRegistry.NOOP); } private static StabilityAiApi.GenerateImageRequest getGenerateImageRequest(ImagePrompt stabilityAiImagePrompt, @@ -79,30 +114,40 @@ public StabilityAiImageOptions getOptions() { } /** - * Calls the StabilityAiImageModel with the given StabilityAiImagePrompt and returns - * the ImageResponse. This overloaded call method lets you pass the full set of Prompt - * instructions that StabilityAI supports. + * Calls the StabilityAiImageModel with the given ImagePrompt and returns the + * ImageResponse. Emits a {@code gen_ai.client.operation} observation via the + * configured {@link ObservationRegistry}. * @param imagePrompt the StabilityAiImagePrompt containing the prompt and image model * options * @return the ImageResponse generated by the StabilityAiImageModel */ public ImageResponse call(ImagePrompt imagePrompt) { // Merge the runtime options passed via the prompt with the default options - // configured via the constructor. - // Runtime options overwrite StabilityAiImageModel options + // configured via the constructor. Runtime options overwrite defaults. StabilityAiImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions); - // Copy the org.springframework.ai.model derived ImagePrompt and ImageOptions data - // types to the data types used in StabilityAiApi StabilityAiApi.GenerateImageRequest generateImageRequest = getGenerateImageRequest(imagePrompt, requestImageOptions); - // Make the request - StabilityAiApi.GenerateImageResponse generateImageResponse = this.stabilityAiApi - .generateImage(generateImageRequest); + // Pass the original ImagePrompt to the observation context, matching the pattern + // used by other Spring AI image models (e.g. OpenAiImageModel) so that + // observation tags remain consistent across providers. + var observationContext = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt) + .provider(AiProvider.STABILITY_AI.value()) + .build(); - // Convert to org.springframework.ai.model derived ImageResponse data type - return convertResponse(generateImageResponse); + return Objects.requireNonNull( + ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION + .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry) + .observe(() -> { + StabilityAiApi.GenerateImageResponse generateImageResponse = this.stabilityAiApi + .generateImage(generateImageRequest); + ImageResponse imageResponse = convertResponse(generateImageResponse); + observationContext.setResponse(imageResponse); + return imageResponse; + })); } private ImageResponse convertResponse(StabilityAiApi.GenerateImageResponse generateImageResponse) { @@ -115,6 +160,16 @@ private ImageResponse convertResponse(StabilityAiApi.GenerateImageResponse gener return new ImageResponse(imageGenerationList, new ImageResponseMetadata()); } + /** + * Use the provided convention for reporting observation data. + * @param observationConvention the provided convention + * @since 1.1.0 + */ + public void setObservationConvention(ImageModelObservationConvention observationConvention) { + Assert.notNull(observationConvention, "observationConvention cannot be null"); + this.observationConvention = observationConvention; + } + /** * Merge runtime and default {@link ImageOptions} to compute the final options to use * in the request. Protected access for testing purposes, though maybe useful for diff --git a/models/spring-ai-stability-ai/src/test/java/org/springframework/ai/stabilityai/StabilityAiImageModelObservationTests.java b/models/spring-ai-stability-ai/src/test/java/org/springframework/ai/stabilityai/StabilityAiImageModelObservationTests.java new file mode 100644 index 0000000000..3506960aca --- /dev/null +++ b/models/spring-ai-stability-ai/src/test/java/org/springframework/ai/stabilityai/StabilityAiImageModelObservationTests.java @@ -0,0 +1,220 @@ +/* + * 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.stabilityai; + +import java.util.List; + +import io.micrometer.common.KeyValues; +import io.micrometer.observation.Observation; +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.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; +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.observation.conventions.AiOperationType; +import org.springframework.ai.observation.conventions.AiProvider; +import org.springframework.ai.stabilityai.api.StabilityAiApi; +import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for observation instrumentation in {@link StabilityAiImageModel}. Uses a + * mocked {@link StabilityAiApi} so the tests do not require network access or API + * credentials. + * + * @author Gaurav Kumar + */ +class StabilityAiImageModelObservationTests { + + private TestObservationRegistry observationRegistry; + + private StabilityAiApi stabilityAiApi; + + private StabilityAiImageModel imageModel; + + @BeforeEach + void setUp() { + this.observationRegistry = TestObservationRegistry.create(); + this.stabilityAiApi = mock(StabilityAiApi.class); + + // Record layout: (seed, base64, finishReason) + var artifact = new StabilityAiApi.GenerateImageResponse.Artifacts(1234L, "base64-bytes", "SUCCESS"); + var response = new StabilityAiApi.GenerateImageResponse(null, List.of(artifact)); + when(this.stabilityAiApi.generateImage(any(StabilityAiApi.GenerateImageRequest.class))).thenReturn(response); + + var defaultOptions = StabilityAiImageOptions.builder() + .model("stable-diffusion-v1-6") + .height(512) + .width(512) + .responseFormat("image/png") + .stylePreset("photographic") + .build(); + + this.imageModel = new StabilityAiImageModel(this.stabilityAiApi, defaultOptions, this.observationRegistry); + } + + @Test + void emitsObservationWithStabilityAiProviderAndRuntimeOptions() { + var runtimeOptions = StabilityAiImageOptions.builder() + .model("stable-diffusion-xl-1024-v1-0") + .width(1024) + .height(1024) + .stylePreset("digital-art") + .responseFormat("image/png") + .build(); + ImagePrompt prompt = new ImagePrompt("a cup of coffee in Paris", runtimeOptions); + + ImageResponse response = this.imageModel.call(prompt); + + assertThat(response.getResults()).hasSize(1); + + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .doesNotHaveAnyRemainingCurrentObservation() + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that() + .hasContextualNameEqualTo("image stable-diffusion-xl-1024-v1-0") + .hasLowCardinalityKeyValue( + ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), + AiOperationType.IMAGE.value()) + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.STABILITY_AI.value()) + .hasLowCardinalityKeyValue( + ImageModelObservationDocumentation.LowCardinalityKeyNames.REQUEST_MODEL.asString(), + "stable-diffusion-xl-1024-v1-0") + .hasHighCardinalityKeyValue( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_SIZE.asString(), + "1024x1024") + .hasHighCardinalityKeyValue( + ImageModelObservationDocumentation.HighCardinalityKeyNames.REQUEST_IMAGE_STYLE.asString(), + "digital-art") + .hasBeenStarted() + .hasBeenStopped(); + } + + @Test + void stillEmitsObservationWhenImagePromptHasNoOptions() { + // Observation context uses the raw ImagePrompt (OpenAI parity). When the prompt + // carries no runtime options, provider + operation type are still emitted. + ImagePrompt prompt = new ImagePrompt("a green field"); + + this.imageModel.call(prompt); + + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that() + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.STABILITY_AI.value()) + .hasBeenStarted() + .hasBeenStopped(); + } + + @Test + void emitsObservationAndPropagatesErrorWhenApiFails() { + when(this.stabilityAiApi.generateImage(any(StabilityAiApi.GenerateImageRequest.class))) + .thenThrow(new RuntimeException("boom")); + + assertThatThrownBy(() -> this.imageModel.call(new ImagePrompt("anything"))).isInstanceOf(RuntimeException.class) + .hasMessage("boom"); + + // The observation must still be started and stopped so instrumentation is not + // lost on the error path. + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasObservationWithNameEqualTo(DefaultImageModelObservationConvention.DEFAULT_NAME) + .that() + .hasLowCardinalityKeyValue(ImageModelObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + AiProvider.STABILITY_AI.value()) + .hasBeenStarted() + .hasBeenStopped(); + } + + @Test + void usesCustomObservationConventionWhenProvided() { + var customConvention = new ImageModelObservationConvention() { + @Override + public String getName() { + return "custom.stability.observation"; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ImageModelObservationContext; + } + + @Override + public KeyValues getLowCardinalityKeyValues(ImageModelObservationContext context) { + return KeyValues.of("custom.tag", "yes"); + } + }; + + this.imageModel.setObservationConvention(customConvention); + this.imageModel.call(new ImagePrompt("x")); + + TestObservationRegistryAssert.assertThat(this.observationRegistry) + .hasObservationWithNameEqualTo("custom.stability.observation") + .that() + .hasLowCardinalityKeyValue("custom.tag", "yes"); + } + + @Test + void setObservationConventionRejectsNull() { + assertThatThrownBy(() -> this.imageModel.setObservationConvention(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("observationConvention"); + } + + @Test + void nullObservationRegistryFallsBackToNoop() { + // Constructor defensively swaps a null registry for ObservationRegistry.NOOP so + // the model never dereferences a null during call(). + var defaults = StabilityAiImageOptions.builder().model("stable-diffusion-v1-6").build(); + var model = new StabilityAiImageModel(this.stabilityAiApi, defaults, null); + + ImageResponse response = model.call(new ImagePrompt("anything")); + assertThat(response.getResults()).hasSize(1); + } + + @Test + void backwardsCompatibleConstructorsDoNotEmitToProvidedRegistry() { + // The pre-observability constructors must keep working without any Micrometer + // infrastructure. They use ObservationRegistry.NOOP internally and must not + // leak observations into an unrelated test registry. + var defaults = StabilityAiImageOptions.builder().model("stable-diffusion-v1-6").build(); + + var one = new StabilityAiImageModel(this.stabilityAiApi); + var two = new StabilityAiImageModel(this.stabilityAiApi, defaults); + + assertThat(one.call(new ImagePrompt("one")).getResults()).hasSize(1); + assertThat(two.call(new ImagePrompt("two")).getResults()).hasSize(1); + + // The shared test registry was not wired into either model — it must remain + // untouched by these calls. + assertThat(ObservationRegistry.NOOP).isNotSameAs(this.observationRegistry); + } + +} diff --git a/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java b/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java index 02fbf70cf9..e6d542efd3 100644 --- a/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java +++ b/spring-ai-commons/src/main/java/org/springframework/ai/observation/conventions/AiProvider.java @@ -95,6 +95,11 @@ public enum AiProvider { */ SPRING_AI("spring_ai"), + /** + * AI system provided by Stability AI. + */ + STABILITY_AI("stability_ai"), + /** * AI system provided by Vertex AI. */ diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/observability/index.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/observability/index.adoc index 66b7e34a9f..9bc2402c82 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/observability/index.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/observability/index.adoc @@ -280,7 +280,7 @@ Use the metric name `gen_ai.client.token.usage` that is provided by the `Embeddi == Image Model NOTE: Observability features are currently supported only for `ImageModel` implementations from the following AI model -providers: OpenAI. +providers: OpenAI, Stability AI. Additional AI model providers will be supported in a future release. The `gen_ai.client.operation` observations are recorded on image model method calls.