diff --git a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageModel.java b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageModel.java index 6e6b22845df..de4b7e26fd7 100644 --- a/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageModel.java +++ b/models/spring-ai-qianfan/src/main/java/org/springframework/ai/qianfan/QianFanImageModel.java @@ -15,6 +15,7 @@ */ package org.springframework.ai.qianfan; +import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.image.Image; @@ -23,10 +24,16 @@ import org.springframework.ai.image.ImageOptions; 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.model.ModelOptionsUtils; +import org.springframework.ai.qianfan.api.QianFanConstants; import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.retry.RetryUtils; import org.springframework.http.ResponseEntity; +import org.springframework.lang.Nullable; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -43,6 +50,8 @@ public class QianFanImageModel implements ImageModel { private final static Logger logger = LoggerFactory.getLogger(QianFanImageModel.class); + private static final ImageModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultImageModelObservationConvention(); + /** * The default options used for the image completion requests. */ @@ -58,6 +67,16 @@ public class QianFanImageModel implements ImageModel { */ private final QianFanImageApi qianFanImageApi; + /** + * Observation registry used for instrumentation. + */ + private final ObservationRegistry observationRegistry; + + /** + * Conventions to use for generating observations. + */ + private ImageModelObservationConvention observationConvention = DEFAULT_OBSERVATION_CONVENTION; + /** * Creates an instance of the QianFanImageModel. * @param qianFanImageApi The QianFanImageApi instance to be used for interacting with @@ -69,48 +88,85 @@ public QianFanImageModel(QianFanImageApi qianFanImageApi) { } /** - * Initializes a new instance of the QianFanImageModel. + * Creates an instance of the QianFanImageModel. + * @param qianFanImageApi The QianFanImageApi instance to be used for interacting with + * the QianFan Image API. + * @param options The QianFanImageOptions to configure the image model. + * @throws IllegalArgumentException if qianFanImageApi is null + */ + public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options) { + this(qianFanImageApi, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); + } + + /** + * Creates an instance of the QianFanImageModel. * @param qianFanImageApi The QianFanImageApi instance to be used for interacting with * the QianFan Image API. * @param options The QianFanImageOptions to configure the image model. * @param retryTemplate The retry template. + * @throws IllegalArgumentException if qianFanImageApi is null */ public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options, RetryTemplate retryTemplate) { + this(qianFanImageApi, options, retryTemplate, ObservationRegistry.NOOP); + } + + /** + * Initializes a new instance of the QianFanImageModel. + * @param qianFanImageApi The QianFanImageApi instance to be used for interacting with + * the QianFan Image API. + * @param options The QianFanImageOptions to configure the image model. + * @param retryTemplate The retry template. + * @param observationRegistry The ObservationRegistry used for instrumentation. + */ + public QianFanImageModel(QianFanImageApi qianFanImageApi, QianFanImageOptions options, RetryTemplate retryTemplate, + ObservationRegistry observationRegistry) { Assert.notNull(qianFanImageApi, "QianFanImageApi must not be null"); Assert.notNull(options, "options must not be null"); Assert.notNull(retryTemplate, "retryTemplate must not be null"); + Assert.notNull(observationRegistry, "observationRegistry must not be null"); this.qianFanImageApi = qianFanImageApi; this.defaultOptions = options; this.retryTemplate = retryTemplate; + this.observationRegistry = observationRegistry; } @Override public ImageResponse call(ImagePrompt imagePrompt) { - return this.retryTemplate.execute(ctx -> { + QianFanImageOptions requestImageOptions = mergeOptions(imagePrompt.getOptions(), this.defaultOptions); - String instructions = imagePrompt.getInstructions().get(0).getText(); + QianFanImageApi.QianFanImageRequest imageRequest = createRequest(imagePrompt, requestImageOptions); - QianFanImageApi.QianFanImageRequest imageRequest = new QianFanImageApi.QianFanImageRequest(instructions, - QianFanImageApi.DEFAULT_IMAGE_MODEL); + var observationContext = ImageModelObservationContext.builder() + .imagePrompt(imagePrompt) + .provider(QianFanConstants.PROVIDER_NAME) + .requestOptions(requestImageOptions) + .build(); - if (this.defaultOptions != null) { - imageRequest = ModelOptionsUtils.merge(this.defaultOptions, imageRequest, - QianFanImageApi.QianFanImageRequest.class); - } + return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION + .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, + this.observationRegistry) + .observe(() -> { - if (imagePrompt.getOptions() != null) { - imageRequest = ModelOptionsUtils.merge(toQianFanImageOptions(imagePrompt.getOptions()), imageRequest, - QianFanImageApi.QianFanImageRequest.class); - } + ResponseEntity imageResponseEntity = this.retryTemplate + .execute(ctx -> this.qianFanImageApi.createImage(imageRequest)); - // Make the request - ResponseEntity imageResponseEntity = this.qianFanImageApi - .createImage(imageRequest); + ImageResponse imageResponse = convertResponse(imageResponseEntity, imageRequest); - // Convert to org.springframework.ai.model derived ImageResponse data type - return convertResponse(imageResponseEntity, imageRequest); - }); + observationContext.setResponse(imageResponse); + + return imageResponse; + }); + } + + private QianFanImageApi.QianFanImageRequest createRequest(ImagePrompt imagePrompt, + QianFanImageOptions requestImageOptions) { + String instructions = imagePrompt.getInstructions().get(0).getText(); + + QianFanImageApi.QianFanImageRequest imageRequest = new QianFanImageApi.QianFanImageRequest(instructions, + QianFanImageApi.DEFAULT_IMAGE_MODEL); + + return ModelOptionsUtils.merge(requestImageOptions, imageRequest, QianFanImageApi.QianFanImageRequest.class); } private ImageResponse convertResponse(ResponseEntity imageResponseEntity, @@ -132,33 +188,32 @@ private ImageResponse convertResponse(ResponseEntity observationRegistry, + ObjectProvider observationConvention) { String apiKey = StringUtils.hasText(imageProperties.getApiKey()) ? imageProperties.getApiKey() : commonProperties.getApiKey(); @@ -116,7 +118,12 @@ public QianFanImageModel qianFanImageModel(QianFanConnectionProperties commonPro var qianFanImageApi = new QianFanImageApi(baseUrl, apiKey, secretKey, restClientBuilder, responseErrorHandler); - return new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate); + var imageModel = new QianFanImageModel(qianFanImageApi, imageProperties.getOptions(), retryTemplate, + observationRegistry.getIfUnique(() -> ObservationRegistry.NOOP)); + + observationConvention.ifAvailable(imageModel::setObservationConvention); + + return imageModel; } private QianFanApi qianFanApi(String baseUrl, String commonBaseUrl, String apiKey, String commonApiKey,